From 3879298f0f7938135135fb6769f26a21a05feeeb Mon Sep 17 00:00:00 2001
From: Apertis CI <devel@lists.apertis.org>
Date: Sat, 6 Mar 2021 05:22:14 +0000
Subject: [PATCH] Import Upstream version 4.1.48

---
 .github/CONTRIBUTING.md                       |    2 +-
 .gitignore                                    |    8 +
 .lgtm.yml                                     |   13 +
 CONTRIBUTING.md                               |    2 +-
 NOTICE.txt                                    |   31 +-
 README.md                                     |   10 +-
 all/pom.xml                                   |   83 +-
 bom/pom.xml                                   |   76 +-
 buffer/pom.xml                                |    2 +-
 .../java/io/netty/buffer/AbstractByteBuf.java |  183 +-
 .../buffer/AbstractByteBufAllocator.java      |   16 +-
 .../netty/buffer/AbstractDerivedByteBuf.java  |   10 +
 .../buffer/AbstractPooledDerivedByteBuf.java  |    9 +-
 .../AbstractReferenceCountedByteBuf.java      |  143 +-
 .../AdvancedLeakAwareCompositeByteBuf.java    |    6 +
 .../main/java/io/netty/buffer/ByteBuf.java    |   40 +-
 .../io/netty/buffer/ByteBufInputStream.java   |   21 +-
 .../io/netty/buffer/ByteBufOutputStream.java  |    6 +-
 .../java/io/netty/buffer/ByteBufUtil.java     |  131 +-
 .../io/netty/buffer/CompositeByteBuf.java     |  419 +-
 .../io/netty/buffer/DefaultByteBufHolder.java |   21 +-
 .../java/io/netty/buffer/EmptyByteBuf.java    |   37 +-
 .../netty/buffer/FixedCompositeByteBuf.java   |    2 +-
 .../main/java/io/netty/buffer/PoolArena.java  |   81 +-
 .../java/io/netty/buffer/PoolSubpage.java     |   43 +-
 .../java/io/netty/buffer/PoolThreadCache.java |   63 +-
 .../java/io/netty/buffer/PooledByteBuf.java   |  136 +-
 .../netty/buffer/PooledByteBufAllocator.java  |   74 +-
 .../io/netty/buffer/PooledDirectByteBuf.java  |  174 +-
 .../netty/buffer/PooledDuplicatedByteBuf.java |   12 +-
 .../io/netty/buffer/PooledHeapByteBuf.java    |  115 +-
 .../io/netty/buffer/PooledSlicedByteBuf.java  |   12 +-
 .../buffer/PooledUnsafeDirectByteBuf.java     |  133 +-
 .../netty/buffer/PooledUnsafeHeapByteBuf.java |   12 +-
 .../netty/buffer/ReadOnlyByteBufferBuf.java   |   19 +-
 .../buffer/ReadOnlyUnsafeDirectByteBuf.java   |   23 +-
 .../java/io/netty/buffer/SwappedByteBuf.java  |   26 +-
 .../main/java/io/netty/buffer/Unpooled.java   |   17 +-
 .../netty/buffer/UnpooledDirectByteBuf.java   |  107 +-
 .../io/netty/buffer/UnpooledHeapByteBuf.java  |   50 +-
 .../buffer/UnpooledUnsafeDirectByteBuf.java   |  360 +-
 .../buffer/UnpooledUnsafeHeapByteBuf.java     |    9 +-
 .../UnpooledUnsafeNoCleanerDirectByteBuf.java |   14 +-
 .../io/netty/buffer/UnreleasableByteBuf.java  |    7 +-
 .../java/io/netty/buffer/WrappedByteBuf.java  |   21 +-
 .../netty/buffer/WrappedCompositeByteBuf.java |   15 +-
 .../io.netty/buffer/native-image.properties   |   15 +
 .../io/netty/buffer/AbstractByteBufTest.java  |   48 +-
 .../buffer/AbstractCompositeByteBufTest.java  |  249 +-
 .../buffer/AbstractPooledByteBufTest.java     |   66 +
 .../buffer/AdvancedLeakAwareByteBufTest.java  |   23 +
 .../buffer/BigEndianDirectByteBufTest.java    |    9 +
 .../netty/buffer/ByteBufDerivationTest.java   |    1 -
 .../io/netty/buffer/ByteBufStreamTest.java    |  103 +-
 .../java/io/netty/buffer/ByteBufUtilTest.java |  105 +
 .../buffer/DefaultByteBufHolderTest.java      |   60 +
 .../netty/buffer/DuplicatedByteBufTest.java   |    7 +
 .../io/netty/buffer/EmptyByteBufTest.java     |   14 +
 .../buffer/FixedCompositeByteBufTest.java     |    4 +-
 .../java/io/netty/buffer/PoolArenaTest.java   |   27 +-
 .../buffer/PooledByteBufAllocatorTest.java    |   55 +-
 .../ReadOnlyDirectByteBufferBufTest.java      |   28 +-
 .../SimpleLeakAwareCompositeByteBufTest.java  |   24 +
 .../io/netty/buffer/SlicedByteBufTest.java    |    7 +
 codec-dns/pom.xml                             |    2 +-
 .../handler/codec/dns/AbstractDnsRecord.java  |   19 +-
 .../codec/dns/DatagramDnsQueryEncoder.java    |   44 +-
 .../codec/dns/DatagramDnsResponseDecoder.java |   79 +-
 .../codec/dns/DefaultDnsRecordDecoder.java    |   74 +-
 .../codec/dns/DefaultDnsRecordEncoder.java    |   23 +-
 .../netty/handler/codec/dns/DnsCodecUtil.java |  132 +
 .../handler/codec/dns/DnsQueryEncoder.java    |   75 +
 .../handler/codec/dns/DnsResponseDecoder.java |  105 +
 .../handler/codec/dns/TcpDnsQueryEncoder.java |   64 +
 .../codec/dns/TcpDnsResponseDecoder.java      |   72 +
 .../dns/DefaultDnsRecordDecoderTest.java      |   98 +-
 codec-haproxy/pom.xml                         |    2 +-
 .../handler/codec/haproxy/HAProxyMessage.java |  159 +-
 .../codec/haproxy/HAProxyMessageDecoder.java  |  231 +-
 .../handler/codec/haproxy/HAProxyTLV.java     |    4 +-
 .../haproxy/HAProxyMessageDecoderTest.java    |  100 +-
 codec-http/pom.xml                            |    3 +-
 .../codec/http/CombinedHttpHeaders.java       |    2 +-
 .../handler/codec/http/DefaultCookie.java     |    6 +-
 .../codec/http/DefaultHttpContent.java        |    6 +-
 .../codec/http/DefaultHttpHeaders.java        |   15 +-
 .../codec/http/DefaultHttpMessage.java        |    7 +-
 .../handler/codec/http/DefaultHttpObject.java |    6 +-
 .../codec/http/DefaultHttpRequest.java        |   12 +-
 .../codec/http/DefaultHttpResponse.java       |    7 +-
 .../handler/codec/http/HttpClientCodec.java   |   94 +-
 .../codec/http/HttpClientUpgradeHandler.java  |   11 +-
 .../codec/http/HttpContentCompressor.java     |    8 +-
 .../codec/http/HttpContentDecoder.java        |  173 +-
 .../codec/http/HttpContentEncoder.java        |   52 +-
 .../netty/handler/codec/http/HttpHeaders.java |    5 +-
 .../netty/handler/codec/http/HttpMessage.java |    2 +-
 .../netty/handler/codec/http/HttpMethod.java  |    6 +
 .../codec/http/HttpObjectAggregator.java      |    7 -
 .../handler/codec/http/HttpObjectDecoder.java |  297 +-
 .../codec/http/HttpResponseStatus.java        |   14 +-
 .../handler/codec/http/HttpServerCodec.java   |   11 +-
 .../codec/http/HttpServerUpgradeHandler.java  |   21 +-
 .../io/netty/handler/codec/http/HttpUtil.java |   44 +-
 .../netty/handler/codec/http/HttpVersion.java |   23 +-
 .../codec/http/QueryStringDecoder.java        |   76 +-
 .../codec/http/QueryStringEncoder.java        |  178 +-
 .../http/cookie/ClientCookieDecoder.java      |    9 +-
 .../http/cookie/ClientCookieEncoder.java      |   12 +-
 .../codec/http/cookie/CookieHeaderNames.java  |   29 +
 .../handler/codec/http/cookie/CookieUtil.java |   26 +-
 .../codec/http/cookie/DefaultCookie.java      |   29 +-
 .../http/cookie/ServerCookieDecoder.java      |   32 +-
 .../http/cookie/ServerCookieEncoder.java      |   24 +-
 .../handler/codec/http/cors/CorsConfig.java   |    2 +-
 .../codec/http/cors/CorsConfigBuilder.java    |    2 +-
 .../handler/codec/http/cors/CorsHandler.java  |    5 +-
 .../http/multipart/AbstractDiskHttpData.java  |   72 +-
 .../http/multipart/AbstractHttpData.java      |   14 +-
 .../multipart/AbstractMemoryHttpData.java     |   41 +-
 .../codec/http/multipart/DiskAttribute.java   |    5 +-
 .../codec/http/multipart/DiskFileUpload.java  |   11 +-
 .../HttpPostMultipartRequestDecoder.java      |   12 +-
 .../multipart/HttpPostRequestDecoder.java     |   22 +-
 .../multipart/HttpPostRequestEncoder.java     |   15 +-
 .../HttpPostStandardRequestDecoder.java       |   24 +-
 .../http/multipart/InternalAttribute.java     |   13 +-
 .../codec/http/multipart/MemoryAttribute.java |    5 +-
 .../http/multipart/MemoryFileUpload.java      |   11 +-
 .../http/websocketx/BinaryWebSocketFrame.java |    2 +-
 .../http/websocketx/CloseWebSocketFrame.java  |   41 +-
 .../ContinuationWebSocketFrame.java           |    8 +-
 .../CorruptedWebSocketFrameException.java     |   64 +
 .../http/websocketx/PingWebSocketFrame.java   |    4 +-
 .../http/websocketx/PongWebSocketFrame.java   |    2 +-
 .../http/websocketx/TextWebSocketFrame.java   |   10 +-
 .../http/websocketx/Utf8FrameValidator.java   |   65 +-
 .../codec/http/websocketx/Utf8Validator.java  |    9 +-
 .../websocketx/WebSocket00FrameDecoder.java   |   16 +-
 .../websocketx/WebSocket07FrameDecoder.java   |   25 +-
 .../websocketx/WebSocket08FrameDecoder.java   |  484 +-
 .../websocketx/WebSocket08FrameEncoder.java   |    4 +-
 .../websocketx/WebSocket13FrameDecoder.java   |   19 +-
 .../websocketx/WebSocketClientHandshaker.java |  187 +-
 .../WebSocketClientHandshaker00.java          |   73 +-
 .../WebSocketClientHandshaker07.java          |  106 +-
 .../WebSocketClientHandshaker08.java          |  111 +-
 .../WebSocketClientHandshaker13.java          |  112 +-
 .../WebSocketClientHandshakerFactory.java     |  101 +-
 .../WebSocketClientProtocolConfig.java        |  392 ++
 .../WebSocketClientProtocolHandler.java       |  202 +-
 ...bSocketClientProtocolHandshakeHandler.java |   66 +
 .../http/websocketx/WebSocketCloseStatus.java |  314 ++
 .../websocketx/WebSocketDecoderConfig.java    |  165 +
 .../codec/http/websocketx/WebSocketFrame.java |    2 +-
 .../websocketx/WebSocketProtocolHandler.java  |  119 +-
 .../websocketx/WebSocketServerHandshaker.java |   59 +-
 .../WebSocketServerHandshaker00.java          |   38 +-
 .../WebSocketServerHandshaker07.java          |   36 +-
 .../WebSocketServerHandshaker08.java          |   43 +-
 .../WebSocketServerHandshaker13.java          |   49 +-
 .../WebSocketServerHandshakerFactory.java     |   43 +-
 .../WebSocketServerProtocolConfig.java        |  296 +
 .../WebSocketServerProtocolHandler.java       |  148 +-
 ...bSocketServerProtocolHandshakeHandler.java |   93 +-
 .../codec/http/websocketx/WebSocketUtil.java  |    5 +
 .../http/websocketx/WebSocketVersion.java     |   38 +-
 .../WebSocketClientExtensionHandler.java      |    5 +-
 .../extensions/WebSocketExtensionData.java    |   13 +-
 .../extensions/WebSocketExtensionFilter.java  |   54 +
 .../WebSocketExtensionFilterProvider.java     |   45 +
 .../WebSocketServerExtensionHandler.java      |   70 +-
 .../compression/DeflateDecoder.java           |  107 +-
 .../compression/DeflateEncoder.java           |   85 +-
 ...DeflateFrameClientExtensionHandshaker.java |   32 +-
 ...DeflateFrameServerExtensionHandshaker.java |   30 +-
 .../compression/PerFrameDeflateDecoder.java   |   31 +-
 .../compression/PerFrameDeflateEncoder.java   |   39 +-
 ...ssageDeflateClientExtensionHandshaker.java |   51 +-
 .../compression/PerMessageDeflateDecoder.java |   40 +-
 .../compression/PerMessageDeflateEncoder.java |   42 +-
 ...ssageDeflateServerExtensionHandshaker.java |   50 +-
 .../codec/http/websocketx/package-info.java   |   10 +-
 .../netty/handler/codec/rtsp/RtspMethods.java |   27 +-
 .../handler/codec/rtsp/RtspVersions.java      |    5 +-
 .../codec/spdy/DefaultSpdyDataFrame.java      |    7 +-
 .../codec/spdy/DefaultSpdyGoAwayFrame.java    |    7 +-
 .../codec/spdy/DefaultSpdyStreamFrame.java    |    7 +-
 .../codec/spdy/DefaultSpdySynReplyFrame.java  |    3 +-
 .../codec/spdy/DefaultSpdySynStreamFrame.java |    8 +-
 .../spdy/DefaultSpdyWindowUpdateFrame.java    |   14 +-
 .../handler/codec/spdy/SpdyCodecUtil.java     |    9 +-
 .../handler/codec/spdy/SpdyFrameDecoder.java  |   18 +-
 .../handler/codec/spdy/SpdyFrameEncoder.java  |    6 +-
 .../codec/spdy/SpdyHeaderBlockRawDecoder.java |   15 +-
 .../codec/spdy/SpdyHeaderBlockRawEncoder.java |    6 +-
 .../spdy/SpdyHeaderBlockZlibEncoder.java      |   10 +-
 .../handler/codec/spdy/SpdyHttpDecoder.java   |   17 +-
 .../handler/codec/spdy/SpdyHttpEncoder.java   |    5 +-
 .../spdy/SpdyHttpResponseStreamIdHandler.java |    4 +-
 .../codec/spdy/SpdyProtocolException.java     |   17 +
 .../codec/spdy/SpdySessionHandler.java        |   29 +-
 .../handler/codec/spdy/SpdySessionStatus.java |    8 +-
 .../handler/codec/spdy/SpdyStreamStatus.java  |    8 +-
 .../codec-http/native-image.properties        |   16 +
 .../codec/http/CombinedHttpHeadersTest.java   |   16 +-
 .../codec/http/HttpClientCodecTest.java       |   87 +
 .../codec/http/HttpContentCompressorTest.java |  160 +-
 .../codec/http/HttpContentDecoderTest.java    |   10 +-
 .../http/HttpContentDecompressorTest.java     |   70 +
 .../codec/http/HttpContentEncoderTest.java    |    4 +-
 .../codec/http/HttpObjectAggregatorTest.java  |  214 +-
 .../codec/http/HttpRequestDecoderTest.java    |  174 +-
 .../codec/http/HttpResponseDecoderTest.java   |   76 +-
 .../handler/codec/http/HttpUtilTest.java      |   31 +
 .../codec/http/QueryStringDecoderTest.java    |   17 +-
 .../codec/http/QueryStringEncoderTest.java    |   11 +-
 .../http/cookie/ClientCookieDecoderTest.java  |   10 +-
 .../http/cookie/ClientCookieEncoderTest.java  |   12 +
 .../http/cookie/ServerCookieDecoderTest.java  |   21 +
 .../http/cookie/ServerCookieEncoderTest.java  |    8 +-
 .../codec/http/cors/CorsHandlerTest.java      |   31 +-
 .../multipart/HttpPostRequestDecoderTest.java |  147 +
 .../multipart/HttpPostRequestEncoderTest.java |   38 +-
 .../WebSocket08EncoderDecoderTest.java        |  102 +-
 .../WebSocketClientHandshaker00Test.java      |    9 +-
 .../WebSocketClientHandshaker07Test.java      |   31 +-
 .../WebSocketClientHandshaker08Test.java      |    7 +-
 .../WebSocketClientHandshaker13Test.java      |   15 +-
 .../WebSocketClientHandshakerTest.java        |   78 +-
 .../websocketx/WebSocketCloseStatusTest.java  |  127 +
 .../WebSocketHandshakeHandOverTest.java       |  185 +-
 .../WebSocketProtocolHandlerTest.java         |   64 +-
 .../websocketx/WebSocketRequestBuilder.java   |    6 +-
 .../WebSocketServerHandshaker00Test.java      |   32 +-
 .../WebSocketServerHandshaker13Test.java      |   65 +-
 .../WebSocketServerProtocolHandlerTest.java   |  236 +-
 .../WebSocketUtf8FrameValidatorTest.java      |    4 +-
 .../WebSocketExtensionFilterProviderTest.java |   32 +
 .../WebSocketExtensionFilterTest.java         |   87 +
 .../WebSocketExtensionTestUtil.java           |    2 +-
 .../WebSocketServerExtensionHandlerTest.java  |   47 +-
 .../PerFrameDeflateDecoderTest.java           |   58 +-
 .../PerFrameDeflateEncoderTest.java           |   69 +-
 .../PerMessageDeflateDecoderTest.java         |  241 +-
 .../PerMessageDeflateEncoderTest.java         |  202 +-
 .../handler/codec/rtsp/RtspDecoderTest.java   |    1 -
 codec-http/src/test/resources/file-03.txt     |    1 +
 codec-http2/pom.xml                           |    2 +-
 ...AbstractHttp2ConnectionHandlerBuilder.java |  161 +-
 .../http2/AbstractHttp2StreamChannel.java     | 1106 ++++
 .../CleartextHttp2ServerUpgradeHandler.java   |   42 +-
 .../CompressorHttp2ConnectionEncoder.java     |   13 +-
 .../DecoratingHttp2ConnectionEncoder.java     |   13 +-
 .../codec/http2/DefaultHttp2Connection.java   |    2 +-
 .../http2/DefaultHttp2ConnectionDecoder.java  |   89 +-
 .../http2/DefaultHttp2ConnectionEncoder.java  |   95 +-
 .../codec/http2/DefaultHttp2FrameWriter.java  |   46 +-
 .../codec/http2/DefaultHttp2GoAwayFrame.java  |    6 +-
 .../http2/DefaultHttp2HeadersDecoder.java     |    9 +-
 .../http2/DefaultHttp2HeadersEncoder.java     |    8 +-
 .../DefaultHttp2LocalFlowController.java      |   31 +-
 .../codec/http2/DefaultHttp2PingFrame.java    |    5 +-
 .../DefaultHttp2RemoteFlowController.java     |    5 +-
 .../http2/DefaultHttp2SettingsAckFrame.java   |   33 +
 .../http2/DefaultHttp2SettingsFrame.java      |   14 +
 .../codec/http2/DefaultHttp2UnknownFrame.java |   22 +-
 .../DelegatingDecompressorFrameListener.java  |    7 +-
 .../handler/codec/http2/HpackDecoder.java     |   39 +-
 .../handler/codec/http2/HpackEncoder.java     |   35 +-
 .../handler/codec/http2/HpackHeaderField.java |   20 +-
 .../codec/http2/HpackHuffmanDecoder.java      | 4848 ++++++++++++++++-
 .../handler/codec/http2/HpackStaticTable.java |    8 +-
 .../netty/handler/codec/http2/HpackUtil.java  |   10 +
 .../codec/http2/Http2ClientUpgradeCodec.java  |   56 +-
 .../handler/codec/http2/Http2CodecUtil.java   |    7 +-
 .../codec/http2/Http2ConnectionHandler.java   |  117 +-
 .../http2/Http2ConnectionHandlerBuilder.java  |    8 +-
 .../http2/Http2ControlFrameLimitEncoder.java  |  113 +
 .../Http2EmptyDataFrameConnectionDecoder.java |   56 +
 .../http2/Http2EmptyDataFrameListener.java    |   65 +
 .../handler/codec/http2/Http2Exception.java   |   28 +-
 .../handler/codec/http2/Http2FrameCodec.java  |  175 +-
 .../codec/http2/Http2FrameCodecBuilder.java   |   58 +-
 .../codec/http2/Http2FrameListener.java       |    2 +-
 .../handler/codec/http2/Http2FrameStream.java |    2 +-
 .../http2/Http2FrameStreamException.java      |    2 +-
 .../handler/codec/http2/Http2Headers.java     |    2 +-
 .../codec/http2/Http2HeadersEncoder.java      |    7 +-
 .../codec/http2/Http2MultiplexCodec.java      | 1169 +---
 .../http2/Http2MultiplexCodecBuilder.java     |   78 +-
 .../codec/http2/Http2MultiplexHandler.java    |  365 ++
 .../http2/Http2PromisedRequestVerifier.java   |    4 +-
 .../codec/http2/Http2SecurityUtil.java        |    2 +-
 .../codec/http2/Http2ServerUpgradeCodec.java  |   20 +-
 .../codec/http2/Http2SettingsAckFrame.java    |   29 +
 .../http2/Http2SettingsReceivedConsumer.java  |   25 +
 .../http2/Http2StreamChannelBootstrap.java    |  138 +-
 .../codec/http2/HttpConversionUtil.java       |   83 +-
 .../http2/HttpToHttp2ConnectionHandler.java   |    9 +-
 .../HttpToHttp2ConnectionHandlerBuilder.java  |    9 +-
 .../http2/InboundHttp2ToHttpAdapter.java      |   15 +-
 .../handler/codec/http2/MaxCapacityQueue.java |  129 +
 .../http2/UniformStreamByteDistributor.java   |    5 +-
 .../WeightedFairQueueByteDistributor.java     |   11 +-
 .../codec-http2/native-image.properties       |   16 +
 ...leartextHttp2ServerUpgradeHandlerTest.java |  114 +-
 .../DecoratingHttp2ConnectionEncoderTest.java |   45 +
 .../DefaultHttp2ConnectionDecoderTest.java    |   34 +-
 .../DefaultHttp2ConnectionEncoderTest.java    |  115 +-
 .../http2/DefaultHttp2ConnectionTest.java     |    4 +-
 .../http2/DefaultHttp2FrameWriterTest.java    |   67 +-
 .../DefaultHttp2LocalFlowControllerTest.java  |   66 +-
 .../handler/codec/http2/HpackDecoderTest.java |    2 +-
 .../handler/codec/http2/HpackEncoderTest.java |    2 +-
 .../handler/codec/http2/HpackHuffmanTest.java |   38 +-
 .../netty/handler/codec/http2/HpackTest.java  |    5 +-
 .../handler/codec/http2/HpackTestCase.java    |   20 +-
 .../http2/Http2ClientUpgradeCodecTest.java    |   32 +-
 .../http2/Http2ConnectionHandlerTest.java     |   80 +-
 .../http2/Http2ConnectionRoundtripTest.java   |   92 +-
 .../Http2ControlFrameLimitEncoderTest.java    |  277 +
 .../codec/http2/Http2DefaultFramesTest.java   |   44 +
 ...p2EmptyDataFrameConnectionDecoderTest.java |   60 +
 .../Http2EmptyDataFrameListenerTest.java      |  142 +
 .../codec/http2/Http2FrameCodecTest.java      |   91 +-
 .../Http2MultiplexClientUpgradeTest.java      |   89 +
 .../Http2MultiplexCodecClientUpgradeTest.java |   67 +-
 .../codec/http2/Http2MultiplexCodecTest.java  |  995 +---
 ...ttp2MultiplexHandlerClientUpgradeTest.java |   30 +
 .../http2/Http2MultiplexHandlerTest.java      |   43 +
 .../codec/http2/Http2MultiplexTest.java       | 1230 +++++
 .../http2/Http2MultiplexTransportTest.java    |  257 +
 .../http2/Http2ServerUpgradeCodecTest.java    |   39 +-
 .../codec/http2/Http2StreamChannelIdTest.java |   60 +
 .../handler/codec/http2/Http2TestUtil.java    |    8 +-
 .../codec/http2/HttpConversionUtilTest.java   |   46 +
 .../codec/http2/LastInboundHandler.java       |    2 +-
 codec-memcache/pom.xml                        |    2 +-
 .../memcache/AbstractMemcacheObject.java      |    7 +-
 .../memcache/DefaultMemcacheContent.java      |    6 +-
 .../binary/AbstractBinaryMemcacheDecoder.java |    6 +-
 .../binary/AbstractBinaryMemcacheMessage.java |   18 +-
 .../binary/DefaultBinaryMemcacheRequest.java  |   10 +
 .../binary/DefaultBinaryMemcacheResponse.java |   12 +-
 .../DefaultFullBinaryMemcacheRequest.java     |   19 +-
 .../DefaultFullBinaryMemcacheResponse.java    |   19 +-
 .../DefaultFullBinaryMemcacheRequestTest.java |   99 +
 ...DefaultFullBinaryMemcacheResponseTest.java |   98 +
 codec-mqtt/pom.xml                            |    2 +-
 .../codec/mqtt/MqttConnectPayload.java        |    6 +-
 .../netty/handler/codec/mqtt/MqttMessage.java |   11 +
 .../handler/codec/mqtt/MqttSubAckPayload.java |    9 +-
 .../codec/mqtt/MqttSubscribePayload.java      |    9 +-
 .../codec/mqtt/MqttUnsubscribePayload.java    |    9 +-
 .../handler/codec/mqtt/MqttCodecTest.java     |    9 +-
 .../codec/mqtt/MqttConnectPayloadTest.java    |    9 +
 codec-redis/pom.xml                           |    2 +-
 .../handler/codec/redis/RedisDecoderTest.java |    8 +
 codec-smtp/pom.xml                            |    2 +-
 .../netty/handler/codec/smtp/SmtpCommand.java |    4 +
 .../codec/smtp/SmtpRequestEncoder.java        |    9 +-
 .../handler/codec/smtp/SmtpRequests.java      |   23 +-
 .../codec/smtp/SmtpRequestEncoderTest.java    |   27 +-
 codec-socks/pom.xml                           |    2 +-
 .../handler/codec/socks/SocksAuthRequest.java |   11 +-
 .../codec/socks/SocksAuthResponse.java        |    6 +-
 .../handler/codec/socks/SocksCmdRequest.java  |   14 +-
 .../handler/codec/socks/SocksCmdResponse.java |    9 +-
 .../handler/codec/socks/SocksInitRequest.java |    6 +-
 .../codec/socks/SocksInitResponse.java        |    6 +-
 .../handler/codec/socks/SocksMessage.java     |    6 +-
 .../handler/codec/socks/SocksRequest.java     |    7 +-
 .../handler/codec/socks/SocksResponse.java    |    7 +-
 .../codec/socksx/AbstractSocksMessage.java    |    6 +-
 .../SocksPortUnificationServerHandler.java    |    7 +-
 .../v4/DefaultSocks4CommandRequest.java       |   18 +-
 .../v4/DefaultSocks4CommandResponse.java      |    6 +-
 .../codec/socksx/v4/Socks4CommandStatus.java  |    8 +-
 .../codec/socksx/v4/Socks4CommandType.java    |    7 +-
 .../v5/DefaultSocks5CommandRequest.java       |   14 +-
 .../v5/DefaultSocks5CommandResponse.java      |   10 +-
 .../v5/DefaultSocks5InitialRequest.java       |    9 +-
 .../v5/DefaultSocks5InitialResponse.java      |    6 +-
 .../v5/DefaultSocks5PasswordAuthRequest.java  |    9 +-
 .../v5/DefaultSocks5PasswordAuthResponse.java |    7 +-
 .../codec/socksx/v5/Socks5AddressType.java    |    8 +-
 .../codec/socksx/v5/Socks5AuthMethod.java     |    8 +-
 .../codec/socksx/v5/Socks5ClientEncoder.java  |    7 +-
 .../v5/Socks5CommandRequestDecoder.java       |    7 +-
 .../v5/Socks5CommandResponseDecoder.java      |    7 +-
 .../codec/socksx/v5/Socks5CommandStatus.java  |    8 +-
 .../codec/socksx/v5/Socks5CommandType.java    |    8 +-
 .../v5/Socks5InitialRequestDecoder.java       |    3 -
 .../socksx/v5/Socks5PasswordAuthStatus.java   |    8 +-
 .../codec/socksx/v5/Socks5ServerEncoder.java  |    7 +-
 .../v5/Socks5InitialRequestDecoderTest.java   |   38 +
 codec-stomp/pom.xml                           |    2 +-
 .../codec/stomp/DefaultStompFrame.java        |    8 +-
 .../stomp/DefaultStompHeadersSubframe.java    |    7 +-
 .../codec/stomp/StompSubframeDecoder.java     |  279 +-
 .../codec/stomp/StompSubframeEncoder.java     |   18 +-
 .../codec/stomp/StompSubframeDecoderTest.java |   49 +-
 .../codec/stomp/StompSubframeEncoderTest.java |   20 +
 .../codec/stomp/StompTestConstants.java       |   13 +
 codec-xml/pom.xml                             |    2 +-
 .../netty/handler/codec/xml/XmlAttribute.java |   28 +-
 .../netty/handler/codec/xml/XmlContent.java   |   12 +-
 .../io/netty/handler/codec/xml/XmlDTD.java    |   12 +-
 .../netty/handler/codec/xml/XmlDecoder.java   |    2 +-
 .../handler/codec/xml/XmlDocumentStart.java   |   20 +-
 .../netty/handler/codec/xml/XmlElement.java   |   24 +-
 .../handler/codec/xml/XmlElementStart.java    |   16 +-
 .../handler/codec/xml/XmlEntityReference.java |   16 +-
 .../netty/handler/codec/xml/XmlNamespace.java |   16 +-
 .../codec/xml/XmlProcessingInstruction.java   |   16 +-
 .../handler/codec/xml/XmlDecoderTest.java     |   22 +-
 codec/pom.xml                                 |    2 +-
 .../handler/codec/AsciiHeadersEncoder.java    |   17 +-
 .../handler/codec/ByteToMessageDecoder.java   |  153 +-
 .../handler/codec/DatagramPacketEncoder.java  |    2 +-
 .../io/netty/handler/codec/DateFormatter.java |   32 +-
 .../io/netty/handler/codec/DecoderResult.java |   11 +-
 .../netty/handler/codec/DefaultHeaders.java   |   29 +-
 .../codec/DelimiterBasedFrameDecoder.java     |   20 +-
 .../codec/FixedLengthFrameDecoder.java        |    7 +-
 .../codec/LengthFieldBasedFrameDecoder.java   |   39 +-
 .../handler/codec/LengthFieldPrepender.java   |   11 +-
 .../handler/codec/MessageAggregator.java      |   34 +-
 .../codec/MessageToMessageEncoder.java        |    2 +-
 .../codec/ProtocolDetectionResult.java        |    4 +-
 .../codec/ReplayingDecoderByteBuf.java        |   26 +-
 .../io/netty/handler/codec/base64/Base64.java |   71 +-
 .../handler/codec/base64/Base64Decoder.java   |    6 +-
 .../handler/codec/base64/Base64Dialect.java   |   66 +-
 .../handler/codec/base64/Base64Encoder.java   |    7 +-
 .../codec/compression/ByteBufChecksum.java    |    7 +-
 .../codec/compression/Bzip2DivSufSort.java    |    4 +-
 .../codec/compression/CompressionUtil.java    |    5 +
 .../codec/compression/JZlibDecoder.java       |   66 +-
 .../codec/compression/JZlibEncoder.java       |   10 +-
 .../codec/compression/JdkZlibDecoder.java     |   76 +-
 .../codec/compression/JdkZlibEncoder.java     |   25 +-
 .../codec/compression/Lz4FrameDecoder.java    |   11 +-
 .../codec/compression/Lz4FrameEncoder.java    |   18 +-
 .../codec/compression/Lz4XXHash32.java        |  107 +
 .../handler/codec/compression/LzfEncoder.java |  129 +-
 .../handler/codec/compression/Snappy.java     |    6 +-
 .../codec/compression/ZlibDecoder.java        |   65 +
 .../codec/protobuf/ProtobufDecoder.java       |    6 +-
 .../CompatibleObjectEncoder.java              |    2 -
 .../ObjectDecoderInputStream.java             |   11 +-
 .../ObjectEncoderOutputStream.java            |    9 +-
 .../handler/codec/string/StringDecoder.java   |    6 +-
 .../handler/codec/string/StringEncoder.java   |    7 +-
 .../handler/codec/ByteToMessageCodecTest.java |    2 +-
 .../codec/ByteToMessageDecoderTest.java       |  191 +-
 .../codec/DatagramPacketEncoderTest.java      |   11 +-
 .../handler/codec/DateFormatterTest.java      |   23 +
 .../handler/codec/DefaultHeadersTest.java     |   78 +-
 .../handler/codec/base64/Base64Test.java      |   20 +-
 .../compression/ByteBufChecksumTest.java      |   90 +
 .../handler/codec/compression/JZlibTest.java  |    4 +-
 .../codec/compression/JdkZlibTest.java        |    4 +-
 .../LengthAwareLzfIntegrationTest.java        |   28 +
 .../compression/SnappyFrameDecoderTest.java   |   56 +-
 .../compression/SnappyFrameEncoderTest.java   |    7 +-
 .../handler/codec/compression/SnappyTest.java |   14 +-
 .../codec/compression/ZlibCrossTest1.java     |    4 +-
 .../codec/compression/ZlibCrossTest2.java     |    4 +-
 .../handler/codec/compression/ZlibTest.java   |   61 +-
 .../CompatibleObjectEncoderTest.java          |    1 -
 .../io/netty/handler/codec/xml/sample-04.xml  |    8 +-
 common/pom.xml                                |   16 +-
 .../netty/util/AbstractReferenceCounted.java  |  131 +-
 .../main/java/io/netty/util/AsciiString.java  |   68 +-
 .../main/java/io/netty/util/AttributeMap.java |    2 +-
 .../main/java/io/netty/util/CharsetUtil.java  |    4 +-
 .../main/java/io/netty/util/ConstantPool.java |   12 +-
 .../io/netty/util/DefaultAttributeMap.java    |   10 +-
 .../java/io/netty/util/HashedWheelTimer.java  |   34 +-
 .../src/main/java/io/netty/util/NetUtil.java  |    9 +-
 .../src/main/java/io/netty/util/Recycler.java |  192 +-
 .../io/netty/util/ResourceLeakDetector.java   |   40 +-
 .../util/ResourceLeakDetectorFactory.java     |    9 +-
 .../io/netty/util/ThreadDeathWatcher.java     |   25 +-
 .../concurrent/AbstractEventExecutor.java     |   22 +
 .../AbstractScheduledEventExecutor.java       |  143 +-
 .../netty/util/concurrent/CompleteFuture.java |   14 +-
 .../netty/util/concurrent/DefaultPromise.java |  117 +-
 .../util/concurrent/DefaultThreadFactory.java |   10 +-
 .../netty/util/concurrent/FailedFuture.java   |    6 +-
 .../util/concurrent/FastThreadLocal.java      |   45 +-
 .../java/io/netty/util/concurrent/Future.java |    4 +-
 .../util/concurrent/GlobalEventExecutor.java  |   33 +-
 .../concurrent/ImmediateEventExecutor.java    |    5 +-
 .../util/concurrent/ImmediateExecutor.java    |    9 +-
 .../NonStickyEventExecutorGroup.java          |    2 +-
 .../util/concurrent/PromiseAggregator.java    |   13 +-
 .../util/concurrent/PromiseCombiner.java      |   58 +-
 .../io/netty/util/concurrent/PromiseTask.java |   69 +-
 .../util/concurrent/ScheduledFutureTask.java  |  110 +-
 .../concurrent/SingleThreadEventExecutor.java |  231 +-
 .../concurrent/ThreadPerTaskExecutor.java     |    7 +-
 .../UnorderedThreadPoolEventExecutor.java     |    4 +-
 .../util/internal/AppendableCharSequence.java |   13 +
 .../java/io/netty/util/internal/Hidden.java   |  105 +
 .../util/internal/InternalThreadLocalMap.java |    6 +-
 .../netty/util/internal/LongAdderCounter.java |    1 +
 .../util/internal/NativeLibraryLoader.java    |   84 +-
 .../io/netty/util/internal/ObjectPool.java    |   87 +
 .../io/netty/util/internal/PendingWrite.java  |   13 +-
 .../util/internal/PlatformDependent.java      |  249 +-
 .../util/internal/PlatformDependent0.java     |  121 +-
 .../internal/PromiseNotificationUtil.java     |    2 +-
 .../netty/util/internal/ReadOnlyIterator.java |    5 +-
 .../util/internal/RecyclableArrayList.java    |   26 +-
 .../util/internal/ReferenceCountUpdater.java  |  179 +
 .../io/netty/util/internal/SocketUtils.java   |   21 +-
 .../io/netty/util/internal/StringUtil.java    |   92 +-
 .../internal/SuppressJava6Requirement.java    |    2 +-
 .../util/internal/SystemPropertyUtil.java     |    4 +-
 .../util/internal/ThreadExecutorMap.java      |   96 +
 .../util/internal/ThreadLocalRandom.java      |    2 +
 .../logging/AbstractInternalLogger.java       |    6 +-
 .../util/internal/logging/CommonsLogger.java  |    6 +-
 .../logging/InternalLoggerFactory.java        |   15 +-
 .../logging/LocationAwareSlf4JLogger.java     |   10 +-
 .../svm/CleanerJava6Substitution.java         |   33 +
 .../svm/PlatformDependent0Substitution.java   |   33 +
 .../svm/PlatformDependentSubstitution.java    |   39 +
 .../svm/UnsafeRefArrayAccessSubstitution.java |   32 +
 .../netty/util/internal/svm/package-info.java |   21 +
 .../io.netty/common/native-image.properties   |   15 +
 ...ockhound.integration.BlockHoundIntegration |   14 +
 .../util/collection/KObjectHashMap.template   |    2 +-
 .../netty/util/AsciiStringCharacterTest.java  |   27 +-
 .../test/java/io/netty/util/RecyclerTest.java |    1 +
 .../util/concurrent/DefaultPromiseTest.java   |   49 +-
 .../util/concurrent/FastThreadLocalTest.java  |   19 +
 .../concurrent/GlobalEventExecutorTest.java   |   74 +-
 .../concurrent/ImmediateExecutorTest.java     |   47 +
 .../util/concurrent/PromiseCombinerTest.java  |   48 +-
 .../SingleThreadEventExecutorTest.java        |  249 +-
 .../internal/AppendableCharSequenceTest.java  |   10 +
 .../internal/DefaultPriorityQueueTest.java    |    2 -
 .../io/netty/util/internal/MathUtilTest.java  |   88 +
 .../internal/NativeLibraryLoaderTest.java     |   74 +
 .../netty/util/internal/StringUtilTest.java   |   66 +-
 .../util/internal/ThreadExecutorMapTest.java  |   63 +
 .../internal/logging/Log4J2LoggerTest.java    |    2 +-
 .../logging/Slf4JLoggerFactoryTest.java       |   21 +-
 .../collection/KObjectHashMapTest.template    |   30 +
 dev-tools/pom.xml                             |    4 +-
 docker/Dockerfile.centos                      |    3 +
 docker/README.md                              |    4 +-
 docker/docker-compose.centos-6.110.yaml       |    2 +-
 docker/docker-compose.centos-6.111.yaml       |    2 +-
 docker/docker-compose.centos-6.112.yaml       |    2 +-
 docker/docker-compose.centos-6.113.yaml       |    2 +-
 docker/docker-compose.centos-6.18.yaml        |    2 +-
 docker/docker-compose.centos-6.19.yaml        |    2 +-
 docker/docker-compose.centos-6.graalvm1.yaml  |   22 +
 docker/docker-compose.centos-6.openj9111.yaml |   22 +
 docker/docker-compose.centos-7.110.yaml       |    2 +-
 docker/docker-compose.centos-7.111.yaml       |    2 +-
 docker/docker-compose.centos-7.112.yaml       |    2 +-
 docker/docker-compose.centos-7.113.yaml       |    2 +-
 docker/docker-compose.centos-7.18.yaml        |    2 +-
 docker/docker-compose.centos-7.19.yaml        |    2 +-
 docker/docker-compose.yaml                    |   14 +-
 example/pom.xml                               |   10 +-
 .../example/http/cors/OkResponseHandler.java  |    4 +-
 .../file/HttpStaticFileServerHandler.java     |  114 +-
 .../HttpHelloWorldServerHandler.java          |   42 +-
 .../example/http/snoop/HttpSnoopClient.java   |    3 +-
 .../http/snoop/HttpSnoopServerHandler.java    |    2 +-
 .../example/http/upload/HttpUploadServer.java |    2 +-
 .../http/upload/HttpUploadServerHandler.java  |   51 +-
 .../WebSocketServerHandler.java               |   38 +-
 .../server/WebSocketFrameHandler.java         |    4 +-
 .../server/WebSocketIndexPageHandler.java     |   44 +-
 .../websocketx/server/WebSocketServer.java    |    2 +-
 .../http2/helloworld/client/Http2Client.java  |   11 +-
 .../client/Http2ClientInitializer.java        |   19 +-
 .../client/Http2ClientFrameInitializer.java   |   57 +
 ...Http2ClientStreamFrameResponseHandler.java |   61 +
 .../frame/client/Http2FrameClient.java        |  124 +
 .../frame/server/Http2OrHttpHandler.java      |    1 -
 .../helloworld/frame/server/Http2Server.java  |    2 +-
 .../frame/server/Http2ServerInitializer.java  |    3 +-
 .../multiplex/server/Http2OrHttpHandler.java  |    6 +-
 .../multiplex/server/Http2Server.java         |    2 +-
 .../server/Http2ServerInitializer.java        |    9 +-
 .../server/HelloWorldHttp1Handler.java        |   19 +-
 .../http2/helloworld/server/Http2Server.java  |    2 +-
 .../server/Http2ServerInitializer.java        |    3 +-
 .../http2/tiles/FallbackRequestHandler.java   |    3 +-
 .../http2/tiles/Http1RequestHandler.java      |   14 +-
 .../example/http2/tiles/Http2Server.java      |    2 +-
 .../netty/example/http2/tiles/HttpServer.java |    2 +-
 .../mqtt/heartBeat/MqttHeartBeatBroker.java   |   64 +
 .../heartBeat/MqttHeartBeatBrokerHandler.java |   82 +
 .../mqtt/heartBeat/MqttHeartBeatClient.java   |   64 +
 .../heartBeat/MqttHeartBeatClientHandler.java |   83 +
 .../netty/example/ocsp/OcspClientExample.java |   12 +-
 .../netty/example/ocsp/OcspServerExample.java |    4 +-
 .../netty/example/spdy/client/SpdyClient.java |    4 +-
 .../example/spdy/client/SpdyFrameLogger.java  |    9 +-
 .../spdy/server/SpdyServerHandler.java        |   34 +-
 .../worldclock/WorldClockProtocol.java        |  147 +-
 handler-proxy/pom.xml                         |    2 +-
 .../netty/handler/proxy/HttpProxyHandler.java |  175 +-
 .../handler/proxy/ProxyConnectionEvent.java   |   22 +-
 .../io/netty/handler/proxy/ProxyHandler.java  |    6 +-
 .../handler/proxy/HttpProxyHandlerTest.java   |   94 +-
 .../netty/handler/proxy/ProxyHandlerTest.java |    4 +-
 handler/pom.xml                               |   15 +-
 .../address/DynamicAddressConnectHandler.java |   82 +
 .../address/ResolveAddressHandler.java        |   66 +
 .../netty/handler/address/package-info.java   |   20 +
 .../handler/flow/FlowControlHandler.java      |   65 +-
 .../flush/FlushConsolidationHandler.java      |   12 +-
 .../handler/ipfilter/IpSubnetFilterRule.java  |   10 +-
 .../handler/ipfilter/RuleBasedIpFilter.java   |    7 +-
 .../netty/handler/logging/ByteBufFormat.java  |   36 +
 .../netty/handler/logging/LoggingHandler.java |  107 +-
 .../netty/handler/ssl/AbstractSniHandler.java |  352 +-
 ...ApplicationProtocolNegotiationHandler.java |   32 +-
 .../java/io/netty/handler/ssl/Conscrypt.java  |   14 +-
 .../handler/ssl/DelegatingSslContext.java     |   16 +
 .../handler/ssl/ExtendedOpenSslSession.java   |    4 +
 .../handler/ssl/Java7SslParametersUtils.java  |    3 +
 .../io/netty/handler/ssl/Java8SslUtils.java   |    3 +
 .../io/netty/handler/ssl/Java9SslEngine.java  |    2 +
 .../io/netty/handler/ssl/Java9SslUtils.java   |    2 +
 .../JdkAlpnApplicationProtocolNegotiator.java |    4 +
 .../handler/ssl/JdkSslClientContext.java      |   24 +-
 .../io/netty/handler/ssl/JdkSslContext.java   |   84 +-
 .../io/netty/handler/ssl/JdkSslEngine.java    |    3 +
 .../handler/ssl/JdkSslServerContext.java      |   64 +-
 .../handler/ssl/KeyManagerFactoryWrapper.java |   44 +
 .../java/io/netty/handler/ssl/OpenSsl.java    |  187 +-
 .../OpenSslCachingKeyMaterialProvider.java    |   13 +-
 .../OpenSslCachingX509KeyManagerFactory.java  |   21 +
 .../handler/ssl/OpenSslClientContext.java     |   10 +-
 .../ssl/OpenSslJavaxX509Certificate.java      |    2 +-
 .../ssl/OpenSslKeyMaterialManager.java        |   18 +-
 .../ssl/OpenSslKeyMaterialProvider.java       |   56 +-
 .../netty/handler/ssl/OpenSslPrivateKey.java  |   65 +-
 .../handler/ssl/OpenSslPrivateKeyMethod.java  |   62 +
 .../handler/ssl/OpenSslServerContext.java     |   13 +-
 .../handler/ssl/OpenSslSessionContext.java    |    6 +-
 ...OpenSslTlsv13X509ExtendedTrustManager.java |  268 +-
 .../handler/ssl/OpenSslX509Certificate.java   |    5 +-
 .../ssl/OpenSslX509KeyManagerFactory.java     |   62 +-
 .../ssl/OpenSslX509TrustManagerWrapper.java   |    5 +
 .../io/netty/handler/ssl/PemPrivateKey.java   |    6 +
 .../java/io/netty/handler/ssl/PemReader.java  |    2 +-
 .../java/io/netty/handler/ssl/PemValue.java   |    2 +-
 .../handler/ssl/PseudoRandomFunction.java     |   94 +
 .../ReferenceCountedOpenSslClientContext.java |   53 +-
 .../ssl/ReferenceCountedOpenSslContext.java   |  197 +-
 .../ssl/ReferenceCountedOpenSslEngine.java    |  318 +-
 .../ReferenceCountedOpenSslServerContext.java |   49 +-
 .../java/io/netty/handler/ssl/SniHandler.java |   11 +-
 .../handler/ssl/SslClientHelloHandler.java    |  312 ++
 .../java/io/netty/handler/ssl/SslContext.java |  192 +-
 .../netty/handler/ssl/SslContextBuilder.java  |  153 +-
 .../java/io/netty/handler/ssl/SslHandler.java |  497 +-
 .../ssl/SslHandshakeTimeoutException.java     |   28 +
 .../handler/ssl/SslMasterKeyHandler.java      |  188 +
 .../io/netty/handler/ssl/SslProvider.java     |   18 +-
 .../java/io/netty/handler/ssl/SslUtils.java   |    5 +-
 .../ssl/SupportedCipherSuiteFilter.java       |   10 +-
 .../ssl/TrustManagerFactoryWrapper.java       |   44 +
 .../handler/ssl/ocsp/OcspClientHandler.java   |    6 +-
 .../util/FingerprintTrustManagerFactory.java  |   11 +-
 .../util/OpenJdkSelfSignedCertGenerator.java  |    2 +
 .../ssl/util/SelfSignedCertificate.java       |   18 +-
 .../ssl/util/SimpleKeyManagerFactory.java     |  154 +
 .../ssl/util/SimpleTrustManagerFactory.java   |   23 +-
 .../ssl/util/X509KeyManagerWrapper.java       |   78 +
 .../ssl/util/X509TrustManagerWrapper.java     |    3 +
 .../io/netty/handler/stream/ChunkedFile.java  |   23 +-
 .../netty/handler/stream/ChunkedNioFile.java  |   35 +-
 .../handler/stream/ChunkedNioStream.java      |    5 +-
 .../netty/handler/stream/ChunkedStream.java   |   11 +-
 .../handler/stream/ChunkedWriteHandler.java   |  154 +-
 .../netty/handler/timeout/IdleStateEvent.java |   38 +-
 .../handler/timeout/IdleStateHandler.java     |   16 +-
 .../handler/timeout/ReadTimeoutException.java |   11 +-
 .../handler/timeout/TimeoutException.java     |    7 +-
 .../timeout/WriteTimeoutException.java        |   13 +-
 .../handler/timeout/WriteTimeoutHandler.java  |    5 +-
 .../AbstractTrafficShapingHandler.java        |    2 +-
 .../traffic/GlobalChannelTrafficCounter.java  |    4 +-
 .../traffic/GlobalTrafficShapingHandler.java  |   10 +-
 .../netty/handler/traffic/TrafficCounter.java |   17 +-
 .../io.netty/handler/native-image.properties  |   15 +
 .../DynamicAddressConnectHandlerTest.java     |  107 +
 .../address/ResolveAddressHandlerTest.java    |  139 +
 .../handler/flow/FlowControlHandlerTest.java  |  119 +-
 .../flush/FlushConsolidationHandlerTest.java  |   22 +
 .../handler/logging/LoggingHandlerTest.java   |   32 +-
 .../ssl/AmazonCorrettoSslEngineTest.java      |  118 +
 .../handler/ssl/CipherSuiteCanaryTest.java    |   34 +-
 .../ssl/ConscryptJdkSslEngineInteropTest.java |   16 +-
 .../ConscryptOpenSslEngineInteropTest.java    |  158 +
 .../handler/ssl/ConscryptSslEngineTest.java   |   16 +-
 .../ssl/JdkConscryptSslEngineInteropTest.java |   16 +-
 .../ssl/JdkOpenSslEngineInteroptTest.java     |   43 +-
 .../netty/handler/ssl/JdkSslEngineTest.java   |   16 +-
 ...OpenSslCachingKeyMaterialProviderTest.java |   21 +-
 .../OpenSslConscryptSslEngineInteropTest.java |  150 +
 .../netty/handler/ssl/OpenSslEngineTest.java  |  355 +-
 .../ssl/OpenSslJdkSslEngineInteroptTest.java  |   57 +-
 .../ssl/OpenSslKeyMaterialProviderTest.java   |  104 +-
 .../ssl/OpenSslPrivateKeyMethodTest.java      |  402 ++
 .../io/netty/handler/ssl/PemEncodedTest.java  |    1 -
 .../handler/ssl/PseudoRandomFunctionTest.java |   53 +
 .../ReferenceCountedOpenSslEngineTest.java    |   39 +-
 .../io/netty/handler/ssl/SSLEngineTest.java   |  695 ++-
 .../handler/ssl/SniClientJava8TestUtil.java   |    2 +-
 .../io/netty/handler/ssl/SniClientTest.java   |    3 +-
 .../io/netty/handler/ssl/SniHandlerTest.java  |  184 +-
 .../handler/ssl/SslContextBuilderTest.java    |  192 +-
 .../ssl/SslContextTrustManagerTest.java       |    2 +-
 .../io/netty/handler/ssl/SslErrorTest.java    |  191 +-
 .../io/netty/handler/ssl/SslHandlerTest.java  |  364 +-
 .../io/netty/handler/ssl/ocsp/OcspTest.java   |    6 +-
 .../stream/ChunkedWriteHandlerTest.java       |  271 +-
 .../handler/timeout/IdleStateEventTest.java   |   34 +
 .../handler/timeout/IdleStateHandlerTest.java |   52 +-
 license/LICENSE.dnsinfo.txt                   |   22 +
 license/LICENSE.hyper-hpack.txt               |   21 +
 license/LICENSE.jboss-marshalling.txt         |  678 +--
 license/LICENSE.nghttp2-hpack.txt             |   23 +
 microbench/README.md                          |    2 +-
 microbench/pom.xml                            |    6 +-
 ...tractReferenceCountedByteBufBenchmark.java |    6 +-
 .../netty/buffer/ByteBufAccessBenchmark.java  |  165 +
 .../codec/DateFormatter2Benchmark.java        |   95 +
 .../codec/http/DecodeHexBenchmark.java        |  136 +
 .../codec/http/HttpMethodMapBenchmark.java    |   39 +-
 .../http/QueryStringDecoderBenchmark.java     |   66 +
 .../http/QueryStringEncoderBenchmark.java     |   95 +
 .../codec/http2/HpackBenchmarkUtil.java       |    2 +-
 .../codec/http2/HpackDecoderBenchmark.java    |    2 +-
 .../handler/codec/http2/HpackHeader.java      |    7 +-
 .../DefaultChannelPipelineBenchmark.java      |   84 +
 .../EmbeddedChannelHandlerContext.java        |    2 +-
 ...ddedChannelWriteReleaseHandlerContext.java |    1 +
 .../epoll/EpollSocketChannelBenchmark.java    |   16 +
 .../BurstCostExecutorsBenchmark.java          |    2 +-
 .../FastThreadLocalFastPathBenchmark.java     |   13 +-
 .../FastThreadLocalSlowPathBenchmark.java     |   13 +-
 .../ReadOnlyHttp2HeadersBenchmark.java        |   33 +-
 .../http/HttpRequestDecoderBenchmark.java     |    6 +-
 .../RecyclableArrayListBenchmark.java         |    4 +-
 .../ScheduleFutureTaskBenchmark.java          |  109 +
 .../netty/util/concurrent/package-info.java   |   19 +
 pom.xml                                       |  236 +-
 resolver-dns-native-macos/pom.xml             |  185 +
 .../src/main/c/dnsinfo.h                      |  106 +
 .../src/main/c/netty_resolver_dns_macos.c     |  263 +
 .../netty/resolver/dns/macos/DnsResolver.java |   81 +
 .../MacOSDnsServerAddressStreamProvider.java  |  179 +
 .../resolver/dns/macos/package-info.java      |   20 +
 ...cOSDnsServerAddressStreamProviderTest.java |   52 +
 resolver-dns/pom.xml                          |    2 +-
 .../dns/AuthoritativeDnsServerCache.java      |    2 -
 .../AuthoritativeDnsServerCacheAdapter.java   |    2 -
 .../dns/BiDnsQueryLifecycleObserver.java      |    2 -
 .../BiDnsQueryLifecycleObserverFactory.java   |    2 -
 .../resolver/dns/DatagramDnsQueryContext.java |   51 +
 .../DefaultAuthoritativeDnsServerCache.java   |    6 +-
 .../netty/resolver/dns/DefaultDnsCache.java   |    2 -
 .../resolver/dns/DefaultDnsCnameCache.java    |    8 +-
 ...DefaultDnsServerAddressStreamProvider.java |    6 +-
 .../dns/DnsAddressResolveContext.java         |   43 +-
 .../resolver/dns/DnsAddressResolverGroup.java |    3 -
 .../java/io/netty/resolver/dns/DnsCache.java  |    2 -
 .../io/netty/resolver/dns/DnsCacheEntry.java  |    3 -
 .../io/netty/resolver/dns/DnsCnameCache.java  |    2 -
 .../netty/resolver/dns/DnsNameResolver.java   |  354 +-
 .../resolver/dns/DnsNameResolverBuilder.java  |   56 +-
 .../dns/DnsNameResolverException.java         |    2 -
 .../dns/DnsNameResolverTimeoutException.java  |    2 -
 .../netty/resolver/dns/DnsQueryContext.java   |   59 +-
 .../dns/DnsQueryLifecycleObserver.java        |    2 -
 .../dns/DnsQueryLifecycleObserverFactory.java |    2 -
 .../resolver/dns/DnsRecordResolveContext.java |   10 +
 .../netty/resolver/dns/DnsResolveContext.java |   96 +-
 .../resolver/dns/DnsServerAddressStream.java  |    3 -
 .../dns/DnsServerAddressStreamProvider.java   |    3 -
 .../dns/DnsServerAddressStreamProviders.java  |  126 +-
 .../resolver/dns/DnsServerAddresses.java      |   15 +-
 .../MultiDnsServerAddressStreamProvider.java  |    3 -
 .../dns/NoopAuthoritativeDnsServerCache.java  |    4 -
 .../io/netty/resolver/dns/NoopDnsCache.java   |    2 -
 .../netty/resolver/dns/NoopDnsCnameCache.java |    2 -
 .../NoopDnsQueryLifecycleObserverFactory.java |    2 -
 .../dns/PreferredAddressTypeComparator.java   |   54 +
 .../RoundRobinDnsAddressResolverGroup.java    |    2 -
 ...uentialDnsServerAddressStreamProvider.java |    3 -
 .../dns/ShuffledDnsServerAddressStream.java   |    1 -
 ...ngletonDnsServerAddressStreamProvider.java |    3 -
 .../resolver/dns/TcpDnsQueryContext.java      |   53 +
 ...esolverDnsServerAddressStreamProvider.java |  106 +-
 .../io/netty/resolver/dns/package-info.java   |    3 -
 ...efaultAuthoritativeDnsServerCacheTest.java |    1 -
 .../resolver/dns/DnsNameResolverTest.java     |  441 +-
 .../DnsServerAddressStreamProvidersTest.java  |   27 +
 .../PreferredAddressTypeComparatorTest.java   |   70 +
 .../io/netty/resolver/dns/TestDnsServer.java  |   36 +-
 ...verDnsServerAddressStreamProviderTest.java |   24 +
 resolver/pom.xml                              |    2 +-
 .../resolver/AbstractAddressResolver.java     |    6 +-
 .../io/netty/resolver/AddressResolver.java    |    2 -
 .../netty/resolver/AddressResolverGroup.java  |   36 +-
 .../netty/resolver/CompositeNameResolver.java |    9 +-
 .../resolver/DefaultAddressResolverGroup.java |    2 -
 .../DefaultHostsFileEntriesResolver.java      |    2 -
 .../netty/resolver/DefaultNameResolver.java   |    2 -
 .../io/netty/resolver/HostsFileEntries.java   |    3 -
 .../resolver/HostsFileEntriesResolver.java    |    3 -
 .../io/netty/resolver/HostsFileParser.java    |    2 -
 .../io/netty/resolver/InetNameResolver.java   |    2 -
 .../resolver/InetSocketAddressResolver.java   |    2 -
 .../java/io/netty/resolver/NameResolver.java  |    2 -
 .../netty/resolver/NoopAddressResolver.java   |    2 -
 .../resolver/NoopAddressResolverGroup.java    |    2 -
 .../netty/resolver/ResolvedAddressTypes.java  |    3 -
 .../RoundRobinInetAddressResolver.java        |    7 +-
 .../io/netty/resolver/SimpleNameResolver.java |    2 -
 .../java/io/netty/resolver/package-info.java  |    3 -
 tarball/pom.xml                               |   18 +-
 testsuite-autobahn/pom.xml                    |    6 +-
 .../autobahn/AutobahnServerHandler.java       |    6 +-
 testsuite-http2/pom.xml                       |    6 +-
 .../http2/HelloWorldHttp1Handler.java         |    2 +-
 .../io/netty/testsuite/http2/Http2Server.java |    2 +-
 .../http2/Http2ServerInitializer.java         |    7 +-
 testsuite-native-image/pom.xml                |  121 +
 .../netty/testsuite/svm/HttpNativeServer.java |   64 +
 .../svm/HttpNativeServerHandler.java          |   67 +
 .../svm/HttpNativeServerInitializer.java      |   33 +
 .../io/netty/testsuite/svm/package-info.java  |   20 +
 testsuite-osgi/pom.xml                        |   99 +-
 .../io/netty/osgitests/OsgiBundleTest.java    |   70 +-
 testsuite-shading/pom.xml                     |   18 +-
 testsuite/pom.xml                             |    3 +-
 .../AbstractSingleThreadEventLoopTest.java    |  168 +
 .../transport/DefaultEventLoopTest.java       |   41 +
 .../testsuite/transport/NioEventLoopTest.java |   41 +
 .../socket/AbstractDatagramTest.java          |   21 +-
 .../socket/AbstractSocketReuseFdTest.java     |  180 +
 ...bstractSocketShutdownOutputByPeerTest.java |  163 +
 .../CompositeBufferGatheringWriteTest.java    |    1 -
 .../socket/DatagramMulticastIPv6Test.java     |   26 +
 .../socket/DatagramMulticastTest.java         |  118 +-
 .../socket/DatagramUnicastIPv6MappedTest.java |   39 +
 .../socket/DatagramUnicastIPv6Test.java       |   50 +
 .../transport/socket/DatagramUnicastTest.java |  209 +-
 .../transport/socket/SocketConnectTest.java   |    2 -
 .../socket/SocketFileRegionTest.java          |   41 +-
 .../SocketShutdownOutputByPeerTest.java       |  139 +-
 .../SocketSslClientRenegotiateTest.java       |  121 +-
 .../socket/SocketSslGreetingTest.java         |  100 +-
 .../socket/SocketTestPermutation.java         |    5 +-
 .../udt/UDTClientServerConnectionTest.java    |   19 +
 .../io/netty/testsuite/util/TestUtils.java    |    9 +-
 transport-blockhound-tests/pom.xml            |   77 +
 .../NettyBlockHoundIntegrationTest.java       |  215 +
 transport-native-epoll/README.md              |    2 +-
 transport-native-epoll/pom.xml                |  126 +-
 .../src/main/c/netty_epoll_linuxsocket.c      |  453 +-
 .../src/main/c/netty_epoll_native.c           |  424 +-
 .../channel/epoll/AbstractEpollChannel.java   |   29 +-
 .../epoll/AbstractEpollStreamChannel.java     |   88 +-
 .../java/io/netty/channel/epoll/Epoll.java    |   18 +-
 .../channel/epoll/EpollChannelConfig.java     |    6 +-
 .../channel/epoll/EpollChannelOption.java     |    2 +
 .../channel/epoll/EpollDatagramChannel.java   |  323 +-
 .../epoll/EpollDatagramChannelConfig.java     |   82 +-
 .../epoll/EpollDomainSocketChannelConfig.java |   84 +-
 .../netty/channel/epoll/EpollEventLoop.java   |  254 +-
 .../channel/epoll/EpollEventLoopGroup.java    |   29 +-
 .../epoll/EpollRecvByteAllocatorHandle.java   |   58 +-
 ...EpollRecvByteAllocatorStreamingHandle.java |    2 +-
 .../epoll/EpollServerChannelConfig.java       |   15 +-
 .../io/netty/channel/epoll/LinuxSocket.java   |  197 +-
 .../java/io/netty/channel/epoll/Native.java   |   89 +-
 .../epoll/NativeDatagramPacketArray.java      |  127 +-
 .../NativeStaticallyReferencedJniMethods.java |    1 +
 .../io/netty/channel/epoll/TcpMd5Util.java    |    4 +-
 .../epoll/EpollDatagramChannelTest.java       |  108 +
 .../epoll/EpollDatagramMulticastIPv6Test.java |   30 +
 .../epoll/EpollDatagramMulticastTest.java     |   29 +
 .../EpollDatagramScatteringReadTest.java      |  276 +
 .../EpollDatagramUnicastIPv6MappedTest.java   |   29 +
 .../epoll/EpollDatagramUnicastIPv6Test.java   |   29 +
 .../epoll/EpollDatagramUnicastTest.java       |    3 +-
 .../epoll/EpollDomainSocketReuseFdTest.java   |   36 +
 ...lDomainSocketShutdownOutputByPeerTest.java |   69 +
 ...lDomainSocketSslClientRenegotiateTest.java |   42 +
 .../EpollDomainSocketSslGreetingTest.java     |    4 +-
 .../channel/epoll/EpollEventLoopTest.java     |   76 +-
 .../channel/epoll/EpollReuseAddrTest.java     |    4 +-
 .../EpollSocketSslClientRenegotiateTest.java  |   36 +
 .../epoll/EpollSocketSslGreetingTest.java     |    4 +-
 .../epoll/EpollSocketSslSessionReuseTest.java |   36 +
 .../epoll/EpollSocketTestPermutation.java     |   43 +-
 .../netty/channel/epoll/EpollSpliceTest.java  |   17 +-
 .../io/netty/channel/epoll/EpollTest.java     |    4 +-
 .../epoll/EpollWriteBeforeRegisteredTest.java |   30 +
 transport-native-kqueue/pom.xml               |   35 +-
 .../src/main/c/netty_kqueue_bsdsocket.c       |  173 +-
 .../src/main/c/netty_kqueue_native.c          |   16 +-
 .../channel/kqueue/AbstractKQueueChannel.java |   65 +-
 .../kqueue/AbstractKQueueStreamChannel.java   |    7 +-
 .../io/netty/channel/kqueue/BsdSocket.java    |   15 +-
 .../java/io/netty/channel/kqueue/KQueue.java  |   16 +-
 .../channel/kqueue/KQueueDatagramChannel.java |  125 +-
 .../KQueueDomainSocketChannelConfig.java      |   78 +-
 .../netty/channel/kqueue/KQueueEventLoop.java |   65 +-
 .../channel/kqueue/KQueueEventLoopGroup.java  |   22 +-
 .../kqueue/KQueueRecvByteAllocatorHandle.java |   48 +-
 .../kqueue/KQueueServerChannelConfig.java     |   11 +-
 .../netty/channel/kqueue/NativeLongArray.java |    4 +
 .../KQueueDatagramUnicastIPv6MappedTest.java  |   29 +
 .../kqueue/KQueueDatagramUnicastIPv6Test.java |   31 +
 .../kqueue/KQueueDatagramUnicastTest.java     |    3 +-
 .../kqueue/KQueueDomainSocketReuseFdTest.java |   36 +
 ...eDomainSocketShutdownOutputByPeerTest.java |   68 +
 ...eDomainSocketSslClientRenegotiateTest.java |   42 +
 .../KQueueDomainSocketSslGreetingTest.java    |    4 +-
 .../kqueue/KQueueETSocketAutoReadTest.java    |    1 -
 .../KQueueETSocketExceptionHandlingTest.java  |    1 -
 .../kqueue/KQueueETSocketReadPendingTest.java |    1 -
 .../channel/kqueue/KQueueEventLoopTest.java   |   20 +-
 .../KQueueSocketConnectionAttemptTest.java    |    4 -
 .../KQueueSocketSslClientRenegotiateTest.java |   36 +
 .../kqueue/KQueueSocketSslGreetingTest.java   |    4 +-
 .../KQueueSocketSslSessionReuseTest.java      |   36 +
 .../channel/kqueue/KQueueSocketTest.java      |   30 +-
 .../kqueue/KQueueSocketTestPermutation.java   |    5 +-
 .../KqueueWriteBeforeRegisteredTest.java      |   30 +
 transport-native-unix-common-tests/pom.xml    |    2 +-
 transport-native-unix-common/pom.xml          |   25 +-
 .../src/main/c/netty_unix_errors.c            |  120 +-
 .../src/main/c/netty_unix_filedescriptor.c    |   60 +-
 .../src/main/c/netty_unix_socket.c            |  339 +-
 .../src/main/c/netty_unix_socket.h            |    5 +-
 .../src/main/c/netty_unix_util.c              |   71 +-
 .../src/main/c/netty_unix_util.h              |   84 +
 .../channel/unix/DatagramSocketAddress.java   |   15 +-
 .../channel/unix/DomainSocketAddress.java     |    7 +-
 .../java/io/netty/channel/unix/Errors.java    |   41 +-
 .../io/netty/channel/unix/FileDescriptor.java |   55 +-
 .../java/io/netty/channel/unix/IovArray.java  |   51 +-
 .../netty/channel/unix/NativeInetAddress.java |    8 +-
 .../java/io/netty/channel/unix/Socket.java    |   98 +-
 transport-rxtx/pom.xml                        |    2 +-
 transport-sctp/pom.xml                        |    2 +-
 .../sctp/DefaultSctpChannelConfig.java        |    6 +-
 .../sctp/DefaultSctpServerChannelConfig.java  |   12 +-
 .../io/netty/channel/sctp/SctpMessage.java    |   12 +-
 .../channel/sctp/SctpNotificationHandler.java |    7 +-
 .../sctp/oio/OioSctpServerChannel.java        |    7 +-
 .../sctp/SctpMessageCompletionHandler.java    |    6 +-
 transport-udt/pom.xml                         |    2 +-
 .../nio/NioUdtMessageConnectorChannel.java    |    4 +-
 .../io/netty/test/udt/util/CaliperBench.java  |    1 -
 transport/pom.xml                             |    2 +-
 .../io/netty/bootstrap/AbstractBootstrap.java |   98 +-
 .../java/io/netty/bootstrap/Bootstrap.java    |   37 +-
 .../io/netty/bootstrap/ServerBootstrap.java   |   81 +-
 .../io/netty/channel/AbstractChannel.java     |   55 +-
 .../AbstractChannelHandlerContext.java        |  363 +-
 .../channel/AdaptiveRecvByteBufAllocator.java |    9 +-
 .../netty/channel/ChannelDuplexHandler.java   |   10 +
 .../io/netty/channel/ChannelException.java    |   19 +
 .../channel/ChannelFlushPromiseNotifier.java  |   16 +-
 .../java/io/netty/channel/ChannelHandler.java |    3 +-
 .../netty/channel/ChannelHandlerAdapter.java  |    5 +
 .../io/netty/channel/ChannelHandlerMask.java  |  205 +
 .../channel/ChannelInboundHandlerAdapter.java |   12 +
 .../io/netty/channel/ChannelMetadata.java     |    7 +-
 .../java/io/netty/channel/ChannelOption.java  |    8 +-
 .../netty/channel/ChannelOutboundBuffer.java  |   34 +-
 .../ChannelOutboundHandlerAdapter.java        |   10 +
 .../netty/channel/CoalescingBufferQueue.java  |    1 -
 .../channel/CombinedChannelDuplexHandler.java |   10 +-
 .../netty/channel/CompleteChannelFuture.java  |    6 +-
 .../channel/DefaultAddressedEnvelope.java     |    6 +-
 .../netty/channel/DefaultChannelConfig.java   |   46 +-
 .../channel/DefaultChannelHandlerContext.java |   13 +-
 .../netty/channel/DefaultChannelPipeline.java |   56 +-
 .../netty/channel/DefaultEventLoopGroup.java  |    9 +
 .../io/netty/channel/DefaultFileRegion.java   |   57 +-
 .../DefaultMaxBytesRecvByteBufAllocator.java  |   20 +-
 ...efaultMaxMessagesRecvByteBufAllocator.java |    6 +-
 .../channel/DefaultMessageSizeEstimator.java  |    6 +-
 .../channel/EventLoopTaskQueueFactory.java    |   35 +
 .../ExtendedClosedChannelException.java       |   32 +
 .../io/netty/channel/FailedChannelFuture.java |    6 +-
 .../channel/FixedRecvByteBufAllocator.java    |    9 +-
 .../netty/channel/MessageSizeEstimator.java   |    4 +-
 .../channel/MultithreadEventLoopGroup.java    |    1 +
 .../io/netty/channel/PendingWriteQueue.java   |   33 +-
 .../channel/SimpleChannelInboundHandler.java  |    9 -
 .../netty/channel/SingleThreadEventLoop.java  |   32 +-
 .../channel/ThreadPerChannelEventLoop.java    |    5 +
 .../ThreadPerChannelEventLoopGroup.java       |   24 +-
 .../io/netty/channel/VoidChannelPromise.java  |    6 +-
 .../netty/channel/WriteBufferWaterMark.java   |    6 +-
 .../channel/embedded/EmbeddedChannel.java     |   24 +-
 .../channel/embedded/EmbeddedEventLoop.java   |    5 +-
 .../channel/group/ChannelGroupException.java  |    9 +-
 .../netty/channel/group/CombinedIterator.java |   14 +-
 .../channel/group/DefaultChannelGroup.java    |   33 +-
 .../group/DefaultChannelGroupFuture.java      |   11 +-
 .../io/netty/channel/local/LocalAddress.java  |    5 +-
 .../io/netty/channel/local/LocalChannel.java  |   15 +-
 .../channel/local/LocalEventLoopGroup.java    |    9 +
 .../netty/channel/nio/AbstractNioChannel.java |   10 +-
 .../io/netty/channel/nio/NioEventLoop.java    |  321 +-
 .../netty/channel/nio/NioEventLoopGroup.java  |   21 +-
 .../channel/oio/OioByteStreamChannel.java     |   11 +-
 .../netty/channel/oio/OioEventLoopGroup.java  |    3 +-
 .../channel/pool/AbstractChannelPoolMap.java  |   59 +-
 .../netty/channel/pool/FixedChannelPool.java  |   84 +-
 .../netty/channel/pool/SimpleChannelPool.java |   42 +-
 .../socket/DefaultDatagramChannelConfig.java  |    6 +-
 .../DefaultServerSocketChannelConfig.java     |   11 +-
 .../socket/DefaultSocketChannelConfig.java    |    6 +-
 .../channel/socket/nio/NioChannelOption.java  |    5 +
 .../socket/nio/NioDatagramChannel.java        |   36 +-
 .../socket/nio/NioServerSocketChannel.java    |    7 +-
 .../channel/socket/nio/NioSocketChannel.java  |    6 +-
 .../socket/nio/ProtocolFamilyConverter.java   |    2 +
 .../socket/oio/OioServerSocketChannel.java    |    7 +-
 .../transport/native-image.properties         |   15 +
 .../io.netty/transport/reflection-config.json |   15 +
 .../io/netty/bootstrap/BootstrapTest.java     |   87 +
 .../io/netty/channel/AbstractChannelTest.java |   91 +-
 .../netty/channel/AbstractEventLoopTest.java  |    2 +-
 .../AdaptiveRecvByteBufAllocatorTest.java     |   20 +
 .../DefaultChannelPipelineTailTest.java       |    4 +-
 .../channel/DefaultChannelPipelineTest.java   |  540 +-
 .../netty/channel/DefaultFileRegionTest.java  |  120 +
 .../DelegatingChannelPromiseNotifierTest.java |    2 -
 .../channel/embedded/EmbeddedChannelTest.java |   22 +
 .../netty/channel/nio/NioEventLoopTest.java   |   79 +
 .../pool/AbstractChannelPoolMapTest.java      |   97 +-
 .../channel/pool/ChannelPoolTestUtils.java    |   29 +
 .../pool/FixedChannelPoolMapDeadlockTest.java |  264 +
 .../channel/pool/FixedChannelPoolTest.java    |   83 +-
 .../channel/pool/SimpleChannelPoolTest.java   |   85 +-
 .../nio/NioServerSocketChannelTest.java       |   18 +
 transport/test.log                            |    0
 1063 files changed, 46237 insertions(+), 14229 deletions(-)
 create mode 100644 .lgtm.yml
 create mode 100644 buffer/src/main/resources/META-INF/native-image/io.netty/buffer/native-image.properties
 create mode 100644 codec-dns/src/main/java/io/netty/handler/codec/dns/DnsCodecUtil.java
 create mode 100644 codec-dns/src/main/java/io/netty/handler/codec/dns/DnsQueryEncoder.java
 create mode 100644 codec-dns/src/main/java/io/netty/handler/codec/dns/DnsResponseDecoder.java
 create mode 100644 codec-dns/src/main/java/io/netty/handler/codec/dns/TcpDnsQueryEncoder.java
 create mode 100644 codec-dns/src/main/java/io/netty/handler/codec/dns/TcpDnsResponseDecoder.java
 create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/CorruptedWebSocketFrameException.java
 create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientProtocolConfig.java
 create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketCloseStatus.java
 create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketDecoderConfig.java
 create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolConfig.java
 create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionFilter.java
 create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionFilterProvider.java
 create mode 100644 codec-http/src/main/resources/META-INF/native-image/io.netty/codec-http/native-image.properties
 create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/HttpContentDecompressorTest.java
 create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketCloseStatusTest.java
 create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionFilterProviderTest.java
 create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionFilterTest.java
 create mode 100644 codec-http/src/test/resources/file-03.txt
 create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2StreamChannel.java
 create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2SettingsAckFrame.java
 create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ControlFrameLimitEncoder.java
 create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/Http2EmptyDataFrameConnectionDecoder.java
 create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/Http2EmptyDataFrameListener.java
 create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MultiplexHandler.java
 create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/Http2SettingsAckFrame.java
 create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/Http2SettingsReceivedConsumer.java
 create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/MaxCapacityQueue.java
 create mode 100644 codec-http2/src/main/resources/META-INF/native-image/io.netty/codec-http2/native-image.properties
 create mode 100644 codec-http2/src/test/java/io/netty/handler/codec/http2/DecoratingHttp2ConnectionEncoderTest.java
 create mode 100644 codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ControlFrameLimitEncoderTest.java
 create mode 100644 codec-http2/src/test/java/io/netty/handler/codec/http2/Http2DefaultFramesTest.java
 create mode 100644 codec-http2/src/test/java/io/netty/handler/codec/http2/Http2EmptyDataFrameConnectionDecoderTest.java
 create mode 100644 codec-http2/src/test/java/io/netty/handler/codec/http2/Http2EmptyDataFrameListenerTest.java
 create mode 100644 codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexClientUpgradeTest.java
 create mode 100644 codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexHandlerClientUpgradeTest.java
 create mode 100644 codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexHandlerTest.java
 create mode 100644 codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexTest.java
 create mode 100644 codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexTransportTest.java
 create mode 100644 codec-http2/src/test/java/io/netty/handler/codec/http2/Http2StreamChannelIdTest.java
 create mode 100644 codec-memcache/src/test/java/io/netty/handler/codec/memcache/binary/DefaultFullBinaryMemcacheRequestTest.java
 create mode 100644 codec-memcache/src/test/java/io/netty/handler/codec/memcache/binary/DefaultFullBinaryMemcacheResponseTest.java
 create mode 100644 codec-socks/src/test/java/io/netty/handler/codec/socksx/v5/Socks5InitialRequestDecoderTest.java
 create mode 100644 codec/src/main/java/io/netty/handler/codec/compression/Lz4XXHash32.java
 create mode 100644 codec/src/test/java/io/netty/handler/codec/compression/ByteBufChecksumTest.java
 create mode 100644 codec/src/test/java/io/netty/handler/codec/compression/LengthAwareLzfIntegrationTest.java
 create mode 100644 common/src/main/java/io/netty/util/internal/Hidden.java
 create mode 100644 common/src/main/java/io/netty/util/internal/ObjectPool.java
 create mode 100644 common/src/main/java/io/netty/util/internal/ReferenceCountUpdater.java
 create mode 100644 common/src/main/java/io/netty/util/internal/ThreadExecutorMap.java
 create mode 100644 common/src/main/java/io/netty/util/internal/svm/CleanerJava6Substitution.java
 create mode 100644 common/src/main/java/io/netty/util/internal/svm/PlatformDependent0Substitution.java
 create mode 100644 common/src/main/java/io/netty/util/internal/svm/PlatformDependentSubstitution.java
 create mode 100644 common/src/main/java/io/netty/util/internal/svm/UnsafeRefArrayAccessSubstitution.java
 create mode 100644 common/src/main/java/io/netty/util/internal/svm/package-info.java
 create mode 100644 common/src/main/resources/META-INF/native-image/io.netty/common/native-image.properties
 create mode 100644 common/src/main/resources/META-INF/services/reactor.blockhound.integration.BlockHoundIntegration
 create mode 100644 common/src/test/java/io/netty/util/concurrent/ImmediateExecutorTest.java
 create mode 100644 common/src/test/java/io/netty/util/internal/MathUtilTest.java
 create mode 100644 common/src/test/java/io/netty/util/internal/ThreadExecutorMapTest.java
 create mode 100644 docker/docker-compose.centos-6.graalvm1.yaml
 create mode 100644 docker/docker-compose.centos-6.openj9111.yaml
 create mode 100644 example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2ClientFrameInitializer.java
 create mode 100644 example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2ClientStreamFrameResponseHandler.java
 create mode 100644 example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2FrameClient.java
 create mode 100644 example/src/main/java/io/netty/example/mqtt/heartBeat/MqttHeartBeatBroker.java
 create mode 100644 example/src/main/java/io/netty/example/mqtt/heartBeat/MqttHeartBeatBrokerHandler.java
 create mode 100644 example/src/main/java/io/netty/example/mqtt/heartBeat/MqttHeartBeatClient.java
 create mode 100644 example/src/main/java/io/netty/example/mqtt/heartBeat/MqttHeartBeatClientHandler.java
 create mode 100644 handler/src/main/java/io/netty/handler/address/DynamicAddressConnectHandler.java
 create mode 100644 handler/src/main/java/io/netty/handler/address/ResolveAddressHandler.java
 create mode 100644 handler/src/main/java/io/netty/handler/address/package-info.java
 create mode 100644 handler/src/main/java/io/netty/handler/logging/ByteBufFormat.java
 create mode 100644 handler/src/main/java/io/netty/handler/ssl/KeyManagerFactoryWrapper.java
 create mode 100644 handler/src/main/java/io/netty/handler/ssl/OpenSslPrivateKeyMethod.java
 create mode 100644 handler/src/main/java/io/netty/handler/ssl/PseudoRandomFunction.java
 create mode 100644 handler/src/main/java/io/netty/handler/ssl/SslClientHelloHandler.java
 create mode 100644 handler/src/main/java/io/netty/handler/ssl/SslHandshakeTimeoutException.java
 create mode 100644 handler/src/main/java/io/netty/handler/ssl/SslMasterKeyHandler.java
 create mode 100644 handler/src/main/java/io/netty/handler/ssl/TrustManagerFactoryWrapper.java
 create mode 100644 handler/src/main/java/io/netty/handler/ssl/util/SimpleKeyManagerFactory.java
 create mode 100644 handler/src/main/java/io/netty/handler/ssl/util/X509KeyManagerWrapper.java
 create mode 100644 handler/src/main/resources/META-INF/native-image/io.netty/handler/native-image.properties
 create mode 100644 handler/src/test/java/io/netty/handler/address/DynamicAddressConnectHandlerTest.java
 create mode 100644 handler/src/test/java/io/netty/handler/address/ResolveAddressHandlerTest.java
 create mode 100644 handler/src/test/java/io/netty/handler/ssl/AmazonCorrettoSslEngineTest.java
 create mode 100644 handler/src/test/java/io/netty/handler/ssl/ConscryptOpenSslEngineInteropTest.java
 create mode 100644 handler/src/test/java/io/netty/handler/ssl/OpenSslConscryptSslEngineInteropTest.java
 create mode 100644 handler/src/test/java/io/netty/handler/ssl/OpenSslPrivateKeyMethodTest.java
 create mode 100644 handler/src/test/java/io/netty/handler/ssl/PseudoRandomFunctionTest.java
 create mode 100644 handler/src/test/java/io/netty/handler/timeout/IdleStateEventTest.java
 create mode 100644 license/LICENSE.dnsinfo.txt
 create mode 100644 license/LICENSE.hyper-hpack.txt
 create mode 100644 license/LICENSE.nghttp2-hpack.txt
 create mode 100644 microbench/src/main/java/io/netty/buffer/ByteBufAccessBenchmark.java
 create mode 100644 microbench/src/main/java/io/netty/handler/codec/DateFormatter2Benchmark.java
 create mode 100644 microbench/src/main/java/io/netty/handler/codec/http/DecodeHexBenchmark.java
 create mode 100644 microbench/src/main/java/io/netty/handler/codec/http/QueryStringDecoderBenchmark.java
 create mode 100644 microbench/src/main/java/io/netty/handler/codec/http/QueryStringEncoderBenchmark.java
 create mode 100644 microbench/src/main/java/io/netty/microbench/channel/DefaultChannelPipelineBenchmark.java
 create mode 100644 microbench/src/main/java/io/netty/util/concurrent/ScheduleFutureTaskBenchmark.java
 create mode 100644 microbench/src/main/java/io/netty/util/concurrent/package-info.java
 create mode 100644 resolver-dns-native-macos/pom.xml
 create mode 100644 resolver-dns-native-macos/src/main/c/dnsinfo.h
 create mode 100644 resolver-dns-native-macos/src/main/c/netty_resolver_dns_macos.c
 create mode 100644 resolver-dns-native-macos/src/main/java/io/netty/resolver/dns/macos/DnsResolver.java
 create mode 100644 resolver-dns-native-macos/src/main/java/io/netty/resolver/dns/macos/MacOSDnsServerAddressStreamProvider.java
 create mode 100644 resolver-dns-native-macos/src/main/java/io/netty/resolver/dns/macos/package-info.java
 create mode 100644 resolver-dns-native-macos/src/test/java/io/netty/resolver/dns/macos/MacOSDnsServerAddressStreamProviderTest.java
 create mode 100644 resolver-dns/src/main/java/io/netty/resolver/dns/DatagramDnsQueryContext.java
 create mode 100644 resolver-dns/src/main/java/io/netty/resolver/dns/PreferredAddressTypeComparator.java
 create mode 100644 resolver-dns/src/main/java/io/netty/resolver/dns/TcpDnsQueryContext.java
 create mode 100644 resolver-dns/src/test/java/io/netty/resolver/dns/DnsServerAddressStreamProvidersTest.java
 create mode 100644 resolver-dns/src/test/java/io/netty/resolver/dns/PreferredAddressTypeComparatorTest.java
 create mode 100644 testsuite-native-image/pom.xml
 create mode 100644 testsuite-native-image/src/main/java/io/netty/testsuite/svm/HttpNativeServer.java
 create mode 100644 testsuite-native-image/src/main/java/io/netty/testsuite/svm/HttpNativeServerHandler.java
 create mode 100644 testsuite-native-image/src/main/java/io/netty/testsuite/svm/HttpNativeServerInitializer.java
 create mode 100644 testsuite-native-image/src/main/java/io/netty/testsuite/svm/package-info.java
 create mode 100644 testsuite/src/main/java/io/netty/testsuite/transport/AbstractSingleThreadEventLoopTest.java
 create mode 100644 testsuite/src/main/java/io/netty/testsuite/transport/DefaultEventLoopTest.java
 create mode 100644 testsuite/src/main/java/io/netty/testsuite/transport/NioEventLoopTest.java
 create mode 100644 testsuite/src/main/java/io/netty/testsuite/transport/socket/AbstractSocketReuseFdTest.java
 create mode 100644 testsuite/src/main/java/io/netty/testsuite/transport/socket/AbstractSocketShutdownOutputByPeerTest.java
 create mode 100644 testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramMulticastIPv6Test.java
 create mode 100644 testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramUnicastIPv6MappedTest.java
 create mode 100644 testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramUnicastIPv6Test.java
 create mode 100644 transport-blockhound-tests/pom.xml
 create mode 100644 transport-blockhound-tests/src/test/java/io/netty/util/internal/NettyBlockHoundIntegrationTest.java
 create mode 100644 transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramChannelTest.java
 create mode 100644 transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramMulticastIPv6Test.java
 create mode 100644 transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramMulticastTest.java
 create mode 100644 transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramScatteringReadTest.java
 create mode 100644 transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramUnicastIPv6MappedTest.java
 create mode 100644 transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramUnicastIPv6Test.java
 create mode 100644 transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDomainSocketReuseFdTest.java
 create mode 100644 transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDomainSocketShutdownOutputByPeerTest.java
 create mode 100644 transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDomainSocketSslClientRenegotiateTest.java
 create mode 100644 transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketSslClientRenegotiateTest.java
 create mode 100644 transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketSslSessionReuseTest.java
 create mode 100644 transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollWriteBeforeRegisteredTest.java
 create mode 100644 transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDatagramUnicastIPv6MappedTest.java
 create mode 100644 transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDatagramUnicastIPv6Test.java
 create mode 100644 transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDomainSocketReuseFdTest.java
 create mode 100644 transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDomainSocketShutdownOutputByPeerTest.java
 create mode 100644 transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDomainSocketSslClientRenegotiateTest.java
 create mode 100644 transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketSslClientRenegotiateTest.java
 create mode 100644 transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketSslSessionReuseTest.java
 create mode 100644 transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KqueueWriteBeforeRegisteredTest.java
 create mode 100644 transport/src/main/java/io/netty/channel/ChannelHandlerMask.java
 create mode 100644 transport/src/main/java/io/netty/channel/EventLoopTaskQueueFactory.java
 create mode 100644 transport/src/main/java/io/netty/channel/ExtendedClosedChannelException.java
 create mode 100644 transport/src/main/resources/META-INF/native-image/io.netty/transport/native-image.properties
 create mode 100644 transport/src/main/resources/META-INF/native-image/io.netty/transport/reflection-config.json
 create mode 100644 transport/src/test/java/io/netty/channel/DefaultFileRegionTest.java
 create mode 100644 transport/src/test/java/io/netty/channel/pool/ChannelPoolTestUtils.java
 create mode 100644 transport/src/test/java/io/netty/channel/pool/FixedChannelPoolMapDeadlockTest.java
 delete mode 100644 transport/test.log

diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 81c2cea..8ca9413 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -1 +1 @@
-Please review the [guidelines for contributing](http://netty.io/wiki/developer-guide.html) for this repository.
+Please review the [guidelines for contributing](https://netty.io/wiki/developer-guide.html) for this repository.
diff --git a/.gitignore b/.gitignore
index ed43883..8af92c7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,3 +37,11 @@ dependency-reduced-pom.xml
 # exclude mainframer files
 mainframer
 .mainframer
+
+# exclude docker-sync stuff
+.docker-sync
+*/.docker-sync
+
+# exclude vscode files
+.vscode/
+*.factorypath
diff --git a/.lgtm.yml b/.lgtm.yml
new file mode 100644
index 0000000..9aa74b1
--- /dev/null
+++ b/.lgtm.yml
@@ -0,0 +1,13 @@
+extraction:
+  java:
+    prepare:
+      packages:
+      - "autoconf"
+      - "automake"
+      - "libtool"
+      - "make"
+      - "tar"
+      - "libaio-dev"
+      - "libssl-dev"
+      - "libapr1-dev"
+      - "lksctp-tools"
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 1a8b4d8..a18232f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -42,5 +42,5 @@ My system has IPv6 disabled.
 
 ## How to contribute your work
 
-Before submitting a pull request or push a commit, please read [our developer guide](http://netty.io/wiki/developer-guide.html).
+Before submitting a pull request or push a commit, please read [our developer guide](https://netty.io/wiki/developer-guide.html).
 
diff --git a/NOTICE.txt b/NOTICE.txt
index f973663..9cf3e81 100644
--- a/NOTICE.txt
+++ b/NOTICE.txt
@@ -4,7 +4,7 @@
 
 Please visit the Netty web site for more information:
 
-  * http://netty.io/
+  * https://netty.io/
 
 Copyright 2014 The Netty Project
 
@@ -162,9 +162,9 @@ This product optionally depends on 'JBoss Marshalling', an alternative Java
 serialization API, which can be obtained at:
 
   * LICENSE:
-    * license/LICENSE.jboss-marshalling.txt (GNU LGPL 2.1)
+    * license/LICENSE.jboss-marshalling.txt (Apache License 2.0)
   * HOMEPAGE:
-    * http://www.jboss.org/jbossmarshalling
+    * https://github.com/jboss-remoting/jboss-marshalling
 
 This product optionally depends on 'Caliper', Google's micro-
 benchmarking framework, which can be obtained at:
@@ -205,6 +205,22 @@ the HTTP/2 HPACK algorithm written by Twitter. It can be obtained at:
     * license/LICENSE.hpack.txt (Apache License 2.0)
   * HOMEPAGE:
     * https://github.com/twitter/hpack
+    
+This product contains a modified version of 'HPACK', a Java implementation of
+the HTTP/2 HPACK algorithm written by Cory Benfield. It can be obtained at:
+
+  * LICENSE:
+    * license/LICENSE.hyper-hpack.txt (MIT License)
+  * HOMEPAGE:
+    * https://github.com/python-hyper/hpack/
+
+This product contains a modified version of 'HPACK', a Java implementation of
+the HTTP/2 HPACK algorithm written by Tatsuhiro Tsujikawa. It can be obtained at:
+
+  * LICENSE:
+    * license/LICENSE.nghttp2-hpack.txt (MIT License)
+  * HOMEPAGE:
+    * https://github.com/nghttp2/nghttp2/
 
 This product contains a modified portion of 'Apache Commons Lang', a Java library
 provides utilities for the java.lang API, which can be obtained at:
@@ -221,3 +237,12 @@ This product contains the Maven wrapper scripts from 'Maven Wrapper', that provi
     * license/LICENSE.mvn-wrapper.txt (Apache License 2.0)
   * HOMEPAGE:
     * https://github.com/takari/maven-wrapper
+
+This product contains the dnsinfo.h header file, that provides a way to retrieve the system DNS configuration on MacOS.
+This private header is also used by Apple's open source
+ mDNSResponder (https://opensource.apple.com/tarballs/mDNSResponder/).
+
+ * LICENSE:
+    * license/LICENSE.dnsinfo.txt (Apache License 2.0)
+  * HOMEPAGE:
+    * http://www.opensource.apple.com/source/configd/configd-453.19/dnsinfo/dnsinfo.h
\ No newline at end of file
diff --git a/README.md b/README.md
index 5c4c4b5..6b557b1 100644
--- a/README.md
+++ b/README.md
@@ -4,20 +4,20 @@ Netty is an asynchronous event-driven network application framework for rapid de
 
 ## Links
 
-* [Web Site](http://netty.io/)
-* [Downloads](http://netty.io/downloads.html)
-* [Documentation](http://netty.io/wiki/)
+* [Web Site](https://netty.io/)
+* [Downloads](https://netty.io/downloads.html)
+* [Documentation](https://netty.io/wiki/)
 * [@netty_project](https://twitter.com/netty_project)
 
 ## How to build
 
-For the detailed information about building and developing Netty, please visit [the developer guide](http://netty.io/wiki/developer-guide.html).  This page only gives very basic information.
+For the detailed information about building and developing Netty, please visit [the developer guide](https://netty.io/wiki/developer-guide.html).  This page only gives very basic information.
 
 You require the following to build Netty:
 
 * Latest stable [Oracle JDK 7](http://www.oracle.com/technetwork/java/)
 * Latest stable [Apache Maven](http://maven.apache.org/)
-* If you are on Linux, you need [additional development packages](http://netty.io/wiki/native-transports.html) installed on your system, because you'll build the native transport.
+* If you are on Linux, you need [additional development packages](https://netty.io/wiki/native-transports.html) installed on your system, because you'll build the native transport.
 
 Note that this is build-time requirement.  JDK 5 (for 3.x) or 6 (for 4.0+) is enough to run your Netty-based application.
 
diff --git a/all/pom.xml b/all/pom.xml
index b97afa8..3a11e7d 100644
--- a/all/pom.xml
+++ b/all/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>io.netty</groupId>
     <artifactId>netty-parent</artifactId>
-    <version>4.1.33.Final</version>
+    <version>4.1.48.Final</version>
   </parent>
 
   <artifactId>netty-all</artifactId>
@@ -31,6 +31,7 @@
   <properties>
     <generatedSourceDir>${project.build.directory}/src</generatedSourceDir>
     <dependencyVersionsDir>${project.build.directory}/versions</dependencyVersionsDir>
+    <skipJapicmp>true</skipJapicmp>
   </properties>
 
   <profiles>
@@ -64,6 +65,14 @@
           <scope>compile</scope>
           <optional>true</optional>
         </dependency>
+        <dependency>
+          <groupId>${project.groupId}</groupId>
+          <artifactId>netty-resolver-dns-native-macos</artifactId>
+          <version>${project.version}</version>
+          <classifier>osx-x86_64</classifier>
+          <scope>compile</scope>
+          <optional>true</optional>
+        </dependency>
       </dependencies>
     </profile>
     <profile>
@@ -88,6 +97,14 @@
           <scope>compile</scope>
           <optional>true</optional>
         </dependency>
+        <dependency>
+          <groupId>${project.groupId}</groupId>
+          <artifactId>netty-resolver-dns-native-macos</artifactId>
+          <version>${project.version}</version>
+          <classifier>osx-x86_64</classifier>
+          <scope>compile</scope>
+          <optional>true</optional>
+        </dependency>
       </dependencies>
     </profile>
 
@@ -111,6 +128,21 @@
           <scope>compile</scope>
           <optional>true</optional>
         </dependency>
+        <!-- Just include the classes for the other platform so these are at least present in the netty-all artifact -->
+        <dependency>
+          <groupId>${project.groupId}</groupId>
+          <artifactId>netty-transport-native-kqueue</artifactId>
+          <version>${project.version}</version>
+          <scope>compile</scope>
+          <optional>true</optional>
+        </dependency>
+        <dependency>
+          <groupId>${project.groupId}</groupId>
+          <artifactId>netty-resolver-dns-native-macos</artifactId>
+          <version>${project.version}</version>
+          <scope>compile</scope>
+          <optional>true</optional>
+        </dependency>
       </dependencies>
     </profile>
     <!-- The mac, openbsd and freebsd  profile will only include the native jar for epol to the all jar.
@@ -133,6 +165,22 @@
           <scope>compile</scope>
           <optional>true</optional>
         </dependency>
+        <dependency>
+          <groupId>${project.groupId}</groupId>
+          <artifactId>netty-resolver-dns-native-macos</artifactId>
+          <version>${project.version}</version>
+          <classifier>osx-x86_64</classifier>
+          <scope>compile</scope>
+          <optional>true</optional>
+        </dependency>
+        <!-- Just include the classes for the other platform so these are at least present in the netty-all artifact -->
+        <dependency>
+          <groupId>${project.groupId}</groupId>
+          <artifactId>netty-transport-native-epoll</artifactId>
+          <version>${project.version}</version>
+          <scope>compile</scope>
+          <optional>true</optional>
+        </dependency>
       </dependencies>
     </profile>
     <profile>
@@ -153,6 +201,14 @@
           <scope>compile</scope>
           <optional>true</optional>
         </dependency>
+        <!-- Just include the classes for the other platform so these are at least present in the netty-all artifact -->
+        <dependency>
+          <groupId>${project.groupId}</groupId>
+          <artifactId>netty-transport-native-epoll</artifactId>
+          <version>${project.version}</version>
+          <scope>compile</scope>
+          <optional>true</optional>
+        </dependency>
       </dependencies>
     </profile>
     <profile>
@@ -173,6 +229,14 @@
           <scope>compile</scope>
           <optional>true</optional>
         </dependency>
+        <!-- Just include the classes for the other platform so these are at least present in the netty-all artifact -->
+        <dependency>
+          <groupId>${project.groupId}</groupId>
+          <artifactId>netty-transport-native-epoll</artifactId>
+          <version>${project.version}</version>
+          <scope>compile</scope>
+          <optional>true</optional>
+        </dependency>
       </dependencies>
     </profile>
 
@@ -205,20 +269,7 @@
               <dependency>
                 <groupId>${project.groupId}</groupId>
                 <artifactId>netty-build</artifactId>
-                <version>19</version>
-                <exclusions>
-                  <!-- Use version 7.3 until a new netty-build release is out -->
-                  <!-- See https://issues.apache.org/jira/browse/JXR-133 -->
-                  <exclusion>
-                    <groupId>com.puppycrawl.tools</groupId>
-                    <artifactId>checkstyle</artifactId>
-                  </exclusion>
-                </exclusions>
-              </dependency>
-              <dependency>
-                <groupId>com.puppycrawl.tools</groupId>
-                <artifactId>checkstyle</artifactId>
-                <version>7.3</version>
+                <version>${netty.build.version}</version>
               </dependency>
             </dependencies>
           </plugin>
@@ -552,7 +603,7 @@
             </goals>
             <configuration>
               <excludes>io/netty/internal/tcnative/**,io/netty/example/**,META-INF/native/libnetty_tcnative*,META-INF/native/include/**,META-INF/native/**/*.a</excludes>
-              <includes>io/netty/**,META-INF/native/**</includes>
+              <includes>io/netty/**,META-INF/native/**,META-INF/native-image/**</includes>
               <includeScope>runtime</includeScope>
               <includeGroupIds>${project.groupId}</includeGroupIds>
               <outputDirectory>${project.build.outputDirectory}</outputDirectory>
diff --git a/bom/pom.xml b/bom/pom.xml
index 34d6b7a..685acaf 100644
--- a/bom/pom.xml
+++ b/bom/pom.xml
@@ -25,16 +25,16 @@
 
   <groupId>io.netty</groupId>
   <artifactId>netty-bom</artifactId>
-  <version>4.1.33.Final</version>
+  <version>4.1.48.Final</version>
   <packaging>pom</packaging>
 
   <name>Netty/BOM</name>
   <description>Netty (Bill of Materials)</description>
-  <url>http://netty.io/</url>
+  <url>https://netty.io/</url>
 
   <organization>
     <name>The Netty Project</name>
-    <url>http://netty.io/</url>
+    <url>https://netty.io/</url>
   </organization>
 
   <licenses>
@@ -49,7 +49,7 @@
     <url>https://github.com/netty/netty</url>
     <connection>scm:git:git://github.com/netty/netty.git</connection>
     <developerConnection>scm:git:ssh://git@github.com/netty/netty.git</developerConnection>
-    <tag>netty-4.1.33.Final</tag>
+    <tag>netty-4.1.48.Final</tag>
   </scm>
 
   <developers>
@@ -57,9 +57,9 @@
       <id>netty.io</id>
       <name>The Netty Project Contributors</name>
       <email>netty@googlegroups.com</email>
-      <url>http://netty.io/</url>
+      <url>https://netty.io/</url>
       <organization>The Netty Project</organization>
-      <organizationUrl>http://netty.io/</organizationUrl>
+      <organizationUrl>https://netty.io/</organizationUrl>
     </developer>
   </developers>
 
@@ -69,165 +69,165 @@
       <dependency>
         <groupId>io.netty</groupId>
         <artifactId>netty-buffer</artifactId>
-        <version>4.1.33.Final</version>
+        <version>4.1.48.Final</version>
       </dependency>
       <dependency>
         <groupId>io.netty</groupId>
         <artifactId>netty-codec</artifactId>
-        <version>4.1.33.Final</version>
+        <version>4.1.48.Final</version>
       </dependency>
       <dependency>
         <groupId>io.netty</groupId>
         <artifactId>netty-codec-dns</artifactId>
-        <version>4.1.33.Final</version>
+        <version>4.1.48.Final</version>
       </dependency>
       <dependency>
         <groupId>io.netty</groupId>
         <artifactId>netty-codec-haproxy</artifactId>
-        <version>4.1.33.Final</version>
+        <version>4.1.48.Final</version>
       </dependency>
       <dependency>
         <groupId>io.netty</groupId>
         <artifactId>netty-codec-http</artifactId>
-        <version>4.1.33.Final</version>
+        <version>4.1.48.Final</version>
       </dependency>
       <dependency>
         <groupId>io.netty</groupId>
         <artifactId>netty-codec-http2</artifactId>
-        <version>4.1.33.Final</version>
+        <version>4.1.48.Final</version>
       </dependency>
       <dependency>
         <groupId>io.netty</groupId>
         <artifactId>netty-codec-memcache</artifactId>
-        <version>4.1.33.Final</version>
+        <version>4.1.48.Final</version>
       </dependency>
       <dependency>
         <groupId>io.netty</groupId>
         <artifactId>netty-codec-mqtt</artifactId>
-        <version>4.1.33.Final</version>
+        <version>4.1.48.Final</version>
       </dependency>
       <dependency>
         <groupId>io.netty</groupId>
         <artifactId>netty-codec-redis</artifactId>
-        <version>4.1.33.Final</version>
+        <version>4.1.48.Final</version>
       </dependency>
       <dependency>
         <groupId>io.netty</groupId>
         <artifactId>netty-codec-smtp</artifactId>
-        <version>4.1.33.Final</version>
+        <version>4.1.48.Final</version>
       </dependency>
       <dependency>
         <groupId>io.netty</groupId>
         <artifactId>netty-codec-socks</artifactId>
-        <version>4.1.33.Final</version>
+        <version>4.1.48.Final</version>
       </dependency>
       <dependency>
         <groupId>io.netty</groupId>
         <artifactId>netty-codec-stomp</artifactId>
-        <version>4.1.33.Final</version>
+        <version>4.1.48.Final</version>
       </dependency>
       <dependency>
         <groupId>io.netty</groupId>
         <artifactId>netty-codec-xml</artifactId>
-        <version>4.1.33.Final</version>
+        <version>4.1.48.Final</version>
       </dependency>
       <dependency>
         <groupId>io.netty</groupId>
         <artifactId>netty-common</artifactId>
-        <version>4.1.33.Final</version>
+        <version>4.1.48.Final</version>
       </dependency>
       <dependency>
         <groupId>io.netty</groupId>
         <artifactId>netty-dev-tools</artifactId>
-        <version>4.1.33.Final</version>
+        <version>4.1.48.Final</version>
       </dependency>
       <dependency>
         <groupId>io.netty</groupId>
         <artifactId>netty-handler</artifactId>
-        <version>4.1.33.Final</version>
+        <version>4.1.48.Final</version>
       </dependency>
       <dependency>
         <groupId>io.netty</groupId>
         <artifactId>netty-handler-proxy</artifactId>
-        <version>4.1.33.Final</version>
+        <version>4.1.48.Final</version>
       </dependency>
       <dependency>
         <groupId>io.netty</groupId>
         <artifactId>netty-resolver</artifactId>
-        <version>4.1.33.Final</version>
+        <version>4.1.48.Final</version>
       </dependency>
       <dependency>
         <groupId>io.netty</groupId>
         <artifactId>netty-resolver-dns</artifactId>
-        <version>4.1.33.Final</version>
+        <version>4.1.48.Final</version>
       </dependency>
       <dependency>
         <groupId>io.netty</groupId>
         <artifactId>netty-transport</artifactId>
-        <version>4.1.33.Final</version>
+        <version>4.1.48.Final</version>
       </dependency>
       <dependency>
         <groupId>io.netty</groupId>
         <artifactId>netty-transport-rxtx</artifactId>
-        <version>4.1.33.Final</version>
+        <version>4.1.48.Final</version>
       </dependency>
       <dependency>
         <groupId>io.netty</groupId>
         <artifactId>netty-transport-sctp</artifactId>
-        <version>4.1.33.Final</version>
+        <version>4.1.48.Final</version>
       </dependency>
       <dependency>
         <groupId>io.netty</groupId>
         <artifactId>netty-transport-udt</artifactId>
-        <version>4.1.33.Final</version>
+        <version>4.1.48.Final</version>
       </dependency>
       <dependency>
         <groupId>io.netty</groupId>
         <artifactId>netty-example</artifactId>
-        <version>4.1.33.Final</version>
+        <version>4.1.48.Final</version>
       </dependency>
       <dependency>
         <groupId>io.netty</groupId>
         <artifactId>netty-all</artifactId>
-        <version>4.1.33.Final</version>
+        <version>4.1.48.Final</version>
       </dependency>
       <dependency>
         <groupId>io.netty</groupId>
         <artifactId>netty-transport-native-unix-common</artifactId>
-        <version>4.1.33.Final</version>
+        <version>4.1.48.Final</version>
       </dependency>
       <dependency>
         <groupId>io.netty</groupId>
         <artifactId>netty-transport-native-unix-common</artifactId>
-        <version>4.1.33.Final</version>
+        <version>4.1.48.Final</version>
         <classifier>linux-x86_64</classifier>
       </dependency>
       <dependency>
         <groupId>io.netty</groupId>
         <artifactId>netty-transport-native-unix-common</artifactId>
-        <version>4.1.33.Final</version>
+        <version>4.1.48.Final</version>
         <classifier>osx-x86_64</classifier>
       </dependency>
       <dependency>
         <groupId>io.netty</groupId>
         <artifactId>netty-transport-native-epoll</artifactId>
-        <version>4.1.33.Final</version>
+        <version>4.1.48.Final</version>
       </dependency>
       <dependency>
         <groupId>io.netty</groupId>
         <artifactId>netty-transport-native-epoll</artifactId>
-        <version>4.1.33.Final</version>
+        <version>4.1.48.Final</version>
         <classifier>linux-x86_64</classifier>
       </dependency>
       <dependency>
         <groupId>io.netty</groupId>
         <artifactId>netty-transport-native-kqueue</artifactId>
-        <version>4.1.33.Final</version>
+        <version>4.1.48.Final</version>
       </dependency>
       <dependency>
         <groupId>io.netty</groupId>
         <artifactId>netty-transport-native-kqueue</artifactId>
-        <version>4.1.33.Final</version>
+        <version>4.1.48.Final</version>
         <classifier>osx-x86_64</classifier>
       </dependency>
     </dependencies>
diff --git a/buffer/pom.xml b/buffer/pom.xml
index 3ea7bd8..cd859ed 100644
--- a/buffer/pom.xml
+++ b/buffer/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>io.netty</groupId>
     <artifactId>netty-parent</artifactId>
-    <version>4.1.33.Final</version>
+    <version>4.1.48.Final</version>
   </parent>
 
   <artifactId>netty-buffer</artifactId>
diff --git a/buffer/src/main/java/io/netty/buffer/AbstractByteBuf.java b/buffer/src/main/java/io/netty/buffer/AbstractByteBuf.java
index 6864cee..9ce8ccc 100644
--- a/buffer/src/main/java/io/netty/buffer/AbstractByteBuf.java
+++ b/buffer/src/main/java/io/netty/buffer/AbstractByteBuf.java
@@ -21,6 +21,7 @@ import io.netty.util.CharsetUtil;
 import io.netty.util.IllegalReferenceCountException;
 import io.netty.util.ResourceLeakDetector;
 import io.netty.util.ResourceLeakDetectorFactory;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.PlatformDependent;
 import io.netty.util.internal.StringUtil;
 import io.netty.util.internal.SystemPropertyUtil;
@@ -38,6 +39,7 @@ import java.nio.channels.ScatteringByteChannel;
 import java.nio.charset.Charset;
 
 import static io.netty.util.internal.MathUtil.isOutOfBounds;
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
 
 /**
  * A skeletal implementation of a buffer.
@@ -73,9 +75,7 @@ public abstract class AbstractByteBuf extends ByteBuf {
     private int maxCapacity;
 
     protected AbstractByteBuf(int maxCapacity) {
-        if (maxCapacity < 0) {
-            throw new IllegalArgumentException("maxCapacity: " + maxCapacity + " (expected: >= 0)");
-        }
+        checkPositiveOrZero(maxCapacity, "maxCapacity");
         this.maxCapacity = maxCapacity;
     }
 
@@ -214,8 +214,8 @@ public abstract class AbstractByteBuf extends ByteBuf {
 
     @Override
     public ByteBuf discardReadBytes() {
-        ensureAccessible();
         if (readerIndex == 0) {
+            ensureAccessible();
             return this;
         }
 
@@ -225,6 +225,7 @@ public abstract class AbstractByteBuf extends ByteBuf {
             adjustMarkers(readerIndex);
             readerIndex = 0;
         } else {
+            ensureAccessible();
             adjustMarkers(readerIndex);
             writerIndex = readerIndex = 0;
         }
@@ -233,23 +234,23 @@ public abstract class AbstractByteBuf extends ByteBuf {
 
     @Override
     public ByteBuf discardSomeReadBytes() {
-        ensureAccessible();
-        if (readerIndex == 0) {
-            return this;
-        }
-
-        if (readerIndex == writerIndex) {
-            adjustMarkers(readerIndex);
-            writerIndex = readerIndex = 0;
-            return this;
-        }
+        if (readerIndex > 0) {
+            if (readerIndex == writerIndex) {
+                ensureAccessible();
+                adjustMarkers(readerIndex);
+                writerIndex = readerIndex = 0;
+                return this;
+            }
 
-        if (readerIndex >= capacity() >>> 1) {
-            setBytes(0, this, readerIndex, writerIndex - readerIndex);
-            writerIndex -= readerIndex;
-            adjustMarkers(readerIndex);
-            readerIndex = 0;
+            if (readerIndex >= capacity() >>> 1) {
+                setBytes(0, this, readerIndex, writerIndex - readerIndex);
+                writerIndex -= readerIndex;
+                adjustMarkers(readerIndex);
+                readerIndex = 0;
+                return this;
+            }
         }
+        ensureAccessible();
         return this;
     }
 
@@ -269,31 +270,37 @@ public abstract class AbstractByteBuf extends ByteBuf {
         }
     }
 
+    // Called after a capacity reduction
+    protected final void trimIndicesToCapacity(int newCapacity) {
+        if (writerIndex() > newCapacity) {
+            setIndex0(Math.min(readerIndex(), newCapacity), newCapacity);
+        }
+    }
+
     @Override
     public ByteBuf ensureWritable(int minWritableBytes) {
-        if (minWritableBytes < 0) {
-            throw new IllegalArgumentException(String.format(
-                    "minWritableBytes: %d (expected: >= 0)", minWritableBytes));
-        }
-        ensureWritable0(minWritableBytes);
+        ensureWritable0(checkPositiveOrZero(minWritableBytes, "minWritableBytes"));
         return this;
     }
 
     final void ensureWritable0(int minWritableBytes) {
-        ensureAccessible();
-        if (minWritableBytes <= writableBytes()) {
+        final int writerIndex = writerIndex();
+        final int targetCapacity = writerIndex + minWritableBytes;
+        if (targetCapacity <= capacity()) {
+            ensureAccessible();
             return;
         }
-        if (checkBounds) {
-            if (minWritableBytes > maxCapacity - writerIndex) {
-                throw new IndexOutOfBoundsException(String.format(
-                        "writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s",
-                        writerIndex, minWritableBytes, maxCapacity, this));
-            }
+        if (checkBounds && targetCapacity > maxCapacity) {
+            ensureAccessible();
+            throw new IndexOutOfBoundsException(String.format(
+                    "writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s",
+                    writerIndex, minWritableBytes, maxCapacity, this));
         }
 
-        // Normalize the current capacity to the power of 2.
-        int newCapacity = alloc().calculateNewCapacity(writerIndex + minWritableBytes, maxCapacity);
+        // Normalize the target capacity to the power of 2.
+        final int fastWritable = maxFastWritableBytes();
+        int newCapacity = fastWritable >= minWritableBytes ? writerIndex + fastWritable
+                : alloc().calculateNewCapacity(targetCapacity, maxCapacity);
 
         // Adjust to the new capacity.
         capacity(newCapacity);
@@ -302,10 +309,7 @@ public abstract class AbstractByteBuf extends ByteBuf {
     @Override
     public int ensureWritable(int minWritableBytes, boolean force) {
         ensureAccessible();
-        if (minWritableBytes < 0) {
-            throw new IllegalArgumentException(String.format(
-                    "minWritableBytes: %d (expected: >= 0)", minWritableBytes));
-        }
+        checkPositiveOrZero(minWritableBytes, "minWritableBytes");
 
         if (minWritableBytes <= writableBytes()) {
             return 0;
@@ -322,8 +326,9 @@ public abstract class AbstractByteBuf extends ByteBuf {
             return 3;
         }
 
-        // Normalize the current capacity to the power of 2.
-        int newCapacity = alloc().calculateNewCapacity(writerIndex + minWritableBytes, maxCapacity);
+        int fastWritable = maxFastWritableBytes();
+        int newCapacity = fastWritable >= minWritableBytes ? writerIndex + fastWritable
+                : alloc().calculateNewCapacity(writerIndex + minWritableBytes, maxCapacity);
 
         // Adjust to the new capacity.
         capacity(newCapacity);
@@ -335,9 +340,7 @@ public abstract class AbstractByteBuf extends ByteBuf {
         if (endianness == order()) {
             return this;
         }
-        if (endianness == null) {
-            throw new NullPointerException("endianness");
-        }
+        ObjectUtil.checkNotNull(endianness, "endianness");
         return newSwappedByteBuf();
     }
 
@@ -645,9 +648,7 @@ public abstract class AbstractByteBuf extends ByteBuf {
     @Override
     public ByteBuf setBytes(int index, ByteBuf src, int length) {
         checkIndex(index, length);
-        if (src == null) {
-            throw new NullPointerException("src");
-        }
+        ObjectUtil.checkNotNull(src, "src");
         if (checkBounds) {
             checkReadableBounds(src, length);
         }
@@ -1248,7 +1249,44 @@ public abstract class AbstractByteBuf extends ByteBuf {
 
     @Override
     public int indexOf(int fromIndex, int toIndex, byte value) {
-        return ByteBufUtil.indexOf(this, fromIndex, toIndex, value);
+        if (fromIndex <= toIndex) {
+            return firstIndexOf(fromIndex, toIndex, value);
+        } else {
+            return lastIndexOf(fromIndex, toIndex, value);
+        }
+    }
+
+    private int firstIndexOf(int fromIndex, int toIndex, byte value) {
+        fromIndex = Math.max(fromIndex, 0);
+        if (fromIndex >= toIndex || capacity() == 0) {
+            return -1;
+        }
+        checkIndex(fromIndex, toIndex - fromIndex);
+
+        for (int i = fromIndex; i < toIndex; i ++) {
+            if (_getByte(i) == value) {
+                return i;
+            }
+        }
+
+        return -1;
+    }
+
+    private int lastIndexOf(int fromIndex, int toIndex, byte value) {
+        fromIndex = Math.min(fromIndex, capacity());
+        if (fromIndex < 0 || capacity() == 0) {
+            return -1;
+        }
+
+        checkIndex(toIndex, fromIndex - toIndex);
+
+        for (int i = fromIndex - 1; i >= toIndex; i --) {
+            if (_getByte(i) == value) {
+                return i;
+            }
+        }
+
+        return -1;
     }
 
     @Override
@@ -1381,30 +1419,38 @@ public abstract class AbstractByteBuf extends ByteBuf {
         checkIndex0(index, fieldLength);
     }
 
-    private static void checkRangeBounds(final int index, final int fieldLength, final int capacity) {
+    private static void checkRangeBounds(final String indexName, final int index,
+            final int fieldLength, final int capacity) {
         if (isOutOfBounds(index, fieldLength, capacity)) {
             throw new IndexOutOfBoundsException(String.format(
-                    "index: %d, length: %d (expected: range(0, %d))", index, fieldLength, capacity));
+                    "%s: %d, length: %d (expected: range(0, %d))", indexName, index, fieldLength, capacity));
         }
     }
 
     final void checkIndex0(int index, int fieldLength) {
         if (checkBounds) {
-            checkRangeBounds(index, fieldLength, capacity());
+            checkRangeBounds("index", index, fieldLength, capacity());
         }
     }
 
     protected final void checkSrcIndex(int index, int length, int srcIndex, int srcCapacity) {
         checkIndex(index, length);
         if (checkBounds) {
-            checkRangeBounds(srcIndex, length, srcCapacity);
+            checkRangeBounds("srcIndex", srcIndex, length, srcCapacity);
         }
     }
 
     protected final void checkDstIndex(int index, int length, int dstIndex, int dstCapacity) {
         checkIndex(index, length);
         if (checkBounds) {
-            checkRangeBounds(dstIndex, length, dstCapacity);
+            checkRangeBounds("dstIndex", dstIndex, length, dstCapacity);
+        }
+    }
+
+    protected final void checkDstIndex(int length, int dstIndex, int dstCapacity) {
+        checkReadableBytes(length);
+        if (checkBounds) {
+            checkRangeBounds("dstIndex", dstIndex, length, dstCapacity);
         }
     }
 
@@ -1414,30 +1460,23 @@ public abstract class AbstractByteBuf extends ByteBuf {
      * than the specified value.
      */
     protected final void checkReadableBytes(int minimumReadableBytes) {
-        if (minimumReadableBytes < 0) {
-            throw new IllegalArgumentException("minimumReadableBytes: " + minimumReadableBytes + " (expected: >= 0)");
-        }
-        checkReadableBytes0(minimumReadableBytes);
+        checkReadableBytes0(checkPositiveOrZero(minimumReadableBytes, "minimumReadableBytes"));
     }
 
     protected final void checkNewCapacity(int newCapacity) {
         ensureAccessible();
-        if (checkBounds) {
-            if (newCapacity < 0 || newCapacity > maxCapacity()) {
-                throw new IllegalArgumentException("newCapacity: " + newCapacity +
-                        " (expected: 0-" + maxCapacity() + ')');
-            }
+        if (checkBounds && (newCapacity < 0 || newCapacity > maxCapacity())) {
+            throw new IllegalArgumentException("newCapacity: " + newCapacity +
+                    " (expected: 0-" + maxCapacity() + ')');
         }
     }
 
     private void checkReadableBytes0(int minimumReadableBytes) {
         ensureAccessible();
-        if (checkBounds) {
-            if (readerIndex > writerIndex - minimumReadableBytes) {
-                throw new IndexOutOfBoundsException(String.format(
-                        "readerIndex(%d) + length(%d) exceeds writerIndex(%d): %s",
-                        readerIndex, minimumReadableBytes, writerIndex, this));
-            }
+        if (checkBounds && readerIndex > writerIndex - minimumReadableBytes) {
+            throw new IndexOutOfBoundsException(String.format(
+                    "readerIndex(%d) + length(%d) exceeds writerIndex(%d): %s",
+                    readerIndex, minimumReadableBytes, writerIndex, this));
         }
     }
 
@@ -1446,19 +1485,11 @@ public abstract class AbstractByteBuf extends ByteBuf {
      * if the buffer was released before.
      */
     protected final void ensureAccessible() {
-        if (checkAccessible && internalRefCnt() == 0) {
+        if (checkAccessible && !isAccessible()) {
             throw new IllegalReferenceCountException(0);
         }
     }
 
-    /**
-     * Returns the reference count that is used internally by {@link #ensureAccessible()} to try to guard
-     * against using the buffer after it was released (best-effort).
-     */
-    int internalRefCnt() {
-        return refCnt();
-    }
-
     final void setIndex0(int readerIndex, int writerIndex) {
         this.readerIndex = readerIndex;
         this.writerIndex = writerIndex;
diff --git a/buffer/src/main/java/io/netty/buffer/AbstractByteBufAllocator.java b/buffer/src/main/java/io/netty/buffer/AbstractByteBufAllocator.java
index 4052514..9e15822 100644
--- a/buffer/src/main/java/io/netty/buffer/AbstractByteBufAllocator.java
+++ b/buffer/src/main/java/io/netty/buffer/AbstractByteBufAllocator.java
@@ -16,6 +16,8 @@
 
 package io.netty.buffer;
 
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
+
 import io.netty.util.ResourceLeakDetector;
 import io.netty.util.ResourceLeakTracker;
 import io.netty.util.internal.PlatformDependent;
@@ -125,7 +127,7 @@ public abstract class AbstractByteBufAllocator implements ByteBufAllocator {
 
     @Override
     public ByteBuf ioBuffer() {
-        if (PlatformDependent.hasUnsafe()) {
+        if (PlatformDependent.hasUnsafe() || isDirectBufferPooled()) {
             return directBuffer(DEFAULT_INITIAL_CAPACITY);
         }
         return heapBuffer(DEFAULT_INITIAL_CAPACITY);
@@ -133,7 +135,7 @@ public abstract class AbstractByteBufAllocator implements ByteBufAllocator {
 
     @Override
     public ByteBuf ioBuffer(int initialCapacity) {
-        if (PlatformDependent.hasUnsafe()) {
+        if (PlatformDependent.hasUnsafe() || isDirectBufferPooled()) {
             return directBuffer(initialCapacity);
         }
         return heapBuffer(initialCapacity);
@@ -141,7 +143,7 @@ public abstract class AbstractByteBufAllocator implements ByteBufAllocator {
 
     @Override
     public ByteBuf ioBuffer(int initialCapacity, int maxCapacity) {
-        if (PlatformDependent.hasUnsafe()) {
+        if (PlatformDependent.hasUnsafe() || isDirectBufferPooled()) {
             return directBuffer(initialCapacity, maxCapacity);
         }
         return heapBuffer(initialCapacity, maxCapacity);
@@ -222,9 +224,7 @@ public abstract class AbstractByteBufAllocator implements ByteBufAllocator {
     }
 
     private static void validate(int initialCapacity, int maxCapacity) {
-        if (initialCapacity < 0) {
-            throw new IllegalArgumentException("initialCapacity: " + initialCapacity + " (expected: 0+)");
-        }
+        checkPositiveOrZero(initialCapacity, "initialCapacity");
         if (initialCapacity > maxCapacity) {
             throw new IllegalArgumentException(String.format(
                     "initialCapacity: %d (expected: not greater than maxCapacity(%d)",
@@ -249,9 +249,7 @@ public abstract class AbstractByteBufAllocator implements ByteBufAllocator {
 
     @Override
     public int calculateNewCapacity(int minNewCapacity, int maxCapacity) {
-        if (minNewCapacity < 0) {
-            throw new IllegalArgumentException("minNewCapacity: " + minNewCapacity + " (expected: 0+)");
-        }
+        checkPositiveOrZero(minNewCapacity, "minNewCapacity");
         if (minNewCapacity > maxCapacity) {
             throw new IllegalArgumentException(String.format(
                     "minNewCapacity: %d (expected: not greater than maxCapacity(%d)",
diff --git a/buffer/src/main/java/io/netty/buffer/AbstractDerivedByteBuf.java b/buffer/src/main/java/io/netty/buffer/AbstractDerivedByteBuf.java
index 58f1d90..3fd10aa 100644
--- a/buffer/src/main/java/io/netty/buffer/AbstractDerivedByteBuf.java
+++ b/buffer/src/main/java/io/netty/buffer/AbstractDerivedByteBuf.java
@@ -31,6 +31,11 @@ public abstract class AbstractDerivedByteBuf extends AbstractByteBuf {
         super(maxCapacity);
     }
 
+    @Override
+    final boolean isAccessible() {
+        return unwrap().isAccessible();
+    }
+
     @Override
     public final int refCnt() {
         return refCnt0();
@@ -112,4 +117,9 @@ public abstract class AbstractDerivedByteBuf extends AbstractByteBuf {
     public ByteBuffer nioBuffer(int index, int length) {
         return unwrap().nioBuffer(index, length);
     }
+
+    @Override
+    public boolean isContiguous() {
+        return unwrap().isContiguous();
+    }
 }
diff --git a/buffer/src/main/java/io/netty/buffer/AbstractPooledDerivedByteBuf.java b/buffer/src/main/java/io/netty/buffer/AbstractPooledDerivedByteBuf.java
index 3388b14..8cf8295 100644
--- a/buffer/src/main/java/io/netty/buffer/AbstractPooledDerivedByteBuf.java
+++ b/buffer/src/main/java/io/netty/buffer/AbstractPooledDerivedByteBuf.java
@@ -16,8 +16,8 @@
 
 package io.netty.buffer;
 
-import io.netty.util.Recycler.Handle;
 import io.netty.util.ReferenceCounted;
+import io.netty.util.internal.ObjectPool.Handle;
 
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
@@ -63,7 +63,7 @@ abstract class AbstractPooledDerivedByteBuf extends AbstractReferenceCountedByte
         try {
             maxCapacity(maxCapacity);
             setIndex0(readerIndex, writerIndex); // It is assumed the bounds checking is done by the caller.
-            setRefCnt(1);
+            resetRefCnt();
 
             @SuppressWarnings("unchecked")
             final U castThis = (U) this;
@@ -123,6 +123,11 @@ abstract class AbstractPooledDerivedByteBuf extends AbstractReferenceCountedByte
         return unwrap().hasMemoryAddress();
     }
 
+    @Override
+    public boolean isContiguous() {
+        return unwrap().isContiguous();
+    }
+
     @Override
     public final int nioBufferCount() {
         return unwrap().nioBufferCount();
diff --git a/buffer/src/main/java/io/netty/buffer/AbstractReferenceCountedByteBuf.java b/buffer/src/main/java/io/netty/buffer/AbstractReferenceCountedByteBuf.java
index ba26b19..22eea0e 100644
--- a/buffer/src/main/java/io/netty/buffer/AbstractReferenceCountedByteBuf.java
+++ b/buffer/src/main/java/io/netty/buffer/AbstractReferenceCountedByteBuf.java
@@ -16,97 +16,73 @@
 
 package io.netty.buffer;
 
-import io.netty.util.IllegalReferenceCountException;
-import io.netty.util.internal.PlatformDependent;
-
 import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
 
-import static io.netty.util.internal.ObjectUtil.checkPositive;
+import io.netty.util.internal.ReferenceCountUpdater;
 
 /**
  * Abstract base class for {@link ByteBuf} implementations that count references.
  */
 public abstract class AbstractReferenceCountedByteBuf extends AbstractByteBuf {
-    private static final long REFCNT_FIELD_OFFSET;
-    private static final AtomicIntegerFieldUpdater<AbstractReferenceCountedByteBuf> refCntUpdater =
+    private static final long REFCNT_FIELD_OFFSET =
+            ReferenceCountUpdater.getUnsafeOffset(AbstractReferenceCountedByteBuf.class, "refCnt");
+    private static final AtomicIntegerFieldUpdater<AbstractReferenceCountedByteBuf> AIF_UPDATER =
             AtomicIntegerFieldUpdater.newUpdater(AbstractReferenceCountedByteBuf.class, "refCnt");
 
-    // even => "real" refcount is (refCnt >>> 1); odd => "real" refcount is 0
-    @SuppressWarnings("unused")
-    private volatile int refCnt = 2;
-
-    static {
-        long refCntFieldOffset = -1;
-        try {
-            if (PlatformDependent.hasUnsafe()) {
-                refCntFieldOffset = PlatformDependent.objectFieldOffset(
-                        AbstractReferenceCountedByteBuf.class.getDeclaredField("refCnt"));
-            }
-        } catch (Throwable ignore) {
-            refCntFieldOffset = -1;
+    private static final ReferenceCountUpdater<AbstractReferenceCountedByteBuf> updater =
+            new ReferenceCountUpdater<AbstractReferenceCountedByteBuf>() {
+        @Override
+        protected AtomicIntegerFieldUpdater<AbstractReferenceCountedByteBuf> updater() {
+            return AIF_UPDATER;
         }
+        @Override
+        protected long unsafeOffset() {
+            return REFCNT_FIELD_OFFSET;
+        }
+    };
 
-        REFCNT_FIELD_OFFSET = refCntFieldOffset;
-    }
-
-    private static int realRefCnt(int rawCnt) {
-        return (rawCnt & 1) != 0 ? 0 : rawCnt >>> 1;
-    }
+    // Value might not equal "real" reference count, all access should be via the updater
+    @SuppressWarnings("unused")
+    private volatile int refCnt = updater.initialValue();
 
     protected AbstractReferenceCountedByteBuf(int maxCapacity) {
         super(maxCapacity);
     }
 
-    private int nonVolatileRawCnt() {
-        // TODO: Once we compile against later versions of Java we can replace the Unsafe usage here by varhandles.
-        return REFCNT_FIELD_OFFSET != -1 ? PlatformDependent.getInt(this, REFCNT_FIELD_OFFSET)
-                : refCntUpdater.get(this);
-    }
-
     @Override
-    int internalRefCnt() {
+    boolean isAccessible() {
         // Try to do non-volatile read for performance as the ensureAccessible() is racy anyway and only provide
         // a best-effort guard.
-        return realRefCnt(nonVolatileRawCnt());
+        return updater.isLiveNonVolatile(this);
     }
 
     @Override
     public int refCnt() {
-        return realRefCnt(refCntUpdater.get(this));
+        return updater.refCnt(this);
     }
 
     /**
      * An unsafe operation intended for use by a subclass that sets the reference count of the buffer directly
      */
-    protected final void setRefCnt(int newRefCnt) {
-        refCntUpdater.set(this, newRefCnt << 1); // overflow OK here
+    protected final void setRefCnt(int refCnt) {
+        updater.setRefCnt(this, refCnt);
+    }
+
+    /**
+     * An unsafe operation intended for use by a subclass that resets the reference count of the buffer to 1
+     */
+    protected final void resetRefCnt() {
+        updater.resetRefCnt(this);
     }
 
     @Override
     public ByteBuf retain() {
-        return retain0(1);
+        return updater.retain(this);
     }
 
     @Override
     public ByteBuf retain(int increment) {
-        return retain0(checkPositive(increment, "increment"));
-    }
-
-    private ByteBuf retain0(final int increment) {
-        // all changes to the raw count are 2x the "real" change
-        int adjustedIncrement = increment << 1; // overflow OK here
-        int oldRef = refCntUpdater.getAndAdd(this, adjustedIncrement);
-        if ((oldRef & 1) != 0) {
-            throw new IllegalReferenceCountException(0, increment);
-        }
-        // don't pass 0!
-        if ((oldRef <= 0 && oldRef + adjustedIncrement >= 0)
-                || (oldRef >= 0 && oldRef + adjustedIncrement < oldRef)) {
-            // overflow case
-            refCntUpdater.getAndAdd(this, -adjustedIncrement);
-            throw new IllegalReferenceCountException(realRefCnt(oldRef), increment);
-        }
-        return this;
+        return updater.retain(this, increment);
     }
 
     @Override
@@ -121,64 +97,19 @@ public abstract class AbstractReferenceCountedByteBuf extends AbstractByteBuf {
 
     @Override
     public boolean release() {
-        return release0(1);
+        return handleRelease(updater.release(this));
     }
 
     @Override
     public boolean release(int decrement) {
-        return release0(checkPositive(decrement, "decrement"));
-    }
-
-    private boolean release0(int decrement) {
-        int rawCnt = nonVolatileRawCnt(), realCnt = toLiveRealCnt(rawCnt, decrement);
-        if (decrement == realCnt) {
-            if (refCntUpdater.compareAndSet(this, rawCnt, 1)) {
-                deallocate();
-                return true;
-            }
-            return retryRelease0(decrement);
-        }
-        return releaseNonFinal0(decrement, rawCnt, realCnt);
-    }
-
-    private boolean releaseNonFinal0(int decrement, int rawCnt, int realCnt) {
-        if (decrement < realCnt
-                // all changes to the raw count are 2x the "real" change
-                && refCntUpdater.compareAndSet(this, rawCnt, rawCnt - (decrement << 1))) {
-            return false;
-        }
-        return retryRelease0(decrement);
+        return handleRelease(updater.release(this, decrement));
     }
 
-    private boolean retryRelease0(int decrement) {
-        for (;;) {
-            int rawCnt = refCntUpdater.get(this), realCnt = toLiveRealCnt(rawCnt, decrement);
-            if (decrement == realCnt) {
-                if (refCntUpdater.compareAndSet(this, rawCnt, 1)) {
-                    deallocate();
-                    return true;
-                }
-            } else if (decrement < realCnt) {
-                // all changes to the raw count are 2x the "real" change
-                if (refCntUpdater.compareAndSet(this, rawCnt, rawCnt - (decrement << 1))) {
-                    return false;
-                }
-            } else {
-                throw new IllegalReferenceCountException(realCnt, -decrement);
-            }
-            Thread.yield(); // this benefits throughput under high contention
-        }
-    }
-
-    /**
-     * Like {@link #realRefCnt(int)} but throws if refCnt == 0
-     */
-    private static int toLiveRealCnt(int rawCnt, int decrement) {
-        if ((rawCnt & 1) == 0) {
-            return rawCnt >>> 1;
+    private boolean handleRelease(boolean result) {
+        if (result) {
+            deallocate();
         }
-        // odd rawCnt => already deallocated
-        throw new IllegalReferenceCountException(0, -decrement);
+        return result;
     }
 
     /**
diff --git a/buffer/src/main/java/io/netty/buffer/AdvancedLeakAwareCompositeByteBuf.java b/buffer/src/main/java/io/netty/buffer/AdvancedLeakAwareCompositeByteBuf.java
index ff93de6..5b36354 100644
--- a/buffer/src/main/java/io/netty/buffer/AdvancedLeakAwareCompositeByteBuf.java
+++ b/buffer/src/main/java/io/netty/buffer/AdvancedLeakAwareCompositeByteBuf.java
@@ -939,6 +939,12 @@ final class AdvancedLeakAwareCompositeByteBuf extends SimpleLeakAwareCompositeBy
         return super.addComponent(increaseWriterIndex, cIndex, buffer);
     }
 
+    @Override
+    public CompositeByteBuf addFlattenedComponents(boolean increaseWriterIndex, ByteBuf buffer) {
+        recordLeakNonRefCountingOperation(leak);
+        return super.addFlattenedComponents(increaseWriterIndex, buffer);
+    }
+
     @Override
     public CompositeByteBuf removeComponent(int cIndex) {
         recordLeakNonRefCountingOperation(leak);
diff --git a/buffer/src/main/java/io/netty/buffer/ByteBuf.java b/buffer/src/main/java/io/netty/buffer/ByteBuf.java
index 2ac7e98..850c85a 100644
--- a/buffer/src/main/java/io/netty/buffer/ByteBuf.java
+++ b/buffer/src/main/java/io/netty/buffer/ByteBuf.java
@@ -245,7 +245,6 @@ import java.nio.charset.UnsupportedCharsetException;
  * Please refer to {@link ByteBufInputStream} and
  * {@link ByteBufOutputStream}.
  */
-@SuppressWarnings("ClassMayBeInterface")
 public abstract class ByteBuf implements ReferenceCounted, Comparable<ByteBuf> {
 
     /**
@@ -422,6 +421,15 @@ public abstract class ByteBuf implements ReferenceCounted, Comparable<ByteBuf> {
      */
     public abstract int maxWritableBytes();
 
+    /**
+     * Returns the maximum number of bytes which can be written for certain without involving
+     * an internal reallocation or data-copy. The returned value will be &ge; {@link #writableBytes()}
+     * and &le; {@link #maxWritableBytes()}.
+     */
+    public int maxFastWritableBytes() {
+        return writableBytes();
+    }
+
     /**
      * Returns {@code true}
      * if and only if {@code (this.writerIndex - this.readerIndex)} is greater
@@ -2052,11 +2060,14 @@ public abstract class ByteBuf implements ReferenceCounted, Comparable<ByteBuf> {
 
     /**
      * Locates the first occurrence of the specified {@code value} in this
-     * buffer.  The search takes place from the specified {@code fromIndex}
-     * (inclusive)  to the specified {@code toIndex} (exclusive).
+     * buffer. The search takes place from the specified {@code fromIndex}
+     * (inclusive) to the specified {@code toIndex} (exclusive).
      * <p>
      * If {@code fromIndex} is greater than {@code toIndex}, the search is
-     * performed in a reversed order.
+     * performed in a reversed order from {@code fromIndex} (exclusive)
+     * down to {@code toIndex} (inclusive).
+     * <p>
+     * Note that the lower index is always included and higher always excluded.
      * <p>
      * This method does not modify {@code readerIndex} or {@code writerIndex} of
      * this buffer.
@@ -2372,6 +2383,19 @@ public abstract class ByteBuf implements ReferenceCounted, Comparable<ByteBuf> {
      */
     public abstract long memoryAddress();
 
+    /**
+     * Returns {@code true} if this {@link ByteBuf} implementation is backed by a single memory region.
+     * Composite buffer implementations must return false even if they currently hold &le; 1 components.
+     * For buffers that return {@code true}, it's guaranteed that a successful call to {@link #discardReadBytes()}
+     * will increase the value of {@link #maxFastWritableBytes()} by the current {@code readerIndex}.
+     * <p>
+     * This method will return {@code false} by default, and a {@code false} return value does not necessarily
+     * mean that the implementation is composite or that it is <i>not</i> backed by a single memory region.
+     */
+    public boolean isContiguous() {
+        return false;
+    }
+
     /**
      * Decodes this buffer's readable bytes into a string with the specified
      * character set name.  This method is identical to
@@ -2445,4 +2469,12 @@ public abstract class ByteBuf implements ReferenceCounted, Comparable<ByteBuf> {
 
     @Override
     public abstract ByteBuf touch(Object hint);
+
+    /**
+     * Used internally by {@link AbstractByteBuf#ensureAccessible()} to try to guard
+     * against using the buffer after it was released (best-effort).
+     */
+    boolean isAccessible() {
+        return refCnt() != 0;
+    }
 }
diff --git a/buffer/src/main/java/io/netty/buffer/ByteBufInputStream.java b/buffer/src/main/java/io/netty/buffer/ByteBufInputStream.java
index 038cd8d..5b371fd 100644
--- a/buffer/src/main/java/io/netty/buffer/ByteBufInputStream.java
+++ b/buffer/src/main/java/io/netty/buffer/ByteBufInputStream.java
@@ -16,6 +16,7 @@
 package io.netty.buffer;
 
 import io.netty.util.ReferenceCounted;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.StringUtil;
 
 import java.io.DataInput;
@@ -102,9 +103,7 @@ public class ByteBufInputStream extends InputStream implements DataInput {
      *            {@code writerIndex}
      */
     public ByteBufInputStream(ByteBuf buffer, int length, boolean releaseOnClose) {
-        if (buffer == null) {
-            throw new NullPointerException("buffer");
-        }
+        ObjectUtil.checkNotNull(buffer, "buffer");
         if (length < 0) {
             if (releaseOnClose) {
                 buffer.release();
@@ -163,7 +162,8 @@ public class ByteBufInputStream extends InputStream implements DataInput {
 
     @Override
     public int read() throws IOException {
-        if (!buffer.isReadable()) {
+        int available = available();
+        if (available == 0) {
             return -1;
         }
         return buffer.readByte() & 0xff;
@@ -203,7 +203,8 @@ public class ByteBufInputStream extends InputStream implements DataInput {
 
     @Override
     public byte readByte() throws IOException {
-        if (!buffer.isReadable()) {
+        int available = available();
+        if (available == 0) {
             throw new EOFException();
         }
         return buffer.readByte();
@@ -245,22 +246,26 @@ public class ByteBufInputStream extends InputStream implements DataInput {
 
     @Override
     public String readLine() throws IOException {
-        if (!buffer.isReadable()) {
+        int available = available();
+        if (available == 0) {
             return null;
         }
+
         if (lineBuf != null) {
             lineBuf.setLength(0);
         }
 
         loop: do {
             int c = buffer.readUnsignedByte();
+            --available;
             switch (c) {
                 case '\n':
                     break loop;
 
                 case '\r':
-                    if (buffer.isReadable() && (char) buffer.getUnsignedByte(buffer.readerIndex()) == '\n') {
+                    if (available > 0 && (char) buffer.getUnsignedByte(buffer.readerIndex()) == '\n') {
                         buffer.skipBytes(1);
+                        --available;
                     }
                     break loop;
 
@@ -270,7 +275,7 @@ public class ByteBufInputStream extends InputStream implements DataInput {
                     }
                     lineBuf.append((char) c);
             }
-        } while (buffer.isReadable());
+        } while (available > 0);
 
         return lineBuf != null && lineBuf.length() > 0 ? lineBuf.toString() : StringUtil.EMPTY_STRING;
     }
diff --git a/buffer/src/main/java/io/netty/buffer/ByteBufOutputStream.java b/buffer/src/main/java/io/netty/buffer/ByteBufOutputStream.java
index c4f7805..d874dc6 100644
--- a/buffer/src/main/java/io/netty/buffer/ByteBufOutputStream.java
+++ b/buffer/src/main/java/io/netty/buffer/ByteBufOutputStream.java
@@ -16,6 +16,7 @@
 package io.netty.buffer;
 
 import io.netty.util.CharsetUtil;
+import io.netty.util.internal.ObjectUtil;
 
 import java.io.DataOutput;
 import java.io.DataOutputStream;
@@ -45,10 +46,7 @@ public class ByteBufOutputStream extends OutputStream implements DataOutput {
      * Creates a new stream which writes data to the specified {@code buffer}.
      */
     public ByteBufOutputStream(ByteBuf buffer) {
-        if (buffer == null) {
-            throw new NullPointerException("buffer");
-        }
-        this.buffer = buffer;
+        this.buffer = ObjectUtil.checkNotNull(buffer, "buffer");
         startIndex = buffer.writerIndex();
     }
 
diff --git a/buffer/src/main/java/io/netty/buffer/ByteBufUtil.java b/buffer/src/main/java/io/netty/buffer/ByteBufUtil.java
index d2b2ec3..9aed166 100644
--- a/buffer/src/main/java/io/netty/buffer/ByteBufUtil.java
+++ b/buffer/src/main/java/io/netty/buffer/ByteBufUtil.java
@@ -18,9 +18,11 @@ package io.netty.buffer;
 import io.netty.util.AsciiString;
 import io.netty.util.ByteProcessor;
 import io.netty.util.CharsetUtil;
-import io.netty.util.Recycler;
-import io.netty.util.Recycler.Handle;
 import io.netty.util.concurrent.FastThreadLocal;
+import io.netty.util.internal.MathUtil;
+import io.netty.util.internal.ObjectPool;
+import io.netty.util.internal.ObjectPool.Handle;
+import io.netty.util.internal.ObjectPool.ObjectCreator;
 import io.netty.util.internal.PlatformDependent;
 import io.netty.util.internal.StringUtil;
 import io.netty.util.internal.SystemPropertyUtil;
@@ -43,6 +45,7 @@ import java.util.Locale;
 
 import static io.netty.util.internal.MathUtil.isOutOfBounds;
 import static io.netty.util.internal.ObjectUtil.checkNotNull;
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
 import static io.netty.util.internal.StringUtil.NEWLINE;
 import static io.netty.util.internal.StringUtil.isSurrogate;
 
@@ -471,6 +474,14 @@ public final class ByteBufUtil {
         return buffer.forEachByteDesc(toIndex, fromIndex - toIndex, new ByteProcessor.IndexOfProcessor(value));
     }
 
+    private static CharSequence checkCharSequenceBounds(CharSequence seq, int start, int end) {
+        if (MathUtil.isOutOfBounds(start, end - start, seq.length())) {
+            throw new IndexOutOfBoundsException("expected: 0 <= start(" + start + ") <= end (" + end
+                    + ") <= seq.length(" + seq.length() + ')');
+        }
+        return seq;
+    }
+
     /**
      * Encode a {@link CharSequence} in <a href="http://en.wikipedia.org/wiki/UTF-8">UTF-8</a> and write
      * it to a {@link ByteBuf} allocated with {@code alloc}.
@@ -495,7 +506,17 @@ public final class ByteBufUtil {
      * This method returns the actual number of bytes written.
      */
     public static int writeUtf8(ByteBuf buf, CharSequence seq) {
-        return reserveAndWriteUtf8(buf, seq, utf8MaxBytes(seq));
+        int seqLength = seq.length();
+        return reserveAndWriteUtf8Seq(buf, seq, 0, seqLength, utf8MaxBytes(seqLength));
+    }
+
+    /**
+     * Equivalent to <code>{@link #writeUtf8(ByteBuf, CharSequence) writeUtf8(buf, seq.subSequence(start, end))}</code>
+     * but avoids subsequence object allocation.
+     */
+    public static int writeUtf8(ByteBuf buf, CharSequence seq, int start, int end) {
+        checkCharSequenceBounds(seq, start, end);
+        return reserveAndWriteUtf8Seq(buf, seq, start, end, utf8MaxBytes(end - start));
     }
 
     /**
@@ -508,6 +529,21 @@ public final class ByteBufUtil {
      * This method returns the actual number of bytes written.
      */
     public static int reserveAndWriteUtf8(ByteBuf buf, CharSequence seq, int reserveBytes) {
+        return reserveAndWriteUtf8Seq(buf, seq, 0, seq.length(), reserveBytes);
+    }
+
+    /**
+     * Equivalent to <code>{@link #reserveAndWriteUtf8(ByteBuf, CharSequence, int)
+     * reserveAndWriteUtf8(buf, seq.subSequence(start, end), reserveBytes)}</code> but avoids
+     * subsequence object allocation if possible.
+     *
+     * @return actual number of bytes written
+     */
+    public static int reserveAndWriteUtf8(ByteBuf buf, CharSequence seq, int start, int end, int reserveBytes) {
+        return reserveAndWriteUtf8Seq(buf, checkCharSequenceBounds(seq, start, end), start, end, reserveBytes);
+    }
+
+    private static int reserveAndWriteUtf8Seq(ByteBuf buf, CharSequence seq, int start, int end, int reserveBytes) {
         for (;;) {
             if (buf instanceof WrappedCompositeByteBuf) {
                 // WrappedCompositeByteBuf is a sub-class of AbstractByteBuf so it needs special handling.
@@ -515,27 +551,31 @@ public final class ByteBufUtil {
             } else if (buf instanceof AbstractByteBuf) {
                 AbstractByteBuf byteBuf = (AbstractByteBuf) buf;
                 byteBuf.ensureWritable0(reserveBytes);
-                int written = writeUtf8(byteBuf, byteBuf.writerIndex, seq, seq.length());
+                int written = writeUtf8(byteBuf, byteBuf.writerIndex, seq, start, end);
                 byteBuf.writerIndex += written;
                 return written;
             } else if (buf instanceof WrappedByteBuf) {
                 // Unwrap as the wrapped buffer may be an AbstractByteBuf and so we can use fast-path.
                 buf = buf.unwrap();
             } else {
-                byte[] bytes = seq.toString().getBytes(CharsetUtil.UTF_8);
+                byte[] bytes = seq.subSequence(start, end).toString().getBytes(CharsetUtil.UTF_8);
                 buf.writeBytes(bytes);
                 return bytes.length;
             }
         }
     }
 
-    // Fast-Path implementation
     static int writeUtf8(AbstractByteBuf buffer, int writerIndex, CharSequence seq, int len) {
+        return writeUtf8(buffer, writerIndex, seq, 0, len);
+    }
+
+    // Fast-Path implementation
+    static int writeUtf8(AbstractByteBuf buffer, int writerIndex, CharSequence seq, int start, int end) {
         int oldWriterIndex = writerIndex;
 
         // We can use the _set methods as these not need to do any index checks and reference checks.
         // This is possible as we called ensureWritable(...) before.
-        for (int i = 0; i < len; i++) {
+        for (int i = start; i < end; i++) {
             char c = seq.charAt(i);
             if (c < 0x80) {
                 buffer._setByte(writerIndex++, (byte) c);
@@ -547,18 +587,13 @@ public final class ByteBufUtil {
                     buffer._setByte(writerIndex++, WRITE_UTF_UNKNOWN);
                     continue;
                 }
-                final char c2;
-                try {
-                    // Surrogate Pair consumes 2 characters. Optimistically try to get the next character to avoid
-                    // duplicate bounds checking with charAt. If an IndexOutOfBoundsException is thrown we will
-                    // re-throw a more informative exception describing the problem.
-                    c2 = seq.charAt(++i);
-                } catch (IndexOutOfBoundsException ignored) {
+                // Surrogate Pair consumes 2 characters.
+                if (++i == end) {
                     buffer._setByte(writerIndex++, WRITE_UTF_UNKNOWN);
                     break;
                 }
                 // Extra method to allow inlining the rest of writeUtf8 which is the most likely code path.
-                writerIndex = writeUtf8Surrogate(buffer, writerIndex, c, c2);
+                writerIndex = writeUtf8Surrogate(buffer, writerIndex, c, seq.charAt(i));
             } else {
                 buffer._setByte(writerIndex++, (byte) (0xe0 | (c >> 12)));
                 buffer._setByte(writerIndex++, (byte) (0x80 | ((c >> 6) & 0x3f)));
@@ -605,22 +640,35 @@ public final class ByteBufUtil {
      * This method is producing the exact length according to {@link #writeUtf8(ByteBuf, CharSequence)}.
      */
     public static int utf8Bytes(final CharSequence seq) {
+        return utf8ByteCount(seq, 0, seq.length());
+    }
+
+    /**
+     * Equivalent to <code>{@link #utf8Bytes(CharSequence) utf8Bytes(seq.subSequence(start, end))}</code>
+     * but avoids subsequence object allocation.
+     * <p>
+     * This method is producing the exact length according to {@link #writeUtf8(ByteBuf, CharSequence, int, int)}.
+     */
+    public static int utf8Bytes(final CharSequence seq, int start, int end) {
+        return utf8ByteCount(checkCharSequenceBounds(seq, start, end), start, end);
+    }
+
+    private static int utf8ByteCount(final CharSequence seq, int start, int end) {
         if (seq instanceof AsciiString) {
-            return seq.length();
+            return end - start;
         }
-        int seqLength = seq.length();
-        int i = 0;
+        int i = start;
         // ASCII fast path
-        while (i < seqLength && seq.charAt(i) < 0x80) {
+        while (i < end && seq.charAt(i) < 0x80) {
             ++i;
         }
         // !ASCII is packed in a separate method to let the ASCII case be smaller
-        return i < seqLength ? i + utf8Bytes(seq, i, seqLength) : i;
+        return i < end ? (i - start) + utf8BytesNonAscii(seq, i, end) : i - start;
     }
 
-    private static int utf8Bytes(final CharSequence seq, final int start, final int length) {
+    private static int utf8BytesNonAscii(final CharSequence seq, final int start, final int end) {
         int encodedLength = 0;
-        for (int i = start; i < length; i++) {
+        for (int i = start; i < end; i++) {
             final char c = seq.charAt(i);
             // making it 100% branchless isn't rewarding due to the many bit operations necessary!
             if (c < 0x800) {
@@ -632,17 +680,13 @@ public final class ByteBufUtil {
                     // WRITE_UTF_UNKNOWN
                     continue;
                 }
-                final char c2;
-                try {
-                    // Surrogate Pair consumes 2 characters. Optimistically try to get the next character to avoid
-                    // duplicate bounds checking with charAt.
-                    c2 = seq.charAt(++i);
-                } catch (IndexOutOfBoundsException ignored) {
+                // Surrogate Pair consumes 2 characters.
+                if (++i == end) {
                     encodedLength++;
                     // WRITE_UTF_UNKNOWN
                     break;
                 }
-                if (!Character.isLowSurrogate(c2)) {
+                if (!Character.isLowSurrogate(seq.charAt(i))) {
                     // WRITE_UTF_UNKNOWN + (Character.isHighSurrogate(c2) ? WRITE_UTF_UNKNOWN : c2)
                     encodedLength += 2;
                     continue;
@@ -1000,9 +1044,7 @@ public final class ByteBufUtil {
         }
 
         private static String hexDump(ByteBuf buffer, int fromIndex, int length) {
-            if (length < 0) {
-              throw new IllegalArgumentException("length: " + length);
-            }
+            checkPositiveOrZero(length, "length");
             if (length == 0) {
               return "";
             }
@@ -1022,9 +1064,7 @@ public final class ByteBufUtil {
         }
 
         private static String hexDump(byte[] array, int fromIndex, int length) {
-            if (length < 0) {
-              throw new IllegalArgumentException("length: " + length);
-            }
+            checkPositiveOrZero(length, "length");
             if (length == 0) {
                 return "";
             }
@@ -1047,7 +1087,7 @@ public final class ByteBufUtil {
             if (length == 0) {
               return StringUtil.EMPTY_STRING;
             } else {
-                int rows = length / 16 + (length % 15 == 0? 0 : 1) + 4;
+                int rows = length / 16 + ((length & 15) == 0? 0 : 1) + 4;
                 StringBuilder buf = new StringBuilder(rows * 80);
                 appendPrettyHexDump(buf, buffer, offset, length);
                 return buf.toString();
@@ -1132,17 +1172,17 @@ public final class ByteBufUtil {
 
     static final class ThreadLocalUnsafeDirectByteBuf extends UnpooledUnsafeDirectByteBuf {
 
-        private static final Recycler<ThreadLocalUnsafeDirectByteBuf> RECYCLER =
-                new Recycler<ThreadLocalUnsafeDirectByteBuf>() {
+        private static final ObjectPool<ThreadLocalUnsafeDirectByteBuf> RECYCLER =
+                ObjectPool.newPool(new ObjectCreator<ThreadLocalUnsafeDirectByteBuf>() {
                     @Override
-                    protected ThreadLocalUnsafeDirectByteBuf newObject(Handle<ThreadLocalUnsafeDirectByteBuf> handle) {
+                    public ThreadLocalUnsafeDirectByteBuf newObject(Handle<ThreadLocalUnsafeDirectByteBuf> handle) {
                         return new ThreadLocalUnsafeDirectByteBuf(handle);
                     }
-                };
+                });
 
         static ThreadLocalUnsafeDirectByteBuf newInstance() {
             ThreadLocalUnsafeDirectByteBuf buf = RECYCLER.get();
-            buf.setRefCnt(1);
+            buf.resetRefCnt();
             return buf;
         }
 
@@ -1166,16 +1206,17 @@ public final class ByteBufUtil {
 
     static final class ThreadLocalDirectByteBuf extends UnpooledDirectByteBuf {
 
-        private static final Recycler<ThreadLocalDirectByteBuf> RECYCLER = new Recycler<ThreadLocalDirectByteBuf>() {
+        private static final ObjectPool<ThreadLocalDirectByteBuf> RECYCLER = ObjectPool.newPool(
+                new ObjectCreator<ThreadLocalDirectByteBuf>() {
             @Override
-            protected ThreadLocalDirectByteBuf newObject(Handle<ThreadLocalDirectByteBuf> handle) {
+            public ThreadLocalDirectByteBuf newObject(Handle<ThreadLocalDirectByteBuf> handle) {
                 return new ThreadLocalDirectByteBuf(handle);
             }
-        };
+        });
 
         static ThreadLocalDirectByteBuf newInstance() {
             ThreadLocalDirectByteBuf buf = RECYCLER.get();
-            buf.setRefCnt(1);
+            buf.resetRefCnt();
             return buf;
         }
 
diff --git a/buffer/src/main/java/io/netty/buffer/CompositeByteBuf.java b/buffer/src/main/java/io/netty/buffer/CompositeByteBuf.java
index 96c14cd..f8cae7b 100644
--- a/buffer/src/main/java/io/netty/buffer/CompositeByteBuf.java
+++ b/buffer/src/main/java/io/netty/buffer/CompositeByteBuf.java
@@ -19,6 +19,7 @@ import io.netty.util.ByteProcessor;
 import io.netty.util.IllegalReferenceCountException;
 import io.netty.util.ReferenceCountUtil;
 import io.netty.util.internal.EmptyArrays;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.RecyclableArrayList;
 
 import java.io.IOException;
@@ -61,14 +62,13 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
 
     private CompositeByteBuf(ByteBufAllocator alloc, boolean direct, int maxNumComponents, int initSize) {
         super(AbstractByteBufAllocator.DEFAULT_MAX_CAPACITY);
-        if (alloc == null) {
-            throw new NullPointerException("alloc");
-        }
+
+        this.alloc = ObjectUtil.checkNotNull(alloc, "alloc");
         if (maxNumComponents < 1) {
             throw new IllegalArgumentException(
                     "maxNumComponents: " + maxNumComponents + " (expected: >= 1)");
         }
-        this.alloc = alloc;
+
         this.direct = direct;
         this.maxNumComponents = maxNumComponents;
         components = newCompArray(initSize, maxNumComponents);
@@ -96,8 +96,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
         this(alloc, direct, maxNumComponents,
                 buffers instanceof Collection ? ((Collection<ByteBuf>) buffers).size() : 0);
 
-        addComponents0(false, 0, buffers);
-        consolidateIfNeeded();
+        addComponents(false, 0, buffers);
         setIndex(0, capacity());
     }
 
@@ -220,10 +219,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
      * {@link CompositeByteBuf}.
      */
     public CompositeByteBuf addComponent(boolean increaseWriterIndex, ByteBuf buffer) {
-        checkNotNull(buffer, "buffer");
-        addComponent0(increaseWriterIndex, componentCount, buffer);
-        consolidateIfNeeded();
-        return this;
+        return addComponent(increaseWriterIndex, componentCount, buffer);
     }
 
     /**
@@ -252,9 +248,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
      * ownership of all {@link ByteBuf} objects is transferred to this {@link CompositeByteBuf}.
      */
     public CompositeByteBuf addComponents(boolean increaseWriterIndex, Iterable<ByteBuf> buffers) {
-        addComponents0(increaseWriterIndex, componentCount, buffers);
-        consolidateIfNeeded();
-        return this;
+        return addComponents(increaseWriterIndex, componentCount, buffers);
     }
 
     /**
@@ -283,7 +277,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
             checkComponentIndex(cIndex);
 
             // No need to consolidate - just add a component to the list.
-            Component c = newComponent(buffer, 0);
+            Component c = newComponent(ensureAccessible(buffer), 0);
             int readableBytes = c.length();
 
             addComp(cIndex, c);
@@ -294,7 +288,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
                 c.reposition(components[cIndex - 1].endOffset);
             }
             if (increaseWriterIndex) {
-                writerIndex(writerIndex() + readableBytes);
+                writerIndex += readableBytes;
             }
             return cIndex;
         } finally {
@@ -304,24 +298,42 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
         }
     }
 
-    // unwrap if already sliced
-    @SuppressWarnings("deprecation")
-    private Component newComponent(ByteBuf buf, int offset) {
-        if (checkAccessible && buf.refCnt() == 0) {
+    private static ByteBuf ensureAccessible(final ByteBuf buf) {
+        if (checkAccessible && !buf.isAccessible()) {
             throw new IllegalReferenceCountException(0);
         }
-        int srcIndex = buf.readerIndex(), len = buf.readableBytes();
-        ByteBuf slice = null;
-        if (buf instanceof AbstractUnpooledSlicedByteBuf) {
-            srcIndex += ((AbstractUnpooledSlicedByteBuf) buf).idx(0);
-            slice = buf;
-            buf = buf.unwrap();
-        } else if (buf instanceof PooledSlicedByteBuf) {
-            srcIndex += ((PooledSlicedByteBuf) buf).adjustment;
-            slice = buf;
-            buf = buf.unwrap();
+        return buf;
+    }
+
+    @SuppressWarnings("deprecation")
+    private Component newComponent(final ByteBuf buf, final int offset) {
+        final int srcIndex = buf.readerIndex();
+        final int len = buf.readableBytes();
+
+        // unpeel any intermediate outer layers (UnreleasableByteBuf, LeakAwareByteBufs, SwappedByteBuf)
+        ByteBuf unwrapped = buf;
+        int unwrappedIndex = srcIndex;
+        while (unwrapped instanceof WrappedByteBuf || unwrapped instanceof SwappedByteBuf) {
+            unwrapped = unwrapped.unwrap();
+        }
+
+        // unwrap if already sliced
+        if (unwrapped instanceof AbstractUnpooledSlicedByteBuf) {
+            unwrappedIndex += ((AbstractUnpooledSlicedByteBuf) unwrapped).idx(0);
+            unwrapped = unwrapped.unwrap();
+        } else if (unwrapped instanceof PooledSlicedByteBuf) {
+            unwrappedIndex += ((PooledSlicedByteBuf) unwrapped).adjustment;
+            unwrapped = unwrapped.unwrap();
+        } else if (unwrapped instanceof DuplicatedByteBuf || unwrapped instanceof PooledDuplicatedByteBuf) {
+            unwrapped = unwrapped.unwrap();
         }
-        return new Component(buf.order(ByteOrder.BIG_ENDIAN), srcIndex, offset, len, slice);
+
+        // We don't need to slice later to expose the internal component if the readable range
+        // is already the entire buffer
+        final ByteBuf slice = buf.capacity() == len ? buf : null;
+
+        return new Component(buf.order(ByteOrder.BIG_ENDIAN), srcIndex,
+                unwrapped.order(ByteOrder.BIG_ENDIAN), unwrappedIndex, offset, len, slice);
     }
 
     /**
@@ -345,20 +357,25 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
         return this;
     }
 
-    private int addComponents0(boolean increaseWriterIndex, final int cIndex, ByteBuf[] buffers, int arrOffset) {
+    private CompositeByteBuf addComponents0(boolean increaseWriterIndex,
+            final int cIndex, ByteBuf[] buffers, int arrOffset) {
         final int len = buffers.length, count = len - arrOffset;
+        // only set ci after we've shifted so that finally block logic is always correct
         int ci = Integer.MAX_VALUE;
         try {
             checkComponentIndex(cIndex);
             shiftComps(cIndex, count); // will increase componentCount
-            ci = cIndex; // only set this after we've shifted so that finally block logic is always correct
             int nextOffset = cIndex > 0 ? components[cIndex - 1].endOffset : 0;
-            for (ByteBuf b; arrOffset < len && (b = buffers[arrOffset]) != null; arrOffset++, ci++) {
-                Component c = newComponent(b, nextOffset);
+            for (ci = cIndex; arrOffset < len; arrOffset++, ci++) {
+                ByteBuf b = buffers[arrOffset];
+                if (b == null) {
+                    break;
+                }
+                Component c = newComponent(ensureAccessible(b), nextOffset);
                 components[ci] = c;
                 nextOffset = c.endOffset;
             }
-            return ci;
+            return this;
         } finally {
             // ci is now the index following the last successfully added component
             if (ci < componentCount) {
@@ -372,14 +389,13 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
                 updateComponentOffsets(ci); // only need to do this here for components after the added ones
             }
             if (increaseWriterIndex && ci > cIndex && ci <= componentCount) {
-                writerIndex(writerIndex() + components[ci - 1].endOffset - components[cIndex].offset);
+                writerIndex += components[ci - 1].endOffset - components[cIndex].offset;
             }
         }
     }
 
     private <T> int addComponents0(boolean increaseWriterIndex, int cIndex,
             ByteWrapper<T> wrapper, T[] buffers, int offset) {
-        checkNotNull(buffers, "buffers");
         checkComponentIndex(cIndex);
 
         // No need for consolidation
@@ -413,18 +429,82 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
      * {@link CompositeByteBuf}.
      */
     public CompositeByteBuf addComponents(int cIndex, Iterable<ByteBuf> buffers) {
-        addComponents0(false, cIndex, buffers);
-        consolidateIfNeeded();
-        return this;
+        return addComponents(false, cIndex, buffers);
+    }
+
+    /**
+     * Add the given {@link ByteBuf} and increase the {@code writerIndex} if {@code increaseWriterIndex} is
+     * {@code true}. If the provided buffer is a {@link CompositeByteBuf} itself, a "shallow copy" of its
+     * readable components will be performed. Thus the actual number of new components added may vary
+     * and in particular will be zero if the provided buffer is not readable.
+     * <p>
+     * {@link ByteBuf#release()} ownership of {@code buffer} is transferred to this {@link CompositeByteBuf}.
+     * @param buffer the {@link ByteBuf} to add. {@link ByteBuf#release()} ownership is transferred to this
+     * {@link CompositeByteBuf}.
+     */
+    public CompositeByteBuf addFlattenedComponents(boolean increaseWriterIndex, ByteBuf buffer) {
+        checkNotNull(buffer, "buffer");
+        final int ridx = buffer.readerIndex();
+        final int widx = buffer.writerIndex();
+        if (ridx == widx) {
+            buffer.release();
+            return this;
+        }
+        if (!(buffer instanceof CompositeByteBuf)) {
+            addComponent0(increaseWriterIndex, componentCount, buffer);
+            consolidateIfNeeded();
+            return this;
+        }
+        final CompositeByteBuf from = (CompositeByteBuf) buffer;
+        from.checkIndex(ridx, widx - ridx);
+        final Component[] fromComponents = from.components;
+        final int compCountBefore = componentCount;
+        final int writerIndexBefore = writerIndex;
+        try {
+            for (int cidx = from.toComponentIndex0(ridx), newOffset = capacity();; cidx++) {
+                final Component component = fromComponents[cidx];
+                final int compOffset = component.offset;
+                final int fromIdx = Math.max(ridx, compOffset);
+                final int toIdx = Math.min(widx, component.endOffset);
+                final int len = toIdx - fromIdx;
+                if (len > 0) { // skip empty components
+                    addComp(componentCount, new Component(
+                            component.srcBuf.retain(), component.srcIdx(fromIdx),
+                            component.buf, component.idx(fromIdx), newOffset, len, null));
+                }
+                if (widx == toIdx) {
+                    break;
+                }
+                newOffset += len;
+            }
+            if (increaseWriterIndex) {
+                writerIndex = writerIndexBefore + (widx - ridx);
+            }
+            consolidateIfNeeded();
+            buffer.release();
+            buffer = null;
+            return this;
+        } finally {
+            if (buffer != null) {
+                // if we did not succeed, attempt to rollback any components that were added
+                if (increaseWriterIndex) {
+                    writerIndex = writerIndexBefore;
+                }
+                for (int cidx = componentCount - 1; cidx >= compCountBefore; cidx--) {
+                    components[cidx].free();
+                    removeComp(cidx);
+                }
+            }
+        }
     }
 
     // TODO optimize further, similar to ByteBuf[] version
     // (difference here is that we don't know *always* know precise size increase in advance,
     // but we do in the most common case that the Iterable is a Collection)
-    private int addComponents0(boolean increaseIndex, int cIndex, Iterable<ByteBuf> buffers) {
+    private CompositeByteBuf addComponents(boolean increaseIndex, int cIndex, Iterable<ByteBuf> buffers) {
         if (buffers instanceof ByteBuf) {
             // If buffers also implements ByteBuf (e.g. CompositeByteBuf), it has to go to addComponent(ByteBuf).
-            return addComponent0(increaseIndex, cIndex, (ByteBuf) buffers);
+            return addComponent(increaseIndex, cIndex, (ByteBuf) buffers);
         }
         checkNotNull(buffers, "buffers");
         Iterator<ByteBuf> it = buffers.iterator();
@@ -440,12 +520,13 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
                 cIndex = addComponent0(increaseIndex, cIndex, b) + 1;
                 cIndex = Math.min(cIndex, componentCount);
             }
-            return cIndex;
         } finally {
             while (it.hasNext()) {
                 ReferenceCountUtil.safeRelease(it.next());
             }
         }
+        consolidateIfNeeded();
+        return this;
     }
 
     /**
@@ -457,18 +538,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
         // operation.
         int size = componentCount;
         if (size > maxNumComponents) {
-            final int capacity = components[size - 1].endOffset;
-
-            ByteBuf consolidated = allocBuffer(capacity);
-            lastAccessed = null;
-
-            // We're not using foreach to avoid creating an iterator.
-            for (int i = 0; i < size; i ++) {
-                components[i].transferTo(consolidated);
-            }
-
-            components[0] = new Component(consolidated, 0, 0, capacity, consolidated);
-            removeCompRange(1, size);
+            consolidate0(0, size);
         }
     }
 
@@ -497,7 +567,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
             return;
         }
 
-        int nextIndex = cIndex > 0 ? components[cIndex].endOffset : 0;
+        int nextIndex = cIndex > 0 ? components[cIndex - 1].endOffset : 0;
         for (; cIndex < size; cIndex++) {
             Component c = components[cIndex];
             c.reposition(nextIndex);
@@ -516,7 +586,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
         if (lastAccessed == comp) {
             lastAccessed = null;
         }
-        comp.freeIfNecessary();
+        comp.free();
         removeComp(cIndex);
         if (comp.length() > 0) {
             // Only need to call updateComponentOffsets if the length was > 0
@@ -547,7 +617,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
             if (lastAccessed == c) {
                 lastAccessed = null;
             }
-            c.freeIfNecessary();
+            c.free();
         }
         removeCompRange(cIndex, endIndex);
 
@@ -748,6 +818,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
                 consolidateIfNeeded();
             }
         } else if (newCapacity < oldCapacity) {
+            lastAccessed = null;
             int i = size - 1;
             for (int bytesToTrim = oldCapacity - newCapacity; i >= 0; i--) {
                 Component c = components[i];
@@ -755,18 +826,23 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
                 if (bytesToTrim < cLength) {
                     // Trim the last component
                     c.endOffset -= bytesToTrim;
-                    c.slice = null;
+                    ByteBuf slice = c.slice;
+                    if (slice != null) {
+                        // We must replace the cached slice with a derived one to ensure that
+                        // it can later be released properly in the case of PooledSlicedByteBuf.
+                        c.slice = slice.slice(0, c.length());
+                    }
                     break;
                 }
-                c.freeIfNecessary();
+                c.free();
                 bytesToTrim -= cLength;
             }
             removeCompRange(i + 1, size);
 
             if (readerIndex() > newCapacity) {
-                setIndex(newCapacity, newCapacity);
-            } else if (writerIndex() > newCapacity) {
-                writerIndex(newCapacity);
+                setIndex0(newCapacity, newCapacity);
+            } else if (writerIndex > newCapacity) {
+                writerIndex = newCapacity;
             }
         }
         return this;
@@ -801,7 +877,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
      */
     public int toComponentIndex(int offset) {
         checkIndex(offset);
-        return toComponentIndex(offset);
+        return toComponentIndex0(offset);
     }
 
     private int toComponentIndex0(int offset) {
@@ -813,6 +889,9 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
                 }
             }
         }
+        if (size <= 2) { // fast-path for 1 and 2 component count
+            return size == 1 || offset < components[0].endOffset ? 0 : 1;
+        }
         for (int low = 0, high = size; low <= high;) {
             int mid = low + high >>> 1;
             Component c = components[mid];
@@ -1076,7 +1155,8 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
 
     @Override
     public CompositeByteBuf setShort(int index, int value) {
-        super.setShort(index, value);
+        checkIndex(index, 2);
+        _setShort(index, value);
         return this;
     }
 
@@ -1110,7 +1190,8 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
 
     @Override
     public CompositeByteBuf setMedium(int index, int value) {
-        super.setMedium(index, value);
+        checkIndex(index, 3);
+        _setMedium(index, value);
         return this;
     }
 
@@ -1144,7 +1225,8 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
 
     @Override
     public CompositeByteBuf setInt(int index, int value) {
-        super.setInt(index, value);
+        checkIndex(index, 4);
+        _setInt(index, value);
         return this;
     }
 
@@ -1178,7 +1260,8 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
 
     @Override
     public CompositeByteBuf setLong(int index, long value) {
-        super.setLong(index, value);
+        checkIndex(index, 8);
+        _setLong(index, value);
         return this;
     }
 
@@ -1286,7 +1369,6 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
 
         int i = toComponentIndex0(index);
         int readBytes = 0;
-
         do {
             Component c = components[i];
             int localLength = Math.min(length, c.endOffset - index);
@@ -1304,15 +1386,11 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
                 }
             }
 
+            index += localReadBytes;
+            length -= localReadBytes;
+            readBytes += localReadBytes;
             if (localReadBytes == localLength) {
-                index += localLength;
-                length -= localLength;
-                readBytes += localLength;
                 i ++;
-            } else {
-                index += localReadBytes;
-                length -= localReadBytes;
-                readBytes += localReadBytes;
             }
         } while (length > 0);
 
@@ -1350,15 +1428,11 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
                 }
             }
 
+            index += localReadBytes;
+            length -= localReadBytes;
+            readBytes += localReadBytes;
             if (localReadBytes == localLength) {
-                index += localLength;
-                length -= localLength;
-                readBytes += localLength;
                 i ++;
-            } else {
-                index += localReadBytes;
-                length -= localReadBytes;
-                readBytes += localReadBytes;
             }
         } while (length > 0);
 
@@ -1396,15 +1470,11 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
                 }
             }
 
+            index += localReadBytes;
+            length -= localReadBytes;
+            readBytes += localReadBytes;
             if (localReadBytes == localLength) {
-                index += localLength;
-                length -= localLength;
-                readBytes += localLength;
                 i ++;
-            } else {
-                index += localReadBytes;
-                length -= localReadBytes;
-                readBytes += localReadBytes;
             }
         } while (length > 0);
 
@@ -1541,8 +1611,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
         case 0:
             return EMPTY_NIO_BUFFER;
         case 1:
-            Component c = components[0];
-            return c.buf.internalNioBuffer(c.idx(index), length);
+            return components[0].internalNioBuffer(index, length);
         default:
             throw new UnsupportedOperationException();
         }
@@ -1566,7 +1635,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
         ByteBuffer[] buffers = nioBuffers(index, length);
 
         if (buffers.length == 1) {
-            return buffers[0].duplicate();
+            return buffers[0];
         }
 
         ByteBuffer merged = ByteBuffer.allocate(length).order(order());
@@ -1618,20 +1687,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
      */
     public CompositeByteBuf consolidate() {
         ensureAccessible();
-        final int numComponents = componentCount;
-        if (numComponents <= 1) {
-            return this;
-        }
-
-        final int capacity = components[numComponents - 1].endOffset;
-        final ByteBuf consolidated = allocBuffer(capacity);
-
-        for (int i = 0; i < numComponents; i ++) {
-            components[i].transferTo(consolidated);
-        }
-        lastAccessed = null;
-        components[0] = new Component(consolidated, 0, 0, capacity, consolidated);
-        removeCompRange(1, numComponents);
+        consolidate0(0, componentCount);
         return this;
     }
 
@@ -1643,13 +1699,18 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
      */
     public CompositeByteBuf consolidate(int cIndex, int numComponents) {
         checkComponentIndex(cIndex, numComponents);
+        consolidate0(cIndex, numComponents);
+        return this;
+    }
+
+    private void consolidate0(int cIndex, int numComponents) {
         if (numComponents <= 1) {
-            return this;
+            return;
         }
 
         final int endCIndex = cIndex + numComponents;
-        final Component last = components[endCIndex - 1];
-        final int capacity = last.endOffset - components[cIndex].offset;
+        final int startOffset = cIndex != 0 ? components[cIndex].offset : 0;
+        final int capacity = components[endCIndex - 1].endOffset - startOffset;
         final ByteBuf consolidated = allocBuffer(capacity);
 
         for (int i = cIndex; i < endCIndex; i ++) {
@@ -1657,9 +1718,10 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
         }
         lastAccessed = null;
         removeCompRange(cIndex + 1, endCIndex);
-        components[cIndex] = new Component(consolidated, 0, 0, capacity, consolidated);
-        updateComponentOffsets(cIndex);
-        return this;
+        components[cIndex] = newComponent(consolidated, 0);
+        if (cIndex != 0 || numComponents != componentCount) {
+            updateComponentOffsets(cIndex);
+        }
     }
 
     /**
@@ -1676,7 +1738,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
         int writerIndex = writerIndex();
         if (readerIndex == writerIndex && writerIndex == capacity()) {
             for (int i = 0, size = componentCount; i < size; i++) {
-                components[i].freeIfNecessary();
+                components[i].free();
             }
             lastAccessed = null;
             clearComps();
@@ -1686,16 +1748,26 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
         }
 
         // Remove read components.
-        int firstComponentId = toComponentIndex0(readerIndex);
-        for (int i = 0; i < firstComponentId; i ++) {
-            components[i].freeIfNecessary();
+        int firstComponentId = 0;
+        Component c = null;
+        for (int size = componentCount; firstComponentId < size; firstComponentId++) {
+            c = components[firstComponentId];
+            if (c.endOffset > readerIndex) {
+                break;
+            }
+            c.free();
+        }
+        if (firstComponentId == 0) {
+            return this; // Nothing to discard
+        }
+        Component la = lastAccessed;
+        if (la != null && la.endOffset <= readerIndex) {
+            lastAccessed = null;
         }
-        lastAccessed = null;
         removeCompRange(0, firstComponentId);
 
         // Update indexes and markers.
-        Component first = components[0];
-        int offset = first.offset;
+        int offset = c.offset;
         updateComponentOffsets(0);
         setIndex(readerIndex - offset, writerIndex - offset);
         adjustMarkers(offset);
@@ -1714,7 +1786,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
         int writerIndex = writerIndex();
         if (readerIndex == writerIndex && writerIndex == capacity()) {
             for (int i = 0, size = componentCount; i < size; i++) {
-                components[i].freeIfNecessary();
+                components[i].free();
             }
             lastAccessed = null;
             clearComps();
@@ -1723,30 +1795,31 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
             return this;
         }
 
-        // Remove read components.
-        int firstComponentId = toComponentIndex0(readerIndex);
-        for (int i = 0; i < firstComponentId; i ++) {
-            Component c = components[i];
-            c.freeIfNecessary();
-            if (lastAccessed == c) {
-                lastAccessed = null;
+        int firstComponentId = 0;
+        Component c = null;
+        for (int size = componentCount; firstComponentId < size; firstComponentId++) {
+            c = components[firstComponentId];
+            if (c.endOffset > readerIndex) {
+                break;
             }
+            c.free();
         }
 
-        // Remove or replace the first readable component with a new slice.
-        Component c = components[firstComponentId];
-        if (readerIndex == c.endOffset) {
-            // new slice would be empty, so remove instead
-            c.freeIfNecessary();
-            if (lastAccessed == c) {
-                lastAccessed = null;
-            }
-            firstComponentId++;
-        } else {
-            c.offset = 0;
-            c.endOffset -= readerIndex;
-            c.adjustment += readerIndex;
-            c.slice = null;
+        // Replace the first readable component with a new slice.
+        int trimmedBytes = readerIndex - c.offset;
+        c.offset = 0;
+        c.endOffset -= readerIndex;
+        c.srcAdjustment += readerIndex;
+        c.adjustment += readerIndex;
+        ByteBuf slice = c.slice;
+        if (slice != null) {
+            // We must replace the cached slice with a derived one to ensure that
+            // it can later be released properly in the case of PooledSlicedByteBuf.
+            c.slice = slice.slice(trimmedBytes, c.length());
+        }
+        Component la = lastAccessed;
+        if (la != null && la.endOffset <= readerIndex) {
+            lastAccessed = null;
         }
 
         removeCompRange(0, firstComponentId);
@@ -1770,21 +1843,32 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
     }
 
     private static final class Component {
-        final ByteBuf buf;
-        int adjustment;
-        int offset;
-        int endOffset;
+        final ByteBuf srcBuf; // the originally added buffer
+        final ByteBuf buf; // srcBuf unwrapped zero or more times
+
+        int srcAdjustment; // index of the start of this CompositeByteBuf relative to srcBuf
+        int adjustment; // index of the start of this CompositeByteBuf relative to buf
+
+        int offset; // offset of this component within this CompositeByteBuf
+        int endOffset; // end offset of this component within this CompositeByteBuf
 
         private ByteBuf slice; // cached slice, may be null
 
-        Component(ByteBuf buf, int srcOffset, int offset, int len, ByteBuf slice) {
+        Component(ByteBuf srcBuf, int srcOffset, ByteBuf buf, int bufOffset,
+                int offset, int len, ByteBuf slice) {
+            this.srcBuf = srcBuf;
+            this.srcAdjustment = srcOffset - offset;
             this.buf = buf;
+            this.adjustment = bufOffset - offset;
             this.offset = offset;
             this.endOffset = offset + len;
-            this.adjustment = srcOffset - offset;
             this.slice = slice;
         }
 
+        int srcIdx(int index) {
+            return index + srcAdjustment;
+        }
+
         int idx(int index) {
             return index + adjustment;
         }
@@ -1796,6 +1880,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
         void reposition(int newOffset) {
             int move = newOffset - offset;
             endOffset += move;
+            srcAdjustment -= move;
             adjustment -= move;
             offset = newOffset;
         }
@@ -1803,28 +1888,31 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
         // copy then release
         void transferTo(ByteBuf dst) {
             dst.writeBytes(buf, idx(offset), length());
-            freeIfNecessary();
+            free();
         }
 
         ByteBuf slice() {
-            return slice != null ? slice : (slice = buf.slice(idx(offset), length()));
+            ByteBuf s = slice;
+            if (s == null) {
+                slice = s = srcBuf.slice(srcIdx(offset), length());
+            }
+            return s;
         }
 
         ByteBuf duplicate() {
-            return buf.duplicate().setIndex(idx(offset), idx(endOffset));
+            return srcBuf.duplicate();
         }
 
-        void freeIfNecessary() {
-            // Release the slice if present since it may have a different
-            // refcount to the unwrapped buf if it is a PooledSlicedByteBuf
-            ByteBuf buffer = slice;
-            if (buffer != null) {
-                buffer.release();
-            } else {
-                buf.release();
-            }
-            // null out in either case since it could be racy
+        ByteBuffer internalNioBuffer(int index, int length) {
+            // Some buffers override this so we must use srcBuf
+            return srcBuf.internalNioBuffer(srcIdx(index), length);
+        }
+
+        void free() {
             slice = null;
+            // Release the original buffer since it may have a different
+            // refcount to the unwrapped buf (e.g. if PooledSlicedByteBuf)
+            srcBuf.release();
         }
     }
 
@@ -2065,7 +2153,7 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
 
     @Override
     public CompositeByteBuf writeBytes(byte[] src) {
-        writeBytes(src, 0, src.length);
+        super.writeBytes(src, 0, src.length);
         return this;
     }
 
@@ -2129,10 +2217,15 @@ public class CompositeByteBuf extends AbstractReferenceCountedByteBuf implements
         // We're not using foreach to avoid creating an iterator.
         // see https://github.com/netty/netty/issues/2642
         for (int i = 0, size = componentCount; i < size; i++) {
-            components[i].freeIfNecessary();
+            components[i].free();
         }
     }
 
+    @Override
+    boolean isAccessible() {
+        return !freed;
+    }
+
     @Override
     public ByteBuf unwrap() {
         return null;
diff --git a/buffer/src/main/java/io/netty/buffer/DefaultByteBufHolder.java b/buffer/src/main/java/io/netty/buffer/DefaultByteBufHolder.java
index 8198eae..ad247ad 100644
--- a/buffer/src/main/java/io/netty/buffer/DefaultByteBufHolder.java
+++ b/buffer/src/main/java/io/netty/buffer/DefaultByteBufHolder.java
@@ -16,6 +16,7 @@
 package io.netty.buffer;
 
 import io.netty.util.IllegalReferenceCountException;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.StringUtil;
 
 /**
@@ -27,10 +28,7 @@ public class DefaultByteBufHolder implements ByteBufHolder {
     private final ByteBuf data;
 
     public DefaultByteBufHolder(ByteBuf data) {
-        if (data == null) {
-            throw new NullPointerException("data");
-        }
-        this.data = data;
+        this.data = ObjectUtil.checkNotNull(data, "data");
     }
 
     @Override
@@ -135,13 +133,24 @@ public class DefaultByteBufHolder implements ByteBufHolder {
         return StringUtil.simpleClassName(this) + '(' + contentToString() + ')';
     }
 
+    /**
+     * This implementation of the {@code equals} operation is restricted to
+     * work only with instances of the same class. The reason for that is that
+     * Netty library already has a number of classes that extend {@link DefaultByteBufHolder} and
+     * override {@code equals} method with an additional comparison logic and we
+     * need the symmetric property of the {@code equals} operation to be preserved.
+     *
+     * @param   o   the reference object with which to compare.
+     * @return  {@code true} if this object is the same as the obj
+     *          argument; {@code false} otherwise.
+     */
     @Override
     public boolean equals(Object o) {
         if (this == o) {
             return true;
         }
-        if (o instanceof ByteBufHolder) {
-            return data.equals(((ByteBufHolder) o).content());
+        if (o != null && getClass() == o.getClass()) {
+            return data.equals(((DefaultByteBufHolder) o).data);
         }
         return false;
     }
diff --git a/buffer/src/main/java/io/netty/buffer/EmptyByteBuf.java b/buffer/src/main/java/io/netty/buffer/EmptyByteBuf.java
index b954318..c9717ba 100644
--- a/buffer/src/main/java/io/netty/buffer/EmptyByteBuf.java
+++ b/buffer/src/main/java/io/netty/buffer/EmptyByteBuf.java
@@ -16,8 +16,11 @@
 
 package io.netty.buffer;
 
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
+
 import io.netty.util.ByteProcessor;
 import io.netty.util.internal.EmptyArrays;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.PlatformDependent;
 import io.netty.util.internal.StringUtil;
 
@@ -62,11 +65,7 @@ public final class EmptyByteBuf extends ByteBuf {
     }
 
     private EmptyByteBuf(ByteBufAllocator alloc, ByteOrder order) {
-        if (alloc == null) {
-            throw new NullPointerException("alloc");
-        }
-
-        this.alloc = alloc;
+        this.alloc = ObjectUtil.checkNotNull(alloc, "alloc");
         this.order = order;
         str = StringUtil.simpleClassName(this) + (order == ByteOrder.BIG_ENDIAN? "BE" : "LE");
     }
@@ -118,10 +117,7 @@ public final class EmptyByteBuf extends ByteBuf {
 
     @Override
     public ByteBuf order(ByteOrder endianness) {
-        if (endianness == null) {
-            throw new NullPointerException("endianness");
-        }
-        if (endianness == order()) {
+        if (ObjectUtil.checkNotNull(endianness, "endianness") == order()) {
             return this;
         }
 
@@ -223,9 +219,7 @@ public final class EmptyByteBuf extends ByteBuf {
 
     @Override
     public ByteBuf ensureWritable(int minWritableBytes) {
-        if (minWritableBytes < 0) {
-            throw new IllegalArgumentException("minWritableBytes: " + minWritableBytes + " (expected: >= 0)");
-        }
+        checkPositiveOrZero(minWritableBytes, "minWritableBytes");
         if (minWritableBytes != 0) {
             throw new IndexOutOfBoundsException();
         }
@@ -234,9 +228,7 @@ public final class EmptyByteBuf extends ByteBuf {
 
     @Override
     public int ensureWritable(int minWritableBytes, boolean force) {
-        if (minWritableBytes < 0) {
-            throw new IllegalArgumentException("minWritableBytes: " + minWritableBytes + " (expected: >= 0)");
-        }
+        checkPositiveOrZero(minWritableBytes, "minWritableBytes");
 
         if (minWritableBytes == 0) {
             return 0;
@@ -686,7 +678,7 @@ public final class EmptyByteBuf extends ByteBuf {
     @Override
     public CharSequence readCharSequence(int length, Charset charset) {
         checkLength(length);
-        return null;
+        return StringUtil.EMPTY_STRING;
     }
 
     @Override
@@ -964,6 +956,11 @@ public final class EmptyByteBuf extends ByteBuf {
         }
     }
 
+    @Override
+    public boolean isContiguous() {
+        return true;
+    }
+
     @Override
     public String toString(Charset charset) {
         return "";
@@ -1048,9 +1045,7 @@ public final class EmptyByteBuf extends ByteBuf {
     }
 
     private ByteBuf checkIndex(int index, int length) {
-        if (length < 0) {
-            throw new IllegalArgumentException("length: " + length);
-        }
+        checkPositiveOrZero(length, "length");
         if (index != 0 || length != 0) {
             throw new IndexOutOfBoundsException();
         }
@@ -1058,9 +1053,7 @@ public final class EmptyByteBuf extends ByteBuf {
     }
 
     private ByteBuf checkLength(int length) {
-        if (length < 0) {
-            throw new IllegalArgumentException("length: " + length + " (expected: >= 0)");
-        }
+        checkPositiveOrZero(length, "length");
         if (length != 0) {
             throw new IndexOutOfBoundsException();
         }
diff --git a/buffer/src/main/java/io/netty/buffer/FixedCompositeByteBuf.java b/buffer/src/main/java/io/netty/buffer/FixedCompositeByteBuf.java
index 08c8d7f..c28af6f 100644
--- a/buffer/src/main/java/io/netty/buffer/FixedCompositeByteBuf.java
+++ b/buffer/src/main/java/io/netty/buffer/FixedCompositeByteBuf.java
@@ -49,7 +49,7 @@ final class FixedCompositeByteBuf extends AbstractReferenceCountedByteBuf {
             order = ByteOrder.BIG_ENDIAN;
             nioBufferCount = 1;
             capacity = 0;
-            direct = false;
+            direct = Unpooled.EMPTY_BUFFER.isDirect();
         } else {
             ByteBuf b = buffers[0];
             this.buffers = buffers;
diff --git a/buffer/src/main/java/io/netty/buffer/PoolArena.java b/buffer/src/main/java/io/netty/buffer/PoolArena.java
index 3bc948b..308eb96 100644
--- a/buffer/src/main/java/io/netty/buffer/PoolArena.java
+++ b/buffer/src/main/java/io/netty/buffer/PoolArena.java
@@ -26,6 +26,7 @@ import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
 
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
 import static java.lang.Math.max;
 
 abstract class PoolArena<T> implements PoolArenaMetric {
@@ -275,7 +276,7 @@ abstract class PoolArena<T> implements PoolArenaMetric {
                 return;
             }
 
-            freeChunk(chunk, handle, sizeClass, nioBuffer);
+            freeChunk(chunk, handle, sizeClass, nioBuffer, false);
         }
     }
 
@@ -286,21 +287,25 @@ abstract class PoolArena<T> implements PoolArenaMetric {
         return isTiny(normCapacity) ? SizeClass.Tiny : SizeClass.Small;
     }
 
-    void freeChunk(PoolChunk<T> chunk, long handle, SizeClass sizeClass, ByteBuffer nioBuffer) {
+    void freeChunk(PoolChunk<T> chunk, long handle, SizeClass sizeClass, ByteBuffer nioBuffer, boolean finalizer) {
         final boolean destroyChunk;
         synchronized (this) {
-            switch (sizeClass) {
-            case Normal:
-                ++deallocationsNormal;
-                break;
-            case Small:
-                ++deallocationsSmall;
-                break;
-            case Tiny:
-                ++deallocationsTiny;
-                break;
-            default:
-                throw new Error();
+            // We only call this if freeChunk is not called because of the PoolThreadCache finalizer as otherwise this
+            // may fail due lazy class-loading in for example tomcat.
+            if (!finalizer) {
+                switch (sizeClass) {
+                    case Normal:
+                        ++deallocationsNormal;
+                        break;
+                    case Small:
+                        ++deallocationsSmall;
+                        break;
+                    case Tiny:
+                        ++deallocationsTiny;
+                        break;
+                    default:
+                        throw new Error();
+                }
             }
             destroyChunk = !chunk.parent.free(chunk, handle, nioBuffer);
         }
@@ -330,9 +335,7 @@ abstract class PoolArena<T> implements PoolArenaMetric {
     }
 
     int normalizeCapacity(int reqCapacity) {
-        if (reqCapacity < 0) {
-            throw new IllegalArgumentException("capacity: " + reqCapacity + " (expected: 0+)");
-        }
+        checkPositiveOrZero(reqCapacity, "reqCapacity");
 
         if (reqCapacity >= chunkSize) {
             return directMemoryCacheAlignment == 0 ? reqCapacity : alignCapacity(reqCapacity);
@@ -376,9 +379,7 @@ abstract class PoolArena<T> implements PoolArenaMetric {
     }
 
     void reallocate(PooledByteBuf<T> buf, int newCapacity, boolean freeOldMemory) {
-        if (newCapacity < 0 || newCapacity > buf.maxCapacity()) {
-            throw new IllegalArgumentException("newCapacity: " + newCapacity);
-        }
+        assert newCapacity >= 0 && newCapacity <= buf.maxCapacity();
 
         int oldCapacity = buf.length;
         if (oldCapacity == newCapacity) {
@@ -391,29 +392,17 @@ abstract class PoolArena<T> implements PoolArenaMetric {
         T oldMemory = buf.memory;
         int oldOffset = buf.offset;
         int oldMaxLength = buf.maxLength;
-        int readerIndex = buf.readerIndex();
-        int writerIndex = buf.writerIndex();
 
+        // This does not touch buf's reader/writer indices
         allocate(parent.threadCache(), buf, newCapacity);
+        int bytesToCopy;
         if (newCapacity > oldCapacity) {
-            memoryCopy(
-                    oldMemory, oldOffset,
-                    buf.memory, buf.offset, oldCapacity);
-        } else if (newCapacity < oldCapacity) {
-            if (readerIndex < newCapacity) {
-                if (writerIndex > newCapacity) {
-                    writerIndex = newCapacity;
-                }
-                memoryCopy(
-                        oldMemory, oldOffset + readerIndex,
-                        buf.memory, buf.offset + readerIndex, writerIndex - readerIndex);
-            } else {
-                readerIndex = writerIndex = newCapacity;
-            }
+            bytesToCopy = oldCapacity;
+        } else {
+            buf.trimIndicesToCapacity(newCapacity);
+            bytesToCopy = newCapacity;
         }
-
-        buf.setIndex(readerIndex, writerIndex);
-
+        memoryCopy(oldMemory, oldOffset, buf, bytesToCopy);
         if (freeOldMemory) {
             free(oldChunk, oldNioBuffer, oldHandle, oldMaxLength, buf.cache);
         }
@@ -580,7 +569,7 @@ abstract class PoolArena<T> implements PoolArenaMetric {
     protected abstract PoolChunk<T> newChunk(int pageSize, int maxOrder, int pageShifts, int chunkSize);
     protected abstract PoolChunk<T> newUnpooledChunk(int capacity);
     protected abstract PooledByteBuf<T> newByteBuf(int maxCapacity);
-    protected abstract void memoryCopy(T src, int srcOffset, T dst, int dstOffset, int length);
+    protected abstract void memoryCopy(T src, int srcOffset, PooledByteBuf<T> dst, int length);
     protected abstract void destroyChunk(PoolChunk<T> chunk);
 
     @Override
@@ -703,12 +692,12 @@ abstract class PoolArena<T> implements PoolArenaMetric {
         }
 
         @Override
-        protected void memoryCopy(byte[] src, int srcOffset, byte[] dst, int dstOffset, int length) {
+        protected void memoryCopy(byte[] src, int srcOffset, PooledByteBuf<byte[]> dst, int length) {
             if (length == 0) {
                 return;
             }
 
-            System.arraycopy(src, srcOffset, dst, dstOffset, length);
+            System.arraycopy(src, srcOffset, dst.memory, dst.offset, length);
         }
     }
 
@@ -788,7 +777,7 @@ abstract class PoolArena<T> implements PoolArenaMetric {
         }
 
         @Override
-        protected void memoryCopy(ByteBuffer src, int srcOffset, ByteBuffer dst, int dstOffset, int length) {
+        protected void memoryCopy(ByteBuffer src, int srcOffset, PooledByteBuf<ByteBuffer> dstBuf, int length) {
             if (length == 0) {
                 return;
             }
@@ -796,13 +785,13 @@ abstract class PoolArena<T> implements PoolArenaMetric {
             if (HAS_UNSAFE) {
                 PlatformDependent.copyMemory(
                         PlatformDependent.directBufferAddress(src) + srcOffset,
-                        PlatformDependent.directBufferAddress(dst) + dstOffset, length);
+                        PlatformDependent.directBufferAddress(dstBuf.memory) + dstBuf.offset, length);
             } else {
                 // We must duplicate the NIO buffers because they may be accessed by other Netty buffers.
                 src = src.duplicate();
-                dst = dst.duplicate();
+                ByteBuffer dst = dstBuf.internalNioBuffer();
                 src.position(srcOffset).limit(srcOffset + length);
-                dst.position(dstOffset);
+                dst.position(dstBuf.offset);
                 dst.put(src);
             }
         }
diff --git a/buffer/src/main/java/io/netty/buffer/PoolSubpage.java b/buffer/src/main/java/io/netty/buffer/PoolSubpage.java
index f897eee..328aae6 100644
--- a/buffer/src/main/java/io/netty/buffer/PoolSubpage.java
+++ b/buffer/src/main/java/io/netty/buffer/PoolSubpage.java
@@ -204,16 +204,24 @@ final class PoolSubpage<T> implements PoolSubpageMetric {
         final int maxNumElems;
         final int numAvail;
         final int elemSize;
-        synchronized (chunk.arena) {
-            if (!this.doNotDestroy) {
-                doNotDestroy = false;
-                // Not used for creating the String.
-                maxNumElems = numAvail = elemSize = -1;
-            } else {
-                doNotDestroy = true;
-                maxNumElems = this.maxNumElems;
-                numAvail = this.numAvail;
-                elemSize = this.elemSize;
+        if (chunk == null) {
+            // This is the head so there is no need to synchronize at all as these never change.
+            doNotDestroy = true;
+            maxNumElems = 0;
+            numAvail = 0;
+            elemSize = -1;
+        } else {
+            synchronized (chunk.arena) {
+                if (!this.doNotDestroy) {
+                    doNotDestroy = false;
+                    // Not used for creating the String.
+                    maxNumElems = numAvail = elemSize = -1;
+                } else {
+                    doNotDestroy = true;
+                    maxNumElems = this.maxNumElems;
+                    numAvail = this.numAvail;
+                    elemSize = this.elemSize;
+                }
             }
         }
 
@@ -227,6 +235,11 @@ final class PoolSubpage<T> implements PoolSubpageMetric {
 
     @Override
     public int maxNumElements() {
+        if (chunk == null) {
+            // It's the head.
+            return 0;
+        }
+
         synchronized (chunk.arena) {
             return maxNumElems;
         }
@@ -234,6 +247,11 @@ final class PoolSubpage<T> implements PoolSubpageMetric {
 
     @Override
     public int numAvailable() {
+        if (chunk == null) {
+            // It's the head.
+            return 0;
+        }
+
         synchronized (chunk.arena) {
             return numAvail;
         }
@@ -241,6 +259,11 @@ final class PoolSubpage<T> implements PoolSubpageMetric {
 
     @Override
     public int elementSize() {
+        if (chunk == null) {
+            // It's the head.
+            return -1;
+        }
+
         synchronized (chunk.arena) {
             return elemSize;
         }
diff --git a/buffer/src/main/java/io/netty/buffer/PoolThreadCache.java b/buffer/src/main/java/io/netty/buffer/PoolThreadCache.java
index de2a9be..c1bf7e6 100644
--- a/buffer/src/main/java/io/netty/buffer/PoolThreadCache.java
+++ b/buffer/src/main/java/io/netty/buffer/PoolThreadCache.java
@@ -17,10 +17,13 @@
 package io.netty.buffer;
 
 
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
+
 import io.netty.buffer.PoolArena.SizeClass;
-import io.netty.util.Recycler;
-import io.netty.util.Recycler.Handle;
 import io.netty.util.internal.MathUtil;
+import io.netty.util.internal.ObjectPool;
+import io.netty.util.internal.ObjectPool.Handle;
+import io.netty.util.internal.ObjectPool.ObjectCreator;
 import io.netty.util.internal.PlatformDependent;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
@@ -65,10 +68,7 @@ final class PoolThreadCache {
     PoolThreadCache(PoolArena<byte[]> heapArena, PoolArena<ByteBuffer> directArena,
                     int tinyCacheSize, int smallCacheSize, int normalCacheSize,
                     int maxCachedBufferCapacity, int freeSweepAllocationThreshold) {
-        if (maxCachedBufferCapacity < 0) {
-            throw new IllegalArgumentException("maxCachedBufferCapacity: "
-                    + maxCachedBufferCapacity + " (expected: >= 0)");
-        }
+        checkPositiveOrZero(maxCachedBufferCapacity, "maxCachedBufferCapacity");
         this.freeSweepAllocationThreshold = freeSweepAllocationThreshold;
         this.heapArena = heapArena;
         this.directArena = directArena;
@@ -228,23 +228,23 @@ final class PoolThreadCache {
         try {
             super.finalize();
         } finally {
-            free();
+            free(true);
         }
     }
 
     /**
      *  Should be called if the Thread that uses this cache is about to exist to release resources out of the cache
      */
-    void free() {
+    void free(boolean finalizer) {
         // As free() may be called either by the finalizer or by FastThreadLocal.onRemoval(...) we need to ensure
         // we only call this one time.
         if (freed.compareAndSet(false, true)) {
-            int numFreed = free(tinySubPageDirectCaches) +
-                    free(smallSubPageDirectCaches) +
-                    free(normalDirectCaches) +
-                    free(tinySubPageHeapCaches) +
-                    free(smallSubPageHeapCaches) +
-                    free(normalHeapCaches);
+            int numFreed = free(tinySubPageDirectCaches, finalizer) +
+                    free(smallSubPageDirectCaches, finalizer) +
+                    free(normalDirectCaches, finalizer) +
+                    free(tinySubPageHeapCaches, finalizer) +
+                    free(smallSubPageHeapCaches, finalizer) +
+                    free(normalHeapCaches, finalizer);
 
             if (numFreed > 0 && logger.isDebugEnabled()) {
                 logger.debug("Freed {} thread-local buffer(s) from thread: {}", numFreed,
@@ -261,23 +261,23 @@ final class PoolThreadCache {
         }
     }
 
-    private static int free(MemoryRegionCache<?>[] caches) {
+    private static int free(MemoryRegionCache<?>[] caches, boolean finalizer) {
         if (caches == null) {
             return 0;
         }
 
         int numFreed = 0;
         for (MemoryRegionCache<?> c: caches) {
-            numFreed += free(c);
+            numFreed += free(c, finalizer);
         }
         return numFreed;
     }
 
-    private static int free(MemoryRegionCache<?> cache) {
+    private static int free(MemoryRegionCache<?> cache, boolean finalizer) {
         if (cache == null) {
             return 0;
         }
-        return cache.free();
+        return cache.free(finalizer);
     }
 
     void trim() {
@@ -419,16 +419,16 @@ final class PoolThreadCache {
         /**
          * Clear out this cache and free up all previous cached {@link PoolChunk}s and {@code handle}s.
          */
-        public final int free() {
-            return free(Integer.MAX_VALUE);
+        public final int free(boolean finalizer) {
+            return free(Integer.MAX_VALUE, finalizer);
         }
 
-        private int free(int max) {
+        private int free(int max, boolean finalizer) {
             int numFreed = 0;
             for (; numFreed < max; numFreed++) {
                 Entry<T> entry = queue.poll();
                 if (entry != null) {
-                    freeEntry(entry);
+                    freeEntry(entry, finalizer);
                 } else {
                     // all cleared
                     return numFreed;
@@ -446,20 +446,23 @@ final class PoolThreadCache {
 
             // We not even allocated all the number that are
             if (free > 0) {
-                free(free);
+                free(free, false);
             }
         }
 
         @SuppressWarnings({ "unchecked", "rawtypes" })
-        private  void freeEntry(Entry entry) {
+        private  void freeEntry(Entry entry, boolean finalizer) {
             PoolChunk chunk = entry.chunk;
             long handle = entry.handle;
             ByteBuffer nioBuffer = entry.nioBuffer;
 
-            // recycle now so PoolChunk can be GC'ed.
-            entry.recycle();
+            if (!finalizer) {
+                // recycle now so PoolChunk can be GC'ed. This will only be done if this is not freed because of
+                // a finalizer.
+                entry.recycle();
+            }
 
-            chunk.arena.freeChunk(chunk, handle, sizeClass, nioBuffer);
+            chunk.arena.freeChunk(chunk, handle, sizeClass, nioBuffer, finalizer);
         }
 
         static final class Entry<T> {
@@ -490,12 +493,12 @@ final class PoolThreadCache {
         }
 
         @SuppressWarnings("rawtypes")
-        private static final Recycler<Entry> RECYCLER = new Recycler<Entry>() {
+        private static final ObjectPool<Entry> RECYCLER = ObjectPool.newPool(new ObjectCreator<Entry>() {
             @SuppressWarnings("unchecked")
             @Override
-            protected Entry newObject(Handle<Entry> handle) {
+            public Entry newObject(Handle<Entry> handle) {
                 return new Entry(handle);
             }
-        };
+        });
     }
 }
diff --git a/buffer/src/main/java/io/netty/buffer/PooledByteBuf.java b/buffer/src/main/java/io/netty/buffer/PooledByteBuf.java
index beffbb0..3ef1e26 100644
--- a/buffer/src/main/java/io/netty/buffer/PooledByteBuf.java
+++ b/buffer/src/main/java/io/netty/buffer/PooledByteBuf.java
@@ -16,15 +16,19 @@
 
 package io.netty.buffer;
 
-import io.netty.util.Recycler;
-import io.netty.util.Recycler.Handle;
+import io.netty.util.internal.ObjectPool.Handle;
 
+import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
+import java.nio.channels.ClosedChannelException;
+import java.nio.channels.FileChannel;
+import java.nio.channels.GatheringByteChannel;
+import java.nio.channels.ScatteringByteChannel;
 
 abstract class PooledByteBuf<T> extends AbstractReferenceCountedByteBuf {
 
-    private final Recycler.Handle<PooledByteBuf<T>> recyclerHandle;
+    private final Handle<PooledByteBuf<T>> recyclerHandle;
 
     protected PoolChunk<T> chunk;
     protected long handle;
@@ -37,7 +41,7 @@ abstract class PooledByteBuf<T> extends AbstractReferenceCountedByteBuf {
     private ByteBufAllocator allocator;
 
     @SuppressWarnings("unchecked")
-    protected PooledByteBuf(Recycler.Handle<? extends PooledByteBuf<T>> recyclerHandle, int maxCapacity) {
+    protected PooledByteBuf(Handle<? extends PooledByteBuf<T>> recyclerHandle, int maxCapacity) {
         super(maxCapacity);
         this.recyclerHandle = (Handle<PooledByteBuf<T>>) recyclerHandle;
     }
@@ -72,7 +76,7 @@ abstract class PooledByteBuf<T> extends AbstractReferenceCountedByteBuf {
      */
     final void reuse(int maxCapacity) {
         maxCapacity(maxCapacity);
-        setRefCnt(1);
+        resetRefCnt();
         setIndex0(0, 0);
         discardMarks();
     }
@@ -82,36 +86,30 @@ abstract class PooledByteBuf<T> extends AbstractReferenceCountedByteBuf {
         return length;
     }
 
+    @Override
+    public int maxFastWritableBytes() {
+        return Math.min(maxLength, maxCapacity()) - writerIndex;
+    }
+
     @Override
     public final ByteBuf capacity(int newCapacity) {
+        if (newCapacity == length) {
+            ensureAccessible();
+            return this;
+        }
         checkNewCapacity(newCapacity);
-
-        // If the request capacity does not require reallocation, just update the length of the memory.
-        if (chunk.unpooled) {
-            if (newCapacity == length) {
-                return this;
-            }
-        } else {
+        if (!chunk.unpooled) {
+            // If the request capacity does not require reallocation, just update the length of the memory.
             if (newCapacity > length) {
                 if (newCapacity <= maxLength) {
                     length = newCapacity;
                     return this;
                 }
-            } else if (newCapacity < length) {
-                if (newCapacity > maxLength >>> 1) {
-                    if (maxLength <= 512) {
-                        if (newCapacity > maxLength - 16) {
-                            length = newCapacity;
-                            setIndex(Math.min(readerIndex(), newCapacity), Math.min(writerIndex(), newCapacity));
-                            return this;
-                        }
-                    } else { // > 512 (i.e. >= 1024)
-                        length = newCapacity;
-                        setIndex(Math.min(readerIndex(), newCapacity), Math.min(writerIndex(), newCapacity));
-                        return this;
-                    }
-                }
-            } else {
+            } else if (newCapacity > maxLength >>> 1 &&
+                    (maxLength > 512 || newCapacity > maxLength - 16)) {
+                // here newCapacity < length
+                length = newCapacity;
+                trimIndicesToCapacity(newCapacity);
                 return this;
             }
         }
@@ -156,6 +154,8 @@ abstract class PooledByteBuf<T> extends AbstractReferenceCountedByteBuf {
         ByteBuffer tmpNioBuf = this.tmpNioBuf;
         if (tmpNioBuf == null) {
             this.tmpNioBuf = tmpNioBuf = newInternalNioBuffer(memory);
+        } else {
+            tmpNioBuf.clear();
         }
         return tmpNioBuf;
     }
@@ -182,4 +182,86 @@ abstract class PooledByteBuf<T> extends AbstractReferenceCountedByteBuf {
     protected final int idx(int index) {
         return offset + index;
     }
+
+    final ByteBuffer _internalNioBuffer(int index, int length, boolean duplicate) {
+        index = idx(index);
+        ByteBuffer buffer = duplicate ? newInternalNioBuffer(memory) : internalNioBuffer();
+        buffer.limit(index + length).position(index);
+        return buffer;
+    }
+
+    ByteBuffer duplicateInternalNioBuffer(int index, int length) {
+        checkIndex(index, length);
+        return _internalNioBuffer(index, length, true);
+    }
+
+    @Override
+    public final ByteBuffer internalNioBuffer(int index, int length) {
+        checkIndex(index, length);
+        return _internalNioBuffer(index, length, false);
+    }
+
+    @Override
+    public final int nioBufferCount() {
+        return 1;
+    }
+
+    @Override
+    public final ByteBuffer nioBuffer(int index, int length) {
+        return duplicateInternalNioBuffer(index, length).slice();
+    }
+
+    @Override
+    public final ByteBuffer[] nioBuffers(int index, int length) {
+        return new ByteBuffer[] { nioBuffer(index, length) };
+    }
+
+    @Override
+    public final boolean isContiguous() {
+        return true;
+    }
+
+    @Override
+    public final int getBytes(int index, GatheringByteChannel out, int length) throws IOException {
+        return out.write(duplicateInternalNioBuffer(index, length));
+    }
+
+    @Override
+    public final int readBytes(GatheringByteChannel out, int length) throws IOException {
+        checkReadableBytes(length);
+        int readBytes = out.write(_internalNioBuffer(readerIndex, length, false));
+        readerIndex += readBytes;
+        return readBytes;
+    }
+
+    @Override
+    public final int getBytes(int index, FileChannel out, long position, int length) throws IOException {
+        return out.write(duplicateInternalNioBuffer(index, length), position);
+    }
+
+    @Override
+    public final int readBytes(FileChannel out, long position, int length) throws IOException {
+        checkReadableBytes(length);
+        int readBytes = out.write(_internalNioBuffer(readerIndex, length, false), position);
+        readerIndex += readBytes;
+        return readBytes;
+    }
+
+    @Override
+    public final int setBytes(int index, ScatteringByteChannel in, int length) throws IOException {
+        try {
+            return in.read(internalNioBuffer(index, length));
+        } catch (ClosedChannelException ignored) {
+            return -1;
+        }
+    }
+
+    @Override
+    public final int setBytes(int index, FileChannel in, long position, int length) throws IOException {
+        try {
+            return in.read(internalNioBuffer(index, length), position);
+        } catch (ClosedChannelException ignored) {
+            return -1;
+        }
+    }
 }
diff --git a/buffer/src/main/java/io/netty/buffer/PooledByteBufAllocator.java b/buffer/src/main/java/io/netty/buffer/PooledByteBufAllocator.java
index de6eee1..3d4bc94 100644
--- a/buffer/src/main/java/io/netty/buffer/PooledByteBufAllocator.java
+++ b/buffer/src/main/java/io/netty/buffer/PooledByteBufAllocator.java
@@ -16,12 +16,16 @@
 
 package io.netty.buffer;
 
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
+
 import io.netty.util.NettyRuntime;
+import io.netty.util.concurrent.EventExecutor;
 import io.netty.util.concurrent.FastThreadLocal;
 import io.netty.util.concurrent.FastThreadLocalThread;
 import io.netty.util.internal.PlatformDependent;
 import io.netty.util.internal.StringUtil;
 import io.netty.util.internal.SystemPropertyUtil;
+import io.netty.util.internal.ThreadExecutorMap;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 
@@ -29,6 +33,7 @@ import java.nio.ByteBuffer;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.concurrent.TimeUnit;
 
 public class PooledByteBufAllocator extends AbstractByteBufAllocator implements ByteBufAllocatorMetricProvider {
 
@@ -43,6 +48,7 @@ public class PooledByteBufAllocator extends AbstractByteBufAllocator implements
     private static final int DEFAULT_NORMAL_CACHE_SIZE;
     private static final int DEFAULT_MAX_CACHED_BUFFER_CAPACITY;
     private static final int DEFAULT_CACHE_TRIM_INTERVAL;
+    private static final long DEFAULT_CACHE_TRIM_INTERVAL_MILLIS;
     private static final boolean DEFAULT_USE_CACHE_FOR_ALL_THREADS;
     private static final int DEFAULT_DIRECT_MEMORY_CACHE_ALIGNMENT;
     static final int DEFAULT_MAX_CACHED_BYTEBUFFERS_PER_CHUNK;
@@ -50,6 +56,13 @@ public class PooledByteBufAllocator extends AbstractByteBufAllocator implements
     private static final int MIN_PAGE_SIZE = 4096;
     private static final int MAX_CHUNK_SIZE = (int) (((long) Integer.MAX_VALUE + 1) / 2);
 
+    private final Runnable trimTask = new Runnable() {
+        @Override
+        public void run() {
+            PooledByteBufAllocator.this.trimCurrentThreadCache();
+        }
+    };
+
     static {
         int defaultPageSize = SystemPropertyUtil.getInt("io.netty.allocator.pageSize", 8192);
         Throwable pageSizeFallbackCause = null;
@@ -111,6 +124,23 @@ public class PooledByteBufAllocator extends AbstractByteBufAllocator implements
         DEFAULT_CACHE_TRIM_INTERVAL = SystemPropertyUtil.getInt(
                 "io.netty.allocator.cacheTrimInterval", 8192);
 
+        if (SystemPropertyUtil.contains("io.netty.allocation.cacheTrimIntervalMillis")) {
+            logger.warn("-Dio.netty.allocation.cacheTrimIntervalMillis is deprecated," +
+                    " use -Dio.netty.allocator.cacheTrimIntervalMillis");
+
+            if (SystemPropertyUtil.contains("io.netty.allocator.cacheTrimIntervalMillis")) {
+                // Both system properties are specified. Use the non-deprecated one.
+                DEFAULT_CACHE_TRIM_INTERVAL_MILLIS = SystemPropertyUtil.getLong(
+                        "io.netty.allocator.cacheTrimIntervalMillis", 0);
+            } else {
+                DEFAULT_CACHE_TRIM_INTERVAL_MILLIS = SystemPropertyUtil.getLong(
+                        "io.netty.allocation.cacheTrimIntervalMillis", 0);
+            }
+        } else {
+            DEFAULT_CACHE_TRIM_INTERVAL_MILLIS = SystemPropertyUtil.getLong(
+                    "io.netty.allocator.cacheTrimIntervalMillis", 0);
+        }
+
         DEFAULT_USE_CACHE_FOR_ALL_THREADS = SystemPropertyUtil.getBoolean(
                 "io.netty.allocator.useCacheForAllThreads", true);
 
@@ -141,6 +171,7 @@ public class PooledByteBufAllocator extends AbstractByteBufAllocator implements
             logger.debug("-Dio.netty.allocator.normalCacheSize: {}", DEFAULT_NORMAL_CACHE_SIZE);
             logger.debug("-Dio.netty.allocator.maxCachedBufferCapacity: {}", DEFAULT_MAX_CACHED_BUFFER_CAPACITY);
             logger.debug("-Dio.netty.allocator.cacheTrimInterval: {}", DEFAULT_CACHE_TRIM_INTERVAL);
+            logger.debug("-Dio.netty.allocator.cacheTrimIntervalMillis: {}", DEFAULT_CACHE_TRIM_INTERVAL_MILLIS);
             logger.debug("-Dio.netty.allocator.useCacheForAllThreads: {}", DEFAULT_USE_CACHE_FOR_ALL_THREADS);
             logger.debug("-Dio.netty.allocator.maxCachedByteBuffersPerChunk: {}",
                     DEFAULT_MAX_CACHED_BYTEBUFFERS_PER_CHUNK);
@@ -215,17 +246,10 @@ public class PooledByteBufAllocator extends AbstractByteBufAllocator implements
         this.normalCacheSize = normalCacheSize;
         chunkSize = validateAndCalculateChunkSize(pageSize, maxOrder);
 
-        if (nHeapArena < 0) {
-            throw new IllegalArgumentException("nHeapArena: " + nHeapArena + " (expected: >= 0)");
-        }
-        if (nDirectArena < 0) {
-            throw new IllegalArgumentException("nDirectArea: " + nDirectArena + " (expected: >= 0)");
-        }
+        checkPositiveOrZero(nHeapArena, "nHeapArena");
+        checkPositiveOrZero(nDirectArena, "nDirectArena");
 
-        if (directMemoryCacheAlignment < 0) {
-            throw new IllegalArgumentException("directMemoryCacheAlignment: "
-                    + directMemoryCacheAlignment + " (expected: >= 0)");
-        }
+        checkPositiveOrZero(directMemoryCacheAlignment, "directMemoryCacheAlignment");
         if (directMemoryCacheAlignment > 0 && !isDirectMemoryCacheAlignmentSupported()) {
             throw new IllegalArgumentException("directMemoryCacheAlignment is not supported");
         }
@@ -443,11 +467,20 @@ public class PooledByteBufAllocator extends AbstractByteBufAllocator implements
             final PoolArena<byte[]> heapArena = leastUsedArena(heapArenas);
             final PoolArena<ByteBuffer> directArena = leastUsedArena(directArenas);
 
-            Thread current = Thread.currentThread();
+            final Thread current = Thread.currentThread();
             if (useCacheForAllThreads || current instanceof FastThreadLocalThread) {
-                return new PoolThreadCache(
+                final PoolThreadCache cache = new PoolThreadCache(
                         heapArena, directArena, tinyCacheSize, smallCacheSize, normalCacheSize,
                         DEFAULT_MAX_CACHED_BUFFER_CAPACITY, DEFAULT_CACHE_TRIM_INTERVAL);
+
+                if (DEFAULT_CACHE_TRIM_INTERVAL_MILLIS > 0) {
+                    final EventExecutor executor = ThreadExecutorMap.currentExecutor();
+                    if (executor != null) {
+                        executor.scheduleAtFixedRate(trimTask, DEFAULT_CACHE_TRIM_INTERVAL_MILLIS,
+                                DEFAULT_CACHE_TRIM_INTERVAL_MILLIS, TimeUnit.MILLISECONDS);
+                    }
+                }
+                return cache;
             }
             // No caching so just use 0 as sizes.
             return new PoolThreadCache(heapArena, directArena, 0, 0, 0, 0, 0);
@@ -455,7 +488,7 @@ public class PooledByteBufAllocator extends AbstractByteBufAllocator implements
 
         @Override
         protected void onRemoval(PoolThreadCache threadCache) {
-            threadCache.free();
+            threadCache.free(false);
         }
 
         private <T> PoolArena<T> leastUsedArena(PoolArena<T>[] arenas) {
@@ -608,6 +641,21 @@ public class PooledByteBufAllocator extends AbstractByteBufAllocator implements
         return cache;
     }
 
+    /**
+     * Trim thread local cache for the current {@link Thread}, which will give back any cached memory that was not
+     * allocated frequently since the last trim operation.
+     *
+     * Returns {@code true} if a cache for the current {@link Thread} exists and so was trimmed, false otherwise.
+     */
+    public boolean trimCurrentThreadCache() {
+        PoolThreadCache cache = threadCache.getIfExists();
+        if (cache != null) {
+            cache.trim();
+            return true;
+        }
+        return false;
+    }
+
     /**
      * Returns the status of the allocator (which contains all metrics) as string. Be aware this may be expensive
      * and so should not called too frequently.
diff --git a/buffer/src/main/java/io/netty/buffer/PooledDirectByteBuf.java b/buffer/src/main/java/io/netty/buffer/PooledDirectByteBuf.java
index 9601150..3d77ecf 100644
--- a/buffer/src/main/java/io/netty/buffer/PooledDirectByteBuf.java
+++ b/buffer/src/main/java/io/netty/buffer/PooledDirectByteBuf.java
@@ -16,25 +16,24 @@
 
 package io.netty.buffer;
 
-import io.netty.util.Recycler;
+import io.netty.util.internal.ObjectPool;
+import io.netty.util.internal.ObjectPool.Handle;
+import io.netty.util.internal.ObjectPool.ObjectCreator;
 
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.nio.ByteBuffer;
-import java.nio.channels.ClosedChannelException;
-import java.nio.channels.FileChannel;
-import java.nio.channels.GatheringByteChannel;
-import java.nio.channels.ScatteringByteChannel;
 
 final class PooledDirectByteBuf extends PooledByteBuf<ByteBuffer> {
 
-    private static final Recycler<PooledDirectByteBuf> RECYCLER = new Recycler<PooledDirectByteBuf>() {
+    private static final ObjectPool<PooledDirectByteBuf> RECYCLER = ObjectPool.newPool(
+            new ObjectCreator<PooledDirectByteBuf>() {
         @Override
-        protected PooledDirectByteBuf newObject(Handle<PooledDirectByteBuf> handle) {
+        public PooledDirectByteBuf newObject(Handle<PooledDirectByteBuf> handle) {
             return new PooledDirectByteBuf(handle, 0);
         }
-    };
+    });
 
     static PooledDirectByteBuf newInstance(int maxCapacity) {
         PooledDirectByteBuf buf = RECYCLER.get();
@@ -42,7 +41,7 @@ final class PooledDirectByteBuf extends PooledByteBuf<ByteBuffer> {
         return buf;
     }
 
-    private PooledDirectByteBuf(Recycler.Handle<PooledDirectByteBuf> recyclerHandle, int maxCapacity) {
+    private PooledDirectByteBuf(Handle<PooledDirectByteBuf> recyclerHandle, int maxCapacity) {
         super(recyclerHandle, maxCapacity);
     }
 
@@ -126,55 +125,30 @@ final class PooledDirectByteBuf extends PooledByteBuf<ByteBuffer> {
 
     @Override
     public ByteBuf getBytes(int index, byte[] dst, int dstIndex, int length) {
-        getBytes(index, dst, dstIndex, length, false);
-        return this;
-    }
-
-    private void getBytes(int index, byte[] dst, int dstIndex, int length, boolean internal) {
         checkDstIndex(index, length, dstIndex, dst.length);
-        ByteBuffer tmpBuf;
-        if (internal) {
-            tmpBuf = internalNioBuffer();
-        } else {
-            tmpBuf = memory.duplicate();
-        }
-        index = idx(index);
-        tmpBuf.clear().position(index).limit(index + length);
-        tmpBuf.get(dst, dstIndex, length);
+        _internalNioBuffer(index, length, true).get(dst, dstIndex, length);
+        return this;
     }
 
     @Override
     public ByteBuf readBytes(byte[] dst, int dstIndex, int length) {
-        checkReadableBytes(length);
-        getBytes(readerIndex, dst, dstIndex, length, true);
+        checkDstIndex(length, dstIndex, dst.length);
+        _internalNioBuffer(readerIndex, length, false).get(dst, dstIndex, length);
         readerIndex += length;
         return this;
     }
 
     @Override
     public ByteBuf getBytes(int index, ByteBuffer dst) {
-        getBytes(index, dst, false);
+        dst.put(duplicateInternalNioBuffer(index, dst.remaining()));
         return this;
     }
 
-    private void getBytes(int index, ByteBuffer dst, boolean internal) {
-        checkIndex(index, dst.remaining());
-        ByteBuffer tmpBuf;
-        if (internal) {
-            tmpBuf = internalNioBuffer();
-        } else {
-            tmpBuf = memory.duplicate();
-        }
-        index = idx(index);
-        tmpBuf.clear().position(index).limit(index + dst.remaining());
-        dst.put(tmpBuf);
-    }
-
     @Override
     public ByteBuf readBytes(ByteBuffer dst) {
         int length = dst.remaining();
         checkReadableBytes(length);
-        getBytes(readerIndex, dst, true);
+        dst.put(_internalNioBuffer(readerIndex, length, false));
         readerIndex += length;
         return this;
     }
@@ -201,61 +175,6 @@ final class PooledDirectByteBuf extends PooledByteBuf<ByteBuffer> {
         return this;
     }
 
-    @Override
-    public int getBytes(int index, GatheringByteChannel out, int length) throws IOException {
-        return getBytes(index, out, length, false);
-    }
-
-    private int getBytes(int index, GatheringByteChannel out, int length, boolean internal) throws IOException {
-        checkIndex(index, length);
-        if (length == 0) {
-            return 0;
-        }
-
-        ByteBuffer tmpBuf;
-        if (internal) {
-            tmpBuf = internalNioBuffer();
-        } else {
-            tmpBuf = memory.duplicate();
-        }
-        index = idx(index);
-        tmpBuf.clear().position(index).limit(index + length);
-        return out.write(tmpBuf);
-    }
-
-    @Override
-    public int getBytes(int index, FileChannel out, long position, int length) throws IOException {
-        return getBytes(index, out, position, length, false);
-    }
-
-    private int getBytes(int index, FileChannel out, long position, int length, boolean internal) throws IOException {
-        checkIndex(index, length);
-        if (length == 0) {
-            return 0;
-        }
-
-        ByteBuffer tmpBuf = internal ? internalNioBuffer() : memory.duplicate();
-        index = idx(index);
-        tmpBuf.clear().position(index).limit(index + length);
-        return out.write(tmpBuf, position);
-    }
-
-    @Override
-    public int readBytes(GatheringByteChannel out, int length) throws IOException {
-        checkReadableBytes(length);
-        int readBytes = getBytes(readerIndex, out, length, true);
-        readerIndex += readBytes;
-        return readBytes;
-    }
-
-    @Override
-    public int readBytes(FileChannel out, long position, int length) throws IOException {
-        checkReadableBytes(length);
-        int readBytes = getBytes(readerIndex, out, position, length, true);
-        readerIndex += readBytes;
-        return readBytes;
-    }
-
     @Override
     protected void _setByte(int index, int value) {
         memory.put(idx(index), (byte) value);
@@ -327,23 +246,21 @@ final class PooledDirectByteBuf extends PooledByteBuf<ByteBuffer> {
     @Override
     public ByteBuf setBytes(int index, byte[] src, int srcIndex, int length) {
         checkSrcIndex(index, length, srcIndex, src.length);
-        ByteBuffer tmpBuf = internalNioBuffer();
-        index = idx(index);
-        tmpBuf.clear().position(index).limit(index + length);
-        tmpBuf.put(src, srcIndex, length);
+        _internalNioBuffer(index, length, false).put(src, srcIndex, length);
         return this;
     }
 
     @Override
     public ByteBuf setBytes(int index, ByteBuffer src) {
-        checkIndex(index, src.remaining());
+        int length = src.remaining();
+        checkIndex(index, length);
         ByteBuffer tmpBuf = internalNioBuffer();
         if (src == tmpBuf) {
             src = src.duplicate();
         }
 
         index = idx(index);
-        tmpBuf.clear().position(index).limit(index + src.remaining());
+        tmpBuf.limit(index + length).position(index);
         tmpBuf.put(src);
         return this;
     }
@@ -357,67 +274,16 @@ final class PooledDirectByteBuf extends PooledByteBuf<ByteBuffer> {
             return readBytes;
         }
         ByteBuffer tmpBuf = internalNioBuffer();
-        tmpBuf.clear().position(idx(index));
+        tmpBuf.position(idx(index));
         tmpBuf.put(tmp, 0, readBytes);
         return readBytes;
     }
 
-    @Override
-    public int setBytes(int index, ScatteringByteChannel in, int length) throws IOException {
-        checkIndex(index, length);
-        ByteBuffer tmpBuf = internalNioBuffer();
-        index = idx(index);
-        tmpBuf.clear().position(index).limit(index + length);
-        try {
-            return in.read(tmpBuf);
-        } catch (ClosedChannelException ignored) {
-            return -1;
-        }
-    }
-
-    @Override
-    public int setBytes(int index, FileChannel in, long position, int length) throws IOException {
-        checkIndex(index, length);
-        ByteBuffer tmpBuf = internalNioBuffer();
-        index = idx(index);
-        tmpBuf.clear().position(index).limit(index + length);
-        try {
-            return in.read(tmpBuf, position);
-        } catch (ClosedChannelException ignored) {
-            return -1;
-        }
-    }
-
     @Override
     public ByteBuf copy(int index, int length) {
         checkIndex(index, length);
         ByteBuf copy = alloc().directBuffer(length, maxCapacity());
-        copy.writeBytes(this, index, length);
-        return copy;
-    }
-
-    @Override
-    public int nioBufferCount() {
-        return 1;
-    }
-
-    @Override
-    public ByteBuffer nioBuffer(int index, int length) {
-        checkIndex(index, length);
-        index = idx(index);
-        return ((ByteBuffer) memory.duplicate().position(index).limit(index + length)).slice();
-    }
-
-    @Override
-    public ByteBuffer[] nioBuffers(int index, int length) {
-        return new ByteBuffer[] { nioBuffer(index, length) };
-    }
-
-    @Override
-    public ByteBuffer internalNioBuffer(int index, int length) {
-        checkIndex(index, length);
-        index = idx(index);
-        return (ByteBuffer) internalNioBuffer().clear().position(index).limit(index + length);
+        return copy.writeBytes(this, index, length);
     }
 
     @Override
diff --git a/buffer/src/main/java/io/netty/buffer/PooledDuplicatedByteBuf.java b/buffer/src/main/java/io/netty/buffer/PooledDuplicatedByteBuf.java
index 1260f4e..89646e2 100644
--- a/buffer/src/main/java/io/netty/buffer/PooledDuplicatedByteBuf.java
+++ b/buffer/src/main/java/io/netty/buffer/PooledDuplicatedByteBuf.java
@@ -17,8 +17,9 @@
 package io.netty.buffer;
 
 import io.netty.util.ByteProcessor;
-import io.netty.util.Recycler;
-import io.netty.util.Recycler.Handle;
+import io.netty.util.internal.ObjectPool;
+import io.netty.util.internal.ObjectPool.Handle;
+import io.netty.util.internal.ObjectPool.ObjectCreator;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -30,12 +31,13 @@ import java.nio.channels.ScatteringByteChannel;
 
 final class PooledDuplicatedByteBuf extends AbstractPooledDerivedByteBuf {
 
-    private static final Recycler<PooledDuplicatedByteBuf> RECYCLER = new Recycler<PooledDuplicatedByteBuf>() {
+    private static final ObjectPool<PooledDuplicatedByteBuf> RECYCLER = ObjectPool.newPool(
+            new ObjectCreator<PooledDuplicatedByteBuf>() {
         @Override
-        protected PooledDuplicatedByteBuf newObject(Handle<PooledDuplicatedByteBuf> handle) {
+        public PooledDuplicatedByteBuf newObject(Handle<PooledDuplicatedByteBuf> handle) {
             return new PooledDuplicatedByteBuf(handle);
         }
-    };
+    });
 
     static PooledDuplicatedByteBuf newInstance(AbstractByteBuf unwrapped, ByteBuf wrapped,
                                                int readerIndex, int writerIndex) {
diff --git a/buffer/src/main/java/io/netty/buffer/PooledHeapByteBuf.java b/buffer/src/main/java/io/netty/buffer/PooledHeapByteBuf.java
index 467bde0..f271e6c 100644
--- a/buffer/src/main/java/io/netty/buffer/PooledHeapByteBuf.java
+++ b/buffer/src/main/java/io/netty/buffer/PooledHeapByteBuf.java
@@ -14,26 +14,25 @@
 
 package io.netty.buffer;
 
-import io.netty.util.Recycler;
+import io.netty.util.internal.ObjectPool;
+import io.netty.util.internal.ObjectPool.Handle;
+import io.netty.util.internal.ObjectPool.ObjectCreator;
 import io.netty.util.internal.PlatformDependent;
 
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.nio.ByteBuffer;
-import java.nio.channels.ClosedChannelException;
-import java.nio.channels.FileChannel;
-import java.nio.channels.GatheringByteChannel;
-import java.nio.channels.ScatteringByteChannel;
 
 class PooledHeapByteBuf extends PooledByteBuf<byte[]> {
 
-    private static final Recycler<PooledHeapByteBuf> RECYCLER = new Recycler<PooledHeapByteBuf>() {
+    private static final ObjectPool<PooledHeapByteBuf> RECYCLER = ObjectPool.newPool(
+            new ObjectCreator<PooledHeapByteBuf>() {
         @Override
-        protected PooledHeapByteBuf newObject(Handle<PooledHeapByteBuf> handle) {
+        public PooledHeapByteBuf newObject(Handle<PooledHeapByteBuf> handle) {
             return new PooledHeapByteBuf(handle, 0);
         }
-    };
+    });
 
     static PooledHeapByteBuf newInstance(int maxCapacity) {
         PooledHeapByteBuf buf = RECYCLER.get();
@@ -41,7 +40,7 @@ class PooledHeapByteBuf extends PooledByteBuf<byte[]> {
         return buf;
     }
 
-    PooledHeapByteBuf(Recycler.Handle<? extends PooledHeapByteBuf> recyclerHandle, int maxCapacity) {
+    PooledHeapByteBuf(Handle<? extends PooledHeapByteBuf> recyclerHandle, int maxCapacity) {
         super(recyclerHandle, maxCapacity);
     }
 
@@ -117,8 +116,9 @@ class PooledHeapByteBuf extends PooledByteBuf<byte[]> {
 
     @Override
     public final ByteBuf getBytes(int index, ByteBuffer dst) {
-        checkIndex(index, dst.remaining());
-        dst.put(memory, idx(index), dst.remaining());
+        int length = dst.remaining();
+        checkIndex(index, length);
+        dst.put(memory, idx(index), length);
         return this;
     }
 
@@ -129,51 +129,6 @@ class PooledHeapByteBuf extends PooledByteBuf<byte[]> {
         return this;
     }
 
-    @Override
-    public final int getBytes(int index, GatheringByteChannel out, int length) throws IOException {
-        return getBytes(index, out, length, false);
-    }
-
-    private int getBytes(int index, GatheringByteChannel out, int length, boolean internal) throws IOException {
-        checkIndex(index, length);
-        index = idx(index);
-        ByteBuffer tmpBuf;
-        if (internal) {
-            tmpBuf = internalNioBuffer();
-        } else {
-            tmpBuf = ByteBuffer.wrap(memory);
-        }
-        return out.write((ByteBuffer) tmpBuf.clear().position(index).limit(index + length));
-    }
-
-    @Override
-    public final int getBytes(int index, FileChannel out, long position, int length) throws IOException {
-        return getBytes(index, out, position, length, false);
-    }
-
-    private int getBytes(int index, FileChannel out, long position, int length, boolean internal) throws IOException {
-        checkIndex(index, length);
-        index = idx(index);
-        ByteBuffer tmpBuf = internal ? internalNioBuffer() : ByteBuffer.wrap(memory);
-        return out.write((ByteBuffer) tmpBuf.clear().position(index).limit(index + length), position);
-    }
-
-    @Override
-    public final int readBytes(GatheringByteChannel out, int length) throws IOException {
-        checkReadableBytes(length);
-        int readBytes = getBytes(readerIndex, out, length, true);
-        readerIndex += readBytes;
-        return readBytes;
-    }
-
-    @Override
-    public final int readBytes(FileChannel out, long position, int length) throws IOException {
-        checkReadableBytes(length);
-        int readBytes = getBytes(readerIndex, out, position, length, true);
-        readerIndex += readBytes;
-        return readBytes;
-    }
-
     @Override
     protected void _setByte(int index, int value) {
         HeapByteBufUtil.setByte(memory, idx(index), value);
@@ -253,59 +208,17 @@ class PooledHeapByteBuf extends PooledByteBuf<byte[]> {
         return in.read(memory, idx(index), length);
     }
 
-    @Override
-    public final int setBytes(int index, ScatteringByteChannel in, int length) throws IOException {
-        checkIndex(index, length);
-        index = idx(index);
-        try {
-            return in.read((ByteBuffer) internalNioBuffer().clear().position(index).limit(index + length));
-        } catch (ClosedChannelException ignored) {
-            return -1;
-        }
-    }
-
-    @Override
-    public final int setBytes(int index, FileChannel in, long position, int length) throws IOException {
-        checkIndex(index, length);
-        index = idx(index);
-        try {
-            return in.read((ByteBuffer) internalNioBuffer().clear().position(index).limit(index + length), position);
-        } catch (ClosedChannelException ignored) {
-            return -1;
-        }
-    }
-
     @Override
     public final ByteBuf copy(int index, int length) {
         checkIndex(index, length);
         ByteBuf copy = alloc().heapBuffer(length, maxCapacity());
-        copy.writeBytes(memory, idx(index), length);
-        return copy;
-    }
-
-    @Override
-    public final int nioBufferCount() {
-        return 1;
-    }
-
-    @Override
-    public final ByteBuffer[] nioBuffers(int index, int length) {
-        return new ByteBuffer[] { nioBuffer(index, length) };
-    }
-
-    @Override
-    public final ByteBuffer nioBuffer(int index, int length) {
-        checkIndex(index, length);
-        index = idx(index);
-        ByteBuffer buf =  ByteBuffer.wrap(memory, index, length);
-        return buf.slice();
+        return copy.writeBytes(memory, idx(index), length);
     }
 
     @Override
-    public final ByteBuffer internalNioBuffer(int index, int length) {
+    final ByteBuffer duplicateInternalNioBuffer(int index, int length) {
         checkIndex(index, length);
-        index = idx(index);
-        return (ByteBuffer) internalNioBuffer().clear().position(index).limit(index + length);
+        return ByteBuffer.wrap(memory, idx(index), length).slice();
     }
 
     @Override
diff --git a/buffer/src/main/java/io/netty/buffer/PooledSlicedByteBuf.java b/buffer/src/main/java/io/netty/buffer/PooledSlicedByteBuf.java
index 4405188..256a5f3 100644
--- a/buffer/src/main/java/io/netty/buffer/PooledSlicedByteBuf.java
+++ b/buffer/src/main/java/io/netty/buffer/PooledSlicedByteBuf.java
@@ -17,8 +17,9 @@
 package io.netty.buffer;
 
 import io.netty.util.ByteProcessor;
-import io.netty.util.Recycler;
-import io.netty.util.Recycler.Handle;
+import io.netty.util.internal.ObjectPool;
+import io.netty.util.internal.ObjectPool.Handle;
+import io.netty.util.internal.ObjectPool.ObjectCreator;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -32,12 +33,13 @@ import static io.netty.buffer.AbstractUnpooledSlicedByteBuf.checkSliceOutOfBound
 
 final class PooledSlicedByteBuf extends AbstractPooledDerivedByteBuf {
 
-    private static final Recycler<PooledSlicedByteBuf> RECYCLER = new Recycler<PooledSlicedByteBuf>() {
+    private static final ObjectPool<PooledSlicedByteBuf> RECYCLER = ObjectPool.newPool(
+            new ObjectCreator<PooledSlicedByteBuf>() {
         @Override
-        protected PooledSlicedByteBuf newObject(Handle<PooledSlicedByteBuf> handle) {
+        public PooledSlicedByteBuf newObject(Handle<PooledSlicedByteBuf> handle) {
             return new PooledSlicedByteBuf(handle);
         }
-    };
+    });
 
     static PooledSlicedByteBuf newInstance(AbstractByteBuf unwrapped, ByteBuf wrapped,
                                            int index, int length) {
diff --git a/buffer/src/main/java/io/netty/buffer/PooledUnsafeDirectByteBuf.java b/buffer/src/main/java/io/netty/buffer/PooledUnsafeDirectByteBuf.java
index e2dc22c..fd7e27d 100644
--- a/buffer/src/main/java/io/netty/buffer/PooledUnsafeDirectByteBuf.java
+++ b/buffer/src/main/java/io/netty/buffer/PooledUnsafeDirectByteBuf.java
@@ -16,25 +16,24 @@
 
 package io.netty.buffer;
 
-import io.netty.util.Recycler;
+import io.netty.util.internal.ObjectPool;
+import io.netty.util.internal.ObjectPool.Handle;
+import io.netty.util.internal.ObjectPool.ObjectCreator;
 import io.netty.util.internal.PlatformDependent;
 
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.nio.ByteBuffer;
-import java.nio.channels.ClosedChannelException;
-import java.nio.channels.FileChannel;
-import java.nio.channels.GatheringByteChannel;
-import java.nio.channels.ScatteringByteChannel;
 
 final class PooledUnsafeDirectByteBuf extends PooledByteBuf<ByteBuffer> {
-    private static final Recycler<PooledUnsafeDirectByteBuf> RECYCLER = new Recycler<PooledUnsafeDirectByteBuf>() {
+    private static final ObjectPool<PooledUnsafeDirectByteBuf> RECYCLER = ObjectPool.newPool(
+            new ObjectCreator<PooledUnsafeDirectByteBuf>() {
         @Override
-        protected PooledUnsafeDirectByteBuf newObject(Handle<PooledUnsafeDirectByteBuf> handle) {
+        public PooledUnsafeDirectByteBuf newObject(Handle<PooledUnsafeDirectByteBuf> handle) {
             return new PooledUnsafeDirectByteBuf(handle, 0);
         }
-    };
+    });
 
     static PooledUnsafeDirectByteBuf newInstance(int maxCapacity) {
         PooledUnsafeDirectByteBuf buf = RECYCLER.get();
@@ -44,7 +43,7 @@ final class PooledUnsafeDirectByteBuf extends PooledByteBuf<ByteBuffer> {
 
     private long memoryAddress;
 
-    private PooledUnsafeDirectByteBuf(Recycler.Handle<PooledUnsafeDirectByteBuf> recyclerHandle, int maxCapacity) {
+    private PooledUnsafeDirectByteBuf(Handle<PooledUnsafeDirectByteBuf> recyclerHandle, int maxCapacity) {
         super(recyclerHandle, maxCapacity);
     }
 
@@ -138,78 +137,12 @@ final class PooledUnsafeDirectByteBuf extends PooledByteBuf<ByteBuffer> {
         return this;
     }
 
-    @Override
-    public ByteBuf readBytes(ByteBuffer dst) {
-        int length = dst.remaining();
-        checkReadableBytes(length);
-        getBytes(readerIndex, dst);
-        readerIndex += length;
-        return this;
-    }
-
     @Override
     public ByteBuf getBytes(int index, OutputStream out, int length) throws IOException {
         UnsafeByteBufUtil.getBytes(this, addr(index), index, out, length);
         return this;
     }
 
-    @Override
-    public int getBytes(int index, GatheringByteChannel out, int length) throws IOException {
-        return getBytes(index, out, length, false);
-    }
-
-    private int getBytes(int index, GatheringByteChannel out, int length, boolean internal) throws IOException {
-        checkIndex(index, length);
-        if (length == 0) {
-            return 0;
-        }
-
-        ByteBuffer tmpBuf;
-        if (internal) {
-            tmpBuf = internalNioBuffer();
-        } else {
-            tmpBuf = memory.duplicate();
-        }
-        index = idx(index);
-        tmpBuf.clear().position(index).limit(index + length);
-        return out.write(tmpBuf);
-    }
-
-    @Override
-    public int getBytes(int index, FileChannel out, long position, int length) throws IOException {
-        return getBytes(index, out, position, length, false);
-    }
-
-    private int getBytes(int index, FileChannel out, long position, int length, boolean internal) throws IOException {
-        checkIndex(index, length);
-        if (length == 0) {
-            return 0;
-        }
-
-        ByteBuffer tmpBuf = internal ? internalNioBuffer() : memory.duplicate();
-        index = idx(index);
-        tmpBuf.clear().position(index).limit(index + length);
-        return out.write(tmpBuf, position);
-    }
-
-    @Override
-    public int readBytes(GatheringByteChannel out, int length)
-            throws IOException {
-        checkReadableBytes(length);
-        int readBytes = getBytes(readerIndex, out, length, true);
-        readerIndex += readBytes;
-        return readBytes;
-    }
-
-    @Override
-    public int readBytes(FileChannel out, long position, int length)
-            throws IOException {
-        checkReadableBytes(length);
-        int readBytes = getBytes(readerIndex, out, position, length, true);
-        readerIndex += readBytes;
-        return readBytes;
-    }
-
     @Override
     protected void _setByte(int index, int value) {
         UnsafeByteBufUtil.setByte(addr(index), (byte) value);
@@ -278,61 +211,11 @@ final class PooledUnsafeDirectByteBuf extends PooledByteBuf<ByteBuffer> {
         return UnsafeByteBufUtil.setBytes(this, addr(index), index, in, length);
     }
 
-    @Override
-    public int setBytes(int index, ScatteringByteChannel in, int length) throws IOException {
-        checkIndex(index, length);
-        ByteBuffer tmpBuf = internalNioBuffer();
-        index = idx(index);
-        tmpBuf.clear().position(index).limit(index + length);
-        try {
-            return in.read(tmpBuf);
-        } catch (ClosedChannelException ignored) {
-            return -1;
-        }
-    }
-
-    @Override
-    public int setBytes(int index, FileChannel in, long position, int length) throws IOException {
-        checkIndex(index, length);
-        ByteBuffer tmpBuf = internalNioBuffer();
-        index = idx(index);
-        tmpBuf.clear().position(index).limit(index + length);
-        try {
-            return in.read(tmpBuf, position);
-        } catch (ClosedChannelException ignored) {
-            return -1;
-        }
-    }
-
     @Override
     public ByteBuf copy(int index, int length) {
         return UnsafeByteBufUtil.copy(this, addr(index), index, length);
     }
 
-    @Override
-    public int nioBufferCount() {
-        return 1;
-    }
-
-    @Override
-    public ByteBuffer[] nioBuffers(int index, int length) {
-        return new ByteBuffer[] { nioBuffer(index, length) };
-    }
-
-    @Override
-    public ByteBuffer nioBuffer(int index, int length) {
-        checkIndex(index, length);
-        index = idx(index);
-        return ((ByteBuffer) memory.duplicate().position(index).limit(index + length)).slice();
-    }
-
-    @Override
-    public ByteBuffer internalNioBuffer(int index, int length) {
-        checkIndex(index, length);
-        index = idx(index);
-        return (ByteBuffer) internalNioBuffer().clear().position(index).limit(index + length);
-    }
-
     @Override
     public boolean hasArray() {
         return false;
diff --git a/buffer/src/main/java/io/netty/buffer/PooledUnsafeHeapByteBuf.java b/buffer/src/main/java/io/netty/buffer/PooledUnsafeHeapByteBuf.java
index a644450..88273af 100644
--- a/buffer/src/main/java/io/netty/buffer/PooledUnsafeHeapByteBuf.java
+++ b/buffer/src/main/java/io/netty/buffer/PooledUnsafeHeapByteBuf.java
@@ -15,18 +15,20 @@
  */
 package io.netty.buffer;
 
-import io.netty.util.Recycler;
-import io.netty.util.Recycler.Handle;
+import io.netty.util.internal.ObjectPool;
+import io.netty.util.internal.ObjectPool.Handle;
+import io.netty.util.internal.ObjectPool.ObjectCreator;
 import io.netty.util.internal.PlatformDependent;
 
 final class PooledUnsafeHeapByteBuf extends PooledHeapByteBuf {
 
-    private static final Recycler<PooledUnsafeHeapByteBuf> RECYCLER = new Recycler<PooledUnsafeHeapByteBuf>() {
+    private static final ObjectPool<PooledUnsafeHeapByteBuf> RECYCLER = ObjectPool.newPool(
+            new ObjectCreator<PooledUnsafeHeapByteBuf>() {
         @Override
-        protected PooledUnsafeHeapByteBuf newObject(Handle<PooledUnsafeHeapByteBuf> handle) {
+        public PooledUnsafeHeapByteBuf newObject(Handle<PooledUnsafeHeapByteBuf> handle) {
             return new PooledUnsafeHeapByteBuf(handle, 0);
         }
-    };
+    });
 
     static PooledUnsafeHeapByteBuf newUnsafeInstance(int maxCapacity) {
         PooledUnsafeHeapByteBuf buf = RECYCLER.get();
diff --git a/buffer/src/main/java/io/netty/buffer/ReadOnlyByteBufferBuf.java b/buffer/src/main/java/io/netty/buffer/ReadOnlyByteBufferBuf.java
index c7cda05..bc79fb0 100644
--- a/buffer/src/main/java/io/netty/buffer/ReadOnlyByteBufferBuf.java
+++ b/buffer/src/main/java/io/netty/buffer/ReadOnlyByteBufferBuf.java
@@ -195,11 +195,6 @@ class ReadOnlyByteBufferBuf extends AbstractReferenceCountedByteBuf {
     public ByteBuf getBytes(int index, byte[] dst, int dstIndex, int length) {
         checkDstIndex(index, length, dstIndex, dst.length);
 
-        if (dstIndex < 0 || dstIndex > dst.length - length) {
-            throw new IndexOutOfBoundsException(String.format(
-                    "dstIndex: %d, length: %d (expected: range(0, %d))", dstIndex, length, dst.length));
-        }
-
         ByteBuffer tmpBuf = internalNioBuffer();
         tmpBuf.clear().position(index).limit(index + length);
         tmpBuf.get(dst, dstIndex, length);
@@ -208,14 +203,10 @@ class ReadOnlyByteBufferBuf extends AbstractReferenceCountedByteBuf {
 
     @Override
     public ByteBuf getBytes(int index, ByteBuffer dst) {
-        checkIndex(index);
-        if (dst == null) {
-            throw new NullPointerException("dst");
-        }
+        checkIndex(index, dst.remaining());
 
-        int bytesToCopy = Math.min(capacity() - index, dst.remaining());
         ByteBuffer tmpBuf = internalNioBuffer();
-        tmpBuf.clear().position(index).limit(index + bytesToCopy);
+        tmpBuf.clear().position(index).limit(index + dst.remaining());
         dst.put(tmpBuf);
         return this;
     }
@@ -453,6 +444,7 @@ class ReadOnlyByteBufferBuf extends AbstractReferenceCountedByteBuf {
 
     @Override
     public ByteBuffer nioBuffer(int index, int length) {
+        checkIndex(index, length);
         return (ByteBuffer) buffer.duplicate().position(index).limit(index + length);
     }
 
@@ -462,6 +454,11 @@ class ReadOnlyByteBufferBuf extends AbstractReferenceCountedByteBuf {
         return (ByteBuffer) internalNioBuffer().clear().position(index).limit(index + length);
     }
 
+    @Override
+    public final boolean isContiguous() {
+        return true;
+    }
+
     @Override
     public boolean hasArray() {
         return buffer.hasArray();
diff --git a/buffer/src/main/java/io/netty/buffer/ReadOnlyUnsafeDirectByteBuf.java b/buffer/src/main/java/io/netty/buffer/ReadOnlyUnsafeDirectByteBuf.java
index 316760e..a1e9714 100644
--- a/buffer/src/main/java/io/netty/buffer/ReadOnlyUnsafeDirectByteBuf.java
+++ b/buffer/src/main/java/io/netty/buffer/ReadOnlyUnsafeDirectByteBuf.java
@@ -16,6 +16,7 @@
 package io.netty.buffer;
 
 
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.PlatformDependent;
 
 import java.nio.ByteBuffer;
@@ -62,9 +63,7 @@ final class ReadOnlyUnsafeDirectByteBuf extends ReadOnlyByteBufferBuf {
     @Override
     public ByteBuf getBytes(int index, ByteBuf dst, int dstIndex, int length) {
         checkIndex(index, length);
-        if (dst == null) {
-            throw new NullPointerException("dst");
-        }
+        ObjectUtil.checkNotNull(dst, "dst");
         if (dstIndex < 0 || dstIndex > dst.capacity() - length) {
             throw new IndexOutOfBoundsException("dstIndex: " + dstIndex);
         }
@@ -82,9 +81,7 @@ final class ReadOnlyUnsafeDirectByteBuf extends ReadOnlyByteBufferBuf {
     @Override
     public ByteBuf getBytes(int index, byte[] dst, int dstIndex, int length) {
         checkIndex(index, length);
-        if (dst == null) {
-            throw new NullPointerException("dst");
-        }
+        ObjectUtil.checkNotNull(dst, "dst");
         if (dstIndex < 0 || dstIndex > dst.length - length) {
             throw new IndexOutOfBoundsException(String.format(
                     "dstIndex: %d, length: %d (expected: range(0, %d))", dstIndex, length, dst.length));
@@ -96,20 +93,6 @@ final class ReadOnlyUnsafeDirectByteBuf extends ReadOnlyByteBufferBuf {
         return this;
     }
 
-    @Override
-    public ByteBuf getBytes(int index, ByteBuffer dst) {
-        checkIndex(index);
-        if (dst == null) {
-            throw new NullPointerException("dst");
-        }
-
-        int bytesToCopy = Math.min(capacity() - index, dst.remaining());
-        ByteBuffer tmpBuf = internalNioBuffer();
-        tmpBuf.clear().position(index).limit(index + bytesToCopy);
-        dst.put(tmpBuf);
-        return this;
-    }
-
     @Override
     public ByteBuf copy(int index, int length) {
         checkIndex(index, length);
diff --git a/buffer/src/main/java/io/netty/buffer/SwappedByteBuf.java b/buffer/src/main/java/io/netty/buffer/SwappedByteBuf.java
index 5d54a1f..05725a9 100644
--- a/buffer/src/main/java/io/netty/buffer/SwappedByteBuf.java
+++ b/buffer/src/main/java/io/netty/buffer/SwappedByteBuf.java
@@ -16,6 +16,7 @@
 package io.netty.buffer;
 
 import io.netty.util.ByteProcessor;
+import io.netty.util.internal.ObjectUtil;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -40,10 +41,7 @@ public class SwappedByteBuf extends ByteBuf {
     private final ByteOrder order;
 
     public SwappedByteBuf(ByteBuf buf) {
-        if (buf == null) {
-            throw new NullPointerException("buf");
-        }
-        this.buf = buf;
+        this.buf = ObjectUtil.checkNotNull(buf, "buf");
         if (buf.order() == ByteOrder.BIG_ENDIAN) {
             order = ByteOrder.LITTLE_ENDIAN;
         } else {
@@ -58,10 +56,7 @@ public class SwappedByteBuf extends ByteBuf {
 
     @Override
     public ByteBuf order(ByteOrder endianness) {
-        if (endianness == null) {
-            throw new NullPointerException("endianness");
-        }
-        if (endianness == order) {
+        if (ObjectUtil.checkNotNull(endianness, "endianness") == order) {
             return this;
         }
         return buf;
@@ -151,6 +146,11 @@ public class SwappedByteBuf extends ByteBuf {
         return buf.maxWritableBytes();
     }
 
+    @Override
+    public int maxFastWritableBytes() {
+        return buf.maxFastWritableBytes();
+    }
+
     @Override
     public boolean isReadable() {
         return buf.isReadable();
@@ -977,6 +977,11 @@ public class SwappedByteBuf extends ByteBuf {
         return buf.hasMemoryAddress();
     }
 
+    @Override
+    public boolean isContiguous() {
+        return buf.isContiguous();
+    }
+
     @Override
     public long memoryAddress() {
         return buf.memoryAddress();
@@ -997,6 +1002,11 @@ public class SwappedByteBuf extends ByteBuf {
         return buf.refCnt();
     }
 
+    @Override
+    final boolean isAccessible() {
+        return buf.isAccessible();
+    }
+
     @Override
     public ByteBuf retain() {
         buf.retain();
diff --git a/buffer/src/main/java/io/netty/buffer/Unpooled.java b/buffer/src/main/java/io/netty/buffer/Unpooled.java
index d7df192..3976f4f 100644
--- a/buffer/src/main/java/io/netty/buffer/Unpooled.java
+++ b/buffer/src/main/java/io/netty/buffer/Unpooled.java
@@ -16,6 +16,7 @@
 package io.netty.buffer;
 
 import io.netty.buffer.CompositeByteBuf.ByteWrapper;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.PlatformDependent;
 
 import java.nio.ByteBuffer;
@@ -575,9 +576,7 @@ public final class Unpooled {
      * {@code 0} and the length of the encoded string respectively.
      */
     public static ByteBuf copiedBuffer(CharSequence string, Charset charset) {
-        if (string == null) {
-            throw new NullPointerException("string");
-        }
+        ObjectUtil.checkNotNull(string, "string");
 
         if (string instanceof CharBuffer) {
             return copiedBuffer((CharBuffer) string, charset);
@@ -594,9 +593,7 @@ public final class Unpooled {
      */
     public static ByteBuf copiedBuffer(
             CharSequence string, int offset, int length, Charset charset) {
-        if (string == null) {
-            throw new NullPointerException("string");
-        }
+        ObjectUtil.checkNotNull(string, "string");
         if (length == 0) {
             return EMPTY_BUFFER;
         }
@@ -626,9 +623,7 @@ public final class Unpooled {
      * {@code 0} and the length of the encoded string respectively.
      */
     public static ByteBuf copiedBuffer(char[] array, Charset charset) {
-        if (array == null) {
-            throw new NullPointerException("array");
-        }
+        ObjectUtil.checkNotNull(array, "array");
         return copiedBuffer(array, 0, array.length, charset);
     }
 
@@ -639,9 +634,7 @@ public final class Unpooled {
      * {@code 0} and the length of the encoded string respectively.
      */
     public static ByteBuf copiedBuffer(char[] array, int offset, int length, Charset charset) {
-        if (array == null) {
-            throw new NullPointerException("array");
-        }
+        ObjectUtil.checkNotNull(array, "array");
         if (length == 0) {
             return EMPTY_BUFFER;
         }
diff --git a/buffer/src/main/java/io/netty/buffer/UnpooledDirectByteBuf.java b/buffer/src/main/java/io/netty/buffer/UnpooledDirectByteBuf.java
index 60167cf..66b48e8 100644
--- a/buffer/src/main/java/io/netty/buffer/UnpooledDirectByteBuf.java
+++ b/buffer/src/main/java/io/netty/buffer/UnpooledDirectByteBuf.java
@@ -15,6 +15,7 @@
  */
 package io.netty.buffer;
 
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.PlatformDependent;
 
 import java.io.IOException;
@@ -27,6 +28,8 @@ import java.nio.channels.FileChannel;
 import java.nio.channels.GatheringByteChannel;
 import java.nio.channels.ScatteringByteChannel;
 
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
+
 /**
  * A NIO {@link ByteBuffer} based buffer. It is recommended to use
  * {@link UnpooledByteBufAllocator#directBuffer(int, int)}, {@link Unpooled#directBuffer(int)} and
@@ -36,7 +39,7 @@ public class UnpooledDirectByteBuf extends AbstractReferenceCountedByteBuf {
 
     private final ByteBufAllocator alloc;
 
-    private ByteBuffer buffer;
+    ByteBuffer buffer; // accessed by UnpooledUnsafeNoCleanerDirectByteBuf.reallocateDirect()
     private ByteBuffer tmpNioBuf;
     private int capacity;
     private boolean doNotFree;
@@ -49,22 +52,16 @@ public class UnpooledDirectByteBuf extends AbstractReferenceCountedByteBuf {
      */
     public UnpooledDirectByteBuf(ByteBufAllocator alloc, int initialCapacity, int maxCapacity) {
         super(maxCapacity);
-        if (alloc == null) {
-            throw new NullPointerException("alloc");
-        }
-        if (initialCapacity < 0) {
-            throw new IllegalArgumentException("initialCapacity: " + initialCapacity);
-        }
-        if (maxCapacity < 0) {
-            throw new IllegalArgumentException("maxCapacity: " + maxCapacity);
-        }
+        ObjectUtil.checkNotNull(alloc, "alloc");
+        checkPositiveOrZero(initialCapacity, "initialCapacity");
+        checkPositiveOrZero(maxCapacity, "maxCapacity");
         if (initialCapacity > maxCapacity) {
             throw new IllegalArgumentException(String.format(
                     "initialCapacity(%d) > maxCapacity(%d)", initialCapacity, maxCapacity));
         }
 
         this.alloc = alloc;
-        setByteBuffer(allocateDirect(initialCapacity));
+        setByteBuffer(allocateDirect(initialCapacity), false);
     }
 
     /**
@@ -73,13 +70,14 @@ public class UnpooledDirectByteBuf extends AbstractReferenceCountedByteBuf {
      * @param maxCapacity the maximum capacity of the underlying direct buffer
      */
     protected UnpooledDirectByteBuf(ByteBufAllocator alloc, ByteBuffer initialBuffer, int maxCapacity) {
+        this(alloc, initialBuffer, maxCapacity, false, true);
+    }
+
+    UnpooledDirectByteBuf(ByteBufAllocator alloc, ByteBuffer initialBuffer,
+            int maxCapacity, boolean doFree, boolean slice) {
         super(maxCapacity);
-        if (alloc == null) {
-            throw new NullPointerException("alloc");
-        }
-        if (initialBuffer == null) {
-            throw new NullPointerException("initialBuffer");
-        }
+        ObjectUtil.checkNotNull(alloc, "alloc");
+        ObjectUtil.checkNotNull(initialBuffer, "initialBuffer");
         if (!initialBuffer.isDirect()) {
             throw new IllegalArgumentException("initialBuffer is not a direct buffer.");
         }
@@ -94,8 +92,8 @@ public class UnpooledDirectByteBuf extends AbstractReferenceCountedByteBuf {
         }
 
         this.alloc = alloc;
-        doNotFree = true;
-        setByteBuffer(initialBuffer.slice().order(ByteOrder.BIG_ENDIAN));
+        doNotFree = !doFree;
+        setByteBuffer((slice ? initialBuffer.slice() : initialBuffer).order(ByteOrder.BIG_ENDIAN), false);
         writerIndex(initialCapacity);
     }
 
@@ -113,13 +111,15 @@ public class UnpooledDirectByteBuf extends AbstractReferenceCountedByteBuf {
         PlatformDependent.freeDirectBuffer(buffer);
     }
 
-    private void setByteBuffer(ByteBuffer buffer) {
-        ByteBuffer oldBuffer = this.buffer;
-        if (oldBuffer != null) {
-            if (doNotFree) {
-                doNotFree = false;
-            } else {
-                freeDirect(oldBuffer);
+    void setByteBuffer(ByteBuffer buffer, boolean tryFree) {
+        if (tryFree) {
+            ByteBuffer oldBuffer = this.buffer;
+            if (oldBuffer != null) {
+                if (doNotFree) {
+                    doNotFree = false;
+                } else {
+                    freeDirect(oldBuffer);
+                }
             }
         }
 
@@ -141,35 +141,23 @@ public class UnpooledDirectByteBuf extends AbstractReferenceCountedByteBuf {
     @Override
     public ByteBuf capacity(int newCapacity) {
         checkNewCapacity(newCapacity);
-
-        int readerIndex = readerIndex();
-        int writerIndex = writerIndex();
-
         int oldCapacity = capacity;
+        if (newCapacity == oldCapacity) {
+            return this;
+        }
+        int bytesToCopy;
         if (newCapacity > oldCapacity) {
-            ByteBuffer oldBuffer = buffer;
-            ByteBuffer newBuffer = allocateDirect(newCapacity);
-            oldBuffer.position(0).limit(oldBuffer.capacity());
-            newBuffer.position(0).limit(oldBuffer.capacity());
-            newBuffer.put(oldBuffer);
-            newBuffer.clear();
-            setByteBuffer(newBuffer);
-        } else if (newCapacity < oldCapacity) {
-            ByteBuffer oldBuffer = buffer;
-            ByteBuffer newBuffer = allocateDirect(newCapacity);
-            if (readerIndex < newCapacity) {
-                if (writerIndex > newCapacity) {
-                    writerIndex(writerIndex = newCapacity);
-                }
-                oldBuffer.position(readerIndex).limit(writerIndex);
-                newBuffer.position(readerIndex).limit(writerIndex);
-                newBuffer.put(oldBuffer);
-                newBuffer.clear();
-            } else {
-                setIndex(newCapacity, newCapacity);
-            }
-            setByteBuffer(newBuffer);
+            bytesToCopy = oldCapacity;
+        } else {
+            trimIndicesToCapacity(newCapacity);
+            bytesToCopy = newCapacity;
         }
+        ByteBuffer oldBuffer = buffer;
+        ByteBuffer newBuffer = allocateDirect(newCapacity);
+        oldBuffer.position(0).limit(bytesToCopy);
+        newBuffer.position(0).limit(bytesToCopy);
+        newBuffer.put(oldBuffer).clear();
+        setByteBuffer(newBuffer, true);
         return this;
     }
 
@@ -310,7 +298,7 @@ public class UnpooledDirectByteBuf extends AbstractReferenceCountedByteBuf {
         return this;
     }
 
-    private void getBytes(int index, byte[] dst, int dstIndex, int length, boolean internal) {
+    void getBytes(int index, byte[] dst, int dstIndex, int length, boolean internal) {
         checkDstIndex(index, length, dstIndex, dst.length);
 
         ByteBuffer tmpBuf;
@@ -337,7 +325,7 @@ public class UnpooledDirectByteBuf extends AbstractReferenceCountedByteBuf {
         return this;
     }
 
-    private void getBytes(int index, ByteBuffer dst, boolean internal) {
+    void getBytes(int index, ByteBuffer dst, boolean internal) {
         checkIndex(index, dst.remaining());
 
         ByteBuffer tmpBuf;
@@ -486,7 +474,7 @@ public class UnpooledDirectByteBuf extends AbstractReferenceCountedByteBuf {
         return this;
     }
 
-    private void getBytes(int index, OutputStream out, int length, boolean internal) throws IOException {
+    void getBytes(int index, OutputStream out, int length, boolean internal) throws IOException {
         ensureAccessible();
         if (length == 0) {
             return;
@@ -579,7 +567,7 @@ public class UnpooledDirectByteBuf extends AbstractReferenceCountedByteBuf {
         ByteBuffer tmpBuf = internalNioBuffer();
         tmpBuf.clear().position(index).limit(index + length);
         try {
-            return in.read(tmpNioBuf);
+            return in.read(tmpBuf);
         } catch (ClosedChannelException ignored) {
             return -1;
         }
@@ -591,7 +579,7 @@ public class UnpooledDirectByteBuf extends AbstractReferenceCountedByteBuf {
         ByteBuffer tmpBuf = internalNioBuffer();
         tmpBuf.clear().position(index).limit(index + length);
         try {
-            return in.read(tmpNioBuf, position);
+            return in.read(tmpBuf, position);
         } catch (ClosedChannelException ignored) {
             return -1;
         }
@@ -607,6 +595,11 @@ public class UnpooledDirectByteBuf extends AbstractReferenceCountedByteBuf {
         return new ByteBuffer[] { nioBuffer(index, length) };
     }
 
+    @Override
+    public final boolean isContiguous() {
+        return true;
+    }
+
     @Override
     public ByteBuf copy(int index, int length) {
         ensureAccessible();
diff --git a/buffer/src/main/java/io/netty/buffer/UnpooledHeapByteBuf.java b/buffer/src/main/java/io/netty/buffer/UnpooledHeapByteBuf.java
index f37ceb0..f5d1fe5 100644
--- a/buffer/src/main/java/io/netty/buffer/UnpooledHeapByteBuf.java
+++ b/buffer/src/main/java/io/netty/buffer/UnpooledHeapByteBuf.java
@@ -50,14 +50,12 @@ public class UnpooledHeapByteBuf extends AbstractReferenceCountedByteBuf {
     public UnpooledHeapByteBuf(ByteBufAllocator alloc, int initialCapacity, int maxCapacity) {
         super(maxCapacity);
 
-        checkNotNull(alloc, "alloc");
-
         if (initialCapacity > maxCapacity) {
             throw new IllegalArgumentException(String.format(
                     "initialCapacity(%d) > maxCapacity(%d)", initialCapacity, maxCapacity));
         }
 
-        this.alloc = alloc;
+        this.alloc = checkNotNull(alloc, "alloc");
         setArray(allocateArray(initialCapacity));
         setIndex(0, 0);
     }
@@ -73,7 +71,6 @@ public class UnpooledHeapByteBuf extends AbstractReferenceCountedByteBuf {
 
         checkNotNull(alloc, "alloc");
         checkNotNull(initialArray, "initialArray");
-
         if (initialArray.length > maxCapacity) {
             throw new IllegalArgumentException(String.format(
                     "initialCapacity(%d) > maxCapacity(%d)", initialArray.length, maxCapacity));
@@ -120,29 +117,23 @@ public class UnpooledHeapByteBuf extends AbstractReferenceCountedByteBuf {
     @Override
     public ByteBuf capacity(int newCapacity) {
         checkNewCapacity(newCapacity);
-
-        int oldCapacity = array.length;
         byte[] oldArray = array;
+        int oldCapacity = oldArray.length;
+        if (newCapacity == oldCapacity) {
+            return this;
+        }
+
+        int bytesToCopy;
         if (newCapacity > oldCapacity) {
-            byte[] newArray = allocateArray(newCapacity);
-            System.arraycopy(oldArray, 0, newArray, 0, oldArray.length);
-            setArray(newArray);
-            freeArray(oldArray);
-        } else if (newCapacity < oldCapacity) {
-            byte[] newArray = allocateArray(newCapacity);
-            int readerIndex = readerIndex();
-            if (readerIndex < newCapacity) {
-                int writerIndex = writerIndex();
-                if (writerIndex > newCapacity) {
-                    writerIndex(writerIndex = newCapacity);
-                }
-                System.arraycopy(oldArray, readerIndex, newArray, readerIndex, writerIndex - readerIndex);
-            } else {
-                setIndex(newCapacity, newCapacity);
-            }
-            setArray(newArray);
-            freeArray(oldArray);
+            bytesToCopy = oldCapacity;
+        } else {
+            trimIndicesToCapacity(newCapacity);
+            bytesToCopy = newCapacity;
         }
+        byte[] newArray = allocateArray(newCapacity);
+        System.arraycopy(oldArray, 0, newArray, 0, bytesToCopy);
+        setArray(newArray);
+        freeArray(oldArray);
         return this;
     }
 
@@ -194,7 +185,7 @@ public class UnpooledHeapByteBuf extends AbstractReferenceCountedByteBuf {
 
     @Override
     public ByteBuf getBytes(int index, ByteBuffer dst) {
-        checkIndex(index, dst.remaining());
+        ensureAccessible();
         dst.put(array, index, dst.remaining());
         return this;
     }
@@ -326,6 +317,11 @@ public class UnpooledHeapByteBuf extends AbstractReferenceCountedByteBuf {
         return (ByteBuffer) internalNioBuffer().clear().position(index).limit(index + length);
     }
 
+    @Override
+    public final boolean isContiguous() {
+        return true;
+    }
+
     @Override
     public byte getByte(int index) {
         ensureAccessible();
@@ -536,9 +532,7 @@ public class UnpooledHeapByteBuf extends AbstractReferenceCountedByteBuf {
     @Override
     public ByteBuf copy(int index, int length) {
         checkIndex(index, length);
-        byte[] copiedArray = PlatformDependent.allocateUninitializedArray(length);
-        System.arraycopy(array, index, copiedArray, 0, length);
-        return new UnpooledHeapByteBuf(alloc(), copiedArray, maxCapacity());
+        return alloc().heapBuffer(length, maxCapacity()).writeBytes(array, index, length);
     }
 
     private ByteBuffer internalNioBuffer() {
diff --git a/buffer/src/main/java/io/netty/buffer/UnpooledUnsafeDirectByteBuf.java b/buffer/src/main/java/io/netty/buffer/UnpooledUnsafeDirectByteBuf.java
index 9d425e3..5526f3f 100644
--- a/buffer/src/main/java/io/netty/buffer/UnpooledUnsafeDirectByteBuf.java
+++ b/buffer/src/main/java/io/netty/buffer/UnpooledUnsafeDirectByteBuf.java
@@ -21,25 +21,14 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.nio.channels.ClosedChannelException;
-import java.nio.channels.FileChannel;
-import java.nio.channels.GatheringByteChannel;
-import java.nio.channels.ScatteringByteChannel;
 
 /**
  * A NIO {@link ByteBuffer} based buffer. It is recommended to use
  * {@link UnpooledByteBufAllocator#directBuffer(int, int)}, {@link Unpooled#directBuffer(int)} and
  * {@link Unpooled#wrappedBuffer(ByteBuffer)} instead of calling the constructor explicitly.}
  */
-public class UnpooledUnsafeDirectByteBuf extends AbstractReferenceCountedByteBuf {
+public class UnpooledUnsafeDirectByteBuf extends UnpooledDirectByteBuf {
 
-    private final ByteBufAllocator alloc;
-
-    private ByteBuffer tmpNioBuf;
-    private int capacity;
-    private boolean doNotFree;
-    ByteBuffer buffer;
     long memoryAddress;
 
     /**
@@ -49,23 +38,7 @@ public class UnpooledUnsafeDirectByteBuf extends AbstractReferenceCountedByteBuf
      * @param maxCapacity     the maximum capacity of the underlying direct buffer
      */
     public UnpooledUnsafeDirectByteBuf(ByteBufAllocator alloc, int initialCapacity, int maxCapacity) {
-        super(maxCapacity);
-        if (alloc == null) {
-            throw new NullPointerException("alloc");
-        }
-        if (initialCapacity < 0) {
-            throw new IllegalArgumentException("initialCapacity: " + initialCapacity);
-        }
-        if (maxCapacity < 0) {
-            throw new IllegalArgumentException("maxCapacity: " + maxCapacity);
-        }
-        if (initialCapacity > maxCapacity) {
-            throw new IllegalArgumentException(String.format(
-                    "initialCapacity(%d) > maxCapacity(%d)", initialCapacity, maxCapacity));
-        }
-
-        this.alloc = alloc;
-        setByteBuffer(allocateDirect(initialCapacity), false);
+        super(alloc, initialCapacity, maxCapacity);
     }
 
     /**
@@ -83,135 +56,17 @@ public class UnpooledUnsafeDirectByteBuf extends AbstractReferenceCountedByteBuf
         // sun/misc/Unsafe.java#l1250
         //
         // We also call slice() explicitly here to preserve behaviour with previous netty releases.
-        this(alloc, initialBuffer.slice(), maxCapacity, false);
+        super(alloc, initialBuffer, maxCapacity, /* doFree = */ false, /* slice = */ true);
     }
 
     UnpooledUnsafeDirectByteBuf(ByteBufAllocator alloc, ByteBuffer initialBuffer, int maxCapacity, boolean doFree) {
-        super(maxCapacity);
-        if (alloc == null) {
-            throw new NullPointerException("alloc");
-        }
-        if (initialBuffer == null) {
-            throw new NullPointerException("initialBuffer");
-        }
-        if (!initialBuffer.isDirect()) {
-            throw new IllegalArgumentException("initialBuffer is not a direct buffer.");
-        }
-        if (initialBuffer.isReadOnly()) {
-            throw new IllegalArgumentException("initialBuffer is a read-only buffer.");
-        }
-
-        int initialCapacity = initialBuffer.remaining();
-        if (initialCapacity > maxCapacity) {
-            throw new IllegalArgumentException(String.format(
-                    "initialCapacity(%d) > maxCapacity(%d)", initialCapacity, maxCapacity));
-        }
-
-        this.alloc = alloc;
-        doNotFree = !doFree;
-        setByteBuffer(initialBuffer.order(ByteOrder.BIG_ENDIAN), false);
-        writerIndex(initialCapacity);
-    }
-
-    /**
-     * Allocate a new direct {@link ByteBuffer} with the given initialCapacity.
-     */
-    protected ByteBuffer allocateDirect(int initialCapacity) {
-        return ByteBuffer.allocateDirect(initialCapacity);
-    }
-
-    /**
-     * Free a direct {@link ByteBuffer}
-     */
-    protected void freeDirect(ByteBuffer buffer) {
-        PlatformDependent.freeDirectBuffer(buffer);
+        super(alloc, initialBuffer, maxCapacity, doFree, false);
     }
 
+    @Override
     final void setByteBuffer(ByteBuffer buffer, boolean tryFree) {
-        if (tryFree) {
-            ByteBuffer oldBuffer = this.buffer;
-            if (oldBuffer != null) {
-                if (doNotFree) {
-                    doNotFree = false;
-                } else {
-                    freeDirect(oldBuffer);
-                }
-            }
-        }
-        this.buffer = buffer;
+        super.setByteBuffer(buffer, tryFree);
         memoryAddress = PlatformDependent.directBufferAddress(buffer);
-        tmpNioBuf = null;
-        capacity = buffer.remaining();
-    }
-
-    @Override
-    public boolean isDirect() {
-        return true;
-    }
-
-    @Override
-    public int capacity() {
-        return capacity;
-    }
-
-    @Override
-    public ByteBuf capacity(int newCapacity) {
-        checkNewCapacity(newCapacity);
-
-        int readerIndex = readerIndex();
-        int writerIndex = writerIndex();
-
-        int oldCapacity = capacity;
-        if (newCapacity > oldCapacity) {
-            ByteBuffer oldBuffer = buffer;
-            ByteBuffer newBuffer = allocateDirect(newCapacity);
-            oldBuffer.position(0).limit(oldBuffer.capacity());
-            newBuffer.position(0).limit(oldBuffer.capacity());
-            newBuffer.put(oldBuffer);
-            newBuffer.clear();
-            setByteBuffer(newBuffer, true);
-        } else if (newCapacity < oldCapacity) {
-            ByteBuffer oldBuffer = buffer;
-            ByteBuffer newBuffer = allocateDirect(newCapacity);
-            if (readerIndex < newCapacity) {
-                if (writerIndex > newCapacity) {
-                    writerIndex(writerIndex = newCapacity);
-                }
-                oldBuffer.position(readerIndex).limit(writerIndex);
-                newBuffer.position(readerIndex).limit(writerIndex);
-                newBuffer.put(oldBuffer);
-                newBuffer.clear();
-            } else {
-                setIndex(newCapacity, newCapacity);
-            }
-            setByteBuffer(newBuffer, true);
-        }
-        return this;
-    }
-
-    @Override
-    public ByteBufAllocator alloc() {
-        return alloc;
-    }
-
-    @Override
-    public ByteOrder order() {
-        return ByteOrder.BIG_ENDIAN;
-    }
-
-    @Override
-    public boolean hasArray() {
-        return false;
-    }
-
-    @Override
-    public byte[] array() {
-        throw new UnsupportedOperationException("direct buffer");
-    }
-
-    @Override
-    public int arrayOffset() {
-        throw new UnsupportedOperationException("direct buffer");
     }
 
     @Override
@@ -225,11 +80,23 @@ public class UnpooledUnsafeDirectByteBuf extends AbstractReferenceCountedByteBuf
         return memoryAddress;
     }
 
+    @Override
+    public byte getByte(int index) {
+        checkIndex(index);
+        return _getByte(index);
+    }
+
     @Override
     protected byte _getByte(int index) {
         return UnsafeByteBufUtil.getByte(addr(index));
     }
 
+    @Override
+    public short getShort(int index) {
+        checkIndex(index, 2);
+        return _getShort(index);
+    }
+
     @Override
     protected short _getShort(int index) {
         return UnsafeByteBufUtil.getShort(addr(index));
@@ -240,6 +107,12 @@ public class UnpooledUnsafeDirectByteBuf extends AbstractReferenceCountedByteBuf
         return UnsafeByteBufUtil.getShortLE(addr(index));
     }
 
+    @Override
+    public int getUnsignedMedium(int index) {
+        checkIndex(index, 3);
+        return _getUnsignedMedium(index);
+    }
+
     @Override
     protected int _getUnsignedMedium(int index) {
         return UnsafeByteBufUtil.getUnsignedMedium(addr(index));
@@ -250,6 +123,12 @@ public class UnpooledUnsafeDirectByteBuf extends AbstractReferenceCountedByteBuf
         return UnsafeByteBufUtil.getUnsignedMediumLE(addr(index));
     }
 
+    @Override
+    public int getInt(int index) {
+        checkIndex(index, 4);
+        return _getInt(index);
+    }
+
     @Override
     protected int _getInt(int index) {
         return UnsafeByteBufUtil.getInt(addr(index));
@@ -260,6 +139,12 @@ public class UnpooledUnsafeDirectByteBuf extends AbstractReferenceCountedByteBuf
         return UnsafeByteBufUtil.getIntLE(addr(index));
     }
 
+    @Override
+    public long getLong(int index) {
+        checkIndex(index, 8);
+        return _getLong(index);
+    }
+
     @Override
     protected long _getLong(int index) {
         return UnsafeByteBufUtil.getLong(addr(index));
@@ -277,23 +162,19 @@ public class UnpooledUnsafeDirectByteBuf extends AbstractReferenceCountedByteBuf
     }
 
     @Override
-    public ByteBuf getBytes(int index, byte[] dst, int dstIndex, int length) {
+    void getBytes(int index, byte[] dst, int dstIndex, int length, boolean internal) {
         UnsafeByteBufUtil.getBytes(this, addr(index), index, dst, dstIndex, length);
-        return this;
     }
 
     @Override
-    public ByteBuf getBytes(int index, ByteBuffer dst) {
+    void getBytes(int index, ByteBuffer dst, boolean internal) {
         UnsafeByteBufUtil.getBytes(this, addr(index), index, dst);
-        return this;
     }
 
     @Override
-    public ByteBuf readBytes(ByteBuffer dst) {
-        int length = dst.remaining();
-        checkReadableBytes(length);
-        getBytes(readerIndex, dst);
-        readerIndex += length;
+    public ByteBuf setByte(int index, int value) {
+        checkIndex(index);
+        _setByte(index, value);
         return this;
     }
 
@@ -302,6 +183,13 @@ public class UnpooledUnsafeDirectByteBuf extends AbstractReferenceCountedByteBuf
         UnsafeByteBufUtil.setByte(addr(index), value);
     }
 
+    @Override
+    public ByteBuf setShort(int index, int value) {
+        checkIndex(index, 2);
+        _setShort(index, value);
+        return this;
+    }
+
     @Override
     protected void _setShort(int index, int value) {
         UnsafeByteBufUtil.setShort(addr(index), value);
@@ -312,6 +200,13 @@ public class UnpooledUnsafeDirectByteBuf extends AbstractReferenceCountedByteBuf
         UnsafeByteBufUtil.setShortLE(addr(index), value);
     }
 
+    @Override
+    public ByteBuf setMedium(int index, int value) {
+        checkIndex(index, 3);
+        _setMedium(index, value);
+        return this;
+    }
+
     @Override
     protected void _setMedium(int index, int value) {
         UnsafeByteBufUtil.setMedium(addr(index), value);
@@ -322,6 +217,13 @@ public class UnpooledUnsafeDirectByteBuf extends AbstractReferenceCountedByteBuf
         UnsafeByteBufUtil.setMediumLE(addr(index), value);
     }
 
+    @Override
+    public ByteBuf setInt(int index, int value) {
+        checkIndex(index, 4);
+        _setInt(index, value);
+        return this;
+    }
+
     @Override
     protected void _setInt(int index, int value) {
         UnsafeByteBufUtil.setInt(addr(index), value);
@@ -332,6 +234,13 @@ public class UnpooledUnsafeDirectByteBuf extends AbstractReferenceCountedByteBuf
         UnsafeByteBufUtil.setIntLE(addr(index), value);
     }
 
+    @Override
+    public ByteBuf setLong(int index, long value) {
+        checkIndex(index, 8);
+        _setLong(index, value);
+        return this;
+    }
+
     @Override
     protected void _setLong(int index, long value) {
         UnsafeByteBufUtil.setLong(addr(index), value);
@@ -361,62 +270,8 @@ public class UnpooledUnsafeDirectByteBuf extends AbstractReferenceCountedByteBuf
     }
 
     @Override
-    public ByteBuf getBytes(int index, OutputStream out, int length) throws IOException {
+    void getBytes(int index, OutputStream out, int length, boolean internal) throws IOException {
         UnsafeByteBufUtil.getBytes(this, addr(index), index, out, length);
-        return this;
-    }
-
-    @Override
-    public int getBytes(int index, GatheringByteChannel out, int length) throws IOException {
-        return getBytes(index, out, length, false);
-    }
-
-    private int getBytes(int index, GatheringByteChannel out, int length, boolean internal) throws IOException {
-        ensureAccessible();
-        if (length == 0) {
-            return 0;
-        }
-
-        ByteBuffer tmpBuf;
-        if (internal) {
-            tmpBuf = internalNioBuffer();
-        } else {
-            tmpBuf = buffer.duplicate();
-        }
-        tmpBuf.clear().position(index).limit(index + length);
-        return out.write(tmpBuf);
-    }
-
-    @Override
-    public int getBytes(int index, FileChannel out, long position, int length) throws IOException {
-        return getBytes(index, out, position, length, false);
-    }
-
-    private int getBytes(int index, FileChannel out, long position, int length, boolean internal) throws IOException {
-        ensureAccessible();
-        if (length == 0) {
-            return 0;
-        }
-
-        ByteBuffer tmpBuf = internal ? internalNioBuffer() : buffer.duplicate();
-        tmpBuf.clear().position(index).limit(index + length);
-        return out.write(tmpBuf, position);
-    }
-
-    @Override
-    public int readBytes(GatheringByteChannel out, int length) throws IOException {
-        checkReadableBytes(length);
-        int readBytes = getBytes(readerIndex, out, length, true);
-        readerIndex += readBytes;
-        return readBytes;
-    }
-
-    @Override
-    public int readBytes(FileChannel out, long position, int length) throws IOException {
-        checkReadableBytes(length);
-        int readBytes = getBytes(readerIndex, out, position, length, true);
-        readerIndex += readBytes;
-        return readBytes;
     }
 
     @Override
@@ -424,85 +279,12 @@ public class UnpooledUnsafeDirectByteBuf extends AbstractReferenceCountedByteBuf
         return UnsafeByteBufUtil.setBytes(this, addr(index), index, in, length);
     }
 
-    @Override
-    public int setBytes(int index, ScatteringByteChannel in, int length) throws IOException {
-        ensureAccessible();
-        ByteBuffer tmpBuf = internalNioBuffer();
-        tmpBuf.clear().position(index).limit(index + length);
-        try {
-            return in.read(tmpBuf);
-        } catch (ClosedChannelException ignored) {
-            return -1;
-        }
-    }
-
-    @Override
-    public int setBytes(int index, FileChannel in, long position, int length) throws IOException {
-        ensureAccessible();
-        ByteBuffer tmpBuf = internalNioBuffer();
-        tmpBuf.clear().position(index).limit(index + length);
-        try {
-            return in.read(tmpBuf, position);
-        } catch (ClosedChannelException ignored) {
-            return -1;
-        }
-    }
-
-    @Override
-    public int nioBufferCount() {
-        return 1;
-    }
-
-    @Override
-    public ByteBuffer[] nioBuffers(int index, int length) {
-        return new ByteBuffer[] { nioBuffer(index, length) };
-    }
-
     @Override
     public ByteBuf copy(int index, int length) {
         return UnsafeByteBufUtil.copy(this, addr(index), index, length);
     }
 
-    @Override
-    public ByteBuffer internalNioBuffer(int index, int length) {
-        checkIndex(index, length);
-        return (ByteBuffer) internalNioBuffer().clear().position(index).limit(index + length);
-    }
-
-    private ByteBuffer internalNioBuffer() {
-        ByteBuffer tmpNioBuf = this.tmpNioBuf;
-        if (tmpNioBuf == null) {
-            this.tmpNioBuf = tmpNioBuf = buffer.duplicate();
-        }
-        return tmpNioBuf;
-    }
-
-    @Override
-    public ByteBuffer nioBuffer(int index, int length) {
-        checkIndex(index, length);
-        return ((ByteBuffer) buffer.duplicate().position(index).limit(index + length)).slice();
-    }
-
-    @Override
-    protected void deallocate() {
-        ByteBuffer buffer = this.buffer;
-        if (buffer == null) {
-            return;
-        }
-
-        this.buffer = null;
-
-        if (!doNotFree) {
-            freeDirect(buffer);
-        }
-    }
-
-    @Override
-    public ByteBuf unwrap() {
-        return null;
-    }
-
-    long addr(int index) {
+    final long addr(int index) {
         return memoryAddress + index;
     }
 
diff --git a/buffer/src/main/java/io/netty/buffer/UnpooledUnsafeHeapByteBuf.java b/buffer/src/main/java/io/netty/buffer/UnpooledUnsafeHeapByteBuf.java
index 0fbe856..ee3a86e 100644
--- a/buffer/src/main/java/io/netty/buffer/UnpooledUnsafeHeapByteBuf.java
+++ b/buffer/src/main/java/io/netty/buffer/UnpooledUnsafeHeapByteBuf.java
@@ -17,7 +17,12 @@ package io.netty.buffer;
 
 import io.netty.util.internal.PlatformDependent;
 
-class UnpooledUnsafeHeapByteBuf extends UnpooledHeapByteBuf {
+/**
+ * Big endian Java heap buffer implementation. It is recommended to use
+ * {@link UnpooledByteBufAllocator#heapBuffer(int, int)}, {@link Unpooled#buffer(int)} and
+ * {@link Unpooled#wrappedBuffer(byte[])} instead of calling the constructor explicitly.
+ */
+public class UnpooledUnsafeHeapByteBuf extends UnpooledHeapByteBuf {
 
     /**
      * Creates a new heap buffer with a newly allocated byte array.
@@ -25,7 +30,7 @@ class UnpooledUnsafeHeapByteBuf extends UnpooledHeapByteBuf {
      * @param initialCapacity the initial capacity of the underlying byte array
      * @param maxCapacity the max capacity of the underlying byte array
      */
-    UnpooledUnsafeHeapByteBuf(ByteBufAllocator alloc, int initialCapacity, int maxCapacity) {
+    public UnpooledUnsafeHeapByteBuf(ByteBufAllocator alloc, int initialCapacity, int maxCapacity) {
         super(alloc, initialCapacity, maxCapacity);
     }
 
diff --git a/buffer/src/main/java/io/netty/buffer/UnpooledUnsafeNoCleanerDirectByteBuf.java b/buffer/src/main/java/io/netty/buffer/UnpooledUnsafeNoCleanerDirectByteBuf.java
index 3b9c05b..cc00e86 100644
--- a/buffer/src/main/java/io/netty/buffer/UnpooledUnsafeNoCleanerDirectByteBuf.java
+++ b/buffer/src/main/java/io/netty/buffer/UnpooledUnsafeNoCleanerDirectByteBuf.java
@@ -48,18 +48,8 @@ class UnpooledUnsafeNoCleanerDirectByteBuf extends UnpooledUnsafeDirectByteBuf {
             return this;
         }
 
-        ByteBuffer newBuffer = reallocateDirect(buffer, newCapacity);
-
-        if (newCapacity < oldCapacity) {
-            if (readerIndex() < newCapacity) {
-                if (writerIndex() > newCapacity) {
-                    writerIndex(newCapacity);
-                }
-            } else {
-                setIndex(newCapacity, newCapacity);
-            }
-        }
-        setByteBuffer(newBuffer, false);
+        trimIndicesToCapacity(newCapacity);
+        setByteBuffer(reallocateDirect(buffer, newCapacity), false);
         return this;
     }
 }
diff --git a/buffer/src/main/java/io/netty/buffer/UnreleasableByteBuf.java b/buffer/src/main/java/io/netty/buffer/UnreleasableByteBuf.java
index ba06103..ad6dcaf 100644
--- a/buffer/src/main/java/io/netty/buffer/UnreleasableByteBuf.java
+++ b/buffer/src/main/java/io/netty/buffer/UnreleasableByteBuf.java
@@ -15,6 +15,8 @@
  */
 package io.netty.buffer;
 
+import io.netty.util.internal.ObjectUtil;
+
 import java.nio.ByteOrder;
 
 /**
@@ -31,10 +33,7 @@ final class UnreleasableByteBuf extends WrappedByteBuf {
 
     @Override
     public ByteBuf order(ByteOrder endianness) {
-        if (endianness == null) {
-            throw new NullPointerException("endianness");
-        }
-        if (endianness == order()) {
+        if (ObjectUtil.checkNotNull(endianness, "endianness") == order()) {
             return this;
         }
 
diff --git a/buffer/src/main/java/io/netty/buffer/WrappedByteBuf.java b/buffer/src/main/java/io/netty/buffer/WrappedByteBuf.java
index 45aa60c..f682f1b 100644
--- a/buffer/src/main/java/io/netty/buffer/WrappedByteBuf.java
+++ b/buffer/src/main/java/io/netty/buffer/WrappedByteBuf.java
@@ -17,6 +17,7 @@
 package io.netty.buffer;
 
 import io.netty.util.ByteProcessor;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.StringUtil;
 
 import java.io.IOException;
@@ -41,10 +42,7 @@ class WrappedByteBuf extends ByteBuf {
     protected final ByteBuf buf;
 
     protected WrappedByteBuf(ByteBuf buf) {
-        if (buf == null) {
-            throw new NullPointerException("buf");
-        }
-        this.buf = buf;
+        this.buf = ObjectUtil.checkNotNull(buf, "buf");
     }
 
     @Override
@@ -52,6 +50,11 @@ class WrappedByteBuf extends ByteBuf {
         return buf.hasMemoryAddress();
     }
 
+    @Override
+    public boolean isContiguous() {
+        return buf.isContiguous();
+    }
+
     @Override
     public final long memoryAddress() {
         return buf.memoryAddress();
@@ -151,6 +154,11 @@ class WrappedByteBuf extends ByteBuf {
         return buf.maxWritableBytes();
     }
 
+    @Override
+    public int maxFastWritableBytes() {
+        return buf.maxFastWritableBytes();
+    }
+
     @Override
     public final boolean isReadable() {
         return buf.isReadable();
@@ -1033,4 +1041,9 @@ class WrappedByteBuf extends ByteBuf {
     public boolean release(int decrement) {
         return buf.release(decrement);
     }
+
+    @Override
+    final boolean isAccessible() {
+        return buf.isAccessible();
+    }
 }
diff --git a/buffer/src/main/java/io/netty/buffer/WrappedCompositeByteBuf.java b/buffer/src/main/java/io/netty/buffer/WrappedCompositeByteBuf.java
index b124eb2..e58623a 100644
--- a/buffer/src/main/java/io/netty/buffer/WrappedCompositeByteBuf.java
+++ b/buffer/src/main/java/io/netty/buffer/WrappedCompositeByteBuf.java
@@ -98,6 +98,11 @@ class WrappedCompositeByteBuf extends CompositeByteBuf {
         return wrapped.maxWritableBytes();
     }
 
+    @Override
+    public int maxFastWritableBytes() {
+        return wrapped.maxFastWritableBytes();
+    }
+
     @Override
     public int ensureWritable(int minWritableBytes, boolean force) {
         return wrapped.ensureWritable(minWritableBytes, force);
@@ -424,8 +429,8 @@ class WrappedCompositeByteBuf extends CompositeByteBuf {
     }
 
     @Override
-    int internalRefCnt() {
-        return wrapped.internalRefCnt();
+    final boolean isAccessible() {
+        return wrapped.isAccessible();
     }
 
     @Override
@@ -548,6 +553,12 @@ class WrappedCompositeByteBuf extends CompositeByteBuf {
         return this;
     }
 
+    @Override
+    public CompositeByteBuf addFlattenedComponents(boolean increaseWriterIndex, ByteBuf buffer) {
+        wrapped.addFlattenedComponents(increaseWriterIndex, buffer);
+        return this;
+    }
+
     @Override
     public CompositeByteBuf removeComponent(int cIndex) {
         wrapped.removeComponent(cIndex);
diff --git a/buffer/src/main/resources/META-INF/native-image/io.netty/buffer/native-image.properties b/buffer/src/main/resources/META-INF/native-image/io.netty/buffer/native-image.properties
new file mode 100644
index 0000000..33f761b
--- /dev/null
+++ b/buffer/src/main/resources/META-INF/native-image/io.netty/buffer/native-image.properties
@@ -0,0 +1,15 @@
+# Copyright 2019 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.
+
+Args = --initialize-at-run-time=io.netty.buffer.PooledByteBufAllocator,io.netty.buffer.ByteBufAllocator,io.netty.buffer.ByteBufUtil,io.netty.buffer.AbstractReferenceCountedByteBuf
diff --git a/buffer/src/test/java/io/netty/buffer/AbstractByteBufTest.java b/buffer/src/test/java/io/netty/buffer/AbstractByteBufTest.java
index 59194ab..a3a1347 100644
--- a/buffer/src/test/java/io/netty/buffer/AbstractByteBufTest.java
+++ b/buffer/src/test/java/io/netty/buffer/AbstractByteBufTest.java
@@ -2125,6 +2125,9 @@ public abstract class AbstractByteBufTest {
     @Test
     public void testIndexOf() {
         buffer.clear();
+        // Ensure the buffer is completely zero'ed.
+        buffer.setZero(0, buffer.capacity());
+
         buffer.writeByte((byte) 1);
         buffer.writeByte((byte) 2);
         buffer.writeByte((byte) 3);
@@ -2135,6 +2138,38 @@ public abstract class AbstractByteBufTest {
         assertEquals(-1, buffer.indexOf(4, 1, (byte) 1));
         assertEquals(1, buffer.indexOf(1, 4, (byte) 2));
         assertEquals(3, buffer.indexOf(4, 1, (byte) 2));
+
+        try {
+            buffer.indexOf(0, buffer.capacity() + 1, (byte) 0);
+            fail();
+        } catch (IndexOutOfBoundsException expected) {
+            // expected
+        }
+
+        try {
+            buffer.indexOf(buffer.capacity(), -1, (byte) 0);
+            fail();
+        } catch (IndexOutOfBoundsException expected) {
+            // expected
+        }
+
+        assertEquals(4, buffer.indexOf(buffer.capacity() + 1, 0, (byte) 1));
+        assertEquals(0, buffer.indexOf(-1, buffer.capacity(), (byte) 1));
+    }
+
+    @Test
+    public void testIndexOfReleaseBuffer() {
+        ByteBuf buffer = releasedBuffer();
+        if (buffer.capacity() != 0) {
+            try {
+                buffer.indexOf(0, 1, (byte) 1);
+                fail();
+            } catch (IllegalReferenceCountException expected) {
+                // expected
+            }
+        } else {
+            assertEquals(-1, buffer.indexOf(0, 1, (byte) 1));
+        }
     }
 
     @Test
@@ -2570,7 +2605,6 @@ public abstract class AbstractByteBufTest {
 
     private ByteBuf releasedBuffer() {
         ByteBuf buffer = newBuffer(8);
-
         // Clear the buffer so we are sure the reader and writer indices are 0.
         // This is important as we may return a slice from newBuffer(...).
         buffer.clear();
@@ -4878,4 +4912,16 @@ public abstract class AbstractByteBufTest {
             buffer.release();
         }
     }
+
+    @Test
+    public void testMaxFastWritableBytes() {
+        ByteBuf buffer = newBuffer(150, 500).writerIndex(100);
+        assertEquals(50, buffer.writableBytes());
+        assertEquals(150, buffer.capacity());
+        assertEquals(500, buffer.maxCapacity());
+        assertEquals(400, buffer.maxWritableBytes());
+        // Default implementation has fast writable == writable
+        assertEquals(50, buffer.maxFastWritableBytes());
+        buffer.release();
+    }
 }
diff --git a/buffer/src/test/java/io/netty/buffer/AbstractCompositeByteBufTest.java b/buffer/src/test/java/io/netty/buffer/AbstractCompositeByteBufTest.java
index c51a991..2c198ac 100644
--- a/buffer/src/test/java/io/netty/buffer/AbstractCompositeByteBufTest.java
+++ b/buffer/src/test/java/io/netty/buffer/AbstractCompositeByteBufTest.java
@@ -16,6 +16,7 @@
 package io.netty.buffer;
 
 import io.netty.util.ReferenceCountUtil;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.PlatformDependent;
 import org.junit.Assume;
 import org.junit.Test;
@@ -57,10 +58,7 @@ public abstract class AbstractCompositeByteBufTest extends AbstractByteBufTest {
     private final ByteOrder order;
 
     protected AbstractCompositeByteBufTest(ByteOrder order) {
-        if (order == null) {
-            throw new NullPointerException("order");
-        }
-        this.order = order;
+        this.order = ObjectUtil.checkNotNull(order, "order");
     }
 
     @Override
@@ -109,6 +107,13 @@ public abstract class AbstractCompositeByteBufTest extends AbstractByteBufTest {
         return false;
     }
 
+    @Test
+    public void testIsContiguous() {
+        ByteBuf buf = newBuffer(4);
+        assertFalse(buf.isContiguous());
+        buf.release();
+    }
+
     /**
      * Tests the "getBufferFor" method
      */
@@ -135,6 +140,41 @@ public abstract class AbstractCompositeByteBufTest extends AbstractByteBufTest {
         buf.release();
     }
 
+    @Test
+    public void testToComponentIndex() {
+        CompositeByteBuf buf = (CompositeByteBuf) wrappedBuffer(new byte[]{1, 2, 3, 4, 5},
+                new byte[]{4, 5, 6, 7, 8, 9, 26}, new byte[]{10, 9, 8, 7, 6, 5, 33});
+
+        // spot checks
+        assertEquals(0, buf.toComponentIndex(4));
+        assertEquals(1, buf.toComponentIndex(5));
+        assertEquals(2, buf.toComponentIndex(15));
+
+        //Loop through each byte
+
+        byte index = 0;
+
+        while (index < buf.capacity()) {
+            int cindex = buf.toComponentIndex(index++);
+            assertTrue(cindex >= 0 && cindex < buf.numComponents());
+        }
+
+        buf.release();
+    }
+
+    @Test
+    public void testToByteIndex() {
+        CompositeByteBuf buf = (CompositeByteBuf) wrappedBuffer(new byte[]{1, 2, 3, 4, 5},
+                new byte[]{4, 5, 6, 7, 8, 9, 26}, new byte[]{10, 9, 8, 7, 6, 5, 33});
+
+        // spot checks
+        assertEquals(0, buf.toByteIndex(0));
+        assertEquals(5, buf.toByteIndex(1));
+        assertEquals(12, buf.toByteIndex(2));
+
+        buf.release();
+    }
+
     @Test
     public void testDiscardReadBytes3() {
         ByteBuf a, b;
@@ -747,6 +787,20 @@ public abstract class AbstractCompositeByteBufTest extends AbstractByteBufTest {
         buf.release();
     }
 
+    @Test
+    public void testRemoveComponents() {
+        CompositeByteBuf buf = compositeBuffer();
+        for (int i = 0; i < 10; i++) {
+            buf.addComponent(wrappedBuffer(new byte[]{1, 2}));
+        }
+        assertEquals(10, buf.numComponents());
+        assertEquals(20, buf.capacity());
+        buf.removeComponents(4, 3);
+        assertEquals(7, buf.numComponents());
+        assertEquals(14, buf.capacity());
+        buf.release();
+    }
+
     @Test
     public void testGatheringWritesHeap() throws Exception {
         testGatheringWrites(buffer().order(order), buffer().order(order));
@@ -928,7 +982,27 @@ public abstract class AbstractCompositeByteBufTest extends AbstractByteBufTest {
     @Override
     @Test
     public void testInternalNioBuffer() {
-        // ignore
+        CompositeByteBuf buf = compositeBuffer();
+        assertEquals(0, buf.internalNioBuffer(0, 0).remaining());
+
+        // If non-derived buffer is added, its internal buffer should be returned
+        ByteBuf concreteBuffer = directBuffer().writeByte(1);
+        buf.addComponent(concreteBuffer);
+        assertSame(concreteBuffer.internalNioBuffer(0, 1), buf.internalNioBuffer(0, 1));
+        buf.release();
+
+        // In derived cases, the original internal buffer must not be used
+        buf = compositeBuffer();
+        concreteBuffer = directBuffer().writeByte(1);
+        buf.addComponent(concreteBuffer.slice());
+        assertNotSame(concreteBuffer.internalNioBuffer(0, 1), buf.internalNioBuffer(0, 1));
+        buf.release();
+
+        buf = compositeBuffer();
+        concreteBuffer = directBuffer().writeByte(1);
+        buf.addComponent(concreteBuffer.duplicate());
+        assertNotSame(concreteBuffer.internalNioBuffer(0, 1), buf.internalNioBuffer(0, 1));
+        buf.release();
     }
 
     @Test
@@ -1047,6 +1121,71 @@ public abstract class AbstractCompositeByteBufTest extends AbstractByteBufTest {
         cbuf.release();
     }
 
+    @Test
+    public void testAddFlattenedComponents() {
+        ByteBuf b1 = Unpooled.wrappedBuffer(new byte[] { 1, 2, 3 });
+        CompositeByteBuf newComposite = Unpooled.compositeBuffer()
+                .addComponent(true, b1)
+                .addFlattenedComponents(true, b1.retain())
+                .addFlattenedComponents(true, Unpooled.EMPTY_BUFFER);
+
+        assertEquals(2, newComposite.numComponents());
+        assertEquals(6, newComposite.capacity());
+        assertEquals(6, newComposite.writerIndex());
+
+        // It is important to use a pooled allocator here to ensure
+        // the slices returned by readRetainedSlice are of type
+        // PooledSlicedByteBuf, which maintains an independent refcount
+        // (so that we can be sure to cover this case)
+        ByteBuf buffer = PooledByteBufAllocator.DEFAULT.buffer()
+              .writeBytes(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
+
+        // use mixture of slice and retained slice
+        ByteBuf s1 = buffer.readRetainedSlice(2);
+        ByteBuf s2 = s1.retainedSlice(0, 2);
+        ByteBuf s3 = buffer.slice(0, 2).retain();
+        ByteBuf s4 = s2.retainedSlice(0, 2);
+        buffer.release();
+
+        ByteBuf compositeToAdd = Unpooled.compositeBuffer()
+            .addComponent(s1)
+            .addComponent(Unpooled.EMPTY_BUFFER)
+            .addComponents(s2, s3, s4);
+        // set readable range to be from middle of first component
+        // to middle of penultimate component
+        compositeToAdd.setIndex(1, 5);
+
+        assertEquals(1, compositeToAdd.refCnt());
+        assertEquals(1, s4.refCnt());
+
+        ByteBuf compositeCopy = compositeToAdd.copy();
+
+        newComposite.addFlattenedComponents(true, compositeToAdd);
+
+        // verify that added range matches
+        ByteBufUtil.equals(compositeCopy, 0,
+                newComposite, 6, compositeCopy.readableBytes());
+
+        // should not include empty component or last component
+        // (latter outside of the readable range)
+        assertEquals(5, newComposite.numComponents());
+        assertEquals(10, newComposite.capacity());
+        assertEquals(10, newComposite.writerIndex());
+
+        assertEquals(0, compositeToAdd.refCnt());
+        // s4 wasn't in added range so should have been jettisoned
+        assertEquals(0, s4.refCnt());
+        assertEquals(1, newComposite.refCnt());
+
+        // releasing composite should release the remaining components
+        newComposite.release();
+        assertEquals(0, newComposite.refCnt());
+        assertEquals(0, s1.refCnt());
+        assertEquals(0, s2.refCnt());
+        assertEquals(0, s3.refCnt());
+        assertEquals(0, b1.refCnt());
+    }
+
     @Test
     public void testIterator() {
         CompositeByteBuf cbuf = compositeBuffer();
@@ -1203,6 +1342,40 @@ public abstract class AbstractCompositeByteBufTest extends AbstractByteBufTest {
         assertEquals(0, b2.refCnt());
     }
 
+    @Test
+    public void testReleasesOnShrink2() {
+        // It is important to use a pooled allocator here to ensure
+        // the slices returned by readRetainedSlice are of type
+        // PooledSlicedByteBuf, which maintains an independent refcount
+        // (so that we can be sure to cover this case)
+        ByteBuf buffer = PooledByteBufAllocator.DEFAULT.buffer();
+
+        buffer.writeShort(1).writeShort(2);
+
+        ByteBuf b1 = buffer.readRetainedSlice(2);
+        ByteBuf b2 = b1.retainedSlice(b1.readerIndex(), 2);
+
+        // composite takes ownership of b1 and b2
+        ByteBuf composite = Unpooled.compositeBuffer()
+            .addComponents(b1, b2);
+
+        assertEquals(4, composite.capacity());
+
+        // reduce capacity down to two, will drop the second component
+        composite.capacity(2);
+        assertEquals(2, composite.capacity());
+
+        // releasing composite should release the components
+        composite.release();
+        assertEquals(0, composite.refCnt());
+        assertEquals(0, b1.refCnt());
+        assertEquals(0, b2.refCnt());
+
+        // release last remaining ref to buffer
+        buffer.release();
+        assertEquals(0, buffer.refCnt());
+    }
+
     @Test
     public void testAllocatorIsSameWhenCopy() {
         testAllocatorIsSameWhenCopy(false);
@@ -1295,4 +1468,70 @@ public abstract class AbstractCompositeByteBufTest extends AbstractByteBufTest {
         assertTrue(buf.release());
     }
 
+    @Test
+    public void testDiscardSomeReadBytesCorrectlyUpdatesLastAccessed() {
+        testDiscardCorrectlyUpdatesLastAccessed(true);
+    }
+
+    @Test
+    public void testDiscardReadBytesCorrectlyUpdatesLastAccessed() {
+        testDiscardCorrectlyUpdatesLastAccessed(false);
+    }
+
+    private static void testDiscardCorrectlyUpdatesLastAccessed(boolean discardSome) {
+        CompositeByteBuf cbuf = compositeBuffer();
+        List<ByteBuf> buffers = new ArrayList<ByteBuf>(4);
+        for (int i = 0; i < 4; i++) {
+            ByteBuf buf = buffer().writeInt(i);
+            cbuf.addComponent(true, buf);
+            buffers.add(buf);
+        }
+
+        // Skip the first 2 bytes which means even if we call discard*ReadBytes() later we can no drop the first
+        // component as it is still used.
+        cbuf.skipBytes(2);
+        if (discardSome) {
+            cbuf.discardSomeReadBytes();
+        } else {
+            cbuf.discardReadBytes();
+        }
+        assertEquals(4, cbuf.numComponents());
+
+        // Now skip 3 bytes which means we should be able to drop the first component on the next discard*ReadBytes()
+        // call.
+        cbuf.skipBytes(3);
+
+        if (discardSome) {
+            cbuf.discardSomeReadBytes();
+        } else {
+            cbuf.discardReadBytes();
+        }
+        assertEquals(3, cbuf.numComponents());
+        // Now skip again 3 bytes which should bring our readerIndex == start of the 3 component.
+        cbuf.skipBytes(3);
+
+        // Read one int (4 bytes) which should bring our readerIndex == start of the 4 component.
+        assertEquals(2, cbuf.readInt());
+        if (discardSome) {
+            cbuf.discardSomeReadBytes();
+        } else {
+            cbuf.discardReadBytes();
+        }
+
+        // Now all except the last component should have been dropped / released.
+        assertEquals(1, cbuf.numComponents());
+        assertEquals(3, cbuf.readInt());
+        if (discardSome) {
+            cbuf.discardSomeReadBytes();
+        } else {
+            cbuf.discardReadBytes();
+        }
+        assertEquals(0, cbuf.numComponents());
+
+        // These should have been released already.
+        for (ByteBuf buffer: buffers) {
+            assertEquals(0, buffer.refCnt());
+        }
+        assertTrue(cbuf.release());
+    }
 }
diff --git a/buffer/src/test/java/io/netty/buffer/AbstractPooledByteBufTest.java b/buffer/src/test/java/io/netty/buffer/AbstractPooledByteBufTest.java
index eb76157..c7f5815 100644
--- a/buffer/src/test/java/io/netty/buffer/AbstractPooledByteBufTest.java
+++ b/buffer/src/test/java/io/netty/buffer/AbstractPooledByteBufTest.java
@@ -21,6 +21,8 @@ import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.greaterThanOrEqualTo;
 import static org.hamcrest.Matchers.is;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 public abstract class AbstractPooledByteBufTest extends AbstractByteBufTest {
@@ -59,4 +61,68 @@ public abstract class AbstractPooledByteBufTest extends AbstractByteBufTest {
             buf.release();
         }
     }
+
+    @Override
+    @Test
+    public void testMaxFastWritableBytes() {
+        ByteBuf buffer = newBuffer(150, 500).writerIndex(100);
+        assertEquals(50, buffer.writableBytes());
+        assertEquals(150, buffer.capacity());
+        assertEquals(500, buffer.maxCapacity());
+        assertEquals(400, buffer.maxWritableBytes());
+
+        int chunkSize = pooledByteBuf(buffer).maxLength;
+        assertTrue(chunkSize >= 150);
+        int remainingInAlloc = Math.min(chunkSize - 100, 400);
+        assertEquals(remainingInAlloc, buffer.maxFastWritableBytes());
+
+        // write up to max, chunk alloc should not change (same handle)
+        long handleBefore = pooledByteBuf(buffer).handle;
+        buffer.writeBytes(new byte[remainingInAlloc]);
+        assertEquals(handleBefore, pooledByteBuf(buffer).handle);
+
+        assertEquals(0, buffer.maxFastWritableBytes());
+        // writing one more should trigger a reallocation (new handle)
+        buffer.writeByte(7);
+        assertNotEquals(handleBefore, pooledByteBuf(buffer).handle);
+
+        // should not exceed maxCapacity even if chunk alloc does
+        buffer.capacity(500);
+        assertEquals(500 - buffer.writerIndex(), buffer.maxFastWritableBytes());
+        buffer.release();
+    }
+
+    private static PooledByteBuf<?> pooledByteBuf(ByteBuf buffer) {
+        // might need to unwrap if swapped (LE) and/or leak-aware-wrapped
+        while (!(buffer instanceof PooledByteBuf)) {
+            buffer = buffer.unwrap();
+        }
+        return (PooledByteBuf<?>) buffer;
+    }
+
+    @Test
+    public void testEnsureWritableDoesntGrowTooMuch() {
+        ByteBuf buffer = newBuffer(150, 500).writerIndex(100);
+
+        assertEquals(50, buffer.writableBytes());
+        int fastWritable = buffer.maxFastWritableBytes();
+        assertTrue(fastWritable > 50);
+
+        long handleBefore = pooledByteBuf(buffer).handle;
+
+        // capacity expansion should not cause reallocation
+        // (should grow precisely the specified amount)
+        buffer.ensureWritable(fastWritable);
+        assertEquals(handleBefore, pooledByteBuf(buffer).handle);
+        assertEquals(100 + fastWritable, buffer.capacity());
+        assertEquals(buffer.writableBytes(), buffer.maxFastWritableBytes());
+        buffer.release();
+    }
+
+    @Test
+    public void testIsContiguous() {
+        ByteBuf buf = newBuffer(4);
+        assertTrue(buf.isContiguous());
+        buf.release();
+    }
 }
diff --git a/buffer/src/test/java/io/netty/buffer/AdvancedLeakAwareByteBufTest.java b/buffer/src/test/java/io/netty/buffer/AdvancedLeakAwareByteBufTest.java
index 4e7747b..60b84d0 100644
--- a/buffer/src/test/java/io/netty/buffer/AdvancedLeakAwareByteBufTest.java
+++ b/buffer/src/test/java/io/netty/buffer/AdvancedLeakAwareByteBufTest.java
@@ -15,6 +15,12 @@
  */
 package io.netty.buffer;
 
+import static io.netty.buffer.Unpooled.*;
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+import io.netty.util.CharsetUtil;
 import io.netty.util.ResourceLeakTracker;
 
 public class AdvancedLeakAwareByteBufTest extends SimpleLeakAwareByteBufTest {
@@ -28,4 +34,21 @@ public class AdvancedLeakAwareByteBufTest extends SimpleLeakAwareByteBufTest {
     protected SimpleLeakAwareByteBuf wrap(ByteBuf buffer, ResourceLeakTracker<ByteBuf> tracker) {
         return new AdvancedLeakAwareByteBuf(buffer, tracker);
     }
+
+    @Test
+    public void testAddComponentWithLeakAwareByteBuf() {
+        NoopResourceLeakTracker<ByteBuf> tracker = new NoopResourceLeakTracker<ByteBuf>();
+
+        ByteBuf buffer = wrappedBuffer("hello world".getBytes(CharsetUtil.US_ASCII)).slice(6, 5);
+        ByteBuf leakAwareBuf = wrap(buffer, tracker);
+
+        CompositeByteBuf composite = compositeBuffer();
+        composite.addComponent(true, leakAwareBuf);
+        byte[] result = new byte[5];
+        ByteBuf bb = composite.component(0);
+        System.out.println(bb);
+        bb.readBytes(result);
+        assertArrayEquals("world".getBytes(CharsetUtil.US_ASCII), result);
+        composite.release();
+    }
 }
diff --git a/buffer/src/test/java/io/netty/buffer/BigEndianDirectByteBufTest.java b/buffer/src/test/java/io/netty/buffer/BigEndianDirectByteBufTest.java
index 6943c2f..8731295 100644
--- a/buffer/src/test/java/io/netty/buffer/BigEndianDirectByteBufTest.java
+++ b/buffer/src/test/java/io/netty/buffer/BigEndianDirectByteBufTest.java
@@ -19,6 +19,8 @@ import static org.junit.Assert.*;
 
 import java.nio.ByteOrder;
 
+import org.junit.Test;
+
 /**
  * Tests big-endian direct channel buffers
  */
@@ -35,4 +37,11 @@ public class BigEndianDirectByteBufTest extends AbstractByteBufTest {
     protected ByteBuf newDirectBuffer(int length, int maxCapacity) {
         return new UnpooledDirectByteBuf(UnpooledByteBufAllocator.DEFAULT, length, maxCapacity);
     }
+
+    @Test
+    public void testIsContiguous() {
+        ByteBuf buf = newBuffer(4);
+        assertTrue(buf.isContiguous());
+        buf.release();
+    }
 }
diff --git a/buffer/src/test/java/io/netty/buffer/ByteBufDerivationTest.java b/buffer/src/test/java/io/netty/buffer/ByteBufDerivationTest.java
index 2983ba9..b641a16 100644
--- a/buffer/src/test/java/io/netty/buffer/ByteBufDerivationTest.java
+++ b/buffer/src/test/java/io/netty/buffer/ByteBufDerivationTest.java
@@ -22,7 +22,6 @@ import java.nio.ByteOrder;
 import java.util.Random;
 
 import static org.hamcrest.Matchers.*;
-import static org.hamcrest.Matchers.sameInstance;
 import static org.junit.Assert.*;
 
 /**
diff --git a/buffer/src/test/java/io/netty/buffer/ByteBufStreamTest.java b/buffer/src/test/java/io/netty/buffer/ByteBufStreamTest.java
index 222ddcf..e0f4f50 100644
--- a/buffer/src/test/java/io/netty/buffer/ByteBufStreamTest.java
+++ b/buffer/src/test/java/io/netty/buffer/ByteBufStreamTest.java
@@ -187,29 +187,110 @@ public class ByteBufStreamTest {
 
         String s = in.readLine();
         assertNull(s);
+        in.close();
 
+        ByteBuf buf2 = Unpooled.buffer();
         int charCount = 7; //total chars in the string below without new line characters
         byte[] abc = "\na\n\nb\r\nc\nd\ne".getBytes(utf8);
-        buf.writeBytes(abc);
-        in.mark(charCount);
-        assertEquals("", in.readLine());
-        assertEquals("a", in.readLine());
-        assertEquals("", in.readLine());
-        assertEquals("b", in.readLine());
-        assertEquals("c", in.readLine());
-        assertEquals("d", in.readLine());
-        assertEquals("e", in.readLine());
+        buf2.writeBytes(abc);
+
+        ByteBufInputStream in2 = new ByteBufInputStream(buf2, true);
+        in2.mark(charCount);
+        assertEquals("", in2.readLine());
+        assertEquals("a", in2.readLine());
+        assertEquals("", in2.readLine());
+        assertEquals("b", in2.readLine());
+        assertEquals("c", in2.readLine());
+        assertEquals("d", in2.readLine());
+        assertEquals("e", in2.readLine());
         assertNull(in.readLine());
 
-        in.reset();
+        in2.reset();
         int count = 0;
-        while (in.readLine() != null) {
+        while (in2.readLine() != null) {
             ++count;
             if (count > charCount) {
                 fail("readLine() should have returned null");
             }
         }
         assertEquals(charCount, count);
+        in2.close();
+    }
+
+    @Test
+    public void testRead() throws Exception {
+        // case1
+        ByteBuf buf = Unpooled.buffer(16);
+        buf.writeBytes(new byte[]{1, 2, 3, 4, 5, 6});
+
+        ByteBufInputStream in = new ByteBufInputStream(buf, 3);
+
+        assertEquals(1, in.read());
+        assertEquals(2, in.read());
+        assertEquals(3, in.read());
+        assertEquals(-1, in.read());
+        assertEquals(-1, in.read());
+        assertEquals(-1, in.read());
+
+        buf.release();
         in.close();
+
+        // case2
+        ByteBuf buf2 = Unpooled.buffer(16);
+        buf2.writeBytes(new byte[]{1, 2, 3, 4, 5, 6});
+
+        ByteBufInputStream in2 = new ByteBufInputStream(buf2, 4);
+
+        assertEquals(1, in2.read());
+        assertEquals(2, in2.read());
+        assertEquals(3, in2.read());
+        assertEquals(4, in2.read());
+        assertNotEquals(5, in2.read());
+        assertEquals(-1, in2.read());
+
+        buf2.release();
+        in2.close();
+    }
+
+    @Test
+    public void testReadLineLengthRespected1() throws Exception {
+        // case1
+        ByteBuf buf = Unpooled.buffer(16);
+        buf.writeBytes(new byte[] { 1, 2, 3, 4, 5, 6 });
+
+        ByteBufInputStream in = new ByteBufInputStream(buf, 0);
+
+        assertNull(in.readLine());
+        buf.release();
+        in.close();
+    }
+
+    @Test
+    public void testReadLineLengthRespected2() throws Exception {
+        ByteBuf buf2 = Unpooled.buffer(16);
+        buf2.writeBytes(new byte[] { 'A', 'B', '\n', 'C', 'E', 'F'});
+
+        ByteBufInputStream in2 = new ByteBufInputStream(buf2, 4);
+
+        assertEquals("AB", in2.readLine());
+        assertEquals("C", in2.readLine());
+        assertNull(in2.readLine());
+        buf2.release();
+        in2.close();
+    }
+
+    @Test(expected = EOFException.class)
+    public void testReadByteLengthRespected() throws Exception {
+        // case1
+        ByteBuf buf = Unpooled.buffer(16);
+        buf.writeBytes(new byte[] { 1, 2, 3, 4, 5, 6 });
+
+        ByteBufInputStream in = new ByteBufInputStream(buf, 0);
+        try {
+            in.readByte();
+        } finally {
+            buf.release();
+            in.close();
+        }
     }
 }
diff --git a/buffer/src/test/java/io/netty/buffer/ByteBufUtilTest.java b/buffer/src/test/java/io/netty/buffer/ByteBufUtilTest.java
index da9b344..3500c54 100644
--- a/buffer/src/test/java/io/netty/buffer/ByteBufUtilTest.java
+++ b/buffer/src/test/java/io/netty/buffer/ByteBufUtilTest.java
@@ -510,6 +510,111 @@ public class ByteBufUtilTest {
         assertTrue(buf instanceof WrappedByteBuf);
     }
 
+    @Test
+    public void testWriteUtf8Subsequence() {
+        String usAscii = "Some UTF-8 like äÄ∏ŒŒ";
+        ByteBuf buf = Unpooled.buffer(16);
+        buf.writeBytes(usAscii.substring(5, 18).getBytes(CharsetUtil.UTF_8));
+        ByteBuf buf2 = Unpooled.buffer(16);
+        ByteBufUtil.writeUtf8(buf2, usAscii, 5, 18);
+
+        assertEquals(buf, buf2);
+
+        buf.release();
+        buf2.release();
+    }
+
+    @Test
+    public void testWriteUtf8SubsequenceSplitSurrogate() {
+        String usAscii = "\uD800\uDC00"; // surrogate pair: one code point, two chars
+        ByteBuf buf = Unpooled.buffer(16);
+        buf.writeBytes(usAscii.substring(0, 1).getBytes(CharsetUtil.UTF_8));
+        ByteBuf buf2 = Unpooled.buffer(16);
+        ByteBufUtil.writeUtf8(buf2, usAscii, 0, 1);
+
+        assertEquals(buf, buf2);
+
+        buf.release();
+        buf2.release();
+    }
+
+    @Test
+    public void testReserveAndWriteUtf8Subsequence() {
+        String usAscii = "Some UTF-8 like äÄ∏ŒŒ";
+        ByteBuf buf = Unpooled.buffer(16);
+        buf.writeBytes(usAscii.substring(5, 18).getBytes(CharsetUtil.UTF_8));
+        ByteBuf buf2 = Unpooled.buffer(16);
+        int count = ByteBufUtil.reserveAndWriteUtf8(buf2, usAscii, 5, 18, 16);
+
+        assertEquals(buf, buf2);
+        assertEquals(buf.readableBytes(), count);
+
+        buf.release();
+        buf2.release();
+    }
+
+    @Test
+    public void testUtf8BytesSubsequence() {
+        String usAscii = "Some UTF-8 like äÄ∏ŒŒ";
+        assertEquals(usAscii.substring(5, 18).getBytes(CharsetUtil.UTF_8).length,
+                ByteBufUtil.utf8Bytes(usAscii, 5, 18));
+    }
+
+    private static int[][] INVALID_RANGES = new int[][] {
+        { -1, 5 }, { 5, 30 }, { 10, 5 }
+    };
+
+    interface TestMethod {
+        int invoke(Object... args);
+    }
+
+    private void testInvalidSubsequences(TestMethod method) {
+        for (int [] range : INVALID_RANGES) {
+            ByteBuf buf = Unpooled.buffer(16);
+            try {
+                method.invoke(buf, "Some UTF-8 like äÄ∏ŒŒ", range[0], range[1]);
+                fail("Did not throw IndexOutOfBoundsException for range (" + range[0] + ", " + range[1] + ")");
+            } catch (IndexOutOfBoundsException iiobe) {
+                // expected
+            } finally {
+                assertFalse(buf.isReadable());
+                buf.release();
+            }
+        }
+    }
+
+    @Test
+    public void testWriteUtf8InvalidSubsequences() {
+        testInvalidSubsequences(new TestMethod() {
+            @Override
+            public int invoke(Object... args) {
+                return ByteBufUtil.writeUtf8((ByteBuf) args[0], (String) args[1],
+                        (Integer) args[2], (Integer) args[3]);
+            }
+        });
+    }
+
+    @Test
+    public void testReserveAndWriteUtf8InvalidSubsequences() {
+        testInvalidSubsequences(new TestMethod() {
+            @Override
+            public int invoke(Object... args) {
+                return ByteBufUtil.reserveAndWriteUtf8((ByteBuf) args[0], (String) args[1],
+                        (Integer) args[2], (Integer) args[3], 32);
+            }
+        });
+    }
+
+    @Test
+    public void testUtf8BytesInvalidSubsequences() {
+        testInvalidSubsequences(new TestMethod() {
+            @Override
+            public int invoke(Object... args) {
+                return ByteBufUtil.utf8Bytes((String) args[1], (Integer) args[2], (Integer) args[3]);
+            }
+        });
+    }
+
     @Test
     public void testDecodeUsAscii() {
         testDecodeString("This is a test", CharsetUtil.US_ASCII);
diff --git a/buffer/src/test/java/io/netty/buffer/DefaultByteBufHolderTest.java b/buffer/src/test/java/io/netty/buffer/DefaultByteBufHolderTest.java
index 4c60d0e..6fd8aca 100644
--- a/buffer/src/test/java/io/netty/buffer/DefaultByteBufHolderTest.java
+++ b/buffer/src/test/java/io/netty/buffer/DefaultByteBufHolderTest.java
@@ -42,4 +42,64 @@ public class DefaultByteBufHolderTest {
             copy.release();
         }
     }
+
+    @SuppressWarnings("SimplifiableJUnitAssertion")
+    @Test
+    public void testDifferentClassesAreNotEqual() {
+        // all objects here have EMPTY_BUFFER data but are instances of different classes
+        // so we want to check that none of them are equal to another.
+        ByteBufHolder dflt = new DefaultByteBufHolder(Unpooled.EMPTY_BUFFER);
+        ByteBufHolder other = new OtherByteBufHolder(Unpooled.EMPTY_BUFFER, 123);
+        ByteBufHolder constant1 = new DefaultByteBufHolder(Unpooled.EMPTY_BUFFER) {
+            // intentionally empty
+        };
+        ByteBufHolder constant2 = new DefaultByteBufHolder(Unpooled.EMPTY_BUFFER) {
+            // intentionally empty
+        };
+        try {
+            // not using 'assertNotEquals' to be explicit about which object we are calling .equals() on
+            assertFalse(dflt.equals(other));
+            assertFalse(dflt.equals(constant1));
+            assertFalse(constant1.equals(dflt));
+            assertFalse(constant1.equals(other));
+            assertFalse(constant1.equals(constant2));
+        } finally {
+            dflt.release();
+            other.release();
+            constant1.release();
+            constant2.release();
+        }
+    }
+
+    private static class OtherByteBufHolder extends DefaultByteBufHolder {
+
+        private final int extraField;
+
+        OtherByteBufHolder(final ByteBuf data, final int extraField) {
+            super(data);
+            this.extraField = extraField;
+        }
+
+        @Override
+        public boolean equals(final Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+            if (!super.equals(o)) {
+                return false;
+            }
+            final OtherByteBufHolder that = (OtherByteBufHolder) o;
+            return extraField == that.extraField;
+        }
+
+        @Override
+        public int hashCode() {
+            int result = super.hashCode();
+            result = 31 * result + extraField;
+            return result;
+        }
+    }
 }
diff --git a/buffer/src/test/java/io/netty/buffer/DuplicatedByteBufTest.java b/buffer/src/test/java/io/netty/buffer/DuplicatedByteBufTest.java
index 2e88657..f96b922 100644
--- a/buffer/src/test/java/io/netty/buffer/DuplicatedByteBufTest.java
+++ b/buffer/src/test/java/io/netty/buffer/DuplicatedByteBufTest.java
@@ -33,6 +33,13 @@ public class DuplicatedByteBufTest extends AbstractByteBufTest {
         return buffer;
     }
 
+    @Test
+    public void testIsContiguous() {
+        ByteBuf buf = newBuffer(4);
+        assertEquals(buf.unwrap().isContiguous(), buf.isContiguous());
+        buf.release();
+    }
+
     @Test(expected = NullPointerException.class)
     public void shouldNotAllowNullInConstructor() {
         new DuplicatedByteBuf(null);
diff --git a/buffer/src/test/java/io/netty/buffer/EmptyByteBufTest.java b/buffer/src/test/java/io/netty/buffer/EmptyByteBufTest.java
index acafca1..0d9fc25 100644
--- a/buffer/src/test/java/io/netty/buffer/EmptyByteBufTest.java
+++ b/buffer/src/test/java/io/netty/buffer/EmptyByteBufTest.java
@@ -14,6 +14,7 @@
  * under the License.
  */package io.netty.buffer;
 
+import io.netty.util.CharsetUtil;
 import org.junit.Test;
 
 import static org.hamcrest.Matchers.*;
@@ -21,6 +22,12 @@ import static org.junit.Assert.*;
 
 public class EmptyByteBufTest {
 
+    @Test
+    public void testIsContiguous() {
+        EmptyByteBuf empty = new EmptyByteBuf(UnpooledByteBufAllocator.DEFAULT);
+        assertTrue(empty.isContiguous());
+    }
+
     @Test
     public void testIsWritable() {
         EmptyByteBuf empty = new EmptyByteBuf(UnpooledByteBufAllocator.DEFAULT);
@@ -93,4 +100,11 @@ public class EmptyByteBufTest {
         assertTrue(emptyAbstract.release());
         assertFalse(empty.release());
     }
+
+    @Test
+    public void testGetCharSequence() {
+        EmptyByteBuf empty = new EmptyByteBuf(UnpooledByteBufAllocator.DEFAULT);
+        assertEquals("", empty.readCharSequence(0, CharsetUtil.US_ASCII));
+    }
+
 }
diff --git a/buffer/src/test/java/io/netty/buffer/FixedCompositeByteBufTest.java b/buffer/src/test/java/io/netty/buffer/FixedCompositeByteBufTest.java
index b6260f7..a564bf1 100644
--- a/buffer/src/test/java/io/netty/buffer/FixedCompositeByteBufTest.java
+++ b/buffer/src/test/java/io/netty/buffer/FixedCompositeByteBufTest.java
@@ -432,10 +432,12 @@ public class FixedCompositeByteBufTest {
     }
 
     @Test
-    public void testHasArrayWhenEmpty() {
+    public void testHasArrayWhenEmptyAndIsDirect() {
         ByteBuf buf = newBuffer(new ByteBuf[0]);
         assertTrue(buf.hasArray());
         assertArrayEquals(EMPTY_BUFFER.array(), buf.array());
+        assertEquals(EMPTY_BUFFER.isDirect(), buf.isDirect());
+        assertEquals(EMPTY_BUFFER.memoryAddress(), buf.memoryAddress());
         buf.release();
     }
 
diff --git a/buffer/src/test/java/io/netty/buffer/PoolArenaTest.java b/buffer/src/test/java/io/netty/buffer/PoolArenaTest.java
index 2c1bd12..3ad331d 100644
--- a/buffer/src/test/java/io/netty/buffer/PoolArenaTest.java
+++ b/buffer/src/test/java/io/netty/buffer/PoolArenaTest.java
@@ -20,6 +20,8 @@ import io.netty.util.internal.PlatformDependent;
 import org.junit.Assert;
 import org.junit.Test;
 
+import static org.junit.Assume.assumeTrue;
+
 import java.nio.ByteBuffer;
 
 public class PoolArenaTest {
@@ -46,6 +48,7 @@ public class PoolArenaTest {
 
     @Test
     public void testDirectArenaOffsetCacheLine() throws Exception {
+        assumeTrue(PlatformDependent.hasUnsafe());
         int capacity = 5;
         int alignment = 128;
 
@@ -64,7 +67,7 @@ public class PoolArenaTest {
     }
 
     @Test
-    public final void testAllocationCounter() {
+    public void testAllocationCounter() {
         final PooledByteBufAllocator allocator = new PooledByteBufAllocator(
                 true,   // preferDirect
                 0,      // nHeapArena
@@ -107,4 +110,26 @@ public class PoolArenaTest {
         Assert.assertEquals(1, metric.numNormalDeallocations());
         Assert.assertEquals(1, metric.numNormalAllocations());
     }
+
+    @Test
+    public void testDirectArenaMemoryCopy() {
+        ByteBuf src = PooledByteBufAllocator.DEFAULT.directBuffer(512);
+        ByteBuf dst = PooledByteBufAllocator.DEFAULT.directBuffer(512);
+
+        PooledByteBuf<ByteBuffer> pooledSrc = unwrapIfNeeded(src);
+        PooledByteBuf<ByteBuffer> pooledDst = unwrapIfNeeded(dst);
+
+        // This causes the internal reused ByteBuffer duplicate limit to be set to 128
+        pooledDst.writeBytes(ByteBuffer.allocate(128));
+        // Ensure internal ByteBuffer duplicate limit is properly reset (used in memoryCopy non-Unsafe case)
+        pooledDst.chunk.arena.memoryCopy(pooledSrc.memory, 0, pooledDst, 512);
+
+        src.release();
+        dst.release();
+    }
+
+    @SuppressWarnings("unchecked")
+    private PooledByteBuf<ByteBuffer> unwrapIfNeeded(ByteBuf buf) {
+        return (PooledByteBuf<ByteBuffer>) (buf instanceof PooledByteBuf ? buf : buf.unwrap());
+    }
 }
diff --git a/buffer/src/test/java/io/netty/buffer/PooledByteBufAllocatorTest.java b/buffer/src/test/java/io/netty/buffer/PooledByteBufAllocatorTest.java
index 495bb76..4f9ce33 100644
--- a/buffer/src/test/java/io/netty/buffer/PooledByteBufAllocatorTest.java
+++ b/buffer/src/test/java/io/netty/buffer/PooledByteBufAllocatorTest.java
@@ -63,6 +63,21 @@ public class PooledByteBufAllocatorTest extends AbstractByteBufAllocatorTest<Poo
         return allocator.metric().chunkSize();
     }
 
+    @Test
+    public void testTrim() {
+        PooledByteBufAllocator allocator = newAllocator(true);
+
+        // Should return false as we never allocated from this thread yet.
+        assertFalse(allocator.trimCurrentThreadCache());
+
+        ByteBuf directBuffer = allocator.directBuffer();
+
+        assertTrue(directBuffer.release());
+
+        // Should return true now a cache exists for the calling thread.
+        assertTrue(allocator.trimCurrentThreadCache());
+    }
+
     @Test
     public void testPooledUnsafeHeapBufferAndUnsafeDirectBuffer() {
         PooledByteBufAllocator allocator = newAllocator(true);
@@ -77,6 +92,25 @@ public class PooledByteBufAllocatorTest extends AbstractByteBufAllocatorTest<Poo
         heapBuffer.release();
     }
 
+    @Test
+    public void testIOBuffersAreDirectWhenUnsafeAvailableOrDirectBuffersPooled() {
+        PooledByteBufAllocator allocator = newAllocator(true);
+        ByteBuf ioBuffer = allocator.ioBuffer();
+
+        assertTrue(ioBuffer.isDirect());
+        ioBuffer.release();
+
+        PooledByteBufAllocator unpooledAllocator = newUnpooledAllocator();
+        ioBuffer = unpooledAllocator.ioBuffer();
+
+        if (PlatformDependent.hasUnsafe()) {
+            assertTrue(ioBuffer.isDirect());
+        } else {
+            assertFalse(ioBuffer.isDirect());
+        }
+        ioBuffer.release();
+    }
+
     @Test
     public void testWithoutUseCacheForAllThreads() {
         assertFalse(Thread.currentThread() instanceof FastThreadLocalThread);
@@ -430,8 +464,14 @@ public class PooledByteBufAllocatorTest extends AbstractByteBufAllocatorTest<Poo
                 Thread.sleep(100);
             }
         } finally {
+            // First mark all AllocationThreads to complete their work and then wait until these are complete
+            // and rethrow if there was any error.
             for (AllocationThread t : threads) {
-                t.finish();
+                t.markAsFinished();
+            }
+
+            for (AllocationThread t: threads) {
+                t.joinAndCheckForError();
             }
         }
     }
@@ -461,7 +501,7 @@ public class PooledByteBufAllocatorTest extends AbstractByteBufAllocatorTest<Poo
         private final ByteBufAllocator allocator;
         private final AtomicReference<Object> finish = new AtomicReference<Object>();
 
-        public AllocationThread(ByteBufAllocator allocator) {
+        AllocationThread(ByteBufAllocator allocator) {
             this.allocator = allocator;
         }
 
@@ -494,14 +534,17 @@ public class PooledByteBufAllocatorTest extends AbstractByteBufAllocatorTest<Poo
             }
         }
 
-        public boolean isFinished() {
+        boolean isFinished() {
             return finish.get() != null;
         }
 
-        public void finish() throws Throwable {
+        void markAsFinished() {
+            finish.compareAndSet(null, Boolean.TRUE);
+        }
+
+        void joinAndCheckForError() throws Throwable {
             try {
                 // Mark as finish if not already done but ensure we not override the previous set error.
-                finish.compareAndSet(null, Boolean.TRUE);
                 join();
             } finally {
                 releaseBuffers();
@@ -509,7 +552,7 @@ public class PooledByteBufAllocatorTest extends AbstractByteBufAllocatorTest<Poo
             checkForError();
         }
 
-        public void checkForError() throws Throwable {
+        void checkForError() throws Throwable {
             Object obj = finish.get();
             if (obj instanceof Throwable) {
                 throw (Throwable) obj;
diff --git a/buffer/src/test/java/io/netty/buffer/ReadOnlyDirectByteBufferBufTest.java b/buffer/src/test/java/io/netty/buffer/ReadOnlyDirectByteBufferBufTest.java
index d51ce11..1e88bda 100644
--- a/buffer/src/test/java/io/netty/buffer/ReadOnlyDirectByteBufferBufTest.java
+++ b/buffer/src/test/java/io/netty/buffer/ReadOnlyDirectByteBufferBufTest.java
@@ -21,9 +21,8 @@ import org.junit.Test;
 
 import java.io.ByteArrayInputStream;
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.RandomAccessFile;
 import java.nio.ByteBuffer;
 import java.nio.ReadOnlyBufferException;
 import java.nio.channels.FileChannel;
@@ -38,6 +37,13 @@ public class ReadOnlyDirectByteBufferBufTest {
         return ByteBuffer.allocateDirect(size);
     }
 
+    @Test
+    public void testIsContiguous() {
+        ByteBuf buf = buffer(allocate(4).asReadOnlyBuffer());
+        Assert.assertTrue(buf.isContiguous());
+        buf.release();
+    }
+
     @Test(expected = IllegalArgumentException.class)
     public void testConstructWithWritable() {
         buffer(allocate(1));
@@ -236,6 +242,20 @@ public class ReadOnlyDirectByteBufferBufTest {
         buf.release();
     }
 
+    @Test(expected = IndexOutOfBoundsException.class)
+    public void testGetBytesByteBuffer() {
+        byte[] bytes = {'a', 'b', 'c', 'd', 'e', 'f', 'g'};
+        // Ensure destination buffer is bigger then what is in the ByteBuf.
+        ByteBuffer nioBuffer = ByteBuffer.allocate(bytes.length + 1);
+        ByteBuf buffer = buffer(((ByteBuffer) allocate(bytes.length)
+                .put(bytes).flip()).asReadOnlyBuffer());
+        try {
+            buffer.getBytes(buffer.readerIndex(), nioBuffer);
+        } finally {
+            buffer.release();
+        }
+    }
+
     @Test
     public void testCopy() {
         ByteBuf buf = buffer(((ByteBuffer) allocate(16).putLong(1).putLong(2).flip()).asReadOnlyBuffer());
@@ -293,12 +313,12 @@ public class ReadOnlyDirectByteBufferBufTest {
         ByteBuf b2 = null;
 
         try {
-            output = new FileOutputStream(file).getChannel();
+            output = new RandomAccessFile(file, "rw").getChannel();
             byte[] bytes = new byte[1024];
             PlatformDependent.threadLocalRandom().nextBytes(bytes);
             output.write(ByteBuffer.wrap(bytes));
 
-            input = new FileInputStream(file).getChannel();
+            input = new RandomAccessFile(file, "r").getChannel();
             ByteBuffer m = input.map(FileChannel.MapMode.READ_ONLY, 0, input.size());
 
             b1 = buffer(m);
diff --git a/buffer/src/test/java/io/netty/buffer/SimpleLeakAwareCompositeByteBufTest.java b/buffer/src/test/java/io/netty/buffer/SimpleLeakAwareCompositeByteBufTest.java
index 713fe72..34596f3 100644
--- a/buffer/src/test/java/io/netty/buffer/SimpleLeakAwareCompositeByteBufTest.java
+++ b/buffer/src/test/java/io/netty/buffer/SimpleLeakAwareCompositeByteBufTest.java
@@ -15,7 +15,9 @@
  */
 package io.netty.buffer;
 
+import io.netty.util.ByteProcessor;
 import io.netty.util.ResourceLeakTracker;
+import org.hamcrest.CoreMatchers;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -23,7 +25,9 @@ import org.junit.Test;
 import java.util.ArrayDeque;
 import java.util.Queue;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
 
 public class SimpleLeakAwareCompositeByteBufTest extends WrappedCompositeByteBufTest {
@@ -131,6 +135,26 @@ public class SimpleLeakAwareCompositeByteBufTest extends WrappedCompositeByteBuf
         assertWrapped(newBuffer(8).asReadOnly());
     }
 
+    @Test
+    public void forEachByteUnderLeakDetectionShouldNotThrowException() {
+        CompositeByteBuf buf = (CompositeByteBuf) newBuffer(8);
+        assertThat(buf, CoreMatchers.instanceOf(SimpleLeakAwareCompositeByteBuf.class));
+        CompositeByteBuf comp = (CompositeByteBuf) newBuffer(8);
+        assertThat(comp, CoreMatchers.instanceOf(SimpleLeakAwareCompositeByteBuf.class));
+
+        ByteBuf inner = comp.alloc().directBuffer(1).writeByte(0);
+        comp.addComponent(true, inner);
+        buf.addComponent(true, comp);
+
+        assertEquals(-1, buf.forEachByte(new ByteProcessor() {
+            @Override
+            public boolean process(byte value) {
+                return true;
+            }
+        }));
+        assertTrue(buf.release());
+    }
+
     protected final void assertWrapped(ByteBuf buf) {
         try {
             assertSame(clazz, buf.getClass());
diff --git a/buffer/src/test/java/io/netty/buffer/SlicedByteBufTest.java b/buffer/src/test/java/io/netty/buffer/SlicedByteBufTest.java
index 4079571..11e9f68 100644
--- a/buffer/src/test/java/io/netty/buffer/SlicedByteBufTest.java
+++ b/buffer/src/test/java/io/netty/buffer/SlicedByteBufTest.java
@@ -47,6 +47,13 @@ public class SlicedByteBufTest extends AbstractByteBufTest {
         return buffer.slice(offset, length);
     }
 
+    @Test
+    public void testIsContiguous() {
+        ByteBuf buf = newBuffer(4);
+        assertEquals(buf.unwrap().isContiguous(), buf.isContiguous());
+        buf.release();
+    }
+
     @Test(expected = NullPointerException.class)
     public void shouldNotAllowNullInConstructor() {
         new SlicedByteBuf(null, 0, 0);
diff --git a/codec-dns/pom.xml b/codec-dns/pom.xml
index 41933ec..b884309 100644
--- a/codec-dns/pom.xml
+++ b/codec-dns/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>io.netty</groupId>
     <artifactId>netty-parent</artifactId>
-    <version>4.1.33.Final</version>
+    <version>4.1.48.Final</version>
   </parent>
 
   <artifactId>netty-codec-dns</artifactId>
diff --git a/codec-dns/src/main/java/io/netty/handler/codec/dns/AbstractDnsRecord.java b/codec-dns/src/main/java/io/netty/handler/codec/dns/AbstractDnsRecord.java
index 28b92c2..6802d39 100644
--- a/codec-dns/src/main/java/io/netty/handler/codec/dns/AbstractDnsRecord.java
+++ b/codec-dns/src/main/java/io/netty/handler/codec/dns/AbstractDnsRecord.java
@@ -15,12 +15,14 @@
  */
 package io.netty.handler.codec.dns;
 
+import io.netty.util.internal.PlatformDependent;
 import io.netty.util.internal.StringUtil;
 import io.netty.util.internal.UnstableApi;
 
 import java.net.IDN;
 
 import static io.netty.util.internal.ObjectUtil.checkNotNull;
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
 
 /**
  * A skeletal implementation of {@link DnsRecord}.
@@ -62,19 +64,28 @@ public abstract class AbstractDnsRecord implements DnsRecord {
      * @param timeToLive the TTL value of the record
      */
     protected AbstractDnsRecord(String name, DnsRecordType type, int dnsClass, long timeToLive) {
-        if (timeToLive < 0) {
-            throw new IllegalArgumentException("timeToLive: " + timeToLive + " (expected: >= 0)");
-        }
+        checkPositiveOrZero(timeToLive, "timeToLive");
         // Convert to ASCII which will also check that the length is not too big.
         // See:
         //   - https://github.com/netty/netty/issues/4937
         //   - https://github.com/netty/netty/issues/4935
-        this.name = appendTrailingDot(IDN.toASCII(checkNotNull(name, "name")));
+        this.name = appendTrailingDot(IDNtoASCII(name));
         this.type = checkNotNull(type, "type");
         this.dnsClass = (short) dnsClass;
         this.timeToLive = timeToLive;
     }
 
+    private static String IDNtoASCII(String name) {
+        checkNotNull(name, "name");
+        if (PlatformDependent.isAndroid() && DefaultDnsRecordDecoder.ROOT.equals(name)) {
+            // Prior Android 10 there was a bug that did not correctly parse ".".
+            //
+            // See https://github.com/netty/netty/issues/10034
+            return name;
+        }
+        return IDN.toASCII(name);
+    }
+
     private static String appendTrailingDot(String name) {
         if (name.length() > 0 && name.charAt(name.length() - 1) != '.') {
             return name + '.';
diff --git a/codec-dns/src/main/java/io/netty/handler/codec/dns/DatagramDnsQueryEncoder.java b/codec-dns/src/main/java/io/netty/handler/codec/dns/DatagramDnsQueryEncoder.java
index dbb562f..695a9c9 100644
--- a/codec-dns/src/main/java/io/netty/handler/codec/dns/DatagramDnsQueryEncoder.java
+++ b/codec-dns/src/main/java/io/netty/handler/codec/dns/DatagramDnsQueryEncoder.java
@@ -26,8 +26,6 @@ import io.netty.util.internal.UnstableApi;
 import java.net.InetSocketAddress;
 import java.util.List;
 
-import static io.netty.util.internal.ObjectUtil.checkNotNull;
-
 /**
  * Encodes a {@link DatagramDnsQuery} (or an {@link AddressedEnvelope} of {@link DnsQuery}} into a
  * {@link DatagramPacket}.
@@ -36,7 +34,7 @@ import static io.netty.util.internal.ObjectUtil.checkNotNull;
 @ChannelHandler.Sharable
 public class DatagramDnsQueryEncoder extends MessageToMessageEncoder<AddressedEnvelope<DnsQuery, InetSocketAddress>> {
 
-    private final DnsRecordEncoder recordEncoder;
+    private final DnsQueryEncoder encoder;
 
     /**
      * Creates a new encoder with {@linkplain DnsRecordEncoder#DEFAULT the default record encoder}.
@@ -49,7 +47,7 @@ public class DatagramDnsQueryEncoder extends MessageToMessageEncoder<AddressedEn
      * Creates a new encoder with the specified {@code recordEncoder}.
      */
     public DatagramDnsQueryEncoder(DnsRecordEncoder recordEncoder) {
-        this.recordEncoder = checkNotNull(recordEncoder, "recordEncoder");
+        this.encoder = new DnsQueryEncoder(recordEncoder);
     }
 
     @Override
@@ -63,9 +61,7 @@ public class DatagramDnsQueryEncoder extends MessageToMessageEncoder<AddressedEn
 
         boolean success = false;
         try {
-            encodeHeader(query, buf);
-            encodeQuestions(query, buf);
-            encodeRecords(query, DnsSection.ADDITIONAL, buf);
+            encoder.encode(query, buf);
             success = true;
         } finally {
             if (!success) {
@@ -85,38 +81,4 @@ public class DatagramDnsQueryEncoder extends MessageToMessageEncoder<AddressedEn
         @SuppressWarnings("unused") AddressedEnvelope<DnsQuery, InetSocketAddress> msg) throws Exception {
         return ctx.alloc().ioBuffer(1024);
     }
-
-    /**
-     * Encodes the header that is always 12 bytes long.
-     *
-     * @param query the query header being encoded
-     * @param buf   the buffer the encoded data should be written to
-     */
-    private static void encodeHeader(DnsQuery query, ByteBuf buf) {
-        buf.writeShort(query.id());
-        int flags = 0;
-        flags |= (query.opCode().byteValue() & 0xFF) << 14;
-        if (query.isRecursionDesired()) {
-            flags |= 1 << 8;
-        }
-        buf.writeShort(flags);
-        buf.writeShort(query.count(DnsSection.QUESTION));
-        buf.writeShort(0); // answerCount
-        buf.writeShort(0); // authorityResourceCount
-        buf.writeShort(query.count(DnsSection.ADDITIONAL));
-    }
-
-    private void encodeQuestions(DnsQuery query, ByteBuf buf) throws Exception {
-        final int count = query.count(DnsSection.QUESTION);
-        for (int i = 0; i < count; i++) {
-            recordEncoder.encodeQuestion((DnsQuestion) query.recordAt(DnsSection.QUESTION, i), buf);
-        }
-    }
-
-    private void encodeRecords(DnsQuery query, DnsSection section, ByteBuf buf) throws Exception {
-        final int count = query.count(section);
-        for (int i = 0; i < count; i++) {
-            recordEncoder.encodeRecord(query.recordAt(section, i), buf);
-        }
-    }
 }
diff --git a/codec-dns/src/main/java/io/netty/handler/codec/dns/DatagramDnsResponseDecoder.java b/codec-dns/src/main/java/io/netty/handler/codec/dns/DatagramDnsResponseDecoder.java
index 547d5ae..b9c4a1c 100644
--- a/codec-dns/src/main/java/io/netty/handler/codec/dns/DatagramDnsResponseDecoder.java
+++ b/codec-dns/src/main/java/io/netty/handler/codec/dns/DatagramDnsResponseDecoder.java
@@ -15,18 +15,15 @@
  */
 package io.netty.handler.codec.dns;
 
-import io.netty.buffer.ByteBuf;
 import io.netty.channel.ChannelHandler;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.socket.DatagramPacket;
-import io.netty.handler.codec.CorruptedFrameException;
 import io.netty.handler.codec.MessageToMessageDecoder;
 import io.netty.util.internal.UnstableApi;
 
+import java.net.InetSocketAddress;
 import java.util.List;
 
-import static io.netty.util.internal.ObjectUtil.checkNotNull;
-
 /**
  * Decodes a {@link DatagramPacket} into a {@link DatagramDnsResponse}.
  */
@@ -34,7 +31,7 @@ import static io.netty.util.internal.ObjectUtil.checkNotNull;
 @ChannelHandler.Sharable
 public class DatagramDnsResponseDecoder extends MessageToMessageDecoder<DatagramPacket> {
 
-    private final DnsRecordDecoder recordDecoder;
+    private final DnsResponseDecoder<InetSocketAddress> responseDecoder;
 
     /**
      * Creates a new decoder with {@linkplain DnsRecordDecoder#DEFAULT the default record decoder}.
@@ -47,73 +44,21 @@ public class DatagramDnsResponseDecoder extends MessageToMessageDecoder<Datagram
      * Creates a new decoder with the specified {@code recordDecoder}.
      */
     public DatagramDnsResponseDecoder(DnsRecordDecoder recordDecoder) {
-        this.recordDecoder = checkNotNull(recordDecoder, "recordDecoder");
+        this.responseDecoder = new DnsResponseDecoder<InetSocketAddress>(recordDecoder) {
+            @Override
+            protected DnsResponse newResponse(InetSocketAddress sender, InetSocketAddress recipient,
+                                              int id, DnsOpCode opCode, DnsResponseCode responseCode) {
+                return new DatagramDnsResponse(sender, recipient, id, opCode, responseCode);
+            }
+        };
     }
 
     @Override
     protected void decode(ChannelHandlerContext ctx, DatagramPacket packet, List<Object> out) throws Exception {
-        final ByteBuf buf = packet.content();
-
-        final DnsResponse response = newResponse(packet, buf);
-        boolean success = false;
-        try {
-            final int questionCount = buf.readUnsignedShort();
-            final int answerCount = buf.readUnsignedShort();
-            final int authorityRecordCount = buf.readUnsignedShort();
-            final int additionalRecordCount = buf.readUnsignedShort();
-
-            decodeQuestions(response, buf, questionCount);
-            decodeRecords(response, DnsSection.ANSWER, buf, answerCount);
-            decodeRecords(response, DnsSection.AUTHORITY, buf, authorityRecordCount);
-            decodeRecords(response, DnsSection.ADDITIONAL, buf, additionalRecordCount);
-
-            out.add(response);
-            success = true;
-        } finally {
-            if (!success) {
-                response.release();
-            }
-        }
+        out.add(decodeResponse(ctx, packet));
     }
 
-    private static DnsResponse newResponse(DatagramPacket packet, ByteBuf buf) {
-        final int id = buf.readUnsignedShort();
-
-        final int flags = buf.readUnsignedShort();
-        if (flags >> 15 == 0) {
-            throw new CorruptedFrameException("not a response");
-        }
-
-        final DnsResponse response = new DatagramDnsResponse(
-            packet.sender(),
-            packet.recipient(),
-            id,
-            DnsOpCode.valueOf((byte) (flags >> 11 & 0xf)), DnsResponseCode.valueOf((byte) (flags & 0xf)));
-
-        response.setRecursionDesired((flags >> 8 & 1) == 1);
-        response.setAuthoritativeAnswer((flags >> 10 & 1) == 1);
-        response.setTruncated((flags >> 9 & 1) == 1);
-        response.setRecursionAvailable((flags >> 7 & 1) == 1);
-        response.setZ(flags >> 4 & 0x7);
-        return response;
-    }
-
-    private void decodeQuestions(DnsResponse response, ByteBuf buf, int questionCount) throws Exception {
-        for (int i = questionCount; i > 0; i --) {
-            response.addRecord(DnsSection.QUESTION, recordDecoder.decodeQuestion(buf));
-        }
-    }
-
-    private void decodeRecords(
-            DnsResponse response, DnsSection section, ByteBuf buf, int count) throws Exception {
-        for (int i = count; i > 0; i --) {
-            final DnsRecord r = recordDecoder.decodeRecord(buf);
-            if (r == null) {
-                // Truncated response
-                break;
-            }
-
-            response.addRecord(section, r);
-        }
+    protected DnsResponse decodeResponse(ChannelHandlerContext ctx, DatagramPacket packet) throws Exception {
+        return responseDecoder.decode(packet.sender(), packet.recipient(), packet.content());
     }
 }
diff --git a/codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsRecordDecoder.java b/codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsRecordDecoder.java
index b4e50ff..70df1c6 100644
--- a/codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsRecordDecoder.java
+++ b/codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsRecordDecoder.java
@@ -16,8 +16,6 @@
 package io.netty.handler.codec.dns;
 
 import io.netty.buffer.ByteBuf;
-import io.netty.handler.codec.CorruptedFrameException;
-import io.netty.util.CharsetUtil;
 import io.netty.util.internal.UnstableApi;
 
 /**
@@ -49,7 +47,7 @@ public class DefaultDnsRecordDecoder implements DnsRecordDecoder {
         final String name = decodeName(in);
 
         final int endOffset = in.writerIndex();
-        if (endOffset - startOffset < 10) {
+        if (endOffset - in.readerIndex() < 10) {
             // Not enough data
             in.readerIndex(startOffset);
             return null;
@@ -98,6 +96,11 @@ public class DefaultDnsRecordDecoder implements DnsRecordDecoder {
             return new DefaultDnsPtrRecord(
                     name, dnsClass, timeToLive, decodeName0(in.duplicate().setIndex(offset, offset + length)));
         }
+        if (type == DnsRecordType.CNAME || type == DnsRecordType.NS) {
+            return new DefaultDnsRawRecord(name, type, dnsClass, timeToLive,
+                                           DnsCodecUtil.decompressDomainName(
+                                                   in.duplicate().setIndex(offset, offset + length)));
+        }
         return new DefaultDnsRawRecord(
                 name, type, dnsClass, timeToLive, in.retainedDuplicate().setIndex(offset, offset + length));
     }
@@ -123,69 +126,6 @@ public class DefaultDnsRecordDecoder implements DnsRecordDecoder {
      * @return the domain name for an entry
      */
     public static String decodeName(ByteBuf in) {
-        int position = -1;
-        int checked = 0;
-        final int end = in.writerIndex();
-        final int readable = in.readableBytes();
-
-        // Looking at the spec we should always have at least enough readable bytes to read a byte here but it seems
-        // some servers do not respect this for empty names. So just workaround this and return an empty name in this
-        // case.
-        //
-        // See:
-        // - https://github.com/netty/netty/issues/5014
-        // - https://www.ietf.org/rfc/rfc1035.txt , Section 3.1
-        if (readable == 0) {
-            return ROOT;
-        }
-
-        final StringBuilder name = new StringBuilder(readable << 1);
-        while (in.isReadable()) {
-            final int len = in.readUnsignedByte();
-            final boolean pointer = (len & 0xc0) == 0xc0;
-            if (pointer) {
-                if (position == -1) {
-                    position = in.readerIndex() + 1;
-                }
-
-                if (!in.isReadable()) {
-                    throw new CorruptedFrameException("truncated pointer in a name");
-                }
-
-                final int next = (len & 0x3f) << 8 | in.readUnsignedByte();
-                if (next >= end) {
-                    throw new CorruptedFrameException("name has an out-of-range pointer");
-                }
-                in.readerIndex(next);
-
-                // check for loops
-                checked += 2;
-                if (checked >= end) {
-                    throw new CorruptedFrameException("name contains a loop.");
-                }
-            } else if (len != 0) {
-                if (!in.isReadable(len)) {
-                    throw new CorruptedFrameException("truncated label in a name");
-                }
-                name.append(in.toString(in.readerIndex(), len, CharsetUtil.UTF_8)).append('.');
-                in.skipBytes(len);
-            } else { // len == 0
-                break;
-            }
-        }
-
-        if (position != -1) {
-            in.readerIndex(position);
-        }
-
-        if (name.length() == 0) {
-            return ROOT;
-        }
-
-        if (name.charAt(name.length() - 1) != '.') {
-            name.append('.');
-        }
-
-        return name.toString();
+        return DnsCodecUtil.decodeDomainName(in);
     }
 }
diff --git a/codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsRecordEncoder.java b/codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsRecordEncoder.java
index 48f60bc..45914ea 100644
--- a/codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsRecordEncoder.java
+++ b/codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsRecordEncoder.java
@@ -16,14 +16,11 @@
 package io.netty.handler.codec.dns;
 
 import io.netty.buffer.ByteBuf;
-import io.netty.buffer.ByteBufUtil;
 import io.netty.channel.socket.InternetProtocolFamily;
 import io.netty.handler.codec.UnsupportedMessageTypeException;
 import io.netty.util.internal.StringUtil;
 import io.netty.util.internal.UnstableApi;
 
-import static io.netty.handler.codec.dns.DefaultDnsRecordDecoder.ROOT;
-
 /**
  * The default {@link DnsRecordEncoder} implementation.
  *
@@ -141,25 +138,7 @@ public class DefaultDnsRecordEncoder implements DnsRecordEncoder {
     }
 
     protected void encodeName(String name, ByteBuf buf) throws Exception {
-        if (ROOT.equals(name)) {
-            // Root domain
-            buf.writeByte(0);
-            return;
-        }
-
-        final String[] labels = name.split("\\.");
-        for (String label : labels) {
-            final int labelLen = label.length();
-            if (labelLen == 0) {
-                // zero-length label means the end of the name.
-                break;
-            }
-
-            buf.writeByte(labelLen);
-            ByteBufUtil.writeAscii(buf, label);
-        }
-
-        buf.writeByte(0); // marks end of name field
+        DnsCodecUtil.encodeDomainName(name, buf);
     }
 
     private static byte padWithZeros(byte b, int lowOrderBitsToPreserve) {
diff --git a/codec-dns/src/main/java/io/netty/handler/codec/dns/DnsCodecUtil.java b/codec-dns/src/main/java/io/netty/handler/codec/dns/DnsCodecUtil.java
new file mode 100644
index 0000000..8804cf7
--- /dev/null
+++ b/codec-dns/src/main/java/io/netty/handler/codec/dns/DnsCodecUtil.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2019 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.dns;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.buffer.Unpooled;
+import io.netty.handler.codec.CorruptedFrameException;
+import io.netty.util.CharsetUtil;
+
+import static io.netty.handler.codec.dns.DefaultDnsRecordDecoder.*;
+
+final class DnsCodecUtil {
+    private DnsCodecUtil() {
+        // Util class
+    }
+
+    static void encodeDomainName(String name, ByteBuf buf) {
+        if (ROOT.equals(name)) {
+            // Root domain
+            buf.writeByte(0);
+            return;
+        }
+
+        final String[] labels = name.split("\\.");
+        for (String label : labels) {
+            final int labelLen = label.length();
+            if (labelLen == 0) {
+                // zero-length label means the end of the name.
+                break;
+            }
+
+            buf.writeByte(labelLen);
+            ByteBufUtil.writeAscii(buf, label);
+        }
+
+        buf.writeByte(0); // marks end of name field
+    }
+
+    static String decodeDomainName(ByteBuf in) {
+        int position = -1;
+        int checked = 0;
+        final int end = in.writerIndex();
+        final int readable = in.readableBytes();
+
+        // Looking at the spec we should always have at least enough readable bytes to read a byte here but it seems
+        // some servers do not respect this for empty names. So just workaround this and return an empty name in this
+        // case.
+        //
+        // See:
+        // - https://github.com/netty/netty/issues/5014
+        // - https://www.ietf.org/rfc/rfc1035.txt , Section 3.1
+        if (readable == 0) {
+            return ROOT;
+        }
+
+        final StringBuilder name = new StringBuilder(readable << 1);
+        while (in.isReadable()) {
+            final int len = in.readUnsignedByte();
+            final boolean pointer = (len & 0xc0) == 0xc0;
+            if (pointer) {
+                if (position == -1) {
+                    position = in.readerIndex() + 1;
+                }
+
+                if (!in.isReadable()) {
+                    throw new CorruptedFrameException("truncated pointer in a name");
+                }
+
+                final int next = (len & 0x3f) << 8 | in.readUnsignedByte();
+                if (next >= end) {
+                    throw new CorruptedFrameException("name has an out-of-range pointer");
+                }
+                in.readerIndex(next);
+
+                // check for loops
+                checked += 2;
+                if (checked >= end) {
+                    throw new CorruptedFrameException("name contains a loop.");
+                }
+            } else if (len != 0) {
+                if (!in.isReadable(len)) {
+                    throw new CorruptedFrameException("truncated label in a name");
+                }
+                name.append(in.toString(in.readerIndex(), len, CharsetUtil.UTF_8)).append('.');
+                in.skipBytes(len);
+            } else { // len == 0
+                break;
+            }
+        }
+
+        if (position != -1) {
+            in.readerIndex(position);
+        }
+
+        if (name.length() == 0) {
+            return ROOT;
+        }
+
+        if (name.charAt(name.length() - 1) != '.') {
+            name.append('.');
+        }
+
+        return name.toString();
+    }
+
+    /**
+     * Decompress pointer data.
+     * @param compression comporession data
+     * @return decompressed data
+     */
+    static ByteBuf decompressDomainName(ByteBuf compression) {
+        String domainName = decodeDomainName(compression);
+        ByteBuf result = compression.alloc().buffer(domainName.length() << 1);
+        encodeDomainName(domainName, result);
+        return result;
+    }
+}
diff --git a/codec-dns/src/main/java/io/netty/handler/codec/dns/DnsQueryEncoder.java b/codec-dns/src/main/java/io/netty/handler/codec/dns/DnsQueryEncoder.java
new file mode 100644
index 0000000..db2a730
--- /dev/null
+++ b/codec-dns/src/main/java/io/netty/handler/codec/dns/DnsQueryEncoder.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2019 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.dns;
+
+import io.netty.buffer.ByteBuf;
+
+import static io.netty.util.internal.ObjectUtil.checkNotNull;
+
+final class DnsQueryEncoder {
+
+    private final DnsRecordEncoder recordEncoder;
+
+    /**
+     * Creates a new encoder with the specified {@code recordEncoder}.
+     */
+    DnsQueryEncoder(DnsRecordEncoder recordEncoder) {
+        this.recordEncoder = checkNotNull(recordEncoder, "recordEncoder");
+    }
+
+    /**
+     * Encodes the given {@link DnsQuery} into a {@link ByteBuf}.
+     */
+    void encode(DnsQuery query, ByteBuf out) throws Exception {
+        encodeHeader(query, out);
+        encodeQuestions(query, out);
+        encodeRecords(query, DnsSection.ADDITIONAL, out);
+    }
+
+    /**
+     * Encodes the header that is always 12 bytes long.
+     *
+     * @param query the query header being encoded
+     * @param buf   the buffer the encoded data should be written to
+     */
+    private static void encodeHeader(DnsQuery query, ByteBuf buf) {
+        buf.writeShort(query.id());
+        int flags = 0;
+        flags |= (query.opCode().byteValue() & 0xFF) << 14;
+        if (query.isRecursionDesired()) {
+            flags |= 1 << 8;
+        }
+        buf.writeShort(flags);
+        buf.writeShort(query.count(DnsSection.QUESTION));
+        buf.writeShort(0); // answerCount
+        buf.writeShort(0); // authorityResourceCount
+        buf.writeShort(query.count(DnsSection.ADDITIONAL));
+    }
+
+    private void encodeQuestions(DnsQuery query, ByteBuf buf) throws Exception {
+        final int count = query.count(DnsSection.QUESTION);
+        for (int i = 0; i < count; i++) {
+            recordEncoder.encodeQuestion((DnsQuestion) query.recordAt(DnsSection.QUESTION, i), buf);
+        }
+    }
+
+    private void encodeRecords(DnsQuery query, DnsSection section, ByteBuf buf) throws Exception {
+        final int count = query.count(section);
+        for (int i = 0; i < count; i++) {
+            recordEncoder.encodeRecord(query.recordAt(section, i), buf);
+        }
+    }
+}
diff --git a/codec-dns/src/main/java/io/netty/handler/codec/dns/DnsResponseDecoder.java b/codec-dns/src/main/java/io/netty/handler/codec/dns/DnsResponseDecoder.java
new file mode 100644
index 0000000..aadf3c2
--- /dev/null
+++ b/codec-dns/src/main/java/io/netty/handler/codec/dns/DnsResponseDecoder.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2019 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.dns;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.handler.codec.CorruptedFrameException;
+
+import java.net.SocketAddress;
+
+import static io.netty.util.internal.ObjectUtil.checkNotNull;
+
+abstract class DnsResponseDecoder<A extends SocketAddress> {
+
+    private final DnsRecordDecoder recordDecoder;
+
+    /**
+     * Creates a new decoder with the specified {@code recordDecoder}.
+     */
+    DnsResponseDecoder(DnsRecordDecoder recordDecoder) {
+        this.recordDecoder = checkNotNull(recordDecoder, "recordDecoder");
+    }
+
+    final DnsResponse decode(A sender, A recipient, ByteBuf buffer) throws Exception {
+        final int id = buffer.readUnsignedShort();
+
+        final int flags = buffer.readUnsignedShort();
+        if (flags >> 15 == 0) {
+            throw new CorruptedFrameException("not a response");
+        }
+
+        final DnsResponse response = newResponse(
+                sender,
+                recipient,
+                id,
+                DnsOpCode.valueOf((byte) (flags >> 11 & 0xf)), DnsResponseCode.valueOf((byte) (flags & 0xf)));
+
+        response.setRecursionDesired((flags >> 8 & 1) == 1);
+        response.setAuthoritativeAnswer((flags >> 10 & 1) == 1);
+        response.setTruncated((flags >> 9 & 1) == 1);
+        response.setRecursionAvailable((flags >> 7 & 1) == 1);
+        response.setZ(flags >> 4 & 0x7);
+
+        boolean success = false;
+        try {
+            final int questionCount = buffer.readUnsignedShort();
+            final int answerCount = buffer.readUnsignedShort();
+            final int authorityRecordCount = buffer.readUnsignedShort();
+            final int additionalRecordCount = buffer.readUnsignedShort();
+
+            decodeQuestions(response, buffer, questionCount);
+            if (!decodeRecords(response, DnsSection.ANSWER, buffer, answerCount)) {
+                success = true;
+                return response;
+            }
+            if (!decodeRecords(response, DnsSection.AUTHORITY, buffer, authorityRecordCount)) {
+                success = true;
+                return response;
+            }
+
+            decodeRecords(response, DnsSection.ADDITIONAL, buffer, additionalRecordCount);
+            success = true;
+            return response;
+        } finally {
+            if (!success) {
+                response.release();
+            }
+        }
+    }
+
+    protected abstract DnsResponse newResponse(A sender, A recipient, int id,
+                                               DnsOpCode opCode, DnsResponseCode responseCode) throws Exception;
+
+    private void decodeQuestions(DnsResponse response, ByteBuf buf, int questionCount) throws Exception {
+        for (int i = questionCount; i > 0; i --) {
+            response.addRecord(DnsSection.QUESTION, recordDecoder.decodeQuestion(buf));
+        }
+    }
+
+    private boolean decodeRecords(
+            DnsResponse response, DnsSection section, ByteBuf buf, int count) throws Exception {
+        for (int i = count; i > 0; i --) {
+            final DnsRecord r = recordDecoder.decodeRecord(buf);
+            if (r == null) {
+                // Truncated response
+                return false;
+            }
+
+            response.addRecord(section, r);
+        }
+        return true;
+    }
+}
diff --git a/codec-dns/src/main/java/io/netty/handler/codec/dns/TcpDnsQueryEncoder.java b/codec-dns/src/main/java/io/netty/handler/codec/dns/TcpDnsQueryEncoder.java
new file mode 100644
index 0000000..2978fff
--- /dev/null
+++ b/codec-dns/src/main/java/io/netty/handler/codec/dns/TcpDnsQueryEncoder.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2019 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.dns;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.MessageToByteEncoder;
+import io.netty.util.internal.UnstableApi;
+
+@ChannelHandler.Sharable
+@UnstableApi
+public final class TcpDnsQueryEncoder extends MessageToByteEncoder<DnsQuery> {
+
+    private final DnsQueryEncoder encoder;
+
+    /**
+     * Creates a new encoder with {@linkplain DnsRecordEncoder#DEFAULT the default record encoder}.
+     */
+    public TcpDnsQueryEncoder() {
+        this(DnsRecordEncoder.DEFAULT);
+    }
+
+    /**
+     * Creates a new encoder with the specified {@code recordEncoder}.
+     */
+    public TcpDnsQueryEncoder(DnsRecordEncoder recordEncoder) {
+        this.encoder = new DnsQueryEncoder(recordEncoder);
+    }
+
+    @Override
+    protected void encode(ChannelHandlerContext ctx, DnsQuery msg, ByteBuf out) throws Exception {
+        // Length is two octets as defined by RFC-7766
+        // See https://tools.ietf.org/html/rfc7766#section-8
+        out.writerIndex(out.writerIndex() + 2);
+        encoder.encode(msg, out);
+
+        // Now fill in the correct length based on the amount of data that we wrote the ByteBuf.
+        out.setShort(0, out.readableBytes() - 2);
+    }
+
+    @Override
+    protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, @SuppressWarnings("unused") DnsQuery msg,
+                                     boolean preferDirect) {
+        if (preferDirect) {
+            return ctx.alloc().ioBuffer(1024);
+        } else {
+            return ctx.alloc().heapBuffer(1024);
+        }
+    }
+}
diff --git a/codec-dns/src/main/java/io/netty/handler/codec/dns/TcpDnsResponseDecoder.java b/codec-dns/src/main/java/io/netty/handler/codec/dns/TcpDnsResponseDecoder.java
new file mode 100644
index 0000000..f8a92d9
--- /dev/null
+++ b/codec-dns/src/main/java/io/netty/handler/codec/dns/TcpDnsResponseDecoder.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2019 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.dns;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
+import io.netty.util.internal.UnstableApi;
+
+import java.net.SocketAddress;
+
+@UnstableApi
+public final class TcpDnsResponseDecoder extends LengthFieldBasedFrameDecoder {
+
+    private final DnsResponseDecoder<SocketAddress> responseDecoder;
+
+    /**
+     * Creates a new decoder with {@linkplain DnsRecordDecoder#DEFAULT the default record decoder}.
+     */
+    public TcpDnsResponseDecoder() {
+        this(DnsRecordDecoder.DEFAULT, 64 * 1024);
+    }
+
+    /**
+     * Creates a new decoder with the specified {@code recordDecoder} and {@code maxFrameLength}
+     */
+    public TcpDnsResponseDecoder(DnsRecordDecoder recordDecoder, int maxFrameLength) {
+        // Length is two octets as defined by RFC-7766
+        // See https://tools.ietf.org/html/rfc7766#section-8
+        super(maxFrameLength, 0, 2, 0, 2);
+
+        this.responseDecoder = new DnsResponseDecoder<SocketAddress>(recordDecoder) {
+            @Override
+            protected DnsResponse newResponse(SocketAddress sender, SocketAddress recipient,
+                                              int id, DnsOpCode opCode, DnsResponseCode responseCode) {
+                return new DefaultDnsResponse(id, opCode, responseCode);
+            }
+        };
+    }
+
+    @Override
+    protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
+        ByteBuf frame = (ByteBuf) super.decode(ctx, in);
+        if (frame == null) {
+            return null;
+        }
+
+        try {
+            return responseDecoder.decode(ctx.channel().remoteAddress(), ctx.channel().localAddress(), frame.slice());
+        } finally {
+            frame.release();
+        }
+    }
+
+    @Override
+    protected ByteBuf extractFrame(ChannelHandlerContext ctx, ByteBuf buffer, int index, int length) {
+        return buffer.copy(index, length);
+    }
+}
diff --git a/codec-dns/src/test/java/io/netty/handler/codec/dns/DefaultDnsRecordDecoderTest.java b/codec-dns/src/test/java/io/netty/handler/codec/dns/DefaultDnsRecordDecoderTest.java
index 6de6ce5..b18c19e 100644
--- a/codec-dns/src/test/java/io/netty/handler/codec/dns/DefaultDnsRecordDecoderTest.java
+++ b/codec-dns/src/test/java/io/netty/handler/codec/dns/DefaultDnsRecordDecoderTest.java
@@ -16,10 +16,11 @@
 package io.netty.handler.codec.dns;
 
 import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
 import io.netty.buffer.Unpooled;
 import org.junit.Test;
 
-import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.*;
 
 public class DefaultDnsRecordDecoderTest {
 
@@ -89,6 +90,81 @@ public class DefaultDnsRecordDecoderTest {
         }
     }
 
+    @Test
+    public void testdecompressCompressPointer() {
+        byte[] compressionPointer = {
+                5, 'n', 'e', 't', 't', 'y', 2, 'i', 'o', 0,
+                (byte) 0xC0, 0
+        };
+        ByteBuf buffer = Unpooled.wrappedBuffer(compressionPointer);
+        ByteBuf uncompressed = null;
+        try {
+            uncompressed = DnsCodecUtil.decompressDomainName(buffer.duplicate().setIndex(10, 12));
+            assertEquals(0, ByteBufUtil.compare(buffer.duplicate().setIndex(0, 10), uncompressed));
+        } finally {
+            buffer.release();
+            if (uncompressed != null) {
+                uncompressed.release();
+            }
+        }
+    }
+
+    @Test
+    public void testdecompressNestedCompressionPointer() {
+        byte[] nestedCompressionPointer = {
+                6, 'g', 'i', 't', 'h', 'u', 'b', 2, 'i', 'o', 0, // github.io
+                5, 'n', 'e', 't', 't', 'y', (byte) 0xC0, 0, // netty.github.io
+                (byte) 0xC0, 11, // netty.github.io
+        };
+        ByteBuf buffer = Unpooled.wrappedBuffer(nestedCompressionPointer);
+        ByteBuf uncompressed = null;
+        try {
+            uncompressed = DnsCodecUtil.decompressDomainName(buffer.duplicate().setIndex(19, 21));
+            assertEquals(0, ByteBufUtil.compare(
+                    Unpooled.wrappedBuffer(new byte[] {
+                            5, 'n', 'e', 't', 't', 'y', 6, 'g', 'i', 't', 'h', 'u', 'b', 2, 'i', 'o', 0
+                    }), uncompressed));
+        } finally {
+            buffer.release();
+            if (uncompressed != null) {
+                uncompressed.release();
+            }
+        }
+    }
+
+    @Test
+    public void testDecodeCompressionRDataPointer() throws Exception {
+        DefaultDnsRecordDecoder decoder = new DefaultDnsRecordDecoder();
+        byte[] compressionPointer = {
+                5, 'n', 'e', 't', 't', 'y', 2, 'i', 'o', 0,
+                (byte) 0xC0, 0
+        };
+        ByteBuf buffer = Unpooled.wrappedBuffer(compressionPointer);
+        DefaultDnsRawRecord cnameRecord = null;
+        DefaultDnsRawRecord nsRecord = null;
+        try {
+            cnameRecord = (DefaultDnsRawRecord) decoder.decodeRecord(
+                    "netty.github.io", DnsRecordType.CNAME, DnsRecord.CLASS_IN, 60, buffer, 10, 2);
+            assertEquals("The rdata of CNAME-type record should be decompressed in advance",
+                         0, ByteBufUtil.compare(buffer.duplicate().setIndex(0, 10), cnameRecord.content()));
+            assertEquals("netty.io.", DnsCodecUtil.decodeDomainName(cnameRecord.content()));
+            nsRecord = (DefaultDnsRawRecord) decoder.decodeRecord(
+                    "netty.github.io", DnsRecordType.NS, DnsRecord.CLASS_IN, 60, buffer, 10, 2);
+            assertEquals("The rdata of NS-type record should be decompressed in advance",
+                         0, ByteBufUtil.compare(buffer.duplicate().setIndex(0, 10), nsRecord.content()));
+            assertEquals("netty.io.", DnsCodecUtil.decodeDomainName(nsRecord.content()));
+        } finally {
+            buffer.release();
+            if (cnameRecord != null) {
+                cnameRecord.release();
+            }
+
+            if (nsRecord != null) {
+                nsRecord.release();
+            }
+        }
+    }
+
     @Test
     public void testDecodeMessageCompression() throws Exception {
         // See https://www.ietf.org/rfc/rfc1035 [4.1.4. Message compression]
@@ -156,4 +232,24 @@ public class DefaultDnsRecordDecoderTest {
             buffer.release();
         }
     }
+
+    @Test
+    public void testTruncatedPacket() throws Exception {
+        ByteBuf buffer = Unpooled.buffer();
+        buffer.writeByte(0);
+        buffer.writeShort(DnsRecordType.A.intValue());
+        buffer.writeShort(1);
+        buffer.writeInt(32);
+
+        // Write a truncated last value.
+        buffer.writeByte(0);
+        DefaultDnsRecordDecoder decoder = new DefaultDnsRecordDecoder();
+        try {
+            int readerIndex = buffer.readerIndex();
+            assertNull(decoder.decodeRecord(buffer));
+            assertEquals(readerIndex, buffer.readerIndex());
+        } finally {
+            buffer.release();
+        }
+    }
 }
diff --git a/codec-haproxy/pom.xml b/codec-haproxy/pom.xml
index 9e5f5f7..6d03c04 100644
--- a/codec-haproxy/pom.xml
+++ b/codec-haproxy/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>io.netty</groupId>
     <artifactId>netty-parent</artifactId>
-    <version>4.1.33.Final</version>
+    <version>4.1.48.Final</version>
   </parent>
 
   <artifactId>netty-codec-haproxy</artifactId>
diff --git a/codec-haproxy/src/main/java/io/netty/handler/codec/haproxy/HAProxyMessage.java b/codec-haproxy/src/main/java/io/netty/handler/codec/haproxy/HAProxyMessage.java
index b40bf42..1777e67 100644
--- a/codec-haproxy/src/main/java/io/netty/handler/codec/haproxy/HAProxyMessage.java
+++ b/codec-haproxy/src/main/java/io/netty/handler/codec/haproxy/HAProxyMessage.java
@@ -17,9 +17,14 @@ package io.netty.handler.codec.haproxy;
 
 import io.netty.buffer.ByteBuf;
 import io.netty.handler.codec.haproxy.HAProxyProxiedProtocol.AddressFamily;
+import io.netty.util.AbstractReferenceCounted;
 import io.netty.util.ByteProcessor;
 import io.netty.util.CharsetUtil;
 import io.netty.util.NetUtil;
+import io.netty.util.ResourceLeakDetector;
+import io.netty.util.ResourceLeakDetectorFactory;
+import io.netty.util.ResourceLeakTracker;
+import io.netty.util.internal.ObjectUtil;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -28,29 +33,11 @@ import java.util.List;
 /**
  * Message container for decoded HAProxy proxy protocol parameters
  */
-public final class HAProxyMessage {
-
-    /**
-     * Version 1 proxy protocol message for 'UNKNOWN' proxied protocols. Per spec, when the proxied protocol is
-     * 'UNKNOWN' we must discard all other header values.
-     */
-    private static final HAProxyMessage V1_UNKNOWN_MSG = new HAProxyMessage(
-            HAProxyProtocolVersion.V1, HAProxyCommand.PROXY, HAProxyProxiedProtocol.UNKNOWN, null, null, 0, 0);
-
-    /**
-     * Version 2 proxy protocol message for 'UNKNOWN' proxied protocols. Per spec, when the proxied protocol is
-     * 'UNKNOWN' we must discard all other header values.
-     */
-    private static final HAProxyMessage V2_UNKNOWN_MSG = new HAProxyMessage(
-            HAProxyProtocolVersion.V2, HAProxyCommand.PROXY, HAProxyProxiedProtocol.UNKNOWN, null, null, 0, 0);
-
-    /**
-     * Version 2 proxy protocol message for local requests. Per spec, we should use an unspecified protocol and family
-     * for 'LOCAL' commands. Per spec, when the proxied protocol is 'UNKNOWN' we must discard all other header values.
-     */
-    private static final HAProxyMessage V2_LOCAL_MSG = new HAProxyMessage(
-            HAProxyProtocolVersion.V2, HAProxyCommand.LOCAL, HAProxyProxiedProtocol.UNKNOWN, null, null, 0, 0);
+public final class HAProxyMessage extends AbstractReferenceCounted {
+    private static final ResourceLeakDetector<HAProxyMessage> leakDetector =
+            ResourceLeakDetectorFactory.instance().newResourceLeakDetector(HAProxyMessage.class);
 
+    private final ResourceLeakTracker<HAProxyMessage> leak;
     private final HAProxyProtocolVersion protocolVersion;
     private final HAProxyCommand command;
     private final HAProxyProxiedProtocol proxiedProtocol;
@@ -90,9 +77,7 @@ public final class HAProxyMessage {
             String sourceAddress, String destinationAddress, int sourcePort, int destinationPort,
             List<HAProxyTLV> tlvs) {
 
-        if (proxiedProtocol == null) {
-            throw new NullPointerException("proxiedProtocol");
-        }
+        ObjectUtil.checkNotNull(proxiedProtocol, "proxiedProtocol");
         AddressFamily addrFamily = proxiedProtocol.addressFamily();
 
         checkAddress(sourceAddress, addrFamily);
@@ -108,6 +93,8 @@ public final class HAProxyMessage {
         this.sourcePort = sourcePort;
         this.destinationPort = destinationPort;
         this.tlvs = Collections.unmodifiableList(tlvs);
+
+        leak = leakDetector.track(this);
     }
 
     /**
@@ -118,9 +105,7 @@ public final class HAProxyMessage {
      * @throws HAProxyProtocolException  if any portion of the header is invalid
      */
     static HAProxyMessage decodeHeader(ByteBuf header) {
-        if (header == null) {
-            throw new NullPointerException("header");
-        }
+        ObjectUtil.checkNotNull(header, "header");
 
         if (header.readableBytes() < 16) {
             throw new HAProxyProtocolException(
@@ -150,7 +135,7 @@ public final class HAProxyMessage {
         }
 
         if (cmd == HAProxyCommand.LOCAL) {
-            return V2_LOCAL_MSG;
+            return unknownMsg(HAProxyProtocolVersion.V2, HAProxyCommand.LOCAL);
         }
 
         // Per spec, the 14th byte is the protocol and address family byte
@@ -162,7 +147,7 @@ public final class HAProxyMessage {
         }
 
         if (protAndFam == HAProxyProxiedProtocol.UNKNOWN) {
-            return V2_UNKNOWN_MSG;
+            return unknownMsg(HAProxyProtocolVersion.V2, HAProxyCommand.PROXY);
         }
 
         int addressInfoLen = header.readUnsignedShort();
@@ -286,7 +271,7 @@ public final class HAProxyMessage {
                 return new HAProxySSLTLV(verify, client, encapsulatedTlvs, rawContent);
             }
             return new HAProxySSLTLV(verify, client, Collections.<HAProxyTLV>emptyList(), rawContent);
-        // If we're not dealing with a SSL Type, we can use the same mechanism
+        // If we're not dealing with an SSL Type, we can use the same mechanism
         case PP2_TYPE_ALPN:
         case PP2_TYPE_AUTHORITY:
         case PP2_TYPE_SSL_VERSION:
@@ -337,7 +322,7 @@ public final class HAProxyMessage {
         }
 
         if (protAndFam == HAProxyProxiedProtocol.UNKNOWN) {
-            return V1_UNKNOWN_MSG;
+            return unknownMsg(HAProxyProtocolVersion.V1, HAProxyCommand.PROXY);
         }
 
         if (numParts != 6) {
@@ -349,6 +334,14 @@ public final class HAProxyMessage {
                 protAndFam, parts[2], parts[3], parts[4], parts[5]);
     }
 
+    /**
+     * Proxy protocol message for 'UNKNOWN' proxied protocols. Per spec, when the proxied protocol is
+     * 'UNKNOWN' we must discard all other header values.
+     */
+    private static HAProxyMessage unknownMsg(HAProxyProtocolVersion version, HAProxyCommand command) {
+        return new HAProxyMessage(version, command, HAProxyProxiedProtocol.UNKNOWN, null, null, 0, 0);
+    }
+
     /**
      * Convert ip address bytes to string representation
      *
@@ -358,31 +351,20 @@ public final class HAProxyMessage {
      */
     private static String ipBytesToString(ByteBuf header, int addressLen) {
         StringBuilder sb = new StringBuilder();
-        if (addressLen == 4) {
-            sb.append(header.readByte() & 0xff);
-            sb.append('.');
-            sb.append(header.readByte() & 0xff);
-            sb.append('.');
-            sb.append(header.readByte() & 0xff);
-            sb.append('.');
-            sb.append(header.readByte() & 0xff);
+        final int ipv4Len = 4;
+        final int ipv6Len = 8;
+        if (addressLen == ipv4Len) {
+            for (int i = 0; i < ipv4Len; i++) {
+                sb.append(header.readByte() & 0xff);
+                sb.append('.');
+            }
         } else {
-            sb.append(Integer.toHexString(header.readUnsignedShort()));
-            sb.append(':');
-            sb.append(Integer.toHexString(header.readUnsignedShort()));
-            sb.append(':');
-            sb.append(Integer.toHexString(header.readUnsignedShort()));
-            sb.append(':');
-            sb.append(Integer.toHexString(header.readUnsignedShort()));
-            sb.append(':');
-            sb.append(Integer.toHexString(header.readUnsignedShort()));
-            sb.append(':');
-            sb.append(Integer.toHexString(header.readUnsignedShort()));
-            sb.append(':');
-            sb.append(Integer.toHexString(header.readUnsignedShort()));
-            sb.append(':');
-            sb.append(Integer.toHexString(header.readUnsignedShort()));
+            for (int i = 0; i < ipv6Len; i++) {
+                sb.append(Integer.toHexString(header.readUnsignedShort()));
+                sb.append(':');
+            }
         }
+        sb.setLength(sb.length() - 1);
         return sb.toString();
     }
 
@@ -416,9 +398,7 @@ public final class HAProxyMessage {
      * @throws HAProxyProtocolException  if the address is invalid
      */
     private static void checkAddress(String address, AddressFamily addrFamily) {
-        if (addrFamily == null) {
-            throw new NullPointerException("addrFamily");
-        }
+        ObjectUtil.checkNotNull(addrFamily, "addrFamily");
 
         switch (addrFamily) {
             case AF_UNSPEC:
@@ -430,9 +410,7 @@ public final class HAProxyMessage {
                 return;
         }
 
-        if (address == null) {
-            throw new NullPointerException("address");
-        }
+        ObjectUtil.checkNotNull(address, "address");
 
         switch (addrFamily) {
             case AF_IPv4:
@@ -519,4 +497,63 @@ public final class HAProxyMessage {
     public List<HAProxyTLV> tlvs() {
         return tlvs;
     }
+
+    @Override
+    public HAProxyMessage touch() {
+        tryRecord();
+        return (HAProxyMessage) super.touch();
+    }
+
+    @Override
+    public HAProxyMessage touch(Object hint) {
+        if (leak != null) {
+            leak.record(hint);
+        }
+        return this;
+    }
+
+    @Override
+    public HAProxyMessage retain() {
+        tryRecord();
+        return (HAProxyMessage) super.retain();
+    }
+
+    @Override
+    public HAProxyMessage retain(int increment) {
+        tryRecord();
+        return (HAProxyMessage) super.retain(increment);
+    }
+
+    @Override
+    public boolean release() {
+        tryRecord();
+        return super.release();
+    }
+
+    @Override
+    public boolean release(int decrement) {
+        tryRecord();
+        return super.release(decrement);
+    }
+
+    private void tryRecord() {
+        if (leak != null) {
+            leak.record();
+        }
+    }
+
+    @Override
+    protected void deallocate() {
+        try {
+            for (HAProxyTLV tlv : tlvs) {
+                tlv.release();
+            }
+        } finally {
+            final ResourceLeakTracker<HAProxyMessage> leak = this.leak;
+            if (leak != null) {
+                boolean closed = leak.close(this);
+                assert closed;
+            }
+        }
+    }
 }
diff --git a/codec-haproxy/src/main/java/io/netty/handler/codec/haproxy/HAProxyMessageDecoder.java b/codec-haproxy/src/main/java/io/netty/handler/codec/haproxy/HAProxyMessageDecoder.java
index df2a663..87bea65 100644
--- a/codec-haproxy/src/main/java/io/netty/handler/codec/haproxy/HAProxyMessageDecoder.java
+++ b/codec-haproxy/src/main/java/io/netty/handler/codec/haproxy/HAProxyMessageDecoder.java
@@ -18,7 +18,6 @@ package io.netty.handler.codec.haproxy;
 import io.netty.buffer.ByteBuf;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.handler.codec.ByteToMessageDecoder;
-import io.netty.handler.codec.LineBasedFrameDecoder;
 import io.netty.handler.codec.ProtocolDetectionResult;
 import io.netty.util.CharsetUtil;
 
@@ -50,11 +49,6 @@ public class HAProxyMessageDecoder extends ByteToMessageDecoder {
      */
     private static final int V2_MAX_TLV = 65535 - 216;
 
-    /**
-     * Version 1 header delimiter is always '\r\n' per spec
-     */
-    private static final int DELIMITER_LENGTH = 2;
-
     /**
      * Binary header prefix
      */
@@ -98,6 +92,11 @@ public class HAProxyMessageDecoder extends ByteToMessageDecoder {
     private static final ProtocolDetectionResult<HAProxyProtocolVersion> DETECTION_RESULT_V2 =
             ProtocolDetectionResult.detected(HAProxyProtocolVersion.V2);
 
+    /**
+     * Used to extract a header frame out of the {@link ByteBuf} and return it.
+     */
+    private HeaderExtractor headerExtractor;
+
     /**
      * {@code true} if we're discarding input because we're already over maxLength
      */
@@ -108,6 +107,11 @@ public class HAProxyMessageDecoder extends ByteToMessageDecoder {
      */
     private int discardedBytes;
 
+    /**
+     * Whether or not to throw an exception as soon as we exceed maxLength.
+     */
+    private final boolean failFast;
+
     /**
      * {@code true} if we're finished decoding the proxy protocol header
      */
@@ -125,14 +129,27 @@ public class HAProxyMessageDecoder extends ByteToMessageDecoder {
     private final int v2MaxHeaderSize;
 
     /**
-     * Creates a new decoder with no additional data (TLV) restrictions
+     * Creates a new decoder with no additional data (TLV) restrictions, and should throw an exception as soon as
+     * we exceed maxLength.
      */
     public HAProxyMessageDecoder() {
+        this(true);
+    }
+
+    /**
+     * Creates a new decoder with no additional data (TLV) restrictions, whether or not to throw an exception as soon
+     * as we exceed maxLength.
+     *
+     * @param failFast Whether or not to throw an exception as soon as we exceed maxLength
+     */
+    public HAProxyMessageDecoder(boolean failFast) {
         v2MaxHeaderSize = V2_MAX_LENGTH;
+        this.failFast = failFast;
     }
 
     /**
-     * Creates a new decoder with restricted additional data (TLV) size
+     * Creates a new decoder with restricted additional data (TLV) size, and should throw an exception as soon as
+     * we exceed maxLength.
      * <p>
      * <b>Note:</b> limiting TLV size only affects processing of v2, binary headers. Also, as allowed by the 1.5 spec
      * TLV data is currently ignored. For maximum performance it would be best to configure your upstream proxy host to
@@ -142,6 +159,17 @@ public class HAProxyMessageDecoder extends ByteToMessageDecoder {
      * @param maxTlvSize maximum number of bytes allowed for additional data (Type-Length-Value vectors) in a v2 header
      */
     public HAProxyMessageDecoder(int maxTlvSize) {
+        this(maxTlvSize, true);
+    }
+
+    /**
+     * Creates a new decoder with restricted additional data (TLV) size, whether or not to throw an exception as soon
+     * as we exceed maxLength.
+     *
+     * @param maxTlvSize maximum number of bytes allowed for additional data (Type-Length-Value vectors) in a v2 header
+     * @param failFast Whether or not to throw an exception as soon as we exceed maxLength
+     */
+    public HAProxyMessageDecoder(int maxTlvSize, boolean failFast) {
         if (maxTlvSize < 1) {
             v2MaxHeaderSize = V2_MIN_LENGTH;
         } else if (maxTlvSize > V2_MAX_TLV) {
@@ -154,6 +182,7 @@ public class HAProxyMessageDecoder extends ByteToMessageDecoder {
                 v2MaxHeaderSize = calcMax;
             }
         }
+        this.failFast = failFast;
     }
 
     /**
@@ -259,7 +288,6 @@ public class HAProxyMessageDecoder extends ByteToMessageDecoder {
 
     /**
      * Create a frame out of the {@link ByteBuf} and return it.
-     * Based on code from {@link LineBasedFrameDecoder#decode(ChannelHandlerContext, ByteBuf)}.
      *
      * @param ctx     the {@link ChannelHandlerContext} which this {@link HAProxyMessageDecoder} belongs to
      * @param buffer  the {@link ByteBuf} from which to read data
@@ -267,42 +295,14 @@ public class HAProxyMessageDecoder extends ByteToMessageDecoder {
      *                be created
      */
     private ByteBuf decodeStruct(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
-        final int eoh = findEndOfHeader(buffer);
-        if (!discarding) {
-            if (eoh >= 0) {
-                final int length = eoh - buffer.readerIndex();
-                if (length > v2MaxHeaderSize) {
-                    buffer.readerIndex(eoh);
-                    failOverLimit(ctx, length);
-                    return null;
-                }
-                return buffer.readSlice(length);
-            } else {
-                final int length = buffer.readableBytes();
-                if (length > v2MaxHeaderSize) {
-                    discardedBytes = length;
-                    buffer.skipBytes(length);
-                    discarding = true;
-                    failOverLimit(ctx, "over " + discardedBytes);
-                }
-                return null;
-            }
-        } else {
-            if (eoh >= 0) {
-                buffer.readerIndex(eoh);
-                discardedBytes = 0;
-                discarding = false;
-            } else {
-                discardedBytes = buffer.readableBytes();
-                buffer.skipBytes(discardedBytes);
-            }
-            return null;
+        if (headerExtractor == null) {
+            headerExtractor = new StructHeaderExtractor(v2MaxHeaderSize);
         }
+        return headerExtractor.extract(ctx, buffer);
     }
 
     /**
      * Create a frame out of the {@link ByteBuf} and return it.
-     * Based on code from {@link LineBasedFrameDecoder#decode(ChannelHandlerContext, ByteBuf)}.
      *
      * @param ctx     the {@link ChannelHandlerContext} which this {@link HAProxyMessageDecoder} belongs to
      * @param buffer  the {@link ByteBuf} from which to read data
@@ -310,40 +310,10 @@ public class HAProxyMessageDecoder extends ByteToMessageDecoder {
      *                be created
      */
     private ByteBuf decodeLine(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
-        final int eol = findEndOfLine(buffer);
-        if (!discarding) {
-            if (eol >= 0) {
-                final int length = eol - buffer.readerIndex();
-                if (length > V1_MAX_LENGTH) {
-                    buffer.readerIndex(eol + DELIMITER_LENGTH);
-                    failOverLimit(ctx, length);
-                    return null;
-                }
-                ByteBuf frame = buffer.readSlice(length);
-                buffer.skipBytes(DELIMITER_LENGTH);
-                return frame;
-            } else {
-                final int length = buffer.readableBytes();
-                if (length > V1_MAX_LENGTH) {
-                    discardedBytes = length;
-                    buffer.skipBytes(length);
-                    discarding = true;
-                    failOverLimit(ctx, "over " + discardedBytes);
-                }
-                return null;
-            }
-        } else {
-            if (eol >= 0) {
-                final int delimLength = buffer.getByte(eol) == '\r' ? 2 : 1;
-                buffer.readerIndex(eol + delimLength);
-                discardedBytes = 0;
-                discarding = false;
-            } else {
-                discardedBytes = buffer.readableBytes();
-                buffer.skipBytes(discardedBytes);
-            }
-            return null;
+        if (headerExtractor == null) {
+            headerExtractor = new LineHeaderExtractor(V1_MAX_LENGTH);
         }
+        return headerExtractor.extract(ctx, buffer);
     }
 
     private void failOverLimit(final ChannelHandlerContext ctx, int length) {
@@ -399,4 +369,119 @@ public class HAProxyMessageDecoder extends ByteToMessageDecoder {
         }
         return true;
     }
+
+    /**
+     * HeaderExtractor create a header frame out of the {@link ByteBuf}.
+     */
+    private abstract class HeaderExtractor {
+        /** Header max size */
+        private final int maxHeaderSize;
+
+        protected HeaderExtractor(int maxHeaderSize) {
+            this.maxHeaderSize = maxHeaderSize;
+        }
+
+        /**
+         * Create a frame out of the {@link ByteBuf} and return it.
+         *
+         * @param ctx     the {@link ChannelHandlerContext} which this {@link HAProxyMessageDecoder} belongs to
+         * @param buffer  the {@link ByteBuf} from which to read data
+         * @return frame  the {@link ByteBuf} which represent the frame or {@code null} if no frame could
+         *                be created
+         * @throws Exception if exceed maxLength
+         */
+        public ByteBuf extract(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
+            final int eoh = findEndOfHeader(buffer);
+            if (!discarding) {
+                if (eoh >= 0) {
+                    final int length = eoh - buffer.readerIndex();
+                    if (length > maxHeaderSize) {
+                        buffer.readerIndex(eoh + delimiterLength(buffer, eoh));
+                        failOverLimit(ctx, length);
+                        return null;
+                    }
+                    ByteBuf frame = buffer.readSlice(length);
+                    buffer.skipBytes(delimiterLength(buffer, eoh));
+                    return frame;
+                } else {
+                    final int length = buffer.readableBytes();
+                    if (length > maxHeaderSize) {
+                        discardedBytes = length;
+                        buffer.skipBytes(length);
+                        discarding = true;
+                        if (failFast) {
+                            failOverLimit(ctx, "over " + discardedBytes);
+                        }
+                    }
+                    return null;
+                }
+            } else {
+                if (eoh >= 0) {
+                    final int length = discardedBytes + eoh - buffer.readerIndex();
+                    buffer.readerIndex(eoh + delimiterLength(buffer, eoh));
+                    discardedBytes = 0;
+                    discarding = false;
+                    if (!failFast) {
+                        failOverLimit(ctx, "over " + length);
+                    }
+                } else {
+                    discardedBytes += buffer.readableBytes();
+                    buffer.skipBytes(buffer.readableBytes());
+                }
+                return null;
+            }
+        }
+
+        /**
+         * Find the end of the header from the given {@link ByteBuf},the end may be a CRLF, or the length given by the
+         * header.
+         *
+         * @param buffer the buffer to be searched
+         * @return {@code -1} if can not find the end, otherwise return the buffer index of end
+         */
+        protected abstract int findEndOfHeader(ByteBuf buffer);
+
+        /**
+         * Get the length of the header delimiter.
+         *
+         * @param buffer the buffer where delimiter is located
+         * @param eoh index of delimiter
+         * @return length of the delimiter
+         */
+        protected abstract int delimiterLength(ByteBuf buffer, int eoh);
+    }
+
+    private final class LineHeaderExtractor extends HeaderExtractor {
+
+        LineHeaderExtractor(int maxHeaderSize) {
+            super(maxHeaderSize);
+        }
+
+        @Override
+        protected int findEndOfHeader(ByteBuf buffer) {
+            return findEndOfLine(buffer);
+        }
+
+        @Override
+        protected int delimiterLength(ByteBuf buffer, int eoh) {
+            return buffer.getByte(eoh) == '\r' ? 2 : 1;
+        }
+    }
+
+    private final class StructHeaderExtractor extends HeaderExtractor {
+
+        StructHeaderExtractor(int maxHeaderSize) {
+            super(maxHeaderSize);
+        }
+
+        @Override
+        protected int findEndOfHeader(ByteBuf buffer) {
+            return HAProxyMessageDecoder.findEndOfHeader(buffer);
+        }
+
+        @Override
+        protected int delimiterLength(ByteBuf buffer, int eoh) {
+            return 0;
+        }
+    }
 }
diff --git a/codec-haproxy/src/main/java/io/netty/handler/codec/haproxy/HAProxyTLV.java b/codec-haproxy/src/main/java/io/netty/handler/codec/haproxy/HAProxyTLV.java
index 380c6aa..38d79a0 100644
--- a/codec-haproxy/src/main/java/io/netty/handler/codec/haproxy/HAProxyTLV.java
+++ b/codec-haproxy/src/main/java/io/netty/handler/codec/haproxy/HAProxyTLV.java
@@ -85,9 +85,7 @@ public class HAProxyTLV extends DefaultByteBufHolder {
      */
     HAProxyTLV(final Type type, final byte typeByteValue, final ByteBuf content) {
         super(content);
-        checkNotNull(type, "type");
-
-        this.type = type;
+        this.type = checkNotNull(type, "type");
         this.typeByteValue = typeByteValue;
     }
 
diff --git a/codec-haproxy/src/test/java/io/netty/handler/codec/haproxy/HAProxyMessageDecoderTest.java b/codec-haproxy/src/test/java/io/netty/handler/codec/haproxy/HAProxyMessageDecoderTest.java
index 2d4039d..2c323ea 100644
--- a/codec-haproxy/src/test/java/io/netty/handler/codec/haproxy/HAProxyMessageDecoderTest.java
+++ b/codec-haproxy/src/test/java/io/netty/handler/codec/haproxy/HAProxyMessageDecoderTest.java
@@ -24,7 +24,9 @@ import io.netty.handler.codec.haproxy.HAProxyProxiedProtocol.AddressFamily;
 import io.netty.handler.codec.haproxy.HAProxyProxiedProtocol.TransportProtocol;
 import io.netty.util.CharsetUtil;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 
 import java.util.List;
 
@@ -32,6 +34,8 @@ import static io.netty.buffer.Unpooled.*;
 import static org.junit.Assert.*;
 
 public class HAProxyMessageDecoderTest {
+    @Rule
+    public ExpectedException exceptionRule = ExpectedException.none();
 
     private EmbeddedChannel ch;
 
@@ -58,6 +62,7 @@ public class HAProxyMessageDecoderTest {
         assertEquals(443, msg.destinationPort());
         assertNull(ch.readInbound());
         assertFalse(ch.finish());
+        assertTrue(msg.release());
     }
 
     @Test
@@ -78,6 +83,7 @@ public class HAProxyMessageDecoderTest {
         assertEquals(443, msg.destinationPort());
         assertNull(ch.readInbound());
         assertFalse(ch.finish());
+        assertTrue(msg.release());
     }
 
     @Test
@@ -98,6 +104,7 @@ public class HAProxyMessageDecoderTest {
         assertEquals(0, msg.destinationPort());
         assertNull(ch.readInbound());
         assertFalse(ch.finish());
+        assertTrue(msg.release());
     }
 
     @Test(expected = HAProxyProtocolException.class)
@@ -161,6 +168,43 @@ public class HAProxyMessageDecoderTest {
         ch.writeInbound(copiedBuffer(header, CharsetUtil.US_ASCII));
     }
 
+    @Test
+    public void testFailSlowHeaderTooLong() {
+        EmbeddedChannel slowFailCh = new EmbeddedChannel(new HAProxyMessageDecoder(false));
+        try {
+            String headerPart1 = "PROXY TCP4 192.168.0.1 192.168.0.11 56324 " +
+                                 "000000000000000000000000000000000000000000000000000000000000000000000443";
+            // Should not throw exception
+            assertFalse(slowFailCh.writeInbound(copiedBuffer(headerPart1, CharsetUtil.US_ASCII)));
+            String headerPart2 = "more header data";
+            // Should not throw exception
+            assertFalse(slowFailCh.writeInbound(copiedBuffer(headerPart2, CharsetUtil.US_ASCII)));
+            String headerPart3 = "end of header\r\n";
+
+            int discarded = headerPart1.length() + headerPart2.length() + headerPart3.length() - 2;
+            // Should throw exception
+            exceptionRule.expect(HAProxyProtocolException.class);
+            exceptionRule.expectMessage("over " + discarded);
+            assertFalse(slowFailCh.writeInbound(copiedBuffer(headerPart3, CharsetUtil.US_ASCII)));
+        } finally {
+            assertFalse(slowFailCh.finishAndReleaseAll());
+        }
+    }
+
+    @Test
+    public void testFailFastHeaderTooLong() {
+        EmbeddedChannel fastFailCh = new EmbeddedChannel(new HAProxyMessageDecoder(true));
+        try {
+            String headerPart1 = "PROXY TCP4 192.168.0.1 192.168.0.11 56324 " +
+                                 "000000000000000000000000000000000000000000000000000000000000000000000443";
+            exceptionRule.expect(HAProxyProtocolException.class); // Should throw exception, fail fast
+            exceptionRule.expectMessage("over " + headerPart1.length());
+            assertFalse(fastFailCh.writeInbound(copiedBuffer(headerPart1, CharsetUtil.US_ASCII)));
+        } finally {
+            assertFalse(fastFailCh.finishAndReleaseAll());
+        }
+    }
+
     @Test
     public void testIncompleteHeader() {
         String header = "PROXY TCP4 192.168.0.1 192.168.0.11 56324";
@@ -264,6 +308,7 @@ public class HAProxyMessageDecoderTest {
         assertEquals(443, msg.destinationPort());
         assertNull(ch.readInbound());
         assertFalse(ch.finish());
+        assertTrue(msg.release());
     }
 
     @Test
@@ -319,6 +364,7 @@ public class HAProxyMessageDecoderTest {
         assertEquals(443, msg.destinationPort());
         assertNull(ch.readInbound());
         assertFalse(ch.finish());
+        assertTrue(msg.release());
     }
 
     @Test
@@ -398,6 +444,7 @@ public class HAProxyMessageDecoderTest {
         assertEquals(443, msg.destinationPort());
         assertNull(ch.readInbound());
         assertFalse(ch.finish());
+        assertTrue(msg.release());
     }
 
     @Test
@@ -476,6 +523,7 @@ public class HAProxyMessageDecoderTest {
         assertEquals(0, msg.destinationPort());
         assertNull(ch.readInbound());
         assertFalse(ch.finish());
+        assertTrue(msg.release());
     }
 
     @Test
@@ -531,6 +579,7 @@ public class HAProxyMessageDecoderTest {
         assertEquals(0, msg.destinationPort());
         assertNull(ch.readInbound());
         assertFalse(ch.finish());
+        assertTrue(msg.release());
     }
 
     @Test
@@ -586,6 +635,7 @@ public class HAProxyMessageDecoderTest {
         assertEquals(0, msg.destinationPort());
         assertNull(ch.readInbound());
         assertFalse(ch.finish());
+        assertTrue(msg.release());
     }
 
     @Test
@@ -642,9 +692,7 @@ public class HAProxyMessageDecoderTest {
         assertTrue(0 < firstTlv.refCnt());
         assertTrue(0 < secondTlv.refCnt());
         assertTrue(0 < thirdTLV.refCnt());
-        assertFalse(thirdTLV.release());
-        assertFalse(secondTlv.release());
-        assertTrue(firstTlv.release());
+        assertTrue(msg.release());
         assertEquals(0, firstTlv.refCnt());
         assertEquals(0, secondTlv.refCnt());
         assertEquals(0, thirdTLV.refCnt());
@@ -653,6 +701,51 @@ public class HAProxyMessageDecoderTest {
         assertFalse(ch.finish());
     }
 
+    @Test
+    public void testReleaseHAProxyMessage() {
+        ch = new EmbeddedChannel(new HAProxyMessageDecoder());
+
+        final byte[] bytes = {
+                13, 10, 13, 10, 0, 13, 10, 81, 85, 73, 84, 10, 33, 17, 0, 35, 127, 0, 0, 1, 127, 0, 0, 1,
+                -55, -90, 7, 89, 32, 0, 20, 5, 0, 0, 0, 0, 33, 0, 5, 84, 76, 83, 118, 49, 34, 0, 4, 76, 69, 65, 70
+        };
+
+        int startChannels = ch.pipeline().names().size();
+        assertTrue(ch.writeInbound(copiedBuffer(bytes)));
+        Object msgObj = ch.readInbound();
+        assertEquals(startChannels - 1, ch.pipeline().names().size());
+        HAProxyMessage msg = (HAProxyMessage) msgObj;
+
+        final List<HAProxyTLV> tlvs = msg.tlvs();
+        assertEquals(3, tlvs.size());
+
+        assertEquals(1, msg.refCnt());
+        for (HAProxyTLV tlv : tlvs) {
+            assertEquals(3, tlv.refCnt());
+        }
+
+        // Retain the haproxy message
+        msg.retain();
+        assertEquals(2, msg.refCnt());
+        for (HAProxyTLV tlv : tlvs) {
+            assertEquals(3, tlv.refCnt());
+        }
+
+        // Decrease the haproxy message refCnt
+        msg.release();
+        assertEquals(1, msg.refCnt());
+        for (HAProxyTLV tlv : tlvs) {
+            assertEquals(3, tlv.refCnt());
+        }
+
+        // Release haproxy message, TLVs will be released with it
+        msg.release();
+        assertEquals(0, msg.refCnt());
+        for (HAProxyTLV tlv : tlvs) {
+            assertEquals(0, tlv.refCnt());
+        }
+    }
+
     @Test
     public void testV2WithTLV() {
         ch = new EmbeddedChannel(new HAProxyMessageDecoder(4));
@@ -738,6 +831,7 @@ public class HAProxyMessageDecoderTest {
         assertEquals(0, msg.destinationPort());
         assertNull(ch.readInbound());
         assertFalse(ch.finish());
+        assertTrue(msg.release());
     }
 
     @Test(expected = HAProxyProtocolException.class)
diff --git a/codec-http/pom.xml b/codec-http/pom.xml
index 65c517e..2f09d40 100644
--- a/codec-http/pom.xml
+++ b/codec-http/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>io.netty</groupId>
     <artifactId>netty-parent</artifactId>
-    <version>4.1.33.Final</version>
+    <version>4.1.48.Final</version>
   </parent>
 
   <artifactId>netty-codec-http</artifactId>
@@ -57,7 +57,6 @@
       <groupId>${project.groupId}</groupId>
       <artifactId>netty-handler</artifactId>
       <version>${project.version}</version>
-      <optional>true</optional>
     </dependency>
     <dependency>
       <groupId>com.jcraft</groupId>
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/CombinedHttpHeaders.java b/codec-http/src/main/java/io/netty/handler/codec/http/CombinedHttpHeaders.java
index ae49493..b8a2d0b 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/CombinedHttpHeaders.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/CombinedHttpHeaders.java
@@ -79,7 +79,7 @@ public class CombinedHttpHeaders extends DefaultHttpHeaders {
             return charSequenceEscaper;
         }
 
-        public CombinedHttpHeadersImpl(HashingStrategy<CharSequence> nameHashingStrategy,
+        CombinedHttpHeadersImpl(HashingStrategy<CharSequence> nameHashingStrategy,
                 ValueConverter<CharSequence> valueConverter,
                 io.netty.handler.codec.DefaultHeaders.NameValidator<CharSequence> nameValidator) {
             super(nameHashingStrategy, valueConverter, nameValidator);
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultCookie.java b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultCookie.java
index 902ba16..850f8e9 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultCookie.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultCookie.java
@@ -15,6 +15,8 @@
  */
 package io.netty.handler.codec.http;
 
+import io.netty.util.internal.ObjectUtil;
+
 import java.util.Collections;
 import java.util.Set;
 import java.util.TreeSet;
@@ -131,9 +133,7 @@ public class DefaultCookie extends io.netty.handler.codec.http.cookie.DefaultCoo
     @Override
     @Deprecated
     public void setPorts(int... ports) {
-        if (ports == null) {
-            throw new NullPointerException("ports");
-        }
+        ObjectUtil.checkNotNull(ports, "ports");
 
         int[] portsCopy = ports.clone();
         if (portsCopy.length == 0) {
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpContent.java b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpContent.java
index 48fad8e..a77cb21 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpContent.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpContent.java
@@ -16,6 +16,7 @@
 package io.netty.handler.codec.http;
 
 import io.netty.buffer.ByteBuf;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.StringUtil;
 
 /**
@@ -29,10 +30,7 @@ public class DefaultHttpContent extends DefaultHttpObject implements HttpContent
      * Creates a new instance with the specified chunk content.
      */
     public DefaultHttpContent(ByteBuf content) {
-        if (content == null) {
-            throw new NullPointerException("content");
-        }
-        this.content = content;
+        this.content = ObjectUtil.checkNotNull(content, "content");
     }
 
     @Override
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpHeaders.java b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpHeaders.java
index 88af27f..675d513 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpHeaders.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpHeaders.java
@@ -78,6 +78,18 @@ public class DefaultHttpHeaders extends HttpHeaders {
         this(true);
     }
 
+    /**
+     * <b>Warning!</b> Setting <code>validate</code> to <code>false</code> will mean that Netty won't
+     * validate & protect against user-supplied header values that are malicious.
+     * This can leave your server implementation vulnerable to
+     * <a href="https://cwe.mitre.org/data/definitions/113.html">
+     *     CWE-113: Improper Neutralization of CRLF Sequences in HTTP Headers ('HTTP Response Splitting')
+     * </a>.
+     * When disabling this validation, it is the responsibility of the caller to ensure that the values supplied
+     * do not contain a non-url-escaped carriage return (CR) and/or line feed (LF) characters.
+     *
+     * @param validate Should Netty validate Header values to ensure they aren't malicious.
+     */
     public DefaultHttpHeaders(boolean validate) {
         this(validate, nameValidator(validate));
     }
@@ -372,8 +384,7 @@ public class DefaultHttpHeaders extends HttpHeaders {
         default:
             // Check to see if the character is not an ASCII character, or invalid
             if (value < 0) {
-                throw new IllegalArgumentException("a header name cannot contain non-ASCII character: " +
-                        value);
+                throw new IllegalArgumentException("a header name cannot contain non-ASCII character: " + value);
             }
         }
     }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpMessage.java b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpMessage.java
index 5a6a45a..3f6682f 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpMessage.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpMessage.java
@@ -15,6 +15,8 @@
  */
 package io.netty.handler.codec.http;
 
+import io.netty.util.internal.ObjectUtil;
+
 import static io.netty.util.internal.ObjectUtil.checkNotNull;
 
 /**
@@ -89,10 +91,7 @@ public abstract class DefaultHttpMessage extends DefaultHttpObject implements Ht
 
     @Override
     public HttpMessage setProtocolVersion(HttpVersion version) {
-        if (version == null) {
-            throw new NullPointerException("version");
-        }
-        this.version = version;
+        this.version = ObjectUtil.checkNotNull(version, "version");
         return this;
     }
 }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpObject.java b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpObject.java
index c26ad39..71d8cdc 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpObject.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpObject.java
@@ -16,6 +16,7 @@
 package io.netty.handler.codec.http;
 
 import io.netty.handler.codec.DecoderResult;
+import io.netty.util.internal.ObjectUtil;
 
 public class DefaultHttpObject implements HttpObject {
 
@@ -39,10 +40,7 @@ public class DefaultHttpObject implements HttpObject {
 
     @Override
     public void setDecoderResult(DecoderResult decoderResult) {
-        if (decoderResult == null) {
-            throw new NullPointerException("decoderResult");
-        }
-        this.decoderResult = decoderResult;
+        this.decoderResult = ObjectUtil.checkNotNull(decoderResult, "decoderResult");
     }
 
     @Override
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpRequest.java b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpRequest.java
index 84be3bb..dbc7dd3 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpRequest.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpRequest.java
@@ -15,6 +15,8 @@
  */
 package io.netty.handler.codec.http;
 
+import io.netty.util.internal.ObjectUtil;
+
 import static io.netty.util.internal.ObjectUtil.checkNotNull;
 
 /**
@@ -88,19 +90,13 @@ public class DefaultHttpRequest extends DefaultHttpMessage implements HttpReques
 
     @Override
     public HttpRequest setMethod(HttpMethod method) {
-        if (method == null) {
-            throw new NullPointerException("method");
-        }
-        this.method = method;
+        this.method = ObjectUtil.checkNotNull(method, "method");
         return this;
     }
 
     @Override
     public HttpRequest setUri(String uri) {
-        if (uri == null) {
-            throw new NullPointerException("uri");
-        }
-        this.uri = uri;
+        this.uri = ObjectUtil.checkNotNull(uri, "uri");
         return this;
     }
 
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpResponse.java b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpResponse.java
index d5b7cf0..8ee4900 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpResponse.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpResponse.java
@@ -15,6 +15,8 @@
  */
 package io.netty.handler.codec.http;
 
+import io.netty.util.internal.ObjectUtil;
+
 import static io.netty.util.internal.ObjectUtil.checkNotNull;
 
 /**
@@ -88,10 +90,7 @@ public class DefaultHttpResponse extends DefaultHttpMessage implements HttpRespo
 
     @Override
     public HttpResponse setStatus(HttpResponseStatus status) {
-        if (status == null) {
-            throw new NullPointerException("status");
-        }
-        this.status = status;
+        this.status = ObjectUtil.checkNotNull(status, "status");
         return this;
     }
 
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpClientCodec.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpClientCodec.java
index dd1da34..1ca4c46 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpClientCodec.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpClientCodec.java
@@ -160,7 +160,7 @@ public final class HttpClientCodec extends CombinedChannelDuplexHandler<HttpResp
                 return;
             }
 
-            if (msg instanceof HttpRequest && !done) {
+            if (msg instanceof HttpRequest) {
                 queue.offer(((HttpRequest) msg).method());
             }
 
@@ -222,57 +222,63 @@ public final class HttpClientCodec extends CombinedChannelDuplexHandler<HttpResp
 
         @Override
         protected boolean isContentAlwaysEmpty(HttpMessage msg) {
+            // Get the method of the HTTP request that corresponds to the
+            // current response.
+            //
+            // Even if we do not use the method to compare we still need to poll it to ensure we keep
+            // request / response pairs in sync.
+            HttpMethod method = queue.poll();
+
             final int statusCode = ((HttpResponse) msg).status().code();
-            if (statusCode == 100 || statusCode == 101) {
-                // 100-continue and 101 switching protocols response should be excluded from paired comparison.
+            if (statusCode >= 100 && statusCode < 200) {
+                // An informational response should be excluded from paired comparison.
                 // Just delegate to super method which has all the needed handling.
                 return super.isContentAlwaysEmpty(msg);
             }
 
-            // Get the getMethod of the HTTP request that corresponds to the
-            // current response.
-            HttpMethod method = queue.poll();
-
-            char firstChar = method.name().charAt(0);
-            switch (firstChar) {
-            case 'H':
-                // According to 4.3, RFC2616:
-                // All responses to the HEAD request method MUST NOT include a
-                // message-body, even though the presence of entity-header fields
-                // might lead one to believe they do.
-                if (HttpMethod.HEAD.equals(method)) {
-                    return true;
-
-                    // The following code was inserted to work around the servers
-                    // that behave incorrectly.  It has been commented out
-                    // because it does not work with well behaving servers.
-                    // Please note, even if the 'Transfer-Encoding: chunked'
-                    // header exists in the HEAD response, the response should
-                    // have absolutely no content.
-                    //
-                    //// Interesting edge case:
-                    //// Some poorly implemented servers will send a zero-byte
-                    //// chunk if Transfer-Encoding of the response is 'chunked'.
-                    ////
-                    //// return !msg.isChunked();
-                }
-                break;
-            case 'C':
-                // Successful CONNECT request results in a response with empty body.
-                if (statusCode == 200) {
-                    if (HttpMethod.CONNECT.equals(method)) {
-                        // Proxy connection established - Parse HTTP only if configured by parseHttpAfterConnectRequest,
-                        // else pass through.
-                        if (!parseHttpAfterConnectRequest) {
-                            done = true;
-                            queue.clear();
+            // If the remote peer did for example send multiple responses for one request (which is not allowed per
+            // spec but may still be possible) method will be null so guard against it.
+            if (method != null) {
+                char firstChar = method.name().charAt(0);
+                switch (firstChar) {
+                    case 'H':
+                        // According to 4.3, RFC2616:
+                        // All responses to the HEAD request method MUST NOT include a
+                        // message-body, even though the presence of entity-header fields
+                        // might lead one to believe they do.
+                        if (HttpMethod.HEAD.equals(method)) {
+                            return true;
+
+                            // The following code was inserted to work around the servers
+                            // that behave incorrectly.  It has been commented out
+                            // because it does not work with well behaving servers.
+                            // Please note, even if the 'Transfer-Encoding: chunked'
+                            // header exists in the HEAD response, the response should
+                            // have absolutely no content.
+                            //
+                            //// Interesting edge case:
+                            //// Some poorly implemented servers will send a zero-byte
+                            //// chunk if Transfer-Encoding of the response is 'chunked'.
+                            ////
+                            //// return !msg.isChunked();
                         }
-                        return true;
-                    }
+                        break;
+                    case 'C':
+                        // Successful CONNECT request results in a response with empty body.
+                        if (statusCode == 200) {
+                            if (HttpMethod.CONNECT.equals(method)) {
+                                // Proxy connection established - Parse HTTP only if configured by
+                                // parseHttpAfterConnectRequest, else pass through.
+                                if (!parseHttpAfterConnectRequest) {
+                                    done = true;
+                                    queue.clear();
+                                }
+                                return true;
+                            }
+                        }
+                        break;
                 }
-                break;
             }
-
             return super.isContentAlwaysEmpty(msg);
         }
 
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpClientUpgradeHandler.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpClientUpgradeHandler.java
index bf2cd55..3ee5705 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpClientUpgradeHandler.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpClientUpgradeHandler.java
@@ -18,6 +18,7 @@ import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelOutboundHandler;
 import io.netty.channel.ChannelPromise;
 import io.netty.util.AsciiString;
+import io.netty.util.internal.ObjectUtil;
 
 import java.net.SocketAddress;
 import java.util.Collection;
@@ -115,14 +116,8 @@ public class HttpClientUpgradeHandler extends HttpObjectAggregator implements Ch
     public HttpClientUpgradeHandler(SourceCodec sourceCodec, UpgradeCodec upgradeCodec,
                                     int maxContentLength) {
         super(maxContentLength);
-        if (sourceCodec == null) {
-            throw new NullPointerException("sourceCodec");
-        }
-        if (upgradeCodec == null) {
-            throw new NullPointerException("upgradeCodec");
-        }
-        this.sourceCodec = sourceCodec;
-        this.upgradeCodec = upgradeCodec;
+        this.sourceCodec = ObjectUtil.checkNotNull(sourceCodec, "sourceCodec");
+        this.upgradeCodec = ObjectUtil.checkNotNull(upgradeCodec, "upgradeCodec");
     }
 
     @Override
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpContentCompressor.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpContentCompressor.java
index e80469f..b40fae6 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpContentCompressor.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpContentCompressor.java
@@ -132,15 +132,15 @@ public class HttpContentCompressor extends HttpContentEncoder {
     }
 
     @Override
-    protected Result beginEncode(HttpResponse headers, String acceptEncoding) throws Exception {
+    protected Result beginEncode(HttpResponse httpResponse, String acceptEncoding) throws Exception {
         if (this.contentSizeThreshold > 0) {
-            if (headers instanceof HttpContent &&
-                    ((HttpContent) headers).content().readableBytes() < contentSizeThreshold) {
+            if (httpResponse instanceof HttpContent &&
+                    ((HttpContent) httpResponse).content().readableBytes() < contentSizeThreshold) {
                 return null;
             }
         }
 
-        String contentEncoding = headers.headers().get(HttpHeaderNames.CONTENT_ENCODING);
+        String contentEncoding = httpResponse.headers().get(HttpHeaderNames.CONTENT_ENCODING);
         if (contentEncoding != null) {
             // Content-Encoding was set, either as something specific or as the IDENTITY encoding
             // Therefore, we should NOT encode here
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpContentDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpContentDecoder.java
index 3eb7ad3..d2513e4 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpContentDecoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpContentDecoder.java
@@ -51,102 +51,107 @@ public abstract class HttpContentDecoder extends MessageToMessageDecoder<HttpObj
     protected ChannelHandlerContext ctx;
     private EmbeddedChannel decoder;
     private boolean continueResponse;
+    private boolean needRead = true;
 
     @Override
     protected void decode(ChannelHandlerContext ctx, HttpObject msg, List<Object> out) throws Exception {
-        if (msg instanceof HttpResponse && ((HttpResponse) msg).status().code() == 100) {
+        try {
+            if (msg instanceof HttpResponse && ((HttpResponse) msg).status().code() == 100) {
 
-            if (!(msg instanceof LastHttpContent)) {
-                continueResponse = true;
+                if (!(msg instanceof LastHttpContent)) {
+                    continueResponse = true;
+                }
+                // 100-continue response must be passed through.
+                out.add(ReferenceCountUtil.retain(msg));
+                return;
             }
-            // 100-continue response must be passed through.
-            out.add(ReferenceCountUtil.retain(msg));
-            return;
-        }
 
-        if (continueResponse) {
-            if (msg instanceof LastHttpContent) {
-                continueResponse = false;
+            if (continueResponse) {
+                if (msg instanceof LastHttpContent) {
+                    continueResponse = false;
+                }
+                // 100-continue response must be passed through.
+                out.add(ReferenceCountUtil.retain(msg));
+                return;
             }
-            // 100-continue response must be passed through.
-            out.add(ReferenceCountUtil.retain(msg));
-            return;
-        }
 
-        if (msg instanceof HttpMessage) {
-            cleanup();
-            final HttpMessage message = (HttpMessage) msg;
-            final HttpHeaders headers = message.headers();
+            if (msg instanceof HttpMessage) {
+                cleanup();
+                final HttpMessage message = (HttpMessage) msg;
+                final HttpHeaders headers = message.headers();
 
-            // Determine the content encoding.
-            String contentEncoding = headers.get(HttpHeaderNames.CONTENT_ENCODING);
-            if (contentEncoding != null) {
-                contentEncoding = contentEncoding.trim();
-            } else {
-                contentEncoding = IDENTITY;
-            }
-            decoder = newContentDecoder(contentEncoding);
+                // Determine the content encoding.
+                String contentEncoding = headers.get(HttpHeaderNames.CONTENT_ENCODING);
+                if (contentEncoding != null) {
+                    contentEncoding = contentEncoding.trim();
+                } else {
+                    contentEncoding = IDENTITY;
+                }
+                decoder = newContentDecoder(contentEncoding);
 
-            if (decoder == null) {
-                if (message instanceof HttpContent) {
-                    ((HttpContent) message).retain();
+                if (decoder == null) {
+                    if (message instanceof HttpContent) {
+                        ((HttpContent) message).retain();
+                    }
+                    out.add(message);
+                    return;
                 }
-                out.add(message);
-                return;
-            }
 
-            // Remove content-length header:
-            // the correct value can be set only after all chunks are processed/decoded.
-            // If buffering is not an issue, add HttpObjectAggregator down the chain, it will set the header.
-            // Otherwise, rely on LastHttpContent message.
-            if (headers.contains(HttpHeaderNames.CONTENT_LENGTH)) {
-                headers.remove(HttpHeaderNames.CONTENT_LENGTH);
-                headers.set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED);
-            }
-            // Either it is already chunked or EOF terminated.
-            // See https://github.com/netty/netty/issues/5892
+                // Remove content-length header:
+                // the correct value can be set only after all chunks are processed/decoded.
+                // If buffering is not an issue, add HttpObjectAggregator down the chain, it will set the header.
+                // Otherwise, rely on LastHttpContent message.
+                if (headers.contains(HttpHeaderNames.CONTENT_LENGTH)) {
+                    headers.remove(HttpHeaderNames.CONTENT_LENGTH);
+                    headers.set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED);
+                }
+                // Either it is already chunked or EOF terminated.
+                // See https://github.com/netty/netty/issues/5892
 
-            // set new content encoding,
-            CharSequence targetContentEncoding = getTargetContentEncoding(contentEncoding);
-            if (HttpHeaderValues.IDENTITY.contentEquals(targetContentEncoding)) {
-                // Do NOT set the 'Content-Encoding' header if the target encoding is 'identity'
-                // as per: http://tools.ietf.org/html/rfc2616#section-14.11
-                headers.remove(HttpHeaderNames.CONTENT_ENCODING);
-            } else {
-                headers.set(HttpHeaderNames.CONTENT_ENCODING, targetContentEncoding);
-            }
+                // set new content encoding,
+                CharSequence targetContentEncoding = getTargetContentEncoding(contentEncoding);
+                if (HttpHeaderValues.IDENTITY.contentEquals(targetContentEncoding)) {
+                    // Do NOT set the 'Content-Encoding' header if the target encoding is 'identity'
+                    // as per: http://tools.ietf.org/html/rfc2616#section-14.11
+                    headers.remove(HttpHeaderNames.CONTENT_ENCODING);
+                } else {
+                    headers.set(HttpHeaderNames.CONTENT_ENCODING, targetContentEncoding);
+                }
 
-            if (message instanceof HttpContent) {
-                // If message is a full request or response object (headers + data), don't copy data part into out.
-                // Output headers only; data part will be decoded below.
-                // Note: "copy" object must not be an instance of LastHttpContent class,
-                // as this would (erroneously) indicate the end of the HttpMessage to other handlers.
-                HttpMessage copy;
-                if (message instanceof HttpRequest) {
-                    HttpRequest r = (HttpRequest) message; // HttpRequest or FullHttpRequest
-                    copy = new DefaultHttpRequest(r.protocolVersion(), r.method(), r.uri());
-                } else if (message instanceof HttpResponse) {
-                    HttpResponse r = (HttpResponse) message; // HttpResponse or FullHttpResponse
-                    copy = new DefaultHttpResponse(r.protocolVersion(), r.status());
+                if (message instanceof HttpContent) {
+                    // If message is a full request or response object (headers + data), don't copy data part into out.
+                    // Output headers only; data part will be decoded below.
+                    // Note: "copy" object must not be an instance of LastHttpContent class,
+                    // as this would (erroneously) indicate the end of the HttpMessage to other handlers.
+                    HttpMessage copy;
+                    if (message instanceof HttpRequest) {
+                        HttpRequest r = (HttpRequest) message; // HttpRequest or FullHttpRequest
+                        copy = new DefaultHttpRequest(r.protocolVersion(), r.method(), r.uri());
+                    } else if (message instanceof HttpResponse) {
+                        HttpResponse r = (HttpResponse) message; // HttpResponse or FullHttpResponse
+                        copy = new DefaultHttpResponse(r.protocolVersion(), r.status());
+                    } else {
+                        throw new CodecException("Object of class " + message.getClass().getName() +
+                                                 " is not an HttpRequest or HttpResponse");
+                    }
+                    copy.headers().set(message.headers());
+                    copy.setDecoderResult(message.decoderResult());
+                    out.add(copy);
                 } else {
-                    throw new CodecException("Object of class " + message.getClass().getName() +
-                                             " is not a HttpRequest or HttpResponse");
+                    out.add(message);
                 }
-                copy.headers().set(message.headers());
-                copy.setDecoderResult(message.decoderResult());
-                out.add(copy);
-            } else {
-                out.add(message);
             }
-        }
 
-        if (msg instanceof HttpContent) {
-            final HttpContent c = (HttpContent) msg;
-            if (decoder == null) {
-                out.add(c.retain());
-            } else {
-                decodeContent(c, out);
+            if (msg instanceof HttpContent) {
+                final HttpContent c = (HttpContent) msg;
+                if (decoder == null) {
+                    out.add(c.retain());
+                } else {
+                    decodeContent(c, out);
+                }
             }
+        } finally {
+            needRead = out.isEmpty();
         }
     }
 
@@ -170,6 +175,20 @@ public abstract class HttpContentDecoder extends MessageToMessageDecoder<HttpObj
         }
     }
 
+    @Override
+    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
+        boolean needRead = this.needRead;
+        this.needRead = true;
+
+        try {
+            ctx.fireChannelReadComplete();
+        } finally {
+            if (needRead && !ctx.channel().config().isAutoRead()) {
+                ctx.read();
+            }
+        }
+    }
+
     /**
      * Returns a new {@link EmbeddedChannel} that decodes the HTTP message
      * content encoded in the specified <tt>contentEncoding</tt>.
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpContentEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpContentEncoder.java
index c486a67..6f1070c 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpContentEncoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpContentEncoder.java
@@ -22,11 +22,15 @@ import io.netty.channel.embedded.EmbeddedChannel;
 import io.netty.handler.codec.DecoderResult;
 import io.netty.handler.codec.MessageToMessageCodec;
 import io.netty.util.ReferenceCountUtil;
+import io.netty.util.internal.ObjectUtil;
+import io.netty.util.internal.StringUtil;
 
 import java.util.ArrayDeque;
 import java.util.List;
 import java.util.Queue;
 
+import static io.netty.handler.codec.http.HttpHeaderNames.*;
+
 /**
  * Encodes the content of the outbound {@link HttpResponse} and {@link HttpContent}.
  * The original content is replaced with the new content encoded by the
@@ -71,21 +75,30 @@ public abstract class HttpContentEncoder extends MessageToMessageCodec<HttpReque
     }
 
     @Override
-    protected void decode(ChannelHandlerContext ctx, HttpRequest msg, List<Object> out)
-            throws Exception {
-        CharSequence acceptedEncoding = msg.headers().get(HttpHeaderNames.ACCEPT_ENCODING);
-        if (acceptedEncoding == null) {
-            acceptedEncoding = HttpContentDecoder.IDENTITY;
+    protected void decode(ChannelHandlerContext ctx, HttpRequest msg, List<Object> out) throws Exception {
+        CharSequence acceptEncoding;
+        List<String> acceptEncodingHeaders = msg.headers().getAll(ACCEPT_ENCODING);
+        switch (acceptEncodingHeaders.size()) {
+        case 0:
+            acceptEncoding = HttpContentDecoder.IDENTITY;
+            break;
+        case 1:
+            acceptEncoding = acceptEncodingHeaders.get(0);
+            break;
+        default:
+            // Multiple message-header fields https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
+            acceptEncoding = StringUtil.join(",", acceptEncodingHeaders);
+            break;
         }
 
-        HttpMethod meth = msg.method();
-        if (meth == HttpMethod.HEAD) {
-            acceptedEncoding = ZERO_LENGTH_HEAD;
-        } else if (meth == HttpMethod.CONNECT) {
-            acceptedEncoding = ZERO_LENGTH_CONNECT;
+        HttpMethod method = msg.method();
+        if (HttpMethod.HEAD.equals(method)) {
+            acceptEncoding = ZERO_LENGTH_HEAD;
+        } else if (HttpMethod.CONNECT.equals(method)) {
+            acceptEncoding = ZERO_LENGTH_CONNECT;
         }
 
-        acceptEncodingQueue.add(acceptedEncoding);
+        acceptEncodingQueue.add(acceptEncoding);
         out.add(ReferenceCountUtil.retain(msg));
     }
 
@@ -275,8 +288,8 @@ public abstract class HttpContentEncoder extends MessageToMessageCodec<HttpReque
     /**
      * Prepare to encode the HTTP message content.
      *
-     * @param headers
-     *        the headers
+     * @param httpResponse
+     *        the http response
      * @param acceptEncoding
      *        the value of the {@code "Accept-Encoding"} header
      *
@@ -286,7 +299,7 @@ public abstract class HttpContentEncoder extends MessageToMessageCodec<HttpReque
      *         {@code null} if {@code acceptEncoding} is unsupported or rejected
      *         and thus the content should be handled as-is (i.e. no encoding).
      */
-    protected abstract Result beginEncode(HttpResponse headers, String acceptEncoding) throws Exception;
+    protected abstract Result beginEncode(HttpResponse httpResponse, String acceptEncoding) throws Exception;
 
     @Override
     public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
@@ -350,15 +363,8 @@ public abstract class HttpContentEncoder extends MessageToMessageCodec<HttpReque
         private final EmbeddedChannel contentEncoder;
 
         public Result(String targetContentEncoding, EmbeddedChannel contentEncoder) {
-            if (targetContentEncoding == null) {
-                throw new NullPointerException("targetContentEncoding");
-            }
-            if (contentEncoder == null) {
-                throw new NullPointerException("contentEncoder");
-            }
-
-            this.targetContentEncoding = targetContentEncoding;
-            this.contentEncoder = contentEncoder;
+            this.targetContentEncoding = ObjectUtil.checkNotNull(targetContentEncoding, "targetContentEncoding");
+            this.contentEncoder = ObjectUtil.checkNotNull(contentEncoder, "contentEncoder");
         }
 
         public String targetContentEncoding() {
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpHeaders.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpHeaders.java
index 694bc4d..580e5ce 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpHeaders.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpHeaders.java
@@ -22,6 +22,7 @@ import io.netty.handler.codec.Headers;
 import io.netty.handler.codec.HeadersUtils;
 import io.netty.util.AsciiString;
 import io.netty.util.CharsetUtil;
+import io.netty.util.internal.ObjectUtil;
 
 import java.text.ParseException;
 import java.util.Calendar;
@@ -1412,9 +1413,7 @@ public abstract class HttpHeaders implements Iterable<Map.Entry<String, String>>
      * @return {@code this}
      */
     public HttpHeaders add(HttpHeaders headers) {
-        if (headers == null) {
-            throw new NullPointerException("headers");
-        }
+        ObjectUtil.checkNotNull(headers, "headers");
         for (Map.Entry<String, String> e: headers) {
             add(e.getKey(), e.getValue());
         }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpMessage.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpMessage.java
index 357ad99..f2f1a40 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpMessage.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpMessage.java
@@ -17,7 +17,7 @@ package io.netty.handler.codec.http;
 
 
 /**
- * An interface that defines a HTTP message, providing common properties for
+ * An interface that defines an HTTP message, providing common properties for
  * {@link HttpRequest} and {@link HttpResponse}.
  *
  * @see HttpResponse
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpMethod.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpMethod.java
index 3d97551..a634bd0 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpMethod.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpMethod.java
@@ -156,6 +156,9 @@ public class HttpMethod implements Comparable<HttpMethod> {
 
     @Override
     public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
         if (!(o instanceof HttpMethod)) {
             return false;
         }
@@ -171,6 +174,9 @@ public class HttpMethod implements Comparable<HttpMethod> {
 
     @Override
     public int compareTo(HttpMethod o) {
+        if (o == this) {
+            return 0;
+        }
         return name().compareTo(o.name());
     }
 
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectAggregator.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectAggregator.java
index 257581f..192a0ed 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectAggregator.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectAggregator.java
@@ -271,13 +271,6 @@ public class HttpObjectAggregator
                     }
                 });
             }
-
-            // If an oversized request was handled properly and the connection is still alive
-            // (i.e. rejected 100-continue). the decoder should prepare to handle a new message.
-            HttpObjectDecoder decoder = ctx.pipeline().get(HttpObjectDecoder.class);
-            if (decoder != null) {
-                decoder.reset();
-            }
         } else if (oversized instanceof HttpResponse) {
             ctx.close();
             throw new TooLongFrameException("Response entity too large: " + oversized);
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectDecoder.java
index af1d642..4134735 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectDecoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectDecoder.java
@@ -15,6 +15,8 @@
  */
 package io.netty.handler.codec.http;
 
+import static io.netty.util.internal.ObjectUtil.checkPositive;
+
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
 import io.netty.channel.ChannelHandlerContext;
@@ -168,21 +170,10 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder {
     protected HttpObjectDecoder(
             int maxInitialLineLength, int maxHeaderSize, int maxChunkSize,
             boolean chunkedSupported, boolean validateHeaders, int initialBufferSize) {
-        if (maxInitialLineLength <= 0) {
-            throw new IllegalArgumentException(
-                    "maxInitialLineLength must be a positive integer: " +
-                     maxInitialLineLength);
-        }
-        if (maxHeaderSize <= 0) {
-            throw new IllegalArgumentException(
-                    "maxHeaderSize must be a positive integer: " +
-                    maxHeaderSize);
-        }
-        if (maxChunkSize <= 0) {
-            throw new IllegalArgumentException(
-                    "maxChunkSize must be a positive integer: " +
-                    maxChunkSize);
-        }
+        checkPositive(maxInitialLineLength, "maxInitialLineLength");
+        checkPositive(maxHeaderSize, "maxHeaderSize");
+        checkPositive(maxChunkSize, "maxChunkSize");
+
         AppendableCharSequence seq = new AppendableCharSequence(initialBufferSize);
         lineParser = new LineParser(seq, maxInitialLineLength);
         headerParser = new HeaderParser(seq, maxHeaderSize);
@@ -198,12 +189,8 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder {
         }
 
         switch (currentState) {
-        case SKIP_CONTROL_CHARS: {
-            if (!skipControlCharacters(buffer)) {
-                return;
-            }
-            currentState = State.READ_INITIAL;
-        }
+        case SKIP_CONTROL_CHARS:
+            // Fall-through
         case READ_INITIAL: try {
             AppendableCharSequence line = lineParser.parse(buffer);
             if (line == null) {
@@ -290,7 +277,7 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder {
 
             // Check if the buffer is readable first as we use the readable byte count
             // to create the HttpChunk. This is needed as otherwise we may end up with
-            // create a HttpChunk instance that contains an empty buffer and so is
+            // create an HttpChunk instance that contains an empty buffer and so is
             // handled like it is the last HttpChunk.
             //
             // See https://github.com/netty/netty/issues/433
@@ -558,22 +545,6 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder {
         return chunk;
     }
 
-    private static boolean skipControlCharacters(ByteBuf buffer) {
-        boolean skiped = false;
-        final int wIdx = buffer.writerIndex();
-        int rIdx = buffer.readerIndex();
-        while (wIdx > rIdx) {
-            int c = buffer.getUnsignedByte(rIdx++);
-            if (!Character.isISOControl(c) && !Character.isWhitespace(c)) {
-                rIdx--;
-                skiped = true;
-                break;
-            }
-        }
-        buffer.readerIndex(rIdx);
-        return skiped;
-    }
-
     private State readHeaders(ByteBuf buffer) {
         final HttpMessage message = this.message;
         final HttpHeaders headers = message.headers();
@@ -584,7 +555,7 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder {
         }
         if (line.length() > 0) {
             do {
-                char firstChar = line.charAt(0);
+                char firstChar = line.charAtUnsafe(0);
                 if (name != null && (firstChar == ' ' || firstChar == '\t')) {
                     //please do not make one line from below code
                     //as it breaks +XX:OptimizeStringConcat optimization
@@ -609,23 +580,73 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder {
         if (name != null) {
             headers.add(name, value);
         }
+
         // reset name and value fields
         name = null;
         value = null;
 
-        State nextState;
+        List<String> values = headers.getAll(HttpHeaderNames.CONTENT_LENGTH);
+        int contentLengthValuesCount = values.size();
+
+        if (contentLengthValuesCount > 0) {
+            // Guard against multiple Content-Length headers as stated in
+            // https://tools.ietf.org/html/rfc7230#section-3.3.2:
+            //
+            // If a message is received that has multiple Content-Length header
+            //   fields with field-values consisting of the same decimal value, or a
+            //   single Content-Length header field with a field value containing a
+            //   list of identical decimal values (e.g., "Content-Length: 42, 42"),
+            //   indicating that duplicate Content-Length header fields have been
+            //   generated or combined by an upstream message processor, then the
+            //   recipient MUST either reject the message as invalid or replace the
+            //   duplicated field-values with a single valid Content-Length field
+            //   containing that decimal value prior to determining the message body
+            //   length or forwarding the message.
+            if (contentLengthValuesCount > 1 && message.protocolVersion() == HttpVersion.HTTP_1_1) {
+                throw new IllegalArgumentException("Multiple Content-Length headers found");
+            }
+            contentLength = Long.parseLong(values.get(0));
+        }
 
         if (isContentAlwaysEmpty(message)) {
             HttpUtil.setTransferEncodingChunked(message, false);
-            nextState = State.SKIP_CONTROL_CHARS;
+            return State.SKIP_CONTROL_CHARS;
         } else if (HttpUtil.isTransferEncodingChunked(message)) {
-            nextState = State.READ_CHUNK_SIZE;
+            if (contentLengthValuesCount > 0 && message.protocolVersion() == HttpVersion.HTTP_1_1) {
+                handleTransferEncodingChunkedWithContentLength(message);
+            }
+            return State.READ_CHUNK_SIZE;
         } else if (contentLength() >= 0) {
-            nextState = State.READ_FIXED_LENGTH_CONTENT;
+            return State.READ_FIXED_LENGTH_CONTENT;
         } else {
-            nextState = State.READ_VARIABLE_LENGTH_CONTENT;
+            return State.READ_VARIABLE_LENGTH_CONTENT;
         }
-        return nextState;
+    }
+
+    /**
+     * Invoked when a message with both a "Transfer-Encoding: chunked" and a "Content-Length" header field is detected.
+     * The default behavior is to <i>remove</i> the Content-Length field, but this method could be overridden
+     * to change the behavior (to, e.g., throw an exception and produce an invalid message).
+     * <p>
+     * See: https://tools.ietf.org/html/rfc7230#section-3.3.3
+     * <pre>
+     *     If a message is received with both a Transfer-Encoding and a
+     *     Content-Length header field, the Transfer-Encoding overrides the
+     *     Content-Length.  Such a message might indicate an attempt to
+     *     perform request smuggling (Section 9.5) or response splitting
+     *     (Section 9.4) and ought to be handled as an error.  A sender MUST
+     *     remove the received Content-Length field prior to forwarding such
+     *     a message downstream.
+     * </pre>
+     * Also see:
+     * https://github.com/apache/tomcat/blob/b693d7c1981fa7f51e58bc8c8e72e3fe80b7b773/
+     * java/org/apache/coyote/http11/Http11Processor.java#L747-L755
+     * https://github.com/nginx/nginx/blob/0ad4393e30c119d250415cb769e3d8bc8dce5186/
+     * src/http/ngx_http_request.c#L1946-L1953
+     */
+    protected void handleTransferEncodingChunkedWithContentLength(HttpMessage message) {
+        message.headers().remove(HttpHeaderNames.CONTENT_LENGTH);
+        contentLength = Long.MIN_VALUE;
     }
 
     private long contentLength() {
@@ -640,49 +661,50 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder {
         if (line == null) {
             return null;
         }
+        LastHttpContent trailer = this.trailer;
+        if (line.length() == 0 && trailer == null) {
+            // We have received the empty line which signals the trailer is complete and did not parse any trailers
+            // before. Just return an empty last content to reduce allocations.
+            return LastHttpContent.EMPTY_LAST_CONTENT;
+        }
+
         CharSequence lastHeader = null;
-        if (line.length() > 0) {
-            LastHttpContent trailer = this.trailer;
-            if (trailer == null) {
-                trailer = this.trailer = new DefaultLastHttpContent(Unpooled.EMPTY_BUFFER, validateHeaders);
-            }
-            do {
-                char firstChar = line.charAt(0);
-                if (lastHeader != null && (firstChar == ' ' || firstChar == '\t')) {
-                    List<String> current = trailer.trailingHeaders().getAll(lastHeader);
-                    if (!current.isEmpty()) {
-                        int lastPos = current.size() - 1;
-                        //please do not make one line from below code
-                        //as it breaks +XX:OptimizeStringConcat optimization
-                        String lineTrimmed = line.toString().trim();
-                        String currentLastPos = current.get(lastPos);
-                        current.set(lastPos, currentLastPos + lineTrimmed);
-                    }
-                } else {
-                    splitHeader(line);
-                    CharSequence headerName = name;
-                    if (!HttpHeaderNames.CONTENT_LENGTH.contentEqualsIgnoreCase(headerName) &&
+        if (trailer == null) {
+            trailer = this.trailer = new DefaultLastHttpContent(Unpooled.EMPTY_BUFFER, validateHeaders);
+        }
+        while (line.length() > 0) {
+            char firstChar = line.charAtUnsafe(0);
+            if (lastHeader != null && (firstChar == ' ' || firstChar == '\t')) {
+                List<String> current = trailer.trailingHeaders().getAll(lastHeader);
+                if (!current.isEmpty()) {
+                    int lastPos = current.size() - 1;
+                    //please do not make one line from below code
+                    //as it breaks +XX:OptimizeStringConcat optimization
+                    String lineTrimmed = line.toString().trim();
+                    String currentLastPos = current.get(lastPos);
+                    current.set(lastPos, currentLastPos + lineTrimmed);
+                }
+            } else {
+                splitHeader(line);
+                CharSequence headerName = name;
+                if (!HttpHeaderNames.CONTENT_LENGTH.contentEqualsIgnoreCase(headerName) &&
                         !HttpHeaderNames.TRANSFER_ENCODING.contentEqualsIgnoreCase(headerName) &&
                         !HttpHeaderNames.TRAILER.contentEqualsIgnoreCase(headerName)) {
-                        trailer.trailingHeaders().add(headerName, value);
-                    }
-                    lastHeader = name;
-                    // reset name and value fields
-                    name = null;
-                    value = null;
-                }
-
-                line = headerParser.parse(buffer);
-                if (line == null) {
-                    return null;
+                    trailer.trailingHeaders().add(headerName, value);
                 }
-            } while (line.length() > 0);
-
-            this.trailer = null;
-            return trailer;
+                lastHeader = name;
+                // reset name and value fields
+                name = null;
+                value = null;
+            }
+            line = headerParser.parse(buffer);
+            if (line == null) {
+                return null;
+            }
         }
 
-        return LastHttpContent.EMPTY_LAST_CONTENT;
+        this.trailer = null;
+        return trailer;
     }
 
     protected abstract boolean isDecodingRequest();
@@ -710,13 +732,13 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder {
         int cStart;
         int cEnd;
 
-        aStart = findNonWhitespace(sb, 0);
-        aEnd = findWhitespace(sb, aStart);
+        aStart = findNonSPLenient(sb, 0);
+        aEnd = findSPLenient(sb, aStart);
 
-        bStart = findNonWhitespace(sb, aEnd);
-        bEnd = findWhitespace(sb, bStart);
+        bStart = findNonSPLenient(sb, aEnd);
+        bEnd = findSPLenient(sb, bStart);
 
-        cStart = findNonWhitespace(sb, bEnd);
+        cStart = findNonSPLenient(sb, bEnd);
         cEnd = findEndOfString(sb);
 
         return new String[] {
@@ -733,23 +755,42 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder {
         int valueStart;
         int valueEnd;
 
-        nameStart = findNonWhitespace(sb, 0);
+        nameStart = findNonWhitespace(sb, 0, false);
         for (nameEnd = nameStart; nameEnd < length; nameEnd ++) {
-            char ch = sb.charAt(nameEnd);
-            if (ch == ':' || Character.isWhitespace(ch)) {
+            char ch = sb.charAtUnsafe(nameEnd);
+            // https://tools.ietf.org/html/rfc7230#section-3.2.4
+            //
+            // No whitespace is allowed between the header field-name and colon. In
+            // the past, differences in the handling of such whitespace have led to
+            // security vulnerabilities in request routing and response handling. A
+            // server MUST reject any received request message that contains
+            // whitespace between a header field-name and colon with a response code
+            // of 400 (Bad Request). A proxy MUST remove any such whitespace from a
+            // response message before forwarding the message downstream.
+            if (ch == ':' ||
+                    // In case of decoding a request we will just continue processing and header validation
+                    // is done in the DefaultHttpHeaders implementation.
+                    //
+                    // In the case of decoding a response we will "skip" the whitespace.
+                    (!isDecodingRequest() && isOWS(ch))) {
                 break;
             }
         }
 
+        if (nameEnd == length) {
+            // There was no colon present at all.
+            throw new IllegalArgumentException("No colon found");
+        }
+
         for (colonEnd = nameEnd; colonEnd < length; colonEnd ++) {
-            if (sb.charAt(colonEnd) == ':') {
+            if (sb.charAtUnsafe(colonEnd) == ':') {
                 colonEnd ++;
                 break;
             }
         }
 
         name = sb.subStringUnsafe(nameStart, nameEnd);
-        valueStart = findNonWhitespace(sb, colonEnd);
+        valueStart = findNonWhitespace(sb, colonEnd, true);
         if (valueStart == length) {
             value = EMPTY_VALUE;
         } else {
@@ -758,19 +799,45 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder {
         }
     }
 
-    private static int findNonWhitespace(AppendableCharSequence sb, int offset) {
+    private static int findNonSPLenient(AppendableCharSequence sb, int offset) {
         for (int result = offset; result < sb.length(); ++result) {
-            if (!Character.isWhitespace(sb.charAtUnsafe(result))) {
+            char c = sb.charAtUnsafe(result);
+            // See https://tools.ietf.org/html/rfc7230#section-3.5
+            if (isSPLenient(c)) {
+                continue;
+            }
+            if (Character.isWhitespace(c)) {
+                // Any other whitespace delimiter is invalid
+                throw new IllegalArgumentException("Invalid separator");
+            }
+            return result;
+        }
+        return sb.length();
+    }
+
+    private static int findSPLenient(AppendableCharSequence sb, int offset) {
+        for (int result = offset; result < sb.length(); ++result) {
+            if (isSPLenient(sb.charAtUnsafe(result))) {
                 return result;
             }
         }
         return sb.length();
     }
 
-    private static int findWhitespace(AppendableCharSequence sb, int offset) {
+    private static boolean isSPLenient(char c) {
+        // See https://tools.ietf.org/html/rfc7230#section-3.5
+        return c == ' ' || c == (char) 0x09 || c == (char) 0x0B || c == (char) 0x0C || c == (char) 0x0D;
+    }
+
+    private static int findNonWhitespace(AppendableCharSequence sb, int offset, boolean validateOWS) {
         for (int result = offset; result < sb.length(); ++result) {
-            if (Character.isWhitespace(sb.charAtUnsafe(result))) {
+            char c = sb.charAtUnsafe(result);
+            if (!Character.isWhitespace(c)) {
                 return result;
+            } else if (validateOWS && !isOWS(c)) {
+                // Only OWS is supported for whitespace
+                throw new IllegalArgumentException("Invalid separator, only a single space or horizontal tab allowed," +
+                        " but received a '" + c + "'");
             }
         }
         return sb.length();
@@ -785,6 +852,10 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder {
         return 0;
     }
 
+    private static boolean isOWS(char ch) {
+        return ch == ' ' || ch == (char) 0x09;
+    }
+
     private static class HeaderParser implements ByteProcessor {
         private final AppendableCharSequence seq;
         private final int maxLength;
@@ -814,13 +885,23 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder {
         @Override
         public boolean process(byte value) throws Exception {
             char nextByte = (char) (value & 0xFF);
-            if (nextByte == HttpConstants.CR) {
-                return true;
-            }
             if (nextByte == HttpConstants.LF) {
+                int len = seq.length();
+                // Drop CR if we had a CRLF pair
+                if (len >= 1 && seq.charAtUnsafe(len - 1) == HttpConstants.CR) {
+                    -- size;
+                    seq.setLength(len - 1);
+                }
                 return false;
             }
 
+            increaseCount();
+
+            seq.append(nextByte);
+            return true;
+        }
+
+        protected final void increaseCount() {
             if (++ size > maxLength) {
                 // TODO: Respond with Bad Request and discard the traffic
                 //    or close the connection.
@@ -828,9 +909,6 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder {
                 //       If decoding a response, just throw an exception.
                 throw newException(maxLength);
             }
-
-            seq.append(nextByte);
-            return true;
         }
 
         protected TooLongFrameException newException(int maxLength) {
@@ -838,7 +916,7 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder {
         }
     }
 
-    private static final class LineParser extends HeaderParser {
+    private final class LineParser extends HeaderParser {
 
         LineParser(AppendableCharSequence seq, int maxLength) {
             super(seq, maxLength);
@@ -850,6 +928,19 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder {
             return super.parse(buffer);
         }
 
+        @Override
+        public boolean process(byte value) throws Exception {
+            if (currentState == State.SKIP_CONTROL_CHARS) {
+                char c = (char) (value & 0xFF);
+                if (Character.isISOControl(c) || Character.isWhitespace(c)) {
+                    increaseCount();
+                    return true;
+                }
+                currentState = State.READ_INITIAL;
+            }
+            return super.process(value);
+        }
+
         @Override
         protected TooLongFrameException newException(int maxLength) {
             return new TooLongFrameException("An HTTP line is larger than " + maxLength + " bytes.");
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpResponseStatus.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpResponseStatus.java
index b7e2c10..941ef49 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpResponseStatus.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpResponseStatus.java
@@ -19,9 +19,11 @@ import io.netty.buffer.ByteBuf;
 import io.netty.buffer.ByteBufUtil;
 import io.netty.util.AsciiString;
 import io.netty.util.CharsetUtil;
+import io.netty.util.internal.ObjectUtil;
 
 import static io.netty.handler.codec.http.HttpConstants.SP;
 import static io.netty.util.ByteProcessor.FIND_ASCII_SPACE;
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
 import static java.lang.Integer.parseInt;
 
 /**
@@ -223,7 +225,7 @@ public class HttpResponseStatus implements Comparable<HttpResponseStatus> {
     /**
      * 421 Misdirected Request
      *
-     * <a href="https://tools.ietf.org/html/draft-ietf-httpbis-http2-15#section-9.1.2">421 Status Code</a>
+     * @see <a href="https://tools.ietf.org/html/rfc7540#section-9.1.2">421 (Misdirected Request) Status Code</a>
      */
     public static final HttpResponseStatus MISDIRECTED_REQUEST = newStatus(421, "Misdirected Request");
 
@@ -538,14 +540,8 @@ public class HttpResponseStatus implements Comparable<HttpResponseStatus> {
     }
 
     private HttpResponseStatus(int code, String reasonPhrase, boolean bytes) {
-        if (code < 0) {
-            throw new IllegalArgumentException(
-                    "code: " + code + " (expected: 0+)");
-        }
-
-        if (reasonPhrase == null) {
-            throw new NullPointerException("reasonPhrase");
-        }
+        checkPositiveOrZero(code, "code");
+        ObjectUtil.checkNotNull(reasonPhrase, "reasonPhrase");
 
         for (int i = 0; i < reasonPhrase.length(); i ++) {
             char c = reasonPhrase.charAt(i);
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpServerCodec.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpServerCodec.java
index 4e8d613..6e36128 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpServerCodec.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpServerCodec.java
@@ -81,16 +81,18 @@ public final class HttpServerCodec extends CombinedChannelDuplexHandler<HttpRequ
     }
 
     private final class HttpServerRequestDecoder extends HttpRequestDecoder {
-        public HttpServerRequestDecoder(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize) {
+
+        HttpServerRequestDecoder(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize) {
             super(maxInitialLineLength, maxHeaderSize, maxChunkSize);
         }
 
-        public HttpServerRequestDecoder(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize,
+        HttpServerRequestDecoder(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize,
                                         boolean validateHeaders) {
             super(maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders);
         }
 
-        public HttpServerRequestDecoder(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize,
+        HttpServerRequestDecoder(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize,
+
                                         boolean validateHeaders, int initialBufferSize) {
             super(maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders, initialBufferSize);
         }
@@ -115,7 +117,8 @@ public final class HttpServerCodec extends CombinedChannelDuplexHandler<HttpRequ
 
         @Override
         protected void sanitizeHeadersBeforeEncode(HttpResponse msg, boolean isAlwaysEmpty) {
-            if (!isAlwaysEmpty && method == HttpMethod.CONNECT && msg.status().codeClass() == HttpStatusClass.SUCCESS) {
+            if (!isAlwaysEmpty && HttpMethod.CONNECT.equals(method)
+                    && msg.status().codeClass() == HttpStatusClass.SUCCESS) {
                 // Stripping Transfer-Encoding:
                 // See https://tools.ietf.org/html/rfc7230#section-3.3.1
                 msg.headers().remove(HttpHeaderNames.TRANSFER_ENCODING);
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpServerUpgradeHandler.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpServerUpgradeHandler.java
index f1f3efc..2b54b0e 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpServerUpgradeHandler.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpServerUpgradeHandler.java
@@ -14,9 +14,6 @@
  */
 package io.netty.handler.codec.http;
 
-import static io.netty.util.AsciiString.containsContentEqualsIgnoreCase;
-import static io.netty.util.AsciiString.containsAllContentEqualsIgnoreCase;
-
 import io.netty.buffer.Unpooled;
 import io.netty.channel.ChannelFuture;
 import io.netty.channel.ChannelFutureListener;
@@ -30,7 +27,10 @@ import java.util.List;
 
 import static io.netty.handler.codec.http.HttpResponseStatus.SWITCHING_PROTOCOLS;
 import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
+import static io.netty.util.AsciiString.containsAllContentEqualsIgnoreCase;
+import static io.netty.util.AsciiString.containsContentEqualsIgnoreCase;
 import static io.netty.util.internal.ObjectUtil.checkNotNull;
+import static io.netty.util.internal.StringUtil.COMMA;
 
 /**
  * A server-side handler that receives HTTP requests and optionally performs a protocol switch if
@@ -284,16 +284,23 @@ public class HttpServerUpgradeHandler extends HttpObjectAggregator {
         }
 
         // Make sure the CONNECTION header is present.
-        CharSequence connectionHeader = request.headers().get(HttpHeaderNames.CONNECTION);
-        if (connectionHeader == null) {
+        List<String> connectionHeaderValues = request.headers().getAll(HttpHeaderNames.CONNECTION);
+
+        if (connectionHeaderValues == null) {
             return false;
         }
 
+        final StringBuilder concatenatedConnectionValue = new StringBuilder(connectionHeaderValues.size() * 10);
+        for (CharSequence connectionHeaderValue : connectionHeaderValues) {
+            concatenatedConnectionValue.append(connectionHeaderValue).append(COMMA);
+        }
+        concatenatedConnectionValue.setLength(concatenatedConnectionValue.length() - 1);
+
         // Make sure the CONNECTION header contains UPGRADE as well as all protocol-specific headers.
         Collection<CharSequence> requiredHeaders = upgradeCodec.requiredUpgradeHeaders();
-        List<CharSequence> values = splitHeader(connectionHeader);
+        List<CharSequence> values = splitHeader(concatenatedConnectionValue);
         if (!containsContentEqualsIgnoreCase(values, HttpHeaderNames.UPGRADE) ||
-            !containsAllContentEqualsIgnoreCase(values, requiredHeaders)) {
+                !containsAllContentEqualsIgnoreCase(values, requiredHeaders)) {
             return false;
         }
 
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpUtil.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpUtil.java
index 94af790..31fd14d 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpUtil.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpUtil.java
@@ -18,6 +18,7 @@ package io.netty.handler.codec.http;
 import java.net.InetSocketAddress;
 import java.net.URI;
 import java.nio.charset.Charset;
+import java.nio.charset.IllegalCharsetNameException;
 import java.nio.charset.UnsupportedCharsetException;
 import java.util.ArrayList;
 import java.util.Iterator;
@@ -26,6 +27,7 @@ import java.util.List;
 import io.netty.util.AsciiString;
 import io.netty.util.CharsetUtil;
 import io.netty.util.NetUtil;
+import io.netty.util.internal.ObjectUtil;
 
 /**
  * Utility methods useful in the HTTP context.
@@ -65,16 +67,9 @@ public final class HttpUtil {
      * {@link HttpVersion#isKeepAliveDefault()}.
      */
     public static boolean isKeepAlive(HttpMessage message) {
-        CharSequence connection = message.headers().get(HttpHeaderNames.CONNECTION);
-        if (HttpHeaderValues.CLOSE.contentEqualsIgnoreCase(connection)) {
-            return false;
-        }
-
-        if (message.protocolVersion().isKeepAliveDefault()) {
-            return !HttpHeaderValues.CLOSE.contentEqualsIgnoreCase(connection);
-        } else {
-            return HttpHeaderValues.KEEP_ALIVE.contentEqualsIgnoreCase(connection);
-        }
+        return !message.headers().containsValue(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE, true) &&
+               (message.protocolVersion().isKeepAliveDefault() ||
+                message.headers().containsValue(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE, true));
     }
 
     /**
@@ -251,13 +246,9 @@ public final class HttpUtil {
      * present
      */
     public static boolean is100ContinueExpected(HttpMessage message) {
-        if (!isExpectHeaderValid(message)) {
-            return false;
-        }
-
-        final String expectValue = message.headers().get(HttpHeaderNames.EXPECT);
-        // unquoted tokens in the expect header are case-insensitive, thus 100-continue is case insensitive
-        return HttpHeaderValues.CONTINUE.toString().equalsIgnoreCase(expectValue);
+        return isExpectHeaderValid(message)
+          // unquoted tokens in the expect header are case-insensitive, thus 100-continue is case insensitive
+          && message.headers().contains(HttpHeaderNames.EXPECT, HttpHeaderValues.CONTINUE, true);
     }
 
     /**
@@ -402,15 +393,14 @@ public final class HttpUtil {
             if (charsetCharSequence != null) {
                 try {
                     return Charset.forName(charsetCharSequence.toString());
+                } catch (IllegalCharsetNameException ignored) {
+                    // just return the default charset
                 } catch (UnsupportedCharsetException ignored) {
-                    return defaultCharset;
+                    // just return the default charset
                 }
-            } else {
-                return defaultCharset;
             }
-        } else {
-            return defaultCharset;
         }
+        return defaultCharset;
     }
 
     /**
@@ -459,9 +449,7 @@ public final class HttpUtil {
      * @throws NullPointerException in case if {@code contentTypeValue == null}
      */
     public static CharSequence getCharsetAsSequence(CharSequence contentTypeValue) {
-        if (contentTypeValue == null) {
-            throw new NullPointerException("contentTypeValue");
-        }
+        ObjectUtil.checkNotNull(contentTypeValue, "contentTypeValue");
 
         int indexOfCharset = AsciiString.indexOfIgnoreCaseAscii(contentTypeValue, CHARSET_EQUALS, 0);
         if (indexOfCharset == AsciiString.INDEX_NOT_FOUND) {
@@ -515,9 +503,7 @@ public final class HttpUtil {
      * @throws NullPointerException in case if {@code contentTypeValue == null}
      */
     public static CharSequence getMimeType(CharSequence contentTypeValue) {
-        if (contentTypeValue == null) {
-            throw new NullPointerException("contentTypeValue");
-        }
+        ObjectUtil.checkNotNull(contentTypeValue, "contentTypeValue");
 
         int indexOfSemicolon = AsciiString.indexOfIgnoreCaseAscii(contentTypeValue, SEMICOLON, 0);
         if (indexOfSemicolon != AsciiString.INDEX_NOT_FOUND) {
@@ -529,7 +515,7 @@ public final class HttpUtil {
 
     /**
      * Formats the host string of an address so it can be used for computing an HTTP component
-     * such as an URL or a Host header
+     * such as a URL or a Host header
      *
      * @param addr the address
      * @return the formatted String
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpVersion.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpVersion.java
index a643f42..dbf1bf6 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpVersion.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpVersion.java
@@ -15,8 +15,11 @@
  */
 package io.netty.handler.codec.http;
 
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
+
 import io.netty.buffer.ByteBuf;
 import io.netty.util.CharsetUtil;
+import io.netty.util.internal.ObjectUtil;
 
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -53,9 +56,7 @@ public class HttpVersion implements Comparable<HttpVersion> {
      * returned.
      */
     public static HttpVersion valueOf(String text) {
-        if (text == null) {
-            throw new NullPointerException("text");
-        }
+        ObjectUtil.checkNotNull(text, "text");
 
         text = text.trim();
 
@@ -107,9 +108,7 @@ public class HttpVersion implements Comparable<HttpVersion> {
      *        the {@code "Connection"} header is set to {@code "close"} explicitly.
      */
     public HttpVersion(String text, boolean keepAliveDefault) {
-        if (text == null) {
-            throw new NullPointerException("text");
-        }
+        ObjectUtil.checkNotNull(text, "text");
 
         text = text.trim().toUpperCase();
         if (text.isEmpty()) {
@@ -149,9 +148,7 @@ public class HttpVersion implements Comparable<HttpVersion> {
     private HttpVersion(
             String protocolName, int majorVersion, int minorVersion,
             boolean keepAliveDefault, boolean bytes) {
-        if (protocolName == null) {
-            throw new NullPointerException("protocolName");
-        }
+        ObjectUtil.checkNotNull(protocolName, "protocolName");
 
         protocolName = protocolName.trim().toUpperCase();
         if (protocolName.isEmpty()) {
@@ -165,12 +162,8 @@ public class HttpVersion implements Comparable<HttpVersion> {
             }
         }
 
-        if (majorVersion < 0) {
-            throw new IllegalArgumentException("negative majorVersion");
-        }
-        if (minorVersion < 0) {
-            throw new IllegalArgumentException("negative minorVersion");
-        }
+        checkPositiveOrZero(majorVersion, "majorVersion");
+        checkPositiveOrZero(minorVersion, "minorVersion");
 
         this.protocolName = protocolName;
         this.majorVersion = majorVersion;
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/QueryStringDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/QueryStringDecoder.java
index 7c631f3..417074c 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/QueryStringDecoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/QueryStringDecoder.java
@@ -16,23 +16,22 @@
 package io.netty.handler.codec.http;
 
 import io.netty.util.CharsetUtil;
+import io.netty.util.internal.PlatformDependent;
 
 import java.net.URI;
 import java.net.URLDecoder;
-import java.nio.ByteBuffer;
-import java.nio.CharBuffer;
-import java.nio.charset.CharacterCodingException;
 import java.nio.charset.Charset;
-import java.nio.charset.CharsetDecoder;
-import java.nio.charset.CoderResult;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
-import static io.netty.util.internal.ObjectUtil.*;
-import static io.netty.util.internal.StringUtil.*;
+import static io.netty.util.internal.ObjectUtil.checkNotNull;
+import static io.netty.util.internal.ObjectUtil.checkPositive;
+import static io.netty.util.internal.StringUtil.EMPTY_STRING;
+import static io.netty.util.internal.StringUtil.SPACE;
+import static io.netty.util.internal.StringUtil.decodeHexByte;
 
 /**
  * Splits an HTTP query string into a path string and key-value parameter pairs.
@@ -54,7 +53,7 @@ import static io.netty.util.internal.StringUtil.*;
  *
  * <h3>HashDOS vulnerability fix</h3>
  *
- * As a workaround to the <a href="http://netty.io/s/hashdos">HashDOS</a> vulnerability, the decoder
+ * As a workaround to the <a href="https://netty.io/s/hashdos">HashDOS</a> vulnerability, the decoder
  * limits the maximum number of decoded key-value parameter pairs, up to {@literal 1024} by
  * default, and you can configure it when you construct the decoder by passing an additional
  * integer parameter.
@@ -68,6 +67,7 @@ public class QueryStringDecoder {
     private final Charset charset;
     private final String uri;
     private final int maxParams;
+    private final boolean semicolonIsNormalChar;
     private int pathEndIdx;
     private String path;
     private Map<String, List<String>> params;
@@ -109,9 +109,19 @@ public class QueryStringDecoder {
      * specified charset.
      */
     public QueryStringDecoder(String uri, Charset charset, boolean hasPath, int maxParams) {
+        this(uri, charset, hasPath, maxParams, false);
+    }
+
+    /**
+     * Creates a new decoder that decodes the specified URI encoded in the
+     * specified charset.
+     */
+    public QueryStringDecoder(String uri, Charset charset, boolean hasPath,
+                              int maxParams, boolean semicolonIsNormalChar) {
         this.uri = checkNotNull(uri, "uri");
         this.charset = checkNotNull(charset, "charset");
         this.maxParams = checkPositive(maxParams, "maxParams");
+        this.semicolonIsNormalChar = semicolonIsNormalChar;
 
         // `-1` means that path end index will be initialized lazily
         pathEndIdx = hasPath ? -1 : 0;
@@ -138,6 +148,14 @@ public class QueryStringDecoder {
      * specified charset.
      */
     public QueryStringDecoder(URI uri, Charset charset, int maxParams) {
+        this(uri, charset, maxParams, false);
+    }
+
+    /**
+     * Creates a new decoder that decodes the specified URI encoded in the
+     * specified charset.
+     */
+    public QueryStringDecoder(URI uri, Charset charset, int maxParams, boolean semicolonIsNormalChar) {
         String rawPath = uri.getRawPath();
         if (rawPath == null) {
             rawPath = EMPTY_STRING;
@@ -147,6 +165,7 @@ public class QueryStringDecoder {
         this.uri = rawQuery == null? rawPath : rawPath + '?' + rawQuery;
         this.charset = checkNotNull(charset, "charset");
         this.maxParams = checkPositive(maxParams, "maxParams");
+        this.semicolonIsNormalChar = semicolonIsNormalChar;
         pathEndIdx = rawPath.length();
     }
 
@@ -177,7 +196,7 @@ public class QueryStringDecoder {
      */
     public Map<String, List<String>> parameters() {
         if (params == null) {
-            params = decodeParams(uri, pathEndIdx(), charset, maxParams);
+            params = decodeParams(uri, pathEndIdx(), charset, maxParams, semicolonIsNormalChar);
         }
         return params;
     }
@@ -204,7 +223,8 @@ public class QueryStringDecoder {
         return pathEndIdx;
     }
 
-    private static Map<String, List<String>> decodeParams(String s, int from, Charset charset, int paramsLimit) {
+    private static Map<String, List<String>> decodeParams(String s, int from, Charset charset, int paramsLimit,
+                                                          boolean semicolonIsNormalChar) {
         int len = s.length();
         if (from >= len) {
             return Collections.emptyMap();
@@ -226,8 +246,12 @@ public class QueryStringDecoder {
                     valueStart = i + 1;
                 }
                 break;
-            case '&':
             case ';':
+                if (semicolonIsNormalChar) {
+                    continue;
+                }
+                // fall-through
+            case '&':
                 if (addParam(s, nameStart, valueStart, i, params, charset)) {
                     paramsLimit--;
                     if (paramsLimit == 0) {
@@ -266,7 +290,7 @@ public class QueryStringDecoder {
     }
 
     /**
-     * Decodes a bit of an URL encoded by a browser.
+     * Decodes a bit of a URL encoded by a browser.
      * <p>
      * This is equivalent to calling {@link #decodeComponent(String, Charset)}
      * with the UTF-8 charset (recommended to comply with RFC 3986, Section 2).
@@ -281,7 +305,7 @@ public class QueryStringDecoder {
     }
 
     /**
-     * Decodes a bit of an URL encoded by a browser.
+     * Decodes a bit of a URL encoded by a browser.
      * <p>
      * The string is expected to be encoded as per RFC 3986, Section 2.
      * This is the encoding used by JavaScript functions {@code encodeURI}
@@ -326,12 +350,10 @@ public class QueryStringDecoder {
             return s.substring(from, toExcluded);
         }
 
-        CharsetDecoder decoder = CharsetUtil.decoder(charset);
-
         // Each encoded byte takes 3 characters (e.g. "%20")
         int decodedCapacity = (toExcluded - firstEscaped) / 3;
-        ByteBuffer byteBuf = ByteBuffer.allocate(decodedCapacity);
-        CharBuffer charBuf = CharBuffer.allocate(decodedCapacity);
+        byte[] buf = PlatformDependent.allocateUninitializedArray(decodedCapacity);
+        int bufIdx;
 
         StringBuilder strBuf = new StringBuilder(len);
         strBuf.append(s, from, firstEscaped);
@@ -343,31 +365,17 @@ public class QueryStringDecoder {
                 continue;
             }
 
-            byteBuf.clear();
+            bufIdx = 0;
             do {
                 if (i + 3 > toExcluded) {
                     throw new IllegalArgumentException("unterminated escape sequence at index " + i + " of: " + s);
                 }
-                byteBuf.put(decodeHexByte(s, i + 1));
+                buf[bufIdx++] = decodeHexByte(s, i + 1);
                 i += 3;
             } while (i < toExcluded && s.charAt(i) == '%');
             i--;
 
-            byteBuf.flip();
-            charBuf.clear();
-            CoderResult result = decoder.reset().decode(byteBuf, charBuf, true);
-            try {
-                if (!result.isUnderflow()) {
-                    result.throwException();
-                }
-                result = decoder.flush(charBuf);
-                if (!result.isUnderflow()) {
-                    result.throwException();
-                }
-            } catch (CharacterCodingException ex) {
-                throw new IllegalStateException(ex);
-            }
-            strBuf.append(charBuf.flip());
+            strBuf.append(new String(buf, 0, bufIdx, charset));
         }
         return strBuf.toString();
     }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/QueryStringEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/QueryStringEncoder.java
index cb1de9f..b624016 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/QueryStringEncoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/QueryStringEncoder.java
@@ -15,17 +15,18 @@
  */
 package io.netty.handler.codec.http;
 
+import io.netty.buffer.ByteBufUtil;
+import io.netty.util.CharsetUtil;
 import io.netty.util.internal.ObjectUtil;
+import io.netty.util.internal.StringUtil;
 
-import java.io.UnsupportedEncodingException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URLEncoder;
 import java.nio.charset.Charset;
-import java.nio.charset.UnsupportedCharsetException;
 
 /**
- * Creates an URL-encoded URI from a path string and key-value parameter pairs.
+ * Creates a URL-encoded URI from a path string and key-value parameter pairs.
  * This encoder is for one time use only.  Create a new instance for each URI.
  *
  * <pre>
@@ -33,13 +34,16 @@ import java.nio.charset.UnsupportedCharsetException;
  * encoder.addParam("recipient", "world");
  * assert encoder.toString().equals("/hello?recipient=world");
  * </pre>
+ *
  * @see QueryStringDecoder
  */
 public class QueryStringEncoder {
 
-    private final String charsetName;
+    private final Charset charset;
     private final StringBuilder uriBuilder;
     private boolean hasParams;
+    private static final byte WRITE_UTF_UNKNOWN = (byte) '?';
+    private static final char[] CHAR_MAP = "0123456789ABCDEF".toCharArray();
 
     /**
      * Creates a new encoder that encodes a URI that starts with the specified
@@ -54,8 +58,9 @@ public class QueryStringEncoder {
      * path string in the specified charset.
      */
     public QueryStringEncoder(String uri, Charset charset) {
+        ObjectUtil.checkNotNull(charset, "charset");
         uriBuilder = new StringBuilder(uri);
-        charsetName = charset.name();
+        this.charset = CharsetUtil.UTF_8.equals(charset) ? null : charset;
     }
 
     /**
@@ -69,10 +74,19 @@ public class QueryStringEncoder {
             uriBuilder.append('?');
             hasParams = true;
         }
-        appendComponent(name, charsetName, uriBuilder);
+
+        encodeComponent(name);
         if (value != null) {
             uriBuilder.append('=');
-            appendComponent(value, charsetName, uriBuilder);
+            encodeComponent(value);
+        }
+    }
+
+    private void encodeComponent(CharSequence s) {
+        if (charset == null) {
+            encodeUtf8Component(s);
+        } else {
+            encodeNonUtf8Component(s);
         }
     }
 
@@ -95,28 +109,142 @@ public class QueryStringEncoder {
         return uriBuilder.toString();
     }
 
-    private static void appendComponent(String s, String charset, StringBuilder sb) {
-        try {
-            s = URLEncoder.encode(s, charset);
-        } catch (UnsupportedEncodingException ignored) {
-            throw new UnsupportedCharsetException(charset);
+    /**
+     * Encode the String as per RFC 3986, Section 2.
+     * <p>
+     * There is a little different between the JDK's encode method : {@link URLEncoder#encode(String, String)}.
+     * The JDK's encoder encode the space to {@code +} and this method directly encode the blank to {@code %20}
+     * beyond that , this method reuse the {@link #uriBuilder} in this class rather then create a new one,
+     * thus generates less garbage for the GC.
+     *
+     * @param s The String to encode
+     */
+    private void encodeNonUtf8Component(CharSequence s) {
+        //Don't allocate memory until needed
+        char[] buf = null;
+
+        for (int i = 0, len = s.length(); i < len;) {
+            char c = s.charAt(i);
+            if (dontNeedEncoding(c)) {
+                uriBuilder.append(c);
+                i++;
+            } else {
+                int index = 0;
+                if (buf == null) {
+                    buf = new char[s.length() - i];
+                }
+
+                do {
+                    buf[index] = c;
+                    index++;
+                    i++;
+                } while (i < s.length() && !dontNeedEncoding(c = s.charAt(i)));
+
+                byte[] bytes = new String(buf, 0, index).getBytes(charset);
+
+                for (byte b : bytes) {
+                    appendEncoded(b);
+                }
+            }
         }
-        // replace all '+' with "%20"
-        int idx = s.indexOf('+');
-        if (idx == -1) {
-            sb.append(s);
-            return;
+    }
+
+    /**
+     * @see ByteBufUtil#writeUtf8(io.netty.buffer.ByteBuf, CharSequence, int, int)
+     */
+    private void encodeUtf8Component(CharSequence s) {
+        for (int i = 0, len = s.length(); i < len; i++) {
+            char c = s.charAt(i);
+            if (!dontNeedEncoding(c)) {
+                encodeUtf8Component(s, i, len);
+                return;
+            }
         }
-        sb.append(s, 0, idx).append("%20");
-        int size = s.length();
-        idx++;
-        for (; idx < size; idx++) {
-            char c = s.charAt(idx);
-            if (c != '+') {
-                sb.append(c);
+        uriBuilder.append(s);
+    }
+
+    private void encodeUtf8Component(CharSequence s, int encodingStart, int len) {
+        if (encodingStart > 0) {
+            // Append non-encoded characters directly first.
+            uriBuilder.append(s, 0, encodingStart);
+        }
+        encodeUtf8ComponentSlow(s, encodingStart, len);
+    }
+
+    private void encodeUtf8ComponentSlow(CharSequence s, int start, int len) {
+        for (int i = start; i < len; i++) {
+            char c = s.charAt(i);
+            if (c < 0x80) {
+                if (dontNeedEncoding(c)) {
+                    uriBuilder.append(c);
+                } else {
+                    appendEncoded(c);
+                }
+            } else if (c < 0x800) {
+                appendEncoded(0xc0 | (c >> 6));
+                appendEncoded(0x80 | (c & 0x3f));
+            } else if (StringUtil.isSurrogate(c)) {
+                if (!Character.isHighSurrogate(c)) {
+                    appendEncoded(WRITE_UTF_UNKNOWN);
+                    continue;
+                }
+                // Surrogate Pair consumes 2 characters.
+                if (++i == s.length()) {
+                    appendEncoded(WRITE_UTF_UNKNOWN);
+                    break;
+                }
+                // Extra method to allow inlining the rest of writeUtf8 which is the most likely code path.
+                writeUtf8Surrogate(c, s.charAt(i));
             } else {
-                sb.append("%20");
+                appendEncoded(0xe0 | (c >> 12));
+                appendEncoded(0x80 | ((c >> 6) & 0x3f));
+                appendEncoded(0x80 | (c & 0x3f));
             }
         }
     }
+
+    private void writeUtf8Surrogate(char c, char c2) {
+        if (!Character.isLowSurrogate(c2)) {
+            appendEncoded(WRITE_UTF_UNKNOWN);
+            appendEncoded(Character.isHighSurrogate(c2) ? WRITE_UTF_UNKNOWN : c2);
+            return;
+        }
+        int codePoint = Character.toCodePoint(c, c2);
+        // See http://www.unicode.org/versions/Unicode7.0.0/ch03.pdf#G2630.
+        appendEncoded(0xf0 | (codePoint >> 18));
+        appendEncoded(0x80 | ((codePoint >> 12) & 0x3f));
+        appendEncoded(0x80 | ((codePoint >> 6) & 0x3f));
+        appendEncoded(0x80 | (codePoint & 0x3f));
+    }
+
+    private void appendEncoded(int b) {
+        uriBuilder.append('%').append(forDigit(b >> 4)).append(forDigit(b));
+    }
+
+    /**
+     * Convert the given digit to a upper hexadecimal char.
+     *
+     * @param digit the number to convert to a character.
+     * @return the {@code char} representation of the specified digit
+     * in hexadecimal.
+     */
+    private static char forDigit(int digit) {
+        return CHAR_MAP[digit & 0xF];
+    }
+
+    /**
+     * Determines whether the given character is a unreserved character.
+     * <p>
+     * unreserved characters do not need to be encoded, and include uppercase and lowercase
+     * letters, decimal digits, hyphen, period, underscore, and tilde.
+     * <p>
+     * unreserved  = ALPHA / DIGIT / "-" / "_" / "." / "*"
+     *
+     * @param ch the char to be judged whether it need to be encode
+     * @return true or false
+     */
+    private static boolean dontNeedEncoding(char ch) {
+        return ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z' || ch >= '0' && ch <= '9'
+                || ch == '-' || ch == '_' || ch == '.' || ch == '*';
+    }
 }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ClientCookieDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ClientCookieDecoder.java
index 74cace7..01a35f3 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ClientCookieDecoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ClientCookieDecoder.java
@@ -16,6 +16,7 @@
 package io.netty.handler.codec.http.cookie;
 
 import io.netty.handler.codec.DateFormatter;
+import io.netty.handler.codec.http.cookie.CookieHeaderNames.SameSite;
 
 import java.util.Date;
 
@@ -154,6 +155,7 @@ public final class ClientCookieDecoder extends CookieDecoder {
         private int expiresEnd;
         private boolean secure;
         private boolean httpOnly;
+        private SameSite sameSite;
 
         CookieBuilder(DefaultCookie cookie, String header) {
             this.cookie = cookie;
@@ -180,6 +182,7 @@ public final class ClientCookieDecoder extends CookieDecoder {
             cookie.setMaxAge(mergeMaxAgeAndExpires());
             cookie.setSecure(secure);
             cookie.setHttpOnly(httpOnly);
+            cookie.setSameSite(sameSite);
             return cookie;
         }
 
@@ -206,7 +209,7 @@ public final class ClientCookieDecoder extends CookieDecoder {
             } else if (length == 7) {
                 parse7(keyStart, valueStart, valueEnd);
             } else if (length == 8) {
-                parse8(keyStart);
+                parse8(keyStart, valueStart, valueEnd);
             }
         }
 
@@ -241,9 +244,11 @@ public final class ClientCookieDecoder extends CookieDecoder {
             }
         }
 
-        private void parse8(int nameStart) {
+        private void parse8(int nameStart, int valueStart, int valueEnd) {
             if (header.regionMatches(true, nameStart, CookieHeaderNames.HTTPONLY, 0, 8)) {
                 httpOnly = true;
+            } else if (header.regionMatches(true, nameStart, CookieHeaderNames.SAMESITE, 0, 8)) {
+                sameSite = SameSite.of(computeValue(valueStart, valueEnd));
             }
         }
 
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ClientCookieEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ClientCookieEncoder.java
index 0a66aaa..ac9bd03 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ClientCookieEncoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ClientCookieEncoder.java
@@ -91,7 +91,8 @@ public final class ClientCookieEncoder extends CookieEncoder {
      * Sort cookies into decreasing order of path length, breaking ties by sorting into increasing chronological
      * order of creation time, as recommended by RFC 6265.
      */
-    private static final Comparator<Cookie> COOKIE_COMPARATOR = new Comparator<Cookie>() {
+    // package-private for testing only.
+    static final Comparator<Cookie> COOKIE_COMPARATOR = new Comparator<Cookie>() {
         @Override
         public int compare(Cookie c1, Cookie c2) {
             String path1 = c1.path();
@@ -103,13 +104,10 @@ public final class ClientCookieEncoder extends CookieEncoder {
             // limited use.
             int len1 = path1 == null ? Integer.MAX_VALUE : path1.length();
             int len2 = path2 == null ? Integer.MAX_VALUE : path2.length();
-            int diff = len2 - len1;
-            if (diff != 0) {
-                return diff;
-            }
-            // Rely on Java's sort stability to retain creation order in cases where
+
+            // Rely on Arrays.sort's stability to retain creation order in cases where
             // cookies have same path length.
-            return -1;
+            return len2 - len1;
         }
     };
 
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/CookieHeaderNames.java b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/CookieHeaderNames.java
index 6d2e7f5..fef0567 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/CookieHeaderNames.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/CookieHeaderNames.java
@@ -28,6 +28,35 @@ public final class CookieHeaderNames {
 
     public static final String HTTPONLY = "HTTPOnly";
 
+    public static final String SAMESITE = "SameSite";
+
+    /**
+     * Possible values for the SameSite attribute.
+     * See <a href="https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-05">changes to RFC6265bis</a>
+     */
+    public enum SameSite {
+        Lax,
+        Strict,
+        None;
+
+        /**
+         * Return the enum value corresponding to the passed in same-site-flag, using a case insensitive comparison.
+         *
+         * @param name value for the SameSite Attribute
+         * @return enum value for the provided name or null
+         */
+        static SameSite of(String name) {
+            if (name != null) {
+                for (SameSite each : SameSite.class.getEnumConstants()) {
+                    if (each.name().equalsIgnoreCase(name)) {
+                        return each;
+                    }
+                }
+            }
+            return null;
+        }
+    }
+
     private CookieHeaderNames() {
         // Unused.
     }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/CookieUtil.java b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/CookieUtil.java
index 1e9d9c8..2e818b9 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/CookieUtil.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/CookieUtil.java
@@ -97,24 +97,24 @@ final class CookieUtil {
 
     static void add(StringBuilder sb, String name, long val) {
         sb.append(name);
-        sb.append((char) HttpConstants.EQUALS);
+        sb.append('=');
         sb.append(val);
-        sb.append((char) HttpConstants.SEMICOLON);
-        sb.append((char) HttpConstants.SP);
+        sb.append(';');
+        sb.append(HttpConstants.SP_CHAR);
     }
 
     static void add(StringBuilder sb, String name, String val) {
         sb.append(name);
-        sb.append((char) HttpConstants.EQUALS);
+        sb.append('=');
         sb.append(val);
-        sb.append((char) HttpConstants.SEMICOLON);
-        sb.append((char) HttpConstants.SP);
+        sb.append(';');
+        sb.append(HttpConstants.SP_CHAR);
     }
 
     static void add(StringBuilder sb, String name) {
         sb.append(name);
-        sb.append((char) HttpConstants.SEMICOLON);
-        sb.append((char) HttpConstants.SP);
+        sb.append(';');
+        sb.append(HttpConstants.SP_CHAR);
     }
 
     static void addQuoted(StringBuilder sb, String name, String val) {
@@ -123,12 +123,12 @@ final class CookieUtil {
         }
 
         sb.append(name);
-        sb.append((char) HttpConstants.EQUALS);
-        sb.append((char) HttpConstants.DOUBLE_QUOTE);
+        sb.append('=');
+        sb.append('"');
         sb.append(val);
-        sb.append((char) HttpConstants.DOUBLE_QUOTE);
-        sb.append((char) HttpConstants.SEMICOLON);
-        sb.append((char) HttpConstants.SP);
+        sb.append('"');
+        sb.append(';');
+        sb.append(HttpConstants.SP_CHAR);
     }
 
     static int firstInvalidCookieNameOctet(CharSequence cs) {
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/DefaultCookie.java b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/DefaultCookie.java
index cbd54cd..137a65d 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/DefaultCookie.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/DefaultCookie.java
@@ -15,7 +15,10 @@
  */
 package io.netty.handler.codec.http.cookie;
 
-import static io.netty.handler.codec.http.cookie.CookieUtil.*;
+import io.netty.handler.codec.http.cookie.CookieHeaderNames.SameSite;
+
+import static io.netty.handler.codec.http.cookie.CookieUtil.stringBuilder;
+import static io.netty.handler.codec.http.cookie.CookieUtil.validateAttributeValue;
 import static io.netty.util.internal.ObjectUtil.checkNotNull;
 
 /**
@@ -31,6 +34,7 @@ public class DefaultCookie implements Cookie {
     private long maxAge = UNDEFINED_MAX_AGE;
     private boolean secure;
     private boolean httpOnly;
+    private SameSite sameSite;
 
     /**
      * Creates a new cookie with the specified name and value.
@@ -119,6 +123,26 @@ public class DefaultCookie implements Cookie {
         this.httpOnly = httpOnly;
     }
 
+    /**
+     * Checks to see if this {@link Cookie} can be sent along cross-site requests.
+     * For more information, please look
+     * <a href="https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-05">here</a>
+     * @return <b>same-site-flag</b> value
+     */
+    public SameSite sameSite() {
+        return sameSite;
+    }
+
+    /**
+     * Determines if this this {@link Cookie} can be sent along cross-site requests.
+     * For more information, please look
+     *  <a href="https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-05">here</a>
+     * @param sameSite <b>same-site-flag</b> value
+     */
+    public void setSameSite(SameSite sameSite) {
+        this.sameSite = sameSite;
+    }
+
     @Override
     public int hashCode() {
         return name().hashCode();
@@ -232,6 +256,9 @@ public class DefaultCookie implements Cookie {
         if (isHttpOnly()) {
             buf.append(", HTTPOnly");
         }
+        if (sameSite() != null) {
+            buf.append(", SameSite=").append(sameSite());
+        }
         return buf.toString();
     }
 }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ServerCookieDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ServerCookieDecoder.java
index cf5349b..f56fb91 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ServerCookieDecoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ServerCookieDecoder.java
@@ -17,7 +17,10 @@ package io.netty.handler.codec.http.cookie;
 
 import static io.netty.util.internal.ObjectUtil.checkNotNull;
 
+import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
+import java.util.List;
 import java.util.Set;
 import java.util.TreeSet;
 
@@ -56,20 +59,41 @@ public final class ServerCookieDecoder extends CookieDecoder {
         super(strict);
     }
 
+    /**
+     * Decodes the specified Set-Cookie HTTP header value into a {@link Cookie}.  Unlike {@link #decode(String)}, this
+     * includes all cookie values present, even if they have the same name.
+     *
+     * @return the decoded {@link Cookie}
+     */
+    public List<Cookie> decodeAll(String header) {
+        List<Cookie> cookies = new ArrayList<Cookie>();
+        decode(cookies, header);
+        return Collections.unmodifiableList(cookies);
+    }
+
     /**
      * Decodes the specified Set-Cookie HTTP header value into a {@link Cookie}.
      *
      * @return the decoded {@link Cookie}
      */
     public Set<Cookie> decode(String header) {
+        Set<Cookie> cookies = new TreeSet<Cookie>();
+        decode(cookies, header);
+        return cookies;
+    }
+
+    /**
+     * Decodes the specified Set-Cookie HTTP header value into a {@link Cookie}.
+     *
+     * @return the decoded {@link Cookie}
+     */
+    private void decode(Collection<? super Cookie> cookies, String header) {
         final int headerLen = checkNotNull(header, "header").length();
 
         if (headerLen == 0) {
-            return Collections.emptySet();
+            return;
         }
 
-        Set<Cookie> cookies = new TreeSet<Cookie>();
-
         int i = 0;
 
         boolean rfc2965Style = false;
@@ -149,7 +173,5 @@ public final class ServerCookieDecoder extends CookieDecoder {
                 cookies.add(cookie);
             }
         }
-
-        return cookies;
     }
 }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ServerCookieEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ServerCookieEncoder.java
index b707dc3..b0ee21a 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ServerCookieEncoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ServerCookieEncoder.java
@@ -15,12 +15,6 @@
  */
 package io.netty.handler.codec.http.cookie;
 
-import static io.netty.handler.codec.http.cookie.CookieUtil.add;
-import static io.netty.handler.codec.http.cookie.CookieUtil.addQuoted;
-import static io.netty.handler.codec.http.cookie.CookieUtil.stringBuilder;
-import static io.netty.handler.codec.http.cookie.CookieUtil.stripTrailingSeparator;
-import static io.netty.util.internal.ObjectUtil.checkNotNull;
-
 import io.netty.handler.codec.DateFormatter;
 import io.netty.handler.codec.http.HttpConstants;
 import io.netty.handler.codec.http.HttpResponse;
@@ -34,6 +28,12 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 
+import static io.netty.handler.codec.http.cookie.CookieUtil.add;
+import static io.netty.handler.codec.http.cookie.CookieUtil.addQuoted;
+import static io.netty.handler.codec.http.cookie.CookieUtil.stringBuilder;
+import static io.netty.handler.codec.http.cookie.CookieUtil.stripTrailingSeparator;
+import static io.netty.util.internal.ObjectUtil.checkNotNull;
+
 /**
  * A <a href="http://tools.ietf.org/html/rfc6265">RFC6265</a> compliant cookie encoder to be used server side,
  * so some fields are sent (Version is typically ignored).
@@ -105,10 +105,10 @@ public final class ServerCookieEncoder extends CookieEncoder {
             add(buf, CookieHeaderNames.MAX_AGE, cookie.maxAge());
             Date expires = new Date(cookie.maxAge() * 1000 + System.currentTimeMillis());
             buf.append(CookieHeaderNames.EXPIRES);
-            buf.append((char) HttpConstants.EQUALS);
+            buf.append('=');
             DateFormatter.append(expires, buf);
-            buf.append((char) HttpConstants.SEMICOLON);
-            buf.append((char) HttpConstants.SP);
+            buf.append(';');
+            buf.append(HttpConstants.SP_CHAR);
         }
 
         if (cookie.path() != null) {
@@ -124,6 +124,12 @@ public final class ServerCookieEncoder extends CookieEncoder {
         if (cookie.isHttpOnly()) {
             add(buf, CookieHeaderNames.HTTPONLY);
         }
+        if (cookie instanceof DefaultCookie) {
+            DefaultCookie c = (DefaultCookie) cookie;
+            if (c.sameSite() != null) {
+                add(buf, CookieHeaderNames.SAMESITE, c.sameSite().name());
+            }
+        }
 
         return stripTrailingSeparator(buf);
     }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cors/CorsConfig.java b/codec-http/src/main/java/io/netty/handler/codec/http/cors/CorsConfig.java
index 5ccae5d..e6036f7 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/cors/CorsConfig.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/cors/CorsConfig.java
@@ -219,7 +219,7 @@ public final class CorsConfig {
      *
      * CORS headers are set after a request is processed. This may not always be desired
      * and this setting will check that the Origin is valid and if it is not valid no
-     * further processing will take place, and a error will be returned to the calling client.
+     * further processing will take place, and an error will be returned to the calling client.
      *
      * @return {@code true} if a CORS request should short-circuit upon receiving an invalid Origin header.
      */
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cors/CorsConfigBuilder.java b/codec-http/src/main/java/io/netty/handler/codec/http/cors/CorsConfigBuilder.java
index c55eed1..c07369d 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/cors/CorsConfigBuilder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/cors/CorsConfigBuilder.java
@@ -341,7 +341,7 @@ public final class CorsConfigBuilder {
      *
      * CORS headers are set after a request is processed. This may not always be desired
      * and this setting will check that the Origin is valid and if it is not valid no
-     * further processing will take place, and a error will be returned to the calling client.
+     * further processing will take place, and an error will be returned to the calling client.
      *
      * @return {@link CorsConfigBuilder} to support method chaining.
      */
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cors/CorsHandler.java b/codec-http/src/main/java/io/netty/handler/codec/http/cors/CorsHandler.java
index 39f79a4..3176957 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/cors/CorsHandler.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/cors/CorsHandler.java
@@ -191,7 +191,7 @@ public class CorsHandler extends ChannelDuplexHandler {
 
     private static boolean isPreflightRequest(final HttpRequest request) {
         final HttpHeaders headers = request.headers();
-        return request.method().equals(OPTIONS) &&
+        return OPTIONS.equals(request.method()) &&
                 headers.contains(HttpHeaderNames.ORIGIN) &&
                 headers.contains(HttpHeaderNames.ACCESS_CONTROL_REQUEST_METHOD);
     }
@@ -228,7 +228,8 @@ public class CorsHandler extends ChannelDuplexHandler {
     }
 
     private static void forbidden(final ChannelHandlerContext ctx, final HttpRequest request) {
-        HttpResponse response = new DefaultFullHttpResponse(request.protocolVersion(), FORBIDDEN);
+        HttpResponse response = new DefaultFullHttpResponse(
+                request.protocolVersion(), FORBIDDEN, ctx.alloc().buffer(0));
         response.headers().set(HttpHeaderNames.CONTENT_LENGTH, HttpHeaderValues.ZERO);
         release(request);
         respond(ctx, request, response);
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractDiskHttpData.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractDiskHttpData.java
index 544bc7c..71357a1 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractDiskHttpData.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractDiskHttpData.java
@@ -18,14 +18,14 @@ package io.netty.handler.codec.http.multipart;
 import io.netty.buffer.ByteBuf;
 import io.netty.handler.codec.http.HttpConstants;
 import io.netty.util.internal.EmptyArrays;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.RandomAccessFile;
 import java.nio.ByteBuffer;
 import java.nio.channels.FileChannel;
 import java.nio.charset.Charset;
@@ -100,9 +100,7 @@ public abstract class AbstractDiskHttpData extends AbstractHttpData {
 
     @Override
     public void setContent(ByteBuf buffer) throws IOException {
-        if (buffer == null) {
-            throw new NullPointerException("buffer");
-        }
+        ObjectUtil.checkNotNull(buffer, "buffer");
         try {
             size = buffer.readableBytes();
             checkSize(size);
@@ -125,9 +123,10 @@ public abstract class AbstractDiskHttpData extends AbstractHttpData {
                 }
                 return;
             }
-            FileOutputStream outputStream = new FileOutputStream(file);
+            RandomAccessFile accessFile = new RandomAccessFile(file, "rw");
+            accessFile.setLength(0);
             try {
-                FileChannel localfileChannel = outputStream.getChannel();
+                FileChannel localfileChannel = accessFile.getChannel();
                 ByteBuffer byteBuffer = buffer.nioBuffer();
                 int written = 0;
                 while (written < size) {
@@ -136,7 +135,7 @@ public abstract class AbstractDiskHttpData extends AbstractHttpData {
                 buffer.readerIndex(buffer.readerIndex() + written);
                 localfileChannel.force(false);
             } finally {
-                outputStream.close();
+                accessFile.close();
             }
             setCompleted();
         } finally {
@@ -163,8 +162,8 @@ public abstract class AbstractDiskHttpData extends AbstractHttpData {
                     file = tempFile();
                 }
                 if (fileChannel == null) {
-                    FileOutputStream outputStream = new FileOutputStream(file);
-                    fileChannel = outputStream.getChannel();
+                    RandomAccessFile accessFile = new RandomAccessFile(file, "rw");
+                    fileChannel = accessFile.getChannel();
                 }
                 while (written < localsize) {
                     written += fileChannel.write(byteBuffer);
@@ -182,17 +181,15 @@ public abstract class AbstractDiskHttpData extends AbstractHttpData {
                 file = tempFile();
             }
             if (fileChannel == null) {
-                FileOutputStream outputStream = new FileOutputStream(file);
-                fileChannel = outputStream.getChannel();
+                RandomAccessFile accessFile = new RandomAccessFile(file, "rw");
+                fileChannel = accessFile.getChannel();
             }
             fileChannel.force(false);
             fileChannel.close();
             fileChannel = null;
             setCompleted();
         } else {
-            if (buffer == null) {
-                throw new NullPointerException("buffer");
-            }
+            ObjectUtil.checkNotNull(buffer, "buffer");
         }
     }
 
@@ -210,17 +207,16 @@ public abstract class AbstractDiskHttpData extends AbstractHttpData {
 
     @Override
     public void setContent(InputStream inputStream) throws IOException {
-        if (inputStream == null) {
-            throw new NullPointerException("inputStream");
-        }
+        ObjectUtil.checkNotNull(inputStream, "inputStream");
         if (file != null) {
             delete();
         }
         file = tempFile();
-        FileOutputStream outputStream = new FileOutputStream(file);
+        RandomAccessFile accessFile = new RandomAccessFile(file, "rw");
+        accessFile.setLength(0);
         int written = 0;
         try {
-            FileChannel localfileChannel = outputStream.getChannel();
+            FileChannel localfileChannel = accessFile.getChannel();
             byte[] bytes = new byte[4096 * 4];
             ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
             int read = inputStream.read(bytes);
@@ -232,7 +228,7 @@ public abstract class AbstractDiskHttpData extends AbstractHttpData {
             }
             localfileChannel.force(false);
         } finally {
-            outputStream.close();
+            accessFile.close();
         }
         size = written;
         if (definedSize > 0 && definedSize < size) {
@@ -290,8 +286,8 @@ public abstract class AbstractDiskHttpData extends AbstractHttpData {
             return EMPTY_BUFFER;
         }
         if (fileChannel == null) {
-            FileInputStream inputStream = new FileInputStream(file);
-            fileChannel = inputStream.getChannel();
+            RandomAccessFile accessFile = new RandomAccessFile(file, "r");
+            fileChannel = accessFile.getChannel();
         }
         int read = 0;
         ByteBuffer byteBuffer = ByteBuffer.allocate(length);
@@ -340,24 +336,22 @@ public abstract class AbstractDiskHttpData extends AbstractHttpData {
 
     @Override
     public boolean renameTo(File dest) throws IOException {
-        if (dest == null) {
-            throw new NullPointerException("dest");
-        }
+        ObjectUtil.checkNotNull(dest, "dest");
         if (file == null) {
             throw new IOException("No file defined so cannot be renamed");
         }
         if (!file.renameTo(dest)) {
             // must copy
             IOException exception = null;
-            FileInputStream inputStream = null;
-            FileOutputStream outputStream = null;
+            RandomAccessFile inputAccessFile = null;
+            RandomAccessFile outputAccessFile = null;
             long chunkSize = 8196;
             long position = 0;
             try {
-                inputStream = new FileInputStream(file);
-                outputStream = new FileOutputStream(dest);
-                FileChannel in = inputStream.getChannel();
-                FileChannel out = outputStream.getChannel();
+                inputAccessFile = new RandomAccessFile(file, "r");
+                outputAccessFile = new RandomAccessFile(dest, "rw");
+                FileChannel in = inputAccessFile.getChannel();
+                FileChannel out = outputAccessFile.getChannel();
                 while (position < size) {
                     if (chunkSize < size - position) {
                         chunkSize = size - position;
@@ -367,9 +361,9 @@ public abstract class AbstractDiskHttpData extends AbstractHttpData {
             } catch (IOException e) {
                 exception = e;
             } finally {
-                if (inputStream != null) {
+                if (inputAccessFile != null) {
                     try {
-                        inputStream.close();
+                        inputAccessFile.close();
                     } catch (IOException e) {
                         if (exception == null) { // Choose to report the first exception
                             exception = e;
@@ -378,9 +372,9 @@ public abstract class AbstractDiskHttpData extends AbstractHttpData {
                         }
                     }
                 }
-                if (outputStream != null) {
+                if (outputAccessFile != null) {
                     try {
-                        outputStream.close();
+                        outputAccessFile.close();
                     } catch (IOException e) {
                         if (exception == null) { // Choose to report the first exception
                             exception = e;
@@ -422,17 +416,17 @@ public abstract class AbstractDiskHttpData extends AbstractHttpData {
             throw new IllegalArgumentException(
                     "File too big to be loaded in memory");
         }
-        FileInputStream inputStream = new FileInputStream(src);
+        RandomAccessFile accessFile = new RandomAccessFile(src, "r");
         byte[] array = new byte[(int) srcsize];
         try {
-            FileChannel fileChannel = inputStream.getChannel();
+            FileChannel fileChannel = accessFile.getChannel();
             ByteBuffer byteBuffer = ByteBuffer.wrap(array);
             int read = 0;
             while (read < srcsize) {
                 read += fileChannel.read(byteBuffer);
             }
         } finally {
-            inputStream.close();
+            accessFile.close();
         }
         return array;
     }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractHttpData.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractHttpData.java
index ff05753..03f0582 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractHttpData.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractHttpData.java
@@ -19,6 +19,7 @@ import io.netty.buffer.ByteBuf;
 import io.netty.channel.ChannelException;
 import io.netty.handler.codec.http.HttpConstants;
 import io.netty.util.AbstractReferenceCounted;
+import io.netty.util.internal.ObjectUtil;
 
 import java.io.IOException;
 import java.nio.charset.Charset;
@@ -40,9 +41,7 @@ public abstract class AbstractHttpData extends AbstractReferenceCounted implemen
     private long maxSize = DefaultHttpDataFactory.MAXSIZE;
 
     protected AbstractHttpData(String name, Charset charset, long size) {
-        if (name == null) {
-            throw new NullPointerException("name");
-        }
+        ObjectUtil.checkNotNull(name, "name");
 
         name = REPLACE_PATTERN.matcher(name).replaceAll(" ");
         name = STRIP_PATTERN.matcher(name).replaceAll("");
@@ -59,7 +58,9 @@ public abstract class AbstractHttpData extends AbstractReferenceCounted implemen
     }
 
     @Override
-    public long getMaxSize() { return maxSize; }
+    public long getMaxSize() {
+        return maxSize;
+    }
 
     @Override
     public void setMaxSize(long maxSize) {
@@ -94,10 +95,7 @@ public abstract class AbstractHttpData extends AbstractReferenceCounted implemen
 
     @Override
     public void setCharset(Charset charset) {
-        if (charset == null) {
-            throw new NullPointerException("charset");
-        }
-        this.charset = charset;
+        this.charset = ObjectUtil.checkNotNull(charset, "charset");
     }
 
     @Override
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractMemoryHttpData.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractMemoryHttpData.java
index 31aa9ce..a08cdad 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractMemoryHttpData.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/AbstractMemoryHttpData.java
@@ -18,12 +18,12 @@ package io.netty.handler.codec.http.multipart;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.CompositeByteBuf;
 import io.netty.handler.codec.http.HttpConstants;
+import io.netty.util.internal.ObjectUtil;
 
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.RandomAccessFile;
 import java.nio.ByteBuffer;
 import java.nio.channels.FileChannel;
 import java.nio.charset.Charset;
@@ -47,9 +47,7 @@ public abstract class AbstractMemoryHttpData extends AbstractHttpData {
 
     @Override
     public void setContent(ByteBuf buffer) throws IOException {
-        if (buffer == null) {
-            throw new NullPointerException("buffer");
-        }
+        ObjectUtil.checkNotNull(buffer, "buffer");
         long localsize = buffer.readableBytes();
         checkSize(localsize);
         if (definedSize > 0 && definedSize < localsize) {
@@ -66,9 +64,8 @@ public abstract class AbstractMemoryHttpData extends AbstractHttpData {
 
     @Override
     public void setContent(InputStream inputStream) throws IOException {
-        if (inputStream == null) {
-            throw new NullPointerException("inputStream");
-        }
+        ObjectUtil.checkNotNull(inputStream, "inputStream");
+
         ByteBuf buffer = buffer();
         byte[] bytes = new byte[4096 * 4];
         int read = inputStream.read(bytes);
@@ -115,25 +112,21 @@ public abstract class AbstractMemoryHttpData extends AbstractHttpData {
         if (last) {
             setCompleted();
         } else {
-            if (buffer == null) {
-                throw new NullPointerException("buffer");
-            }
+            ObjectUtil.checkNotNull(buffer, "buffer");
         }
     }
 
     @Override
     public void setContent(File file) throws IOException {
-        if (file == null) {
-            throw new NullPointerException("file");
-        }
+        ObjectUtil.checkNotNull(file, "file");
+
         long newsize = file.length();
         if (newsize > Integer.MAX_VALUE) {
-            throw new IllegalArgumentException(
-                    "File too big to be loaded in memory");
+            throw new IllegalArgumentException("File too big to be loaded in memory");
         }
         checkSize(newsize);
-        FileInputStream inputStream = new FileInputStream(file);
-        FileChannel fileChannel = inputStream.getChannel();
+        RandomAccessFile accessFile = new RandomAccessFile(file, "r");
+        FileChannel fileChannel = accessFile.getChannel();
         byte[] array = new byte[(int) newsize];
         ByteBuffer byteBuffer = ByteBuffer.wrap(array);
         int read = 0;
@@ -141,7 +134,7 @@ public abstract class AbstractMemoryHttpData extends AbstractHttpData {
             read += fileChannel.read(byteBuffer);
         }
         fileChannel.close();
-        inputStream.close();
+        accessFile.close();
         byteBuffer.flip();
         if (byteBuf != null) {
             byteBuf.release();
@@ -222,9 +215,7 @@ public abstract class AbstractMemoryHttpData extends AbstractHttpData {
 
     @Override
     public boolean renameTo(File dest) throws IOException {
-        if (dest == null) {
-            throw new NullPointerException("dest");
-        }
+        ObjectUtil.checkNotNull(dest, "dest");
         if (byteBuf == null) {
             // empty file
             if (!dest.createNewFile()) {
@@ -233,8 +224,8 @@ public abstract class AbstractMemoryHttpData extends AbstractHttpData {
             return true;
         }
         int length = byteBuf.readableBytes();
-        FileOutputStream outputStream = new FileOutputStream(dest);
-        FileChannel fileChannel = outputStream.getChannel();
+        RandomAccessFile accessFile = new RandomAccessFile(dest, "rw");
+        FileChannel fileChannel = accessFile.getChannel();
         int written = 0;
         if (byteBuf.nioBufferCount() == 1) {
             ByteBuffer byteBuffer = byteBuf.nioBuffer();
@@ -250,7 +241,7 @@ public abstract class AbstractMemoryHttpData extends AbstractHttpData {
 
         fileChannel.force(false);
         fileChannel.close();
-        outputStream.close();
+        accessFile.close();
         return written == length;
     }
 
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/DiskAttribute.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/DiskAttribute.java
index 7439188..7905273 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/DiskAttribute.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/DiskAttribute.java
@@ -18,6 +18,7 @@ package io.netty.handler.codec.http.multipart;
 import io.netty.buffer.ByteBuf;
 import io.netty.channel.ChannelException;
 import io.netty.handler.codec.http.HttpConstants;
+import io.netty.util.internal.ObjectUtil;
 
 import java.io.IOException;
 import java.nio.charset.Charset;
@@ -77,9 +78,7 @@ public class DiskAttribute extends AbstractDiskHttpData implements Attribute {
 
     @Override
     public void setValue(String value) throws IOException {
-        if (value == null) {
-            throw new NullPointerException("value");
-        }
+        ObjectUtil.checkNotNull(value, "value");
         byte [] bytes = value.getBytes(getCharset());
         checkSize(bytes.length);
         ByteBuf buffer = wrappedBuffer(bytes);
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/DiskFileUpload.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/DiskFileUpload.java
index 1a5076f..adb16a1 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/DiskFileUpload.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/DiskFileUpload.java
@@ -19,6 +19,7 @@ import io.netty.buffer.ByteBuf;
 import io.netty.channel.ChannelException;
 import io.netty.handler.codec.http.HttpHeaderNames;
 import io.netty.handler.codec.http.HttpHeaderValues;
+import io.netty.util.internal.ObjectUtil;
 
 import java.io.File;
 import java.io.IOException;
@@ -62,10 +63,7 @@ public class DiskFileUpload extends AbstractDiskHttpData implements FileUpload {
 
     @Override
     public void setFilename(String filename) {
-        if (filename == null) {
-            throw new NullPointerException("filename");
-        }
-        this.filename = filename;
+        this.filename = ObjectUtil.checkNotNull(filename, "filename");
     }
 
     @Override
@@ -93,10 +91,7 @@ public class DiskFileUpload extends AbstractDiskHttpData implements FileUpload {
 
     @Override
     public void setContentType(String contentType) {
-        if (contentType == null) {
-            throw new NullPointerException("contentType");
-        }
-        this.contentType = contentType;
+        this.contentType = ObjectUtil.checkNotNull(contentType, "contentType");
     }
 
     @Override
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostMultipartRequestDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostMultipartRequestDecoder.java
index 4fefc11..4d59e4d 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostMultipartRequestDecoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostMultipartRequestDecoder.java
@@ -764,8 +764,6 @@ public class HttpPostMultipartRequestDecoder implements InterfaceHttpPostRequest
                         }
                     }
                 }
-            } else {
-                throw new ErrorDataDecoderException("Unknown Params: " + newline);
             }
         }
         // Is it a FileUpload
@@ -813,7 +811,7 @@ public class HttpPostMultipartRequestDecoder implements InterfaceHttpPostRequest
         } else if (FILENAME_ENCODED.equals(name)) {
             try {
                 name = HttpHeaderValues.FILENAME.toString();
-                String[] split = value.split("'", 3);
+                String[] split = cleanString(value).split("'", 3);
                 value = QueryStringDecoder.decodeComponent(split[2], Charset.forName(split[0]));
             } catch (ArrayIndexOutOfBoundsException e) {
                  throw new ErrorDataDecoderException(e);
@@ -933,19 +931,15 @@ public class HttpPostMultipartRequestDecoder implements InterfaceHttpPostRequest
      */
     @Override
     public void destroy() {
-        checkDestroyed();
+        // Release all data items, including those not yet pulled
         cleanFiles();
+
         destroyed = true;
 
         if (undecodedChunk != null && undecodedChunk.refCnt() > 0) {
             undecodedChunk.release();
             undecodedChunk = null;
         }
-
-        // release all data which was not yet pulled
-        for (int i = bodyListHttpDataRank; i < bodyListHttpData.size(); i++) {
-            bodyListHttpData.get(i).release();
-        }
     }
 
     /**
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoder.java
index 0c10626..0183b23 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoder.java
@@ -21,6 +21,7 @@ import io.netty.handler.codec.http.HttpContent;
 import io.netty.handler.codec.http.HttpHeaderNames;
 import io.netty.handler.codec.http.HttpHeaderValues;
 import io.netty.handler.codec.http.HttpRequest;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.StringUtil;
 
 import java.nio.charset.Charset;
@@ -83,15 +84,10 @@ public class HttpPostRequestDecoder implements InterfaceHttpPostRequestDecoder {
      *             errors
      */
     public HttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request, Charset charset) {
-        if (factory == null) {
-            throw new NullPointerException("factory");
-        }
-        if (request == null) {
-            throw new NullPointerException("request");
-        }
-        if (charset == null) {
-            throw new NullPointerException("charset");
-        }
+        ObjectUtil.checkNotNull(factory, "factory");
+        ObjectUtil.checkNotNull(request, "request");
+        ObjectUtil.checkNotNull(charset, "charset");
+
         // Fill default values
         if (isMultipart(request)) {
             decoder = new HttpPostMultipartRequestDecoder(factory, request, charset);
@@ -140,11 +136,11 @@ public class HttpPostRequestDecoder implements InterfaceHttpPostRequestDecoder {
      * @return True if the request is a Multipart request
      */
     public static boolean isMultipart(HttpRequest request) {
-        if (request.headers().contains(HttpHeaderNames.CONTENT_TYPE)) {
-            return getMultipartDataBoundary(request.headers().get(HttpHeaderNames.CONTENT_TYPE)) != null;
-        } else {
-            return false;
+        String mimeType = request.headers().get(HttpHeaderNames.CONTENT_TYPE);
+        if (mimeType != null && mimeType.startsWith(HttpHeaderValues.MULTIPART_FORM_DATA.toString())) {
+            return getMultipartDataBoundary(mimeType) != null;
         }
+        return false;
     }
 
     /**
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestEncoder.java
index 21faffe..f42e7b0 100755
--- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestEncoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestEncoder.java
@@ -34,6 +34,7 @@ import io.netty.handler.codec.http.HttpUtil;
 import io.netty.handler.codec.http.HttpVersion;
 import io.netty.handler.codec.http.LastHttpContent;
 import io.netty.handler.stream.ChunkedInput;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.PlatformDependent;
 import io.netty.util.internal.StringUtil;
 
@@ -310,9 +311,7 @@ public class HttpPostRequestEncoder implements ChunkedInput<HttpContent> {
      *             if the encoding is in error or if the finalize were already done
      */
     public void setBodyHttpDatas(List<InterfaceHttpData> datas) throws ErrorDataEncoderException {
-        if (datas == null) {
-            throw new NullPointerException("datas");
-        }
+        ObjectUtil.checkNotNull(datas, "datas");
         globalBodySize = 0;
         bodyListDatas.clear();
         currentFileUpload = null;
@@ -638,7 +637,7 @@ public class HttpPostRequestEncoder implements ChunkedInput<HttpContent> {
                         replacement.append("; ")
                                    .append(HttpHeaderValues.FILENAME)
                                    .append("=\"")
-                                   .append(fileUpload.getFilename())
+                                   .append(currentFileUpload.getFilename())
                                    .append('"');
                     }
 
@@ -867,7 +866,7 @@ public class HttpPostRequestEncoder implements ChunkedInput<HttpContent> {
 
     /**
      *
-     * @return the next ByteBuf to send as a HttpChunk and modifying currentBuffer accordingly
+     * @return the next ByteBuf to send as an HttpChunk and modifying currentBuffer accordingly
      */
     private ByteBuf fillByteBuf() {
         int length = currentBuffer.readableBytes();
@@ -977,7 +976,11 @@ public class HttpPostRequestEncoder implements ChunkedInput<HttpContent> {
         if (buffer.capacity() == 0) {
             currentData = null;
             if (currentBuffer == null) {
-                currentBuffer = delimiter;
+                if (delimiter == null) {
+                    return null;
+                } else {
+                    currentBuffer = delimiter;
+                }
             } else {
                 if (delimiter != null) {
                     currentBuffer = wrappedBuffer(currentBuffer, delimiter);
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostStandardRequestDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostStandardRequestDecoder.java
index 88cf05e..7b94a7c 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostStandardRequestDecoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostStandardRequestDecoder.java
@@ -26,6 +26,7 @@ import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.EndOfDataDec
 import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.ErrorDataDecoderException;
 import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.MultiPartStatus;
 import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.NotEnoughDataDecoderException;
+import io.netty.util.internal.PlatformDependent;
 
 import java.io.IOException;
 import java.nio.charset.Charset;
@@ -148,13 +149,18 @@ public class HttpPostStandardRequestDecoder implements InterfaceHttpPostRequestD
         this.request = checkNotNull(request, "request");
         this.charset = checkNotNull(charset, "charset");
         this.factory = checkNotNull(factory, "factory");
-        if (request instanceof HttpContent) {
-            // Offer automatically if the given request is als type of HttpContent
-            // See #1089
-            offer((HttpContent) request);
-        } else {
-            undecodedChunk = buffer();
-            parseBody();
+        try {
+            if (request instanceof HttpContent) {
+                // Offer automatically if the given request is als type of HttpContent
+                // See #1089
+                offer((HttpContent) request);
+            } else {
+                undecodedChunk = buffer();
+                parseBody();
+            }
+        } catch (Throwable e) {
+            destroy();
+            PlatformDependent.throwException(e);
         }
     }
 
@@ -481,6 +487,10 @@ public class HttpPostStandardRequestDecoder implements InterfaceHttpPostRequestD
             // error while decoding
             undecodedChunk.readerIndex(firstpos);
             throw new ErrorDataDecoderException(e);
+        } catch (IllegalArgumentException e) {
+            // error while decoding
+            undecodedChunk.readerIndex(firstpos);
+            throw new ErrorDataDecoderException(e);
         }
     }
 
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/InternalAttribute.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/InternalAttribute.java
index 991100e..ccb17cd 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/InternalAttribute.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/InternalAttribute.java
@@ -18,6 +18,7 @@ package io.netty.handler.codec.http.multipart;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
 import io.netty.util.AbstractReferenceCounted;
+import io.netty.util.internal.ObjectUtil;
 
 import java.nio.charset.Charset;
 import java.util.ArrayList;
@@ -42,27 +43,21 @@ final class InternalAttribute extends AbstractReferenceCounted implements Interf
     }
 
     public void addValue(String value) {
-        if (value == null) {
-            throw new NullPointerException("value");
-        }
+        ObjectUtil.checkNotNull(value, "value");
         ByteBuf buf = Unpooled.copiedBuffer(value, charset);
         this.value.add(buf);
         size += buf.readableBytes();
     }
 
     public void addValue(String value, int rank) {
-        if (value == null) {
-            throw new NullPointerException("value");
-        }
+        ObjectUtil.checkNotNull(value, "value");
         ByteBuf buf = Unpooled.copiedBuffer(value, charset);
         this.value.add(rank, buf);
         size += buf.readableBytes();
     }
 
     public void setValue(String value, int rank) {
-        if (value == null) {
-            throw new NullPointerException("value");
-        }
+        ObjectUtil.checkNotNull(value, "value");
         ByteBuf buf = Unpooled.copiedBuffer(value, charset);
         ByteBuf old = this.value.set(rank, buf);
         if (old != null) {
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MemoryAttribute.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MemoryAttribute.java
index 63c8c34..780929c 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MemoryAttribute.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MemoryAttribute.java
@@ -18,6 +18,7 @@ package io.netty.handler.codec.http.multipart;
 import io.netty.buffer.ByteBuf;
 import io.netty.channel.ChannelException;
 import io.netty.handler.codec.http.HttpConstants;
+import io.netty.util.internal.ObjectUtil;
 
 import java.io.IOException;
 import java.nio.charset.Charset;
@@ -66,9 +67,7 @@ public class MemoryAttribute extends AbstractMemoryHttpData implements Attribute
 
     @Override
     public void setValue(String value) throws IOException {
-        if (value == null) {
-            throw new NullPointerException("value");
-        }
+        ObjectUtil.checkNotNull(value, "value");
         byte [] bytes = value.getBytes(getCharset());
         checkSize(bytes.length);
         ByteBuf buffer = wrappedBuffer(bytes);
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MemoryFileUpload.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MemoryFileUpload.java
index 28e3859..88e08f8 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MemoryFileUpload.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/MemoryFileUpload.java
@@ -19,6 +19,7 @@ import io.netty.buffer.ByteBuf;
 import io.netty.channel.ChannelException;
 import io.netty.handler.codec.http.HttpHeaderNames;
 import io.netty.handler.codec.http.HttpHeaderValues;
+import io.netty.util.internal.ObjectUtil;
 
 import java.io.IOException;
 import java.nio.charset.Charset;
@@ -56,10 +57,7 @@ public class MemoryFileUpload extends AbstractMemoryHttpData implements FileUplo
 
     @Override
     public void setFilename(String filename) {
-        if (filename == null) {
-            throw new NullPointerException("filename");
-        }
-        this.filename = filename;
+        this.filename = ObjectUtil.checkNotNull(filename, "filename");
     }
 
     @Override
@@ -87,10 +85,7 @@ public class MemoryFileUpload extends AbstractMemoryHttpData implements FileUplo
 
     @Override
     public void setContentType(String contentType) {
-        if (contentType == null) {
-            throw new NullPointerException("contentType");
-        }
-        this.contentType = contentType;
+        this.contentType = ObjectUtil.checkNotNull(contentType, "contentType");
     }
 
     @Override
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/BinaryWebSocketFrame.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/BinaryWebSocketFrame.java
index 5fbcd90..91de5c3 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/BinaryWebSocketFrame.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/BinaryWebSocketFrame.java
@@ -19,7 +19,7 @@ import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
 
 /**
- * Web Socket frame containing binary data
+ * Web Socket frame containing binary data.
  */
 public class BinaryWebSocketFrame extends WebSocketFrame {
 
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/CloseWebSocketFrame.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/CloseWebSocketFrame.java
index 371be6e..0286c6f 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/CloseWebSocketFrame.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/CloseWebSocketFrame.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012 The Netty Project
+ * Copyright 2019 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
@@ -21,7 +21,7 @@ import io.netty.util.CharsetUtil;
 import io.netty.util.internal.StringUtil;
 
 /**
- * Web Socket Frame for closing the connection
+ * Web Socket Frame for closing the connection.
  */
 public class CloseWebSocketFrame extends WebSocketFrame {
 
@@ -33,7 +33,31 @@ public class CloseWebSocketFrame extends WebSocketFrame {
     }
 
     /**
-     * Creates a new empty close frame with closing getStatus code and reason text
+     * Creates a new empty close frame with closing status code and reason text
+     *
+     * @param status
+     *            Status code as per <a href="http://tools.ietf.org/html/rfc6455#section-7.4">RFC 6455</a>. For
+     *            example, <tt>1000</tt> indicates normal closure.
+     */
+    public CloseWebSocketFrame(WebSocketCloseStatus status) {
+        this(status.code(), status.reasonText());
+    }
+
+    /**
+     * Creates a new empty close frame with closing status code and reason text
+     *
+     * @param status
+     *            Status code as per <a href="http://tools.ietf.org/html/rfc6455#section-7.4">RFC 6455</a>. For
+     *            example, <tt>1000</tt> indicates normal closure.
+     * @param reasonText
+     *            Reason text. Set to null if no text.
+     */
+    public CloseWebSocketFrame(WebSocketCloseStatus status, String reasonText) {
+        this(status.code(), reasonText);
+    }
+
+    /**
+     * Creates a new empty close frame with closing status code and reason text
      *
      * @param statusCode
      *            Integer status code as per <a href="http://tools.ietf.org/html/rfc6455#section-7.4">RFC 6455</a>. For
@@ -46,12 +70,12 @@ public class CloseWebSocketFrame extends WebSocketFrame {
     }
 
     /**
-     * Creates a new close frame with no losing getStatus code and no reason text
+     * Creates a new close frame with no losing status code and no reason text
      *
      * @param finalFragment
      *            flag indicating if this frame is the final fragment
      * @param rsv
-     *            reserved bits used for protocol extensions
+     *            reserved bits used for protocol extensions.
      */
     public CloseWebSocketFrame(boolean finalFragment, int rsv) {
         this(finalFragment, rsv, Unpooled.buffer(0));
@@ -105,7 +129,7 @@ public class CloseWebSocketFrame extends WebSocketFrame {
 
     /**
      * Returns the closing status code as per <a href="http://tools.ietf.org/html/rfc6455#section-7.4">RFC 6455</a>. If
-     * a getStatus code is set, -1 is returned.
+     * a status code is set, -1 is returned.
      */
     public int statusCode() {
         ByteBuf binaryData = content();
@@ -114,10 +138,7 @@ public class CloseWebSocketFrame extends WebSocketFrame {
         }
 
         binaryData.readerIndex(0);
-        int statusCode = binaryData.readShort();
-        binaryData.readerIndex(0);
-
-        return statusCode;
+        return binaryData.getShort(0);
     }
 
     /**
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/ContinuationWebSocketFrame.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/ContinuationWebSocketFrame.java
index bd25ea0..515fe4e 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/ContinuationWebSocketFrame.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/ContinuationWebSocketFrame.java
@@ -43,7 +43,7 @@ public class ContinuationWebSocketFrame extends WebSocketFrame {
     }
 
     /**
-     * Creates a new continuation frame with the specified binary data
+     * Creates a new continuation frame with the specified binary data.
      *
      * @param finalFragment
      *            flag indicating if this frame is the final fragment
@@ -71,17 +71,17 @@ public class ContinuationWebSocketFrame extends WebSocketFrame {
     }
 
     /**
-     * Returns the text data in this frame
+     * Returns the text data in this frame.
      */
     public String text() {
         return content().toString(CharsetUtil.UTF_8);
     }
 
     /**
-     * Sets the string for this frame
+     * Sets the string for this frame.
      *
      * @param text
-     *            text to store
+     *            text to store.
      */
     private static ByteBuf fromText(String text) {
         if (text == null || text.isEmpty()) {
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/CorruptedWebSocketFrameException.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/CorruptedWebSocketFrameException.java
new file mode 100644
index 0000000..ceeec83
--- /dev/null
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/CorruptedWebSocketFrameException.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2019 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.http.websocketx;
+
+import io.netty.handler.codec.CorruptedFrameException;
+import io.netty.handler.codec.DecoderException;
+
+/**
+ * An {@link DecoderException} which is thrown when the received {@link WebSocketFrame} data could not be decoded by
+ * an inbound handler.
+ */
+public final class CorruptedWebSocketFrameException extends CorruptedFrameException {
+
+    private static final long serialVersionUID = 3918055132492988338L;
+
+    private final WebSocketCloseStatus closeStatus;
+
+    /**
+     * Creates a new instance.
+     */
+    public CorruptedWebSocketFrameException() {
+        this(WebSocketCloseStatus.PROTOCOL_ERROR, null, null);
+    }
+
+    /**
+     * Creates a new instance.
+     */
+    public CorruptedWebSocketFrameException(WebSocketCloseStatus status, String message, Throwable cause) {
+        super(message == null ? status.reasonText() : message, cause);
+        closeStatus = status;
+    }
+
+    /**
+     * Creates a new instance.
+     */
+    public CorruptedWebSocketFrameException(WebSocketCloseStatus status, String message) {
+        this(status, message, null);
+    }
+
+    /**
+     * Creates a new instance.
+     */
+    public CorruptedWebSocketFrameException(WebSocketCloseStatus status, Throwable cause) {
+        this(status, null, cause);
+    }
+
+    public WebSocketCloseStatus closeStatus() {
+        return closeStatus;
+    }
+
+}
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/PingWebSocketFrame.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/PingWebSocketFrame.java
index 08e7025..6c3c79c 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/PingWebSocketFrame.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/PingWebSocketFrame.java
@@ -19,7 +19,7 @@ import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
 
 /**
- * Web Socket frame containing binary data
+ * Web Socket frame containing binary data.
  */
 public class PingWebSocketFrame extends WebSocketFrame {
 
@@ -41,7 +41,7 @@ public class PingWebSocketFrame extends WebSocketFrame {
     }
 
     /**
-     * Creates a new ping frame with the specified binary data
+     * Creates a new ping frame with the specified binary data.
      *
      * @param finalFragment
      *            flag indicating if this frame is the final fragment
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/PongWebSocketFrame.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/PongWebSocketFrame.java
index 29c0b0f..d1b08eb 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/PongWebSocketFrame.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/PongWebSocketFrame.java
@@ -19,7 +19,7 @@ import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
 
 /**
- * Web Socket frame containing binary data
+ * Web Socket frame containing binary data.
  */
 public class PongWebSocketFrame extends WebSocketFrame {
 
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/TextWebSocketFrame.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/TextWebSocketFrame.java
index 9b12471..7d6e2aa 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/TextWebSocketFrame.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/TextWebSocketFrame.java
@@ -20,7 +20,7 @@ import io.netty.buffer.Unpooled;
 import io.netty.util.CharsetUtil;
 
 /**
- * Web Socket text frame
+ * Web Socket text frame.
  */
 public class TextWebSocketFrame extends WebSocketFrame {
 
@@ -35,7 +35,7 @@ public class TextWebSocketFrame extends WebSocketFrame {
      * Creates a new text frame with the specified text string. The final fragment flag is set to true.
      *
      * @param text
-     *            String to put in the frame
+     *            String to put in the frame.
      */
     public TextWebSocketFrame(String text) {
         super(fromText(text));
@@ -59,7 +59,7 @@ public class TextWebSocketFrame extends WebSocketFrame {
      * @param rsv
      *            reserved bits used for protocol extensions
      * @param text
-     *            String to put in the frame
+     *            String to put in the frame.
      */
     public TextWebSocketFrame(boolean finalFragment, int rsv, String text) {
         super(finalFragment, rsv, fromText(text));
@@ -74,7 +74,7 @@ public class TextWebSocketFrame extends WebSocketFrame {
     }
 
     /**
-     * Creates a new text frame with the specified binary data. The final fragment flag is set to true.
+     * Creates a new text frame with the specified binary data and the final fragment flag.
      *
      * @param finalFragment
      *            flag indicating if this frame is the final fragment
@@ -88,7 +88,7 @@ public class TextWebSocketFrame extends WebSocketFrame {
     }
 
     /**
-     * Returns the text data in this frame
+     * Returns the text data in this frame.
      */
     public String text() {
         return content().toString(CharsetUtil.UTF_8);
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/Utf8FrameValidator.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/Utf8FrameValidator.java
index 5ce5ec3..7edf5cf 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/Utf8FrameValidator.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/Utf8FrameValidator.java
@@ -35,42 +35,47 @@ public class Utf8FrameValidator extends ChannelInboundHandlerAdapter {
         if (msg instanceof WebSocketFrame) {
             WebSocketFrame frame = (WebSocketFrame) msg;
 
-            // Processing for possible fragmented messages for text and binary
-            // frames
-            if (((WebSocketFrame) msg).isFinalFragment()) {
-                // Final frame of the sequence. Apparently ping frames are
-                // allowed in the middle of a fragmented message
-                if (!(frame instanceof PingWebSocketFrame)) {
-                    fragmentedFramesCount = 0;
+            try {
+                // Processing for possible fragmented messages for text and binary
+                // frames
+                if (((WebSocketFrame) msg).isFinalFragment()) {
+                    // Final frame of the sequence. Apparently ping frames are
+                    // allowed in the middle of a fragmented message
+                    if (!(frame instanceof PingWebSocketFrame)) {
+                        fragmentedFramesCount = 0;
 
-                    // Check text for UTF8 correctness
-                    if ((frame instanceof TextWebSocketFrame) ||
-                            (utf8Validator != null && utf8Validator.isChecking())) {
-                        // Check UTF-8 correctness for this payload
-                        checkUTF8String(frame.content());
+                        // Check text for UTF8 correctness
+                        if ((frame instanceof TextWebSocketFrame) ||
+                                (utf8Validator != null && utf8Validator.isChecking())) {
+                            // Check UTF-8 correctness for this payload
+                            checkUTF8String(frame.content());
 
-                        // This does a second check to make sure UTF-8
-                        // correctness for entire text message
-                        utf8Validator.finish();
-                    }
-                }
-            } else {
-                // Not final frame so we can expect more frames in the
-                // fragmented sequence
-                if (fragmentedFramesCount == 0) {
-                    // First text or binary frame for a fragmented set
-                    if (frame instanceof TextWebSocketFrame) {
-                        checkUTF8String(frame.content());
+                            // This does a second check to make sure UTF-8
+                            // correctness for entire text message
+                            utf8Validator.finish();
+                        }
                     }
                 } else {
-                    // Subsequent frames - only check if init frame is text
-                    if (utf8Validator != null && utf8Validator.isChecking()) {
-                        checkUTF8String(frame.content());
+                    // Not final frame so we can expect more frames in the
+                    // fragmented sequence
+                    if (fragmentedFramesCount == 0) {
+                        // First text or binary frame for a fragmented set
+                        if (frame instanceof TextWebSocketFrame) {
+                            checkUTF8String(frame.content());
+                        }
+                    } else {
+                        // Subsequent frames - only check if init frame is text
+                        if (utf8Validator != null && utf8Validator.isChecking()) {
+                            checkUTF8String(frame.content());
+                        }
                     }
-                }
 
-                // Increment counter
-                fragmentedFramesCount++;
+                    // Increment counter
+                    fragmentedFramesCount++;
+                }
+            } catch (CorruptedWebSocketFrameException e) {
+                frame.release();
+                throw e;
             }
         }
 
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/Utf8Validator.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/Utf8Validator.java
index 3a377e7..be85dc2 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/Utf8Validator.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/Utf8Validator.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012 The Netty Project
+ * Copyright 2019 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
@@ -36,7 +36,6 @@
 package io.netty.handler.codec.http.websocketx;
 
 import io.netty.buffer.ByteBuf;
-import io.netty.handler.codec.CorruptedFrameException;
 import io.netty.util.ByteProcessor;
 
 /**
@@ -79,7 +78,8 @@ final class Utf8Validator implements ByteProcessor {
         codep = 0;
         if (state != UTF8_ACCEPT) {
             state = UTF8_ACCEPT;
-            throw new CorruptedFrameException("bytes are not UTF-8");
+            throw new CorruptedWebSocketFrameException(
+                WebSocketCloseStatus.INVALID_PAYLOAD_DATA, "bytes are not UTF-8");
         }
     }
 
@@ -93,7 +93,8 @@ final class Utf8Validator implements ByteProcessor {
 
         if (state == UTF8_REJECT) {
             checking = false;
-            throw new CorruptedFrameException("bytes are not UTF-8");
+            throw new CorruptedWebSocketFrameException(
+                WebSocketCloseStatus.INVALID_PAYLOAD_DATA, "bytes are not UTF-8");
         }
         return true;
     }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket00FrameDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket00FrameDecoder.java
index 1f6bad5..6d827db 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket00FrameDecoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket00FrameDecoder.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012 The Netty Project
+ * Copyright 2019 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
@@ -19,6 +19,7 @@ import io.netty.buffer.ByteBuf;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.handler.codec.ReplayingDecoder;
 import io.netty.handler.codec.TooLongFrameException;
+import io.netty.util.internal.ObjectUtil;
 
 import java.util.List;
 
@@ -52,6 +53,17 @@ public class WebSocket00FrameDecoder extends ReplayingDecoder<Void> implements W
         this.maxFrameSize = maxFrameSize;
     }
 
+    /**
+     * Creates a new instance of {@code WebSocketFrameDecoder} with the specified {@code maxFrameSize}. If the client
+     * sends a frame size larger than {@code maxFrameSize}, the channel will be closed.
+     *
+     * @param decoderConfig
+     *            Frames decoder configuration.
+     */
+    public WebSocket00FrameDecoder(WebSocketDecoderConfig decoderConfig) {
+        this.maxFrameSize = ObjectUtil.checkNotNull(decoderConfig, "decoderConfig").maxFramePayloadLength();
+    }
+
     @Override
     protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
         // Discard all data received if closing handshake was received before.
@@ -96,7 +108,7 @@ public class WebSocket00FrameDecoder extends ReplayingDecoder<Void> implements W
 
         if (type == (byte) 0xFF && frameSize == 0) {
             receivedClosingHandshake = true;
-            return new CloseWebSocketFrame();
+            return new CloseWebSocketFrame(true, 0, ctx.alloc().buffer(0));
         }
         ByteBuf payload = readBytes(ctx.alloc(), buffer, (int) frameSize);
         return new BinaryWebSocketFrame(payload);
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket07FrameDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket07FrameDecoder.java
index 0ecb571..6aa2776 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket07FrameDecoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket07FrameDecoder.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012 The Netty Project
+ * Copyright 2019 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
@@ -71,7 +71,11 @@ public class WebSocket07FrameDecoder extends WebSocket08FrameDecoder {
      *            helps check for denial of services attacks.
      */
     public WebSocket07FrameDecoder(boolean expectMaskedFrames, boolean allowExtensions, int maxFramePayloadLength) {
-        this(expectMaskedFrames, allowExtensions, maxFramePayloadLength, false);
+        this(WebSocketDecoderConfig.newBuilder()
+            .expectMaskedFrames(expectMaskedFrames)
+            .allowExtensions(allowExtensions)
+            .maxFramePayloadLength(maxFramePayloadLength)
+            .build());
     }
 
     /**
@@ -91,6 +95,21 @@ public class WebSocket07FrameDecoder extends WebSocket08FrameDecoder {
      */
     public WebSocket07FrameDecoder(boolean expectMaskedFrames, boolean allowExtensions, int maxFramePayloadLength,
                                    boolean allowMaskMismatch) {
-        super(expectMaskedFrames, allowExtensions, maxFramePayloadLength, allowMaskMismatch);
+        this(WebSocketDecoderConfig.newBuilder()
+            .expectMaskedFrames(expectMaskedFrames)
+            .allowExtensions(allowExtensions)
+            .maxFramePayloadLength(maxFramePayloadLength)
+            .allowMaskMismatch(allowMaskMismatch)
+            .build());
+    }
+
+    /**
+     * Constructor
+     *
+     * @param decoderConfig
+     *            Frames decoder configuration.
+     */
+    public WebSocket07FrameDecoder(WebSocketDecoderConfig decoderConfig) {
+        super(decoderConfig);
     }
 }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket08FrameDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket08FrameDecoder.java
index f5a6dc1..a1320f9 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket08FrameDecoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket08FrameDecoder.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012 The Netty Project
+ * Copyright 2019 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
@@ -58,8 +58,8 @@ import io.netty.buffer.Unpooled;
 import io.netty.channel.ChannelFutureListener;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.handler.codec.ByteToMessageDecoder;
-import io.netty.handler.codec.CorruptedFrameException;
 import io.netty.handler.codec.TooLongFrameException;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 
@@ -93,10 +93,7 @@ public class WebSocket08FrameDecoder extends ByteToMessageDecoder
     private static final byte OPCODE_PING = 0x9;
     private static final byte OPCODE_PONG = 0xA;
 
-    private final long maxFramePayloadLength;
-    private final boolean allowExtensions;
-    private final boolean expectMaskedFrames;
-    private final boolean allowMaskMismatch;
+    private final WebSocketDecoderConfig config;
 
     private int fragmentedFramesCount;
     private boolean frameFinalFlag;
@@ -142,242 +139,255 @@ public class WebSocket08FrameDecoder extends ByteToMessageDecoder
      */
     public WebSocket08FrameDecoder(boolean expectMaskedFrames, boolean allowExtensions, int maxFramePayloadLength,
                                    boolean allowMaskMismatch) {
-        this.expectMaskedFrames = expectMaskedFrames;
-        this.allowMaskMismatch = allowMaskMismatch;
-        this.allowExtensions = allowExtensions;
-        this.maxFramePayloadLength = maxFramePayloadLength;
+        this(WebSocketDecoderConfig.newBuilder()
+            .expectMaskedFrames(expectMaskedFrames)
+            .allowExtensions(allowExtensions)
+            .maxFramePayloadLength(maxFramePayloadLength)
+            .allowMaskMismatch(allowMaskMismatch)
+            .build());
+    }
+
+    /**
+     * Constructor
+     *
+     * @param decoderConfig
+     *            Frames decoder configuration.
+     */
+    public WebSocket08FrameDecoder(WebSocketDecoderConfig decoderConfig) {
+        this.config = ObjectUtil.checkNotNull(decoderConfig, "decoderConfig");
     }
 
     @Override
     protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
-
         // Discard all data received if closing handshake was received before.
         if (receivedClosingHandshake) {
             in.skipBytes(actualReadableBytes());
             return;
         }
-            switch (state) {
-                case READING_FIRST:
-                    if (!in.isReadable()) {
-                        return;
-                    }
 
-                    framePayloadLength = 0;
+        switch (state) {
+        case READING_FIRST:
+            if (!in.isReadable()) {
+                return;
+            }
 
-                    // FIN, RSV, OPCODE
-                    byte b = in.readByte();
-                    frameFinalFlag = (b & 0x80) != 0;
-                    frameRsv = (b & 0x70) >> 4;
-                    frameOpcode = b & 0x0F;
+            framePayloadLength = 0;
 
-                    if (logger.isDebugEnabled()) {
-                        logger.debug("Decoding WebSocket Frame opCode={}", frameOpcode);
-                    }
+            // FIN, RSV, OPCODE
+            byte b = in.readByte();
+            frameFinalFlag = (b & 0x80) != 0;
+            frameRsv = (b & 0x70) >> 4;
+            frameOpcode = b & 0x0F;
 
-                    state = State.READING_SECOND;
-                case READING_SECOND:
-                    if (!in.isReadable()) {
-                        return;
-                    }
-                    // MASK, PAYLOAD LEN 1
-                    b = in.readByte();
-                    frameMasked = (b & 0x80) != 0;
-                    framePayloadLen1 = b & 0x7F;
-
-                    if (frameRsv != 0 && !allowExtensions) {
-                        protocolViolation(ctx, "RSV != 0 and no extension negotiated, RSV:" + frameRsv);
-                        return;
-                    }
+            if (logger.isTraceEnabled()) {
+                logger.trace("Decoding WebSocket Frame opCode={}", frameOpcode);
+            }
 
-                    if (!allowMaskMismatch && expectMaskedFrames != frameMasked) {
-                        protocolViolation(ctx, "received a frame that is not masked as expected");
-                        return;
-                    }
+            state = State.READING_SECOND;
+        case READING_SECOND:
+            if (!in.isReadable()) {
+                return;
+            }
+            // MASK, PAYLOAD LEN 1
+            b = in.readByte();
+            frameMasked = (b & 0x80) != 0;
+            framePayloadLen1 = b & 0x7F;
+
+            if (frameRsv != 0 && !config.allowExtensions()) {
+                protocolViolation(ctx, in, "RSV != 0 and no extension negotiated, RSV:" + frameRsv);
+                return;
+            }
 
-                    if (frameOpcode > 7) { // control frame (have MSB in opcode set)
-
-                        // control frames MUST NOT be fragmented
-                        if (!frameFinalFlag) {
-                            protocolViolation(ctx, "fragmented control frame");
-                            return;
-                        }
-
-                        // control frames MUST have payload 125 octets or less
-                        if (framePayloadLen1 > 125) {
-                            protocolViolation(ctx, "control frame with payload length > 125 octets");
-                            return;
-                        }
-
-                        // check for reserved control frame opcodes
-                        if (!(frameOpcode == OPCODE_CLOSE || frameOpcode == OPCODE_PING
-                                || frameOpcode == OPCODE_PONG)) {
-                            protocolViolation(ctx, "control frame using reserved opcode " + frameOpcode);
-                            return;
-                        }
-
-                        // close frame : if there is a body, the first two bytes of the
-                        // body MUST be a 2-byte unsigned integer (in network byte
-                        // order) representing a getStatus code
-                        if (frameOpcode == 8 && framePayloadLen1 == 1) {
-                            protocolViolation(ctx, "received close control frame with payload len 1");
-                            return;
-                        }
-                    } else { // data frame
-                        // check for reserved data frame opcodes
-                        if (!(frameOpcode == OPCODE_CONT || frameOpcode == OPCODE_TEXT
-                                || frameOpcode == OPCODE_BINARY)) {
-                            protocolViolation(ctx, "data frame using reserved opcode " + frameOpcode);
-                            return;
-                        }
-
-                        // check opcode vs message fragmentation state 1/2
-                        if (fragmentedFramesCount == 0 && frameOpcode == OPCODE_CONT) {
-                            protocolViolation(ctx, "received continuation data frame outside fragmented message");
-                            return;
-                        }
-
-                        // check opcode vs message fragmentation state 2/2
-                        if (fragmentedFramesCount != 0 && frameOpcode != OPCODE_CONT && frameOpcode != OPCODE_PING) {
-                            protocolViolation(ctx,
-                                    "received non-continuation data frame while inside fragmented message");
-                            return;
-                        }
-                    }
+            if (!config.allowMaskMismatch() && config.expectMaskedFrames() != frameMasked) {
+                protocolViolation(ctx, in, "received a frame that is not masked as expected");
+                return;
+            }
 
-                    state = State.READING_SIZE;
-                 case READING_SIZE:
-
-                    // Read frame payload length
-                    if (framePayloadLen1 == 126) {
-                        if (in.readableBytes() < 2) {
-                            return;
-                        }
-                        framePayloadLength = in.readUnsignedShort();
-                        if (framePayloadLength < 126) {
-                            protocolViolation(ctx, "invalid data frame length (not using minimal length encoding)");
-                            return;
-                        }
-                    } else if (framePayloadLen1 == 127) {
-                        if (in.readableBytes() < 8) {
-                            return;
-                        }
-                        framePayloadLength = in.readLong();
-                        // TODO: check if it's bigger than 0x7FFFFFFFFFFFFFFF, Maybe
-                        // just check if it's negative?
-
-                        if (framePayloadLength < 65536) {
-                            protocolViolation(ctx, "invalid data frame length (not using minimal length encoding)");
-                            return;
-                        }
-                    } else {
-                        framePayloadLength = framePayloadLen1;
-                    }
+            if (frameOpcode > 7) { // control frame (have MSB in opcode set)
 
-                    if (framePayloadLength > maxFramePayloadLength) {
-                        protocolViolation(ctx, "Max frame length of " + maxFramePayloadLength + " has been exceeded.");
-                        return;
-                    }
+                // control frames MUST NOT be fragmented
+                if (!frameFinalFlag) {
+                    protocolViolation(ctx, in, "fragmented control frame");
+                    return;
+                }
 
-                    if (logger.isDebugEnabled()) {
-                        logger.debug("Decoding WebSocket Frame length={}", framePayloadLength);
-                    }
+                // control frames MUST have payload 125 octets or less
+                if (framePayloadLen1 > 125) {
+                    protocolViolation(ctx, in, "control frame with payload length > 125 octets");
+                    return;
+                }
 
-                    state = State.MASKING_KEY;
-                case MASKING_KEY:
-                    if (frameMasked) {
-                        if (in.readableBytes() < 4) {
-                            return;
-                        }
-                        if (maskingKey == null) {
-                            maskingKey = new byte[4];
-                        }
-                        in.readBytes(maskingKey);
-                    }
-                    state = State.PAYLOAD;
-                case PAYLOAD:
-                    if (in.readableBytes() < framePayloadLength) {
-                        return;
-                    }
+                // check for reserved control frame opcodes
+                if (!(frameOpcode == OPCODE_CLOSE || frameOpcode == OPCODE_PING
+                      || frameOpcode == OPCODE_PONG)) {
+                    protocolViolation(ctx, in, "control frame using reserved opcode " + frameOpcode);
+                    return;
+                }
 
-                    ByteBuf payloadBuffer = null;
-                    try {
-                        payloadBuffer = readBytes(ctx.alloc(), in, toFrameLength(framePayloadLength));
-
-                        // Now we have all the data, the next checkpoint must be the next
-                        // frame
-                        state = State.READING_FIRST;
-
-                        // Unmask data if needed
-                        if (frameMasked) {
-                            unmask(payloadBuffer);
-                        }
-
-                        // Processing ping/pong/close frames because they cannot be
-                        // fragmented
-                        if (frameOpcode == OPCODE_PING) {
-                            out.add(new PingWebSocketFrame(frameFinalFlag, frameRsv, payloadBuffer));
-                            payloadBuffer = null;
-                            return;
-                        }
-                        if (frameOpcode == OPCODE_PONG) {
-                            out.add(new PongWebSocketFrame(frameFinalFlag, frameRsv, payloadBuffer));
-                            payloadBuffer = null;
-                            return;
-                        }
-                        if (frameOpcode == OPCODE_CLOSE) {
-                            receivedClosingHandshake = true;
-                            checkCloseFrameBody(ctx, payloadBuffer);
-                            out.add(new CloseWebSocketFrame(frameFinalFlag, frameRsv, payloadBuffer));
-                            payloadBuffer = null;
-                            return;
-                        }
-
-                        // Processing for possible fragmented messages for text and binary
-                        // frames
-                        if (frameFinalFlag) {
-                            // Final frame of the sequence. Apparently ping frames are
-                            // allowed in the middle of a fragmented message
-                            if (frameOpcode != OPCODE_PING) {
-                                fragmentedFramesCount = 0;
-                            }
-                        } else {
-                            // Increment counter
-                            fragmentedFramesCount++;
-                        }
-
-                        // Return the frame
-                        if (frameOpcode == OPCODE_TEXT) {
-                            out.add(new TextWebSocketFrame(frameFinalFlag, frameRsv, payloadBuffer));
-                            payloadBuffer = null;
-                            return;
-                        } else if (frameOpcode == OPCODE_BINARY) {
-                            out.add(new BinaryWebSocketFrame(frameFinalFlag, frameRsv, payloadBuffer));
-                            payloadBuffer = null;
-                            return;
-                        } else if (frameOpcode == OPCODE_CONT) {
-                            out.add(new ContinuationWebSocketFrame(frameFinalFlag, frameRsv,
-                                    payloadBuffer));
-                            payloadBuffer = null;
-                            return;
-                        } else {
-                            throw new UnsupportedOperationException("Cannot decode web socket frame with opcode: "
-                                    + frameOpcode);
-                        }
-                    } finally {
-                        if (payloadBuffer != null) {
-                            payloadBuffer.release();
-                        }
-                    }
-                case CORRUPT:
-                    if (in.isReadable()) {
-                        // If we don't keep reading Netty will throw an exception saying
-                        // we can't return null if no bytes read and state not changed.
-                        in.readByte();
+                // close frame : if there is a body, the first two bytes of the
+                // body MUST be a 2-byte unsigned integer (in network byte
+                // order) representing a getStatus code
+                if (frameOpcode == 8 && framePayloadLen1 == 1) {
+                    protocolViolation(ctx, in, "received close control frame with payload len 1");
+                    return;
+                }
+            } else { // data frame
+                // check for reserved data frame opcodes
+                if (!(frameOpcode == OPCODE_CONT || frameOpcode == OPCODE_TEXT
+                      || frameOpcode == OPCODE_BINARY)) {
+                    protocolViolation(ctx, in, "data frame using reserved opcode " + frameOpcode);
+                    return;
+                }
+
+                // check opcode vs message fragmentation state 1/2
+                if (fragmentedFramesCount == 0 && frameOpcode == OPCODE_CONT) {
+                    protocolViolation(ctx, in, "received continuation data frame outside fragmented message");
+                    return;
+                }
+
+                // check opcode vs message fragmentation state 2/2
+                if (fragmentedFramesCount != 0 && frameOpcode != OPCODE_CONT && frameOpcode != OPCODE_PING) {
+                    protocolViolation(ctx, in,
+                                      "received non-continuation data frame while inside fragmented message");
+                    return;
+                }
+            }
+
+            state = State.READING_SIZE;
+        case READING_SIZE:
+
+            // Read frame payload length
+            if (framePayloadLen1 == 126) {
+                if (in.readableBytes() < 2) {
+                    return;
+                }
+                framePayloadLength = in.readUnsignedShort();
+                if (framePayloadLength < 126) {
+                    protocolViolation(ctx, in, "invalid data frame length (not using minimal length encoding)");
+                    return;
+                }
+            } else if (framePayloadLen1 == 127) {
+                if (in.readableBytes() < 8) {
+                    return;
+                }
+                framePayloadLength = in.readLong();
+                // TODO: check if it's bigger than 0x7FFFFFFFFFFFFFFF, Maybe
+                // just check if it's negative?
+
+                if (framePayloadLength < 65536) {
+                    protocolViolation(ctx, in, "invalid data frame length (not using minimal length encoding)");
+                    return;
+                }
+            } else {
+                framePayloadLength = framePayloadLen1;
+            }
+
+            if (framePayloadLength > config.maxFramePayloadLength()) {
+                protocolViolation(ctx, in, WebSocketCloseStatus.MESSAGE_TOO_BIG,
+                    "Max frame length of " + config.maxFramePayloadLength() + " has been exceeded.");
+                return;
+            }
+
+            if (logger.isTraceEnabled()) {
+                logger.trace("Decoding WebSocket Frame length={}", framePayloadLength);
+            }
+
+            state = State.MASKING_KEY;
+        case MASKING_KEY:
+            if (frameMasked) {
+                if (in.readableBytes() < 4) {
+                    return;
+                }
+                if (maskingKey == null) {
+                    maskingKey = new byte[4];
+                }
+                in.readBytes(maskingKey);
+            }
+            state = State.PAYLOAD;
+        case PAYLOAD:
+            if (in.readableBytes() < framePayloadLength) {
+                return;
+            }
+
+            ByteBuf payloadBuffer = null;
+            try {
+                payloadBuffer = readBytes(ctx.alloc(), in, toFrameLength(framePayloadLength));
+
+                // Now we have all the data, the next checkpoint must be the next
+                // frame
+                state = State.READING_FIRST;
+
+                // Unmask data if needed
+                if (frameMasked) {
+                    unmask(payloadBuffer);
+                }
+
+                // Processing ping/pong/close frames because they cannot be
+                // fragmented
+                if (frameOpcode == OPCODE_PING) {
+                    out.add(new PingWebSocketFrame(frameFinalFlag, frameRsv, payloadBuffer));
+                    payloadBuffer = null;
+                    return;
+                }
+                if (frameOpcode == OPCODE_PONG) {
+                    out.add(new PongWebSocketFrame(frameFinalFlag, frameRsv, payloadBuffer));
+                    payloadBuffer = null;
+                    return;
+                }
+                if (frameOpcode == OPCODE_CLOSE) {
+                    receivedClosingHandshake = true;
+                    checkCloseFrameBody(ctx, payloadBuffer);
+                    out.add(new CloseWebSocketFrame(frameFinalFlag, frameRsv, payloadBuffer));
+                    payloadBuffer = null;
+                    return;
+                }
+
+                // Processing for possible fragmented messages for text and binary
+                // frames
+                if (frameFinalFlag) {
+                    // Final frame of the sequence. Apparently ping frames are
+                    // allowed in the middle of a fragmented message
+                    if (frameOpcode != OPCODE_PING) {
+                        fragmentedFramesCount = 0;
                     }
+                } else {
+                    // Increment counter
+                    fragmentedFramesCount++;
+                }
+
+                // Return the frame
+                if (frameOpcode == OPCODE_TEXT) {
+                    out.add(new TextWebSocketFrame(frameFinalFlag, frameRsv, payloadBuffer));
+                    payloadBuffer = null;
                     return;
-                default:
-                    throw new Error("Shouldn't reach here.");
+                } else if (frameOpcode == OPCODE_BINARY) {
+                    out.add(new BinaryWebSocketFrame(frameFinalFlag, frameRsv, payloadBuffer));
+                    payloadBuffer = null;
+                    return;
+                } else if (frameOpcode == OPCODE_CONT) {
+                    out.add(new ContinuationWebSocketFrame(frameFinalFlag, frameRsv,
+                                                           payloadBuffer));
+                    payloadBuffer = null;
+                    return;
+                } else {
+                    throw new UnsupportedOperationException("Cannot decode web socket frame with opcode: "
+                                                            + frameOpcode);
+                }
+            } finally {
+                if (payloadBuffer != null) {
+                    payloadBuffer.release();
+                }
             }
+        case CORRUPT:
+            if (in.isReadable()) {
+                // If we don't keep reading Netty will throw an exception saying
+                // we can't return null if no bytes read and state not changed.
+                in.readByte();
+            }
+            return;
+        default:
+            throw new Error("Shouldn't reach here.");
+        }
     }
 
     private void unmask(ByteBuf frame) {
@@ -408,18 +418,33 @@ public class WebSocket08FrameDecoder extends ByteToMessageDecoder
         }
     }
 
-    private void protocolViolation(ChannelHandlerContext ctx, String reason) {
-        protocolViolation(ctx, new CorruptedFrameException(reason));
+    private void protocolViolation(ChannelHandlerContext ctx, ByteBuf in, String reason) {
+        protocolViolation(ctx, in, WebSocketCloseStatus.PROTOCOL_ERROR, reason);
+    }
+
+    private void protocolViolation(ChannelHandlerContext ctx, ByteBuf in, WebSocketCloseStatus status, String reason) {
+        protocolViolation(ctx, in, new CorruptedWebSocketFrameException(status, reason));
     }
 
-    private void protocolViolation(ChannelHandlerContext ctx, CorruptedFrameException ex) {
+    private void protocolViolation(ChannelHandlerContext ctx, ByteBuf in, CorruptedWebSocketFrameException ex) {
         state = State.CORRUPT;
-        if (ctx.channel().isActive()) {
+        int readableBytes = in.readableBytes();
+        if (readableBytes > 0) {
+            // Fix for memory leak, caused by ByteToMessageDecoder#channelRead:
+            // buffer 'cumulation' is released ONLY when no more readable bytes available.
+            in.skipBytes(readableBytes);
+        }
+        if (ctx.channel().isActive() && config.closeOnProtocolViolation()) {
             Object closeMessage;
             if (receivedClosingHandshake) {
                 closeMessage = Unpooled.EMPTY_BUFFER;
             } else {
-                closeMessage = new CloseWebSocketFrame(1002, null);
+                WebSocketCloseStatus closeStatus = ex.closeStatus();
+                String reasonText = ex.getMessage();
+                if (reasonText == null) {
+                    reasonText = closeStatus.reasonText();
+                }
+                closeMessage = new CloseWebSocketFrame(closeStatus, reasonText);
             }
             ctx.writeAndFlush(closeMessage).addListener(ChannelFutureListener.CLOSE);
         }
@@ -441,7 +466,7 @@ public class WebSocket08FrameDecoder extends ByteToMessageDecoder
             return;
         }
         if (buffer.readableBytes() == 1) {
-            protocolViolation(ctx, "Invalid close frame body");
+            protocolViolation(ctx, buffer, WebSocketCloseStatus.INVALID_PAYLOAD_DATA, "Invalid close frame body");
         }
 
         // Save reader index
@@ -450,17 +475,16 @@ public class WebSocket08FrameDecoder extends ByteToMessageDecoder
 
         // Must have 2 byte integer within the valid range
         int statusCode = buffer.readShort();
-        if (statusCode >= 0 && statusCode <= 999 || statusCode >= 1004 && statusCode <= 1006
-                || statusCode >= 1015 && statusCode <= 2999) {
-            protocolViolation(ctx, "Invalid close frame getStatus code: " + statusCode);
+        if (!WebSocketCloseStatus.isValidStatusCode(statusCode)) {
+            protocolViolation(ctx, buffer, "Invalid close frame getStatus code: " + statusCode);
         }
 
         // May have UTF-8 message
         if (buffer.isReadable()) {
             try {
                 new Utf8Validator().check(buffer);
-            } catch (CorruptedFrameException ex) {
-                protocolViolation(ctx, ex);
+            } catch (CorruptedWebSocketFrameException ex) {
+                protocolViolation(ctx, buffer, ex);
             }
         }
 
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket08FrameEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket08FrameEncoder.java
index cb16953..b8ef822 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket08FrameEncoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket08FrameEncoder.java
@@ -126,8 +126,8 @@ public class WebSocket08FrameEncoder extends MessageToMessageEncoder<WebSocketFr
 
         int length = data.readableBytes();
 
-        if (logger.isDebugEnabled()) {
-            logger.debug("Encoding WebSocket Frame opCode=" + opcode + " length=" + length);
+        if (logger.isTraceEnabled()) {
+            logger.trace("Encoding WebSocket Frame opCode={} length={}", opcode, length);
         }
 
         int b0 = 0;
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket13FrameDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket13FrameDecoder.java
index 04fd682..0c4a9b1 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket13FrameDecoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocket13FrameDecoder.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012 The Netty Project
+ * Copyright 2019 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
@@ -91,6 +91,21 @@ public class WebSocket13FrameDecoder extends WebSocket08FrameDecoder {
      */
     public WebSocket13FrameDecoder(boolean expectMaskedFrames, boolean allowExtensions, int maxFramePayloadLength,
                                    boolean allowMaskMismatch) {
-        super(expectMaskedFrames, allowExtensions, maxFramePayloadLength, allowMaskMismatch);
+        this(WebSocketDecoderConfig.newBuilder()
+            .expectMaskedFrames(expectMaskedFrames)
+            .allowExtensions(allowExtensions)
+            .maxFramePayloadLength(maxFramePayloadLength)
+            .allowMaskMismatch(allowMaskMismatch)
+            .build());
+    }
+
+    /**
+     * Constructor
+     *
+     * @param decoderConfig
+     *            Frames decoder configuration.
+     */
+    public WebSocket13FrameDecoder(WebSocketDecoderConfig decoderConfig) {
+        super(decoderConfig);
     }
 }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker.java
index 44c9144..08423e3 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker.java
@@ -35,21 +35,23 @@ import io.netty.handler.codec.http.HttpResponseDecoder;
 import io.netty.handler.codec.http.HttpScheme;
 import io.netty.util.NetUtil;
 import io.netty.util.ReferenceCountUtil;
-import io.netty.util.internal.ThrowableUtil;
+import io.netty.util.internal.ObjectUtil;
 
 import java.net.URI;
 import java.nio.channels.ClosedChannelException;
 import java.util.Locale;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
 
 /**
  * Base class for web socket client handshake implementations
  */
 public abstract class WebSocketClientHandshaker {
-    private static final ClosedChannelException CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace(
-            new ClosedChannelException(), WebSocketClientHandshaker.class, "processHandshake(...)");
 
     private static final String HTTP_SCHEME_PREFIX = HttpScheme.HTTP + "://";
     private static final String HTTPS_SCHEME_PREFIX = HttpScheme.HTTPS + "://";
+    protected static final int DEFAULT_FORCE_CLOSE_TIMEOUT_MILLIS = 10000;
 
     private final URI uri;
 
@@ -57,6 +59,15 @@ public abstract class WebSocketClientHandshaker {
 
     private volatile boolean handshakeComplete;
 
+    private volatile long forceCloseTimeoutMillis = DEFAULT_FORCE_CLOSE_TIMEOUT_MILLIS;
+
+    private volatile int forceCloseInit;
+
+    private static final AtomicIntegerFieldUpdater<WebSocketClientHandshaker> FORCE_CLOSE_INIT_UPDATER =
+            AtomicIntegerFieldUpdater.newUpdater(WebSocketClientHandshaker.class, "forceCloseInit");
+
+    private volatile boolean forceCloseComplete;
+
     private final String expectedSubprotocol;
 
     private volatile String actualSubprotocol;
@@ -65,6 +76,8 @@ public abstract class WebSocketClientHandshaker {
 
     private final int maxFramePayloadLength;
 
+    private final boolean absoluteUpgradeUrl;
+
     /**
      * Base constructor
      *
@@ -82,11 +95,62 @@ public abstract class WebSocketClientHandshaker {
      */
     protected WebSocketClientHandshaker(URI uri, WebSocketVersion version, String subprotocol,
                                         HttpHeaders customHeaders, int maxFramePayloadLength) {
+        this(uri, version, subprotocol, customHeaders, maxFramePayloadLength, DEFAULT_FORCE_CLOSE_TIMEOUT_MILLIS);
+    }
+
+    /**
+     * Base constructor
+     *
+     * @param uri
+     *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
+     *            sent to this URL.
+     * @param version
+     *            Version of web socket specification to use to connect to the server
+     * @param subprotocol
+     *            Sub protocol request sent to the server.
+     * @param customHeaders
+     *            Map of custom headers to add to the client request
+     * @param maxFramePayloadLength
+     *            Maximum length of a frame's payload
+     * @param forceCloseTimeoutMillis
+     *            Close the connection if it was not closed by the server after timeout specified
+     */
+    protected WebSocketClientHandshaker(URI uri, WebSocketVersion version, String subprotocol,
+                                        HttpHeaders customHeaders, int maxFramePayloadLength,
+                                        long forceCloseTimeoutMillis) {
+        this(uri, version, subprotocol, customHeaders, maxFramePayloadLength, forceCloseTimeoutMillis, false);
+    }
+
+    /**
+     * Base constructor
+     *
+     * @param uri
+     *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
+     *            sent to this URL.
+     * @param version
+     *            Version of web socket specification to use to connect to the server
+     * @param subprotocol
+     *            Sub protocol request sent to the server.
+     * @param customHeaders
+     *            Map of custom headers to add to the client request
+     * @param maxFramePayloadLength
+     *            Maximum length of a frame's payload
+     * @param forceCloseTimeoutMillis
+     *            Close the connection if it was not closed by the server after timeout specified
+     * @param  absoluteUpgradeUrl
+     *            Use an absolute url for the Upgrade request, typically when connecting through an HTTP proxy over
+     *            clear HTTP
+     */
+    protected WebSocketClientHandshaker(URI uri, WebSocketVersion version, String subprotocol,
+                                        HttpHeaders customHeaders, int maxFramePayloadLength,
+                                        long forceCloseTimeoutMillis, boolean absoluteUpgradeUrl) {
         this.uri = uri;
         this.version = version;
         expectedSubprotocol = subprotocol;
         this.customHeaders = customHeaders;
         this.maxFramePayloadLength = maxFramePayloadLength;
+        this.forceCloseTimeoutMillis = forceCloseTimeoutMillis;
+        this.absoluteUpgradeUrl = absoluteUpgradeUrl;
     }
 
     /**
@@ -140,6 +204,29 @@ public abstract class WebSocketClientHandshaker {
         this.actualSubprotocol = actualSubprotocol;
     }
 
+    public long forceCloseTimeoutMillis() {
+        return forceCloseTimeoutMillis;
+    }
+
+    /**
+     * Flag to indicate if the closing handshake was initiated because of timeout.
+     * For testing only.
+     */
+    protected boolean isForceCloseComplete() {
+        return forceCloseComplete;
+    }
+
+    /**
+     * Sets timeout to close the connection if it was not closed by the server.
+     *
+     * @param forceCloseTimeoutMillis
+     *            Close the connection if it was not closed by the server after timeout specified
+     */
+    public WebSocketClientHandshaker setForceCloseTimeoutMillis(long forceCloseTimeoutMillis) {
+        this.forceCloseTimeoutMillis = forceCloseTimeoutMillis;
+        return this;
+    }
+
     /**
      * Begins the opening handshake
      *
@@ -147,9 +234,7 @@ public abstract class WebSocketClientHandshaker {
      *            Channel
      */
     public ChannelFuture handshake(Channel channel) {
-        if (channel == null) {
-            throw new NullPointerException("channel");
-        }
+        ObjectUtil.checkNotNull(channel, "channel");
         return handshake(channel, channel.newPromise());
     }
 
@@ -162,18 +247,19 @@ public abstract class WebSocketClientHandshaker {
      *            the {@link ChannelPromise} to be notified when the opening handshake is sent
      */
     public final ChannelFuture handshake(Channel channel, final ChannelPromise promise) {
-        FullHttpRequest request =  newHandshakeRequest();
-
-        HttpResponseDecoder decoder = channel.pipeline().get(HttpResponseDecoder.class);
+        ChannelPipeline pipeline = channel.pipeline();
+        HttpResponseDecoder decoder = pipeline.get(HttpResponseDecoder.class);
         if (decoder == null) {
-            HttpClientCodec codec = channel.pipeline().get(HttpClientCodec.class);
+            HttpClientCodec codec = pipeline.get(HttpClientCodec.class);
             if (codec == null) {
                promise.setFailure(new IllegalStateException("ChannelPipeline does not contain " +
-                       "a HttpResponseDecoder or HttpClientCodec"));
+                       "an HttpResponseDecoder or HttpClientCodec"));
                return promise;
             }
         }
 
+        FullHttpRequest request = newHandshakeRequest();
+
         channel.writeAndFlush(request).addListener(new ChannelFutureListener() {
             @Override
             public void operationComplete(ChannelFuture future) {
@@ -185,7 +271,7 @@ public abstract class WebSocketClientHandshaker {
                     }
                     if (ctx == null) {
                         promise.setFailure(new IllegalStateException("ChannelPipeline does not contain " +
-                                "a HttpRequestEncoder or HttpClientCodec"));
+                                "an HttpRequestEncoder or HttpClientCodec"));
                         return;
                     }
                     p.addAfter(ctx.name(), "ws-encoder", newWebSocketEncoder());
@@ -263,7 +349,7 @@ public abstract class WebSocketClientHandshaker {
             ctx = p.context(HttpClientCodec.class);
             if (ctx == null) {
                 throw new IllegalStateException("ChannelPipeline does not contain " +
-                        "a HttpRequestEncoder or HttpClientCodec");
+                        "an HttpRequestEncoder or HttpClientCodec");
             }
             final HttpClientCodec codec =  (HttpClientCodec) ctx.handler();
             // Remove the encoder part of the codec as the user may start writing frames after this method returns.
@@ -342,7 +428,7 @@ public abstract class WebSocketClientHandshaker {
                 ctx = p.context(HttpClientCodec.class);
                 if (ctx == null) {
                     return promise.setFailure(new IllegalStateException("ChannelPipeline does not contain " +
-                            "a HttpResponseDecoder or HttpClientCodec"));
+                            "an HttpResponseDecoder or HttpClientCodec"));
                 }
             }
             // Add aggregator and ensure we feed the HttpResponse so it is aggregated. A limit of 8192 should be more
@@ -374,7 +460,9 @@ public abstract class WebSocketClientHandshaker {
                 @Override
                 public void channelInactive(ChannelHandlerContext ctx) throws Exception {
                     // Fail promise if Channel was closed
-                    promise.tryFailure(CLOSED_CHANNEL_EXCEPTION);
+                    if (!promise.isDone()) {
+                        promise.tryFailure(new ClosedChannelException());
+                    }
                     ctx.fireChannelInactive();
                 }
             });
@@ -411,9 +499,7 @@ public abstract class WebSocketClientHandshaker {
      *            Closing Frame that was received
      */
     public ChannelFuture close(Channel channel, CloseWebSocketFrame frame) {
-        if (channel == null) {
-            throw new NullPointerException("channel");
-        }
+        ObjectUtil.checkNotNull(channel, "channel");
         return close(channel, frame, channel.newPromise());
     }
 
@@ -428,23 +514,61 @@ public abstract class WebSocketClientHandshaker {
      *            the {@link ChannelPromise} to be notified when the closing handshake is done
      */
     public ChannelFuture close(Channel channel, CloseWebSocketFrame frame, ChannelPromise promise) {
-        if (channel == null) {
-            throw new NullPointerException("channel");
+        ObjectUtil.checkNotNull(channel, "channel");
+        channel.writeAndFlush(frame, promise);
+        applyForceCloseTimeout(channel, promise);
+        return promise;
+    }
+
+    private void applyForceCloseTimeout(final Channel channel, ChannelFuture flushFuture) {
+        final long forceCloseTimeoutMillis = this.forceCloseTimeoutMillis;
+        final WebSocketClientHandshaker handshaker = this;
+        if (forceCloseTimeoutMillis <= 0 || !channel.isActive() || forceCloseInit != 0) {
+            return;
         }
-        return channel.writeAndFlush(frame, promise);
+
+        flushFuture.addListener(new ChannelFutureListener() {
+            @Override
+            public void operationComplete(ChannelFuture future) throws Exception {
+                // If flush operation failed, there is no reason to expect
+                // a server to receive CloseFrame. Thus this should be handled
+                // by the application separately.
+                // Also, close might be called twice from different threads.
+                if (future.isSuccess() && channel.isActive() &&
+                        FORCE_CLOSE_INIT_UPDATER.compareAndSet(handshaker, 0, 1)) {
+                    final Future<?> forceCloseFuture = channel.eventLoop().schedule(new Runnable() {
+                        @Override
+                        public void run() {
+                            if (channel.isActive()) {
+                                channel.close();
+                                forceCloseComplete = true;
+                            }
+                        }
+                    }, forceCloseTimeoutMillis, TimeUnit.MILLISECONDS);
+
+                    channel.closeFuture().addListener(new ChannelFutureListener() {
+                        @Override
+                        public void operationComplete(ChannelFuture future) throws Exception {
+                            forceCloseFuture.cancel(false);
+                        }
+                    });
+                }
+            }
+        });
     }
 
     /**
      * Return the constructed raw path for the give {@link URI}.
      */
-    static String rawPath(URI wsURL) {
-        String path = wsURL.getRawPath();
-        String query = wsURL.getRawQuery();
-        if (query != null && !query.isEmpty()) {
-            path = path + '?' + query;
+    protected String upgradeUrl(URI wsURL) {
+        if (absoluteUpgradeUrl) {
+            return wsURL.toString();
         }
 
-        return path == null || path.isEmpty() ? "/" : path;
+        String path = wsURL.getRawPath();
+        path = path == null || path.isEmpty() ? "/" : path;
+        String query = wsURL.getRawQuery();
+        return query != null && !query.isEmpty() ? path + '?' + query : path;
     }
 
     static CharSequence websocketHostValue(URI wsURL) {
@@ -453,14 +577,15 @@ public abstract class WebSocketClientHandshaker {
             return wsURL.getHost();
         }
         String host = wsURL.getHost();
+        String scheme = wsURL.getScheme();
         if (port == HttpScheme.HTTP.port()) {
-            return HttpScheme.HTTP.name().contentEquals(wsURL.getScheme())
-                    || WebSocketScheme.WS.name().contentEquals(wsURL.getScheme()) ?
+            return HttpScheme.HTTP.name().contentEquals(scheme)
+                    || WebSocketScheme.WS.name().contentEquals(scheme) ?
                     host : NetUtil.toSocketAddressString(host, port);
         }
         if (port == HttpScheme.HTTPS.port()) {
-            return HttpScheme.HTTPS.name().contentEquals(wsURL.getScheme())
-                    || WebSocketScheme.WSS.name().contentEquals(wsURL.getScheme()) ?
+            return HttpScheme.HTTPS.name().contentEquals(scheme)
+                    || WebSocketScheme.WSS.name().contentEquals(scheme) ?
                     host : NetUtil.toSocketAddressString(host, port);
         }
 
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker00.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker00.java
index f026263..ef4b6a3 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker00.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker00.java
@@ -48,7 +48,7 @@ public class WebSocketClientHandshaker00 extends WebSocketClientHandshaker {
     private ByteBuf expectedChallengeResponseBytes;
 
     /**
-     * Constructor specifying the destination web socket location and version to initiate
+     * Creates a new instance with the specified destination WebSocket location and version to initiate.
      *
      * @param webSocketURL
      *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
@@ -64,7 +64,58 @@ public class WebSocketClientHandshaker00 extends WebSocketClientHandshaker {
      */
     public WebSocketClientHandshaker00(URI webSocketURL, WebSocketVersion version, String subprotocol,
             HttpHeaders customHeaders, int maxFramePayloadLength) {
-        super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength);
+        this(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength,
+                DEFAULT_FORCE_CLOSE_TIMEOUT_MILLIS);
+    }
+
+    /**
+     * Creates a new instance with the specified destination WebSocket location and version to initiate.
+     *
+     * @param webSocketURL
+     *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
+     *            sent to this URL.
+     * @param version
+     *            Version of web socket specification to use to connect to the server
+     * @param subprotocol
+     *            Sub protocol request sent to the server.
+     * @param customHeaders
+     *            Map of custom headers to add to the client request
+     * @param maxFramePayloadLength
+     *            Maximum length of a frame's payload
+     * @param forceCloseTimeoutMillis
+     *            Close the connection if it was not closed by the server after timeout specified
+     */
+    public WebSocketClientHandshaker00(URI webSocketURL, WebSocketVersion version, String subprotocol,
+                                       HttpHeaders customHeaders, int maxFramePayloadLength,
+                                       long forceCloseTimeoutMillis) {
+        this(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength, forceCloseTimeoutMillis, false);
+    }
+
+    /**
+     * Creates a new instance with the specified destination WebSocket location and version to initiate.
+     *
+     * @param webSocketURL
+     *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
+     *            sent to this URL.
+     * @param version
+     *            Version of web socket specification to use to connect to the server
+     * @param subprotocol
+     *            Sub protocol request sent to the server.
+     * @param customHeaders
+     *            Map of custom headers to add to the client request
+     * @param maxFramePayloadLength
+     *            Maximum length of a frame's payload
+     * @param forceCloseTimeoutMillis
+     *            Close the connection if it was not closed by the server after timeout specified
+     * @param  absoluteUpgradeUrl
+     *            Use an absolute url for the Upgrade request, typically when connecting through an HTTP proxy over
+     *            clear HTTP
+     */
+    WebSocketClientHandshaker00(URI webSocketURL, WebSocketVersion version, String subprotocol,
+                                HttpHeaders customHeaders, int maxFramePayloadLength,
+                                long forceCloseTimeoutMillis, boolean absoluteUpgradeUrl) {
+        super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength, forceCloseTimeoutMillis,
+                absoluteUpgradeUrl);
     }
 
     /**
@@ -124,12 +175,11 @@ public class WebSocketClientHandshaker00 extends WebSocketClientHandshaker {
         System.arraycopy(key3, 0, challenge, 8, 8);
         expectedChallengeResponseBytes = Unpooled.wrappedBuffer(WebSocketUtil.md5(challenge));
 
-        // Get path
         URI wsURL = uri();
-        String path = rawPath(wsURL);
 
         // Format request
-        FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, path);
+        FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, upgradeUrl(wsURL),
+                Unpooled.wrappedBuffer(key3));
         HttpHeaders headers = request.headers();
 
         if (customHeaders != null) {
@@ -139,10 +189,13 @@ public class WebSocketClientHandshaker00 extends WebSocketClientHandshaker {
         headers.set(HttpHeaderNames.UPGRADE, WEBSOCKET)
                .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE)
                .set(HttpHeaderNames.HOST, websocketHostValue(wsURL))
-               .set(HttpHeaderNames.ORIGIN, websocketOriginValue(wsURL))
                .set(HttpHeaderNames.SEC_WEBSOCKET_KEY1, key1)
                .set(HttpHeaderNames.SEC_WEBSOCKET_KEY2, key2);
 
+        if (!headers.contains(HttpHeaderNames.ORIGIN)) {
+            headers.set(HttpHeaderNames.ORIGIN, websocketOriginValue(wsURL));
+        }
+
         String expectedSubprotocol = expectedSubprotocol();
         if (expectedSubprotocol != null && !expectedSubprotocol.isEmpty()) {
             headers.set(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL, expectedSubprotocol);
@@ -151,7 +204,6 @@ public class WebSocketClientHandshaker00 extends WebSocketClientHandshaker {
         // Set Content-Length to workaround some known defect.
         // See also: http://www.ietf.org/mail-archive/web/hybi/current/msg02149.html
         headers.set(HttpHeaderNames.CONTENT_LENGTH, key3.length);
-        request.content().writeBytes(key3);
         return request;
     }
 
@@ -243,4 +295,11 @@ public class WebSocketClientHandshaker00 extends WebSocketClientHandshaker {
     protected WebSocketFrameEncoder newWebSocketEncoder() {
         return new WebSocket00FrameEncoder();
     }
+
+    @Override
+    public WebSocketClientHandshaker00 setForceCloseTimeoutMillis(long forceCloseTimeoutMillis) {
+        super.setForceCloseTimeoutMillis(forceCloseTimeoutMillis);
+        return this;
+    }
+
 }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker07.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker07.java
index 4632a4a..779b714 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker07.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker07.java
@@ -15,6 +15,7 @@
  */
 package io.netty.handler.codec.http.websocketx;
 
+import io.netty.buffer.Unpooled;
 import io.netty.handler.codec.http.DefaultFullHttpRequest;
 import io.netty.handler.codec.http.FullHttpRequest;
 import io.netty.handler.codec.http.FullHttpResponse;
@@ -94,10 +95,81 @@ public class WebSocketClientHandshaker07 extends WebSocketClientHandshaker {
      *            When set to true, frames which are not masked properly according to the standard will still be
      *            accepted.
      */
+    public WebSocketClientHandshaker07(URI webSocketURL, WebSocketVersion version, String subprotocol,
+                                       boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
+                                       boolean performMasking, boolean allowMaskMismatch) {
+        this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, performMasking,
+                allowMaskMismatch, DEFAULT_FORCE_CLOSE_TIMEOUT_MILLIS);
+    }
+
+    /**
+     * Creates a new instance.
+     *
+     * @param webSocketURL
+     *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
+     *            sent to this URL.
+     * @param version
+     *            Version of web socket specification to use to connect to the server
+     * @param subprotocol
+     *            Sub protocol request sent to the server.
+     * @param allowExtensions
+     *            Allow extensions to be used in the reserved bits of the web socket frame
+     * @param customHeaders
+     *            Map of custom headers to add to the client request
+     * @param maxFramePayloadLength
+     *            Maximum length of a frame's payload
+     * @param performMasking
+     *            Whether to mask all written websocket frames. This must be set to true in order to be fully compatible
+     *            with the websocket specifications. Client applications that communicate with a non-standard server
+     *            which doesn't require masking might set this to false to achieve a higher performance.
+     * @param allowMaskMismatch
+     *            When set to true, frames which are not masked properly according to the standard will still be
+     *            accepted
+     * @param forceCloseTimeoutMillis
+     *            Close the connection if it was not closed by the server after timeout specified.
+     */
     public WebSocketClientHandshaker07(URI webSocketURL, WebSocketVersion version, String subprotocol,
             boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
-            boolean performMasking, boolean allowMaskMismatch) {
-        super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength);
+            boolean performMasking, boolean allowMaskMismatch, long forceCloseTimeoutMillis) {
+        this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, performMasking,
+                allowMaskMismatch, forceCloseTimeoutMillis, false);
+    }
+
+    /**
+     * Creates a new instance.
+     *
+     * @param webSocketURL
+     *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
+     *            sent to this URL.
+     * @param version
+     *            Version of web socket specification to use to connect to the server
+     * @param subprotocol
+     *            Sub protocol request sent to the server.
+     * @param allowExtensions
+     *            Allow extensions to be used in the reserved bits of the web socket frame
+     * @param customHeaders
+     *            Map of custom headers to add to the client request
+     * @param maxFramePayloadLength
+     *            Maximum length of a frame's payload
+     * @param performMasking
+     *            Whether to mask all written websocket frames. This must be set to true in order to be fully compatible
+     *            with the websocket specifications. Client applications that communicate with a non-standard server
+     *            which doesn't require masking might set this to false to achieve a higher performance.
+     * @param allowMaskMismatch
+     *            When set to true, frames which are not masked properly according to the standard will still be
+     *            accepted
+     * @param forceCloseTimeoutMillis
+     *            Close the connection if it was not closed by the server after timeout specified.
+     * @param  absoluteUpgradeUrl
+     *            Use an absolute url for the Upgrade request, typically when connecting through an HTTP proxy over
+     *            clear HTTP
+     */
+    WebSocketClientHandshaker07(URI webSocketURL, WebSocketVersion version, String subprotocol,
+                                boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
+                                boolean performMasking, boolean allowMaskMismatch, long forceCloseTimeoutMillis,
+                                boolean absoluteUpgradeUrl) {
+        super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength, forceCloseTimeoutMillis,
+                absoluteUpgradeUrl);
         this.allowExtensions = allowExtensions;
         this.performMasking = performMasking;
         this.allowMaskMismatch = allowMaskMismatch;
@@ -123,9 +195,7 @@ public class WebSocketClientHandshaker07 extends WebSocketClientHandshaker {
      */
     @Override
     protected FullHttpRequest newHandshakeRequest() {
-        // Get path
         URI wsURL = uri();
-        String path = rawPath(wsURL);
 
         // Get 16 bit nonce and base 64 encode it
         byte[] nonce = WebSocketUtil.randomBytes(16);
@@ -142,25 +212,36 @@ public class WebSocketClientHandshaker07 extends WebSocketClientHandshaker {
         }
 
         // Format request
-        FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, path);
+        FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, upgradeUrl(wsURL),
+                Unpooled.EMPTY_BUFFER);
         HttpHeaders headers = request.headers();
 
         if (customHeaders != null) {
             headers.add(customHeaders);
+            if (!headers.contains(HttpHeaderNames.HOST)) {
+                // Only add HOST header if customHeaders did not contain it.
+                //
+                // See https://github.com/netty/netty/issues/10101
+                headers.set(HttpHeaderNames.HOST, websocketHostValue(wsURL));
+            }
+        } else {
+            headers.set(HttpHeaderNames.HOST, websocketHostValue(wsURL));
         }
 
         headers.set(HttpHeaderNames.UPGRADE, HttpHeaderValues.WEBSOCKET)
                .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE)
-               .set(HttpHeaderNames.SEC_WEBSOCKET_KEY, key)
-               .set(HttpHeaderNames.HOST, websocketHostValue(wsURL))
-               .set(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN, websocketOriginValue(wsURL));
+               .set(HttpHeaderNames.SEC_WEBSOCKET_KEY, key);
+
+        if (!headers.contains(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN)) {
+            headers.set(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN, websocketOriginValue(wsURL));
+        }
 
         String expectedSubprotocol = expectedSubprotocol();
         if (expectedSubprotocol != null && !expectedSubprotocol.isEmpty()) {
             headers.set(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL, expectedSubprotocol);
         }
 
-        headers.set(HttpHeaderNames.SEC_WEBSOCKET_VERSION, "7");
+        headers.set(HttpHeaderNames.SEC_WEBSOCKET_VERSION, version().toAsciiString());
         return request;
     }
 
@@ -216,4 +297,11 @@ public class WebSocketClientHandshaker07 extends WebSocketClientHandshaker {
     protected WebSocketFrameEncoder newWebSocketEncoder() {
         return new WebSocket07FrameEncoder(performMasking);
     }
+
+    @Override
+    public WebSocketClientHandshaker07 setForceCloseTimeoutMillis(long forceCloseTimeoutMillis) {
+        super.setForceCloseTimeoutMillis(forceCloseTimeoutMillis);
+        return this;
+    }
+
 }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker08.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker08.java
index 1a11aa6..c28efd0 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker08.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker08.java
@@ -15,6 +15,7 @@
  */
 package io.netty.handler.codec.http.websocketx;
 
+import io.netty.buffer.Unpooled;
 import io.netty.handler.codec.http.DefaultFullHttpRequest;
 import io.netty.handler.codec.http.FullHttpRequest;
 import io.netty.handler.codec.http.FullHttpResponse;
@@ -68,7 +69,8 @@ public class WebSocketClientHandshaker08 extends WebSocketClientHandshaker {
      */
     public WebSocketClientHandshaker08(URI webSocketURL, WebSocketVersion version, String subprotocol,
                                        boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength) {
-        this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, true, false);
+        this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, true,
+                false, DEFAULT_FORCE_CLOSE_TIMEOUT_MILLIS);
     }
 
     /**
@@ -93,12 +95,83 @@ public class WebSocketClientHandshaker08 extends WebSocketClientHandshaker {
      *            which doesn't require masking might set this to false to achieve a higher performance.
      * @param allowMaskMismatch
      *            When set to true, frames which are not masked properly according to the standard will still be
-     *            accepted.
+     *            accepted
+     */
+    public WebSocketClientHandshaker08(URI webSocketURL, WebSocketVersion version, String subprotocol,
+                                       boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
+                                       boolean performMasking, boolean allowMaskMismatch) {
+        this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, performMasking,
+                allowMaskMismatch, DEFAULT_FORCE_CLOSE_TIMEOUT_MILLIS);
+    }
+
+    /**
+     * Creates a new instance.
+     *
+     * @param webSocketURL
+     *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
+     *            sent to this URL.
+     * @param version
+     *            Version of web socket specification to use to connect to the server
+     * @param subprotocol
+     *            Sub protocol request sent to the server.
+     * @param allowExtensions
+     *            Allow extensions to be used in the reserved bits of the web socket frame
+     * @param customHeaders
+     *            Map of custom headers to add to the client request
+     * @param maxFramePayloadLength
+     *            Maximum length of a frame's payload
+     * @param performMasking
+     *            Whether to mask all written websocket frames. This must be set to true in order to be fully compatible
+     *            with the websocket specifications. Client applications that communicate with a non-standard server
+     *            which doesn't require masking might set this to false to achieve a higher performance.
+     * @param allowMaskMismatch
+     *            When set to true, frames which are not masked properly according to the standard will still be
+     *            accepted
+     * @param forceCloseTimeoutMillis
+     *            Close the connection if it was not closed by the server after timeout specified.
      */
     public WebSocketClientHandshaker08(URI webSocketURL, WebSocketVersion version, String subprotocol,
             boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
-            boolean performMasking, boolean allowMaskMismatch) {
-        super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength);
+            boolean performMasking, boolean allowMaskMismatch, long forceCloseTimeoutMillis) {
+        this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, performMasking,
+                allowMaskMismatch, forceCloseTimeoutMillis, false);
+    }
+
+    /**
+     * Creates a new instance.
+     *
+     * @param webSocketURL
+     *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
+     *            sent to this URL.
+     * @param version
+     *            Version of web socket specification to use to connect to the server
+     * @param subprotocol
+     *            Sub protocol request sent to the server.
+     * @param allowExtensions
+     *            Allow extensions to be used in the reserved bits of the web socket frame
+     * @param customHeaders
+     *            Map of custom headers to add to the client request
+     * @param maxFramePayloadLength
+     *            Maximum length of a frame's payload
+     * @param performMasking
+     *            Whether to mask all written websocket frames. This must be set to true in order to be fully compatible
+     *            with the websocket specifications. Client applications that communicate with a non-standard server
+     *            which doesn't require masking might set this to false to achieve a higher performance.
+     * @param allowMaskMismatch
+     *            When set to true, frames which are not masked properly according to the standard will still be
+     *            accepted
+     * @param forceCloseTimeoutMillis
+     *            Close the connection if it was not closed by the server after timeout specified.
+     * @param  absoluteUpgradeUrl
+     *            Use an absolute url for the Upgrade request, typically when connecting through an HTTP proxy over
+     *            clear HTTP
+     */
+    WebSocketClientHandshaker08(URI webSocketURL, WebSocketVersion version, String subprotocol,
+                                boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
+                                boolean performMasking, boolean allowMaskMismatch, long forceCloseTimeoutMillis,
+                                boolean absoluteUpgradeUrl) {
+        super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength, forceCloseTimeoutMillis,
+                absoluteUpgradeUrl);
         this.allowExtensions = allowExtensions;
         this.performMasking = performMasking;
         this.allowMaskMismatch = allowMaskMismatch;
@@ -124,9 +197,7 @@ public class WebSocketClientHandshaker08 extends WebSocketClientHandshaker {
      */
     @Override
     protected FullHttpRequest newHandshakeRequest() {
-        // Get path
         URI wsURL = uri();
-        String path = rawPath(wsURL);
 
         // Get 16 bit nonce and base 64 encode it
         byte[] nonce = WebSocketUtil.randomBytes(16);
@@ -143,25 +214,36 @@ public class WebSocketClientHandshaker08 extends WebSocketClientHandshaker {
         }
 
         // Format request
-        FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, path);
+        FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, upgradeUrl(wsURL),
+                Unpooled.EMPTY_BUFFER);
         HttpHeaders headers = request.headers();
 
         if (customHeaders != null) {
             headers.add(customHeaders);
+            if (!headers.contains(HttpHeaderNames.HOST)) {
+                // Only add HOST header if customHeaders did not contain it.
+                //
+                // See https://github.com/netty/netty/issues/10101
+                headers.set(HttpHeaderNames.HOST, websocketHostValue(wsURL));
+            }
+        } else {
+            headers.set(HttpHeaderNames.HOST, websocketHostValue(wsURL));
         }
 
         headers.set(HttpHeaderNames.UPGRADE, HttpHeaderValues.WEBSOCKET)
                .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE)
-               .set(HttpHeaderNames.SEC_WEBSOCKET_KEY, key)
-               .set(HttpHeaderNames.HOST, websocketHostValue(wsURL))
-               .set(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN, websocketOriginValue(wsURL));
+               .set(HttpHeaderNames.SEC_WEBSOCKET_KEY, key);
+
+        if (!headers.contains(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN)) {
+            headers.set(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN, websocketOriginValue(wsURL));
+        }
 
         String expectedSubprotocol = expectedSubprotocol();
         if (expectedSubprotocol != null && !expectedSubprotocol.isEmpty()) {
             headers.set(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL, expectedSubprotocol);
         }
 
-        headers.set(HttpHeaderNames.SEC_WEBSOCKET_VERSION, "8");
+        headers.set(HttpHeaderNames.SEC_WEBSOCKET_VERSION, version().toAsciiString());
         return request;
     }
 
@@ -217,4 +299,11 @@ public class WebSocketClientHandshaker08 extends WebSocketClientHandshaker {
     protected WebSocketFrameEncoder newWebSocketEncoder() {
         return new WebSocket08FrameEncoder(performMasking);
     }
+
+    @Override
+    public WebSocketClientHandshaker08 setForceCloseTimeoutMillis(long forceCloseTimeoutMillis) {
+        super.setForceCloseTimeoutMillis(forceCloseTimeoutMillis);
+        return this;
+    }
+
 }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker13.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker13.java
index 808f7fc..2d061d6 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker13.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker13.java
@@ -15,6 +15,7 @@
  */
 package io.netty.handler.codec.http.websocketx;
 
+import io.netty.buffer.Unpooled;
 import io.netty.handler.codec.http.DefaultFullHttpRequest;
 import io.netty.handler.codec.http.FullHttpRequest;
 import io.netty.handler.codec.http.FullHttpResponse;
@@ -68,7 +69,8 @@ public class WebSocketClientHandshaker13 extends WebSocketClientHandshaker {
      */
     public WebSocketClientHandshaker13(URI webSocketURL, WebSocketVersion version, String subprotocol,
                                        boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength) {
-        this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, true, false);
+        this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength,
+                true, false);
     }
 
     /**
@@ -98,7 +100,79 @@ public class WebSocketClientHandshaker13 extends WebSocketClientHandshaker {
     public WebSocketClientHandshaker13(URI webSocketURL, WebSocketVersion version, String subprotocol,
             boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
             boolean performMasking, boolean allowMaskMismatch) {
-        super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength);
+        this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength,
+                performMasking, allowMaskMismatch, DEFAULT_FORCE_CLOSE_TIMEOUT_MILLIS);
+    }
+
+    /**
+     * Creates a new instance.
+     *
+     * @param webSocketURL
+     *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
+     *            sent to this URL.
+     * @param version
+     *            Version of web socket specification to use to connect to the server
+     * @param subprotocol
+     *            Sub protocol request sent to the server.
+     * @param allowExtensions
+     *            Allow extensions to be used in the reserved bits of the web socket frame
+     * @param customHeaders
+     *            Map of custom headers to add to the client request
+     * @param maxFramePayloadLength
+     *            Maximum length of a frame's payload
+     * @param performMasking
+     *            Whether to mask all written websocket frames. This must be set to true in order to be fully compatible
+     *            with the websocket specifications. Client applications that communicate with a non-standard server
+     *            which doesn't require masking might set this to false to achieve a higher performance.
+     * @param allowMaskMismatch
+     *            When set to true, frames which are not masked properly according to the standard will still be
+     *            accepted
+     * @param forceCloseTimeoutMillis
+     *            Close the connection if it was not closed by the server after timeout specified.
+     */
+    public WebSocketClientHandshaker13(URI webSocketURL, WebSocketVersion version, String subprotocol,
+                                       boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
+                                       boolean performMasking, boolean allowMaskMismatch,
+                                       long forceCloseTimeoutMillis) {
+        this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength, performMasking,
+                allowMaskMismatch, forceCloseTimeoutMillis, false);
+    }
+
+    /**
+     * Creates a new instance.
+     *
+     * @param webSocketURL
+     *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
+     *            sent to this URL.
+     * @param version
+     *            Version of web socket specification to use to connect to the server
+     * @param subprotocol
+     *            Sub protocol request sent to the server.
+     * @param allowExtensions
+     *            Allow extensions to be used in the reserved bits of the web socket frame
+     * @param customHeaders
+     *            Map of custom headers to add to the client request
+     * @param maxFramePayloadLength
+     *            Maximum length of a frame's payload
+     * @param performMasking
+     *            Whether to mask all written websocket frames. This must be set to true in order to be fully compatible
+     *            with the websocket specifications. Client applications that communicate with a non-standard server
+     *            which doesn't require masking might set this to false to achieve a higher performance.
+     * @param allowMaskMismatch
+     *            When set to true, frames which are not masked properly according to the standard will still be
+     *            accepted
+     * @param forceCloseTimeoutMillis
+     *            Close the connection if it was not closed by the server after timeout specified.
+     * @param  absoluteUpgradeUrl
+     *            Use an absolute url for the Upgrade request, typically when connecting through an HTTP proxy over
+     *            clear HTTP
+     */
+    WebSocketClientHandshaker13(URI webSocketURL, WebSocketVersion version, String subprotocol,
+                                boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
+                                boolean performMasking, boolean allowMaskMismatch,
+                                long forceCloseTimeoutMillis, boolean absoluteUpgradeUrl) {
+        super(webSocketURL, version, subprotocol, customHeaders, maxFramePayloadLength, forceCloseTimeoutMillis,
+                absoluteUpgradeUrl);
         this.allowExtensions = allowExtensions;
         this.performMasking = performMasking;
         this.allowMaskMismatch = allowMaskMismatch;
@@ -116,7 +190,7 @@ public class WebSocketClientHandshaker13 extends WebSocketClientHandshaker {
      * Upgrade: websocket
      * Connection: Upgrade
      * Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
-     * Sec-WebSocket-Origin: http://example.com
+     * Origin: http://example.com
      * Sec-WebSocket-Protocol: chat, superchat
      * Sec-WebSocket-Version: 13
      * </pre>
@@ -124,9 +198,7 @@ public class WebSocketClientHandshaker13 extends WebSocketClientHandshaker {
      */
     @Override
     protected FullHttpRequest newHandshakeRequest() {
-        // Get path
         URI wsURL = uri();
-        String path = rawPath(wsURL);
 
         // Get 16 bit nonce and base 64 encode it
         byte[] nonce = WebSocketUtil.randomBytes(16);
@@ -143,25 +215,36 @@ public class WebSocketClientHandshaker13 extends WebSocketClientHandshaker {
         }
 
         // Format request
-        FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, path);
+        FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, upgradeUrl(wsURL),
+                Unpooled.EMPTY_BUFFER);
         HttpHeaders headers = request.headers();
 
         if (customHeaders != null) {
             headers.add(customHeaders);
+            if (!headers.contains(HttpHeaderNames.HOST)) {
+                // Only add HOST header if customHeaders did not contain it.
+                //
+                // See https://github.com/netty/netty/issues/10101
+                headers.set(HttpHeaderNames.HOST, websocketHostValue(wsURL));
+            }
+        } else {
+            headers.set(HttpHeaderNames.HOST, websocketHostValue(wsURL));
         }
 
         headers.set(HttpHeaderNames.UPGRADE, HttpHeaderValues.WEBSOCKET)
                .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE)
-               .set(HttpHeaderNames.SEC_WEBSOCKET_KEY, key)
-               .set(HttpHeaderNames.HOST, websocketHostValue(wsURL))
-               .set(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN, websocketOriginValue(wsURL));
+               .set(HttpHeaderNames.SEC_WEBSOCKET_KEY, key);
+
+        if (!headers.contains(HttpHeaderNames.ORIGIN)) {
+            headers.set(HttpHeaderNames.ORIGIN, websocketOriginValue(wsURL));
+        }
 
         String expectedSubprotocol = expectedSubprotocol();
         if (expectedSubprotocol != null && !expectedSubprotocol.isEmpty()) {
             headers.set(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL, expectedSubprotocol);
         }
 
-        headers.set(HttpHeaderNames.SEC_WEBSOCKET_VERSION, "13");
+        headers.set(HttpHeaderNames.SEC_WEBSOCKET_VERSION, version().toAsciiString());
         return request;
     }
 
@@ -180,7 +263,7 @@ public class WebSocketClientHandshaker13 extends WebSocketClientHandshaker {
      *
      * @param response
      *            HTTP response returned from the server for the request sent by beginOpeningHandshake00().
-     * @throws WebSocketHandshakeException
+     * @throws WebSocketHandshakeException if handshake response is invalid.
      */
     @Override
     protected void verify(FullHttpResponse response) {
@@ -217,4 +300,11 @@ public class WebSocketClientHandshaker13 extends WebSocketClientHandshaker {
     protected WebSocketFrameEncoder newWebSocketEncoder() {
         return new WebSocket13FrameEncoder(performMasking);
     }
+
+    @Override
+    public WebSocketClientHandshaker13 setForceCloseTimeoutMillis(long forceCloseTimeoutMillis) {
+        super.setForceCloseTimeoutMillis(forceCloseTimeoutMillis);
+        return this;
+    }
+
 }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshakerFactory.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshakerFactory.java
index b07825f..01826ea 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshakerFactory.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshakerFactory.java
@@ -107,24 +107,117 @@ public final class WebSocketClientHandshakerFactory {
             URI webSocketURL, WebSocketVersion version, String subprotocol,
             boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
             boolean performMasking, boolean allowMaskMismatch) {
+        return newHandshaker(webSocketURL, version, subprotocol, allowExtensions, customHeaders,
+                maxFramePayloadLength, performMasking, allowMaskMismatch, -1);
+    }
+
+    /**
+     * Creates a new handshaker.
+     *
+     * @param webSocketURL
+     *            URL for web socket communications. e.g "ws://myhost.com/mypath".
+     *            Subsequent web socket frames will be sent to this URL.
+     * @param version
+     *            Version of web socket specification to use to connect to the server
+     * @param subprotocol
+     *            Sub protocol request sent to the server. Null if no sub-protocol support is required.
+     * @param allowExtensions
+     *            Allow extensions to be used in the reserved bits of the web socket frame
+     * @param customHeaders
+     *            Custom HTTP headers to send during the handshake
+     * @param maxFramePayloadLength
+     *            Maximum allowable frame payload length. Setting this value to your application's
+     *            requirement may reduce denial of service attacks using long data frames.
+     * @param performMasking
+     *            Whether to mask all written websocket frames. This must be set to true in order to be fully compatible
+     *            with the websocket specifications. Client applications that communicate with a non-standard server
+     *            which doesn't require masking might set this to false to achieve a higher performance.
+     * @param allowMaskMismatch
+     *            When set to true, frames which are not masked properly according to the standard will still be
+     *            accepted.
+     * @param forceCloseTimeoutMillis
+     *            Close the connection if it was not closed by the server after timeout specified
+     */
+    public static WebSocketClientHandshaker newHandshaker(
+            URI webSocketURL, WebSocketVersion version, String subprotocol,
+            boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
+            boolean performMasking, boolean allowMaskMismatch, long forceCloseTimeoutMillis) {
         if (version == V13) {
             return new WebSocketClientHandshaker13(
                     webSocketURL, V13, subprotocol, allowExtensions, customHeaders,
-                    maxFramePayloadLength, performMasking, allowMaskMismatch);
+                    maxFramePayloadLength, performMasking, allowMaskMismatch, forceCloseTimeoutMillis);
         }
         if (version == V08) {
             return new WebSocketClientHandshaker08(
                     webSocketURL, V08, subprotocol, allowExtensions, customHeaders,
-                    maxFramePayloadLength, performMasking, allowMaskMismatch);
+                    maxFramePayloadLength, performMasking, allowMaskMismatch, forceCloseTimeoutMillis);
         }
         if (version == V07) {
             return new WebSocketClientHandshaker07(
                     webSocketURL, V07, subprotocol, allowExtensions, customHeaders,
-                    maxFramePayloadLength, performMasking, allowMaskMismatch);
+                    maxFramePayloadLength, performMasking, allowMaskMismatch, forceCloseTimeoutMillis);
+        }
+        if (version == V00) {
+            return new WebSocketClientHandshaker00(
+                    webSocketURL, V00, subprotocol, customHeaders, maxFramePayloadLength, forceCloseTimeoutMillis);
+        }
+
+        throw new WebSocketHandshakeException("Protocol version " + version + " not supported.");
+    }
+
+    /**
+     * Creates a new handshaker.
+     *
+     * @param webSocketURL
+     *            URL for web socket communications. e.g "ws://myhost.com/mypath".
+     *            Subsequent web socket frames will be sent to this URL.
+     * @param version
+     *            Version of web socket specification to use to connect to the server
+     * @param subprotocol
+     *            Sub protocol request sent to the server. Null if no sub-protocol support is required.
+     * @param allowExtensions
+     *            Allow extensions to be used in the reserved bits of the web socket frame
+     * @param customHeaders
+     *            Custom HTTP headers to send during the handshake
+     * @param maxFramePayloadLength
+     *            Maximum allowable frame payload length. Setting this value to your application's
+     *            requirement may reduce denial of service attacks using long data frames.
+     * @param performMasking
+     *            Whether to mask all written websocket frames. This must be set to true in order to be fully compatible
+     *            with the websocket specifications. Client applications that communicate with a non-standard server
+     *            which doesn't require masking might set this to false to achieve a higher performance.
+     * @param allowMaskMismatch
+     *            When set to true, frames which are not masked properly according to the standard will still be
+     *            accepted.
+     * @param forceCloseTimeoutMillis
+     *            Close the connection if it was not closed by the server after timeout specified
+     * @param  absoluteUpgradeUrl
+     *            Use an absolute url for the Upgrade request, typically when connecting through an HTTP proxy over
+     *            clear HTTP
+     */
+    public static WebSocketClientHandshaker newHandshaker(
+        URI webSocketURL, WebSocketVersion version, String subprotocol,
+        boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
+        boolean performMasking, boolean allowMaskMismatch, long forceCloseTimeoutMillis, boolean absoluteUpgradeUrl) {
+        if (version == V13) {
+            return new WebSocketClientHandshaker13(
+                webSocketURL, V13, subprotocol, allowExtensions, customHeaders,
+                maxFramePayloadLength, performMasking, allowMaskMismatch, forceCloseTimeoutMillis, absoluteUpgradeUrl);
+        }
+        if (version == V08) {
+            return new WebSocketClientHandshaker08(
+                webSocketURL, V08, subprotocol, allowExtensions, customHeaders,
+                maxFramePayloadLength, performMasking, allowMaskMismatch, forceCloseTimeoutMillis, absoluteUpgradeUrl);
+        }
+        if (version == V07) {
+            return new WebSocketClientHandshaker07(
+                webSocketURL, V07, subprotocol, allowExtensions, customHeaders,
+                maxFramePayloadLength, performMasking, allowMaskMismatch, forceCloseTimeoutMillis, absoluteUpgradeUrl);
         }
         if (version == V00) {
             return new WebSocketClientHandshaker00(
-                    webSocketURL, V00, subprotocol, customHeaders, maxFramePayloadLength);
+                webSocketURL, V00, subprotocol, customHeaders,
+                maxFramePayloadLength, forceCloseTimeoutMillis, absoluteUpgradeUrl);
         }
 
         throw new WebSocketHandshakeException("Protocol version " + version + " not supported.");
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientProtocolConfig.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientProtocolConfig.java
new file mode 100644
index 0000000..5605a8d
--- /dev/null
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientProtocolConfig.java
@@ -0,0 +1,392 @@
+/*
+ * Copyright 2019 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.http.websocketx;
+
+import io.netty.handler.codec.http.EmptyHttpHeaders;
+import io.netty.handler.codec.http.HttpHeaders;
+import io.netty.handler.codec.http.websocketx.WebSocketClientProtocolHandler.ClientHandshakeStateEvent;
+import io.netty.util.internal.ObjectUtil;
+
+import java.net.URI;
+
+import static io.netty.handler.codec.http.websocketx.WebSocketServerProtocolConfig.DEFAULT_HANDSHAKE_TIMEOUT_MILLIS;
+import static io.netty.util.internal.ObjectUtil.checkPositive;
+
+/**
+ * WebSocket server configuration.
+ */
+public final class WebSocketClientProtocolConfig {
+
+    static final boolean DEFAULT_PERFORM_MASKING = true;
+    static final boolean DEFAULT_ALLOW_MASK_MISMATCH = false;
+    static final boolean DEFAULT_HANDLE_CLOSE_FRAMES = true;
+    static final boolean DEFAULT_DROP_PONG_FRAMES = true;
+
+    private final URI webSocketUri;
+    private final String subprotocol;
+    private final WebSocketVersion version;
+    private final boolean allowExtensions;
+    private final HttpHeaders customHeaders;
+    private final int maxFramePayloadLength;
+    private final boolean performMasking;
+    private final boolean allowMaskMismatch;
+    private final boolean handleCloseFrames;
+    private final WebSocketCloseStatus sendCloseFrame;
+    private final boolean dropPongFrames;
+    private final long handshakeTimeoutMillis;
+    private final long forceCloseTimeoutMillis;
+    private final boolean absoluteUpgradeUrl;
+
+    private WebSocketClientProtocolConfig(
+        URI webSocketUri,
+        String subprotocol,
+        WebSocketVersion version,
+        boolean allowExtensions,
+        HttpHeaders customHeaders,
+        int maxFramePayloadLength,
+        boolean performMasking,
+        boolean allowMaskMismatch,
+        boolean handleCloseFrames,
+        WebSocketCloseStatus sendCloseFrame,
+        boolean dropPongFrames,
+        long handshakeTimeoutMillis,
+        long forceCloseTimeoutMillis,
+        boolean absoluteUpgradeUrl
+    ) {
+        this.webSocketUri = webSocketUri;
+        this.subprotocol = subprotocol;
+        this.version = version;
+        this.allowExtensions = allowExtensions;
+        this.customHeaders = customHeaders;
+        this.maxFramePayloadLength = maxFramePayloadLength;
+        this.performMasking = performMasking;
+        this.allowMaskMismatch = allowMaskMismatch;
+        this.forceCloseTimeoutMillis = forceCloseTimeoutMillis;
+        this.handleCloseFrames = handleCloseFrames;
+        this.sendCloseFrame = sendCloseFrame;
+        this.dropPongFrames = dropPongFrames;
+        this.handshakeTimeoutMillis = checkPositive(handshakeTimeoutMillis, "handshakeTimeoutMillis");
+        this.absoluteUpgradeUrl = absoluteUpgradeUrl;
+    }
+
+    public URI webSocketUri() {
+        return webSocketUri;
+    }
+
+    public String subprotocol() {
+        return subprotocol;
+    }
+
+    public WebSocketVersion version() {
+        return version;
+    }
+
+    public boolean allowExtensions() {
+        return allowExtensions;
+    }
+
+    public HttpHeaders customHeaders() {
+        return customHeaders;
+    }
+
+    public int maxFramePayloadLength() {
+        return maxFramePayloadLength;
+    }
+
+    public boolean performMasking() {
+        return performMasking;
+    }
+
+    public boolean allowMaskMismatch() {
+        return allowMaskMismatch;
+    }
+
+    public boolean handleCloseFrames() {
+        return handleCloseFrames;
+    }
+
+    public WebSocketCloseStatus sendCloseFrame() {
+        return sendCloseFrame;
+    }
+
+    public boolean dropPongFrames() {
+        return dropPongFrames;
+    }
+
+    public long handshakeTimeoutMillis() {
+        return handshakeTimeoutMillis;
+    }
+
+    public long forceCloseTimeoutMillis() {
+        return forceCloseTimeoutMillis;
+    }
+
+    public boolean absoluteUpgradeUrl() {
+        return absoluteUpgradeUrl;
+    }
+
+    @Override
+    public String toString() {
+        return "WebSocketClientProtocolConfig" +
+            " {webSocketUri=" + webSocketUri +
+            ", subprotocol=" + subprotocol +
+            ", version=" + version +
+            ", allowExtensions=" + allowExtensions +
+            ", customHeaders=" + customHeaders +
+            ", maxFramePayloadLength=" + maxFramePayloadLength +
+            ", performMasking=" + performMasking +
+            ", allowMaskMismatch=" + allowMaskMismatch +
+            ", handleCloseFrames=" + handleCloseFrames +
+            ", sendCloseFrame=" + sendCloseFrame +
+            ", dropPongFrames=" + dropPongFrames +
+            ", handshakeTimeoutMillis=" + handshakeTimeoutMillis +
+            ", forceCloseTimeoutMillis=" + forceCloseTimeoutMillis +
+            ", absoluteUpgradeUrl=" + absoluteUpgradeUrl +
+            "}";
+    }
+
+    public Builder toBuilder() {
+        return new Builder(this);
+    }
+
+    public static Builder newBuilder() {
+        return new Builder(
+                URI.create("https://localhost/"),
+                null,
+                WebSocketVersion.V13,
+                false,
+                EmptyHttpHeaders.INSTANCE,
+                65536,
+                DEFAULT_PERFORM_MASKING,
+                DEFAULT_ALLOW_MASK_MISMATCH,
+                DEFAULT_HANDLE_CLOSE_FRAMES,
+                WebSocketCloseStatus.NORMAL_CLOSURE,
+                DEFAULT_DROP_PONG_FRAMES,
+                DEFAULT_HANDSHAKE_TIMEOUT_MILLIS,
+                -1,
+                false);
+    }
+
+    public static final class Builder {
+        private URI webSocketUri;
+        private String subprotocol;
+        private WebSocketVersion version;
+        private boolean allowExtensions;
+        private HttpHeaders customHeaders;
+        private int maxFramePayloadLength;
+        private boolean performMasking;
+        private boolean allowMaskMismatch;
+        private boolean handleCloseFrames;
+        private WebSocketCloseStatus sendCloseFrame;
+        private boolean dropPongFrames;
+        private long handshakeTimeoutMillis;
+        private long forceCloseTimeoutMillis;
+        private boolean absoluteUpgradeUrl;
+
+        private Builder(WebSocketClientProtocolConfig clientConfig) {
+            this(ObjectUtil.checkNotNull(clientConfig, "clientConfig").webSocketUri(),
+                 clientConfig.subprotocol(),
+                 clientConfig.version(),
+                 clientConfig.allowExtensions(),
+                 clientConfig.customHeaders(),
+                 clientConfig.maxFramePayloadLength(),
+                 clientConfig.performMasking(),
+                 clientConfig.allowMaskMismatch(),
+                 clientConfig.handleCloseFrames(),
+                 clientConfig.sendCloseFrame(),
+                 clientConfig.dropPongFrames(),
+                 clientConfig.handshakeTimeoutMillis(),
+                 clientConfig.forceCloseTimeoutMillis(),
+                 clientConfig.absoluteUpgradeUrl());
+        }
+
+        private Builder(URI webSocketUri,
+                        String subprotocol,
+                        WebSocketVersion version,
+                        boolean allowExtensions,
+                        HttpHeaders customHeaders,
+                        int maxFramePayloadLength,
+                        boolean performMasking,
+                        boolean allowMaskMismatch,
+                        boolean handleCloseFrames,
+                        WebSocketCloseStatus sendCloseFrame,
+                        boolean dropPongFrames,
+                        long handshakeTimeoutMillis,
+                        long forceCloseTimeoutMillis,
+                        boolean absoluteUpgradeUrl) {
+            this.webSocketUri = webSocketUri;
+            this.subprotocol = subprotocol;
+            this.version = version;
+            this.allowExtensions = allowExtensions;
+            this.customHeaders = customHeaders;
+            this.maxFramePayloadLength = maxFramePayloadLength;
+            this.performMasking = performMasking;
+            this.allowMaskMismatch = allowMaskMismatch;
+            this.handleCloseFrames = handleCloseFrames;
+            this.sendCloseFrame = sendCloseFrame;
+            this.dropPongFrames = dropPongFrames;
+            this.handshakeTimeoutMillis = handshakeTimeoutMillis;
+            this.forceCloseTimeoutMillis = forceCloseTimeoutMillis;
+            this.absoluteUpgradeUrl = absoluteUpgradeUrl;
+        }
+
+        /**
+         * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
+         * sent to this URL.
+         */
+        public Builder webSocketUri(String webSocketUri) {
+            return webSocketUri(URI.create(webSocketUri));
+        }
+
+        /**
+         * URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
+         * sent to this URL.
+         */
+        public Builder webSocketUri(URI webSocketUri) {
+            this.webSocketUri = webSocketUri;
+            return this;
+        }
+
+        /**
+         * Sub protocol request sent to the server.
+         */
+        public Builder subprotocol(String subprotocol) {
+            this.subprotocol = subprotocol;
+            return this;
+        }
+
+        /**
+         * Version of web socket specification to use to connect to the server
+         */
+        public Builder version(WebSocketVersion version) {
+            this.version = version;
+            return this;
+        }
+
+        /**
+         * Allow extensions to be used in the reserved bits of the web socket frame
+         */
+        public Builder allowExtensions(boolean allowExtensions) {
+            this.allowExtensions = allowExtensions;
+            return this;
+        }
+
+        /**
+         * Map of custom headers to add to the client request
+         */
+        public Builder customHeaders(HttpHeaders customHeaders) {
+            this.customHeaders = customHeaders;
+            return this;
+        }
+
+        /**
+         * Maximum length of a frame's payload
+         */
+        public Builder maxFramePayloadLength(int maxFramePayloadLength) {
+            this.maxFramePayloadLength = maxFramePayloadLength;
+            return this;
+        }
+
+        /**
+         * Whether to mask all written websocket frames. This must be set to true in order to be fully compatible
+         * with the websocket specifications. Client applications that communicate with a non-standard server
+         * which doesn't require masking might set this to false to achieve a higher performance.
+         */
+        public Builder performMasking(boolean performMasking) {
+            this.performMasking = performMasking;
+            return this;
+        }
+
+        /**
+         * When set to true, frames which are not masked properly according to the standard will still be accepted.
+         */
+        public Builder allowMaskMismatch(boolean allowMaskMismatch) {
+            this.allowMaskMismatch = allowMaskMismatch;
+            return this;
+        }
+
+        /**
+         * {@code true} if close frames should not be forwarded and just close the channel
+         */
+        public Builder handleCloseFrames(boolean handleCloseFrames) {
+            this.handleCloseFrames = handleCloseFrames;
+            return this;
+        }
+
+        /**
+         * Close frame to send, when close frame was not send manually. Or {@code null} to disable proper close.
+         */
+        public Builder sendCloseFrame(WebSocketCloseStatus sendCloseFrame) {
+            this.sendCloseFrame = sendCloseFrame;
+            return this;
+        }
+
+        /**
+         * {@code true} if pong frames should not be forwarded
+         */
+        public Builder dropPongFrames(boolean dropPongFrames) {
+            this.dropPongFrames = dropPongFrames;
+            return this;
+        }
+
+        /**
+         * Handshake timeout in mills, when handshake timeout, will trigger user
+         * event {@link ClientHandshakeStateEvent#HANDSHAKE_TIMEOUT}
+         */
+        public Builder handshakeTimeoutMillis(long handshakeTimeoutMillis) {
+            this.handshakeTimeoutMillis = handshakeTimeoutMillis;
+            return this;
+        }
+
+        /**
+         * Close the connection if it was not closed by the server after timeout specified
+         */
+        public Builder forceCloseTimeoutMillis(long forceCloseTimeoutMillis) {
+            this.forceCloseTimeoutMillis = forceCloseTimeoutMillis;
+            return this;
+        }
+
+        /**
+         * Use an absolute url for the Upgrade request, typically when connecting through an HTTP proxy over clear HTTP
+         */
+        public Builder absoluteUpgradeUrl(boolean absoluteUpgradeUrl) {
+            this.absoluteUpgradeUrl = absoluteUpgradeUrl;
+            return this;
+        }
+
+        /**
+         * Build unmodifiable client protocol configuration.
+         */
+        public WebSocketClientProtocolConfig build() {
+            return new WebSocketClientProtocolConfig(
+                webSocketUri,
+                subprotocol,
+                version,
+                allowExtensions,
+                customHeaders,
+                maxFramePayloadLength,
+                performMasking,
+                allowMaskMismatch,
+                handleCloseFrames,
+                sendCloseFrame,
+                dropPongFrames,
+                handshakeTimeoutMillis,
+                forceCloseTimeoutMillis,
+                absoluteUpgradeUrl
+            );
+        }
+    }
+}
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientProtocolHandler.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientProtocolHandler.java
index 7c85c57..cdbc853 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientProtocolHandler.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientProtocolHandler.java
@@ -23,6 +23,13 @@ import io.netty.handler.codec.http.HttpHeaders;
 import java.net.URI;
 import java.util.List;
 
+import static io.netty.handler.codec.http.websocketx.WebSocketClientProtocolConfig.DEFAULT_ALLOW_MASK_MISMATCH;
+import static io.netty.handler.codec.http.websocketx.WebSocketClientProtocolConfig.DEFAULT_DROP_PONG_FRAMES;
+import static io.netty.handler.codec.http.websocketx.WebSocketClientProtocolConfig.DEFAULT_HANDLE_CLOSE_FRAMES;
+import static io.netty.handler.codec.http.websocketx.WebSocketClientProtocolConfig.DEFAULT_PERFORM_MASKING;
+import static io.netty.handler.codec.http.websocketx.WebSocketServerProtocolConfig.DEFAULT_HANDSHAKE_TIMEOUT_MILLIS;
+import static io.netty.util.internal.ObjectUtil.*;
+
 /**
  * This handler does all the heavy lifting for you to run a websocket client.
  *
@@ -38,19 +45,25 @@ import java.util.List;
  * {@link ClientHandshakeStateEvent#HANDSHAKE_ISSUED} or {@link ClientHandshakeStateEvent#HANDSHAKE_COMPLETE}.
  */
 public class WebSocketClientProtocolHandler extends WebSocketProtocolHandler {
-
     private final WebSocketClientHandshaker handshaker;
-    private final boolean handleCloseFrames;
+    private final WebSocketClientProtocolConfig clientConfig;
 
     /**
      * Returns the used handshaker
      */
-    public WebSocketClientHandshaker handshaker() { return handshaker; }
+    public WebSocketClientHandshaker handshaker() {
+        return handshaker;
+    }
 
     /**
      * Events that are fired to notify about handshake status
      */
     public enum ClientHandshakeStateEvent {
+        /**
+         * The Handshake was timed out
+         */
+        HANDSHAKE_TIMEOUT,
+
         /**
          * The Handshake was started but the server did not response yet to the request
          */
@@ -62,6 +75,30 @@ public class WebSocketClientProtocolHandler extends WebSocketProtocolHandler {
         HANDSHAKE_COMPLETE
     }
 
+    /**
+     * Base constructor
+     *
+     * @param clientConfig
+     *            Client protocol configuration.
+     */
+    public WebSocketClientProtocolHandler(WebSocketClientProtocolConfig clientConfig) {
+        super(checkNotNull(clientConfig, "clientConfig").dropPongFrames(),
+              clientConfig.sendCloseFrame(), clientConfig.forceCloseTimeoutMillis());
+        this.handshaker = WebSocketClientHandshakerFactory.newHandshaker(
+            clientConfig.webSocketUri(),
+            clientConfig.version(),
+            clientConfig.subprotocol(),
+            clientConfig.allowExtensions(),
+            clientConfig.customHeaders(),
+            clientConfig.maxFramePayloadLength(),
+            clientConfig.performMasking(),
+            clientConfig.allowMaskMismatch(),
+            clientConfig.forceCloseTimeoutMillis(),
+            clientConfig.absoluteUpgradeUrl()
+        );
+        this.clientConfig = clientConfig;
+    }
+
     /**
      * Base constructor
      *
@@ -90,9 +127,45 @@ public class WebSocketClientProtocolHandler extends WebSocketProtocolHandler {
                                           boolean allowExtensions, HttpHeaders customHeaders,
                                           int maxFramePayloadLength, boolean handleCloseFrames,
                                           boolean performMasking, boolean allowMaskMismatch) {
+        this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength,
+            handleCloseFrames, performMasking, allowMaskMismatch, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS);
+    }
+
+    /**
+     * Base constructor
+     *
+     * @param webSocketURL
+     *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
+     *            sent to this URL.
+     * @param version
+     *            Version of web socket specification to use to connect to the server
+     * @param subprotocol
+     *            Sub protocol request sent to the server.
+     * @param customHeaders
+     *            Map of custom headers to add to the client request
+     * @param maxFramePayloadLength
+     *            Maximum length of a frame's payload
+     * @param handleCloseFrames
+     *            {@code true} if close frames should not be forwarded and just close the channel
+     * @param performMasking
+     *            Whether to mask all written websocket frames. This must be set to true in order to be fully compatible
+     *            with the websocket specifications. Client applications that communicate with a non-standard server
+     *            which doesn't require masking might set this to false to achieve a higher performance.
+     * @param allowMaskMismatch
+     *            When set to true, frames which are not masked properly according to the standard will still be
+     *            accepted.
+     * @param handshakeTimeoutMillis
+     *            Handshake timeout in mills, when handshake timeout, will trigger user
+     *            event {@link ClientHandshakeStateEvent#HANDSHAKE_TIMEOUT}
+     */
+    public WebSocketClientProtocolHandler(URI webSocketURL, WebSocketVersion version, String subprotocol,
+                                          boolean allowExtensions, HttpHeaders customHeaders,
+                                          int maxFramePayloadLength, boolean handleCloseFrames, boolean performMasking,
+                                          boolean allowMaskMismatch, long handshakeTimeoutMillis) {
         this(WebSocketClientHandshakerFactory.newHandshaker(webSocketURL, version, subprotocol,
                                                             allowExtensions, customHeaders, maxFramePayloadLength,
-                                                            performMasking, allowMaskMismatch), handleCloseFrames);
+                                                            performMasking, allowMaskMismatch),
+             handleCloseFrames, handshakeTimeoutMillis);
     }
 
     /**
@@ -116,7 +189,34 @@ public class WebSocketClientProtocolHandler extends WebSocketProtocolHandler {
                                                    boolean allowExtensions, HttpHeaders customHeaders,
                                                    int maxFramePayloadLength, boolean handleCloseFrames) {
         this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength,
-             handleCloseFrames, true, false);
+             handleCloseFrames, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS);
+    }
+
+    /**
+     * Base constructor
+     *
+     * @param webSocketURL
+     *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
+     *            sent to this URL.
+     * @param version
+     *            Version of web socket specification to use to connect to the server
+     * @param subprotocol
+     *            Sub protocol request sent to the server.
+     * @param customHeaders
+     *            Map of custom headers to add to the client request
+     * @param maxFramePayloadLength
+     *            Maximum length of a frame's payload
+     * @param handleCloseFrames
+     *            {@code true} if close frames should not be forwarded and just close the channel
+     * @param handshakeTimeoutMillis
+     *            Handshake timeout in mills, when handshake timeout, will trigger user
+     *            event {@link ClientHandshakeStateEvent#HANDSHAKE_TIMEOUT}
+     */
+    public WebSocketClientProtocolHandler(URI webSocketURL, WebSocketVersion version, String subprotocol,
+                                          boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength,
+                                          boolean handleCloseFrames, long handshakeTimeoutMillis) {
+        this(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength,
+             handleCloseFrames, DEFAULT_PERFORM_MASKING, DEFAULT_ALLOW_MASK_MISMATCH, handshakeTimeoutMillis);
     }
 
     /**
@@ -137,8 +237,33 @@ public class WebSocketClientProtocolHandler extends WebSocketProtocolHandler {
     public WebSocketClientProtocolHandler(URI webSocketURL, WebSocketVersion version, String subprotocol,
                                           boolean allowExtensions, HttpHeaders customHeaders,
                                           int maxFramePayloadLength) {
-        this(webSocketURL, version, subprotocol,
-                allowExtensions, customHeaders, maxFramePayloadLength, true);
+        this(webSocketURL, version, subprotocol, allowExtensions,
+             customHeaders, maxFramePayloadLength, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS);
+    }
+
+    /**
+     * Base constructor
+     *
+     * @param webSocketURL
+     *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
+     *            sent to this URL.
+     * @param version
+     *            Version of web socket specification to use to connect to the server
+     * @param subprotocol
+     *            Sub protocol request sent to the server.
+     * @param customHeaders
+     *            Map of custom headers to add to the client request
+     * @param maxFramePayloadLength
+     *            Maximum length of a frame's payload
+     * @param handshakeTimeoutMillis
+     *            Handshake timeout in mills, when handshake timeout, will trigger user
+     *            event {@link ClientHandshakeStateEvent#HANDSHAKE_TIMEOUT}
+     */
+    public WebSocketClientProtocolHandler(URI webSocketURL, WebSocketVersion version, String subprotocol,
+                                          boolean allowExtensions, HttpHeaders customHeaders,
+                                          int maxFramePayloadLength, long handshakeTimeoutMillis) {
+        this(webSocketURL, version, subprotocol, allowExtensions, customHeaders,
+             maxFramePayloadLength, DEFAULT_HANDLE_CLOSE_FRAMES, handshakeTimeoutMillis);
     }
 
     /**
@@ -151,7 +276,24 @@ public class WebSocketClientProtocolHandler extends WebSocketProtocolHandler {
      *            {@code true} if close frames should not be forwarded and just close the channel
      */
     public WebSocketClientProtocolHandler(WebSocketClientHandshaker handshaker, boolean handleCloseFrames) {
-        this(handshaker, handleCloseFrames, true);
+        this(handshaker, handleCloseFrames, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS);
+    }
+
+    /**
+     * Base constructor
+     *
+     * @param handshaker
+     *            The {@link WebSocketClientHandshaker} which will be used to issue the handshake once the connection
+     *            was established to the remote peer.
+     * @param handleCloseFrames
+     *            {@code true} if close frames should not be forwarded and just close the channel
+     * @param handshakeTimeoutMillis
+     *            Handshake timeout in mills, when handshake timeout, will trigger user
+     *            event {@link ClientHandshakeStateEvent#HANDSHAKE_TIMEOUT}
+     */
+    public WebSocketClientProtocolHandler(WebSocketClientHandshaker handshaker, boolean handleCloseFrames,
+                                          long handshakeTimeoutMillis) {
+        this(handshaker, handleCloseFrames, DEFAULT_DROP_PONG_FRAMES, handshakeTimeoutMillis);
     }
 
     /**
@@ -167,9 +309,31 @@ public class WebSocketClientProtocolHandler extends WebSocketProtocolHandler {
      */
     public WebSocketClientProtocolHandler(WebSocketClientHandshaker handshaker, boolean handleCloseFrames,
                                           boolean dropPongFrames) {
+        this(handshaker, handleCloseFrames, dropPongFrames, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS);
+    }
+
+    /**
+     * Base constructor
+     *
+     * @param handshaker
+     *            The {@link WebSocketClientHandshaker} which will be used to issue the handshake once the connection
+     *            was established to the remote peer.
+     * @param handleCloseFrames
+     *            {@code true} if close frames should not be forwarded and just close the channel
+     * @param dropPongFrames
+     *            {@code true} if pong frames should not be forwarded
+     * @param handshakeTimeoutMillis
+     *            Handshake timeout in mills, when handshake timeout, will trigger user
+     *            event {@link ClientHandshakeStateEvent#HANDSHAKE_TIMEOUT}
+     */
+    public WebSocketClientProtocolHandler(WebSocketClientHandshaker handshaker, boolean handleCloseFrames,
+                                          boolean dropPongFrames, long handshakeTimeoutMillis) {
         super(dropPongFrames);
         this.handshaker = handshaker;
-        this.handleCloseFrames = handleCloseFrames;
+        this.clientConfig = WebSocketClientProtocolConfig.newBuilder()
+            .handleCloseFrames(handleCloseFrames)
+            .handshakeTimeoutMillis(handshakeTimeoutMillis)
+            .build();
     }
 
     /**
@@ -180,12 +344,26 @@ public class WebSocketClientProtocolHandler extends WebSocketProtocolHandler {
      *            was established to the remote peer.
      */
     public WebSocketClientProtocolHandler(WebSocketClientHandshaker handshaker) {
-        this(handshaker, true);
+        this(handshaker, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS);
+    }
+
+    /**
+     * Base constructor
+     *
+     * @param handshaker
+     *            The {@link WebSocketClientHandshaker} which will be used to issue the handshake once the connection
+     *            was established to the remote peer.
+     * @param handshakeTimeoutMillis
+     *            Handshake timeout in mills, when handshake timeout, will trigger user
+     *            event {@link ClientHandshakeStateEvent#HANDSHAKE_TIMEOUT}
+     */
+    public WebSocketClientProtocolHandler(WebSocketClientHandshaker handshaker, long handshakeTimeoutMillis) {
+        this(handshaker, DEFAULT_HANDLE_CLOSE_FRAMES, handshakeTimeoutMillis);
     }
 
     @Override
     protected void decode(ChannelHandlerContext ctx, WebSocketFrame frame, List<Object> out) throws Exception {
-        if (handleCloseFrames && frame instanceof CloseWebSocketFrame) {
+        if (clientConfig.handleCloseFrames() && frame instanceof CloseWebSocketFrame) {
             ctx.close();
             return;
         }
@@ -198,7 +376,7 @@ public class WebSocketClientProtocolHandler extends WebSocketProtocolHandler {
         if (cp.get(WebSocketClientProtocolHandshakeHandler.class) == null) {
             // Add the WebSocketClientProtocolHandshakeHandler before this one.
             ctx.pipeline().addBefore(ctx.name(), WebSocketClientProtocolHandshakeHandler.class.getName(),
-                    new WebSocketClientProtocolHandshakeHandler(handshaker));
+                new WebSocketClientProtocolHandshakeHandler(handshaker, clientConfig.handshakeTimeoutMillis()));
         }
         if (cp.get(Utf8FrameValidator.class) == null) {
             // Add the UFT8 checking before this one.
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientProtocolHandshakeHandler.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientProtocolHandshakeHandler.java
index 3d130c6..701af1b 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientProtocolHandshakeHandler.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketClientProtocolHandshakeHandler.java
@@ -19,13 +19,37 @@ import io.netty.channel.ChannelFuture;
 import io.netty.channel.ChannelFutureListener;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.channel.ChannelPromise;
 import io.netty.handler.codec.http.FullHttpResponse;
+import io.netty.handler.codec.http.websocketx.WebSocketClientProtocolHandler.ClientHandshakeStateEvent;
+import io.netty.util.concurrent.Future;
+import io.netty.util.concurrent.FutureListener;
+
+import java.util.concurrent.TimeUnit;
+
+import static io.netty.util.internal.ObjectUtil.*;
 
 class WebSocketClientProtocolHandshakeHandler extends ChannelInboundHandlerAdapter {
+    private static final long DEFAULT_HANDSHAKE_TIMEOUT_MS = 10000L;
+
     private final WebSocketClientHandshaker handshaker;
+    private final long handshakeTimeoutMillis;
+    private ChannelHandlerContext ctx;
+    private ChannelPromise handshakePromise;
 
     WebSocketClientProtocolHandshakeHandler(WebSocketClientHandshaker handshaker) {
+        this(handshaker, DEFAULT_HANDSHAKE_TIMEOUT_MS);
+    }
+
+    WebSocketClientProtocolHandshakeHandler(WebSocketClientHandshaker handshaker, long handshakeTimeoutMillis) {
         this.handshaker = handshaker;
+        this.handshakeTimeoutMillis = checkPositive(handshakeTimeoutMillis, "handshakeTimeoutMillis");
+    }
+
+    @Override
+    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
+        this.ctx = ctx;
+        handshakePromise = ctx.newPromise();
     }
 
     @Override
@@ -35,6 +59,7 @@ class WebSocketClientProtocolHandshakeHandler extends ChannelInboundHandlerAdapt
             @Override
             public void operationComplete(ChannelFuture future) throws Exception {
                 if (!future.isSuccess()) {
+                    handshakePromise.tryFailure(future.cause());
                     ctx.fireExceptionCaught(future.cause());
                 } else {
                     ctx.fireUserEventTriggered(
@@ -42,6 +67,7 @@ class WebSocketClientProtocolHandshakeHandler extends ChannelInboundHandlerAdapt
                 }
             }
         });
+        applyHandshakeTimeout();
     }
 
     @Override
@@ -55,6 +81,7 @@ class WebSocketClientProtocolHandshakeHandler extends ChannelInboundHandlerAdapt
         try {
             if (!handshaker.isHandshakeComplete()) {
                 handshaker.finishHandshake(ctx.channel(), response);
+                handshakePromise.trySuccess();
                 ctx.fireUserEventTriggered(
                         WebSocketClientProtocolHandler.ClientHandshakeStateEvent.HANDSHAKE_COMPLETE);
                 ctx.pipeline().remove(this);
@@ -65,4 +92,43 @@ class WebSocketClientProtocolHandshakeHandler extends ChannelInboundHandlerAdapt
             response.release();
         }
     }
+
+    private void applyHandshakeTimeout() {
+        final ChannelPromise localHandshakePromise = handshakePromise;
+        if (handshakeTimeoutMillis <= 0 || localHandshakePromise.isDone()) {
+            return;
+        }
+
+        final Future<?> timeoutFuture = ctx.executor().schedule(new Runnable() {
+            @Override
+            public void run() {
+                if (localHandshakePromise.isDone()) {
+                    return;
+                }
+
+                if (localHandshakePromise.tryFailure(new WebSocketHandshakeException("handshake timed out"))) {
+                    ctx.flush()
+                       .fireUserEventTriggered(ClientHandshakeStateEvent.HANDSHAKE_TIMEOUT)
+                       .close();
+                }
+            }
+        }, handshakeTimeoutMillis, TimeUnit.MILLISECONDS);
+
+        // Cancel the handshake timeout when handshake is finished.
+        localHandshakePromise.addListener(new FutureListener<Void>() {
+            @Override
+            public void operationComplete(Future<Void> f) throws Exception {
+                timeoutFuture.cancel(false);
+            }
+        });
+    }
+
+    /**
+     * This method is visible for testing.
+     *
+     * @return current handshake future
+     */
+    ChannelFuture getHandshakeFuture() {
+        return handshakePromise;
+    }
 }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketCloseStatus.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketCloseStatus.java
new file mode 100644
index 0000000..3069613
--- /dev/null
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketCloseStatus.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright 2019 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.http.websocketx;
+
+import static io.netty.util.internal.ObjectUtil.checkNotNull;
+
+/**
+ * WebSocket status codes specified in RFC-6455.
+ * <pre>
+ *
+ * RFC-6455 The WebSocket Protocol, December 2011:
+ * <a href="https://tools.ietf.org/html/rfc6455#section-7.4.1"
+ *         >https://tools.ietf.org/html/rfc6455#section-7.4.1</a>
+ *
+ * WebSocket Protocol Registries, April 2019:
+ * <a href="https://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number"
+ *         >https://www.iana.org/assignments/websocket/websocket.xhtml</a>
+ *
+ * 7.4.1.  Defined Status Codes
+ *
+ * Endpoints MAY use the following pre-defined status codes when sending
+ * a Close frame.
+ *
+ * 1000
+ *
+ *    1000 indicates a normal closure, meaning that the purpose for
+ *    which the connection was established has been fulfilled.
+ *
+ * 1001
+ *
+ *    1001 indicates that an endpoint is "going away", such as a server
+ *    going down or a browser having navigated away from a page.
+ *
+ * 1002
+ *
+ *    1002 indicates that an endpoint is terminating the connection due
+ *    to a protocol error.
+ *
+ * 1003
+ *
+ *    1003 indicates that an endpoint is terminating the connection
+ *    because it has received a type of data it cannot accept (e.g., an
+ *    endpoint that understands only text data MAY send this if it
+ *    receives a binary message).
+ *
+ * 1004
+ *
+ *    Reserved. The specific meaning might be defined in the future.
+ *
+ * 1005
+ *
+ *    1005 is a reserved value and MUST NOT be set as a status code in a
+ *    Close control frame by an endpoint. It is designated for use in
+ *    applications expecting a status code to indicate that no status
+ *    code was actually present.
+ *
+ * 1006
+ *
+ *    1006 is a reserved value and MUST NOT be set as a status code in a
+ *    Close control frame by an endpoint. It is designated for use in
+ *    applications expecting a status code to indicate that the
+ *    connection was closed abnormally, e.g., without sending or
+ *    receiving a Close control frame.
+ *
+ * 1007
+ *
+ *    1007 indicates that an endpoint is terminating the connection
+ *    because it has received data within a message that was not
+ *    consistent with the type of the message (e.g., non-UTF-8 [RFC3629]
+ *    data within a text message).
+ *
+ * 1008
+ *
+ *    1008 indicates that an endpoint is terminating the connection
+ *    because it has received a message that violates its policy. This
+ *    is a generic status code that can be returned when there is no
+ *    other more suitable status code (e.g., 1003 or 1009) or if there
+ *    is a need to hide specific details about the policy.
+ *
+ * 1009
+ *
+ *    1009 indicates that an endpoint is terminating the connection
+ *    because it has received a message that is too big for it to
+ *    process.
+ *
+ * 1010
+ *
+ *    1010 indicates that an endpoint (client) is terminating the
+ *    connection because it has expected the server to negotiate one or
+ *    more extension, but the server didn't return them in the response
+ *    message of the WebSocket handshake. The list of extensions that
+ *    are needed SHOULD appear in the /reason/ part of the Close frame.
+ *    Note that this status code is not used by the server, because it
+ *    can fail the WebSocket handshake instead.
+ *
+ * 1011
+ *
+ *    1011 indicates that a server is terminating the connection because
+ *    it encountered an unexpected condition that prevented it from
+ *    fulfilling the request.
+ *
+ * 1012 (IANA Registry, Non RFC-6455)
+ *
+ *    1012 indicates that the service is restarted. a client may reconnect,
+ *    and if it choses to do, should reconnect using a randomized delay
+ *    of 5 - 30 seconds.
+ *
+ * 1013 (IANA Registry, Non RFC-6455)
+ *
+ *    1013 indicates that the service is experiencing overload. a client
+ *    should only connect to a different IP (when there are multiple for the
+ *    target) or reconnect to the same IP upon user action.
+ *
+ * 1014 (IANA Registry, Non RFC-6455)
+ *
+ *    The server was acting as a gateway or proxy and received an invalid
+ *    response from the upstream server. This is similar to 502 HTTP Status Code.
+ *
+ * 1015
+ *
+ *    1015 is a reserved value and MUST NOT be set as a status code in a
+ *    Close control frame by an endpoint. It is designated for use in
+ *    applications expecting a status code to indicate that the
+ *    connection was closed due to a failure to perform a TLS handshake
+ *    (e.g., the server certificate can't be verified).
+ *
+ *
+ * 7.4.2. Reserved Status Code Ranges
+ *
+ * 0-999
+ *
+ *    Status codes in the range 0-999 are not used.
+ *
+ * 1000-2999
+ *
+ *    Status codes in the range 1000-2999 are reserved for definition by
+ *    this protocol, its future revisions, and extensions specified in a
+ *    permanent and readily available public specification.
+ *
+ * 3000-3999
+ *
+ *    Status codes in the range 3000-3999 are reserved for use by
+ *    libraries, frameworks, and applications. These status codes are
+ *    registered directly with IANA. The interpretation of these codes
+ *    is undefined by this protocol.
+ *
+ * 4000-4999
+ *
+ *    Status codes in the range 4000-4999 are reserved for private use
+ *    and thus can't be registered. Such codes can be used by prior
+ *    agreements between WebSocket applications. The interpretation of
+ *    these codes is undefined by this protocol.
+ * </pre>
+ * <p>
+ * While {@link WebSocketCloseStatus} is enum-like structure, its instances should NOT be compared by reference.
+ * Instead, either {@link #equals(Object)} should be used or direct comparison of {@link #code()} value.
+ */
+public final class WebSocketCloseStatus implements Comparable<WebSocketCloseStatus> {
+
+    public static final WebSocketCloseStatus NORMAL_CLOSURE =
+        new WebSocketCloseStatus(1000, "Bye");
+
+    public static final WebSocketCloseStatus ENDPOINT_UNAVAILABLE =
+        new WebSocketCloseStatus(1001, "Endpoint unavailable");
+
+    public static final WebSocketCloseStatus PROTOCOL_ERROR =
+        new WebSocketCloseStatus(1002, "Protocol error");
+
+    public static final WebSocketCloseStatus INVALID_MESSAGE_TYPE =
+        new WebSocketCloseStatus(1003, "Invalid message type");
+
+    public static final WebSocketCloseStatus INVALID_PAYLOAD_DATA =
+        new WebSocketCloseStatus(1007, "Invalid payload data");
+
+    public static final WebSocketCloseStatus POLICY_VIOLATION =
+        new WebSocketCloseStatus(1008, "Policy violation");
+
+    public static final WebSocketCloseStatus MESSAGE_TOO_BIG =
+        new WebSocketCloseStatus(1009, "Message too big");
+
+    public static final WebSocketCloseStatus MANDATORY_EXTENSION =
+        new WebSocketCloseStatus(1010, "Mandatory extension");
+
+    public static final WebSocketCloseStatus INTERNAL_SERVER_ERROR =
+        new WebSocketCloseStatus(1011, "Internal server error");
+
+    public static final WebSocketCloseStatus SERVICE_RESTART =
+        new WebSocketCloseStatus(1012, "Service Restart");
+
+    public static final WebSocketCloseStatus TRY_AGAIN_LATER =
+        new WebSocketCloseStatus(1013, "Try Again Later");
+
+    public static final WebSocketCloseStatus BAD_GATEWAY =
+        new WebSocketCloseStatus(1014, "Bad Gateway");
+
+    // 1004, 1005, 1006, 1015 are reserved and should never be used by user
+    //public static final WebSocketCloseStatus SPECIFIC_MEANING = register(1004, "...");
+    //public static final WebSocketCloseStatus EMPTY = register(1005, "Empty");
+    //public static final WebSocketCloseStatus ABNORMAL_CLOSURE = register(1006, "Abnormal closure");
+    //public static final WebSocketCloseStatus TLS_HANDSHAKE_FAILED(1015, "TLS handshake failed");
+
+    private final int statusCode;
+    private final String reasonText;
+    private String text;
+
+    public WebSocketCloseStatus(int statusCode, String reasonText) {
+        if (!isValidStatusCode(statusCode)) {
+            throw new IllegalArgumentException(
+                "WebSocket close status code does NOT comply with RFC-6455: " + statusCode);
+        }
+        this.statusCode = statusCode;
+        this.reasonText = checkNotNull(reasonText, "reasonText");
+    }
+
+    public int code() {
+        return statusCode;
+    }
+
+    public String reasonText() {
+        return reasonText;
+    }
+
+    /**
+     * Order of {@link WebSocketCloseStatus} only depends on {@link #code()}.
+     */
+    @Override
+    public int compareTo(WebSocketCloseStatus o) {
+        return code() - o.code();
+    }
+
+    /**
+     * Equality of {@link WebSocketCloseStatus} only depends on {@link #code()}.
+     */
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (null == o || getClass() != o.getClass()) {
+            return false;
+        }
+
+        WebSocketCloseStatus that = (WebSocketCloseStatus) o;
+
+        return statusCode == that.statusCode;
+    }
+
+    @Override
+    public int hashCode() {
+        return statusCode;
+    }
+
+    @Override
+    public String toString() {
+        String text = this.text;
+        if (text == null) {
+            // E.g.: "1000 Bye", "1009 Message too big"
+            this.text = text = code() + " " + reasonText();
+        }
+        return text;
+    }
+
+    public static boolean isValidStatusCode(int code) {
+        return code < 0 ||
+            1000 <= code && code <= 1003 ||
+            1007 <= code && code <= 1014 ||
+            3000 <= code;
+    }
+
+    public static WebSocketCloseStatus valueOf(int code) {
+        switch (code) {
+            case 1000:
+                return NORMAL_CLOSURE;
+            case 1001:
+                return ENDPOINT_UNAVAILABLE;
+            case 1002:
+                return PROTOCOL_ERROR;
+            case 1003:
+                return INVALID_MESSAGE_TYPE;
+            case 1007:
+                return INVALID_PAYLOAD_DATA;
+            case 1008:
+                return POLICY_VIOLATION;
+            case 1009:
+                return MESSAGE_TOO_BIG;
+            case 1010:
+                return MANDATORY_EXTENSION;
+            case 1011:
+                return INTERNAL_SERVER_ERROR;
+            case 1012:
+                return SERVICE_RESTART;
+            case 1013:
+                return TRY_AGAIN_LATER;
+            case 1014:
+                return BAD_GATEWAY;
+            default:
+                return new WebSocketCloseStatus(code, "Close status #" + code);
+        }
+    }
+
+}
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketDecoderConfig.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketDecoderConfig.java
new file mode 100644
index 0000000..8880ced
--- /dev/null
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketDecoderConfig.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2019 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.http.websocketx;
+
+import io.netty.util.internal.ObjectUtil;
+
+/**
+ * Frames decoder configuration.
+ */
+public final class WebSocketDecoderConfig {
+
+    static final WebSocketDecoderConfig DEFAULT =
+        new WebSocketDecoderConfig(65536, true, false, false, true, true);
+
+    private final int maxFramePayloadLength;
+    private final boolean expectMaskedFrames;
+    private final boolean allowMaskMismatch;
+    private final boolean allowExtensions;
+    private final boolean closeOnProtocolViolation;
+    private final boolean withUTF8Validator;
+
+    /**
+     * Constructor
+     *
+     * @param maxFramePayloadLength
+     *            Maximum length of a frame's payload. Setting this to an appropriate value for you application
+     *            helps check for denial of services attacks.
+     * @param expectMaskedFrames
+     *            Web socket servers must set this to true processed incoming masked payload. Client implementations
+     *            must set this to false.
+     * @param allowMaskMismatch
+     *            Allows to loosen the masking requirement on received frames. When this is set to false then also
+     *            frames which are not masked properly according to the standard will still be accepted.
+     * @param allowExtensions
+     *            Flag to allow reserved extension bits to be used or not
+     * @param closeOnProtocolViolation
+     *            Flag to send close frame immediately on any protocol violation.ion.
+     * @param withUTF8Validator
+     *            Allows you to avoid adding of Utf8FrameValidator to the pipeline on the
+     *            WebSocketServerProtocolHandler creation. This is useful (less overhead)
+     *            when you use only BinaryWebSocketFrame within your web socket connection.
+     */
+    private WebSocketDecoderConfig(int maxFramePayloadLength, boolean expectMaskedFrames, boolean allowMaskMismatch,
+                                  boolean allowExtensions, boolean closeOnProtocolViolation,
+                                  boolean withUTF8Validator) {
+        this.maxFramePayloadLength = maxFramePayloadLength;
+        this.expectMaskedFrames = expectMaskedFrames;
+        this.allowMaskMismatch = allowMaskMismatch;
+        this.allowExtensions = allowExtensions;
+        this.closeOnProtocolViolation = closeOnProtocolViolation;
+        this.withUTF8Validator = withUTF8Validator;
+    }
+
+    public int maxFramePayloadLength() {
+        return maxFramePayloadLength;
+    }
+
+    public boolean expectMaskedFrames() {
+        return expectMaskedFrames;
+    }
+
+    public boolean allowMaskMismatch() {
+        return allowMaskMismatch;
+    }
+
+    public boolean allowExtensions() {
+        return allowExtensions;
+    }
+
+    public boolean closeOnProtocolViolation() {
+        return closeOnProtocolViolation;
+    }
+
+    public boolean withUTF8Validator() {
+        return withUTF8Validator;
+    }
+
+    @Override
+    public String toString() {
+        return "WebSocketDecoderConfig" +
+            " [maxFramePayloadLength=" + maxFramePayloadLength +
+            ", expectMaskedFrames=" + expectMaskedFrames +
+            ", allowMaskMismatch=" + allowMaskMismatch +
+            ", allowExtensions=" + allowExtensions +
+            ", closeOnProtocolViolation=" + closeOnProtocolViolation +
+            ", withUTF8Validator=" + withUTF8Validator +
+            "]";
+    }
+
+    public Builder toBuilder() {
+        return new Builder(this);
+    }
+
+    public static Builder newBuilder() {
+        return new Builder(DEFAULT);
+    }
+
+    public static final class Builder {
+        private int maxFramePayloadLength;
+        private boolean expectMaskedFrames;
+        private boolean allowMaskMismatch;
+        private boolean allowExtensions;
+        private boolean closeOnProtocolViolation;
+        private boolean withUTF8Validator;
+
+        private Builder(WebSocketDecoderConfig decoderConfig) {
+            ObjectUtil.checkNotNull(decoderConfig, "decoderConfig");
+            maxFramePayloadLength = decoderConfig.maxFramePayloadLength();
+            expectMaskedFrames = decoderConfig.expectMaskedFrames();
+            allowMaskMismatch = decoderConfig.allowMaskMismatch();
+            allowExtensions = decoderConfig.allowExtensions();
+            closeOnProtocolViolation = decoderConfig.closeOnProtocolViolation();
+            withUTF8Validator = decoderConfig.withUTF8Validator();
+        }
+
+        public Builder maxFramePayloadLength(int maxFramePayloadLength) {
+            this.maxFramePayloadLength = maxFramePayloadLength;
+            return this;
+        }
+
+        public Builder expectMaskedFrames(boolean expectMaskedFrames) {
+            this.expectMaskedFrames = expectMaskedFrames;
+            return this;
+        }
+
+        public Builder allowMaskMismatch(boolean allowMaskMismatch) {
+            this.allowMaskMismatch = allowMaskMismatch;
+            return this;
+        }
+
+        public Builder allowExtensions(boolean allowExtensions) {
+            this.allowExtensions = allowExtensions;
+            return this;
+        }
+
+        public Builder closeOnProtocolViolation(boolean closeOnProtocolViolation) {
+            this.closeOnProtocolViolation = closeOnProtocolViolation;
+            return this;
+        }
+
+        public Builder withUTF8Validator(boolean withUTF8Validator) {
+            this.withUTF8Validator = withUTF8Validator;
+            return this;
+        }
+
+        public WebSocketDecoderConfig build() {
+            return new WebSocketDecoderConfig(
+                    maxFramePayloadLength, expectMaskedFrames, allowMaskMismatch,
+                    allowExtensions, closeOnProtocolViolation, withUTF8Validator);
+        }
+    }
+}
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketFrame.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketFrame.java
index 677f047..b6f9a55 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketFrame.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketFrame.java
@@ -20,7 +20,7 @@ import io.netty.buffer.DefaultByteBufHolder;
 import io.netty.util.internal.StringUtil;
 
 /**
- * Base class for web socket frames
+ * Base class for web socket frames.
  */
 public abstract class WebSocketFrame extends DefaultByteBufHolder {
 
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketProtocolHandler.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketProtocolHandler.java
index 53532ca..ccbccac 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketProtocolHandler.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketProtocolHandler.java
@@ -16,14 +16,27 @@
 package io.netty.handler.codec.http.websocketx;
 
 
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelFutureListener;
 import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelOutboundHandler;
+import io.netty.channel.ChannelPromise;
 import io.netty.handler.codec.MessageToMessageDecoder;
+import io.netty.util.ReferenceCountUtil;
+import io.netty.util.concurrent.ScheduledFuture;
 
+import java.net.SocketAddress;
+import java.nio.channels.ClosedChannelException;
 import java.util.List;
+import java.util.concurrent.TimeUnit;
 
-abstract class WebSocketProtocolHandler extends MessageToMessageDecoder<WebSocketFrame> {
+abstract class WebSocketProtocolHandler extends MessageToMessageDecoder<WebSocketFrame>
+        implements ChannelOutboundHandler {
 
     private final boolean dropPongFrames;
+    private final WebSocketCloseStatus closeStatus;
+    private final long forceCloseTimeoutMillis;
+    private ChannelPromise closeSent;
 
     /**
      * Creates a new {@link WebSocketProtocolHandler} that will <i>drop</i> {@link PongWebSocketFrame}s.
@@ -40,7 +53,15 @@ abstract class WebSocketProtocolHandler extends MessageToMessageDecoder<WebSocke
      *            {@code true} if {@link PongWebSocketFrame}s should be dropped
      */
     WebSocketProtocolHandler(boolean dropPongFrames) {
+        this(dropPongFrames, null, 0L);
+    }
+
+    WebSocketProtocolHandler(boolean dropPongFrames,
+                             WebSocketCloseStatus closeStatus,
+                             long forceCloseTimeoutMillis) {
         this.dropPongFrames = dropPongFrames;
+        this.closeStatus = closeStatus;
+        this.forceCloseTimeoutMillis = forceCloseTimeoutMillis;
     }
 
     @Override
@@ -48,15 +69,111 @@ abstract class WebSocketProtocolHandler extends MessageToMessageDecoder<WebSocke
         if (frame instanceof PingWebSocketFrame) {
             frame.content().retain();
             ctx.channel().writeAndFlush(new PongWebSocketFrame(frame.content()));
+            readIfNeeded(ctx);
             return;
         }
         if (frame instanceof PongWebSocketFrame && dropPongFrames) {
+            readIfNeeded(ctx);
             return;
         }
 
         out.add(frame.retain());
     }
 
+    private static void readIfNeeded(ChannelHandlerContext ctx) {
+        if (!ctx.channel().config().isAutoRead()) {
+            ctx.read();
+        }
+    }
+
+    @Override
+    public void close(final ChannelHandlerContext ctx, final ChannelPromise promise) throws Exception {
+        if (closeStatus == null || !ctx.channel().isActive()) {
+            ctx.close(promise);
+        } else {
+            if (closeSent == null) {
+                write(ctx, new CloseWebSocketFrame(closeStatus), ctx.newPromise());
+            }
+            flush(ctx);
+            applyCloseSentTimeout(ctx);
+            closeSent.addListener(new ChannelFutureListener() {
+                @Override
+                public void operationComplete(ChannelFuture future) {
+                    ctx.close(promise);
+                }
+            });
+        }
+    }
+
+    @Override
+    public void write(final ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
+        if (closeSent != null) {
+            ReferenceCountUtil.release(msg);
+            promise.setFailure(new ClosedChannelException());
+            return;
+        }
+        if (msg instanceof CloseWebSocketFrame) {
+            promise = promise.unvoid();
+            closeSent = promise;
+        }
+        ctx.write(msg, promise);
+    }
+
+    private void applyCloseSentTimeout(ChannelHandlerContext ctx) {
+        if (closeSent.isDone() || forceCloseTimeoutMillis < 0) {
+            return;
+        }
+
+        final ScheduledFuture<?> timeoutTask = ctx.executor().schedule(new Runnable() {
+            @Override
+            public void run() {
+                if (!closeSent.isDone()) {
+                    closeSent.tryFailure(new WebSocketHandshakeException("send close frame timed out"));
+                }
+            }
+        }, forceCloseTimeoutMillis, TimeUnit.MILLISECONDS);
+
+        closeSent.addListener(new ChannelFutureListener() {
+            @Override
+            public void operationComplete(ChannelFuture future) {
+                timeoutTask.cancel(false);
+            }
+        });
+    }
+
+    @Override
+    public void bind(ChannelHandlerContext ctx, SocketAddress localAddress,
+                     ChannelPromise promise) throws Exception {
+        ctx.bind(localAddress, promise);
+    }
+
+    @Override
+    public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress,
+                        SocketAddress localAddress, ChannelPromise promise) throws Exception {
+        ctx.connect(remoteAddress, localAddress, promise);
+    }
+
+    @Override
+    public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise)
+            throws Exception {
+        ctx.disconnect(promise);
+    }
+
+    @Override
+    public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
+        ctx.deregister(promise);
+    }
+
+    @Override
+    public void read(ChannelHandlerContext ctx) throws Exception {
+        ctx.read();
+    }
+
+    @Override
+    public void flush(ChannelHandlerContext ctx) throws Exception {
+        ctx.flush();
+    }
+
     @Override
     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
         ctx.fireExceptionCaught(cause);
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker.java
index 4eb4348..c6b1b0e 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012 The Netty Project
+ * Copyright 2019 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
@@ -33,7 +33,7 @@ import io.netty.handler.codec.http.HttpResponseEncoder;
 import io.netty.handler.codec.http.HttpServerCodec;
 import io.netty.util.ReferenceCountUtil;
 import io.netty.util.internal.EmptyArrays;
-import io.netty.util.internal.ThrowableUtil;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 
@@ -47,8 +47,6 @@ import java.util.Set;
  */
 public abstract class WebSocketServerHandshaker {
     protected static final InternalLogger logger = InternalLoggerFactory.getInstance(WebSocketServerHandshaker.class);
-    private static final ClosedChannelException CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace(
-            new ClosedChannelException(), WebSocketServerHandshaker.class, "handshake(...)");
 
     private final String uri;
 
@@ -56,7 +54,7 @@ public abstract class WebSocketServerHandshaker {
 
     private final WebSocketVersion version;
 
-    private final int maxFramePayloadLength;
+    private final WebSocketDecoderConfig decoderConfig;
 
     private String selectedSubprotocol;
 
@@ -81,6 +79,26 @@ public abstract class WebSocketServerHandshaker {
     protected WebSocketServerHandshaker(
             WebSocketVersion version, String uri, String subprotocols,
             int maxFramePayloadLength) {
+        this(version, uri, subprotocols, WebSocketDecoderConfig.newBuilder()
+            .maxFramePayloadLength(maxFramePayloadLength)
+            .build());
+    }
+
+    /**
+     * Constructor specifying the destination web socket location
+     *
+     * @param version
+     *            the protocol version
+     * @param uri
+     *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
+     *            sent to this URL.
+     * @param subprotocols
+     *            CSV of supported protocols. Null if sub protocols not supported.
+     * @param decoderConfig
+     *            Frames decoder configuration.
+     */
+    protected WebSocketServerHandshaker(
+            WebSocketVersion version, String uri, String subprotocols, WebSocketDecoderConfig decoderConfig) {
         this.version = version;
         this.uri = uri;
         if (subprotocols != null) {
@@ -92,7 +110,7 @@ public abstract class WebSocketServerHandshaker {
         } else {
             this.subprotocols = EmptyArrays.EMPTY_STRINGS;
         }
-        this.maxFramePayloadLength = maxFramePayloadLength;
+        this.decoderConfig = ObjectUtil.checkNotNull(decoderConfig, "decoderConfig");
     }
 
     /**
@@ -124,7 +142,16 @@ public abstract class WebSocketServerHandshaker {
      * @return The maximum length for a frame's payload
      */
     public int maxFramePayloadLength() {
-        return maxFramePayloadLength;
+        return decoderConfig.maxFramePayloadLength();
+    }
+
+    /**
+     * Gets this decoder configuration.
+     *
+     * @return This decoder configuration.
+     */
+    public WebSocketDecoderConfig decoderConfig() {
+        return decoderConfig;
     }
 
     /**
@@ -175,15 +202,15 @@ public abstract class WebSocketServerHandshaker {
         ChannelHandlerContext ctx = p.context(HttpRequestDecoder.class);
         final String encoderName;
         if (ctx == null) {
-            // this means the user use a HttpServerCodec
+            // this means the user use an HttpServerCodec
             ctx = p.context(HttpServerCodec.class);
             if (ctx == null) {
                 promise.setFailure(
                         new IllegalStateException("No HttpDecoder and no HttpServerCodec in the pipeline"));
                 return promise;
             }
-            p.addBefore(ctx.name(), "wsdecoder", newWebsocketDecoder());
             p.addBefore(ctx.name(), "wsencoder", newWebSocketEncoder());
+            p.addBefore(ctx.name(), "wsdecoder", newWebsocketDecoder());
             encoderName = ctx.name();
         } else {
             p.replace(ctx.name(), "wsdecoder", newWebsocketDecoder());
@@ -249,7 +276,7 @@ public abstract class WebSocketServerHandshaker {
         ChannelPipeline p = channel.pipeline();
         ChannelHandlerContext ctx = p.context(HttpRequestDecoder.class);
         if (ctx == null) {
-            // this means the user use a HttpServerCodec
+            // this means the user use an HttpServerCodec
             ctx = p.context(HttpServerCodec.class);
             if (ctx == null) {
                 promise.setFailure(
@@ -282,7 +309,9 @@ public abstract class WebSocketServerHandshaker {
             @Override
             public void channelInactive(ChannelHandlerContext ctx) throws Exception {
                 // Fail promise if Channel was closed
-                promise.tryFailure(CLOSED_CHANNEL_EXCEPTION);
+                if (!promise.isDone()) {
+                    promise.tryFailure(new ClosedChannelException());
+                }
                 ctx.fireChannelInactive();
             }
         });
@@ -308,9 +337,7 @@ public abstract class WebSocketServerHandshaker {
      *            Closing Frame that was received
      */
     public ChannelFuture close(Channel channel, CloseWebSocketFrame frame) {
-        if (channel == null) {
-            throw new NullPointerException("channel");
-        }
+        ObjectUtil.checkNotNull(channel, "channel");
         return close(channel, frame, channel.newPromise());
     }
 
@@ -325,9 +352,7 @@ public abstract class WebSocketServerHandshaker {
      *            the {@link ChannelPromise} to be notified when the closing handshake is done
      */
     public ChannelFuture close(Channel channel, CloseWebSocketFrame frame, ChannelPromise promise) {
-        if (channel == null) {
-            throw new NullPointerException("channel");
-        }
+        ObjectUtil.checkNotNull(channel, "channel");
         return channel.writeAndFlush(frame, promise).addListener(ChannelFutureListener.CLOSE);
     }
 
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker00.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker00.java
index ff1797d..7f2cbe2 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker00.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker00.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012 The Netty Project
+ * Copyright 2019 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
@@ -60,7 +60,24 @@ public class WebSocketServerHandshaker00 extends WebSocketServerHandshaker {
      *            reduce denial of service attacks using long data frames.
      */
     public WebSocketServerHandshaker00(String webSocketURL, String subprotocols, int maxFramePayloadLength) {
-        super(WebSocketVersion.V00, webSocketURL, subprotocols, maxFramePayloadLength);
+        this(webSocketURL, subprotocols, WebSocketDecoderConfig.newBuilder()
+            .maxFramePayloadLength(maxFramePayloadLength)
+            .build());
+    }
+
+    /**
+     * Constructor specifying the destination web socket location
+     *
+     * @param webSocketURL
+     *            URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web socket frames will be
+     *            sent to this URL.
+     * @param subprotocols
+     *            CSV of supported protocols
+     * @param decoderConfig
+     *            Frames decoder configuration.
+     */
+    public WebSocketServerHandshaker00(String webSocketURL, String subprotocols, WebSocketDecoderConfig decoderConfig) {
+        super(WebSocketVersion.V00, webSocketURL, subprotocols, decoderConfig);
     }
 
     /**
@@ -116,9 +133,16 @@ public class WebSocketServerHandshaker00 extends WebSocketServerHandshaker {
         boolean isHixie76 = req.headers().contains(HttpHeaderNames.SEC_WEBSOCKET_KEY1) &&
                             req.headers().contains(HttpHeaderNames.SEC_WEBSOCKET_KEY2);
 
+        String origin = req.headers().get(HttpHeaderNames.ORIGIN);
+        //throw before allocating FullHttpResponse
+        if (origin == null && !isHixie76) {
+            throw new WebSocketHandshakeException("Missing origin header, got only " + req.headers().names());
+        }
+
         // Create the WebSocket handshake response.
         FullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, new HttpResponseStatus(101,
-                isHixie76 ? "WebSocket Protocol Handshake" : "Web Socket Protocol Handshake"));
+                isHixie76 ? "WebSocket Protocol Handshake" : "Web Socket Protocol Handshake"),
+                req.content().alloc().buffer(0));
         if (headers != null) {
             res.headers().add(headers);
         }
@@ -129,7 +153,7 @@ public class WebSocketServerHandshaker00 extends WebSocketServerHandshaker {
         // Fill in the headers and contents depending on handshake getMethod.
         if (isHixie76) {
             // New handshake getMethod with a challenge:
-            res.headers().add(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN, req.headers().get(HttpHeaderNames.ORIGIN));
+            res.headers().add(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN, origin);
             res.headers().add(HttpHeaderNames.SEC_WEBSOCKET_LOCATION, uri());
 
             String subprotocols = req.headers().get(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL);
@@ -152,14 +176,14 @@ public class WebSocketServerHandshaker00 extends WebSocketServerHandshaker {
             int b = (int) (Long.parseLong(BEGINNING_DIGIT.matcher(key2).replaceAll("")) /
                            BEGINNING_SPACE.matcher(key2).replaceAll("").length());
             long c = req.content().readLong();
-            ByteBuf input = Unpooled.buffer(16);
+            ByteBuf input = Unpooled.wrappedBuffer(new byte[16]).setIndex(0, 0);
             input.writeInt(a);
             input.writeInt(b);
             input.writeLong(c);
             res.content().writeBytes(WebSocketUtil.md5(input.array()));
         } else {
             // Old Hixie 75 handshake getMethod with no challenge:
-            res.headers().add(HttpHeaderNames.WEBSOCKET_ORIGIN, req.headers().get(HttpHeaderNames.ORIGIN));
+            res.headers().add(HttpHeaderNames.WEBSOCKET_ORIGIN, origin);
             res.headers().add(HttpHeaderNames.WEBSOCKET_LOCATION, uri());
 
             String protocol = req.headers().get(HttpHeaderNames.WEBSOCKET_PROTOCOL);
@@ -185,7 +209,7 @@ public class WebSocketServerHandshaker00 extends WebSocketServerHandshaker {
 
     @Override
     protected WebSocketFrameDecoder newWebsocketDecoder() {
-        return new WebSocket00FrameDecoder(maxFramePayloadLength());
+        return new WebSocket00FrameDecoder(decoderConfig());
     }
 
     @Override
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker07.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker07.java
index 6529109..54f8ff4 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker07.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker07.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012 The Netty Project
+ * Copyright 2019 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
@@ -37,9 +37,6 @@ public class WebSocketServerHandshaker07 extends WebSocketServerHandshaker {
 
     public static final String WEBSOCKET_07_ACCEPT_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
 
-    private final boolean allowExtensions;
-    private final boolean allowMaskMismatch;
-
     /**
      * Constructor specifying the destination web socket location
      *
@@ -79,9 +76,21 @@ public class WebSocketServerHandshaker07 extends WebSocketServerHandshaker {
     public WebSocketServerHandshaker07(
             String webSocketURL, String subprotocols, boolean allowExtensions, int maxFramePayloadLength,
             boolean allowMaskMismatch) {
-        super(WebSocketVersion.V07, webSocketURL, subprotocols, maxFramePayloadLength);
-        this.allowExtensions = allowExtensions;
-        this.allowMaskMismatch = allowMaskMismatch;
+        this(webSocketURL, subprotocols, WebSocketDecoderConfig.newBuilder()
+            .allowExtensions(allowExtensions)
+            .maxFramePayloadLength(maxFramePayloadLength)
+            .allowMaskMismatch(allowMaskMismatch)
+            .build());
+    }
+
+    /**
+     * Constructor specifying the destination web socket location
+     *
+     * @param decoderConfig
+     *            Frames decoder configuration.
+     */
+    public WebSocketServerHandshaker07(String webSocketURL, String subprotocols, WebSocketDecoderConfig decoderConfig) {
+        super(WebSocketVersion.V07, webSocketURL, subprotocols, decoderConfig);
     }
 
     /**
@@ -119,18 +128,19 @@ public class WebSocketServerHandshaker07 extends WebSocketServerHandshaker {
      */
     @Override
     protected FullHttpResponse newHandshakeResponse(FullHttpRequest req, HttpHeaders headers) {
+        CharSequence key = req.headers().get(HttpHeaderNames.SEC_WEBSOCKET_KEY);
+        if (key == null) {
+            throw new WebSocketHandshakeException("not a WebSocket request: missing key");
+        }
 
         FullHttpResponse res =
-                new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.SWITCHING_PROTOCOLS);
+                new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.SWITCHING_PROTOCOLS,
+                        req.content().alloc().buffer(0));
 
         if (headers != null) {
             res.headers().add(headers);
         }
 
-        CharSequence key = req.headers().get(HttpHeaderNames.SEC_WEBSOCKET_KEY);
-        if (key == null) {
-            throw new WebSocketHandshakeException("not a WebSocket request: missing key");
-        }
         String acceptSeed = key + WEBSOCKET_07_ACCEPT_GUID;
         byte[] sha1 = WebSocketUtil.sha1(acceptSeed.getBytes(CharsetUtil.US_ASCII));
         String accept = WebSocketUtil.base64(sha1);
@@ -159,7 +169,7 @@ public class WebSocketServerHandshaker07 extends WebSocketServerHandshaker {
 
     @Override
     protected WebSocketFrameDecoder newWebsocketDecoder() {
-        return new WebSocket07FrameDecoder(true, allowExtensions, maxFramePayloadLength(), allowMaskMismatch);
+        return new WebSocket07FrameDecoder(decoderConfig());
     }
 
     @Override
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker08.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker08.java
index f0b58f8..1e454f4 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker08.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker08.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012 The Netty Project
+ * Copyright 2019 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
@@ -37,9 +37,6 @@ public class WebSocketServerHandshaker08 extends WebSocketServerHandshaker {
 
     public static final String WEBSOCKET_08_ACCEPT_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
 
-    private final boolean allowExtensions;
-    private final boolean allowMaskMismatch;
-
     /**
      * Constructor specifying the destination web socket location
      *
@@ -79,9 +76,27 @@ public class WebSocketServerHandshaker08 extends WebSocketServerHandshaker {
     public WebSocketServerHandshaker08(
             String webSocketURL, String subprotocols, boolean allowExtensions, int maxFramePayloadLength,
             boolean allowMaskMismatch) {
-        super(WebSocketVersion.V08, webSocketURL, subprotocols, maxFramePayloadLength);
-        this.allowExtensions = allowExtensions;
-        this.allowMaskMismatch = allowMaskMismatch;
+        this(webSocketURL, subprotocols, WebSocketDecoderConfig.newBuilder()
+            .allowExtensions(allowExtensions)
+            .maxFramePayloadLength(maxFramePayloadLength)
+            .allowMaskMismatch(allowMaskMismatch)
+            .build());
+    }
+
+    /**
+     * Constructor specifying the destination web socket location
+     *
+     * @param webSocketURL
+     *            URL for web socket communications. e.g "ws://myhost.com/mypath".
+     *            Subsequent web socket frames will be sent to this URL.
+     * @param subprotocols
+     *            CSV of supported protocols
+     * @param decoderConfig
+     *            Frames decoder configuration.
+     */
+    public WebSocketServerHandshaker08(
+        String webSocketURL, String subprotocols, WebSocketDecoderConfig decoderConfig) {
+        super(WebSocketVersion.V08, webSocketURL, subprotocols, decoderConfig);
     }
 
     /**
@@ -120,16 +135,18 @@ public class WebSocketServerHandshaker08 extends WebSocketServerHandshaker {
      */
     @Override
     protected FullHttpResponse newHandshakeResponse(FullHttpRequest req, HttpHeaders headers) {
-        FullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.SWITCHING_PROTOCOLS);
+        CharSequence key = req.headers().get(HttpHeaderNames.SEC_WEBSOCKET_KEY);
+        if (key == null) {
+            throw new WebSocketHandshakeException("not a WebSocket request: missing key");
+        }
+
+        FullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.SWITCHING_PROTOCOLS,
+                req.content().alloc().buffer(0));
 
         if (headers != null) {
             res.headers().add(headers);
         }
 
-        CharSequence key = req.headers().get(HttpHeaderNames.SEC_WEBSOCKET_KEY);
-        if (key == null) {
-            throw new WebSocketHandshakeException("not a WebSocket request: missing key");
-        }
         String acceptSeed = key + WEBSOCKET_08_ACCEPT_GUID;
         byte[] sha1 = WebSocketUtil.sha1(acceptSeed.getBytes(CharsetUtil.US_ASCII));
         String accept = WebSocketUtil.base64(sha1);
@@ -158,7 +175,7 @@ public class WebSocketServerHandshaker08 extends WebSocketServerHandshaker {
 
     @Override
     protected WebSocketFrameDecoder newWebsocketDecoder() {
-        return new WebSocket08FrameDecoder(true, allowExtensions, maxFramePayloadLength(), allowMaskMismatch);
+        return new WebSocket08FrameDecoder(decoderConfig());
     }
 
     @Override
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker13.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker13.java
index f36d06c..8b445be 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker13.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker13.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012 The Netty Project
+ * Copyright 2019 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
@@ -28,17 +28,14 @@ import static io.netty.handler.codec.http.HttpVersion.*;
 
 /**
  * <p>
- * Performs server side opening and closing handshakes for <a href="http://netty.io/s/rfc6455">RFC 6455</a>
- * (originally web socket specification <a href="http://netty.io/s/ws-17">draft-ietf-hybi-thewebsocketprotocol-17</a>).
+ * Performs server side opening and closing handshakes for <a href="https://netty.io/s/rfc6455">RFC 6455</a>
+ * (originally web socket specification <a href="https://netty.io/s/ws-17">draft-ietf-hybi-thewebsocketprotocol-17</a>).
  * </p>
  */
 public class WebSocketServerHandshaker13 extends WebSocketServerHandshaker {
 
     public static final String WEBSOCKET_13_ACCEPT_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
 
-    private final boolean allowExtensions;
-    private final boolean allowMaskMismatch;
-
     /**
      * Constructor specifying the destination web socket location
      *
@@ -78,9 +75,27 @@ public class WebSocketServerHandshaker13 extends WebSocketServerHandshaker {
     public WebSocketServerHandshaker13(
             String webSocketURL, String subprotocols, boolean allowExtensions, int maxFramePayloadLength,
             boolean allowMaskMismatch) {
-        super(WebSocketVersion.V13, webSocketURL, subprotocols, maxFramePayloadLength);
-        this.allowExtensions = allowExtensions;
-        this.allowMaskMismatch = allowMaskMismatch;
+        this(webSocketURL, subprotocols, WebSocketDecoderConfig.newBuilder()
+            .allowExtensions(allowExtensions)
+            .maxFramePayloadLength(maxFramePayloadLength)
+            .allowMaskMismatch(allowMaskMismatch)
+            .build());
+    }
+
+    /**
+     * Constructor specifying the destination web socket location
+     *
+     * @param webSocketURL
+     *        URL for web socket communications. e.g "ws://myhost.com/mypath". Subsequent web
+     *        socket frames will be sent to this URL.
+     * @param subprotocols
+     *        CSV of supported protocols
+     * @param decoderConfig
+     *            Frames decoder configuration.
+     */
+    public WebSocketServerHandshaker13(
+            String webSocketURL, String subprotocols, WebSocketDecoderConfig decoderConfig) {
+        super(WebSocketVersion.V13, webSocketURL, subprotocols, decoderConfig);
     }
 
     /**
@@ -100,7 +115,7 @@ public class WebSocketServerHandshaker13 extends WebSocketServerHandshaker {
      * Upgrade: websocket
      * Connection: Upgrade
      * Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
-     * Sec-WebSocket-Origin: http://example.com
+     * Origin: http://example.com
      * Sec-WebSocket-Protocol: chat, superchat
      * Sec-WebSocket-Version: 13
      * </pre>
@@ -119,15 +134,17 @@ public class WebSocketServerHandshaker13 extends WebSocketServerHandshaker {
      */
     @Override
     protected FullHttpResponse newHandshakeResponse(FullHttpRequest req, HttpHeaders headers) {
-        FullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.SWITCHING_PROTOCOLS);
-        if (headers != null) {
-            res.headers().add(headers);
-        }
-
         CharSequence key = req.headers().get(HttpHeaderNames.SEC_WEBSOCKET_KEY);
         if (key == null) {
             throw new WebSocketHandshakeException("not a WebSocket request: missing key");
         }
+
+        FullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.SWITCHING_PROTOCOLS,
+                req.content().alloc().buffer(0));
+        if (headers != null) {
+            res.headers().add(headers);
+        }
+
         String acceptSeed = key + WEBSOCKET_13_ACCEPT_GUID;
         byte[] sha1 = WebSocketUtil.sha1(acceptSeed.getBytes(CharsetUtil.US_ASCII));
         String accept = WebSocketUtil.base64(sha1);
@@ -156,7 +173,7 @@ public class WebSocketServerHandshaker13 extends WebSocketServerHandshaker {
 
     @Override
     protected WebSocketFrameDecoder newWebsocketDecoder() {
-        return new WebSocket13FrameDecoder(true, allowExtensions, maxFramePayloadLength(), allowMaskMismatch);
+        return new WebSocket13FrameDecoder(decoderConfig());
     }
 
     @Override
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshakerFactory.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshakerFactory.java
index 27fdfa0..8f1740f 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshakerFactory.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshakerFactory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012 The Netty Project
+ * Copyright 2019 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
@@ -25,6 +25,7 @@ import io.netty.handler.codec.http.HttpRequest;
 import io.netty.handler.codec.http.HttpResponse;
 import io.netty.handler.codec.http.HttpResponseStatus;
 import io.netty.handler.codec.http.HttpVersion;
+import io.netty.util.internal.ObjectUtil;
 
 /**
  * Auto-detects the version of the Web Socket protocol in use and creates a new proper
@@ -36,11 +37,7 @@ public class WebSocketServerHandshakerFactory {
 
     private final String subprotocols;
 
-    private final boolean allowExtensions;
-
-    private final int maxFramePayloadLength;
-
-    private final boolean allowMaskMismatch;
+    private final WebSocketDecoderConfig decoderConfig;
 
     /**
      * Constructor specifying the destination web socket location
@@ -98,11 +95,29 @@ public class WebSocketServerHandshakerFactory {
     public WebSocketServerHandshakerFactory(
             String webSocketURL, String subprotocols, boolean allowExtensions,
             int maxFramePayloadLength, boolean allowMaskMismatch) {
+        this(webSocketURL, subprotocols, WebSocketDecoderConfig.newBuilder()
+            .allowExtensions(allowExtensions)
+            .maxFramePayloadLength(maxFramePayloadLength)
+            .allowMaskMismatch(allowMaskMismatch)
+            .build());
+    }
+
+    /**
+     * Constructor specifying the destination web socket location
+     *
+     * @param webSocketURL
+     *            URL for web socket communications. e.g "ws://myhost.com/mypath".
+     *            Subsequent web socket frames will be sent to this URL.
+     * @param subprotocols
+     *            CSV of supported protocols. Null if sub protocols not supported.
+     * @param decoderConfig
+     *            Frames decoder options.
+     */
+    public WebSocketServerHandshakerFactory(
+            String webSocketURL, String subprotocols, WebSocketDecoderConfig decoderConfig) {
         this.webSocketURL = webSocketURL;
         this.subprotocols = subprotocols;
-        this.allowExtensions = allowExtensions;
-        this.maxFramePayloadLength = maxFramePayloadLength;
-        this.allowMaskMismatch = allowMaskMismatch;
+        this.decoderConfig = ObjectUtil.checkNotNull(decoderConfig, "decoderConfig");
     }
 
     /**
@@ -118,21 +133,21 @@ public class WebSocketServerHandshakerFactory {
             if (version.equals(WebSocketVersion.V13.toHttpHeaderValue())) {
                 // Version 13 of the wire protocol - RFC 6455 (version 17 of the draft hybi specification).
                 return new WebSocketServerHandshaker13(
-                        webSocketURL, subprotocols, allowExtensions, maxFramePayloadLength, allowMaskMismatch);
+                        webSocketURL, subprotocols, decoderConfig);
             } else if (version.equals(WebSocketVersion.V08.toHttpHeaderValue())) {
                 // Version 8 of the wire protocol - version 10 of the draft hybi specification.
                 return new WebSocketServerHandshaker08(
-                        webSocketURL, subprotocols, allowExtensions, maxFramePayloadLength, allowMaskMismatch);
+                        webSocketURL, subprotocols, decoderConfig);
             } else if (version.equals(WebSocketVersion.V07.toHttpHeaderValue())) {
                 // Version 8 of the wire protocol - version 07 of the draft hybi specification.
                 return new WebSocketServerHandshaker07(
-                        webSocketURL, subprotocols, allowExtensions, maxFramePayloadLength, allowMaskMismatch);
+                        webSocketURL, subprotocols, decoderConfig);
             } else {
                 return null;
             }
         } else {
             // Assume version 00 where version header was not specified
-            return new WebSocketServerHandshaker00(webSocketURL, subprotocols, maxFramePayloadLength);
+            return new WebSocketServerHandshaker00(webSocketURL, subprotocols, decoderConfig);
         }
     }
 
@@ -157,7 +172,7 @@ public class WebSocketServerHandshakerFactory {
     public static ChannelFuture sendUnsupportedVersionResponse(Channel channel, ChannelPromise promise) {
         HttpResponse res = new DefaultFullHttpResponse(
                 HttpVersion.HTTP_1_1,
-                HttpResponseStatus.UPGRADE_REQUIRED);
+                HttpResponseStatus.UPGRADE_REQUIRED, channel.alloc().buffer(0));
         res.headers().set(HttpHeaderNames.SEC_WEBSOCKET_VERSION, WebSocketVersion.V13.toHttpHeaderValue());
         HttpUtil.setContentLength(res, 0);
         return channel.writeAndFlush(res, promise);
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolConfig.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolConfig.java
new file mode 100644
index 0000000..a96d3ac
--- /dev/null
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolConfig.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright 2019 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.http.websocketx;
+
+import io.netty.handler.codec.http.websocketx.WebSocketClientProtocolHandler.ClientHandshakeStateEvent;
+import io.netty.util.internal.ObjectUtil;
+
+import static io.netty.util.internal.ObjectUtil.checkPositive;
+
+/**
+ * WebSocket server configuration.
+ */
+public final class WebSocketServerProtocolConfig {
+
+    static final long DEFAULT_HANDSHAKE_TIMEOUT_MILLIS = 10000L;
+
+    private final String websocketPath;
+    private final String subprotocols;
+    private final boolean checkStartsWith;
+    private final long handshakeTimeoutMillis;
+    private final long forceCloseTimeoutMillis;
+    private final boolean handleCloseFrames;
+    private final WebSocketCloseStatus sendCloseFrame;
+    private final boolean dropPongFrames;
+    private final WebSocketDecoderConfig decoderConfig;
+
+    private WebSocketServerProtocolConfig(
+        String websocketPath,
+        String subprotocols,
+        boolean checkStartsWith,
+        long handshakeTimeoutMillis,
+        long forceCloseTimeoutMillis,
+        boolean handleCloseFrames,
+        WebSocketCloseStatus sendCloseFrame,
+        boolean dropPongFrames,
+        WebSocketDecoderConfig decoderConfig
+    ) {
+        this.websocketPath = websocketPath;
+        this.subprotocols = subprotocols;
+        this.checkStartsWith = checkStartsWith;
+        this.handshakeTimeoutMillis = checkPositive(handshakeTimeoutMillis, "handshakeTimeoutMillis");
+        this.forceCloseTimeoutMillis = forceCloseTimeoutMillis;
+        this.handleCloseFrames = handleCloseFrames;
+        this.sendCloseFrame = sendCloseFrame;
+        this.dropPongFrames = dropPongFrames;
+        this.decoderConfig = decoderConfig == null ? WebSocketDecoderConfig.DEFAULT : decoderConfig;
+    }
+
+    public String websocketPath() {
+        return websocketPath;
+    }
+
+    public String subprotocols() {
+        return subprotocols;
+    }
+
+    public boolean checkStartsWith() {
+        return checkStartsWith;
+    }
+
+    public long handshakeTimeoutMillis() {
+        return handshakeTimeoutMillis;
+    }
+
+    public long forceCloseTimeoutMillis() {
+        return forceCloseTimeoutMillis;
+    }
+
+    public boolean handleCloseFrames() {
+        return handleCloseFrames;
+    }
+
+    public WebSocketCloseStatus sendCloseFrame() {
+        return sendCloseFrame;
+    }
+
+    public boolean dropPongFrames() {
+        return dropPongFrames;
+    }
+
+    public WebSocketDecoderConfig decoderConfig() {
+        return decoderConfig;
+    }
+
+    @Override
+    public String toString() {
+        return "WebSocketServerProtocolConfig" +
+            " {websocketPath=" + websocketPath +
+            ", subprotocols=" + subprotocols +
+            ", checkStartsWith=" + checkStartsWith +
+            ", handshakeTimeoutMillis=" + handshakeTimeoutMillis +
+            ", forceCloseTimeoutMillis=" + forceCloseTimeoutMillis +
+            ", handleCloseFrames=" + handleCloseFrames +
+            ", sendCloseFrame=" + sendCloseFrame +
+            ", dropPongFrames=" + dropPongFrames +
+            ", decoderConfig=" + decoderConfig +
+            "}";
+    }
+
+    public Builder toBuilder() {
+        return new Builder(this);
+    }
+
+    public static Builder newBuilder() {
+        return new Builder("/", null, false, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS, 0L,
+                           true, WebSocketCloseStatus.NORMAL_CLOSURE, true, WebSocketDecoderConfig.DEFAULT);
+    }
+
+    public static final class Builder {
+        private String websocketPath;
+        private String subprotocols;
+        private boolean checkStartsWith;
+        private long handshakeTimeoutMillis;
+        private long forceCloseTimeoutMillis;
+        private boolean handleCloseFrames;
+        private WebSocketCloseStatus sendCloseFrame;
+        private boolean dropPongFrames;
+        private WebSocketDecoderConfig decoderConfig;
+        private WebSocketDecoderConfig.Builder decoderConfigBuilder;
+
+        private Builder(WebSocketServerProtocolConfig serverConfig) {
+            this(ObjectUtil.checkNotNull(serverConfig, "serverConfig").websocketPath(),
+                 serverConfig.subprotocols(),
+                 serverConfig.checkStartsWith(),
+                 serverConfig.handshakeTimeoutMillis(),
+                 serverConfig.forceCloseTimeoutMillis(),
+                 serverConfig.handleCloseFrames(),
+                 serverConfig.sendCloseFrame(),
+                 serverConfig.dropPongFrames(),
+                 serverConfig.decoderConfig()
+            );
+        }
+
+        private Builder(String websocketPath,
+                        String subprotocols,
+                        boolean checkStartsWith,
+                        long handshakeTimeoutMillis,
+                        long forceCloseTimeoutMillis,
+                        boolean handleCloseFrames,
+                        WebSocketCloseStatus sendCloseFrame,
+                        boolean dropPongFrames,
+                        WebSocketDecoderConfig decoderConfig) {
+            this.websocketPath = websocketPath;
+            this.subprotocols = subprotocols;
+            this.checkStartsWith = checkStartsWith;
+            this.handshakeTimeoutMillis = handshakeTimeoutMillis;
+            this.forceCloseTimeoutMillis = forceCloseTimeoutMillis;
+            this.handleCloseFrames = handleCloseFrames;
+            this.sendCloseFrame = sendCloseFrame;
+            this.dropPongFrames = dropPongFrames;
+            this.decoderConfig = decoderConfig;
+        }
+
+        /**
+         * URI path component to handle websocket upgrade requests on.
+         */
+        public Builder websocketPath(String websocketPath) {
+            this.websocketPath = websocketPath;
+            return this;
+        }
+
+        /**
+         * CSV of supported protocols
+         */
+        public Builder subprotocols(String subprotocols) {
+            this.subprotocols = subprotocols;
+            return this;
+        }
+
+        /**
+         * {@code true} to handle all requests, where URI path component starts from
+         * {@link WebSocketServerProtocolConfig#websocketPath()}, {@code false} for exact match (default).
+         */
+        public Builder checkStartsWith(boolean checkStartsWith) {
+            this.checkStartsWith = checkStartsWith;
+            return this;
+        }
+
+        /**
+         * Handshake timeout in mills, when handshake timeout, will trigger user
+         * event {@link ClientHandshakeStateEvent#HANDSHAKE_TIMEOUT}
+         */
+        public Builder handshakeTimeoutMillis(long handshakeTimeoutMillis) {
+            this.handshakeTimeoutMillis = handshakeTimeoutMillis;
+            return this;
+        }
+
+        /**
+         * Close the connection if it was not closed by the client after timeout specified
+         */
+        public Builder forceCloseTimeoutMillis(long forceCloseTimeoutMillis) {
+            this.forceCloseTimeoutMillis = forceCloseTimeoutMillis;
+            return this;
+        }
+
+        /**
+         * {@code true} if close frames should not be forwarded and just close the channel
+         */
+        public Builder handleCloseFrames(boolean handleCloseFrames) {
+            this.handleCloseFrames = handleCloseFrames;
+            return this;
+        }
+
+        /**
+         * Close frame to send, when close frame was not send manually. Or {@code null} to disable proper close.
+         */
+        public Builder sendCloseFrame(WebSocketCloseStatus sendCloseFrame) {
+            this.sendCloseFrame = sendCloseFrame;
+            return this;
+        }
+
+        /**
+         * {@code true} if pong frames should not be forwarded
+         */
+        public Builder dropPongFrames(boolean dropPongFrames) {
+            this.dropPongFrames = dropPongFrames;
+            return this;
+        }
+
+        /**
+         * Frames decoder configuration.
+         */
+        public Builder decoderConfig(WebSocketDecoderConfig decoderConfig) {
+            this.decoderConfig = decoderConfig == null ? WebSocketDecoderConfig.DEFAULT : decoderConfig;
+            this.decoderConfigBuilder = null;
+            return this;
+        }
+
+        private WebSocketDecoderConfig.Builder decoderConfigBuilder() {
+            if (decoderConfigBuilder == null) {
+                decoderConfigBuilder = decoderConfig.toBuilder();
+            }
+            return decoderConfigBuilder;
+        }
+
+        public Builder maxFramePayloadLength(int maxFramePayloadLength) {
+            decoderConfigBuilder().maxFramePayloadLength(maxFramePayloadLength);
+            return this;
+        }
+
+        public Builder expectMaskedFrames(boolean expectMaskedFrames) {
+            decoderConfigBuilder().expectMaskedFrames(expectMaskedFrames);
+            return this;
+        }
+
+        public Builder allowMaskMismatch(boolean allowMaskMismatch) {
+            decoderConfigBuilder().allowMaskMismatch(allowMaskMismatch);
+            return this;
+        }
+
+        public Builder allowExtensions(boolean allowExtensions) {
+            decoderConfigBuilder().allowExtensions(allowExtensions);
+            return this;
+        }
+
+        public Builder closeOnProtocolViolation(boolean closeOnProtocolViolation) {
+            decoderConfigBuilder().closeOnProtocolViolation(closeOnProtocolViolation);
+            return this;
+        }
+
+        public Builder withUTF8Validator(boolean withUTF8Validator) {
+            decoderConfigBuilder().withUTF8Validator(withUTF8Validator);
+            return this;
+        }
+
+        /**
+         * Build unmodifiable server protocol configuration.
+         */
+        public WebSocketServerProtocolConfig build() {
+            return new WebSocketServerProtocolConfig(
+                websocketPath,
+                subprotocols,
+                checkStartsWith,
+                handshakeTimeoutMillis,
+                forceCloseTimeoutMillis,
+                handleCloseFrames,
+                sendCloseFrame,
+                dropPongFrames,
+                decoderConfigBuilder == null ? decoderConfig : decoderConfigBuilder.build()
+            );
+        }
+    }
+}
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolHandler.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolHandler.java
index 3a19b14..9d7f6ab 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolHandler.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolHandler.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012 The Netty Project
+ * Copyright 2019 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
@@ -18,13 +18,10 @@ package io.netty.handler.codec.http.websocketx;
 import io.netty.buffer.Unpooled;
 import io.netty.channel.Channel;
 import io.netty.channel.ChannelFutureListener;
-import io.netty.channel.ChannelHandler;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelInboundHandler;
-import io.netty.channel.ChannelInboundHandlerAdapter;
 import io.netty.channel.ChannelPipeline;
 import io.netty.handler.codec.http.DefaultFullHttpResponse;
-import io.netty.handler.codec.http.FullHttpRequest;
 import io.netty.handler.codec.http.FullHttpResponse;
 import io.netty.handler.codec.http.HttpHeaders;
 import io.netty.handler.codec.http.HttpResponseStatus;
@@ -32,7 +29,9 @@ import io.netty.util.AttributeKey;
 
 import java.util.List;
 
-import static io.netty.handler.codec.http.HttpVersion.*;
+import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
+import static io.netty.handler.codec.http.websocketx.WebSocketServerProtocolConfig.DEFAULT_HANDSHAKE_TIMEOUT_MILLIS;
+import static io.netty.util.internal.ObjectUtil.checkNotNull;
 
 /**
  * This handler does all the heavy lifting for you to run a websocket server.
@@ -64,7 +63,12 @@ public class WebSocketServerProtocolHandler extends WebSocketProtocolHandler {
          * it provides extra information about the handshake
          */
         @Deprecated
-        HANDSHAKE_COMPLETE
+        HANDSHAKE_COMPLETE,
+
+        /**
+         * The Handshake was timed out
+         */
+        HANDSHAKE_TIMEOUT
     }
 
     /**
@@ -97,54 +101,119 @@ public class WebSocketServerProtocolHandler extends WebSocketProtocolHandler {
     private static final AttributeKey<WebSocketServerHandshaker> HANDSHAKER_ATTR_KEY =
             AttributeKey.valueOf(WebSocketServerHandshaker.class, "HANDSHAKER");
 
-    private final String websocketPath;
-    private final String subprotocols;
-    private final boolean allowExtensions;
-    private final int maxFramePayloadLength;
-    private final boolean allowMaskMismatch;
-    private final boolean checkStartsWith;
+    private final WebSocketServerProtocolConfig serverConfig;
+
+    /**
+     * Base constructor
+     *
+     * @param serverConfig
+     *            Server protocol configuration.
+     */
+    public WebSocketServerProtocolHandler(WebSocketServerProtocolConfig serverConfig) {
+        super(checkNotNull(serverConfig, "serverConfig").dropPongFrames(),
+              serverConfig.sendCloseFrame(),
+              serverConfig.forceCloseTimeoutMillis()
+        );
+        this.serverConfig = serverConfig;
+    }
 
     public WebSocketServerProtocolHandler(String websocketPath) {
-        this(websocketPath, null, false);
+        this(websocketPath, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS);
+    }
+
+    public WebSocketServerProtocolHandler(String websocketPath, long handshakeTimeoutMillis) {
+        this(websocketPath, false, handshakeTimeoutMillis);
     }
 
     public WebSocketServerProtocolHandler(String websocketPath, boolean checkStartsWith) {
-        this(websocketPath, null, false, 65536, false, checkStartsWith);
+        this(websocketPath, checkStartsWith, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS);
+    }
+
+    public WebSocketServerProtocolHandler(String websocketPath, boolean checkStartsWith, long handshakeTimeoutMillis) {
+        this(websocketPath, null, false, 65536, false, checkStartsWith, handshakeTimeoutMillis);
     }
 
     public WebSocketServerProtocolHandler(String websocketPath, String subprotocols) {
-        this(websocketPath, subprotocols, false);
+        this(websocketPath, subprotocols, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS);
+    }
+
+    public WebSocketServerProtocolHandler(String websocketPath, String subprotocols, long handshakeTimeoutMillis) {
+        this(websocketPath, subprotocols, false, handshakeTimeoutMillis);
     }
 
     public WebSocketServerProtocolHandler(String websocketPath, String subprotocols, boolean allowExtensions) {
-        this(websocketPath, subprotocols, allowExtensions, 65536);
+        this(websocketPath, subprotocols, allowExtensions, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS);
+    }
+
+    public WebSocketServerProtocolHandler(String websocketPath, String subprotocols, boolean allowExtensions,
+                                          long handshakeTimeoutMillis) {
+        this(websocketPath, subprotocols, allowExtensions, 65536, handshakeTimeoutMillis);
     }
 
     public WebSocketServerProtocolHandler(String websocketPath, String subprotocols,
                                           boolean allowExtensions, int maxFrameSize) {
-        this(websocketPath, subprotocols, allowExtensions, maxFrameSize, false);
+        this(websocketPath, subprotocols, allowExtensions, maxFrameSize, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS);
+    }
+
+    public WebSocketServerProtocolHandler(String websocketPath, String subprotocols,
+                                          boolean allowExtensions, int maxFrameSize, long handshakeTimeoutMillis) {
+        this(websocketPath, subprotocols, allowExtensions, maxFrameSize, false, handshakeTimeoutMillis);
     }
 
     public WebSocketServerProtocolHandler(String websocketPath, String subprotocols,
             boolean allowExtensions, int maxFrameSize, boolean allowMaskMismatch) {
-        this(websocketPath, subprotocols, allowExtensions, maxFrameSize, allowMaskMismatch, false);
+        this(websocketPath, subprotocols, allowExtensions, maxFrameSize, allowMaskMismatch,
+             DEFAULT_HANDSHAKE_TIMEOUT_MILLIS);
+    }
+
+    public WebSocketServerProtocolHandler(String websocketPath, String subprotocols, boolean allowExtensions,
+                                          int maxFrameSize, boolean allowMaskMismatch, long handshakeTimeoutMillis) {
+        this(websocketPath, subprotocols, allowExtensions, maxFrameSize, allowMaskMismatch, false,
+             handshakeTimeoutMillis);
     }
 
     public WebSocketServerProtocolHandler(String websocketPath, String subprotocols,
             boolean allowExtensions, int maxFrameSize, boolean allowMaskMismatch, boolean checkStartsWith) {
-        this(websocketPath, subprotocols, allowExtensions, maxFrameSize, allowMaskMismatch, checkStartsWith, true);
+        this(websocketPath, subprotocols, allowExtensions, maxFrameSize, allowMaskMismatch, checkStartsWith,
+             DEFAULT_HANDSHAKE_TIMEOUT_MILLIS);
+    }
+
+    public WebSocketServerProtocolHandler(String websocketPath, String subprotocols,
+                                          boolean allowExtensions, int maxFrameSize, boolean allowMaskMismatch,
+                                          boolean checkStartsWith, long handshakeTimeoutMillis) {
+        this(websocketPath, subprotocols, allowExtensions, maxFrameSize, allowMaskMismatch, checkStartsWith, true,
+             handshakeTimeoutMillis);
     }
 
     public WebSocketServerProtocolHandler(String websocketPath, String subprotocols,
                                           boolean allowExtensions, int maxFrameSize, boolean allowMaskMismatch,
                                           boolean checkStartsWith, boolean dropPongFrames) {
-        super(dropPongFrames);
-        this.websocketPath = websocketPath;
-        this.subprotocols = subprotocols;
-        this.allowExtensions = allowExtensions;
-        maxFramePayloadLength = maxFrameSize;
-        this.allowMaskMismatch = allowMaskMismatch;
-        this.checkStartsWith = checkStartsWith;
+        this(websocketPath, subprotocols, allowExtensions, maxFrameSize, allowMaskMismatch, checkStartsWith,
+             dropPongFrames, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS);
+    }
+
+    public WebSocketServerProtocolHandler(String websocketPath, String subprotocols, boolean allowExtensions,
+                                          int maxFrameSize, boolean allowMaskMismatch, boolean checkStartsWith,
+                                          boolean dropPongFrames, long handshakeTimeoutMillis) {
+        this(websocketPath, subprotocols, checkStartsWith, dropPongFrames, handshakeTimeoutMillis,
+            WebSocketDecoderConfig.newBuilder()
+                .maxFramePayloadLength(maxFrameSize)
+                .allowMaskMismatch(allowMaskMismatch)
+                .allowExtensions(allowExtensions)
+                .build());
+    }
+
+    public WebSocketServerProtocolHandler(String websocketPath, String subprotocols, boolean checkStartsWith,
+                                          boolean dropPongFrames, long handshakeTimeoutMillis,
+                                          WebSocketDecoderConfig decoderConfig) {
+        this(WebSocketServerProtocolConfig.newBuilder()
+            .websocketPath(websocketPath)
+            .subprotocols(subprotocols)
+            .checkStartsWith(checkStartsWith)
+            .handshakeTimeoutMillis(handshakeTimeoutMillis)
+            .dropPongFrames(dropPongFrames)
+            .decoderConfig(decoderConfig)
+            .build());
     }
 
     @Override
@@ -152,20 +221,19 @@ public class WebSocketServerProtocolHandler extends WebSocketProtocolHandler {
         ChannelPipeline cp = ctx.pipeline();
         if (cp.get(WebSocketServerProtocolHandshakeHandler.class) == null) {
             // Add the WebSocketHandshakeHandler before this one.
-            ctx.pipeline().addBefore(ctx.name(), WebSocketServerProtocolHandshakeHandler.class.getName(),
-                    new WebSocketServerProtocolHandshakeHandler(websocketPath, subprotocols,
-                            allowExtensions, maxFramePayloadLength, allowMaskMismatch, checkStartsWith));
+            cp.addBefore(ctx.name(), WebSocketServerProtocolHandshakeHandler.class.getName(),
+                    new WebSocketServerProtocolHandshakeHandler(serverConfig));
         }
-        if (cp.get(Utf8FrameValidator.class) == null) {
+        if (serverConfig.decoderConfig().withUTF8Validator() && cp.get(Utf8FrameValidator.class) == null) {
             // Add the UFT8 checking before this one.
-            ctx.pipeline().addBefore(ctx.name(), Utf8FrameValidator.class.getName(),
+            cp.addBefore(ctx.name(), Utf8FrameValidator.class.getName(),
                     new Utf8FrameValidator());
         }
     }
 
     @Override
     protected void decode(ChannelHandlerContext ctx, WebSocketFrame frame, List<Object> out) throws Exception {
-        if (frame instanceof CloseWebSocketFrame) {
+        if (serverConfig.handleCloseFrames() && frame instanceof CloseWebSocketFrame) {
             WebSocketServerHandshaker handshaker = getHandshaker(ctx.channel());
             if (handshaker != null) {
                 frame.retain();
@@ -197,20 +265,4 @@ public class WebSocketServerProtocolHandler extends WebSocketProtocolHandler {
     static void setHandshaker(Channel channel, WebSocketServerHandshaker handshaker) {
         channel.attr(HANDSHAKER_ATTR_KEY).set(handshaker);
     }
-
-    static ChannelHandler forbiddenHttpRequestResponder() {
-        return new ChannelInboundHandlerAdapter() {
-            @Override
-            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
-                if (msg instanceof FullHttpRequest) {
-                    ((FullHttpRequest) msg).release();
-                    FullHttpResponse response =
-                            new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.FORBIDDEN);
-                    ctx.channel().writeAndFlush(response);
-                } else {
-                    ctx.fireChannelRead(msg);
-                }
-            }
-        };
-    }
 }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolHandshakeHandler.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolHandshakeHandler.java
index 26e01f1..ec9b4ff 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolHandshakeHandler.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolHandshakeHandler.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012 The Netty Project
+ * Copyright 2019 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
@@ -20,43 +20,42 @@ import io.netty.channel.ChannelFutureListener;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelInboundHandlerAdapter;
 import io.netty.channel.ChannelPipeline;
+import io.netty.channel.ChannelPromise;
 import io.netty.handler.codec.http.DefaultFullHttpResponse;
 import io.netty.handler.codec.http.FullHttpRequest;
 import io.netty.handler.codec.http.HttpHeaderNames;
 import io.netty.handler.codec.http.HttpRequest;
 import io.netty.handler.codec.http.HttpResponse;
+import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler.ServerHandshakeStateEvent;
 import io.netty.handler.ssl.SslHandler;
+import io.netty.util.concurrent.Future;
+import io.netty.util.concurrent.FutureListener;
+
+import java.util.concurrent.TimeUnit;
 
-import static io.netty.handler.codec.http.HttpUtil.*;
 import static io.netty.handler.codec.http.HttpMethod.*;
 import static io.netty.handler.codec.http.HttpResponseStatus.*;
+import static io.netty.handler.codec.http.HttpUtil.*;
 import static io.netty.handler.codec.http.HttpVersion.*;
+import static io.netty.util.internal.ObjectUtil.*;
 
 /**
  * Handles the HTTP handshake (the HTTP Upgrade request) for {@link WebSocketServerProtocolHandler}.
  */
 class WebSocketServerProtocolHandshakeHandler extends ChannelInboundHandlerAdapter {
 
-    private final String websocketPath;
-    private final String subprotocols;
-    private final boolean allowExtensions;
-    private final int maxFramePayloadSize;
-    private final boolean allowMaskMismatch;
-    private final boolean checkStartsWith;
+    private final WebSocketServerProtocolConfig serverConfig;
+    private ChannelHandlerContext ctx;
+    private ChannelPromise handshakePromise;
 
-    WebSocketServerProtocolHandshakeHandler(String websocketPath, String subprotocols,
-            boolean allowExtensions, int maxFrameSize, boolean allowMaskMismatch) {
-        this(websocketPath, subprotocols, allowExtensions, maxFrameSize, allowMaskMismatch, false);
+    WebSocketServerProtocolHandshakeHandler(WebSocketServerProtocolConfig serverConfig) {
+        this.serverConfig = checkNotNull(serverConfig, "serverConfig");
     }
 
-    WebSocketServerProtocolHandshakeHandler(String websocketPath, String subprotocols,
-            boolean allowExtensions, int maxFrameSize, boolean allowMaskMismatch, boolean checkStartsWith) {
-        this.websocketPath = websocketPath;
-        this.subprotocols = subprotocols;
-        this.allowExtensions = allowExtensions;
-        maxFramePayloadSize = maxFrameSize;
-        this.allowMaskMismatch = allowMaskMismatch;
-        this.checkStartsWith = checkStartsWith;
+    @Override
+    public void handlerAdded(ChannelHandlerContext ctx) {
+        this.ctx = ctx;
+        handshakePromise = ctx.newPromise();
     }
 
     @Override
@@ -68,25 +67,36 @@ class WebSocketServerProtocolHandshakeHandler extends ChannelInboundHandlerAdapt
         }
 
         try {
-            if (req.method() != GET) {
-                sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN));
+            if (!GET.equals(req.method())) {
+                sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN, ctx.alloc().buffer(0)));
                 return;
             }
 
             final WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
-                    getWebSocketLocation(ctx.pipeline(), req, websocketPath), subprotocols,
-                            allowExtensions, maxFramePayloadSize, allowMaskMismatch);
+                    getWebSocketLocation(ctx.pipeline(), req, serverConfig.websocketPath()),
+                    serverConfig.subprotocols(), serverConfig.decoderConfig());
             final WebSocketServerHandshaker handshaker = wsFactory.newHandshaker(req);
+            final ChannelPromise localHandshakePromise = handshakePromise;
             if (handshaker == null) {
                 WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
             } else {
+                // Ensure we set the handshaker and replace this handler before we
+                // trigger the actual handshake. Otherwise we may receive websocket bytes in this handler
+                // before we had a chance to replace it.
+                //
+                // See https://github.com/netty/netty/issues/9471.
+                WebSocketServerProtocolHandler.setHandshaker(ctx.channel(), handshaker);
+                ctx.pipeline().remove(this);
+
                 final ChannelFuture handshakeFuture = handshaker.handshake(ctx.channel(), req);
                 handshakeFuture.addListener(new ChannelFutureListener() {
                     @Override
-                    public void operationComplete(ChannelFuture future) throws Exception {
+                    public void operationComplete(ChannelFuture future) {
                         if (!future.isSuccess()) {
+                            localHandshakePromise.tryFailure(future.cause());
                             ctx.fireExceptionCaught(future.cause());
                         } else {
+                            localHandshakePromise.trySuccess();
                             // Kept for compatibility
                             ctx.fireUserEventTriggered(
                                     WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE);
@@ -96,9 +106,7 @@ class WebSocketServerProtocolHandshakeHandler extends ChannelInboundHandlerAdapt
                         }
                     }
                 });
-                WebSocketServerProtocolHandler.setHandshaker(ctx.channel(), handshaker);
-                ctx.pipeline().replace(this, "WS403Responder",
-                        WebSocketServerProtocolHandler.forbiddenHttpRequestResponder());
+                applyHandshakeTimeout();
             }
         } finally {
             req.release();
@@ -106,7 +114,8 @@ class WebSocketServerProtocolHandshakeHandler extends ChannelInboundHandlerAdapt
     }
 
     private boolean isNotWebSocketPath(FullHttpRequest req) {
-        return checkStartsWith ? !req.uri().startsWith(websocketPath) : !req.uri().equals(websocketPath);
+        String websocketPath = serverConfig.websocketPath();
+        return serverConfig.checkStartsWith() ? !req.uri().startsWith(websocketPath) : !req.uri().equals(websocketPath);
     }
 
     private static void sendHttpResponse(ChannelHandlerContext ctx, HttpRequest req, HttpResponse res) {
@@ -125,4 +134,32 @@ class WebSocketServerProtocolHandshakeHandler extends ChannelInboundHandlerAdapt
         String host = req.headers().get(HttpHeaderNames.HOST);
         return protocol + "://" + host + path;
     }
+
+    private void applyHandshakeTimeout() {
+        final ChannelPromise localHandshakePromise = handshakePromise;
+        final long handshakeTimeoutMillis = serverConfig.handshakeTimeoutMillis();
+        if (handshakeTimeoutMillis <= 0 || localHandshakePromise.isDone()) {
+            return;
+        }
+
+        final Future<?> timeoutFuture = ctx.executor().schedule(new Runnable() {
+            @Override
+            public void run() {
+                if (!localHandshakePromise.isDone() &&
+                        localHandshakePromise.tryFailure(new WebSocketHandshakeException("handshake timed out"))) {
+                    ctx.flush()
+                       .fireUserEventTriggered(ServerHandshakeStateEvent.HANDSHAKE_TIMEOUT)
+                       .close();
+                }
+            }
+        }, handshakeTimeoutMillis, TimeUnit.MILLISECONDS);
+
+        // Cancel the handshake timeout when handshake is finished.
+        localHandshakePromise.addListener(new FutureListener<Void>() {
+            @Override
+            public void operationComplete(Future<Void> f) {
+                timeoutFuture.cancel(false);
+            }
+        });
+    }
 }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketUtil.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketUtil.java
index b6a138b..fbd43eb 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketUtil.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketUtil.java
@@ -21,6 +21,7 @@ import io.netty.handler.codec.base64.Base64;
 import io.netty.util.CharsetUtil;
 import io.netty.util.concurrent.FastThreadLocal;
 import io.netty.util.internal.PlatformDependent;
+import io.netty.util.internal.SuppressJava6Requirement;
 
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
@@ -90,7 +91,11 @@ final class WebSocketUtil {
      * @param data The data to encode
      * @return An encoded string containing the data
      */
+    @SuppressJava6Requirement(reason = "Guarded with java version check")
     static String base64(byte[] data) {
+        if (PlatformDependent.javaVersion() >= 8) {
+            return java.util.Base64.getEncoder().encodeToString(data);
+        }
         ByteBuf encodedData = Unpooled.wrappedBuffer(data);
         ByteBuf encoded = Base64.encode(encodedData);
         String encodedString = encoded.toString(CharsetUtil.UTF_8);
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketVersion.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketVersion.java
index 2cb1c19..ed39608 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketVersion.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/WebSocketVersion.java
@@ -15,6 +15,9 @@
  */
 package io.netty.handler.codec.http.websocketx;
 
+import io.netty.util.AsciiString;
+import io.netty.util.internal.StringUtil;
+
 /**
  * <p>
  * Versions of the web socket specification.
@@ -25,49 +28,50 @@ package io.netty.handler.codec.http.websocketx;
  * </p>
  */
 public enum WebSocketVersion {
-    UNKNOWN,
+    UNKNOWN(AsciiString.cached(StringUtil.EMPTY_STRING)),
 
     /**
      * <a href= "http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00"
      * >draft-ietf-hybi-thewebsocketprotocol- 00</a>.
      */
-    V00,
+    V00(AsciiString.cached("0")),
 
     /**
      * <a href= "http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-07"
      * >draft-ietf-hybi-thewebsocketprotocol- 07</a>
      */
-    V07,
+    V07(AsciiString.cached("7")),
 
     /**
      * <a href= "http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10"
      * >draft-ietf-hybi-thewebsocketprotocol- 10</a>
      */
-    V08,
+    V08(AsciiString.cached("8")),
 
     /**
      * <a href="http://tools.ietf.org/html/rfc6455 ">RFC 6455</a>. This was originally <a href=
      * "http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17" >draft-ietf-hybi-thewebsocketprotocol-
      * 17</a>
      */
-    V13;
+    V13(AsciiString.cached("13"));
+
+    private final AsciiString headerValue;
 
+    WebSocketVersion(AsciiString headerValue) {
+        this.headerValue = headerValue;
+    }
     /**
      * @return Value for HTTP Header 'Sec-WebSocket-Version'
      */
     public String toHttpHeaderValue() {
-        if (this == V00) {
-            return "0";
-        }
-        if (this == V07) {
-            return "7";
-        }
-        if (this == V08) {
-            return "8";
-        }
-        if (this == V13) {
-            return "13";
+        return toAsciiString().toString();
+    }
+
+    AsciiString toAsciiString() {
+        if (this == UNKNOWN) {
+            // Let's special case this to preserve behaviour
+            throw new IllegalStateException("Unknown web socket version: " + this);
         }
-        throw new IllegalStateException("Unknown web socket version: " + this);
+        return headerValue;
     }
 }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketClientExtensionHandler.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketClientExtensionHandler.java
index 92ee9cc..b8b3912 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketClientExtensionHandler.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketClientExtensionHandler.java
@@ -22,6 +22,7 @@ import io.netty.handler.codec.CodecException;
 import io.netty.handler.codec.http.HttpHeaderNames;
 import io.netty.handler.codec.http.HttpRequest;
 import io.netty.handler.codec.http.HttpResponse;
+import io.netty.util.internal.ObjectUtil;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -50,9 +51,7 @@ public class WebSocketClientExtensionHandler extends ChannelDuplexHandler {
      *      with fallback configuration.
      */
     public WebSocketClientExtensionHandler(WebSocketClientExtensionHandshaker... extensionHandshakers) {
-        if (extensionHandshakers == null) {
-            throw new NullPointerException("extensionHandshakers");
-        }
+        ObjectUtil.checkNotNull(extensionHandshakers, "extensionHandshakers");
         if (extensionHandshakers.length == 0) {
             throw new IllegalArgumentException("extensionHandshakers must contains at least one handshaker");
         }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionData.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionData.java
index eb3c586..1f61c68 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionData.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionData.java
@@ -15,6 +15,8 @@
  */
 package io.netty.handler.codec.http.websocketx.extensions;
 
+import io.netty.util.internal.ObjectUtil;
+
 import java.util.Collections;
 import java.util.Map;
 
@@ -29,14 +31,9 @@ public final class WebSocketExtensionData {
     private final Map<String, String> parameters;
 
     public WebSocketExtensionData(String name, Map<String, String> parameters) {
-        if (name == null) {
-            throw new NullPointerException("name");
-        }
-        if (parameters == null) {
-            throw new NullPointerException("parameters");
-        }
-        this.name = name;
-        this.parameters = Collections.unmodifiableMap(parameters);
+        this.name = ObjectUtil.checkNotNull(name, "name");
+        this.parameters = Collections.unmodifiableMap(
+                ObjectUtil.checkNotNull(parameters, "parameters"));
     }
 
     /**
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionFilter.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionFilter.java
new file mode 100644
index 0000000..6485e27
--- /dev/null
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionFilter.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2019 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.http.websocketx.extensions;
+
+import io.netty.handler.codec.http.websocketx.WebSocketFrame;
+
+/**
+ * Filter that is responsible to skip the evaluation of a certain extension
+ * according to standard.
+ */
+public interface WebSocketExtensionFilter {
+
+    /**
+     * A {@link WebSocketExtensionFilter} that never skip the evaluation of an
+     * any given extensions {@link WebSocketExtension}.
+     */
+    WebSocketExtensionFilter NEVER_SKIP = new WebSocketExtensionFilter() {
+        @Override
+        public boolean mustSkip(WebSocketFrame frame) {
+            return false;
+        }
+    };
+
+    /**
+     * A {@link WebSocketExtensionFilter} that always skip the evaluation of an
+     * any given extensions {@link WebSocketExtension}.
+     */
+    WebSocketExtensionFilter ALWAYS_SKIP = new WebSocketExtensionFilter() {
+        @Override
+        public boolean mustSkip(WebSocketFrame frame) {
+            return true;
+        }
+    };
+
+    /**
+     * Returns {@code true} if the evaluation of the extension must skipped
+     * for the given frame otherwise {@code false}.
+     */
+    boolean mustSkip(WebSocketFrame frame);
+
+}
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionFilterProvider.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionFilterProvider.java
new file mode 100644
index 0000000..a5eff2b
--- /dev/null
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionFilterProvider.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2019 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.http.websocketx.extensions;
+
+/**
+ * Extension filter provider that is responsible to provide filters for a certain {@link WebSocketExtension} extension.
+ */
+public interface WebSocketExtensionFilterProvider {
+
+    WebSocketExtensionFilterProvider DEFAULT = new WebSocketExtensionFilterProvider() {
+        @Override
+        public WebSocketExtensionFilter encoderFilter() {
+            return WebSocketExtensionFilter.NEVER_SKIP;
+        }
+
+        @Override
+        public WebSocketExtensionFilter decoderFilter() {
+            return WebSocketExtensionFilter.NEVER_SKIP;
+        }
+    };
+
+    /**
+     * Returns the extension filter for {@link WebSocketExtensionEncoder} encoder.
+     */
+    WebSocketExtensionFilter encoderFilter();
+
+    /**
+     * Returns the extension filter for {@link WebSocketExtensionDecoder} decoder.
+     */
+    WebSocketExtensionFilter decoderFilter();
+
+}
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketServerExtensionHandler.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketServerExtensionHandler.java
index 1b2e391..ff0ee3e 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketServerExtensionHandler.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketServerExtensionHandler.java
@@ -21,8 +21,10 @@ import io.netty.channel.ChannelFutureListener;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelPromise;
 import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpHeaders;
 import io.netty.handler.codec.http.HttpRequest;
 import io.netty.handler.codec.http.HttpResponse;
+import io.netty.util.internal.ObjectUtil;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -53,9 +55,7 @@ public class WebSocketServerExtensionHandler extends ChannelDuplexHandler {
      *      with fallback configuration.
      */
     public WebSocketServerExtensionHandler(WebSocketServerExtensionHandshaker... extensionHandshakers) {
-        if (extensionHandshakers == null) {
-            throw new NullPointerException("extensionHandshakers");
-        }
+        ObjectUtil.checkNotNull(extensionHandshakers, "extensionHandshakers");
         if (extensionHandshakers.length == 0) {
             throw new IllegalArgumentException("extensionHandshakers must contains at least one handshaker");
         }
@@ -63,8 +63,7 @@ public class WebSocketServerExtensionHandler extends ChannelDuplexHandler {
     }
 
     @Override
-    public void channelRead(ChannelHandlerContext ctx, Object msg)
-            throws Exception {
+    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
         if (msg instanceof HttpRequest) {
             HttpRequest request = (HttpRequest) msg;
 
@@ -104,35 +103,48 @@ public class WebSocketServerExtensionHandler extends ChannelDuplexHandler {
 
     @Override
     public void write(final ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
-        if (msg instanceof HttpResponse &&
-                WebSocketExtensionUtil.isWebsocketUpgrade(((HttpResponse) msg).headers()) && validExtensions != null) {
-            HttpResponse response = (HttpResponse) msg;
-            String headerValue = response.headers().getAsString(HttpHeaderNames.SEC_WEBSOCKET_EXTENSIONS);
-
-            for (WebSocketServerExtension extension : validExtensions) {
-                WebSocketExtensionData extensionData = extension.newReponseData();
-                headerValue = WebSocketExtensionUtil.appendExtension(headerValue,
-                        extensionData.name(), extensionData.parameters());
-            }
+        if (msg instanceof HttpResponse) {
+            HttpHeaders headers = ((HttpResponse) msg).headers();
 
-            promise.addListener(new ChannelFutureListener() {
-                @Override
-                public void operationComplete(ChannelFuture future) throws Exception {
-                    if (future.isSuccess()) {
-                        for (WebSocketServerExtension extension : validExtensions) {
-                            WebSocketExtensionDecoder decoder = extension.newExtensionDecoder();
-                            WebSocketExtensionEncoder encoder = extension.newExtensionEncoder();
-                            ctx.pipeline().addAfter(ctx.name(), decoder.getClass().getName(), decoder);
-                            ctx.pipeline().addAfter(ctx.name(), encoder.getClass().getName(), encoder);
-                        }
+            if (WebSocketExtensionUtil.isWebsocketUpgrade(headers)) {
+
+                if (validExtensions != null) {
+                    String headerValue = headers.getAsString(HttpHeaderNames.SEC_WEBSOCKET_EXTENSIONS);
+
+                    for (WebSocketServerExtension extension : validExtensions) {
+                        WebSocketExtensionData extensionData = extension.newReponseData();
+                        headerValue = WebSocketExtensionUtil.appendExtension(headerValue,
+                                                                             extensionData.name(),
+                                                                             extensionData.parameters());
                     }
+                    promise.addListener(new ChannelFutureListener() {
+                        @Override
+                        public void operationComplete(ChannelFuture future) {
+                            if (future.isSuccess()) {
+                                for (WebSocketServerExtension extension : validExtensions) {
+                                    WebSocketExtensionDecoder decoder = extension.newExtensionDecoder();
+                                    WebSocketExtensionEncoder encoder = extension.newExtensionEncoder();
+                                    ctx.pipeline()
+                                       .addAfter(ctx.name(), decoder.getClass().getName(), decoder)
+                                       .addAfter(ctx.name(), encoder.getClass().getName(), encoder);
+                                }
+                            }
+                        }
+                    });
 
-                    ctx.pipeline().remove(ctx.name());
+                    if (headerValue != null) {
+                        headers.set(HttpHeaderNames.SEC_WEBSOCKET_EXTENSIONS, headerValue);
+                    }
                 }
-            });
 
-            if (headerValue != null) {
-                response.headers().set(HttpHeaderNames.SEC_WEBSOCKET_EXTENSIONS, headerValue);
+                promise.addListener(new ChannelFutureListener() {
+                    @Override
+                    public void operationComplete(ChannelFuture future) {
+                        if (future.isSuccess()) {
+                            ctx.pipeline().remove(WebSocketServerExtensionHandler.this);
+                        }
+                    }
+                });
             }
         }
 
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateDecoder.java
index 89053fd..4ed3dc8 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateDecoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateDecoder.java
@@ -28,27 +28,47 @@ import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame;
 import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
 import io.netty.handler.codec.http.websocketx.WebSocketFrame;
 import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionDecoder;
+import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionFilter;
 
 import java.util.List;
 
+import static io.netty.util.internal.ObjectUtil.*;
+
 /**
  * Deflate implementation of a payload decompressor for
  * <tt>io.netty.handler.codec.http.websocketx.WebSocketFrame</tt>.
  */
 abstract class DeflateDecoder extends WebSocketExtensionDecoder {
 
-    static final byte[] FRAME_TAIL = new byte[] {0x00, 0x00, (byte) 0xff, (byte) 0xff};
+    static final ByteBuf FRAME_TAIL = Unpooled.unreleasableBuffer(
+            Unpooled.wrappedBuffer(new byte[] {0x00, 0x00, (byte) 0xff, (byte) 0xff}))
+            .asReadOnly();
+
+    static final ByteBuf EMPTY_DEFLATE_BLOCK = Unpooled.unreleasableBuffer(
+            Unpooled.wrappedBuffer(new byte[] { 0x00 }))
+            .asReadOnly();
 
     private final boolean noContext;
+    private final WebSocketExtensionFilter extensionDecoderFilter;
 
     private EmbeddedChannel decoder;
 
     /**
      * Constructor
+     *
      * @param noContext true to disable context takeover.
+     * @param extensionDecoderFilter extension decoder filter.
      */
-    public DeflateDecoder(boolean noContext) {
+    DeflateDecoder(boolean noContext, WebSocketExtensionFilter extensionDecoderFilter) {
         this.noContext = noContext;
+        this.extensionDecoderFilter = checkNotNull(extensionDecoderFilter, "extensionDecoderFilter");
+    }
+
+    /**
+     * Returns the extension decoder filter.
+     */
+    protected WebSocketExtensionFilter extensionDecoderFilter() {
+        return extensionDecoderFilter;
     }
 
     protected abstract boolean appendFrameTail(WebSocketFrame msg);
@@ -57,6 +77,35 @@ abstract class DeflateDecoder extends WebSocketExtensionDecoder {
 
     @Override
     protected void decode(ChannelHandlerContext ctx, WebSocketFrame msg, List<Object> out) throws Exception {
+        final ByteBuf decompressedContent = decompressContent(ctx, msg);
+
+        final WebSocketFrame outMsg;
+        if (msg instanceof TextWebSocketFrame) {
+            outMsg = new TextWebSocketFrame(msg.isFinalFragment(), newRsv(msg), decompressedContent);
+        } else if (msg instanceof BinaryWebSocketFrame) {
+            outMsg = new BinaryWebSocketFrame(msg.isFinalFragment(), newRsv(msg), decompressedContent);
+        } else if (msg instanceof ContinuationWebSocketFrame) {
+            outMsg = new ContinuationWebSocketFrame(msg.isFinalFragment(), newRsv(msg), decompressedContent);
+        } else {
+            throw new CodecException("unexpected frame type: " + msg.getClass().getName());
+        }
+
+        out.add(outMsg);
+    }
+
+    @Override
+    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
+        cleanup();
+        super.handlerRemoved(ctx);
+    }
+
+    @Override
+    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
+        cleanup();
+        super.channelInactive(ctx);
+    }
+
+    private ByteBuf decompressContent(ChannelHandlerContext ctx, WebSocketFrame msg) {
         if (decoder == null) {
             if (!(msg instanceof TextWebSocketFrame) && !(msg instanceof BinaryWebSocketFrame)) {
                 throw new CodecException("unexpected initial frame type: " + msg.getClass().getName());
@@ -65,12 +114,14 @@ abstract class DeflateDecoder extends WebSocketExtensionDecoder {
         }
 
         boolean readable = msg.content().isReadable();
+        boolean emptyDeflateBlock = EMPTY_DEFLATE_BLOCK.equals(msg.content());
+
         decoder.writeInbound(msg.content().retain());
         if (appendFrameTail(msg)) {
-            decoder.writeInbound(Unpooled.wrappedBuffer(FRAME_TAIL));
+            decoder.writeInbound(FRAME_TAIL.duplicate());
         }
 
-        CompositeByteBuf compositeUncompressedContent = ctx.alloc().compositeBuffer();
+        CompositeByteBuf compositeDecompressedContent = ctx.alloc().compositeBuffer();
         for (;;) {
             ByteBuf partUncompressedContent = decoder.readInbound();
             if (partUncompressedContent == null) {
@@ -80,58 +131,30 @@ abstract class DeflateDecoder extends WebSocketExtensionDecoder {
                 partUncompressedContent.release();
                 continue;
             }
-            compositeUncompressedContent.addComponent(true, partUncompressedContent);
+            compositeDecompressedContent.addComponent(true, partUncompressedContent);
         }
         // Correctly handle empty frames
         // See https://github.com/netty/netty/issues/4348
-        if (readable && compositeUncompressedContent.numComponents() <= 0) {
-            compositeUncompressedContent.release();
-            throw new CodecException("cannot read uncompressed buffer");
+        if (!emptyDeflateBlock && readable && compositeDecompressedContent.numComponents() <= 0) {
+            // Sometimes after fragmentation the last frame
+            // May contain left-over data that doesn't affect decompression
+            if (!(msg instanceof ContinuationWebSocketFrame)) {
+                compositeDecompressedContent.release();
+                throw new CodecException("cannot read uncompressed buffer");
+            }
         }
 
         if (msg.isFinalFragment() && noContext) {
             cleanup();
         }
 
-        WebSocketFrame outMsg;
-        if (msg instanceof TextWebSocketFrame) {
-            outMsg = new TextWebSocketFrame(msg.isFinalFragment(), newRsv(msg), compositeUncompressedContent);
-        } else if (msg instanceof BinaryWebSocketFrame) {
-            outMsg = new BinaryWebSocketFrame(msg.isFinalFragment(), newRsv(msg), compositeUncompressedContent);
-        } else if (msg instanceof ContinuationWebSocketFrame) {
-            outMsg = new ContinuationWebSocketFrame(msg.isFinalFragment(), newRsv(msg),
-                    compositeUncompressedContent);
-        } else {
-            throw new CodecException("unexpected frame type: " + msg.getClass().getName());
-        }
-        out.add(outMsg);
-    }
-
-    @Override
-    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
-        cleanup();
-        super.handlerRemoved(ctx);
-    }
-
-    @Override
-    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
-        cleanup();
-        super.channelInactive(ctx);
+        return compositeDecompressedContent;
     }
 
     private void cleanup() {
         if (decoder != null) {
             // Clean-up the previous encoder if not cleaned up correctly.
-            if (decoder.finish()) {
-                for (;;) {
-                    ByteBuf buf = decoder.readOutbound();
-                    if (buf == null) {
-                        break;
-                    }
-                    // Release the buffer
-                    buf.release();
-                }
-            }
+            decoder.finishAndReleaseAll();
             decoder = null;
         }
     }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateEncoder.java
index 1aff7b7..4f3daa8 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateEncoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateEncoder.java
@@ -15,7 +15,6 @@
  */
 package io.netty.handler.codec.http.websocketx.extensions.compression;
 
-import static io.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateDecoder.*;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.CompositeByteBuf;
 import io.netty.channel.ChannelHandlerContext;
@@ -28,9 +27,13 @@ import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame;
 import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
 import io.netty.handler.codec.http.websocketx.WebSocketFrame;
 import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionEncoder;
+import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionFilter;
 
 import java.util.List;
 
+import static io.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateDecoder.*;
+import static io.netty.util.internal.ObjectUtil.*;
+
 /**
  * Deflate implementation of a payload compressor for
  * <tt>io.netty.handler.codec.http.websocketx.WebSocketFrame</tt>.
@@ -40,6 +43,7 @@ abstract class DeflateEncoder extends WebSocketExtensionEncoder {
     private final int compressionLevel;
     private final int windowSize;
     private final boolean noContext;
+    private final WebSocketExtensionFilter extensionEncoderFilter;
 
     private EmbeddedChannel encoder;
 
@@ -48,11 +52,21 @@ abstract class DeflateEncoder extends WebSocketExtensionEncoder {
      * @param compressionLevel compression level of the compressor.
      * @param windowSize maximum size of the window compressor buffer.
      * @param noContext true to disable context takeover.
+     * @param extensionEncoderFilter extension encoder filter.
      */
-    public DeflateEncoder(int compressionLevel, int windowSize, boolean noContext) {
+    DeflateEncoder(int compressionLevel, int windowSize, boolean noContext,
+                   WebSocketExtensionFilter extensionEncoderFilter) {
         this.compressionLevel = compressionLevel;
         this.windowSize = windowSize;
         this.noContext = noContext;
+        this.extensionEncoderFilter = checkNotNull(extensionEncoderFilter, "extensionEncoderFilter");
+    }
+
+    /**
+     * Returns the extension encoder filter.
+     */
+    protected WebSocketExtensionFilter extensionEncoderFilter() {
+        return extensionEncoderFilter;
     }
 
     /**
@@ -68,8 +82,39 @@ abstract class DeflateEncoder extends WebSocketExtensionEncoder {
     protected abstract boolean removeFrameTail(WebSocketFrame msg);
 
     @Override
-    protected void encode(ChannelHandlerContext ctx, WebSocketFrame msg,
-            List<Object> out) throws Exception {
+    protected void encode(ChannelHandlerContext ctx, WebSocketFrame msg, List<Object> out) throws Exception {
+        final ByteBuf compressedContent;
+        if (msg.content().isReadable()) {
+            compressedContent = compressContent(ctx, msg);
+        } else if (msg.isFinalFragment()) {
+            // Set empty DEFLATE block manually for unknown buffer size
+            // https://tools.ietf.org/html/rfc7692#section-7.2.3.6
+            compressedContent = EMPTY_DEFLATE_BLOCK.duplicate();
+        } else {
+            throw new CodecException("cannot compress content buffer");
+        }
+
+        final WebSocketFrame outMsg;
+        if (msg instanceof TextWebSocketFrame) {
+            outMsg = new TextWebSocketFrame(msg.isFinalFragment(), rsv(msg), compressedContent);
+        } else if (msg instanceof BinaryWebSocketFrame) {
+            outMsg = new BinaryWebSocketFrame(msg.isFinalFragment(), rsv(msg), compressedContent);
+        } else if (msg instanceof ContinuationWebSocketFrame) {
+            outMsg = new ContinuationWebSocketFrame(msg.isFinalFragment(), rsv(msg), compressedContent);
+        } else {
+            throw new CodecException("unexpected frame type: " + msg.getClass().getName());
+        }
+
+        out.add(outMsg);
+    }
+
+    @Override
+    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
+        cleanup();
+        super.handlerRemoved(ctx);
+    }
+
+    private ByteBuf compressContent(ChannelHandlerContext ctx, WebSocketFrame msg) {
         if (encoder == null) {
             encoder = new EmbeddedChannel(ZlibCodecFactory.newZlibEncoder(
                     ZlibWrapper.NONE, compressionLevel, windowSize, 8));
@@ -89,6 +134,7 @@ abstract class DeflateEncoder extends WebSocketExtensionEncoder {
             }
             fullCompressedContent.addComponent(true, partCompressedContent);
         }
+
         if (fullCompressedContent.numComponents() <= 0) {
             fullCompressedContent.release();
             throw new CodecException("cannot read compressed buffer");
@@ -100,44 +146,19 @@ abstract class DeflateEncoder extends WebSocketExtensionEncoder {
 
         ByteBuf compressedContent;
         if (removeFrameTail(msg)) {
-            int realLength = fullCompressedContent.readableBytes() - FRAME_TAIL.length;
+            int realLength = fullCompressedContent.readableBytes() - FRAME_TAIL.readableBytes();
             compressedContent = fullCompressedContent.slice(0, realLength);
         } else {
             compressedContent = fullCompressedContent;
         }
 
-        WebSocketFrame outMsg;
-        if (msg instanceof TextWebSocketFrame) {
-            outMsg = new TextWebSocketFrame(msg.isFinalFragment(), rsv(msg), compressedContent);
-        } else if (msg instanceof BinaryWebSocketFrame) {
-            outMsg = new BinaryWebSocketFrame(msg.isFinalFragment(), rsv(msg), compressedContent);
-        } else if (msg instanceof ContinuationWebSocketFrame) {
-            outMsg = new ContinuationWebSocketFrame(msg.isFinalFragment(), rsv(msg), compressedContent);
-        } else {
-            throw new CodecException("unexpected frame type: " + msg.getClass().getName());
-        }
-        out.add(outMsg);
-    }
-
-    @Override
-    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
-        cleanup();
-        super.handlerRemoved(ctx);
+        return compressedContent;
     }
 
     private void cleanup() {
         if (encoder != null) {
             // Clean-up the previous encoder if not cleaned up correctly.
-            if (encoder.finish()) {
-                for (;;) {
-                    ByteBuf buf = encoder.readOutbound();
-                    if (buf == null) {
-                        break;
-                    }
-                    // Release the buffer
-                    buf.release();
-                }
-            }
+            encoder.finishAndReleaseAll();
             encoder = null;
         }
     }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateFrameClientExtensionHandshaker.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateFrameClientExtensionHandshaker.java
index 6671d1f..c5f8b60 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateFrameClientExtensionHandshaker.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateFrameClientExtensionHandshaker.java
@@ -15,16 +15,18 @@
  */
 package io.netty.handler.codec.http.websocketx.extensions.compression;
 
-import static io.netty.handler.codec.http.websocketx.extensions.compression.
-        DeflateFrameServerExtensionHandshaker.*;
 import io.netty.handler.codec.http.websocketx.extensions.WebSocketClientExtension;
 import io.netty.handler.codec.http.websocketx.extensions.WebSocketClientExtensionHandshaker;
 import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionData;
 import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionDecoder;
 import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionEncoder;
+import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionFilterProvider;
 
 import java.util.Collections;
 
+import static io.netty.handler.codec.http.websocketx.extensions.compression.DeflateFrameServerExtensionHandshaker.*;
+import static io.netty.util.internal.ObjectUtil.*;
+
 /**
  * <a href="https://tools.ietf.org/id/draft-tyoshino-hybi-websocket-perframe-deflate-06.txt">perframe-deflate</a>
  * handshake implementation.
@@ -33,6 +35,7 @@ public final class DeflateFrameClientExtensionHandshaker implements WebSocketCli
 
     private final int compressionLevel;
     private final boolean useWebkitExtensionName;
+    private final WebSocketExtensionFilterProvider extensionFilterProvider;
 
     /**
      * Constructor with default configuration.
@@ -48,12 +51,26 @@ public final class DeflateFrameClientExtensionHandshaker implements WebSocketCli
      *            Compression level between 0 and 9 (default is 6).
      */
     public DeflateFrameClientExtensionHandshaker(int compressionLevel, boolean useWebkitExtensionName) {
+        this(compressionLevel, useWebkitExtensionName, WebSocketExtensionFilterProvider.DEFAULT);
+    }
+
+    /**
+     * Constructor with custom configuration.
+     *
+     * @param compressionLevel
+     *            Compression level between 0 and 9 (default is 6).
+     * @param extensionFilterProvider
+     *            provides client extension filters for per frame deflate encoder and decoder.
+     */
+    public DeflateFrameClientExtensionHandshaker(int compressionLevel, boolean useWebkitExtensionName,
+            WebSocketExtensionFilterProvider extensionFilterProvider) {
         if (compressionLevel < 0 || compressionLevel > 9) {
             throw new IllegalArgumentException(
                     "compressionLevel: " + compressionLevel + " (expected: 0-9)");
         }
         this.compressionLevel = compressionLevel;
         this.useWebkitExtensionName = useWebkitExtensionName;
+        this.extensionFilterProvider = checkNotNull(extensionFilterProvider, "extensionFilterProvider");
     }
 
     @Override
@@ -71,7 +88,7 @@ public final class DeflateFrameClientExtensionHandshaker implements WebSocketCli
         }
 
         if (extensionData.parameters().isEmpty()) {
-            return new DeflateFrameClientExtension(compressionLevel);
+            return new DeflateFrameClientExtension(compressionLevel, extensionFilterProvider);
         } else {
             return null;
         }
@@ -80,9 +97,11 @@ public final class DeflateFrameClientExtensionHandshaker implements WebSocketCli
     private static class DeflateFrameClientExtension implements WebSocketClientExtension {
 
         private final int compressionLevel;
+        private final WebSocketExtensionFilterProvider extensionFilterProvider;
 
-        public DeflateFrameClientExtension(int compressionLevel) {
+        DeflateFrameClientExtension(int compressionLevel, WebSocketExtensionFilterProvider extensionFilterProvider) {
             this.compressionLevel = compressionLevel;
+            this.extensionFilterProvider = extensionFilterProvider;
         }
 
         @Override
@@ -92,12 +111,13 @@ public final class DeflateFrameClientExtensionHandshaker implements WebSocketCli
 
         @Override
         public WebSocketExtensionEncoder newExtensionEncoder() {
-            return new PerFrameDeflateEncoder(compressionLevel, 15, false);
+            return new PerFrameDeflateEncoder(compressionLevel, 15, false,
+                                              extensionFilterProvider.encoderFilter());
         }
 
         @Override
         public WebSocketExtensionDecoder newExtensionDecoder() {
-            return new PerFrameDeflateDecoder(false);
+            return new PerFrameDeflateDecoder(false, extensionFilterProvider.decoderFilter());
         }
     }
 
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateFrameServerExtensionHandshaker.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateFrameServerExtensionHandshaker.java
index e7ea9f3..7a06a22 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateFrameServerExtensionHandshaker.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/DeflateFrameServerExtensionHandshaker.java
@@ -18,11 +18,14 @@ package io.netty.handler.codec.http.websocketx.extensions.compression;
 import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionData;
 import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionDecoder;
 import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionEncoder;
+import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionFilterProvider;
 import io.netty.handler.codec.http.websocketx.extensions.WebSocketServerExtension;
 import io.netty.handler.codec.http.websocketx.extensions.WebSocketServerExtensionHandshaker;
 
 import java.util.Collections;
 
+import static io.netty.util.internal.ObjectUtil.*;
+
 /**
  * <a href="https://tools.ietf.org/id/draft-tyoshino-hybi-websocket-perframe-deflate-06.txt">perframe-deflate</a>
  * handshake implementation.
@@ -33,6 +36,7 @@ public final class DeflateFrameServerExtensionHandshaker implements WebSocketSer
     static final String DEFLATE_FRAME_EXTENSION = "deflate-frame";
 
     private final int compressionLevel;
+    private final WebSocketExtensionFilterProvider extensionFilterProvider;
 
     /**
      * Constructor with default configuration.
@@ -48,11 +52,25 @@ public final class DeflateFrameServerExtensionHandshaker implements WebSocketSer
      *            Compression level between 0 and 9 (default is 6).
      */
     public DeflateFrameServerExtensionHandshaker(int compressionLevel) {
+        this(compressionLevel, WebSocketExtensionFilterProvider.DEFAULT);
+    }
+
+    /**
+     * Constructor with custom configuration.
+     *
+     * @param compressionLevel
+     *            Compression level between 0 and 9 (default is 6).
+     * @param extensionFilterProvider
+     *            provides server extension filters for per frame deflate encoder and decoder.
+     */
+    public DeflateFrameServerExtensionHandshaker(int compressionLevel,
+            WebSocketExtensionFilterProvider extensionFilterProvider) {
         if (compressionLevel < 0 || compressionLevel > 9) {
             throw new IllegalArgumentException(
                     "compressionLevel: " + compressionLevel + " (expected: 0-9)");
         }
         this.compressionLevel = compressionLevel;
+        this.extensionFilterProvider = checkNotNull(extensionFilterProvider, "extensionFilterProvider");
     }
 
     @Override
@@ -63,7 +81,7 @@ public final class DeflateFrameServerExtensionHandshaker implements WebSocketSer
         }
 
         if (extensionData.parameters().isEmpty()) {
-            return new DeflateFrameServerExtension(compressionLevel, extensionData.name());
+            return new DeflateFrameServerExtension(compressionLevel, extensionData.name(), extensionFilterProvider);
         } else {
             return null;
         }
@@ -73,10 +91,13 @@ public final class DeflateFrameServerExtensionHandshaker implements WebSocketSer
 
         private final String extensionName;
         private final int compressionLevel;
+        private final WebSocketExtensionFilterProvider extensionFilterProvider;
 
-        public DeflateFrameServerExtension(int compressionLevel, String extensionName) {
+        DeflateFrameServerExtension(int compressionLevel, String extensionName,
+                WebSocketExtensionFilterProvider extensionFilterProvider) {
             this.extensionName = extensionName;
             this.compressionLevel = compressionLevel;
+            this.extensionFilterProvider = extensionFilterProvider;
         }
 
         @Override
@@ -86,12 +107,13 @@ public final class DeflateFrameServerExtensionHandshaker implements WebSocketSer
 
         @Override
         public WebSocketExtensionEncoder newExtensionEncoder() {
-            return new PerFrameDeflateEncoder(compressionLevel, 15, false);
+            return new PerFrameDeflateEncoder(compressionLevel, 15, false,
+                                              extensionFilterProvider.encoderFilter());
         }
 
         @Override
         public WebSocketExtensionDecoder newExtensionDecoder() {
-            return new PerFrameDeflateDecoder(false);
+            return new PerFrameDeflateDecoder(false, extensionFilterProvider.decoderFilter());
         }
 
         @Override
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateDecoder.java
index ad95544..dc15352 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateDecoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateDecoder.java
@@ -20,6 +20,7 @@ import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame;
 import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
 import io.netty.handler.codec.http.websocketx.WebSocketFrame;
 import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtension;
+import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionFilter;
 
 /**
  * Per-frame implementation of deflate decompressor.
@@ -28,18 +29,37 @@ class PerFrameDeflateDecoder extends DeflateDecoder {
 
     /**
      * Constructor
+     *
      * @param noContext true to disable context takeover.
      */
-    public PerFrameDeflateDecoder(boolean noContext) {
-        super(noContext);
+    PerFrameDeflateDecoder(boolean noContext) {
+        super(noContext, WebSocketExtensionFilter.NEVER_SKIP);
+    }
+
+    /**
+     * Constructor
+     *
+     * @param noContext true to disable context takeover.
+     * @param extensionDecoderFilter extension decoder filter for per frame deflate decoder.
+     */
+    PerFrameDeflateDecoder(boolean noContext, WebSocketExtensionFilter extensionDecoderFilter) {
+        super(noContext, extensionDecoderFilter);
     }
 
     @Override
     public boolean acceptInboundMessage(Object msg) throws Exception {
-        return (msg instanceof TextWebSocketFrame ||
-                msg instanceof BinaryWebSocketFrame ||
+        if (!super.acceptInboundMessage(msg)) {
+            return false;
+        }
+
+        WebSocketFrame wsFrame = (WebSocketFrame) msg;
+        if (extensionDecoderFilter().mustSkip(wsFrame)) {
+            return false;
+        }
+
+        return (msg instanceof TextWebSocketFrame || msg instanceof BinaryWebSocketFrame ||
                 msg instanceof ContinuationWebSocketFrame) &&
-                    (((WebSocketFrame) msg).rsv() & WebSocketExtension.RSV1) > 0;
+               (wsFrame.rsv() & WebSocketExtension.RSV1) > 0;
     }
 
     @Override
@@ -51,4 +71,5 @@ class PerFrameDeflateDecoder extends DeflateDecoder {
     protected boolean appendFrameTail(WebSocketFrame msg) {
         return true;
     }
+
 }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateEncoder.java
index aaffd8d..5da86b5 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateEncoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateEncoder.java
@@ -20,6 +20,7 @@ import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame;
 import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
 import io.netty.handler.codec.http.websocketx.WebSocketFrame;
 import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtension;
+import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionFilter;
 
 /**
  * Per-frame implementation of deflate compressor.
@@ -28,21 +29,43 @@ class PerFrameDeflateEncoder extends DeflateEncoder {
 
     /**
      * Constructor
+     *
      * @param compressionLevel compression level of the compressor.
-     * @param windowSize maximum size of the window compressor buffer.
-     * @param noContext true to disable context takeover.
+     * @param windowSize       maximum size of the window compressor buffer.
+     * @param noContext        true to disable context takeover.
      */
-    public PerFrameDeflateEncoder(int compressionLevel, int windowSize, boolean noContext) {
-        super(compressionLevel, windowSize, noContext);
+    PerFrameDeflateEncoder(int compressionLevel, int windowSize, boolean noContext) {
+        super(compressionLevel, windowSize, noContext, WebSocketExtensionFilter.NEVER_SKIP);
+    }
+
+    /**
+     * Constructor
+     *
+     * @param compressionLevel compression level of the compressor.
+     * @param windowSize       maximum size of the window compressor buffer.
+     * @param noContext        true to disable context takeover.
+     * @param extensionEncoderFilter extension encoder filter for per frame deflate encoder.
+     */
+    PerFrameDeflateEncoder(int compressionLevel, int windowSize, boolean noContext,
+                           WebSocketExtensionFilter extensionEncoderFilter) {
+        super(compressionLevel, windowSize, noContext, extensionEncoderFilter);
     }
 
     @Override
     public boolean acceptOutboundMessage(Object msg) throws Exception {
-        return (msg instanceof TextWebSocketFrame ||
-                msg instanceof BinaryWebSocketFrame ||
+        if (!super.acceptOutboundMessage(msg)) {
+            return false;
+        }
+
+        WebSocketFrame wsFrame = (WebSocketFrame) msg;
+        if (extensionEncoderFilter().mustSkip(wsFrame)) {
+            return false;
+        }
+
+        return (msg instanceof TextWebSocketFrame || msg instanceof BinaryWebSocketFrame ||
                 msg instanceof ContinuationWebSocketFrame) &&
-                    ((WebSocketFrame) msg).content().readableBytes() > 0 &&
-                    (((WebSocketFrame) msg).rsv() & WebSocketExtension.RSV1) == 0;
+               wsFrame.content().readableBytes() > 0 &&
+               (wsFrame.rsv() & WebSocketExtension.RSV1) == 0;
     }
 
     @Override
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateClientExtensionHandshaker.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateClientExtensionHandshaker.java
index ddebaaa..df6f5f6 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateClientExtensionHandshaker.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateClientExtensionHandshaker.java
@@ -15,20 +15,21 @@
  */
 package io.netty.handler.codec.http.websocketx.extensions.compression;
 
-import static io.netty.handler.codec.http.websocketx.extensions.compression.
-        PerMessageDeflateServerExtensionHandshaker.*;
-
 import io.netty.handler.codec.compression.ZlibCodecFactory;
 import io.netty.handler.codec.http.websocketx.extensions.WebSocketClientExtension;
 import io.netty.handler.codec.http.websocketx.extensions.WebSocketClientExtensionHandshaker;
 import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionData;
 import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionDecoder;
 import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionEncoder;
+import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionFilterProvider;
 
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Map.Entry;
 
+import static io.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateServerExtensionHandshaker.*;
+import static io.netty.util.internal.ObjectUtil.*;
+
 /**
  * <a href="http://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-18">permessage-deflate</a>
  * handshake implementation.
@@ -40,6 +41,7 @@ public final class PerMessageDeflateClientExtensionHandshaker implements WebSock
     private final int requestedServerWindowSize;
     private final boolean allowClientNoContext;
     private final boolean requestedServerNoContext;
+    private final WebSocketExtensionFilterProvider extensionFilterProvider;
 
     /**
      * Constructor with default configuration.
@@ -68,6 +70,34 @@ public final class PerMessageDeflateClientExtensionHandshaker implements WebSock
     public PerMessageDeflateClientExtensionHandshaker(int compressionLevel,
             boolean allowClientWindowSize, int requestedServerWindowSize,
             boolean allowClientNoContext, boolean requestedServerNoContext) {
+        this(compressionLevel, allowClientWindowSize, requestedServerWindowSize,
+             allowClientNoContext, requestedServerNoContext, WebSocketExtensionFilterProvider.DEFAULT);
+    }
+
+    /**
+     * Constructor with custom configuration.
+     *
+     * @param compressionLevel
+     *            Compression level between 0 and 9 (default is 6).
+     * @param allowClientWindowSize
+     *            allows WebSocket server to customize the client inflater window size
+     *            (default is false).
+     * @param requestedServerWindowSize
+     *            indicates the requested sever window size to use if server inflater is customizable.
+     * @param allowClientNoContext
+     *            allows WebSocket server to activate client_no_context_takeover
+     *            (default is false).
+     * @param requestedServerNoContext
+     *            indicates if client needs to activate server_no_context_takeover
+     *            if server is compatible with (default is false).
+     * @param extensionFilterProvider
+     *            provides client extension filters for per message deflate encoder and decoder.
+     */
+    public PerMessageDeflateClientExtensionHandshaker(int compressionLevel,
+            boolean allowClientWindowSize, int requestedServerWindowSize,
+            boolean allowClientNoContext, boolean requestedServerNoContext,
+            WebSocketExtensionFilterProvider extensionFilterProvider) {
+
         if (requestedServerWindowSize > MAX_WINDOW_SIZE || requestedServerWindowSize < MIN_WINDOW_SIZE) {
             throw new IllegalArgumentException(
                     "requestedServerWindowSize: " + requestedServerWindowSize + " (expected: 8-15)");
@@ -81,6 +111,7 @@ public final class PerMessageDeflateClientExtensionHandshaker implements WebSock
         this.requestedServerWindowSize = requestedServerWindowSize;
         this.allowClientNoContext = allowClientNoContext;
         this.requestedServerNoContext = requestedServerNoContext;
+        this.extensionFilterProvider = checkNotNull(extensionFilterProvider, "extensionFilterProvider");
     }
 
     @Override
@@ -158,7 +189,7 @@ public final class PerMessageDeflateClientExtensionHandshaker implements WebSock
 
         if (succeed) {
             return new PermessageDeflateExtension(serverNoContext, serverWindowSize,
-                    clientNoContext, clientWindowSize);
+                    clientNoContext, clientWindowSize, extensionFilterProvider);
         } else {
             return null;
         }
@@ -170,28 +201,32 @@ public final class PerMessageDeflateClientExtensionHandshaker implements WebSock
         private final int serverWindowSize;
         private final boolean clientNoContext;
         private final int clientWindowSize;
+        private final WebSocketExtensionFilterProvider extensionFilterProvider;
 
         @Override
         public int rsv() {
             return RSV1;
         }
 
-        public PermessageDeflateExtension(boolean serverNoContext, int serverWindowSize,
-                boolean clientNoContext, int clientWindowSize) {
+        PermessageDeflateExtension(boolean serverNoContext, int serverWindowSize,
+                boolean clientNoContext, int clientWindowSize,
+                WebSocketExtensionFilterProvider extensionFilterProvider) {
             this.serverNoContext = serverNoContext;
             this.serverWindowSize = serverWindowSize;
             this.clientNoContext = clientNoContext;
             this.clientWindowSize = clientWindowSize;
+            this.extensionFilterProvider = extensionFilterProvider;
         }
 
         @Override
         public WebSocketExtensionEncoder newExtensionEncoder() {
-            return new PerMessageDeflateEncoder(compressionLevel, clientWindowSize, clientNoContext);
+            return new PerMessageDeflateEncoder(compressionLevel, clientWindowSize, clientNoContext,
+                                                extensionFilterProvider.encoderFilter());
         }
 
         @Override
         public WebSocketExtensionDecoder newExtensionDecoder() {
-            return new PerMessageDeflateDecoder(serverNoContext);
+            return new PerMessageDeflateDecoder(serverNoContext, extensionFilterProvider.decoderFilter());
         }
     }
 
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateDecoder.java
index a69294e..d2512bb 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateDecoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateDecoder.java
@@ -21,6 +21,7 @@ import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame;
 import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
 import io.netty.handler.codec.http.websocketx.WebSocketFrame;
 import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtension;
+import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionFilter;
 
 import java.util.List;
 
@@ -33,23 +34,45 @@ class PerMessageDeflateDecoder extends DeflateDecoder {
 
     /**
      * Constructor
+     *
      * @param noContext true to disable context takeover.
      */
-    public PerMessageDeflateDecoder(boolean noContext) {
-        super(noContext);
+    PerMessageDeflateDecoder(boolean noContext) {
+        super(noContext, WebSocketExtensionFilter.NEVER_SKIP);
+    }
+
+    /**
+     * Constructor
+     *
+     * @param noContext true to disable context takeover.
+     * @param extensionDecoderFilter extension decoder for per message deflate decoder.
+     */
+    PerMessageDeflateDecoder(boolean noContext, WebSocketExtensionFilter extensionDecoderFilter) {
+        super(noContext, extensionDecoderFilter);
     }
 
     @Override
     public boolean acceptInboundMessage(Object msg) throws Exception {
-        return ((msg instanceof TextWebSocketFrame ||
-                 msg instanceof BinaryWebSocketFrame) &&
-                    (((WebSocketFrame) msg).rsv() & WebSocketExtension.RSV1) > 0) ||
-                (msg instanceof ContinuationWebSocketFrame && compressing);
+        if (!super.acceptInboundMessage(msg)) {
+            return false;
+        }
+
+        WebSocketFrame wsFrame = (WebSocketFrame) msg;
+        if (extensionDecoderFilter().mustSkip(wsFrame)) {
+            if (compressing) {
+                throw new IllegalStateException("Cannot skip per message deflate decoder, compression in progress");
+            }
+            return false;
+        }
+
+        return ((wsFrame instanceof TextWebSocketFrame || wsFrame instanceof BinaryWebSocketFrame) &&
+                (wsFrame.rsv() & WebSocketExtension.RSV1) > 0) ||
+               (wsFrame instanceof ContinuationWebSocketFrame && compressing);
     }
 
     @Override
     protected int newRsv(WebSocketFrame msg) {
-        return (msg.rsv() & WebSocketExtension.RSV1) > 0 ?
+        return (msg.rsv() & WebSocketExtension.RSV1) > 0?
                 msg.rsv() ^ WebSocketExtension.RSV1 : msg.rsv();
     }
 
@@ -60,7 +83,7 @@ class PerMessageDeflateDecoder extends DeflateDecoder {
 
     @Override
     protected void decode(ChannelHandlerContext ctx, WebSocketFrame msg,
-        List<Object> out) throws Exception {
+                          List<Object> out) throws Exception {
         super.decode(ctx, msg, out);
 
         if (msg.isFinalFragment()) {
@@ -69,4 +92,5 @@ class PerMessageDeflateDecoder extends DeflateDecoder {
             compressing = true;
         }
     }
+
 }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateEncoder.java
index b1cdc66..12a7c46 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateEncoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateEncoder.java
@@ -21,6 +21,7 @@ import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame;
 import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
 import io.netty.handler.codec.http.websocketx.WebSocketFrame;
 import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtension;
+import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionFilter;
 
 import java.util.List;
 
@@ -33,25 +34,50 @@ class PerMessageDeflateEncoder extends DeflateEncoder {
 
     /**
      * Constructor
+     *
      * @param compressionLevel compression level of the compressor.
      * @param windowSize maximum size of the window compressor buffer.
      * @param noContext true to disable context takeover.
      */
-    public PerMessageDeflateEncoder(int compressionLevel, int windowSize, boolean noContext) {
-        super(compressionLevel, windowSize, noContext);
+    PerMessageDeflateEncoder(int compressionLevel, int windowSize, boolean noContext) {
+        super(compressionLevel, windowSize, noContext, WebSocketExtensionFilter.NEVER_SKIP);
+    }
+
+    /**
+     * Constructor
+     *
+     * @param compressionLevel compression level of the compressor.
+     * @param windowSize maximum size of the window compressor buffer.
+     * @param noContext true to disable context takeover.
+     * @param extensionEncoderFilter extension filter for per message deflate encoder.
+     */
+    PerMessageDeflateEncoder(int compressionLevel, int windowSize, boolean noContext,
+                             WebSocketExtensionFilter extensionEncoderFilter) {
+        super(compressionLevel, windowSize, noContext, extensionEncoderFilter);
     }
 
     @Override
     public boolean acceptOutboundMessage(Object msg) throws Exception {
-        return ((msg instanceof TextWebSocketFrame ||
-                msg instanceof BinaryWebSocketFrame) &&
-                   (((WebSocketFrame) msg).rsv() & WebSocketExtension.RSV1) == 0) ||
-               (msg instanceof ContinuationWebSocketFrame && compressing);
+        if (!super.acceptOutboundMessage(msg)) {
+            return false;
+        }
+
+        WebSocketFrame wsFrame = (WebSocketFrame) msg;
+        if (extensionEncoderFilter().mustSkip(wsFrame)) {
+            if (compressing) {
+                throw new IllegalStateException("Cannot skip per message deflate encoder, compression in progress");
+            }
+            return false;
+        }
+
+        return ((wsFrame instanceof TextWebSocketFrame || wsFrame instanceof BinaryWebSocketFrame) &&
+                (wsFrame.rsv() & WebSocketExtension.RSV1) == 0) ||
+               (wsFrame instanceof ContinuationWebSocketFrame && compressing);
     }
 
     @Override
     protected int rsv(WebSocketFrame msg) {
-        return msg instanceof TextWebSocketFrame || msg instanceof BinaryWebSocketFrame ?
+        return msg instanceof TextWebSocketFrame || msg instanceof BinaryWebSocketFrame?
                 msg.rsv() | WebSocketExtension.RSV1 : msg.rsv();
     }
 
@@ -62,7 +88,7 @@ class PerMessageDeflateEncoder extends DeflateEncoder {
 
     @Override
     protected void encode(ChannelHandlerContext ctx, WebSocketFrame msg,
-            List<Object> out) throws Exception {
+                          List<Object> out) throws Exception {
         super.encode(ctx, msg, out);
 
         if (msg.isFinalFragment()) {
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateServerExtensionHandshaker.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateServerExtensionHandshaker.java
index 0bf0162..3eafeb9 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateServerExtensionHandshaker.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateServerExtensionHandshaker.java
@@ -19,6 +19,7 @@ import io.netty.handler.codec.compression.ZlibCodecFactory;
 import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionData;
 import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionDecoder;
 import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionEncoder;
+import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionFilterProvider;
 import io.netty.handler.codec.http.websocketx.extensions.WebSocketServerExtension;
 import io.netty.handler.codec.http.websocketx.extensions.WebSocketServerExtensionHandshaker;
 
@@ -26,6 +27,8 @@ import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Map.Entry;
 
+import static io.netty.util.internal.ObjectUtil.*;
+
 /**
  * <a href="http://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-18">permessage-deflate</a>
  * handshake implementation.
@@ -46,6 +49,7 @@ public final class PerMessageDeflateServerExtensionHandshaker implements WebSock
     private final int preferredClientWindowSize;
     private final boolean allowServerNoContext;
     private final boolean preferredClientNoContext;
+    private final WebSocketExtensionFilterProvider extensionFilterProvider;
 
     /**
      * Constructor with default configuration.
@@ -71,9 +75,36 @@ public final class PerMessageDeflateServerExtensionHandshaker implements WebSock
      *            indicates if server prefers to activate client_no_context_takeover
      *            if client is compatible with (default is false).
      */
-    public PerMessageDeflateServerExtensionHandshaker(int compressionLevel,
-            boolean allowServerWindowSize, int preferredClientWindowSize,
+    public PerMessageDeflateServerExtensionHandshaker(int compressionLevel, boolean allowServerWindowSize,
+            int preferredClientWindowSize,
             boolean allowServerNoContext, boolean preferredClientNoContext) {
+        this(compressionLevel, allowServerWindowSize, preferredClientWindowSize, allowServerNoContext,
+             preferredClientNoContext, WebSocketExtensionFilterProvider.DEFAULT);
+    }
+
+    /**
+     * Constructor with custom configuration.
+     *
+     * @param compressionLevel
+     *            Compression level between 0 and 9 (default is 6).
+     * @param allowServerWindowSize
+     *            allows WebSocket client to customize the server inflater window size
+     *            (default is false).
+     * @param preferredClientWindowSize
+     *            indicates the preferred client window size to use if client inflater is customizable.
+     * @param allowServerNoContext
+     *            allows WebSocket client to activate server_no_context_takeover
+     *            (default is false).
+     * @param preferredClientNoContext
+     *            indicates if server prefers to activate client_no_context_takeover
+     *            if client is compatible with (default is false).
+     * @param extensionFilterProvider
+     *            provides server extension filters for per message deflate encoder and decoder.
+     */
+    public PerMessageDeflateServerExtensionHandshaker(int compressionLevel, boolean allowServerWindowSize,
+            int preferredClientWindowSize,
+            boolean allowServerNoContext, boolean preferredClientNoContext,
+            WebSocketExtensionFilterProvider extensionFilterProvider) {
         if (preferredClientWindowSize > MAX_WINDOW_SIZE || preferredClientWindowSize < MIN_WINDOW_SIZE) {
             throw new IllegalArgumentException(
                     "preferredServerWindowSize: " + preferredClientWindowSize + " (expected: 8-15)");
@@ -87,6 +118,7 @@ public final class PerMessageDeflateServerExtensionHandshaker implements WebSock
         this.preferredClientWindowSize = preferredClientWindowSize;
         this.allowServerNoContext = allowServerNoContext;
         this.preferredClientNoContext = preferredClientNoContext;
+        this.extensionFilterProvider = checkNotNull(extensionFilterProvider, "extensionFilterProvider");
     }
 
     @Override
@@ -137,7 +169,7 @@ public final class PerMessageDeflateServerExtensionHandshaker implements WebSock
 
         if (deflateEnabled) {
             return new PermessageDeflateExtension(compressionLevel, serverNoContext,
-                    serverWindowSize, clientNoContext, clientWindowSize);
+                    serverWindowSize, clientNoContext, clientWindowSize, extensionFilterProvider);
         } else {
             return null;
         }
@@ -150,14 +182,17 @@ public final class PerMessageDeflateServerExtensionHandshaker implements WebSock
         private final int serverWindowSize;
         private final boolean clientNoContext;
         private final int clientWindowSize;
+        private final WebSocketExtensionFilterProvider extensionFilterProvider;
 
-        public PermessageDeflateExtension(int compressionLevel, boolean serverNoContext,
-                int serverWindowSize, boolean clientNoContext, int clientWindowSize) {
+        PermessageDeflateExtension(int compressionLevel, boolean serverNoContext,
+                int serverWindowSize, boolean clientNoContext, int clientWindowSize,
+                WebSocketExtensionFilterProvider extensionFilterProvider) {
             this.compressionLevel = compressionLevel;
             this.serverNoContext = serverNoContext;
             this.serverWindowSize = serverWindowSize;
             this.clientNoContext = clientNoContext;
             this.clientWindowSize = clientWindowSize;
+            this.extensionFilterProvider = extensionFilterProvider;
         }
 
         @Override
@@ -167,12 +202,13 @@ public final class PerMessageDeflateServerExtensionHandshaker implements WebSock
 
         @Override
         public WebSocketExtensionEncoder newExtensionEncoder() {
-            return new PerMessageDeflateEncoder(compressionLevel, serverWindowSize, serverNoContext);
+            return new PerMessageDeflateEncoder(compressionLevel, serverWindowSize, serverNoContext,
+                                                extensionFilterProvider.encoderFilter());
         }
 
         @Override
         public WebSocketExtensionDecoder newExtensionDecoder() {
-            return new PerMessageDeflateDecoder(clientNoContext);
+            return new PerMessageDeflateDecoder(clientNoContext, extensionFilterProvider.decoderFilter());
         }
 
         @Override
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/package-info.java b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/package-info.java
index 1425a82..824acb3 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/package-info.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/websocketx/package-info.java
@@ -21,11 +21,11 @@
  * This package supports different web socket specification versions (hence the X suffix).
  * The specification current supported are:
  * <ul>
- * <li><a href="http://netty.io/s/ws-00">draft-ietf-hybi-thewebsocketprotocol-00</a></li>
- * <li><a href="http://netty.io/s/ws-07">draft-ietf-hybi-thewebsocketprotocol-07</a></li>
- * <li><a href="http://netty.io/s/ws-10">draft-ietf-hybi-thewebsocketprotocol-10</a></li>
- * <li><a href="http://netty.io/s/rfc6455">RFC 6455</a>
- *     (originally <a href="http://netty.io/s/ws-17">draft-ietf-hybi-thewebsocketprotocol-17</a>)</li>
+ * <li><a href="https://netty.io/s/ws-00">draft-ietf-hybi-thewebsocketprotocol-00</a></li>
+ * <li><a href="https://netty.io/s/ws-07">draft-ietf-hybi-thewebsocketprotocol-07</a></li>
+ * <li><a href="https://netty.io/s/ws-10">draft-ietf-hybi-thewebsocketprotocol-10</a></li>
+ * <li><a href="https://netty.io/s/rfc6455">RFC 6455</a>
+ *     (originally <a href="https://netty.io/s/ws-17">draft-ietf-hybi-thewebsocketprotocol-17</a>)</li>
 
  * </ul>
  * </p>
diff --git a/codec-http/src/main/java/io/netty/handler/codec/rtsp/RtspMethods.java b/codec-http/src/main/java/io/netty/handler/codec/rtsp/RtspMethods.java
index 3e62928..8d98061 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/rtsp/RtspMethods.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/rtsp/RtspMethods.java
@@ -16,6 +16,7 @@
 package io.netty.handler.codec.rtsp;
 
 import io.netty.handler.codec.http.HttpMethod;
+import io.netty.util.internal.ObjectUtil;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -38,62 +39,62 @@ public final class RtspMethods {
      * The DESCRIBE getMethod retrieves the description of a presentation or
      * media object identified by the request URL from a server.
      */
-    public static final HttpMethod DESCRIBE = new HttpMethod("DESCRIBE");
+    public static final HttpMethod DESCRIBE = HttpMethod.valueOf("DESCRIBE");
 
     /**
      * The ANNOUNCE posts the description of a presentation or media object
      * identified by the request URL to a server, or updates the client-side
      * session description in real-time.
      */
-    public static final HttpMethod ANNOUNCE = new HttpMethod("ANNOUNCE");
+    public static final HttpMethod ANNOUNCE = HttpMethod.valueOf("ANNOUNCE");
 
     /**
      * The SETUP request for a URI specifies the transport mechanism to be
      * used for the streamed media.
      */
-    public static final HttpMethod SETUP = new HttpMethod("SETUP");
+    public static final HttpMethod SETUP = HttpMethod.valueOf("SETUP");
 
     /**
      * The PLAY getMethod tells the server to start sending data via the
      * mechanism specified in SETUP.
      */
-    public static final HttpMethod PLAY = new HttpMethod("PLAY");
+    public static final HttpMethod PLAY = HttpMethod.valueOf("PLAY");
 
     /**
      * The PAUSE request causes the stream delivery to be interrupted
      * (halted) temporarily.
      */
-    public static final HttpMethod PAUSE = new HttpMethod("PAUSE");
+    public static final HttpMethod PAUSE = HttpMethod.valueOf("PAUSE");
 
     /**
      * The TEARDOWN request stops the stream delivery for the given URI,
      * freeing the resources associated with it.
      */
-    public static final HttpMethod TEARDOWN = new HttpMethod("TEARDOWN");
+    public static final HttpMethod TEARDOWN = HttpMethod.valueOf("TEARDOWN");
 
     /**
      * The GET_PARAMETER request retrieves the value of a parameter of a
      * presentation or stream specified in the URI.
      */
-    public static final HttpMethod GET_PARAMETER = new HttpMethod("GET_PARAMETER");
+    public static final HttpMethod GET_PARAMETER = HttpMethod.valueOf("GET_PARAMETER");
 
     /**
      * The SET_PARAMETER requests to set the value of a parameter for a
      * presentation or stream specified by the URI.
      */
-    public static final HttpMethod SET_PARAMETER = new HttpMethod("SET_PARAMETER");
+    public static final HttpMethod SET_PARAMETER = HttpMethod.valueOf("SET_PARAMETER");
 
     /**
      * The REDIRECT request informs the client that it must connect to another
      * server location.
      */
-    public static final HttpMethod REDIRECT = new HttpMethod("REDIRECT");
+    public static final HttpMethod REDIRECT = HttpMethod.valueOf("REDIRECT");
 
     /**
      * The RECORD getMethod initiates recording a range of media data according to
      * the presentation description.
      */
-    public static final HttpMethod RECORD = new HttpMethod("RECORD");
+    public static final HttpMethod RECORD = HttpMethod.valueOf("RECORD");
 
     private static final Map<String, HttpMethod> methodMap = new HashMap<String, HttpMethod>();
 
@@ -117,9 +118,7 @@ public final class RtspMethods {
      * will be returned.  Otherwise, a new instance will be returned.
      */
     public static HttpMethod valueOf(String name) {
-        if (name == null) {
-            throw new NullPointerException("name");
-        }
+        ObjectUtil.checkNotNull(name, "name");
 
         name = name.trim().toUpperCase();
         if (name.isEmpty()) {
@@ -130,7 +129,7 @@ public final class RtspMethods {
         if (result != null) {
             return result;
         } else {
-            return new HttpMethod(name);
+            return HttpMethod.valueOf(name);
         }
     }
 
diff --git a/codec-http/src/main/java/io/netty/handler/codec/rtsp/RtspVersions.java b/codec-http/src/main/java/io/netty/handler/codec/rtsp/RtspVersions.java
index 94d0368..19a060c 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/rtsp/RtspVersions.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/rtsp/RtspVersions.java
@@ -16,6 +16,7 @@
 package io.netty.handler.codec.rtsp;
 
 import io.netty.handler.codec.http.HttpVersion;
+import io.netty.util.internal.ObjectUtil;
 
 /**
  * The version of RTSP.
@@ -34,9 +35,7 @@ public final class RtspVersions {
      * Otherwise, a new {@link HttpVersion} instance will be returned.
      */
     public static HttpVersion valueOf(String text) {
-        if (text == null) {
-            throw new NullPointerException("text");
-        }
+        ObjectUtil.checkNotNull(text, "text");
 
         text = text.trim().toUpperCase();
         if ("RTSP/1.0".equals(text)) {
diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyDataFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyDataFrame.java
index a1e9d73..322dd8c 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyDataFrame.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyDataFrame.java
@@ -18,6 +18,7 @@ package io.netty.handler.codec.spdy;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
 import io.netty.util.IllegalReferenceCountException;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.StringUtil;
 
 /**
@@ -44,10 +45,8 @@ public class DefaultSpdyDataFrame extends DefaultSpdyStreamFrame implements Spdy
      */
     public DefaultSpdyDataFrame(int streamId, ByteBuf data) {
         super(streamId);
-        if (data == null) {
-            throw new NullPointerException("data");
-        }
-        this.data = validate(data);
+        this.data = validate(
+                ObjectUtil.checkNotNull(data, "data"));
     }
 
     private static ByteBuf validate(ByteBuf data) {
diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyGoAwayFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyGoAwayFrame.java
index 4d88875..79c21f2 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyGoAwayFrame.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyGoAwayFrame.java
@@ -15,6 +15,8 @@
  */
 package io.netty.handler.codec.spdy;
 
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
+
 import io.netty.util.internal.StringUtil;
 
 /**
@@ -62,10 +64,7 @@ public class DefaultSpdyGoAwayFrame implements SpdyGoAwayFrame {
 
     @Override
     public SpdyGoAwayFrame setLastGoodStreamId(int lastGoodStreamId) {
-        if (lastGoodStreamId < 0) {
-            throw new IllegalArgumentException("Last-good-stream-ID"
-                    + " cannot be negative: " + lastGoodStreamId);
-        }
+        checkPositiveOrZero(lastGoodStreamId, "lastGoodStreamId");
         this.lastGoodStreamId = lastGoodStreamId;
         return this;
     }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyStreamFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyStreamFrame.java
index 4618d4d..487844e 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyStreamFrame.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyStreamFrame.java
@@ -15,6 +15,8 @@
  */
 package io.netty.handler.codec.spdy;
 
+import static io.netty.util.internal.ObjectUtil.checkPositive;
+
 /**
  * The default {@link SpdyStreamFrame} implementation.
  */
@@ -39,10 +41,7 @@ public abstract class DefaultSpdyStreamFrame implements SpdyStreamFrame {
 
     @Override
     public SpdyStreamFrame setStreamId(int streamId) {
-        if (streamId <= 0) {
-            throw new IllegalArgumentException(
-                    "Stream-ID must be positive: " + streamId);
-        }
+        checkPositive(streamId, "streamId");
         this.streamId = streamId;
         return this;
     }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdySynReplyFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdySynReplyFrame.java
index 7efc905..f757d1d 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdySynReplyFrame.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdySynReplyFrame.java
@@ -20,8 +20,7 @@ import io.netty.util.internal.StringUtil;
 /**
  * The default {@link SpdySynReplyFrame} implementation.
  */
-public class DefaultSpdySynReplyFrame extends DefaultSpdyHeadersFrame
-        implements SpdySynReplyFrame {
+public class DefaultSpdySynReplyFrame extends DefaultSpdyHeadersFrame implements SpdySynReplyFrame {
 
     /**
      * Creates a new instance.
diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdySynStreamFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdySynStreamFrame.java
index f8adc1c..46fe301 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdySynStreamFrame.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdySynStreamFrame.java
@@ -15,6 +15,8 @@
  */
 package io.netty.handler.codec.spdy;
 
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
+
 import io.netty.util.internal.StringUtil;
 
 /**
@@ -77,11 +79,7 @@ public class DefaultSpdySynStreamFrame extends DefaultSpdyHeadersFrame
 
     @Override
     public SpdySynStreamFrame setAssociatedStreamId(int associatedStreamId) {
-        if (associatedStreamId < 0) {
-            throw new IllegalArgumentException(
-                    "Associated-To-Stream-ID cannot be negative: " +
-                    associatedStreamId);
-        }
+        checkPositiveOrZero(associatedStreamId, "associatedStreamId");
         this.associatedStreamId = associatedStreamId;
         return this;
     }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyWindowUpdateFrame.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyWindowUpdateFrame.java
index f14611b..22b0406 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyWindowUpdateFrame.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyWindowUpdateFrame.java
@@ -15,6 +15,9 @@
  */
 package io.netty.handler.codec.spdy;
 
+import static io.netty.util.internal.ObjectUtil.checkPositive;
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
+
 import io.netty.util.internal.StringUtil;
 
 /**
@@ -43,10 +46,7 @@ public class DefaultSpdyWindowUpdateFrame implements SpdyWindowUpdateFrame {
 
     @Override
     public SpdyWindowUpdateFrame setStreamId(int streamId) {
-        if (streamId < 0) {
-            throw new IllegalArgumentException(
-                    "Stream-ID cannot be negative: " + streamId);
-        }
+        checkPositiveOrZero(streamId, "streamId");
         this.streamId = streamId;
         return this;
     }
@@ -58,11 +58,7 @@ public class DefaultSpdyWindowUpdateFrame implements SpdyWindowUpdateFrame {
 
     @Override
     public SpdyWindowUpdateFrame setDeltaWindowSize(int deltaWindowSize) {
-        if (deltaWindowSize <= 0) {
-            throw new IllegalArgumentException(
-                    "Delta-Window-Size must be positive: " +
-                    deltaWindowSize);
-        }
+        checkPositive(deltaWindowSize, "deltaWindowSize");
         this.deltaWindowSize = deltaWindowSize;
         return this;
     }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyCodecUtil.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyCodecUtil.java
index b25167c..0c61967 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyCodecUtil.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyCodecUtil.java
@@ -16,6 +16,7 @@
 package io.netty.handler.codec.spdy;
 
 import io.netty.buffer.ByteBuf;
+import io.netty.util.internal.ObjectUtil;
 
 final class SpdyCodecUtil {
 
@@ -286,9 +287,7 @@ final class SpdyCodecUtil {
      * Validate a SPDY header name.
      */
     static void validateHeaderName(CharSequence name) {
-        if (name == null) {
-            throw new NullPointerException("name");
-        }
+        ObjectUtil.checkNotNull(name, "name");
         if (name.length() == 0) {
             throw new IllegalArgumentException(
                     "name cannot be length zero");
@@ -319,9 +318,7 @@ final class SpdyCodecUtil {
      * Validate a SPDY header value. Does not validate max length.
      */
     static void validateHeaderValue(CharSequence value) {
-        if (value == null) {
-            throw new NullPointerException("value");
-        }
+        ObjectUtil.checkNotNull(value, "value");
         for (int i = 0; i < value.length(); i ++) {
             char c = value.charAt(i);
             if (c == 0) {
diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameDecoder.java
index e0d1112..c02081f 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameDecoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameDecoder.java
@@ -38,8 +38,10 @@ import static io.netty.handler.codec.spdy.SpdyCodecUtil.getSignedInt;
 import static io.netty.handler.codec.spdy.SpdyCodecUtil.getUnsignedInt;
 import static io.netty.handler.codec.spdy.SpdyCodecUtil.getUnsignedMedium;
 import static io.netty.handler.codec.spdy.SpdyCodecUtil.getUnsignedShort;
+
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
+import io.netty.util.internal.ObjectUtil;
 
 /**
  * Decodes {@link ByteBuf}s into SPDY Frames.
@@ -89,19 +91,9 @@ public class SpdyFrameDecoder {
      * Creates a new instance with the specified parameters.
      */
     public SpdyFrameDecoder(SpdyVersion spdyVersion, SpdyFrameDecoderDelegate delegate, int maxChunkSize) {
-        if (spdyVersion == null) {
-            throw new NullPointerException("spdyVersion");
-        }
-        if (delegate == null) {
-            throw new NullPointerException("delegate");
-        }
-        if (maxChunkSize <= 0) {
-            throw new IllegalArgumentException(
-                    "maxChunkSize must be a positive integer: " + maxChunkSize);
-        }
-        this.spdyVersion = spdyVersion.getVersion();
-        this.delegate = delegate;
-        this.maxChunkSize = maxChunkSize;
+        this.spdyVersion = ObjectUtil.checkNotNull(spdyVersion, "spdyVersion").getVersion();
+        this.delegate = ObjectUtil.checkNotNull(delegate, "delegate");
+        this.maxChunkSize = ObjectUtil.checkPositive(maxChunkSize, "maxChunkSize");
         state = State.READ_COMMON_HEADER;
     }
 
diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameEncoder.java
index 1524f95..448147a 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameEncoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyFrameEncoder.java
@@ -17,6 +17,7 @@ package io.netty.handler.codec.spdy;
 
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.ByteBufAllocator;
+import io.netty.util.internal.ObjectUtil;
 
 import java.nio.ByteOrder;
 import java.util.Set;
@@ -34,10 +35,7 @@ public class SpdyFrameEncoder {
      * Creates a new instance with the specified {@code spdyVersion}.
      */
     public SpdyFrameEncoder(SpdyVersion spdyVersion) {
-        if (spdyVersion == null) {
-            throw new NullPointerException("spdyVersion");
-        }
-        version = spdyVersion.getVersion();
+        version = ObjectUtil.checkNotNull(spdyVersion, "spdyVersion").getVersion();
     }
 
     private void writeControlFrameHeader(ByteBuf buffer, int type, byte flags, int length) {
diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockRawDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockRawDecoder.java
index 70d4607..548b845 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockRawDecoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockRawDecoder.java
@@ -17,8 +17,9 @@ package io.netty.handler.codec.spdy;
 
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.ByteBufAllocator;
+import io.netty.util.internal.ObjectUtil;
 
-import static io.netty.handler.codec.spdy.SpdyCodecUtil.*;
+import static io.netty.handler.codec.spdy.SpdyCodecUtil.getSignedInt;
 
 public class SpdyHeaderBlockRawDecoder extends SpdyHeaderBlockDecoder {
 
@@ -48,9 +49,7 @@ public class SpdyHeaderBlockRawDecoder extends SpdyHeaderBlockDecoder {
     }
 
     public SpdyHeaderBlockRawDecoder(SpdyVersion spdyVersion, int maxHeaderSize) {
-        if (spdyVersion == null) {
-            throw new NullPointerException("spdyVersion");
-        }
+        ObjectUtil.checkNotNull(spdyVersion, "spdyVersion");
         this.maxHeaderSize = maxHeaderSize;
         state = State.READ_NUM_HEADERS;
     }
@@ -63,12 +62,8 @@ public class SpdyHeaderBlockRawDecoder extends SpdyHeaderBlockDecoder {
 
     @Override
     void decode(ByteBufAllocator alloc, ByteBuf headerBlock, SpdyHeadersFrame frame) throws Exception {
-        if (headerBlock == null) {
-            throw new NullPointerException("headerBlock");
-        }
-        if (frame == null) {
-            throw new NullPointerException("frame");
-        }
+        ObjectUtil.checkNotNull(headerBlock, "headerBlock");
+        ObjectUtil.checkNotNull(frame, "frame");
 
         if (cumulation == null) {
             decodeHeaderBlock(headerBlock, frame);
diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockRawEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockRawEncoder.java
index afd5479..310d757 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockRawEncoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockRawEncoder.java
@@ -19,6 +19,7 @@ import io.netty.buffer.ByteBuf;
 import io.netty.buffer.ByteBufAllocator;
 import io.netty.buffer.ByteBufUtil;
 import io.netty.buffer.Unpooled;
+import io.netty.util.internal.ObjectUtil;
 
 import java.util.Set;
 
@@ -29,10 +30,7 @@ public class SpdyHeaderBlockRawEncoder extends SpdyHeaderBlockEncoder {
     private final int version;
 
     public SpdyHeaderBlockRawEncoder(SpdyVersion version) {
-        if (version == null) {
-            throw new NullPointerException("version");
-        }
-        this.version = version.getVersion();
+        this.version = ObjectUtil.checkNotNull(version, "version").getVersion();
     }
 
     private static void setLengthField(ByteBuf buffer, int writerIndex, int length) {
diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockZlibEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockZlibEncoder.java
index 9e4cf31..e3fef9d 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockZlibEncoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHeaderBlockZlibEncoder.java
@@ -18,6 +18,8 @@ package io.netty.handler.codec.spdy;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.ByteBufAllocator;
 import io.netty.buffer.Unpooled;
+import io.netty.util.internal.PlatformDependent;
+import io.netty.util.internal.SuppressJava6Requirement;
 
 import java.util.zip.Deflater;
 
@@ -70,11 +72,17 @@ class SpdyHeaderBlockZlibEncoder extends SpdyHeaderBlockRawEncoder {
         }
     }
 
+    @SuppressJava6Requirement(reason = "Guarded by java version check")
     private boolean compressInto(ByteBuf compressed) {
         byte[] out = compressed.array();
         int off = compressed.arrayOffset() + compressed.writerIndex();
         int toWrite = compressed.writableBytes();
-        int numBytes = compressor.deflate(out, off, toWrite, Deflater.SYNC_FLUSH);
+        final int numBytes;
+        if (PlatformDependent.javaVersion() >= 7) {
+            numBytes = compressor.deflate(out, off, toWrite, Deflater.SYNC_FLUSH);
+        } else {
+            numBytes = compressor.deflate(out, off, toWrite);
+        }
         compressed.writerIndex(compressed.writerIndex() + numBytes);
         return numBytes == toWrite;
     }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpDecoder.java
index 366ad15..73f15dd 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpDecoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpDecoder.java
@@ -32,12 +32,14 @@ import io.netty.handler.codec.http.HttpResponseStatus;
 import io.netty.handler.codec.http.HttpVersion;
 import io.netty.handler.codec.spdy.SpdyHttpHeaders.Names;
 import io.netty.util.ReferenceCountUtil;
+import io.netty.util.internal.ObjectUtil;
 
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
 import static io.netty.handler.codec.spdy.SpdyHeaders.HttpNames.*;
+import static io.netty.util.internal.ObjectUtil.checkPositive;
 
 /**
  * Decodes {@link SpdySynStreamFrame}s, {@link SpdySynReplyFrame}s,
@@ -100,15 +102,8 @@ public class SpdyHttpDecoder extends MessageToMessageDecoder<SpdyFrame> {
      */
     protected SpdyHttpDecoder(SpdyVersion version, int maxContentLength, Map<Integer,
             FullHttpMessage> messageMap, boolean validateHeaders) {
-        if (version == null) {
-            throw new NullPointerException("version");
-        }
-        if (maxContentLength <= 0) {
-            throw new IllegalArgumentException(
-                    "maxContentLength must be a positive integer: " + maxContentLength);
-        }
-        spdyVersion = version.getVersion();
-        this.maxContentLength = maxContentLength;
+        spdyVersion = ObjectUtil.checkNotNull(version, "version").getVersion();
+        this.maxContentLength = checkPositive(maxContentLength, "maxContentLength");
         this.messageMap = messageMap;
         this.validateHeaders = validateHeaders;
     }
@@ -195,7 +190,7 @@ public class SpdyHttpDecoder extends MessageToMessageDecoder<SpdyFrame> {
                 // SYN_STREAM frames initiated by the client are HTTP requests
 
                 // If a client sends a request with a truncated header block, the server must
-                // reply with a HTTP 431 REQUEST HEADER FIELDS TOO LARGE reply.
+                // reply with an HTTP 431 REQUEST HEADER FIELDS TOO LARGE reply.
                 if (spdySynStreamFrame.isTruncated()) {
                     SpdySynReplyFrame spdySynReplyFrame = new DefaultSpdySynReplyFrame(streamId);
                     spdySynReplyFrame.setLast(true);
@@ -220,7 +215,7 @@ public class SpdyHttpDecoder extends MessageToMessageDecoder<SpdyFrame> {
                     }
                 } catch (Throwable t) {
                     // If a client sends a SYN_STREAM without all of the getMethod, url (host and path),
-                    // scheme, and version headers the server must reply with a HTTP 400 BAD REQUEST reply.
+                    // scheme, and version headers the server must reply with an HTTP 400 BAD REQUEST reply.
                     // Also sends HTTP 400 BAD REQUEST reply if header name/value pairs are invalid
                     SpdySynReplyFrame spdySynReplyFrame = new DefaultSpdySynReplyFrame(streamId);
                     spdySynReplyFrame.setLast(true);
diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpEncoder.java
index d3c5d58..f1912c6 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpEncoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpEncoder.java
@@ -28,6 +28,7 @@ import io.netty.handler.codec.http.HttpRequest;
 import io.netty.handler.codec.http.HttpResponse;
 import io.netty.handler.codec.http.LastHttpContent;
 import io.netty.util.AsciiString;
+import io.netty.util.internal.ObjectUtil;
 
 import java.util.Iterator;
 import java.util.List;
@@ -144,9 +145,7 @@ public class SpdyHttpEncoder extends MessageToMessageEncoder<HttpObject> {
      * @param validateHeaders    validate the header names and values when adding them to the {@link SpdyHeaders}
      */
     public SpdyHttpEncoder(SpdyVersion version, boolean headersToLowerCase, boolean validateHeaders) {
-        if (version == null) {
-            throw new NullPointerException("version");
-        }
+        ObjectUtil.checkNotNull(version, "version");
         this.headersToLowerCase = headersToLowerCase;
         this.validateHeaders = validateHeaders;
     }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpResponseStreamIdHandler.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpResponseStreamIdHandler.java
index 4ad32e4..e664adf 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpResponseStreamIdHandler.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyHttpResponseStreamIdHandler.java
@@ -21,7 +21,7 @@ import io.netty.handler.codec.http.HttpMessage;
 import io.netty.handler.codec.spdy.SpdyHttpHeaders.Names;
 import io.netty.util.ReferenceCountUtil;
 
-import java.util.LinkedList;
+import java.util.ArrayDeque;
 import java.util.List;
 import java.util.Queue;
 
@@ -33,7 +33,7 @@ import java.util.Queue;
 public class SpdyHttpResponseStreamIdHandler extends
         MessageToMessageCodec<Object, HttpMessage> {
     private static final Integer NO_ID = -1;
-    private final Queue<Integer> ids = new LinkedList<Integer>();
+    private final Queue<Integer> ids = new ArrayDeque<Integer>();
 
     @Override
     public boolean acceptInboundMessage(Object msg) throws Exception {
diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyProtocolException.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyProtocolException.java
index 2b5bcbc..477599a 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyProtocolException.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyProtocolException.java
@@ -15,6 +15,9 @@
  */
 package io.netty.handler.codec.spdy;
 
+import io.netty.util.internal.PlatformDependent;
+import io.netty.util.internal.SuppressJava6Requirement;
+
 public class SpdyProtocolException extends Exception {
 
     private static final long serialVersionUID = 7870000537743847264L;
@@ -44,4 +47,18 @@ public class SpdyProtocolException extends Exception {
     public SpdyProtocolException(Throwable cause) {
         super(cause);
     }
+
+    static SpdyProtocolException newStatic(String message) {
+        if (PlatformDependent.javaVersion() >= 7) {
+            return new SpdyProtocolException(message, true);
+        }
+        return new SpdyProtocolException(message);
+    }
+
+    @SuppressJava6Requirement(reason = "uses Java 7+ Exception.<init>(String, Throwable, boolean, boolean)" +
+            " but is guarded by version checks")
+    private SpdyProtocolException(String message, boolean shared) {
+        super(message, null, false, true);
+        assert shared;
+    }
 }
diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySessionHandler.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySessionHandler.java
index 394f6c2..e3e00cf 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySessionHandler.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySessionHandler.java
@@ -20,12 +20,14 @@ import io.netty.channel.ChannelFuture;
 import io.netty.channel.ChannelFutureListener;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelPromise;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.ThrowableUtil;
 
 import java.util.concurrent.atomic.AtomicInteger;
 
 import static io.netty.handler.codec.spdy.SpdyCodecUtil.SPDY_SESSION_STREAM_ID;
 import static io.netty.handler.codec.spdy.SpdyCodecUtil.isServerId;
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
 
 /**
  * Manages streams within a SPDY session.
@@ -33,9 +35,9 @@ import static io.netty.handler.codec.spdy.SpdyCodecUtil.isServerId;
 public class SpdySessionHandler extends ChannelDuplexHandler {
 
     private static final SpdyProtocolException PROTOCOL_EXCEPTION = ThrowableUtil.unknownStackTrace(
-            new SpdyProtocolException(), SpdySessionHandler.class, "handleOutboundMessage(...)");
+            SpdyProtocolException.newStatic(null), SpdySessionHandler.class, "handleOutboundMessage(...)");
     private static final SpdyProtocolException STREAM_CLOSED = ThrowableUtil.unknownStackTrace(
-            new SpdyProtocolException("Stream closed"), SpdySessionHandler.class, "removeStream(...)");
+            SpdyProtocolException.newStatic("Stream closed"), SpdySessionHandler.class, "removeStream(...)");
 
     private static final int DEFAULT_WINDOW_SIZE = 64 * 1024; // 64 KB default initial window size
     private int initialSendWindowSize    = DEFAULT_WINDOW_SIZE;
@@ -69,24 +71,19 @@ public class SpdySessionHandler extends ChannelDuplexHandler {
      *                handle the client endpoint of the connection.
      */
     public SpdySessionHandler(SpdyVersion version, boolean server) {
-        if (version == null) {
-            throw new NullPointerException("version");
-        }
+        this.minorVersion = ObjectUtil.checkNotNull(version, "version").getMinorVersion();
         this.server = server;
-        minorVersion = version.getMinorVersion();
     }
 
     public void setSessionReceiveWindowSize(int sessionReceiveWindowSize) {
-      if (sessionReceiveWindowSize < 0) {
-        throw new IllegalArgumentException("sessionReceiveWindowSize");
-      }
-      // This will not send a window update frame immediately.
-      // If this value increases the allowed receive window size,
-      // a WINDOW_UPDATE frame will be sent when only half of the
-      // session window size remains during data frame processing.
-      // If this value decreases the allowed receive window size,
-      // the window will be reduced as data frames are processed.
-      initialSessionReceiveWindowSize = sessionReceiveWindowSize;
+        checkPositiveOrZero(sessionReceiveWindowSize, "sessionReceiveWindowSize");
+        // This will not send a window update frame immediately.
+        // If this value increases the allowed receive window size,
+        // a WINDOW_UPDATE frame will be sent when only half of the
+        // session window size remains during data frame processing.
+        // If this value decreases the allowed receive window size,
+        // the window will be reduced as data frames are processed.
+        initialSessionReceiveWindowSize = sessionReceiveWindowSize;
     }
 
     @Override
diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySessionStatus.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySessionStatus.java
index fd79d1e..7ca86c8 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySessionStatus.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdySessionStatus.java
@@ -15,6 +15,8 @@
  */
 package io.netty.handler.codec.spdy;
 
+import io.netty.util.internal.ObjectUtil;
+
 /**
  * The SPDY session status code and its description.
  */
@@ -65,12 +67,8 @@ public class SpdySessionStatus implements Comparable<SpdySessionStatus> {
      * {@code statusPhrase}.
      */
     public SpdySessionStatus(int code, String statusPhrase) {
-        if (statusPhrase == null) {
-            throw new NullPointerException("statusPhrase");
-        }
-
+        this.statusPhrase = ObjectUtil.checkNotNull(statusPhrase, "statusPhrase");
         this.code = code;
-        this.statusPhrase = statusPhrase;
     }
 
     /**
diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyStreamStatus.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyStreamStatus.java
index 75ed740..c2b8674 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyStreamStatus.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyStreamStatus.java
@@ -15,6 +15,8 @@
  */
 package io.netty.handler.codec.spdy;
 
+import io.netty.util.internal.ObjectUtil;
+
 /**
  * The SPDY stream status code and its description.
  */
@@ -139,12 +141,8 @@ public class SpdyStreamStatus implements Comparable<SpdyStreamStatus> {
                     "0 is not a valid status code for a RST_STREAM");
         }
 
-        if (statusPhrase == null) {
-            throw new NullPointerException("statusPhrase");
-        }
-
+        this.statusPhrase = ObjectUtil.checkNotNull(statusPhrase, "statusPhrase");
         this.code = code;
-        this.statusPhrase = statusPhrase;
     }
 
     /**
diff --git a/codec-http/src/main/resources/META-INF/native-image/io.netty/codec-http/native-image.properties b/codec-http/src/main/resources/META-INF/native-image/io.netty/codec-http/native-image.properties
new file mode 100644
index 0000000..df76109
--- /dev/null
+++ b/codec-http/src/main/resources/META-INF/native-image/io.netty/codec-http/native-image.properties
@@ -0,0 +1,16 @@
+# Copyright 2019 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.
+
+Args = --initialize-at-build-time=io.netty \
+       --initialize-at-run-time=io.netty.handler.codec.http.HttpObjectEncoder,io.netty.handler.codec.http.websocketx.WebSocket00FrameEncoder,io.netty.handler.codec.http.websocketx.extensions.compression.DeflateDecoder
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/CombinedHttpHeadersTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/CombinedHttpHeadersTest.java
index c63c885..2023030 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/CombinedHttpHeadersTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/CombinedHttpHeadersTest.java
@@ -55,7 +55,7 @@ public class CombinedHttpHeadersTest {
         otherHeaders.add(HEADER_NAME, "a");
         otherHeaders.add(HEADER_NAME, "b");
         headers.add(otherHeaders);
-        assertEquals("a,b", headers.get(HEADER_NAME).toString());
+        assertEquals("a,b", headers.get(HEADER_NAME));
     }
 
     @Test
@@ -66,7 +66,7 @@ public class CombinedHttpHeadersTest {
         otherHeaders.add(HEADER_NAME, "b");
         otherHeaders.add(HEADER_NAME, "c");
         headers.add(otherHeaders);
-        assertEquals("a,b,c", headers.get(HEADER_NAME).toString());
+        assertEquals("a,b,c", headers.get(HEADER_NAME));
     }
 
     @Test
@@ -99,7 +99,7 @@ public class CombinedHttpHeadersTest {
         otherHeaders.add(HEADER_NAME, "b");
         otherHeaders.add(HEADER_NAME, "c");
         headers.set(otherHeaders);
-        assertEquals("b,c", headers.get(HEADER_NAME).toString());
+        assertEquals("b,c", headers.get(HEADER_NAME));
     }
 
     @Test
@@ -110,7 +110,7 @@ public class CombinedHttpHeadersTest {
         otherHeaders.add(HEADER_NAME, "b");
         otherHeaders.add(HEADER_NAME, "c");
         headers.add(otherHeaders);
-        assertEquals("a,b,c", headers.get(HEADER_NAME).toString());
+        assertEquals("a,b,c", headers.get(HEADER_NAME));
     }
 
     @Test
@@ -121,7 +121,7 @@ public class CombinedHttpHeadersTest {
         otherHeaders.add(HEADER_NAME, "b");
         otherHeaders.add(HEADER_NAME, "c");
         headers.set(otherHeaders);
-        assertEquals("b,c", headers.get(HEADER_NAME).toString());
+        assertEquals("b,c", headers.get(HEADER_NAME));
     }
 
     @Test
@@ -196,7 +196,7 @@ public class CombinedHttpHeadersTest {
     public void addIterableCsvEmpty() {
         final CombinedHttpHeaders headers = newCombinedHttpHeaders();
         headers.add(HEADER_NAME, Collections.<CharSequence>emptyList());
-        assertEquals(Arrays.asList(""), headers.getAll(HEADER_NAME));
+        assertEquals(Collections.singletonList(""), headers.getAll(HEADER_NAME));
     }
 
     @Test
@@ -294,9 +294,9 @@ public class CombinedHttpHeadersTest {
         headers.set(HEADER_NAME, Arrays.asList("\"a\"", "\"b\"", "\"c\""));
         assertEquals(Arrays.asList("a", "b", "c"), headers.getAll(HEADER_NAME));
         headers.set(HEADER_NAME, "a,b,c");
-        assertEquals(Arrays.asList("a,b,c"), headers.getAll(HEADER_NAME));
+        assertEquals(Collections.singletonList("a,b,c"), headers.getAll(HEADER_NAME));
         headers.set(HEADER_NAME, "\"a,b,c\"");
-        assertEquals(Arrays.asList("a,b,c"), headers.getAll(HEADER_NAME));
+        assertEquals(Collections.singletonList("a,b,c"), headers.getAll(HEADER_NAME));
     }
 
     @Test
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/HttpClientCodecTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/HttpClientCodecTest.java
index 16a6eff..a9195d7 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/HttpClientCodecTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/HttpClientCodecTest.java
@@ -35,6 +35,7 @@ import io.netty.handler.codec.CodecException;
 import io.netty.handler.codec.PrematureChannelClosureException;
 import io.netty.util.CharsetUtil;
 import io.netty.util.NetUtil;
+import org.hamcrest.CoreMatchers;
 import org.junit.Test;
 
 import java.net.InetSocketAddress;
@@ -331,4 +332,90 @@ public class HttpClientCodecTest {
 
         assertThat(ch.readInbound(), is(nullValue()));
     }
+
+    @Test
+    public void testWebDavResponse() {
+        byte[] data = ("HTTP/1.1 102 Processing\r\n" +
+                       "Status-URI: Status-URI:http://status.com; 404\r\n" +
+                       "\r\n" +
+                       "1234567812345678").getBytes();
+        EmbeddedChannel ch = new EmbeddedChannel(new HttpClientCodec());
+        assertTrue(ch.writeInbound(Unpooled.wrappedBuffer(data)));
+
+        HttpResponse res = ch.readInbound();
+        assertThat(res.protocolVersion(), sameInstance(HttpVersion.HTTP_1_1));
+        assertThat(res.status(), is(HttpResponseStatus.PROCESSING));
+        HttpContent content = ch.readInbound();
+        // HTTP 102 is not allowed to have content.
+        assertThat(content.content().readableBytes(), is(0));
+        content.release();
+
+        assertThat(ch.finish(), is(false));
+    }
+
+    @Test
+    public void testInformationalResponseKeepsPairsInSync() {
+        byte[] data = ("HTTP/1.1 102 Processing\r\n" +
+                "Status-URI: Status-URI:http://status.com; 404\r\n" +
+                "\r\n").getBytes();
+        byte[] data2 = ("HTTP/1.1 200 OK\r\n" +
+                "Content-Length: 8\r\n" +
+                "\r\n" +
+                "12345678").getBytes();
+        EmbeddedChannel ch = new EmbeddedChannel(new HttpClientCodec());
+        assertTrue(ch.writeOutbound(new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.HEAD, "/")));
+        ByteBuf buffer = ch.readOutbound();
+        buffer.release();
+        assertNull(ch.readOutbound());
+        assertTrue(ch.writeInbound(Unpooled.wrappedBuffer(data)));
+        HttpResponse res = ch.readInbound();
+        assertThat(res.protocolVersion(), sameInstance(HttpVersion.HTTP_1_1));
+        assertThat(res.status(), is(HttpResponseStatus.PROCESSING));
+        HttpContent content = ch.readInbound();
+        // HTTP 102 is not allowed to have content.
+        assertThat(content.content().readableBytes(), is(0));
+        assertThat(content, CoreMatchers.<HttpContent>instanceOf(LastHttpContent.class));
+        content.release();
+
+        assertTrue(ch.writeOutbound(new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/")));
+        buffer = ch.readOutbound();
+        buffer.release();
+        assertNull(ch.readOutbound());
+        assertTrue(ch.writeInbound(Unpooled.wrappedBuffer(data2)));
+
+        res = ch.readInbound();
+        assertThat(res.protocolVersion(), sameInstance(HttpVersion.HTTP_1_1));
+        assertThat(res.status(), is(HttpResponseStatus.OK));
+        content = ch.readInbound();
+        // HTTP 200 has content.
+        assertThat(content.content().readableBytes(), is(8));
+        assertThat(content, CoreMatchers.<HttpContent>instanceOf(LastHttpContent.class));
+        content.release();
+
+        assertThat(ch.finish(), is(false));
+    }
+
+    @Test
+    public void testMultipleResponses() {
+        String response = "HTTP/1.1 200 OK\r\n" +
+                "Content-Length: 0\r\n\r\n";
+
+        HttpClientCodec codec = new HttpClientCodec(4096, 8192, 8192, true);
+        EmbeddedChannel ch = new EmbeddedChannel(codec, new HttpObjectAggregator(1024));
+
+        HttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "http://localhost/");
+        assertTrue(ch.writeOutbound(request));
+
+        assertTrue(ch.writeInbound(Unpooled.copiedBuffer(response, CharsetUtil.UTF_8)));
+        assertTrue(ch.writeInbound(Unpooled.copiedBuffer(response, CharsetUtil.UTF_8)));
+        FullHttpResponse resp = ch.readInbound();
+        assertTrue(resp.decoderResult().isSuccess());
+        resp.release();
+
+        resp = ch.readInbound();
+        assertTrue(resp.decoderResult().isSuccess());
+        resp.release();
+        assertTrue(ch.finishAndReleaseAll());
+    }
+
 }
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentCompressorTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentCompressorTest.java
index 508eb5a..bb43b3c 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentCompressorTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentCompressorTest.java
@@ -15,14 +15,32 @@
  */
 package io.netty.handler.codec.http;
 
+import io.netty.bootstrap.Bootstrap;
+import io.netty.bootstrap.ServerBootstrap;
 import io.netty.buffer.ByteBufUtil;
 import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelOutboundHandlerAdapter;
+import io.netty.channel.ChannelPromise;
+import io.netty.channel.DefaultEventLoopGroup;
+import io.netty.channel.EventLoopGroup;
 import io.netty.channel.embedded.EmbeddedChannel;
+import io.netty.channel.local.LocalAddress;
+import io.netty.channel.local.LocalChannel;
+import io.netty.channel.local.LocalServerChannel;
 import io.netty.handler.codec.DecoderResult;
 import io.netty.handler.codec.EncoderException;
 import io.netty.handler.codec.compression.ZlibWrapper;
 import io.netty.util.CharsetUtil;
 import io.netty.util.ReferenceCountUtil;
+import java.util.UUID;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
 import org.junit.Test;
 
 import static io.netty.handler.codec.http.HttpHeadersTestUtils.of;
@@ -259,6 +277,104 @@ public class HttpContentCompressorTest {
         assertThat(ch.readOutbound(), is(nullValue()));
     }
 
+    @Test
+    public void testExecutorPreserveOrdering() throws Exception {
+        final EventLoopGroup compressorGroup = new DefaultEventLoopGroup(1);
+        EventLoopGroup localGroup = new DefaultEventLoopGroup(1);
+        Channel server = null;
+        Channel client = null;
+        try {
+            ServerBootstrap bootstrap = new ServerBootstrap()
+                .channel(LocalServerChannel.class)
+                .group(localGroup)
+                .childHandler(new ChannelInitializer<LocalChannel>() {
+                @Override
+                protected void initChannel(LocalChannel ch) throws Exception {
+                    ch.pipeline()
+                        .addLast(new HttpServerCodec())
+                        .addLast(new HttpObjectAggregator(1024))
+                        .addLast(compressorGroup, new HttpContentCompressor())
+                        .addLast(new ChannelOutboundHandlerAdapter() {
+                            @Override
+                            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)
+                                throws Exception {
+                                super.write(ctx, msg, promise);
+                            }
+                        })
+                        .addLast(new ChannelInboundHandlerAdapter() {
+                            @Override
+                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+                                if (msg instanceof FullHttpRequest) {
+                                    FullHttpResponse res =
+                                        new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK,
+                                            Unpooled.copiedBuffer("Hello, World", CharsetUtil.US_ASCII));
+                                    ctx.writeAndFlush(res);
+                                    ReferenceCountUtil.release(msg);
+                                    return;
+                                }
+                                super.channelRead(ctx, msg);
+                            }
+                        });
+                }
+            });
+
+            LocalAddress address = new LocalAddress(UUID.randomUUID().toString());
+            server = bootstrap.bind(address).sync().channel();
+
+            final BlockingQueue<HttpObject> responses = new LinkedBlockingQueue<HttpObject>();
+
+            client = new Bootstrap()
+                .channel(LocalChannel.class)
+                .remoteAddress(address)
+                .group(localGroup)
+                .handler(new ChannelInitializer<LocalChannel>() {
+                @Override
+                protected void initChannel(LocalChannel ch) throws Exception {
+                    ch.pipeline().addLast(new HttpClientCodec()).addLast(new ChannelInboundHandlerAdapter() {
+                        @Override
+                        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+                            if (msg instanceof HttpObject) {
+                                responses.put((HttpObject) msg);
+                                return;
+                            }
+                            super.channelRead(ctx, msg);
+                        }
+                    });
+                }
+            }).connect().sync().channel();
+
+            client.writeAndFlush(newRequest()).sync();
+
+            assertEncodedResponse((HttpResponse) responses.poll(1, TimeUnit.SECONDS));
+            HttpContent c = (HttpContent) responses.poll(1, TimeUnit.SECONDS);
+            assertNotNull(c);
+            assertThat(ByteBufUtil.hexDump(c.content()),
+                is("1f8b0800000000000000f248cdc9c9d75108cf2fca4901000000ffff"));
+            c.release();
+
+            c = (HttpContent) responses.poll(1, TimeUnit.SECONDS);
+            assertNotNull(c);
+            assertThat(ByteBufUtil.hexDump(c.content()), is("0300c6865b260c000000"));
+            c.release();
+
+            LastHttpContent last = (LastHttpContent) responses.poll(1, TimeUnit.SECONDS);
+            assertNotNull(last);
+            assertThat(last.content().readableBytes(), is(0));
+            last.release();
+
+            assertNull(responses.poll(1, TimeUnit.SECONDS));
+        } finally {
+            if (client != null) {
+                client.close().sync();
+            }
+            if (server != null) {
+                server.close().sync();
+            }
+            compressorGroup.shutdownGracefully();
+            localGroup.shutdownGracefully();
+        }
+    }
+
     /**
      * If the length of the content is unknown, {@link HttpContentEncoder} should not skip encoding the content
      * even if the actual length is turned out to be 0.
@@ -272,7 +388,7 @@ public class HttpContentCompressorTest {
         assertEncodedResponse(ch);
 
         ch.writeOutbound(LastHttpContent.EMPTY_LAST_CONTENT);
-        HttpContent chunk = (HttpContent) ch.readOutbound();
+        HttpContent chunk = ch.readOutbound();
         assertThat(ByteBufUtil.hexDump(chunk.content()), is("1f8b080000000000000003000000000000000000"));
         assertThat(chunk, is(instanceOf(HttpContent.class)));
         chunk.release();
@@ -423,7 +539,7 @@ public class HttpContentCompressorTest {
         res.headers().set(HttpHeaderNames.CONTENT_ENCODING, HttpHeaderValues.IDENTITY);
         assertTrue(ch.writeOutbound(res));
 
-        FullHttpResponse response = (FullHttpResponse) ch.readOutbound();
+        FullHttpResponse response = ch.readOutbound();
         assertEquals(String.valueOf(len), response.headers().get(HttpHeaderNames.CONTENT_LENGTH));
         assertEquals(HttpHeaderValues.IDENTITY.toString(), response.headers().get(HttpHeaderNames.CONTENT_ENCODING));
         assertEquals("Hello, World", response.content().toString(CharsetUtil.US_ASCII));
@@ -445,7 +561,7 @@ public class HttpContentCompressorTest {
         res.headers().set(HttpHeaderNames.CONTENT_ENCODING, "ascii");
         assertTrue(ch.writeOutbound(res));
 
-        FullHttpResponse response = (FullHttpResponse) ch.readOutbound();
+        FullHttpResponse response = ch.readOutbound();
         assertEquals(String.valueOf(len), response.headers().get(HttpHeaderNames.CONTENT_LENGTH));
         assertEquals("ascii", response.headers().get(HttpHeaderNames.CONTENT_ENCODING));
         assertEquals("Hello, World", response.content().toString(CharsetUtil.US_ASCII));
@@ -500,6 +616,39 @@ public class HttpContentCompressorTest {
         assertTrue(ch.finishAndReleaseAll());
     }
 
+    @Test
+    public void testMultipleAcceptEncodingHeaders() {
+        FullHttpRequest request = newRequest();
+        request.headers().set(HttpHeaderNames.ACCEPT_ENCODING, "unknown; q=1.0")
+               .add(HttpHeaderNames.ACCEPT_ENCODING, "gzip; q=0.5")
+               .add(HttpHeaderNames.ACCEPT_ENCODING, "deflate; q=0");
+
+        EmbeddedChannel ch = new EmbeddedChannel(new HttpContentCompressor());
+
+        assertTrue(ch.writeInbound(request));
+
+        FullHttpResponse res = new DefaultFullHttpResponse(
+                HttpVersion.HTTP_1_1, HttpResponseStatus.OK,
+                Unpooled.copiedBuffer("Gzip Win", CharsetUtil.US_ASCII));
+        assertTrue(ch.writeOutbound(res));
+
+        assertEncodedResponse(ch);
+        HttpContent c = ch.readOutbound();
+        assertThat(ByteBufUtil.hexDump(c.content()), is("1f8b080000000000000072afca2c5008cfcc03000000ffff"));
+        c.release();
+
+        c = ch.readOutbound();
+        assertThat(ByteBufUtil.hexDump(c.content()), is("03001f2ebf0f08000000"));
+        c.release();
+
+        LastHttpContent last = ch.readOutbound();
+        assertThat(last.content().readableBytes(), is(0));
+        last.release();
+
+        assertThat(ch.readOutbound(), is(nullValue()));
+        assertTrue(ch.finishAndReleaseAll());
+    }
+
     private static FullHttpRequest newRequest() {
         FullHttpRequest req = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/");
         req.headers().set(HttpHeaderNames.ACCEPT_ENCODING, "gzip");
@@ -510,7 +659,10 @@ public class HttpContentCompressorTest {
         Object o = ch.readOutbound();
         assertThat(o, is(instanceOf(HttpResponse.class)));
 
-        HttpResponse res = (HttpResponse) o;
+        assertEncodedResponse((HttpResponse) o);
+    }
+
+    private static void assertEncodedResponse(HttpResponse res) {
         assertThat(res, is(not(instanceOf(HttpContent.class))));
         assertThat(res.headers().get(HttpHeaderNames.TRANSFER_ENCODING), is("chunked"));
         assertThat(res.headers().get(HttpHeaderNames.CONTENT_LENGTH), is(nullValue()));
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentDecoderTest.java
index ff06285..24833a7 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentDecoderTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentDecoderTest.java
@@ -178,7 +178,7 @@ public class HttpContentDecoderTest {
         assertThat(o, is(instanceOf(FullHttpResponse.class)));
         FullHttpResponse r = (FullHttpResponse) o;
         assertEquals(100, r.status().code());
-        assertTrue(channel.writeInbound(Unpooled.wrappedBuffer(GZ_HELLO_WORLD)));
+        assertTrue(channel.writeInbound(Unpooled.copiedBuffer(GZ_HELLO_WORLD)));
         r.release();
 
         assertHasInboundMessages(channel, true);
@@ -205,7 +205,7 @@ public class HttpContentDecoderTest {
         FullHttpResponse r = (FullHttpResponse) o;
         assertEquals(100, r.status().code());
         r.release();
-        assertTrue(channel.writeInbound(Unpooled.wrappedBuffer(GZ_HELLO_WORLD)));
+        assertTrue(channel.writeInbound(Unpooled.copiedBuffer(GZ_HELLO_WORLD)));
 
         assertHasInboundMessages(channel, true);
         assertHasOutboundMessages(channel, false);
@@ -232,7 +232,7 @@ public class HttpContentDecoderTest {
         FullHttpResponse r = (FullHttpResponse) o;
         assertEquals(100, r.status().code());
         r.release();
-        assertTrue(channel.writeInbound(Unpooled.wrappedBuffer(GZ_HELLO_WORLD)));
+        assertTrue(channel.writeInbound(Unpooled.copiedBuffer(GZ_HELLO_WORLD)));
 
         assertHasInboundMessages(channel, true);
         assertHasOutboundMessages(channel, false);
@@ -259,7 +259,7 @@ public class HttpContentDecoderTest {
         FullHttpResponse r = (FullHttpResponse) o;
         assertEquals(100, r.status().code());
         r.release();
-        assertTrue(channel.writeInbound(Unpooled.wrappedBuffer(GZ_HELLO_WORLD)));
+        assertTrue(channel.writeInbound(Unpooled.copiedBuffer(GZ_HELLO_WORLD)));
 
         assertHasInboundMessages(channel, true);
         assertHasOutboundMessages(channel, false);
@@ -566,7 +566,7 @@ public class HttpContentDecoderTest {
     private static byte[] gzDecompress(byte[] input) {
         ZlibDecoder decoder = ZlibCodecFactory.newZlibDecoder(ZlibWrapper.GZIP);
         EmbeddedChannel channel = new EmbeddedChannel(decoder);
-        assertTrue(channel.writeInbound(Unpooled.wrappedBuffer(input)));
+        assertTrue(channel.writeInbound(Unpooled.copiedBuffer(input)));
         assertTrue(channel.finish()); // close the channel to indicate end-of-data
 
         int outputSize = 0;
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentDecompressorTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentDecompressorTest.java
new file mode 100644
index 0000000..4a659fa
--- /dev/null
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentDecompressorTest.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2019 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.http;
+
+import io.netty.buffer.Unpooled;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.channel.ChannelOutboundHandlerAdapter;
+import io.netty.channel.embedded.EmbeddedChannel;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class HttpContentDecompressorTest {
+
+    // See https://github.com/netty/netty/issues/8915.
+    @Test
+    public void testInvokeReadWhenNotProduceMessage() {
+        final AtomicInteger readCalled = new AtomicInteger();
+        EmbeddedChannel channel = new EmbeddedChannel(new ChannelOutboundHandlerAdapter() {
+            @Override
+            public void read(ChannelHandlerContext ctx) {
+                readCalled.incrementAndGet();
+                ctx.read();
+            }
+        }, new HttpContentDecompressor(), new ChannelInboundHandlerAdapter() {
+            @Override
+            public void channelRead(ChannelHandlerContext ctx, Object msg) {
+                ctx.fireChannelRead(msg);
+                ctx.read();
+            }
+        });
+
+        channel.config().setAutoRead(false);
+
+        readCalled.set(0);
+        HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
+        response.headers().set(HttpHeaderNames.CONTENT_ENCODING, "gzip");
+        response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json;charset=UTF-8");
+        response.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED);
+
+        Assert.assertTrue(channel.writeInbound(response));
+
+        // we triggered read explicitly
+        Assert.assertEquals(1, readCalled.get());
+
+        Assert.assertTrue(channel.readInbound() instanceof HttpResponse);
+
+        Assert.assertFalse(channel.writeInbound(new DefaultHttpContent(Unpooled.EMPTY_BUFFER)));
+
+        // read was triggered by the HttpContentDecompressor itself as it did not produce any message to the next
+        // inbound handler.
+        Assert.assertEquals(2, readCalled.get());
+        Assert.assertFalse(channel.finishAndReleaseAll());
+    }
+}
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentEncoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentEncoderTest.java
index 6301ee8..9bb8838 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentEncoderTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentEncoderTest.java
@@ -41,7 +41,7 @@ public class HttpContentEncoderTest {
 
     private static final class TestEncoder extends HttpContentEncoder {
         @Override
-        protected Result beginEncode(HttpResponse headers, String acceptEncoding) {
+        protected Result beginEncode(HttpResponse httpResponse, String acceptEncoding) {
             return new Result("test", new EmbeddedChannel(new MessageToByteEncoder<ByteBuf>() {
                 @Override
                 protected void encode(ChannelHandlerContext ctx, ByteBuf in, ByteBuf out) throws Exception {
@@ -395,7 +395,7 @@ public class HttpContentEncoderTest {
     public void testCleanupThrows() {
         HttpContentEncoder encoder = new HttpContentEncoder() {
             @Override
-            protected Result beginEncode(HttpResponse headers, String acceptEncoding) throws Exception {
+            protected Result beginEncode(HttpResponse httpResponse, String acceptEncoding) throws Exception {
                 return new Result("myencoding", new EmbeddedChannel(
                         new ChannelInboundHandlerAdapter() {
                     @Override
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/HttpObjectAggregatorTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/HttpObjectAggregatorTest.java
index 7dc0ac5..1a97659 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/HttpObjectAggregatorTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/HttpObjectAggregatorTest.java
@@ -23,7 +23,10 @@ import io.netty.channel.embedded.EmbeddedChannel;
 import io.netty.handler.codec.DecoderResult;
 import io.netty.handler.codec.DecoderResultProvider;
 import io.netty.handler.codec.TooLongFrameException;
+import io.netty.util.AsciiString;
 import io.netty.util.CharsetUtil;
+import io.netty.util.ReferenceCountUtil;
+
 import org.junit.Test;
 import org.mockito.Mockito;
 
@@ -33,6 +36,7 @@ import java.util.List;
 import static io.netty.handler.codec.http.HttpHeadersTestUtils.of;
 import static org.hamcrest.CoreMatchers.instanceOf;
 import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -40,6 +44,7 @@ import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.junit.Assert.assertSame;
 
 public class HttpObjectAggregatorTest {
 
@@ -139,9 +144,60 @@ public class HttpObjectAggregatorTest {
         assertFalse(embedder.finish());
     }
 
+    @Test
+    public void testOversizedRequestWithContentLengthAndDecoder() {
+        EmbeddedChannel embedder = new EmbeddedChannel(new HttpRequestDecoder(), new HttpObjectAggregator(4, false));
+        assertFalse(embedder.writeInbound(Unpooled.copiedBuffer(
+                "PUT /upload HTTP/1.1\r\n" +
+                        "Content-Length: 5\r\n\r\n", CharsetUtil.US_ASCII)));
+
+        assertNull(embedder.readInbound());
+
+        FullHttpResponse response = embedder.readOutbound();
+        assertEquals(HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE, response.status());
+        assertEquals("0", response.headers().get(HttpHeaderNames.CONTENT_LENGTH));
+
+        assertTrue(embedder.isOpen());
+
+        assertFalse(embedder.writeInbound(Unpooled.wrappedBuffer(new byte[] { 1, 2, 3, 4 })));
+        assertFalse(embedder.writeInbound(Unpooled.wrappedBuffer(new byte[] { 5 })));
+
+        assertNull(embedder.readOutbound());
+
+        assertFalse(embedder.writeInbound(Unpooled.copiedBuffer(
+                "PUT /upload HTTP/1.1\r\n" +
+                        "Content-Length: 2\r\n\r\n", CharsetUtil.US_ASCII)));
+
+        assertEquals(HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE, response.status());
+        assertEquals("0", response.headers().get(HttpHeaderNames.CONTENT_LENGTH));
+
+        assertThat(response, instanceOf(LastHttpContent.class));
+        ReferenceCountUtil.release(response);
+
+        assertTrue(embedder.isOpen());
+
+        assertFalse(embedder.writeInbound(Unpooled.copiedBuffer(new byte[] { 1 })));
+        assertNull(embedder.readOutbound());
+        assertTrue(embedder.writeInbound(Unpooled.copiedBuffer(new byte[] { 2 })));
+        assertNull(embedder.readOutbound());
+
+        FullHttpRequest request = embedder.readInbound();
+        assertEquals(HttpVersion.HTTP_1_1, request.protocolVersion());
+        assertEquals(HttpMethod.PUT, request.method());
+        assertEquals("/upload", request.uri());
+        assertEquals(2, HttpUtil.getContentLength(request));
+
+        byte[] actual = new byte[request.content().readableBytes()];
+        request.content().readBytes(actual);
+        assertArrayEquals(new byte[] { 1, 2 }, actual);
+        request.release();
+
+        assertFalse(embedder.finish());
+    }
+
     @Test
     public void testOversizedRequestWithoutKeepAlive() {
-        // send a HTTP/1.0 request with no keep-alive header
+        // send an HTTP/1.0 request with no keep-alive header
         HttpRequest message = new DefaultHttpRequest(HttpVersion.HTTP_1_0, HttpMethod.PUT, "http://localhost");
         HttpUtil.setContentLength(message, 5);
         checkOversizedRequest(message);
@@ -162,11 +218,48 @@ public class HttpObjectAggregatorTest {
         assertEquals(HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE, response.status());
         assertEquals("0", response.headers().get(HttpHeaderNames.CONTENT_LENGTH));
 
+        assertThat(response, instanceOf(LastHttpContent.class));
+        ReferenceCountUtil.release(response);
+
         if (serverShouldCloseConnection(message, response)) {
             assertFalse(embedder.isOpen());
+
+            try {
+                embedder.writeInbound(new DefaultHttpContent(Unpooled.EMPTY_BUFFER));
+                fail();
+            } catch (Exception e) {
+                assertThat(e, instanceOf(ClosedChannelException.class));
+                // expected
+            }
             assertFalse(embedder.finish());
         } else {
             assertTrue(embedder.isOpen());
+            assertFalse(embedder.writeInbound(new DefaultHttpContent(Unpooled.copiedBuffer(new byte[8]))));
+            assertFalse(embedder.writeInbound(new DefaultHttpContent(Unpooled.copiedBuffer(new byte[8]))));
+
+            // Now start a new message and ensure we will not reject it again.
+            HttpRequest message2 = new DefaultHttpRequest(HttpVersion.HTTP_1_0, HttpMethod.PUT, "http://localhost");
+            HttpUtil.setContentLength(message, 2);
+
+            assertFalse(embedder.writeInbound(message2));
+            assertNull(embedder.readOutbound());
+            assertFalse(embedder.writeInbound(new DefaultHttpContent(Unpooled.copiedBuffer(new byte[] { 1 }))));
+            assertNull(embedder.readOutbound());
+            assertTrue(embedder.writeInbound(new DefaultLastHttpContent(Unpooled.copiedBuffer(new byte[] { 2 }))));
+            assertNull(embedder.readOutbound());
+
+            FullHttpRequest request = embedder.readInbound();
+            assertEquals(message2.protocolVersion(), request.protocolVersion());
+            assertEquals(message2.method(), request.method());
+            assertEquals(message2.uri(), request.uri());
+            assertEquals(2, HttpUtil.getContentLength(request));
+
+            byte[] actual = new byte[request.content().readableBytes()];
+            request.content().readBytes(actual);
+            assertArrayEquals(new byte[] { 1, 2 }, actual);
+            request.release();
+
+            assertFalse(embedder.finish());
         }
     }
 
@@ -517,4 +610,123 @@ public class HttpObjectAggregatorTest {
         aggregatedRep.release();
         replacedRep.release();
     }
+
+    @Test
+    public void testSelectiveRequestAggregation() {
+        HttpObjectAggregator myPostAggregator = new HttpObjectAggregator(1024 * 1024) {
+            @Override
+            protected boolean isStartMessage(HttpObject msg) throws Exception {
+                if (msg instanceof HttpRequest) {
+                    HttpRequest request = (HttpRequest) msg;
+                    HttpMethod method = request.method();
+
+                    if (method.equals(HttpMethod.POST)) {
+                        return true;
+                    }
+                }
+
+                return false;
+            }
+        };
+
+        EmbeddedChannel channel = new EmbeddedChannel(myPostAggregator);
+
+        try {
+            // Aggregate: POST
+            HttpRequest request1 = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/");
+            HttpContent content1 = new DefaultHttpContent(Unpooled.copiedBuffer("Hello, World!", CharsetUtil.UTF_8));
+            request1.headers().set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN);
+
+            assertTrue(channel.writeInbound(request1, content1, LastHttpContent.EMPTY_LAST_CONTENT));
+
+            // Getting an aggregated response out
+            Object msg1 = channel.readInbound();
+            try {
+                assertTrue(msg1 instanceof FullHttpRequest);
+            } finally {
+                ReferenceCountUtil.release(msg1);
+            }
+
+            // Don't aggregate: non-POST
+            HttpRequest request2 = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.PUT, "/");
+            HttpContent content2 = new DefaultHttpContent(Unpooled.copiedBuffer("Hello, World!", CharsetUtil.UTF_8));
+            request2.headers().set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN);
+
+            try {
+                assertTrue(channel.writeInbound(request2, content2, LastHttpContent.EMPTY_LAST_CONTENT));
+
+                // Getting the same response objects out
+                assertSame(request2, channel.readInbound());
+                assertSame(content2, channel.readInbound());
+                assertSame(LastHttpContent.EMPTY_LAST_CONTENT, channel.readInbound());
+            } finally {
+              ReferenceCountUtil.release(request2);
+              ReferenceCountUtil.release(content2);
+            }
+
+            assertFalse(channel.finish());
+        } finally {
+          channel.close();
+        }
+    }
+
+    @Test
+    public void testSelectiveResponseAggregation() {
+        HttpObjectAggregator myTextAggregator = new HttpObjectAggregator(1024 * 1024) {
+            @Override
+            protected boolean isStartMessage(HttpObject msg) throws Exception {
+                if (msg instanceof HttpResponse) {
+                    HttpResponse response = (HttpResponse) msg;
+                    HttpHeaders headers = response.headers();
+
+                    String contentType = headers.get(HttpHeaderNames.CONTENT_TYPE);
+                    if (AsciiString.contentEqualsIgnoreCase(contentType, HttpHeaderValues.TEXT_PLAIN)) {
+                        return true;
+                    }
+                }
+
+                return false;
+            }
+        };
+
+        EmbeddedChannel channel = new EmbeddedChannel(myTextAggregator);
+
+        try {
+            // Aggregate: text/plain
+            HttpResponse response1 = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
+            HttpContent content1 = new DefaultHttpContent(Unpooled.copiedBuffer("Hello, World!", CharsetUtil.UTF_8));
+            response1.headers().set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN);
+
+            assertTrue(channel.writeInbound(response1, content1, LastHttpContent.EMPTY_LAST_CONTENT));
+
+            // Getting an aggregated response out
+            Object msg1 = channel.readInbound();
+            try {
+                assertTrue(msg1 instanceof FullHttpResponse);
+            } finally {
+                ReferenceCountUtil.release(msg1);
+            }
+
+            // Don't aggregate: application/json
+            HttpResponse response2 = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
+            HttpContent content2 = new DefaultHttpContent(Unpooled.copiedBuffer("{key: 'value'}", CharsetUtil.UTF_8));
+            response2.headers().set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON);
+
+            try {
+                assertTrue(channel.writeInbound(response2, content2, LastHttpContent.EMPTY_LAST_CONTENT));
+
+                // Getting the same response objects out
+                assertSame(response2, channel.readInbound());
+                assertSame(content2, channel.readInbound());
+                assertSame(LastHttpContent.EMPTY_LAST_CONTENT, channel.readInbound());
+            } finally {
+                ReferenceCountUtil.release(response2);
+                ReferenceCountUtil.release(content2);
+            }
+
+            assertFalse(channel.finish());
+        } finally {
+          channel.close();
+        }
+    }
 }
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestDecoderTest.java
index 4572063..4f83d82 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestDecoderTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestDecoderTest.java
@@ -81,7 +81,7 @@ public class HttpRequestDecoderTest {
 
     private static void testDecodeWholeRequestAtOnce(byte[] content) {
         EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder());
-        assertTrue(channel.writeInbound(Unpooled.wrappedBuffer(content)));
+        assertTrue(channel.writeInbound(Unpooled.copiedBuffer(content)));
         HttpRequest req = channel.readInbound();
         assertNotNull(req);
         checkHeaders(req.headers());
@@ -145,14 +145,14 @@ public class HttpRequestDecoderTest {
                 amount = headerLength -  a;
             }
 
-            // if header is done it should produce a HttpRequest
-            channel.writeInbound(Unpooled.wrappedBuffer(content, a, amount));
+            // if header is done it should produce an HttpRequest
+            channel.writeInbound(Unpooled.copiedBuffer(content, a, amount));
             a += amount;
         }
 
         for (int i = CONTENT_LENGTH; i > 0; i --) {
             // Should produce HttpContent
-            channel.writeInbound(Unpooled.wrappedBuffer(content, content.length - i, 1));
+            channel.writeInbound(Unpooled.copiedBuffer(content, content.length - i, 1));
         }
 
         HttpRequest req = channel.readInbound();
@@ -308,6 +308,42 @@ public class HttpRequestDecoderTest {
         assertFalse(channel.finish());
     }
 
+    @Test
+    public void testTooLargeInitialLineWithWSOnly() {
+        testTooLargeInitialLineWithControlCharsOnly("                    ");
+    }
+
+    @Test
+    public void testTooLargeInitialLineWithCRLFOnly() {
+        testTooLargeInitialLineWithControlCharsOnly("\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n");
+    }
+
+    private static void testTooLargeInitialLineWithControlCharsOnly(String controlChars) {
+        EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder(15, 1024, 1024));
+        String requestStr = controlChars + "GET / HTTP/1.1\r\n" +
+                "Host: localhost1\r\n\r\n";
+
+        assertTrue(channel.writeInbound(Unpooled.copiedBuffer(requestStr, CharsetUtil.US_ASCII)));
+        HttpRequest request = channel.readInbound();
+        assertTrue(request.decoderResult().isFailure());
+        assertTrue(request.decoderResult().cause() instanceof TooLongFrameException);
+        assertFalse(channel.finish());
+    }
+
+    @Test
+    public void testInitialLineWithLeadingControlChars() {
+        EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder());
+        String crlf = "\r\n";
+        String request =  crlf + "GET /some/path HTTP/1.1" + crlf +
+                "Host: localhost" + crlf + crlf;
+        assertTrue(channel.writeInbound(Unpooled.copiedBuffer(request, CharsetUtil.US_ASCII)));
+        HttpRequest req = channel.readInbound();
+        assertEquals(HttpMethod.GET, req.method());
+        assertEquals("/some/path", req.uri());
+        assertEquals(HttpVersion.HTTP_1_1, req.protocolVersion());
+        assertTrue(channel.finishAndReleaseAll());
+    }
+
     @Test
     public void testTooLargeHeaders() {
         EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder(1024, 10, 1024));
@@ -320,4 +356,134 @@ public class HttpRequestDecoderTest {
         assertTrue(request.decoderResult().cause() instanceof TooLongFrameException);
         assertFalse(channel.finish());
     }
+
+    @Test
+    public void testWhitespace() {
+        String requestStr = "GET /some/path HTTP/1.1\r\n" +
+                "Transfer-Encoding : chunked\r\n" +
+                "Host: netty.io\r\n\r\n";
+        testInvalidHeaders0(requestStr);
+    }
+
+    @Test
+    public void testWhitespaceBeforeTransferEncoding01() {
+        String requestStr = "GET /some/path HTTP/1.1\r\n" +
+                " Transfer-Encoding : chunked\r\n" +
+                "Content-Length: 1\r\n" +
+                "Host: netty.io\r\n\r\n" +
+                "a";
+        testInvalidHeaders0(requestStr);
+    }
+
+    @Test
+    public void testWhitespaceBeforeTransferEncoding02() {
+        String requestStr = "POST / HTTP/1.1" +
+                " Transfer-Encoding : chunked\r\n" +
+                "Host: target.com" +
+                "Content-Length: 65\r\n\r\n" +
+                "0\r\n\r\n" +
+                "GET /maliciousRequest HTTP/1.1\r\n" +
+                "Host: evilServer.com\r\n" +
+                "Foo: x";
+        testInvalidHeaders0(requestStr);
+    }
+
+    @Test
+    public void testHeaderWithNoValueAndMissingColon() {
+        String requestStr = "GET /some/path HTTP/1.1\r\n" +
+                "Content-Length: 0\r\n" +
+                "Host:\r\n" +
+                "netty.io\r\n\r\n";
+        testInvalidHeaders0(requestStr);
+    }
+
+    @Test
+    public void testMultipleContentLengthHeaders() {
+        String requestStr = "GET /some/path HTTP/1.1\r\n" +
+                "Content-Length: 1\r\n" +
+                "Content-Length: 0\r\n\r\n" +
+                "b";
+        testInvalidHeaders0(requestStr);
+    }
+
+    @Test
+    public void testMultipleContentLengthHeaders2() {
+        String requestStr = "GET /some/path HTTP/1.1\r\n" +
+                "Content-Length: 1\r\n" +
+                "Connection: close\r\n" +
+                "Content-Length: 0\r\n\r\n" +
+                "b";
+        testInvalidHeaders0(requestStr);
+    }
+
+    @Test
+    public void testContentLengthHeaderWithCommaValue() {
+        String requestStr = "GET /some/path HTTP/1.1\r\n" +
+                "Content-Length: 1,1\r\n\r\n" +
+                "b";
+        testInvalidHeaders0(requestStr);
+    }
+
+    @Test
+    public void testMultipleContentLengthHeadersWithFolding() {
+        String requestStr = "POST / HTTP/1.1\r\n" +
+                "Host: example.com\r\n" +
+                "Connection: close\r\n" +
+                "Content-Length: 5\r\n" +
+                "Content-Length:\r\n" +
+                "\t6\r\n\r\n" +
+                "123456";
+        testInvalidHeaders0(requestStr);
+    }
+
+    @Test
+    public void testContentLengthAndTransferEncodingHeadersWithVerticalTab() {
+        testContentLengthAndTransferEncodingHeadersWithInvalidSeparator((char) 0x0b, false);
+        testContentLengthAndTransferEncodingHeadersWithInvalidSeparator((char) 0x0b, true);
+    }
+
+    @Test
+    public void testContentLengthAndTransferEncodingHeadersWithCR() {
+        testContentLengthAndTransferEncodingHeadersWithInvalidSeparator((char) 0x0d, false);
+        testContentLengthAndTransferEncodingHeadersWithInvalidSeparator((char) 0x0d, true);
+    }
+
+    private static void testContentLengthAndTransferEncodingHeadersWithInvalidSeparator(
+            char separator, boolean extraLine) {
+        String requestStr = "POST / HTTP/1.1\r\n" +
+                "Host: example.com\r\n" +
+                "Connection: close\r\n" +
+                "Content-Length: 9\r\n" +
+                "Transfer-Encoding:" + separator + "chunked\r\n\r\n" +
+                (extraLine ? "0\r\n\r\n" : "") +
+                "something\r\n\r\n";
+        testInvalidHeaders0(requestStr);
+    }
+
+    @Test
+    public void testContentLengthHeaderAndChunked() {
+        String requestStr = "POST / HTTP/1.1\r\n" +
+                "Host: example.com\r\n" +
+                "Connection: close\r\n" +
+                "Content-Length: 5\r\n" +
+                "Transfer-Encoding: chunked\r\n\r\n" +
+                "0\r\n\r\n";
+        EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder());
+        assertTrue(channel.writeInbound(Unpooled.copiedBuffer(requestStr, CharsetUtil.US_ASCII)));
+        HttpRequest request = channel.readInbound();
+        assertFalse(request.decoderResult().isFailure());
+        assertTrue(request.headers().contains("Transfer-Encoding", "chunked", false));
+        assertFalse(request.headers().contains("Content-Length"));
+        LastHttpContent c = channel.readInbound();
+        assertFalse(channel.finish());
+    }
+
+    private static void testInvalidHeaders0(String requestStr) {
+        EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder());
+        assertTrue(channel.writeInbound(Unpooled.copiedBuffer(requestStr, CharsetUtil.US_ASCII)));
+        HttpRequest request = channel.readInbound();
+        assertTrue(request.decoderResult().isFailure());
+        assertTrue(request.decoderResult().cause() instanceof IllegalArgumentException);
+        assertFalse(channel.finish());
+    }
 }
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/HttpResponseDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/HttpResponseDecoderTest.java
index 017dbd5..b0ccd0c 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/HttpResponseDecoderTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/HttpResponseDecoderTest.java
@@ -50,7 +50,7 @@ public class HttpResponseDecoderTest {
         final int maxHeaderSize = 8192;
 
         final EmbeddedChannel ch = new EmbeddedChannel(new HttpResponseDecoder(4096, maxHeaderSize, 8192));
-        final char[] bytes = new char[maxHeaderSize / 2 - 2];
+        final char[] bytes = new char[maxHeaderSize / 2 - 4];
         Arrays.fill(bytes, 'a');
 
         ch.writeInbound(Unpooled.copiedBuffer("HTTP/1.1 200 OK\r\n", CharsetUtil.US_ASCII));
@@ -122,7 +122,7 @@ public class HttpResponseDecoderTest {
         for (int i = 0; i < 10; i++) {
             assertFalse(ch.writeInbound(Unpooled.copiedBuffer(Integer.toHexString(data.length) + "\r\n",
                     CharsetUtil.US_ASCII)));
-            assertTrue(ch.writeInbound(Unpooled.wrappedBuffer(data)));
+            assertTrue(ch.writeInbound(Unpooled.copiedBuffer(data)));
             HttpContent content = ch.readInbound();
             assertEquals(data.length, content.content().readableBytes());
 
@@ -164,7 +164,7 @@ public class HttpResponseDecoderTest {
         for (int i = 0; i < 10; i++) {
             assertFalse(ch.writeInbound(Unpooled.copiedBuffer(Integer.toHexString(data.length) + "\r\n",
                     CharsetUtil.US_ASCII)));
-            assertTrue(ch.writeInbound(Unpooled.wrappedBuffer(data)));
+            assertTrue(ch.writeInbound(Unpooled.copiedBuffer(data)));
 
             byte[] decodedData = new byte[data.length];
             HttpContent content = ch.readInbound();
@@ -446,13 +446,13 @@ public class HttpResponseDecoderTest {
                 amount = headerLength -  a;
             }
 
-            // if header is done it should produce a HttpRequest
+            // if header is done it should produce an HttpRequest
             boolean headerDone = a + amount == headerLength;
-            assertEquals(headerDone, ch.writeInbound(Unpooled.wrappedBuffer(content, a, amount)));
+            assertEquals(headerDone, ch.writeInbound(Unpooled.copiedBuffer(content, a, amount)));
             a += amount;
         }
 
-        ch.writeInbound(Unpooled.wrappedBuffer(content, headerLength, content.length - headerLength));
+        ch.writeInbound(Unpooled.copiedBuffer(content, headerLength, content.length - headerLength));
         HttpResponse res = ch.readInbound();
         assertThat(res.protocolVersion(), sameInstance(HttpVersion.HTTP_1_1));
         assertThat(res.status(), is(HttpResponseStatus.OK));
@@ -483,8 +483,8 @@ public class HttpResponseDecoderTest {
         for (int i = 0; i < data.length; i++) {
             data[i] = (byte) i;
         }
-        ch.writeInbound(Unpooled.wrappedBuffer(data, 0, data.length / 2));
-        ch.writeInbound(Unpooled.wrappedBuffer(data, 5, data.length / 2));
+        ch.writeInbound(Unpooled.copiedBuffer(data, 0, data.length / 2));
+        ch.writeInbound(Unpooled.copiedBuffer(data, 5, data.length / 2));
 
         HttpResponse res = ch.readInbound();
         assertThat(res.protocolVersion(), sameInstance(HttpVersion.HTTP_1_1));
@@ -492,12 +492,12 @@ public class HttpResponseDecoderTest {
 
         HttpContent firstContent = ch.readInbound();
         assertThat(firstContent.content().readableBytes(), is(5));
-        assertEquals(Unpooled.wrappedBuffer(data, 0, 5), firstContent.content());
+        assertEquals(Unpooled.copiedBuffer(data, 0, 5), firstContent.content());
         firstContent.release();
 
         LastHttpContent lastContent = ch.readInbound();
         assertEquals(5, lastContent.content().readableBytes());
-        assertEquals(Unpooled.wrappedBuffer(data, 5, 5), lastContent.content());
+        assertEquals(Unpooled.copiedBuffer(data, 5, 5), lastContent.content());
         lastContent.release();
 
         assertThat(ch.finish(), is(false));
@@ -524,15 +524,15 @@ public class HttpResponseDecoderTest {
                 amount = header.length -  a;
             }
 
-            ch.writeInbound(Unpooled.wrappedBuffer(header, a, amount));
+            ch.writeInbound(Unpooled.copiedBuffer(header, a, amount));
             a += amount;
         }
         byte[] data = new byte[10];
         for (int i = 0; i < data.length; i++) {
             data[i] = (byte) i;
         }
-        ch.writeInbound(Unpooled.wrappedBuffer(data, 0, data.length / 2));
-        ch.writeInbound(Unpooled.wrappedBuffer(data, 5, data.length / 2));
+        ch.writeInbound(Unpooled.copiedBuffer(data, 0, data.length / 2));
+        ch.writeInbound(Unpooled.copiedBuffer(data, 5, data.length / 2));
 
         HttpResponse res = ch.readInbound();
         assertThat(res.protocolVersion(), sameInstance(HttpVersion.HTTP_1_1));
@@ -589,7 +589,7 @@ public class HttpResponseDecoderTest {
         byte[] otherData = {1, 2, 3, 4};
 
         EmbeddedChannel ch = new EmbeddedChannel(new HttpResponseDecoder());
-        ch.writeInbound(Unpooled.wrappedBuffer(data, otherData));
+        ch.writeInbound(Unpooled.copiedBuffer(data, otherData));
 
         HttpResponse res = ch.readInbound();
         assertThat(res.protocolVersion(), sameInstance(HttpVersion.HTTP_1_1));
@@ -625,7 +625,7 @@ public class HttpResponseDecoderTest {
 
         EmbeddedChannel ch = new EmbeddedChannel(new HttpResponseDecoder());
 
-        ch.writeInbound(Unpooled.wrappedBuffer(data));
+        ch.writeInbound(Unpooled.copiedBuffer(data));
 
         // Garbage input should generate the 999 Unknown response.
         HttpResponse res = ch.readInbound();
@@ -636,7 +636,7 @@ public class HttpResponseDecoderTest {
         assertThat(ch.readInbound(), is(nullValue()));
 
         // More garbage should not generate anything (i.e. the decoder discards anything beyond this point.)
-        ch.writeInbound(Unpooled.wrappedBuffer(data));
+        ch.writeInbound(Unpooled.copiedBuffer(data));
         assertThat(ch.readInbound(), is(nullValue()));
 
         // Closing the connection should not generate anything since the protocol has been violated.
@@ -683,4 +683,48 @@ public class HttpResponseDecoderTest {
         assertThat(message.decoderResult().cause(), instanceOf(PrematureChannelClosureException.class));
         assertNull(channel.readInbound());
     }
+
+    @Test
+    public void testTrailerWithEmptyLineInSeparateBuffer() {
+        HttpResponseDecoder decoder = new HttpResponseDecoder();
+        EmbeddedChannel channel = new EmbeddedChannel(decoder);
+
+        String headers = "HTTP/1.1 200 OK\r\n"
+                + "Transfer-Encoding: chunked\r\n"
+                + "Trailer: My-Trailer\r\n";
+        assertFalse(channel.writeInbound(Unpooled.copiedBuffer(headers.getBytes(CharsetUtil.US_ASCII))));
+        assertTrue(channel.writeInbound(Unpooled.copiedBuffer("\r\n".getBytes(CharsetUtil.US_ASCII))));
+
+        assertTrue(channel.writeInbound(Unpooled.copiedBuffer("0\r\n", CharsetUtil.US_ASCII)));
+        assertTrue(channel.writeInbound(Unpooled.copiedBuffer("My-Trailer: 42\r\n", CharsetUtil.US_ASCII)));
+        assertTrue(channel.writeInbound(Unpooled.copiedBuffer("\r\n", CharsetUtil.US_ASCII)));
+
+        HttpResponse response = channel.readInbound();
+        assertEquals(2, response.headers().size());
+        assertEquals("chunked", response.headers().get(HttpHeaderNames.TRANSFER_ENCODING));
+        assertEquals("My-Trailer", response.headers().get(HttpHeaderNames.TRAILER));
+
+        LastHttpContent lastContent = channel.readInbound();
+        assertEquals(1, lastContent.trailingHeaders().size());
+        assertEquals("42", lastContent.trailingHeaders().get("My-Trailer"));
+        assertEquals(0, lastContent.content().readableBytes());
+        lastContent.release();
+
+        assertFalse(channel.finish());
+    }
+
+    @Test
+    public void testWhitespace() {
+        EmbeddedChannel channel = new EmbeddedChannel(new HttpResponseDecoder());
+        String requestStr = "HTTP/1.1 200 OK\r\n" +
+                "Transfer-Encoding : chunked\r\n" +
+                "Host: netty.io\n\r\n";
+
+        assertTrue(channel.writeInbound(Unpooled.copiedBuffer(requestStr, CharsetUtil.US_ASCII)));
+        HttpResponse response = channel.readInbound();
+        assertFalse(response.decoderResult().isFailure());
+        assertEquals(HttpHeaderValues.CHUNKED.toString(), response.headers().get(HttpHeaderNames.TRANSFER_ENCODING));
+        assertEquals("netty.io", response.headers().get(HttpHeaderNames.HOST));
+        assertFalse(channel.finish());
+    }
 }
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/HttpUtilTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/HttpUtilTest.java
index 3159606..186b498 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/HttpUtilTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/HttpUtilTest.java
@@ -109,6 +109,7 @@ public class HttpUtilTest {
     public void testGetCharset_defaultValue() {
         final String SIMPLE_CONTENT_TYPE = "text/html";
         final String CONTENT_TYPE_WITH_INCORRECT_CHARSET = "text/html; charset=UTFFF";
+        final String CONTENT_TYPE_WITH_ILLEGAL_CHARSET_NAME = "text/html; charset=!illegal!";
 
         HttpMessage message = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
         message.headers().set(HttpHeaderNames.CONTENT_TYPE, SIMPLE_CONTENT_TYPE);
@@ -127,6 +128,15 @@ public class HttpUtilTest {
         assertEquals(CharsetUtil.UTF_8, HttpUtil.getCharset(message, StandardCharsets.UTF_8));
         assertEquals(CharsetUtil.UTF_8,
                      HttpUtil.getCharset(CONTENT_TYPE_WITH_INCORRECT_CHARSET, StandardCharsets.UTF_8));
+
+        message.headers().set(HttpHeaderNames.CONTENT_TYPE, CONTENT_TYPE_WITH_ILLEGAL_CHARSET_NAME);
+        assertEquals(CharsetUtil.ISO_8859_1, HttpUtil.getCharset(message));
+        assertEquals(CharsetUtil.ISO_8859_1, HttpUtil.getCharset(CONTENT_TYPE_WITH_ILLEGAL_CHARSET_NAME));
+
+        message.headers().set(HttpHeaderNames.CONTENT_TYPE, CONTENT_TYPE_WITH_ILLEGAL_CHARSET_NAME);
+        assertEquals(CharsetUtil.UTF_8, HttpUtil.getCharset(message, StandardCharsets.UTF_8));
+        assertEquals(CharsetUtil.UTF_8,
+                HttpUtil.getCharset(CONTENT_TYPE_WITH_ILLEGAL_CHARSET_NAME, StandardCharsets.UTF_8));
     }
 
     @Test
@@ -317,4 +327,25 @@ public class HttpUtilTest {
             "http:localhost/http_1_0");
         assertFalse(HttpUtil.isKeepAlive(http10Message));
     }
+
+    @Test
+    public void testKeepAliveIfConnectionHeaderMultipleValues() {
+        HttpMessage http11Message = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET,
+            "http:localhost/http_1_1");
+        http11Message.headers().set(
+                HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE + ", " + HttpHeaderValues.CLOSE);
+        assertFalse(HttpUtil.isKeepAlive(http11Message));
+
+        http11Message.headers().set(
+                HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE + ", Close");
+        assertFalse(HttpUtil.isKeepAlive(http11Message));
+
+        http11Message.headers().set(
+                HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE + ", " + HttpHeaderValues.UPGRADE);
+        assertFalse(HttpUtil.isKeepAlive(http11Message));
+
+        http11Message.headers().set(
+                HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE + ", " + HttpHeaderValues.KEEP_ALIVE);
+        assertTrue(HttpUtil.isKeepAlive(http11Message));
+    }
 }
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/QueryStringDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/QueryStringDecoderTest.java
index a0071c4..5937d66 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/QueryStringDecoderTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/QueryStringDecoderTest.java
@@ -129,6 +129,13 @@ public class QueryStringDecoderTest {
         assertQueryString("/foo?a=1&a=&a=", "/foo?a=1&a&a=");
     }
 
+    @Test
+    public void testSemicolon() {
+        assertQueryString("/foo?a=1;2", "/foo?a=1;2", false);
+        // ";" should be treated as a normal character, see #8855
+        assertQueryString("/foo?a=1;2", "/foo?a=1%3B2", true);
+    }
+
     @Test
     public void testPathSpecific() {
         // decode escaped characters
@@ -225,8 +232,14 @@ public class QueryStringDecoderTest {
     }
 
     private static void assertQueryString(String expected, String actual) {
-        QueryStringDecoder ed = new QueryStringDecoder(expected, CharsetUtil.UTF_8);
-        QueryStringDecoder ad = new QueryStringDecoder(actual, CharsetUtil.UTF_8);
+        assertQueryString(expected, actual, false);
+    }
+
+    private static void assertQueryString(String expected, String actual, boolean semicolonIsNormalChar) {
+        QueryStringDecoder ed = new QueryStringDecoder(expected, CharsetUtil.UTF_8, true,
+                1024, semicolonIsNormalChar);
+        QueryStringDecoder ad = new QueryStringDecoder(actual, CharsetUtil.UTF_8, true,
+                1024, semicolonIsNormalChar);
         Assert.assertEquals(ed.path(), ad.path());
         Assert.assertEquals(ed.parameters(), ad.parameters());
     }
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/QueryStringEncoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/QueryStringEncoderTest.java
index a9f6f90..ba4d2b6 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/QueryStringEncoderTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/QueryStringEncoderTest.java
@@ -15,12 +15,12 @@
  */
 package io.netty.handler.codec.http;
 
-import java.net.URI;
-import java.nio.charset.Charset;
-
 import org.junit.Assert;
 import org.junit.Test;
 
+import java.net.URI;
+import java.nio.charset.Charset;
+
 public class QueryStringEncoderTest {
 
     @Test
@@ -37,6 +37,11 @@ public class QueryStringEncoderTest {
         Assert.assertEquals("/foo/\u00A5?a=%C2%A5", e.toString());
         Assert.assertEquals(new URI("/foo/\u00A5?a=%C2%A5"), e.toUri());
 
+        e = new QueryStringEncoder("/foo/\u00A5");
+        e.addParam("a", "abc\u00A5");
+        Assert.assertEquals("/foo/\u00A5?a=abc%C2%A5", e.toString());
+        Assert.assertEquals(new URI("/foo/\u00A5?a=abc%C2%A5"), e.toUri());
+
         e = new QueryStringEncoder("/foo");
         e.addParam("a", "1");
         e.addParam("b", "2");
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ClientCookieDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ClientCookieDecoderTest.java
index 2cd69f9..24e3361 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ClientCookieDecoderTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ClientCookieDecoderTest.java
@@ -16,6 +16,7 @@
 package io.netty.handler.codec.http.cookie;
 
 import io.netty.handler.codec.DateFormatter;
+import io.netty.handler.codec.http.cookie.CookieHeaderNames.SameSite;
 import org.junit.Test;
 
 import java.util.ArrayList;
@@ -25,6 +26,8 @@ import java.util.Date;
 import java.util.Iterator;
 import java.util.TimeZone;
 
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
 import static org.junit.Assert.*;
 
 public class ClientCookieDecoderTest {
@@ -32,7 +35,7 @@ public class ClientCookieDecoderTest {
     public void testDecodingSingleCookieV0() {
         String cookieString = "myCookie=myValue;expires="
                 + DateFormatter.format(new Date(System.currentTimeMillis() + 50000))
-                + ";path=/apathsomewhere;domain=.adomainsomewhere;secure;";
+                + ";path=/apathsomewhere;domain=.adomainsomewhere;secure;SameSite=None";
 
         Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString);
         assertNotNull(cookie);
@@ -44,6 +47,9 @@ public class ClientCookieDecoderTest {
                 cookie.maxAge() >= 40 && cookie.maxAge() <= 60);
         assertEquals("/apathsomewhere", cookie.path());
         assertTrue(cookie.isSecure());
+
+        assertThat(cookie, is(instanceOf(DefaultCookie.class)));
+        assertEquals(SameSite.None, ((DefaultCookie) cookie).sameSite());
     }
 
     @Test
@@ -259,7 +265,7 @@ public class ClientCookieDecoderTest {
                 "'=KqtH";
 
         Cookie cookie = ClientCookieDecoder.STRICT.decode("bh=\"" + longValue
-                                                   + "\";");
+                + "\";");
         assertEquals("bh", cookie.name());
         assertEquals(longValue, cookie.value());
     }
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ClientCookieEncoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ClientCookieEncoderTest.java
index af81059..8fbe544 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ClientCookieEncoderTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ClientCookieEncoderTest.java
@@ -51,4 +51,16 @@ public class ClientCookieEncoderTest {
     public void testRejectCookieValueWithSemicolon() {
         ClientCookieEncoder.STRICT.encode(new DefaultCookie("myCookie", "foo;bar"));
     }
+
+    @Test
+    public void testComparatorForSamePathLength() {
+        Cookie cookie = new DefaultCookie("test", "value");
+        cookie.setPath("1");
+
+        Cookie cookie2 = new DefaultCookie("test", "value");
+        cookie2.setPath("2");
+
+        assertEquals(0, ClientCookieEncoder.COOKIE_COMPARATOR.compare(cookie, cookie2));
+        assertEquals(0, ClientCookieEncoder.COOKIE_COMPARATOR.compare(cookie2, cookie));
+    }
 }
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ServerCookieDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ServerCookieDecoderTest.java
index b157bc3..b6b3365 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ServerCookieDecoderTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ServerCookieDecoderTest.java
@@ -15,6 +15,7 @@
  */
 package io.netty.handler.codec.http.cookie;
 
+import java.util.List;
 import org.junit.Test;
 
 import java.util.Iterator;
@@ -53,6 +54,26 @@ public class ServerCookieDecoderTest {
         assertEquals("myValue3", cookie.value());
     }
 
+    @Test
+    public void testDecodingAllMultipleCookies() {
+        String c1 = "myCookie=myValue;";
+        String c2 = "myCookie=myValue2;";
+        String c3 = "myCookie=myValue3;";
+
+        List<Cookie> cookies = ServerCookieDecoder.STRICT.decodeAll(c1 + c2 + c3);
+        assertEquals(3, cookies.size());
+        Iterator<Cookie> it = cookies.iterator();
+        Cookie cookie = it.next();
+        assertNotNull(cookie);
+        assertEquals("myValue", cookie.value());
+        cookie = it.next();
+        assertNotNull(cookie);
+        assertEquals("myValue2", cookie.value());
+        cookie = it.next();
+        assertNotNull(cookie);
+        assertEquals("myValue3", cookie.value());
+    }
+
     @Test
     public void testDecodingGoogleAnalyticsCookie() {
         String source =
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ServerCookieEncoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ServerCookieEncoderTest.java
index 82f813f..42f9c21 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ServerCookieEncoderTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ServerCookieEncoderTest.java
@@ -32,6 +32,7 @@ import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import io.netty.handler.codec.http.cookie.CookieHeaderNames.SameSite;
 import org.junit.Test;
 
 public class ServerCookieEncoderTest {
@@ -41,13 +42,14 @@ public class ServerCookieEncoderTest {
 
         int maxAge = 50;
 
-        String result =
-                "myCookie=myValue; Max-Age=50; Expires=(.+?); Path=/apathsomewhere; Domain=.adomainsomewhere; Secure";
-        Cookie cookie = new DefaultCookie("myCookie", "myValue");
+        String result = "myCookie=myValue; Max-Age=50; Expires=(.+?); Path=/apathsomewhere;" +
+                " Domain=.adomainsomewhere; Secure; SameSite=Lax";
+        DefaultCookie cookie = new DefaultCookie("myCookie", "myValue");
         cookie.setDomain(".adomainsomewhere");
         cookie.setMaxAge(maxAge);
         cookie.setPath("/apathsomewhere");
         cookie.setSecure(true);
+        cookie.setSameSite(SameSite.Lax);
 
         String encodedCookie = ServerCookieEncoder.STRICT.encode(cookie);
 
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/cors/CorsHandlerTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/cors/CorsHandlerTest.java
index c4975e3..12685df 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/cors/CorsHandlerTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/cors/CorsHandlerTest.java
@@ -53,6 +53,7 @@ public class CorsHandlerTest {
     public void nonCorsRequest() {
         final HttpResponse response = simpleRequest(forAnyOrigin().build(), null);
         assertThat(response.headers().contains(ACCESS_CONTROL_ALLOW_ORIGIN), is(false));
+        assertThat(ReferenceCountUtil.release(response), is(true));
     }
 
     @Test
@@ -60,6 +61,7 @@ public class CorsHandlerTest {
         final HttpResponse response = simpleRequest(forAnyOrigin().build(), "http://localhost:7777");
         assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN), is("*"));
         assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_HEADERS), is(nullValue()));
+        assertThat(ReferenceCountUtil.release(response), is(true));
     }
 
     @Test
@@ -70,6 +72,7 @@ public class CorsHandlerTest {
         assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN), is("null"));
         assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_CREDENTIALS), is(equalTo("true")));
         assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_HEADERS), is(nullValue()));
+        assertThat(ReferenceCountUtil.release(response), is(true));
     }
 
     @Test
@@ -78,6 +81,7 @@ public class CorsHandlerTest {
         final HttpResponse response = simpleRequest(forOrigin(origin).build(), origin);
         assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN), is(origin));
         assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_HEADERS), is(nullValue()));
+        assertThat(ReferenceCountUtil.release(response), is(true));
     }
 
     @Test
@@ -88,9 +92,12 @@ public class CorsHandlerTest {
         final HttpResponse response1 = simpleRequest(forOrigins(origins).build(), origin1);
         assertThat(response1.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN), is(origin1));
         assertThat(response1.headers().get(ACCESS_CONTROL_ALLOW_HEADERS), is(nullValue()));
+        assertThat(ReferenceCountUtil.release(response1), is(true));
+
         final HttpResponse response2 = simpleRequest(forOrigins(origins).build(), origin2);
         assertThat(response2.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN), is(origin2));
         assertThat(response2.headers().get(ACCESS_CONTROL_ALLOW_HEADERS), is(nullValue()));
+        assertThat(ReferenceCountUtil.release(response2), is(true));
     }
 
     @Test
@@ -100,6 +107,7 @@ public class CorsHandlerTest {
                 forOrigins("https://localhost:8888").build(), origin);
         assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN), is(nullValue()));
         assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_HEADERS), is(nullValue()));
+        assertThat(ReferenceCountUtil.release(response), is(true));
     }
 
     @Test
@@ -112,6 +120,7 @@ public class CorsHandlerTest {
         assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_METHODS), containsString("GET"));
         assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_METHODS), containsString("DELETE"));
         assertThat(response.headers().get(VARY), equalTo(ORIGIN.toString()));
+        assertThat(ReferenceCountUtil.release(response), is(true));
     }
 
     @Test
@@ -127,6 +136,7 @@ public class CorsHandlerTest {
         assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_HEADERS), containsString("content-type"));
         assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_HEADERS), containsString("xheader1"));
         assertThat(response.headers().get(VARY), equalTo(ORIGIN.toString()));
+        assertThat(ReferenceCountUtil.release(response), is(true));
     }
 
     @Test
@@ -136,6 +146,7 @@ public class CorsHandlerTest {
         assertThat(response.headers().get(CONTENT_LENGTH), is("0"));
         assertThat(response.headers().get(DATE), is(notNullValue()));
         assertThat(response.headers().get(VARY), equalTo(ORIGIN.toString()));
+        assertThat(ReferenceCountUtil.release(response), is(true));
     }
 
     @Test
@@ -147,6 +158,7 @@ public class CorsHandlerTest {
         assertThat(response.headers().get(of("CustomHeader")), equalTo("somevalue"));
         assertThat(response.headers().get(VARY), equalTo(ORIGIN.toString()));
         assertThat(response.headers().get(CONTENT_LENGTH), is("0"));
+        assertThat(ReferenceCountUtil.release(response), is(true));
     }
 
     @Test
@@ -155,6 +167,7 @@ public class CorsHandlerTest {
         final CorsConfig config = forOrigin("http://localhost").build();
         final HttpResponse response = preflightRequest(config, origin, "xheader1");
         assertThat(response.headers().contains(ACCESS_CONTROL_ALLOW_ORIGIN), is(false));
+        assertThat(ReferenceCountUtil.release(response), is(true));
     }
 
     @Test
@@ -168,6 +181,7 @@ public class CorsHandlerTest {
         final HttpResponse response = preflightRequest(config, "http://localhost:8888", "content-type, xheader1");
         assertValues(response, headerName, value1, value2);
         assertThat(response.headers().get(VARY), equalTo(ORIGIN.toString()));
+        assertThat(ReferenceCountUtil.release(response), is(true));
     }
 
     @Test
@@ -181,6 +195,7 @@ public class CorsHandlerTest {
         final HttpResponse response = preflightRequest(config, "http://localhost:8888", "content-type, xheader1");
         assertValues(response, headerName, value1, value2);
         assertThat(response.headers().get(VARY), equalTo(ORIGIN.toString()));
+        assertThat(ReferenceCountUtil.release(response), is(true));
     }
 
     @Test
@@ -195,6 +210,7 @@ public class CorsHandlerTest {
         final HttpResponse response = preflightRequest(config, "http://localhost:8888", "content-type, xheader1");
         assertThat(response.headers().get(of("GenHeader")), equalTo("generatedValue"));
         assertThat(response.headers().get(VARY), equalTo(ORIGIN.toString()));
+        assertThat(ReferenceCountUtil.release(response), is(true));
     }
 
     @Test
@@ -207,6 +223,7 @@ public class CorsHandlerTest {
         final HttpResponse response = preflightRequest(config, origin, "content-type, xheader1");
         assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN), is(equalTo("null")));
         assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_CREDENTIALS), is(equalTo("true")));
+        assertThat(ReferenceCountUtil.release(response), is(true));
     }
 
     @Test
@@ -215,6 +232,7 @@ public class CorsHandlerTest {
         final CorsConfig config = forOrigin(origin).allowCredentials().build();
         final HttpResponse response = preflightRequest(config, origin, "content-type, xheader1");
         assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_CREDENTIALS), is(equalTo("true")));
+        assertThat(ReferenceCountUtil.release(response), is(true));
     }
 
     @Test
@@ -223,6 +241,7 @@ public class CorsHandlerTest {
         final HttpResponse response = preflightRequest(config, "http://localhost:8888", "");
         // the only valid value for Access-Control-Allow-Credentials is true.
         assertThat(response.headers().contains(ACCESS_CONTROL_ALLOW_CREDENTIALS), is(false));
+        assertThat(ReferenceCountUtil.release(response), is(true));
     }
 
     @Test
@@ -232,6 +251,7 @@ public class CorsHandlerTest {
         assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN), equalTo("*"));
         assertThat(response.headers().get(ACCESS_CONTROL_EXPOSE_HEADERS), containsString("custom1"));
         assertThat(response.headers().get(ACCESS_CONTROL_EXPOSE_HEADERS), containsString("custom2"));
+        assertThat(ReferenceCountUtil.release(response), is(true));
     }
 
     @Test
@@ -239,6 +259,7 @@ public class CorsHandlerTest {
         final CorsConfig config = forAnyOrigin().allowCredentials().build();
         final HttpResponse response = simpleRequest(config, "http://localhost:7777");
         assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_CREDENTIALS), equalTo("true"));
+        assertThat(ReferenceCountUtil.release(response), is(true));
     }
 
     @Test
@@ -246,6 +267,7 @@ public class CorsHandlerTest {
         final CorsConfig config = forAnyOrigin().build();
         final HttpResponse response = simpleRequest(config, "http://localhost:7777");
         assertThat(response.headers().contains(ACCESS_CONTROL_ALLOW_CREDENTIALS), is(false));
+        assertThat(ReferenceCountUtil.release(response), is(true));
     }
 
     @Test
@@ -255,6 +277,7 @@ public class CorsHandlerTest {
         assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_CREDENTIALS), equalTo("true"));
         assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN), equalTo("http://localhost:7777"));
         assertThat(response.headers().get(VARY), equalTo(ORIGIN.toString()));
+        assertThat(ReferenceCountUtil.release(response), is(true));
     }
 
     @Test
@@ -263,6 +286,7 @@ public class CorsHandlerTest {
         final HttpResponse response = simpleRequest(config, "http://localhost:7777");
         assertThat(response.headers().get(ACCESS_CONTROL_EXPOSE_HEADERS), containsString("one"));
         assertThat(response.headers().get(ACCESS_CONTROL_EXPOSE_HEADERS), containsString("two"));
+        assertThat(ReferenceCountUtil.release(response), is(true));
     }
 
     @Test
@@ -271,6 +295,7 @@ public class CorsHandlerTest {
         final HttpResponse response = simpleRequest(config, "http://localhost:7777");
         assertThat(response.status(), is(FORBIDDEN));
         assertThat(response.headers().get(CONTENT_LENGTH), is("0"));
+        assertThat(ReferenceCountUtil.release(response), is(true));
     }
 
     @Test
@@ -279,6 +304,7 @@ public class CorsHandlerTest {
         final HttpResponse response = simpleRequest(config, "http://localhost:7777");
         assertThat(response.status(), is(OK));
         assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN), is(nullValue()));
+        assertThat(ReferenceCountUtil.release(response), is(true));
     }
 
     @Test
@@ -287,6 +313,7 @@ public class CorsHandlerTest {
         final HttpResponse response = simpleRequest(config, null);
         assertThat(response.status(), is(OK));
         assertThat(response.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN), is(nullValue()));
+        assertThat(ReferenceCountUtil.release(response), is(true));
     }
 
     @Test
@@ -478,7 +505,9 @@ public class CorsHandlerTest {
             httpRequest.headers().set(ACCESS_CONTROL_REQUEST_HEADERS, requestHeaders);
         }
         assertThat(channel.writeInbound(httpRequest), is(false));
-        return (HttpResponse) channel.readOutbound();
+        HttpResponse response =  channel.readOutbound();
+        assertThat(channel.finish(), is(false));
+        return response;
     }
 
     private static HttpResponse preflightRequest(final CorsConfig config,
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoderTest.java
index f8b756c..40771e0 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoderTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostRequestDecoderTest.java
@@ -24,10 +24,12 @@ import io.netty.handler.codec.http.DefaultFullHttpRequest;
 import io.netty.handler.codec.http.DefaultHttpContent;
 import io.netty.handler.codec.http.DefaultHttpRequest;
 import io.netty.handler.codec.http.DefaultLastHttpContent;
+import io.netty.handler.codec.http.FullHttpRequest;
 import io.netty.handler.codec.http.HttpContent;
 import io.netty.handler.codec.http.HttpHeaderNames;
 import io.netty.handler.codec.http.HttpHeaderValues;
 import io.netty.handler.codec.http.HttpMethod;
+import io.netty.handler.codec.http.HttpRequest;
 import io.netty.handler.codec.http.HttpVersion;
 import io.netty.handler.codec.http.LastHttpContent;
 import io.netty.util.CharsetUtil;
@@ -413,6 +415,39 @@ public class HttpPostRequestDecoderTest {
         decoder.destroy();
     }
 
+    @Test
+    public void testDecodeOtherMimeHeaderFields() throws Exception {
+        final String boundary = "74e78d11b0214bdcbc2f86491eeb4902";
+        String filecontent = "123456";
+
+        final String body = "--" + boundary + "\r\n" +
+                            "Content-Disposition: form-data; name=\"file\"; filename=" + "\"" + "attached.txt" + "\"" +
+                            "\r\n" +
+                            "Content-Type: application/octet-stream" + "\r\n" +
+                            "Content-Encoding: gzip" + "\r\n" +
+                            "\r\n" +
+                            filecontent +
+                            "\r\n" +
+                            "--" + boundary + "--";
+
+        final DefaultFullHttpRequest req = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1,
+                                                                      HttpMethod.POST,
+                                                                      "http://localhost",
+                                                                      Unpooled.wrappedBuffer(body.getBytes()));
+        req.headers().add(HttpHeaderNames.CONTENT_TYPE, "multipart/form-data; boundary=" + boundary);
+        req.headers().add(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED);
+        final DefaultHttpDataFactory inMemoryFactory = new DefaultHttpDataFactory(false);
+        final HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(inMemoryFactory, req);
+        assertFalse(decoder.getBodyHttpDatas().isEmpty());
+        InterfaceHttpData part1 = decoder.getBodyHttpDatas().get(0);
+        assertTrue("the item should be a FileUpload", part1 instanceof FileUpload);
+        FileUpload fileUpload = (FileUpload) part1;
+        byte[] fileBytes = fileUpload.get();
+        assertTrue("the filecontent should not be decoded", filecontent.equals(new String(fileBytes)));
+        decoder.destroy();
+        req.release();
+    }
+
     @Test
     public void testMultipartRequestWithFileInvalidCharset() throws Exception {
         final String boundary = "dLV9Wyq26L_-JQxk6ferf-RT153LhOO";
@@ -656,4 +691,116 @@ public class HttpPostRequestDecoderTest {
         assertEquals("tmp-0.txt", fileUpload.getFilename());
         decoder.destroy();
     }
+
+    // https://github.com/netty/netty/issues/8575
+    @Test
+    public void testMultipartRequest() throws Exception {
+        String BOUNDARY = "01f136d9282f";
+
+        ByteBuf byteBuf = Unpooled.wrappedBuffer(("--" + BOUNDARY + "\n" +
+                "Content-Disposition: form-data; name=\"msg_id\"\n" +
+                "\n" +
+                "15200\n" +
+                "--" + BOUNDARY + "\n" +
+                "Content-Disposition: form-data; name=\"msg\"\n" +
+                "\n" +
+                "test message\n" +
+                "--" + BOUNDARY + "--").getBytes());
+
+        FullHttpRequest req = new DefaultFullHttpRequest(HttpVersion.HTTP_1_0, HttpMethod.POST, "/up", byteBuf);
+        req.headers().add(HttpHeaderNames.CONTENT_TYPE, "multipart/form-data; boundary=" + BOUNDARY);
+
+        HttpPostRequestDecoder decoder =
+                new HttpPostRequestDecoder(new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE),
+                        req,
+                        CharsetUtil.UTF_8);
+
+        assertTrue(decoder.isMultipart());
+        assertFalse(decoder.getBodyHttpDatas().isEmpty());
+        assertEquals(2, decoder.getBodyHttpDatas().size());
+        assertEquals("test message", ((Attribute) decoder.getBodyHttpData("msg")).getValue());
+        assertEquals("15200", ((Attribute) decoder.getBodyHttpData("msg_id")).getValue());
+
+        decoder.destroy();
+        assertEquals(1, req.refCnt());
+    }
+
+    @Test(expected = HttpPostRequestDecoder.ErrorDataDecoderException.class)
+    public void testNotLeak() {
+        FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/",
+                Unpooled.copiedBuffer("a=1&&b=2", CharsetUtil.US_ASCII));
+        try {
+            new HttpPostStandardRequestDecoder(request);
+        } finally {
+            assertTrue(request.release());
+        }
+    }
+
+    @Test(expected = HttpPostRequestDecoder.ErrorDataDecoderException.class)
+    public void testNotLeakDirectBufferWhenWrapIllegalArgumentException() {
+        testNotLeakWhenWrapIllegalArgumentException(Unpooled.directBuffer());
+    }
+
+    @Test(expected = HttpPostRequestDecoder.ErrorDataDecoderException.class)
+    public void testNotLeakHeapBufferWhenWrapIllegalArgumentException() {
+        testNotLeakWhenWrapIllegalArgumentException(Unpooled.buffer());
+    }
+
+    private static void testNotLeakWhenWrapIllegalArgumentException(ByteBuf buf) {
+        buf.writeCharSequence("==", CharsetUtil.US_ASCII);
+        FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/", buf);
+        try {
+            new HttpPostStandardRequestDecoder(request);
+        } finally {
+            assertTrue(request.release());
+        }
+    }
+
+    @Test
+    public void testMultipartFormDataContentType() {
+        HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/");
+        assertFalse(HttpPostRequestDecoder.isMultipart(request));
+
+        String multipartDataValue = HttpHeaderValues.MULTIPART_FORM_DATA + ";" + "boundary=gc0p4Jq0M2Yt08jU534c0p";
+        request.headers().set(HttpHeaderNames.CONTENT_TYPE, ";" + multipartDataValue);
+        assertFalse(HttpPostRequestDecoder.isMultipart(request));
+
+        request.headers().set(HttpHeaderNames.CONTENT_TYPE, multipartDataValue);
+        assertTrue(HttpPostRequestDecoder.isMultipart(request));
+    }
+
+    // see https://github.com/netty/netty/issues/10087
+    @Test
+    public void testDecodeWithLanguageContentDispositionFieldParametersForFix() throws Exception {
+
+        final String boundary = "952178786863262625034234";
+
+        String encoding = "UTF-8";
+        String filename = "测试test.txt";
+        String filenameEncoded = URLEncoder.encode(filename, encoding);
+
+        final String body = "--" + boundary + "\r\n" +
+                "Content-Disposition: form-data; name=\"file\"; filename*=\"" +
+                encoding + "''" + filenameEncoded + "\"\r\n" +
+                "\r\n" +
+                "foo\r\n" +
+                "\r\n" +
+                "--" + boundary + "--";
+
+        final DefaultFullHttpRequest req = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1,
+                HttpMethod.POST,
+                "http://localhost",
+                Unpooled.wrappedBuffer(body.getBytes()));
+
+        req.headers().add(HttpHeaderNames.CONTENT_TYPE, "multipart/form-data; boundary=" + boundary);
+        final DefaultHttpDataFactory inMemoryFactory = new DefaultHttpDataFactory(false);
+        final HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(inMemoryFactory, req);
+        assertFalse(decoder.getBodyHttpDatas().isEmpty());
+        InterfaceHttpData part1 = decoder.getBodyHttpDatas().get(0);
+        assertTrue("the item should be a FileUpload", part1 instanceof FileUpload);
+        FileUpload fileUpload = (FileUpload) part1;
+        assertEquals("the filename should be decoded", filename, fileUpload.getFilename());
+        decoder.destroy();
+        req.release();
+    }
 }
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostRequestEncoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostRequestEncoderTest.java
index 7669f97..389fffa 100755
--- a/codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostRequestEncoderTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostRequestEncoderTest.java
@@ -18,10 +18,12 @@ package io.netty.handler.codec.http.multipart;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.ByteBufAllocator;
 import io.netty.buffer.Unpooled;
+import io.netty.handler.codec.http.DefaultHttpRequest;
 import io.netty.handler.codec.http.DefaultFullHttpRequest;
 import io.netty.handler.codec.http.HttpConstants;
 import io.netty.handler.codec.http.HttpContent;
 import io.netty.handler.codec.http.HttpMethod;
+import io.netty.handler.codec.http.HttpRequest;
 import io.netty.handler.codec.http.HttpVersion;
 import io.netty.handler.codec.http.LastHttpContent;
 import io.netty.handler.codec.http.multipart.HttpPostRequestEncoder.EncoderMode;
@@ -32,7 +34,6 @@ import org.junit.Test;
 
 import java.io.ByteArrayInputStream;
 import java.io.File;
-import java.nio.charset.Charset;
 import java.util.Arrays;
 import java.util.List;
 
@@ -139,9 +140,11 @@ public class HttpPostRequestEncoderTest {
         HttpPostRequestEncoder encoder = new HttpPostRequestEncoder(request, true);
         File file1 = new File(getClass().getResource("/file-01.txt").toURI());
         File file2 = new File(getClass().getResource("/file-02.txt").toURI());
+        File file3 = new File(getClass().getResource("/file-03.txt").toURI());
         encoder.addBodyAttribute("foo", "bar");
         encoder.addBodyFileUpload("quux", file1, "text/plain", false);
         encoder.addBodyFileUpload("quux", file2, "text/plain", false);
+        encoder.addBodyFileUpload("quux", file3, "text/plain", false);
 
         // We have to query the value of these two fields before finalizing
         // the request, which unsets one of them.
@@ -160,7 +163,7 @@ public class HttpPostRequestEncoderTest {
                 CONTENT_TYPE + ": multipart/mixed; boundary=" + multipartMixedBoundary + "\r\n" +
                 "\r\n" +
                 "--" + multipartMixedBoundary + "\r\n" +
-                CONTENT_DISPOSITION + ": attachment; filename=\"file-02.txt\"" + "\r\n" +
+                CONTENT_DISPOSITION + ": attachment; filename=\"file-01.txt\"" + "\r\n" +
                 CONTENT_LENGTH + ": " + file1.length() + "\r\n" +
                 CONTENT_TYPE + ": text/plain" + "\r\n" +
                 CONTENT_TRANSFER_ENCODING + ": binary" + "\r\n" +
@@ -175,6 +178,14 @@ public class HttpPostRequestEncoderTest {
                 "\r\n" +
                 "File 02" + StringUtil.NEWLINE +
                 "\r\n" +
+                "--" + multipartMixedBoundary + "\r\n" +
+                CONTENT_DISPOSITION + ": attachment; filename=\"file-03.txt\"" + "\r\n" +
+                CONTENT_LENGTH + ": " + file3.length() + "\r\n" +
+                CONTENT_TYPE + ": text/plain" + "\r\n" +
+                CONTENT_TRANSFER_ENCODING + ": binary" + "\r\n" +
+                "\r\n" +
+                "File 03" + StringUtil.NEWLINE +
+                "\r\n" +
                 "--" + multipartMixedBoundary + "--" + "\r\n" +
                 "--" + multipartDataBoundary + "--" + "\r\n";
 
@@ -434,4 +445,27 @@ public class HttpPostRequestEncoderTest {
                 + readable, expectedSize);
         httpContent.release();
     }
+
+    @Test
+    public void testEncodeChunkedContent() throws Exception {
+        HttpRequest req = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/");
+        HttpPostRequestEncoder encoder = new HttpPostRequestEncoder(req, false);
+
+        int length = 8077 + 8096;
+        char[] array = new char[length];
+        Arrays.fill(array, 'a');
+        String longText = new String(array);
+
+        encoder.addBodyAttribute("data", longText);
+        encoder.addBodyAttribute("moreData", "abcd");
+
+        assertNotNull(encoder.finalizeRequest());
+
+        while (!encoder.isEndOfInput()) {
+            encoder.readChunk((ByteBufAllocator) null).release();
+        }
+
+        assertTrue(encoder.isEndOfInput());
+        encoder.cleanFiles();
+    }
 }
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocket08EncoderDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocket08EncoderDecoderTest.java
index a84a81a..543867d 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocket08EncoderDecoderTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocket08EncoderDecoderTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2014 The Netty Project
+ * Copyright 2019 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
@@ -53,6 +53,72 @@ public class WebSocket08EncoderDecoderTest {
         strTestData = s.toString();
     }
 
+    @Test
+    public void testWebSocketProtocolViolation() {
+        // Given
+        initTestData();
+
+        int maxPayloadLength = 255;
+        String errorMessage = "Max frame length of " + maxPayloadLength + " has been exceeded.";
+        WebSocketCloseStatus expectedStatus = WebSocketCloseStatus.MESSAGE_TOO_BIG;
+
+        // With auto-close
+        WebSocketDecoderConfig config = WebSocketDecoderConfig.newBuilder()
+            .maxFramePayloadLength(maxPayloadLength)
+            .closeOnProtocolViolation(true)
+            .build();
+        EmbeddedChannel inChannel = new EmbeddedChannel(new WebSocket08FrameDecoder(config));
+        EmbeddedChannel outChannel = new EmbeddedChannel(new WebSocket08FrameEncoder(true));
+
+        executeProtocolViolationTest(outChannel, inChannel, maxPayloadLength + 1, expectedStatus, errorMessage);
+
+        CloseWebSocketFrame response = inChannel.readOutbound();
+        Assert.assertNotNull(response);
+        Assert.assertEquals(expectedStatus.code(), response.statusCode());
+        Assert.assertEquals(errorMessage, response.reasonText());
+        response.release();
+
+        Assert.assertFalse(inChannel.finish());
+        Assert.assertFalse(outChannel.finish());
+
+        // Without auto-close
+        config = WebSocketDecoderConfig.newBuilder()
+            .maxFramePayloadLength(maxPayloadLength)
+            .closeOnProtocolViolation(false)
+            .build();
+        inChannel = new EmbeddedChannel(new WebSocket08FrameDecoder(config));
+        outChannel = new EmbeddedChannel(new WebSocket08FrameEncoder(true));
+
+        executeProtocolViolationTest(outChannel, inChannel, maxPayloadLength + 1, expectedStatus, errorMessage);
+
+        response = inChannel.readOutbound();
+        Assert.assertNull(response);
+
+        Assert.assertFalse(inChannel.finish());
+        Assert.assertFalse(outChannel.finish());
+
+        // Release test data
+        binTestData.release();
+    }
+
+    private void executeProtocolViolationTest(EmbeddedChannel outChannel, EmbeddedChannel inChannel,
+            int testDataLength, WebSocketCloseStatus expectedStatus, String errorMessage) {
+        CorruptedWebSocketFrameException corrupted = null;
+
+        try {
+            testBinaryWithLen(outChannel, inChannel, testDataLength);
+        } catch (CorruptedWebSocketFrameException e) {
+            corrupted = e;
+        }
+
+        BinaryWebSocketFrame exceedingFrame = inChannel.readInbound();
+        Assert.assertNull(exceedingFrame);
+
+        Assert.assertNotNull(corrupted);
+        Assert.assertEquals(expectedStatus, corrupted.closeStatus());
+        Assert.assertEquals(errorMessage, corrupted.getMessage());
+    }
+
     @Test
     public void testWebSocketEncodingAndDecoding() {
         initTestData();
@@ -108,16 +174,7 @@ public class WebSocket08EncoderDecoderTest {
         String testStr = strTestData.substring(0, testDataLength);
         outChannel.writeOutbound(new TextWebSocketFrame(testStr));
 
-        // Transfer encoded data into decoder
-        // Loop because there might be multiple frames (gathering write)
-        while (true) {
-            ByteBuf encoded = outChannel.readOutbound();
-            if (encoded != null) {
-                inChannel.writeInbound(encoded);
-            } else {
-                break;
-            }
-        }
+        transfer(outChannel, inChannel);
 
         Object decoded = inChannel.readInbound();
         Assert.assertNotNull(decoded);
@@ -132,16 +189,7 @@ public class WebSocket08EncoderDecoderTest {
         binTestData.setIndex(0, testDataLength); // Send only len bytes
         outChannel.writeOutbound(new BinaryWebSocketFrame(binTestData));
 
-        // Transfer encoded data into decoder
-        // Loop because there might be multiple frames (gathering write)
-        while (true) {
-            ByteBuf encoded = outChannel.readOutbound();
-            if (encoded != null) {
-                inChannel.writeInbound(encoded);
-            } else {
-                break;
-            }
-        }
+        transfer(outChannel, inChannel);
 
         Object decoded = inChannel.readInbound();
         Assert.assertNotNull(decoded);
@@ -154,4 +202,16 @@ public class WebSocket08EncoderDecoderTest {
         }
         binFrame.release();
     }
+
+    private void transfer(EmbeddedChannel outChannel, EmbeddedChannel inChannel) {
+        // Transfer encoded data into decoder
+        // Loop because there might be multiple frames (gathering write)
+        for (;;) {
+            ByteBuf encoded = outChannel.readOutbound();
+            if (encoded == null) {
+                return;
+            }
+            inChannel.writeInbound(encoded);
+        }
+    }
 }
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker00Test.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker00Test.java
index 33c6ce6..efbe22e 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker00Test.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker00Test.java
@@ -22,8 +22,10 @@ import java.net.URI;
 
 public class WebSocketClientHandshaker00Test extends WebSocketClientHandshakerTest {
     @Override
-    protected WebSocketClientHandshaker newHandshaker(URI uri, String subprotocol, HttpHeaders headers) {
-        return new WebSocketClientHandshaker00(uri, WebSocketVersion.V00, subprotocol, headers, 1024);
+    protected WebSocketClientHandshaker newHandshaker(URI uri, String subprotocol, HttpHeaders headers,
+                                                      boolean absoluteUpgradeUrl) {
+        return new WebSocketClientHandshaker00(uri, WebSocketVersion.V00, subprotocol, headers,
+          1024, 10000, absoluteUpgradeUrl);
     }
 
     @Override
@@ -37,12 +39,11 @@ public class WebSocketClientHandshaker00Test extends WebSocketClientHandshakerTe
     }
 
     @Override
-    protected CharSequence[] getHandshakeHeaderNames() {
+    protected CharSequence[] getHandshakeRequiredHeaderNames() {
         return new CharSequence[] {
                 HttpHeaderNames.CONNECTION,
                 HttpHeaderNames.UPGRADE,
                 HttpHeaderNames.HOST,
-                HttpHeaderNames.ORIGIN,
                 HttpHeaderNames.SEC_WEBSOCKET_KEY1,
                 HttpHeaderNames.SEC_WEBSOCKET_KEY2,
         };
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker07Test.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker07Test.java
index 9ff3e84..dea8b94 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker07Test.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker07Test.java
@@ -15,15 +15,39 @@
  */
 package io.netty.handler.codec.http.websocketx;
 
+import io.netty.handler.codec.http.DefaultHttpHeaders;
+import io.netty.handler.codec.http.FullHttpRequest;
 import io.netty.handler.codec.http.HttpHeaderNames;
 import io.netty.handler.codec.http.HttpHeaders;
+import org.junit.Test;
 
 import java.net.URI;
 
+import static org.junit.Assert.assertEquals;
+
 public class WebSocketClientHandshaker07Test extends WebSocketClientHandshakerTest {
+
+    @Test
+    public void testHostHeaderPreserved() {
+        URI uri = URI.create("ws://localhost:9999");
+        WebSocketClientHandshaker handshaker = newHandshaker(uri, null,
+                new DefaultHttpHeaders().set(HttpHeaderNames.HOST, "test.netty.io"), false);
+
+        FullHttpRequest request = handshaker.newHandshakeRequest();
+        try {
+            assertEquals("/", request.uri());
+            assertEquals("test.netty.io", request.headers().get(HttpHeaderNames.HOST));
+        } finally {
+            request.release();
+        }
+    }
+
     @Override
-    protected WebSocketClientHandshaker newHandshaker(URI uri, String subprotocol, HttpHeaders headers) {
-        return new WebSocketClientHandshaker07(uri, WebSocketVersion.V07, subprotocol, false, headers, 1024);
+    protected WebSocketClientHandshaker newHandshaker(URI uri, String subprotocol, HttpHeaders headers,
+                                                      boolean absoluteUpgradeUrl) {
+        return new WebSocketClientHandshaker07(uri, WebSocketVersion.V07, subprotocol, false, headers,
+          1024, true, false, 10000,
+          absoluteUpgradeUrl);
     }
 
     @Override
@@ -37,13 +61,12 @@ public class WebSocketClientHandshaker07Test extends WebSocketClientHandshakerTe
     }
 
     @Override
-    protected CharSequence[] getHandshakeHeaderNames() {
+    protected CharSequence[] getHandshakeRequiredHeaderNames() {
         return new CharSequence[] {
                 HttpHeaderNames.UPGRADE,
                 HttpHeaderNames.CONNECTION,
                 HttpHeaderNames.SEC_WEBSOCKET_KEY,
                 HttpHeaderNames.HOST,
-                HttpHeaderNames.SEC_WEBSOCKET_ORIGIN,
                 HttpHeaderNames.SEC_WEBSOCKET_VERSION,
         };
     }
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker08Test.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker08Test.java
index 1efb682..79c6dd4 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker08Test.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker08Test.java
@@ -21,7 +21,10 @@ import java.net.URI;
 
 public class WebSocketClientHandshaker08Test extends WebSocketClientHandshaker07Test {
     @Override
-    protected WebSocketClientHandshaker newHandshaker(URI uri, String subprotocol, HttpHeaders headers) {
-        return new WebSocketClientHandshaker08(uri, WebSocketVersion.V08, subprotocol, false, headers, 1024);
+    protected WebSocketClientHandshaker newHandshaker(URI uri, String subprotocol, HttpHeaders headers,
+                                                      boolean absoluteUpgradeUrl) {
+        return new WebSocketClientHandshaker08(uri, WebSocketVersion.V08, subprotocol, false, headers,
+          1024, true, true, 10000,
+          absoluteUpgradeUrl);
     }
 }
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker13Test.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker13Test.java
index 1727178..cdd9bd7 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker13Test.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshaker13Test.java
@@ -15,13 +15,24 @@
  */
 package io.netty.handler.codec.http.websocketx;
 
+import io.netty.handler.codec.http.HttpHeaderNames;
 import io.netty.handler.codec.http.HttpHeaders;
 
 import java.net.URI;
 
 public class WebSocketClientHandshaker13Test extends WebSocketClientHandshaker07Test {
+
     @Override
-    protected WebSocketClientHandshaker newHandshaker(URI uri, String subprotocol, HttpHeaders headers) {
-        return new WebSocketClientHandshaker13(uri, WebSocketVersion.V13, subprotocol, false, headers, 1024);
+    protected WebSocketClientHandshaker newHandshaker(URI uri, String subprotocol, HttpHeaders headers,
+                                                      boolean absoluteUpgradeUrl) {
+        return new WebSocketClientHandshaker13(uri, WebSocketVersion.V13, subprotocol, false, headers,
+          1024, true, true, 10000,
+          absoluteUpgradeUrl);
     }
+
+    @Override
+    protected CharSequence getOriginHeaderName() {
+        return HttpHeaderNames.ORIGIN;
+    }
+
 }
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshakerTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshakerTest.java
index 2054af5..e74a7bf 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshakerTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketClientHandshakerTest.java
@@ -32,7 +32,7 @@ import io.netty.handler.codec.http.HttpObjectAggregator;
 import io.netty.handler.codec.http.HttpRequestEncoder;
 import io.netty.handler.codec.http.HttpResponseDecoder;
 import io.netty.util.CharsetUtil;
-import io.netty.util.internal.PlatformDependent;
+
 import org.junit.Test;
 
 import java.net.URI;
@@ -40,17 +40,18 @@ import java.net.URI;
 import static org.junit.Assert.*;
 
 public abstract class WebSocketClientHandshakerTest {
-    protected abstract WebSocketClientHandshaker newHandshaker(URI uri, String subprotocol, HttpHeaders headers);
+    protected abstract WebSocketClientHandshaker newHandshaker(URI uri, String subprotocol, HttpHeaders headers,
+                                                               boolean absoluteUpgradeUrl);
 
     protected WebSocketClientHandshaker newHandshaker(URI uri) {
-        return newHandshaker(uri, null, null);
+        return newHandshaker(uri, null, null, false);
     }
 
     protected abstract CharSequence getOriginHeaderName();
 
     protected abstract CharSequence getProtocolHeaderName();
 
-    protected abstract CharSequence[] getHandshakeHeaderNames();
+    protected abstract CharSequence[] getHandshakeRequiredHeaderNames();
 
     @Test
     public void hostHeaderWs() {
@@ -160,6 +161,19 @@ public abstract class WebSocketClientHandshakerTest {
         testOriginHeader("//LOCALHOST/", "http://localhost");
     }
 
+    @Test
+    public void testSetOriginFromCustomHeaders() {
+        HttpHeaders customHeaders = new DefaultHttpHeaders().set(getOriginHeaderName(), "http://example.com");
+        WebSocketClientHandshaker handshaker = newHandshaker(URI.create("ws://server.example.com/chat"), null,
+                                                             customHeaders, false);
+        FullHttpRequest request = handshaker.newHandshakeRequest();
+        try {
+            assertEquals("http://example.com", request.headers().get(getOriginHeaderName()));
+        } finally {
+            request.release();
+        }
+    }
+
     private void testHostHeader(String uri, String expected) {
         testHeaderDefaultHttp(uri, HttpHeaderNames.HOST, expected);
     }
@@ -180,7 +194,7 @@ public abstract class WebSocketClientHandshakerTest {
 
     @Test
     @SuppressWarnings("deprecation")
-    public void testRawPath() {
+    public void testUpgradeUrl() {
         URI uri = URI.create("ws://localhost:9999/path%20with%20ws");
         WebSocketClientHandshaker handshaker = newHandshaker(uri);
         FullHttpRequest request = handshaker.newHandshakeRequest();
@@ -192,7 +206,7 @@ public abstract class WebSocketClientHandshakerTest {
     }
 
     @Test
-    public void testRawPathWithQuery() {
+    public void testUpgradeUrlWithQuery() {
         URI uri = URI.create("ws://localhost:9999/path%20with%20ws?a=b%20c");
         WebSocketClientHandshaker handshaker = newHandshaker(uri);
         FullHttpRequest request = handshaker.newHandshakeRequest();
@@ -203,6 +217,42 @@ public abstract class WebSocketClientHandshakerTest {
         }
     }
 
+    @Test
+    public void testUpgradeUrlWithoutPath() {
+        URI uri = URI.create("ws://localhost:9999");
+        WebSocketClientHandshaker handshaker = newHandshaker(uri);
+        FullHttpRequest request = handshaker.newHandshakeRequest();
+        try {
+            assertEquals("/", request.uri());
+        } finally {
+            request.release();
+        }
+    }
+
+    @Test
+    public void testUpgradeUrlWithoutPathWithQuery() {
+        URI uri = URI.create("ws://localhost:9999?a=b%20c");
+        WebSocketClientHandshaker handshaker = newHandshaker(uri);
+        FullHttpRequest request = handshaker.newHandshakeRequest();
+        try {
+            assertEquals("/?a=b%20c", request.uri());
+        } finally {
+            request.release();
+        }
+    }
+
+    @Test
+    public void testAbsoluteUpgradeUrlWithQuery() {
+        URI uri = URI.create("ws://localhost:9999/path%20with%20ws?a=b%20c");
+        WebSocketClientHandshaker handshaker = newHandshaker(uri, null, null, true);
+        FullHttpRequest request = handshaker.newHandshakeRequest();
+        try {
+            assertEquals("ws://localhost:9999/path%20with%20ws?a=b%20c", request.uri());
+        } finally {
+            request.release();
+        }
+    }
+
     @Test(timeout = 3000)
     public void testHttpResponseAndFrameInSameBuffer() {
         testHttpResponseAndFrameInSameBuffer(false);
@@ -217,7 +267,7 @@ public abstract class WebSocketClientHandshakerTest {
         String url = "ws://localhost:9999/ws";
         final WebSocketClientHandshaker shaker = newHandshaker(URI.create(url));
         final WebSocketClientHandshaker handshaker = new WebSocketClientHandshaker(
-                shaker.uri(), shaker.version(), null, EmptyHttpHeaders.INSTANCE, Integer.MAX_VALUE) {
+                shaker.uri(), shaker.version(), null, EmptyHttpHeaders.INSTANCE, Integer.MAX_VALUE, -1) {
             @Override
             protected FullHttpRequest newHandshakeRequest() {
                 return shaker.newHandshakeRequest();
@@ -246,7 +296,9 @@ public abstract class WebSocketClientHandshakerTest {
         // Create a EmbeddedChannel which we will use to encode a BinaryWebsocketFrame to bytes and so use these
         // to test the actual handshaker.
         WebSocketServerHandshakerFactory factory = new WebSocketServerHandshakerFactory(url, null, false);
-        WebSocketServerHandshaker socketServerHandshaker = factory.newHandshaker(shaker.newHandshakeRequest());
+        FullHttpRequest request = shaker.newHandshakeRequest();
+        WebSocketServerHandshaker socketServerHandshaker = factory.newHandshaker(request);
+        request.release();
         EmbeddedChannel websocketChannel = new EmbeddedChannel(socketServerHandshaker.newWebSocketEncoder(),
                 socketServerHandshaker.newWebsocketDecoder());
         assertTrue(websocketChannel.writeOutbound(new BinaryWebSocketFrame(Unpooled.wrappedBuffer(data))));
@@ -311,18 +363,20 @@ public abstract class WebSocketClientHandshakerTest {
         String bogusHeaderValue = "bogusHeaderValue";
 
         // add values for the headers that are reserved for use in the websockets handshake
-        for (CharSequence header : getHandshakeHeaderNames()) {
-            inputHeaders.add(header, bogusHeaderValue);
+        for (CharSequence header : getHandshakeRequiredHeaderNames()) {
+            if (!HttpHeaderNames.HOST.equals(header)) {
+                inputHeaders.add(header, bogusHeaderValue);
+            }
         }
         inputHeaders.add(getProtocolHeaderName(), bogusSubProtocol);
 
         String realSubProtocol = "realSubProtocol";
-        WebSocketClientHandshaker handshaker = newHandshaker(uri, realSubProtocol, inputHeaders);
+        WebSocketClientHandshaker handshaker = newHandshaker(uri, realSubProtocol, inputHeaders, false);
         FullHttpRequest request = handshaker.newHandshakeRequest();
         HttpHeaders outputHeaders = request.headers();
 
         // the header values passed in originally have been replaced with values generated by the Handshaker
-        for (CharSequence header : getHandshakeHeaderNames()) {
+        for (CharSequence header : getHandshakeRequiredHeaderNames()) {
             assertEquals(1, outputHeaders.getAll(header).size());
             assertNotEquals(bogusHeaderValue, outputHeaders.get(header));
         }
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketCloseStatusTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketCloseStatusTest.java
new file mode 100644
index 0000000..05cb07d
--- /dev/null
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketCloseStatusTest.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2019 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.http.websocketx;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import org.hamcrest.Matchers;
+import org.junit.Assert;
+import org.junit.Test;
+
+import static io.netty.handler.codec.http.websocketx.WebSocketCloseStatus.*;
+
+public class WebSocketCloseStatusTest {
+
+    private final List<WebSocketCloseStatus> validCodes = Arrays.asList(
+        NORMAL_CLOSURE,
+        ENDPOINT_UNAVAILABLE,
+        PROTOCOL_ERROR,
+        INVALID_MESSAGE_TYPE,
+        INVALID_PAYLOAD_DATA,
+        POLICY_VIOLATION,
+        MESSAGE_TOO_BIG,
+        MANDATORY_EXTENSION,
+        INTERNAL_SERVER_ERROR,
+        SERVICE_RESTART,
+        TRY_AGAIN_LATER,
+        BAD_GATEWAY
+    );
+
+    @Test
+    public void testToString() {
+        Assert.assertEquals("1000 Bye", NORMAL_CLOSURE.toString());
+    }
+
+    @Test
+    public void testKnownStatuses() {
+        Assert.assertSame(NORMAL_CLOSURE, valueOf(1000));
+        Assert.assertSame(ENDPOINT_UNAVAILABLE, valueOf(1001));
+        Assert.assertSame(PROTOCOL_ERROR, valueOf(1002));
+        Assert.assertSame(INVALID_MESSAGE_TYPE, valueOf(1003));
+        Assert.assertSame(INVALID_PAYLOAD_DATA, valueOf(1007));
+        Assert.assertSame(POLICY_VIOLATION, valueOf(1008));
+        Assert.assertSame(MESSAGE_TOO_BIG, valueOf(1009));
+        Assert.assertSame(MANDATORY_EXTENSION, valueOf(1010));
+        Assert.assertSame(INTERNAL_SERVER_ERROR, valueOf(1011));
+        Assert.assertSame(SERVICE_RESTART, valueOf(1012));
+        Assert.assertSame(TRY_AGAIN_LATER, valueOf(1013));
+        Assert.assertSame(BAD_GATEWAY, valueOf(1014));
+    }
+
+    @Test
+    public void testNaturalOrder() {
+        Assert.assertThat(PROTOCOL_ERROR, Matchers.greaterThan(NORMAL_CLOSURE));
+        Assert.assertThat(PROTOCOL_ERROR, Matchers.greaterThan(valueOf(1001)));
+        Assert.assertThat(PROTOCOL_ERROR, Matchers.comparesEqualTo(PROTOCOL_ERROR));
+        Assert.assertThat(PROTOCOL_ERROR, Matchers.comparesEqualTo(valueOf(1002)));
+        Assert.assertThat(PROTOCOL_ERROR, Matchers.lessThan(INVALID_MESSAGE_TYPE));
+        Assert.assertThat(PROTOCOL_ERROR, Matchers.lessThan(valueOf(1007)));
+    }
+
+    @Test
+    public void testUserDefinedStatuses() {
+        // Given, when
+        WebSocketCloseStatus feedTimeot = new WebSocketCloseStatus(6033, "Feed timed out");
+        WebSocketCloseStatus untradablePrice = new WebSocketCloseStatus(6034, "Untradable price");
+
+        // Then
+        Assert.assertNotSame(feedTimeot, valueOf(6033));
+        Assert.assertEquals(feedTimeot.code(), 6033);
+        Assert.assertEquals(feedTimeot.reasonText(), "Feed timed out");
+
+        Assert.assertNotSame(untradablePrice, valueOf(6034));
+        Assert.assertEquals(untradablePrice.code(), 6034);
+        Assert.assertEquals(untradablePrice.reasonText(), "Untradable price");
+    }
+
+    @Test
+    public void testRfc6455CodeValidation() {
+        // Given
+        List<Integer> knownCodes = Arrays.asList(
+            NORMAL_CLOSURE.code(),
+            ENDPOINT_UNAVAILABLE.code(),
+            PROTOCOL_ERROR.code(),
+            INVALID_MESSAGE_TYPE.code(),
+            INVALID_PAYLOAD_DATA.code(),
+            POLICY_VIOLATION.code(),
+            MESSAGE_TOO_BIG.code(),
+            MANDATORY_EXTENSION.code(),
+            INTERNAL_SERVER_ERROR.code(),
+            SERVICE_RESTART.code(),
+            TRY_AGAIN_LATER.code(),
+            BAD_GATEWAY.code()
+        );
+
+        SortedSet<Integer> invalidCodes = new TreeSet<Integer>();
+
+        // When
+        for (int statusCode = Short.MIN_VALUE; statusCode < Short.MAX_VALUE; statusCode++) {
+            if (!isValidStatusCode(statusCode)) {
+                invalidCodes.add(statusCode);
+            }
+        }
+
+        // Then
+        Assert.assertEquals(0, invalidCodes.first().intValue());
+        Assert.assertEquals(2999, invalidCodes.last().intValue());
+        Assert.assertEquals(3000 - validCodes.size(), invalidCodes.size());
+
+        invalidCodes.retainAll(knownCodes);
+        Assert.assertEquals(invalidCodes, Collections.emptySet());
+    }
+
+}
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketHandshakeHandOverTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketHandshakeHandOverTest.java
index 10ad774..0245078 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketHandshakeHandOverTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketHandshakeHandOverTest.java
@@ -17,10 +17,13 @@ package io.netty.handler.codec.http.websocketx;
 
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelFutureListener;
 import io.netty.channel.ChannelHandler;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.SimpleChannelInboundHandler;
 import io.netty.channel.embedded.EmbeddedChannel;
+import io.netty.handler.codec.http.EmptyHttpHeaders;
 import io.netty.handler.codec.http.HttpClientCodec;
 import io.netty.handler.codec.http.HttpObjectAggregator;
 import io.netty.handler.codec.http.HttpServerCodec;
@@ -30,6 +33,7 @@ import org.junit.Before;
 import org.junit.Test;
 
 import java.net.URI;
+import java.util.List;
 
 import static org.junit.Assert.*;
 
@@ -39,6 +43,28 @@ public class WebSocketHandshakeHandOverTest {
     private WebSocketServerProtocolHandler.HandshakeComplete serverHandshakeComplete;
     private boolean clientReceivedHandshake;
     private boolean clientReceivedMessage;
+    private boolean serverReceivedCloseHandshake;
+    private boolean clientForceClosed;
+    private boolean clientHandshakeTimeout;
+
+    private final class CloseNoOpServerProtocolHandler extends WebSocketServerProtocolHandler {
+        CloseNoOpServerProtocolHandler(String websocketPath) {
+            super(WebSocketServerProtocolConfig.newBuilder()
+                .websocketPath(websocketPath)
+                .allowExtensions(false)
+                .sendCloseFrame(null)
+                .build());
+        }
+
+        @Override
+        protected void decode(ChannelHandlerContext ctx, WebSocketFrame frame, List<Object> out) throws Exception {
+            if (frame instanceof CloseWebSocketFrame) {
+                serverReceivedCloseHandshake = true;
+                return;
+            }
+            super.decode(ctx, frame, out);
+        }
+    }
 
     @Before
     public void setUp() {
@@ -46,6 +72,9 @@ public class WebSocketHandshakeHandOverTest {
         serverHandshakeComplete = null;
         clientReceivedHandshake = false;
         clientReceivedMessage = false;
+        serverReceivedCloseHandshake = false;
+        clientForceClosed = false;
+        clientHandshakeTimeout = false;
     }
 
     @Test
@@ -95,6 +124,124 @@ public class WebSocketHandshakeHandOverTest {
         assertTrue(clientReceivedMessage);
     }
 
+    @Test(expected = WebSocketHandshakeException.class)
+    public void testClientHandshakeTimeout() throws Exception {
+        EmbeddedChannel serverChannel = createServerChannel(new SimpleChannelInboundHandler<Object>() {
+            @Override
+            public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
+                if (evt == ServerHandshakeStateEvent.HANDSHAKE_COMPLETE) {
+                    serverReceivedHandshake = true;
+                    // immediately send a message to the client on connect
+                    ctx.writeAndFlush(new TextWebSocketFrame("abc"));
+                } else if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) {
+                    serverHandshakeComplete = (WebSocketServerProtocolHandler.HandshakeComplete) evt;
+                }
+            }
+
+            @Override
+            protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
+            }
+        });
+
+        EmbeddedChannel clientChannel = createClientChannel(new SimpleChannelInboundHandler<Object>() {
+            @Override
+            public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
+                if (evt == ClientHandshakeStateEvent.HANDSHAKE_COMPLETE) {
+                    clientReceivedHandshake = true;
+                } else if (evt == ClientHandshakeStateEvent.HANDSHAKE_TIMEOUT) {
+                    clientHandshakeTimeout = true;
+                }
+            }
+
+            @Override
+            protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
+                if (msg instanceof TextWebSocketFrame) {
+                    clientReceivedMessage = true;
+                }
+            }
+        }, 100);
+        // Client send the handshake request to server
+        transferAllDataWithMerge(clientChannel, serverChannel);
+        // Server do not send the response back
+        // transferAllDataWithMerge(serverChannel, clientChannel);
+        WebSocketClientProtocolHandshakeHandler handshakeHandler =
+                (WebSocketClientProtocolHandshakeHandler) clientChannel
+                        .pipeline().get(WebSocketClientProtocolHandshakeHandler.class.getName());
+
+        while (!handshakeHandler.getHandshakeFuture().isDone()) {
+            Thread.sleep(10);
+            // We need to run all pending tasks as the handshake timeout is scheduled on the EventLoop.
+            clientChannel.runScheduledPendingTasks();
+        }
+        assertTrue(clientHandshakeTimeout);
+        assertFalse(clientReceivedHandshake);
+        assertFalse(clientReceivedMessage);
+        // Should throw WebSocketHandshakeException
+        try {
+            handshakeHandler.getHandshakeFuture().syncUninterruptibly();
+        } finally {
+            serverChannel.finishAndReleaseAll();
+        }
+    }
+
+    @Test(timeout = 10000)
+    public void testClientHandshakerForceClose() throws Exception {
+        final WebSocketClientHandshaker handshaker = WebSocketClientHandshakerFactory.newHandshaker(
+                new URI("ws://localhost:1234/test"), WebSocketVersion.V13, null, true,
+                EmptyHttpHeaders.INSTANCE, Integer.MAX_VALUE, true, false, 20);
+
+        EmbeddedChannel serverChannel = createServerChannel(
+                new CloseNoOpServerProtocolHandler("/test"),
+                new SimpleChannelInboundHandler<Object>() {
+                    @Override
+                    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
+                    }
+                });
+
+        EmbeddedChannel clientChannel = createClientChannel(handshaker, new SimpleChannelInboundHandler<Object>() {
+            @Override
+            public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
+                if (evt == ClientHandshakeStateEvent.HANDSHAKE_COMPLETE) {
+                    ctx.channel().closeFuture().addListener(new ChannelFutureListener() {
+                        @Override
+                        public void operationComplete(ChannelFuture future) throws Exception {
+                            clientForceClosed = true;
+                        }
+                    });
+                    handshaker.close(ctx.channel(), new CloseWebSocketFrame());
+                }
+            }
+            @Override
+            protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
+            }
+        });
+
+        // Transfer the handshake from the client to the server
+        transferAllDataWithMerge(clientChannel, serverChannel);
+        // Transfer the handshake from the server to client
+        transferAllDataWithMerge(serverChannel, clientChannel);
+
+        // Transfer closing handshake
+        transferAllDataWithMerge(clientChannel, serverChannel);
+        assertTrue(serverReceivedCloseHandshake);
+        // Should not be closed yet as we disabled closing the connection on the server
+        assertFalse(clientForceClosed);
+
+        while (!clientForceClosed) {
+            Thread.sleep(10);
+            // We need to run all pending tasks as the force close timeout is scheduled on the EventLoop.
+            clientChannel.runPendingTasks();
+        }
+
+        // clientForceClosed would be set to TRUE after any close,
+        // so check here that force close timeout was actually fired
+        assertTrue(handshaker.isForceCloseComplete());
+
+        // Both should be empty
+        assertFalse(serverChannel.finishAndReleaseAll());
+        assertFalse(clientChannel.finishAndReleaseAll());
+    }
+
     /**
      * Transfers all pending data from the source channel into the destination channel.<br>
      * Merges all data into a single buffer before transmission into the destination.
@@ -128,12 +275,35 @@ public class WebSocketHandshakeHandOverTest {
     }
 
     private static EmbeddedChannel createClientChannel(ChannelHandler handler) throws Exception {
+        return createClientChannel(handler, WebSocketClientProtocolConfig.newBuilder()
+            .webSocketUri("ws://localhost:1234/test")
+            .subprotocol("test-proto-2")
+            .build());
+    }
+
+    private static EmbeddedChannel createClientChannel(ChannelHandler handler, long timeoutMillis) throws Exception {
+        return createClientChannel(handler, WebSocketClientProtocolConfig.newBuilder()
+            .webSocketUri("ws://localhost:1234/test")
+            .subprotocol("test-proto-2")
+            .handshakeTimeoutMillis(timeoutMillis)
+            .build());
+    }
+
+    private static EmbeddedChannel createClientChannel(ChannelHandler handler, WebSocketClientProtocolConfig config) {
+        return new EmbeddedChannel(
+                new HttpClientCodec(),
+                new HttpObjectAggregator(8192),
+                new WebSocketClientProtocolHandler(config),
+                handler);
+    }
+
+    private static EmbeddedChannel createClientChannel(WebSocketClientHandshaker handshaker,
+                                                       ChannelHandler handler) throws Exception {
         return new EmbeddedChannel(
                 new HttpClientCodec(),
                 new HttpObjectAggregator(8192),
-                new WebSocketClientProtocolHandler(new URI("ws://localhost:1234/test"),
-                                                   WebSocketVersion.V13, "test-proto-2",
-                                                   false, null, 65536),
+                // Note that we're switching off close frames handling on purpose to test forced close on timeout.
+                new WebSocketClientProtocolHandler(handshaker, false, false),
                 handler);
     }
 
@@ -144,4 +314,13 @@ public class WebSocketHandshakeHandOverTest {
                 new WebSocketServerProtocolHandler("/test", "test-proto-1, test-proto-2", false),
                 handler);
     }
+
+    private static EmbeddedChannel createServerChannel(WebSocketServerProtocolHandler webSocketHandler,
+                                                       ChannelHandler handler) {
+        return new EmbeddedChannel(
+                new HttpServerCodec(),
+                new HttpObjectAggregator(8192),
+                webSocketHandler,
+                handler);
+    }
 }
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketProtocolHandlerTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketProtocolHandlerTest.java
index af74982..b59cc1e 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketProtocolHandlerTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketProtocolHandlerTest.java
@@ -19,9 +19,10 @@ package io.netty.handler.codec.http.websocketx;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
 import io.netty.channel.embedded.EmbeddedChannel;
-import io.netty.util.CharsetUtil;
+import io.netty.handler.flow.FlowControlHandler;
 import org.junit.Test;
 
+import static io.netty.util.CharsetUtil.UTF_8;
 import static org.junit.Assert.*;
 
 /**
@@ -31,7 +32,7 @@ public class WebSocketProtocolHandlerTest {
 
     @Test
     public void testPingFrame() {
-        ByteBuf pingData = Unpooled.copiedBuffer("Hello, world", CharsetUtil.UTF_8);
+        ByteBuf pingData = Unpooled.copiedBuffer("Hello, world", UTF_8);
         EmbeddedChannel channel = new EmbeddedChannel(new WebSocketProtocolHandler() { });
 
         PingWebSocketFrame inputMessage = new PingWebSocketFrame(pingData);
@@ -45,6 +46,65 @@ public class WebSocketProtocolHandlerTest {
         assertFalse(channel.finish());
     }
 
+    @Test
+    public void testPingPongFlowControlWhenAutoReadIsDisabled() {
+        String text1 = "Hello, world #1";
+        String text2 = "Hello, world #2";
+        String text3 = "Hello, world #3";
+        String text4 = "Hello, world #4";
+
+        EmbeddedChannel channel = new EmbeddedChannel();
+        channel.config().setAutoRead(false);
+        channel.pipeline().addLast(new FlowControlHandler());
+        channel.pipeline().addLast(new WebSocketProtocolHandler() { });
+
+        // When
+        assertFalse(channel.writeInbound(
+            new PingWebSocketFrame(Unpooled.copiedBuffer(text1, UTF_8)),
+            new TextWebSocketFrame(text2),
+            new TextWebSocketFrame(text3),
+            new PingWebSocketFrame(Unpooled.copiedBuffer(text4, UTF_8))
+        ));
+
+        // Then - no messages were handled or propagated
+        assertNull(channel.readInbound());
+        assertNull(channel.readOutbound());
+
+        // When
+        channel.read();
+
+        // Then - pong frame was written to the outbound
+        PongWebSocketFrame response1 = channel.readOutbound();
+        assertEquals(text1, response1.content().toString(UTF_8));
+
+        // And - one requested message was handled and propagated inbound
+        TextWebSocketFrame message2 = channel.readInbound();
+        assertEquals(text2, message2.text());
+
+        // And - no more messages were handled or propagated
+        assertNull(channel.readInbound());
+        assertNull(channel.readOutbound());
+
+        // When
+        channel.read();
+
+        // Then - one requested message was handled and propagated inbound
+        TextWebSocketFrame message3 = channel.readInbound();
+        assertEquals(text3, message3.text());
+
+        // And - no more messages were handled or propagated
+        // Precisely, ping frame 'text4' was NOT read or handled.
+        // It would be handle ONLY on the next 'channel.read()' call.
+        assertNull(channel.readInbound());
+        assertNull(channel.readOutbound());
+
+        // Cleanup
+        response1.release();
+        message2.release();
+        message3.release();
+        assertFalse(channel.finish());
+    }
+
     @Test
     public void testPongFrameDropFrameFalse() {
         EmbeddedChannel channel = new EmbeddedChannel(new WebSocketProtocolHandler(false) { });
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketRequestBuilder.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketRequestBuilder.java
index fd199b8..65ef489 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketRequestBuilder.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketRequestBuilder.java
@@ -138,7 +138,11 @@ public class WebSocketRequestBuilder {
             headers.set(HttpHeaderNames.SEC_WEBSOCKET_KEY, key);
         }
         if (origin != null) {
-            headers.set(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN, origin);
+            if (version == WebSocketVersion.V13 || version == WebSocketVersion.V00) {
+                headers.set(HttpHeaderNames.ORIGIN, origin);
+            } else {
+                headers.set(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN, origin);
+            }
         }
         if (version != null) {
             headers.set(HttpHeaderNames.SEC_WEBSOCKET_VERSION, version.toHttpHeaderValue());
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker00Test.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker00Test.java
index 76826ab..8783e0b 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker00Test.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker00Test.java
@@ -32,7 +32,9 @@ import io.netty.util.CharsetUtil;
 import org.junit.Assert;
 import org.junit.Test;
 
-import static io.netty.handler.codec.http.HttpVersion.*;
+import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
 
 public class WebSocketServerHandshaker00Test {
 
@@ -46,6 +48,34 @@ public class WebSocketServerHandshaker00Test {
         testPerformOpeningHandshake0(false);
     }
 
+    @Test
+    public void testPerformHandshakeWithoutOriginHeader() {
+        EmbeddedChannel ch = new EmbeddedChannel(
+            new HttpObjectAggregator(42), new HttpRequestDecoder(), new HttpResponseEncoder());
+
+        FullHttpRequest req = new DefaultFullHttpRequest(
+            HTTP_1_1, HttpMethod.GET, "/chat", Unpooled.copiedBuffer("^n:ds[4U", CharsetUtil.US_ASCII));
+
+        req.headers().set(HttpHeaderNames.HOST, "server.example.com");
+        req.headers().set(HttpHeaderNames.UPGRADE, HttpHeaderValues.WEBSOCKET);
+        req.headers().set(HttpHeaderNames.CONNECTION, "Upgrade");
+        req.headers().set(HttpHeaderNames.SEC_WEBSOCKET_KEY1, "4 @1  46546xW%0l 1 5");
+        req.headers().set(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL, "chat, superchat");
+
+        WebSocketServerHandshaker00 handshaker00 = new WebSocketServerHandshaker00(
+            "ws://example.com/chat", "chat", Integer.MAX_VALUE);
+        try {
+            handshaker00.handshake(ch, req);
+            fail("Expecting WebSocketHandshakeException");
+        } catch (WebSocketHandshakeException e) {
+            assertEquals("Missing origin header, got only "
+                    + "[host, upgrade, connection, sec-websocket-key1, sec-websocket-protocol]",
+                e.getMessage());
+        } finally {
+            req.release();
+        }
+    }
+
     private static void testPerformOpeningHandshake0(boolean subProtocol) {
         EmbeddedChannel ch = new EmbeddedChannel(
                 new HttpObjectAggregator(42), new HttpRequestDecoder(), new HttpResponseEncoder());
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker13Test.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker13Test.java
index b66851e..25340df 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker13Test.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketServerHandshaker13Test.java
@@ -16,6 +16,8 @@
 package io.netty.handler.codec.http.websocketx;
 
 import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.ChannelHandler;
 import io.netty.channel.embedded.EmbeddedChannel;
 import io.netty.handler.codec.http.DefaultFullHttpRequest;
 import io.netty.handler.codec.http.FullHttpRequest;
@@ -27,10 +29,15 @@ import io.netty.handler.codec.http.HttpRequestDecoder;
 import io.netty.handler.codec.http.HttpResponse;
 import io.netty.handler.codec.http.HttpResponseDecoder;
 import io.netty.handler.codec.http.HttpResponseEncoder;
+import io.netty.handler.codec.http.HttpServerCodec;
 import io.netty.util.ReferenceCountUtil;
+import io.netty.util.ReferenceCounted;
+import org.hamcrest.CoreMatchers;
 import org.junit.Assert;
 import org.junit.Test;
 
+import java.util.Iterator;
+
 import static io.netty.handler.codec.http.HttpVersion.*;
 
 public class WebSocketServerHandshaker13Test {
@@ -47,8 +54,50 @@ public class WebSocketServerHandshaker13Test {
 
     private static void testPerformOpeningHandshake0(boolean subProtocol) {
         EmbeddedChannel ch = new EmbeddedChannel(
-                new HttpObjectAggregator(42), new HttpRequestDecoder(), new HttpResponseEncoder());
+                new HttpObjectAggregator(42), new HttpResponseEncoder(), new HttpRequestDecoder());
+
+        if (subProtocol) {
+            testUpgrade0(ch, new WebSocketServerHandshaker13(
+                    "ws://example.com/chat", "chat", false, Integer.MAX_VALUE, false));
+        } else {
+            testUpgrade0(ch, new WebSocketServerHandshaker13(
+                    "ws://example.com/chat", null, false, Integer.MAX_VALUE, false));
+        }
+        Assert.assertFalse(ch.finish());
+    }
 
+    @Test
+    public void testCloseReasonWithEncoderAndDecoder() {
+        testCloseReason0(new HttpResponseEncoder(), new HttpRequestDecoder());
+    }
+
+    @Test
+    public void testCloseReasonWithCodec() {
+        testCloseReason0(new HttpServerCodec());
+    }
+
+    private static void testCloseReason0(ChannelHandler... handlers) {
+        EmbeddedChannel ch = new EmbeddedChannel(
+                new HttpObjectAggregator(42));
+        ch.pipeline().addLast(handlers);
+        testUpgrade0(ch, new WebSocketServerHandshaker13("ws://example.com/chat", "chat",
+                WebSocketDecoderConfig.newBuilder().maxFramePayloadLength(4).closeOnProtocolViolation(true).build()));
+
+        ch.writeOutbound(new BinaryWebSocketFrame(Unpooled.wrappedBuffer(new byte[8])));
+        ByteBuf buffer = ch.readOutbound();
+        try {
+            ch.writeInbound(buffer);
+            Assert.fail();
+        } catch (CorruptedWebSocketFrameException expected) {
+            // expected
+        }
+        ReferenceCounted closeMessage = ch.readOutbound();
+        Assert.assertThat(closeMessage, CoreMatchers.instanceOf(ByteBuf.class));
+        closeMessage.release();
+        Assert.assertFalse(ch.finish());
+    }
+
+    private static void testUpgrade0(EmbeddedChannel ch, WebSocketServerHandshaker13 handshaker) {
         FullHttpRequest req = new DefaultFullHttpRequest(HTTP_1_1, HttpMethod.GET, "/chat");
         req.headers().set(HttpHeaderNames.HOST, "server.example.com");
         req.headers().set(HttpHeaderNames.UPGRADE, HttpHeaderValues.WEBSOCKET);
@@ -58,13 +107,7 @@ public class WebSocketServerHandshaker13Test {
         req.headers().set(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL, "chat, superchat");
         req.headers().set(HttpHeaderNames.SEC_WEBSOCKET_VERSION, "13");
 
-        if (subProtocol) {
-            new WebSocketServerHandshaker13(
-                    "ws://example.com/chat", "chat", false, Integer.MAX_VALUE, false).handshake(ch, req);
-        } else {
-            new WebSocketServerHandshaker13(
-                    "ws://example.com/chat", null, false, Integer.MAX_VALUE, false).handshake(ch, req);
-        }
+        handshaker.handshake(ch, req);
 
         ByteBuf resBuf = ch.readOutbound();
 
@@ -74,8 +117,10 @@ public class WebSocketServerHandshaker13Test {
 
         Assert.assertEquals(
                 "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=", res.headers().get(HttpHeaderNames.SEC_WEBSOCKET_ACCEPT));
-        if (subProtocol) {
-            Assert.assertEquals("chat", res.headers().get(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL));
+        Iterator<String> subProtocols = handshaker.subprotocols().iterator();
+        if (subProtocols.hasNext()) {
+            Assert.assertEquals(subProtocols.next(),
+                    res.headers().get(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL));
         } else {
             Assert.assertNull(res.headers().get(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL));
         }
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolHandlerTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolHandlerTest.java
index 50bd6be..69cda51 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolHandlerTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketServerProtocolHandlerTest.java
@@ -15,6 +15,7 @@
  */
 package io.netty.handler.codec.http.websocketx;
 
+import io.netty.buffer.ByteBuf;
 import io.netty.channel.ChannelHandler;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelInboundHandlerAdapter;
@@ -24,11 +25,15 @@ import io.netty.channel.embedded.EmbeddedChannel;
 import io.netty.handler.codec.http.DefaultFullHttpRequest;
 import io.netty.handler.codec.http.FullHttpRequest;
 import io.netty.handler.codec.http.FullHttpResponse;
+import io.netty.handler.codec.http.HttpClientCodec;
 import io.netty.handler.codec.http.HttpHeaderValues;
 import io.netty.handler.codec.http.HttpMethod;
+import io.netty.handler.codec.http.HttpObjectAggregator;
 import io.netty.handler.codec.http.HttpRequest;
 import io.netty.handler.codec.http.HttpRequestDecoder;
 import io.netty.handler.codec.http.HttpResponseEncoder;
+import io.netty.handler.codec.http.HttpServerCodec;
+import io.netty.util.CharsetUtil;
 import io.netty.util.ReferenceCountUtil;
 import org.junit.Before;
 import org.junit.Test;
@@ -50,7 +55,7 @@ public class WebSocketServerProtocolHandlerTest {
     }
 
     @Test
-    public void testHttpUpgradeRequest() throws Exception {
+    public void testHttpUpgradeRequest() {
         EmbeddedChannel ch = createChannel(new MockOutboundHandler());
         ChannelHandlerContext handshakerCtx = ch.pipeline().context(WebSocketServerProtocolHandshakeHandler.class);
         writeUpgradeRequest(ch);
@@ -59,22 +64,29 @@ public class WebSocketServerProtocolHandlerTest {
         assertEquals(SWITCHING_PROTOCOLS, response.status());
         response.release();
         assertNotNull(WebSocketServerProtocolHandler.getHandshaker(handshakerCtx.channel()));
+        assertFalse(ch.finish());
     }
 
     @Test
-    public void testSubsequentHttpRequestsAfterUpgradeShouldReturn403() throws Exception {
-        EmbeddedChannel ch = createChannel();
-
+    public void testWebSocketServerProtocolHandshakeHandlerReplacedBeforeHandshake() {
+        EmbeddedChannel ch = createChannel(new MockOutboundHandler());
+        ChannelHandlerContext handshakerCtx = ch.pipeline().context(WebSocketServerProtocolHandshakeHandler.class);
+        ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
+            @Override
+            public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
+                if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) {
+                    // We should have removed the handler already.
+                    assertNull(ctx.pipeline().context(WebSocketServerProtocolHandshakeHandler.class));
+                }
+            }
+        });
         writeUpgradeRequest(ch);
 
         FullHttpResponse response = responses.remove();
         assertEquals(SWITCHING_PROTOCOLS, response.status());
         response.release();
-
-        ch.writeInbound(new DefaultFullHttpRequest(HTTP_1_1, HttpMethod.GET, "/test"));
-        response = responses.remove();
-        assertEquals(FORBIDDEN, response.status());
-        response.release();
+        assertNotNull(WebSocketServerProtocolHandler.getHandshaker(handshakerCtx.channel()));
+        assertFalse(ch.finish());
     }
 
     @Test
@@ -94,6 +106,7 @@ public class WebSocketServerProtocolHandlerTest {
         assertEquals(BAD_REQUEST, response.status());
         assertEquals("not a WebSocket handshake request: missing upgrade", getResponseMessage(response));
         response.release();
+        assertFalse(ch.finish());
     }
 
     @Test
@@ -114,6 +127,49 @@ public class WebSocketServerProtocolHandlerTest {
         assertEquals(BAD_REQUEST, response.status());
         assertEquals("not a WebSocket request: missing key", getResponseMessage(response));
         response.release();
+        assertFalse(ch.finish());
+    }
+
+    @Test
+    public void testCreateUTF8Validator() {
+        WebSocketServerProtocolConfig config = WebSocketServerProtocolConfig.newBuilder()
+                .websocketPath("/test")
+                .withUTF8Validator(true)
+                .build();
+
+        EmbeddedChannel ch = new EmbeddedChannel(
+                new WebSocketServerProtocolHandler(config),
+                new HttpRequestDecoder(),
+                new HttpResponseEncoder(),
+                new MockOutboundHandler());
+        writeUpgradeRequest(ch);
+
+        FullHttpResponse response = responses.remove();
+        assertEquals(SWITCHING_PROTOCOLS, response.status());
+        response.release();
+
+        assertNotNull(ch.pipeline().get(Utf8FrameValidator.class));
+    }
+
+    @Test
+    public void testDoNotCreateUTF8Validator() {
+        WebSocketServerProtocolConfig config = WebSocketServerProtocolConfig.newBuilder()
+                .websocketPath("/test")
+                .withUTF8Validator(false)
+                .build();
+
+        EmbeddedChannel ch = new EmbeddedChannel(
+                new WebSocketServerProtocolHandler(config),
+                new HttpRequestDecoder(),
+                new HttpResponseEncoder(),
+                new MockOutboundHandler());
+        writeUpgradeRequest(ch);
+
+        FullHttpResponse response = responses.remove();
+        assertEquals(SWITCHING_PROTOCOLS, response.status());
+        response.release();
+
+        assertNull(ch.pipeline().get(Utf8FrameValidator.class));
     }
 
     @Test
@@ -122,6 +178,10 @@ public class WebSocketServerProtocolHandlerTest {
         EmbeddedChannel ch = createChannel(customTextFrameHandler);
         writeUpgradeRequest(ch);
 
+        FullHttpResponse response = responses.remove();
+        assertEquals(SWITCHING_PROTOCOLS, response.status());
+        response.release();
+
         if (ch.pipeline().context(HttpRequestDecoder.class) != null) {
             // Removing the HttpRequestDecoder because we are writing a TextWebSocketFrame and thus
             // decoding is not necessary.
@@ -131,6 +191,150 @@ public class WebSocketServerProtocolHandlerTest {
         ch.writeInbound(new TextWebSocketFrame("payload"));
 
         assertEquals("processed: payload", customTextFrameHandler.getContent());
+        assertFalse(ch.finish());
+    }
+
+    @Test
+    public void testExplicitCloseFrameSentWhenServerChannelClosed() throws Exception {
+        WebSocketCloseStatus closeStatus = WebSocketCloseStatus.ENDPOINT_UNAVAILABLE;
+        EmbeddedChannel client = createClient();
+        EmbeddedChannel server = createServer();
+
+        assertFalse(server.writeInbound(client.readOutbound()));
+        assertFalse(client.writeInbound(server.readOutbound()));
+
+        // When server channel closed with explicit close-frame
+        assertTrue(server.writeOutbound(new CloseWebSocketFrame(closeStatus)));
+        server.close();
+
+        // Then client receives provided close-frame
+        assertTrue(client.writeInbound(server.readOutbound()));
+        assertFalse(server.isOpen());
+
+        CloseWebSocketFrame closeMessage = client.readInbound();
+        assertEquals(closeMessage.statusCode(), closeStatus.code());
+        closeMessage.release();
+
+        client.close();
+        assertTrue(ReferenceCountUtil.release(client.readOutbound()));
+        assertFalse(client.finishAndReleaseAll());
+        assertFalse(server.finishAndReleaseAll());
+    }
+
+    @Test
+    public void testCloseFrameSentWhenServerChannelClosedSilently() throws Exception {
+        EmbeddedChannel client = createClient();
+        EmbeddedChannel server = createServer();
+
+        assertFalse(server.writeInbound(client.readOutbound()));
+        assertFalse(client.writeInbound(server.readOutbound()));
+
+        // When server channel closed without explicit close-frame
+        server.close();
+
+        // Then client receives NORMAL_CLOSURE close-frame
+        assertTrue(client.writeInbound(server.readOutbound()));
+        assertFalse(server.isOpen());
+
+        CloseWebSocketFrame closeMessage = client.readInbound();
+        assertEquals(closeMessage.statusCode(), WebSocketCloseStatus.NORMAL_CLOSURE.code());
+        closeMessage.release();
+
+        client.close();
+        assertTrue(ReferenceCountUtil.release(client.readOutbound()));
+        assertFalse(client.finishAndReleaseAll());
+        assertFalse(server.finishAndReleaseAll());
+    }
+
+    @Test
+    public void testExplicitCloseFrameSentWhenClientChannelClosed() throws Exception {
+        WebSocketCloseStatus closeStatus = WebSocketCloseStatus.INVALID_PAYLOAD_DATA;
+        EmbeddedChannel client = createClient();
+        EmbeddedChannel server = createServer();
+
+        assertFalse(server.writeInbound(client.readOutbound()));
+        assertFalse(client.writeInbound(server.readOutbound()));
+
+        // When client channel closed with explicit close-frame
+        assertTrue(client.writeOutbound(new CloseWebSocketFrame(closeStatus)));
+        client.close();
+
+        // Then client receives provided close-frame
+        assertFalse(server.writeInbound(client.readOutbound()));
+        assertFalse(client.isOpen());
+        assertFalse(server.isOpen());
+
+        CloseWebSocketFrame closeMessage = decode(server.<ByteBuf>readOutbound(), CloseWebSocketFrame.class);
+        assertEquals(closeMessage.statusCode(), closeStatus.code());
+        closeMessage.release();
+
+        assertFalse(client.finishAndReleaseAll());
+        assertFalse(server.finishAndReleaseAll());
+    }
+
+    @Test
+    public void testCloseFrameSentWhenClientChannelClosedSilently() throws Exception {
+        EmbeddedChannel client = createClient();
+        EmbeddedChannel server = createServer();
+
+        assertFalse(server.writeInbound(client.readOutbound()));
+        assertFalse(client.writeInbound(server.readOutbound()));
+
+        // When client channel closed without explicit close-frame
+        client.close();
+
+        // Then server receives NORMAL_CLOSURE close-frame
+        assertFalse(server.writeInbound(client.readOutbound()));
+        assertFalse(client.isOpen());
+        assertFalse(server.isOpen());
+
+        CloseWebSocketFrame closeMessage = decode(server.<ByteBuf>readOutbound(), CloseWebSocketFrame.class);
+        assertEquals(closeMessage, new CloseWebSocketFrame(WebSocketCloseStatus.NORMAL_CLOSURE));
+        closeMessage.release();
+
+        assertFalse(client.finishAndReleaseAll());
+        assertFalse(server.finishAndReleaseAll());
+    }
+
+    private EmbeddedChannel createClient(ChannelHandler... handlers) throws Exception {
+        WebSocketClientProtocolConfig clientConfig = WebSocketClientProtocolConfig.newBuilder()
+            .webSocketUri("http://test/test")
+            .dropPongFrames(false)
+            .handleCloseFrames(false)
+            .build();
+        EmbeddedChannel ch = new EmbeddedChannel(false, false,
+            new HttpClientCodec(),
+            new HttpObjectAggregator(8192),
+            new WebSocketClientProtocolHandler(clientConfig)
+        );
+        ch.pipeline().addLast(handlers);
+        ch.register();
+        return ch;
+    }
+
+    private EmbeddedChannel createServer(ChannelHandler... handlers) throws Exception {
+        WebSocketServerProtocolConfig serverConfig = WebSocketServerProtocolConfig.newBuilder()
+            .websocketPath("/test")
+            .dropPongFrames(false)
+            .build();
+        EmbeddedChannel ch = new EmbeddedChannel(false, false,
+            new HttpServerCodec(),
+            new HttpObjectAggregator(8192),
+            new WebSocketServerProtocolHandler(serverConfig)
+        );
+        ch.pipeline().addLast(handlers);
+        ch.register();
+        return ch;
+    }
+
+    @SuppressWarnings("SameParameterValue")
+    private <T> T decode(ByteBuf input, Class<T> clazz) {
+        EmbeddedChannel ch = new EmbeddedChannel(new WebSocket13FrameDecoder(true, false, 65536, true));
+        assertTrue(ch.writeInbound(input));
+        Object decoded = ch.readInbound();
+        assertNotNull(decoded);
+        assertFalse(ch.finish());
+        return clazz.cast(decoded);
     }
 
     private EmbeddedChannel createChannel() {
@@ -138,8 +342,12 @@ public class WebSocketServerProtocolHandlerTest {
     }
 
     private EmbeddedChannel createChannel(ChannelHandler handler) {
+        WebSocketServerProtocolConfig serverConfig = WebSocketServerProtocolConfig.newBuilder()
+            .websocketPath("/test")
+            .sendCloseFrame(null)
+            .build();
         return new EmbeddedChannel(
-                new WebSocketServerProtocolHandler("/test", null, false),
+                new WebSocketServerProtocolHandler(serverConfig),
                 new HttpRequestDecoder(),
                 new HttpResponseEncoder(),
                 new MockOutboundHandler(),
@@ -151,19 +359,19 @@ public class WebSocketServerProtocolHandlerTest {
     }
 
     private static String getResponseMessage(FullHttpResponse response) {
-        return new String(response.content().array());
+        return response.content().toString(CharsetUtil.UTF_8);
     }
 
     private class MockOutboundHandler extends ChannelOutboundHandlerAdapter {
 
         @Override
-        public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
+        public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
             responses.add((FullHttpResponse) msg);
             promise.setSuccess();
         }
 
         @Override
-        public void flush(ChannelHandlerContext ctx) throws Exception {
+        public void flush(ChannelHandlerContext ctx) {
         }
     }
 
@@ -171,7 +379,7 @@ public class WebSocketServerProtocolHandlerTest {
         private String content;
 
         @Override
-        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+        public void channelRead(ChannelHandlerContext ctx, Object msg) {
             assertNull(content);
             content = "processed: " + ((TextWebSocketFrame) msg).text();
             ReferenceCountUtil.release(msg);
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketUtf8FrameValidatorTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketUtf8FrameValidatorTest.java
index c3bb0ed..84428f3 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketUtf8FrameValidatorTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/WebSocketUtf8FrameValidatorTest.java
@@ -36,8 +36,9 @@ public class WebSocketUtf8FrameValidatorTest {
 
     private void assertCorruptedFrameExceptionHandling(byte[] data) {
         EmbeddedChannel channel = new EmbeddedChannel(new Utf8FrameValidator());
+        TextWebSocketFrame frame = new TextWebSocketFrame(Unpooled.copiedBuffer(data));
         try {
-            channel.writeInbound(new TextWebSocketFrame(Unpooled.copiedBuffer(data)));
+            channel.writeInbound(frame);
             Assert.fail();
         } catch (CorruptedFrameException e) {
             // expected exception
@@ -51,5 +52,6 @@ public class WebSocketUtf8FrameValidatorTest {
             buf.release();
         }
         Assert.assertNull(channel.readOutbound());
+        Assert.assertEquals(0, frame.refCnt());
     }
 }
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionFilterProviderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionFilterProviderTest.java
new file mode 100644
index 0000000..66d5dbf
--- /dev/null
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionFilterProviderTest.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2019 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.http.websocketx.extensions;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class WebSocketExtensionFilterProviderTest {
+
+    @Test
+    public void testDefaultExtensionFilterProvider() {
+        WebSocketExtensionFilterProvider defaultProvider = WebSocketExtensionFilterProvider.DEFAULT;
+        assertNotNull(defaultProvider);
+
+        assertEquals(WebSocketExtensionFilter.NEVER_SKIP, defaultProvider.decoderFilter());
+        assertEquals(WebSocketExtensionFilter.NEVER_SKIP, defaultProvider.encoderFilter());
+    }
+}
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionFilterTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionFilterTest.java
new file mode 100644
index 0000000..4d2a122
--- /dev/null
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionFilterTest.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2019 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.http.websocketx.extensions;
+
+import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.PingWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.PongWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class WebSocketExtensionFilterTest {
+
+    @Test
+    public void testNeverSkip() {
+        WebSocketExtensionFilter neverSkip = WebSocketExtensionFilter.NEVER_SKIP;
+
+        BinaryWebSocketFrame binaryFrame = new BinaryWebSocketFrame();
+        assertFalse(neverSkip.mustSkip(binaryFrame));
+        assertTrue(binaryFrame.release());
+
+        TextWebSocketFrame textFrame = new TextWebSocketFrame();
+        assertFalse(neverSkip.mustSkip(textFrame));
+        assertTrue(textFrame.release());
+
+        PingWebSocketFrame pingFrame = new PingWebSocketFrame();
+        assertFalse(neverSkip.mustSkip(pingFrame));
+        assertTrue(pingFrame.release());
+
+        PongWebSocketFrame pongFrame = new PongWebSocketFrame();
+        assertFalse(neverSkip.mustSkip(pongFrame));
+        assertTrue(pongFrame.release());
+
+        CloseWebSocketFrame closeFrame = new CloseWebSocketFrame();
+        assertFalse(neverSkip.mustSkip(closeFrame));
+        assertTrue(closeFrame.release());
+
+        ContinuationWebSocketFrame continuationFrame = new ContinuationWebSocketFrame();
+        assertFalse(neverSkip.mustSkip(continuationFrame));
+        assertTrue(continuationFrame.release());
+    }
+
+    @Test
+    public void testAlwaysSkip() {
+        WebSocketExtensionFilter neverSkip = WebSocketExtensionFilter.ALWAYS_SKIP;
+
+        BinaryWebSocketFrame binaryFrame = new BinaryWebSocketFrame();
+        assertTrue(neverSkip.mustSkip(binaryFrame));
+        assertTrue(binaryFrame.release());
+
+        TextWebSocketFrame textFrame = new TextWebSocketFrame();
+        assertTrue(neverSkip.mustSkip(textFrame));
+        assertTrue(textFrame.release());
+
+        PingWebSocketFrame pingFrame = new PingWebSocketFrame();
+        assertTrue(neverSkip.mustSkip(pingFrame));
+        assertTrue(pingFrame.release());
+
+        PongWebSocketFrame pongFrame = new PongWebSocketFrame();
+        assertTrue(neverSkip.mustSkip(pongFrame));
+        assertTrue(pongFrame.release());
+
+        CloseWebSocketFrame closeFrame = new CloseWebSocketFrame();
+        assertTrue(neverSkip.mustSkip(closeFrame));
+        assertTrue(closeFrame.release());
+
+        ContinuationWebSocketFrame continuationFrame = new ContinuationWebSocketFrame();
+        assertTrue(neverSkip.mustSkip(continuationFrame));
+        assertTrue(continuationFrame.release());
+    }
+}
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionTestUtil.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionTestUtil.java
index 411b167..38867fe 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionTestUtil.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketExtensionTestUtil.java
@@ -69,7 +69,7 @@ public final class WebSocketExtensionTestUtil {
 
         private final String name;
 
-        public WebSocketExtensionDataMatcher(String name) {
+        WebSocketExtensionDataMatcher(String name) {
             this.name = name;
         }
 
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketServerExtensionHandlerTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketServerExtensionHandlerTest.java
index c02853f..ba4e3cc 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketServerExtensionHandlerTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/WebSocketServerExtensionHandlerTest.java
@@ -15,11 +15,13 @@
  */
 package io.netty.handler.codec.http.websocketx.extensions;
 
+import io.netty.channel.ChannelPromise;
 import io.netty.channel.embedded.EmbeddedChannel;
 import io.netty.handler.codec.http.HttpHeaderNames;
 import io.netty.handler.codec.http.HttpRequest;
 import io.netty.handler.codec.http.HttpResponse;
 
+import java.io.IOException;
 import java.util.Collections;
 import java.util.List;
 
@@ -62,8 +64,9 @@ public class WebSocketServerExtensionHandlerTest {
         when(fallbackExtensionMock.rsv()).thenReturn(WebSocketExtension.RSV1);
 
         // execute
-        EmbeddedChannel ch = new EmbeddedChannel(new WebSocketServerExtensionHandler(
-                mainHandshakerMock, fallbackHandshakerMock));
+        WebSocketServerExtensionHandler extensionHandler =
+                new WebSocketServerExtensionHandler(mainHandshakerMock, fallbackHandshakerMock);
+        EmbeddedChannel ch = new EmbeddedChannel(extensionHandler);
 
         HttpRequest req = newUpgradeRequest("main, fallback");
         ch.writeInbound(req);
@@ -76,6 +79,7 @@ public class WebSocketServerExtensionHandlerTest {
                 res2.headers().get(HttpHeaderNames.SEC_WEBSOCKET_EXTENSIONS));
 
         // test
+        assertNull(ch.pipeline().context(extensionHandler));
         assertEquals(1, resExts.size());
         assertEquals("main", resExts.get(0).name());
         assertTrue(resExts.get(0).parameters().isEmpty());
@@ -119,8 +123,9 @@ public class WebSocketServerExtensionHandlerTest {
         when(fallbackExtensionMock.newExtensionDecoder()).thenReturn(new Dummy2Decoder());
 
         // execute
-        EmbeddedChannel ch = new EmbeddedChannel(new WebSocketServerExtensionHandler(
-                mainHandshakerMock, fallbackHandshakerMock));
+        WebSocketServerExtensionHandler extensionHandler =
+                new WebSocketServerExtensionHandler(mainHandshakerMock, fallbackHandshakerMock);
+        EmbeddedChannel ch = new EmbeddedChannel(extensionHandler);
 
         HttpRequest req = newUpgradeRequest("main, fallback");
         ch.writeInbound(req);
@@ -133,6 +138,7 @@ public class WebSocketServerExtensionHandlerTest {
                 res2.headers().get(HttpHeaderNames.SEC_WEBSOCKET_EXTENSIONS));
 
         // test
+        assertNull(ch.pipeline().context(extensionHandler));
         assertEquals(2, resExts.size());
         assertEquals("main", resExts.get(0).name());
         assertEquals("fallback", resExts.get(1).name());
@@ -170,8 +176,9 @@ public class WebSocketServerExtensionHandlerTest {
                 thenReturn(null);
 
         // execute
-        EmbeddedChannel ch = new EmbeddedChannel(new WebSocketServerExtensionHandler(
-                mainHandshakerMock, fallbackHandshakerMock));
+        WebSocketServerExtensionHandler extensionHandler =
+                new WebSocketServerExtensionHandler(mainHandshakerMock, fallbackHandshakerMock);
+        EmbeddedChannel ch = new EmbeddedChannel(extensionHandler);
 
         HttpRequest req = newUpgradeRequest("unknown, unknown2");
         ch.writeInbound(req);
@@ -182,6 +189,7 @@ public class WebSocketServerExtensionHandlerTest {
         HttpResponse res2 = ch.readOutbound();
 
         // test
+        assertNull(ch.pipeline().context(extensionHandler));
         assertFalse(res2.headers().contains(HttpHeaderNames.SEC_WEBSOCKET_EXTENSIONS));
 
         verify(mainHandshakerMock).handshakeExtension(webSocketExtensionDataMatcher("unknown"));
@@ -190,4 +198,31 @@ public class WebSocketServerExtensionHandlerTest {
         verify(fallbackHandshakerMock).handshakeExtension(webSocketExtensionDataMatcher("unknown"));
         verify(fallbackHandshakerMock).handshakeExtension(webSocketExtensionDataMatcher("unknown2"));
     }
+
+    @Test
+    public void testExtensionHandlerNotRemovedByFailureWritePromise() {
+        // initialize
+        when(mainHandshakerMock.handshakeExtension(webSocketExtensionDataMatcher("main")))
+                .thenReturn(mainExtensionMock);
+        when(mainExtensionMock.newReponseData()).thenReturn(
+                new WebSocketExtensionData("main", Collections.<String, String>emptyMap()));
+
+        // execute
+        WebSocketServerExtensionHandler extensionHandler =
+                new WebSocketServerExtensionHandler(mainHandshakerMock);
+        EmbeddedChannel ch = new EmbeddedChannel(extensionHandler);
+
+        HttpRequest req = newUpgradeRequest("main");
+        ch.writeInbound(req);
+
+        HttpResponse res = newUpgradeResponse(null);
+        ChannelPromise failurePromise = ch.newPromise();
+        ch.writeOneOutbound(res, failurePromise);
+        failurePromise.setFailure(new IOException("Cannot write response"));
+
+        // test
+        assertNull(ch.readOutbound());
+        assertNotNull(ch.pipeline().context(extensionHandler));
+        assertTrue(ch.finish());
+    }
 }
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateDecoderTest.java
index 67baa51..1d153f3 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateDecoderTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateDecoderTest.java
@@ -15,22 +15,20 @@
  */
 package io.netty.handler.codec.http.websocketx.extensions.compression;
 
-import static io.netty.handler.codec.http.websocketx.extensions.WebSocketExtension.RSV1;
-import static io.netty.handler.codec.http.websocketx.extensions.WebSocketExtension.RSV3;
-import static org.junit.Assert.*;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
 import io.netty.channel.embedded.EmbeddedChannel;
 import io.netty.handler.codec.compression.ZlibCodecFactory;
 import io.netty.handler.codec.compression.ZlibWrapper;
 import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
-import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
 import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtension;
+import org.junit.Test;
 
-import java.util.Arrays;
 import java.util.Random;
 
-import org.junit.Test;
+import static io.netty.handler.codec.http.websocketx.extensions.WebSocketExtension.*;
+import static io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionFilter.*;
+import static org.junit.Assert.*;
 
 public class PerFrameDeflateDecoderTest {
 
@@ -46,7 +44,7 @@ public class PerFrameDeflateDecoderTest {
         byte[] payload = new byte[300];
         random.nextBytes(payload);
 
-        encoderChannel.writeOutbound(Unpooled.wrappedBuffer(payload));
+        assertTrue(encoderChannel.writeOutbound(Unpooled.wrappedBuffer(payload)));
         ByteBuf compressedPayload = encoderChannel.readOutbound();
 
         BinaryWebSocketFrame compressedFrame = new BinaryWebSocketFrame(true,
@@ -54,19 +52,18 @@ public class PerFrameDeflateDecoderTest {
                 compressedPayload.slice(0, compressedPayload.readableBytes() - 4));
 
         // execute
-        decoderChannel.writeInbound(compressedFrame);
+        assertTrue(decoderChannel.writeInbound(compressedFrame));
         BinaryWebSocketFrame uncompressedFrame = decoderChannel.readInbound();
 
         // test
         assertNotNull(uncompressedFrame);
         assertNotNull(uncompressedFrame.content());
-        assertTrue(uncompressedFrame instanceof BinaryWebSocketFrame);
         assertEquals(RSV3, uncompressedFrame.rsv());
         assertEquals(300, uncompressedFrame.content().readableBytes());
 
         byte[] finalPayload = new byte[300];
         uncompressedFrame.content().readBytes(finalPayload);
-        assertTrue(Arrays.equals(finalPayload, payload));
+        assertArrayEquals(finalPayload, payload);
         uncompressedFrame.release();
     }
 
@@ -82,19 +79,18 @@ public class PerFrameDeflateDecoderTest {
                 RSV3, Unpooled.wrappedBuffer(payload));
 
         // execute
-        decoderChannel.writeInbound(frame);
+        assertTrue(decoderChannel.writeInbound(frame));
         BinaryWebSocketFrame newFrame = decoderChannel.readInbound();
 
         // test
         assertNotNull(newFrame);
         assertNotNull(newFrame.content());
-        assertTrue(newFrame instanceof BinaryWebSocketFrame);
         assertEquals(RSV3, newFrame.rsv());
         assertEquals(300, newFrame.content().readableBytes());
 
         byte[] finalPayload = new byte[300];
         newFrame.content().readBytes(finalPayload);
-        assertTrue(Arrays.equals(finalPayload, payload));
+        assertArrayEquals(finalPayload, payload);
         newFrame.release();
     }
 
@@ -105,21 +101,51 @@ public class PerFrameDeflateDecoderTest {
                 ZlibCodecFactory.newZlibEncoder(ZlibWrapper.NONE, 9, 15, 8));
         EmbeddedChannel decoderChannel = new EmbeddedChannel(new PerFrameDeflateDecoder(false));
 
-        encoderChannel.writeOutbound(Unpooled.EMPTY_BUFFER);
+        assertTrue(encoderChannel.writeOutbound(Unpooled.EMPTY_BUFFER));
         ByteBuf compressedPayload = encoderChannel.readOutbound();
         BinaryWebSocketFrame compressedFrame =
                 new BinaryWebSocketFrame(true, RSV1 | RSV3, compressedPayload);
 
         // execute
-        decoderChannel.writeInbound(compressedFrame);
+        assertTrue(decoderChannel.writeInbound(compressedFrame));
         BinaryWebSocketFrame uncompressedFrame = decoderChannel.readInbound();
 
         // test
         assertNotNull(uncompressedFrame);
         assertNotNull(uncompressedFrame.content());
-        assertTrue(uncompressedFrame instanceof BinaryWebSocketFrame);
         assertEquals(RSV3, uncompressedFrame.rsv());
         assertEquals(0, uncompressedFrame.content().readableBytes());
         uncompressedFrame.release();
     }
+
+    @Test
+    public void testDecompressionSkip() {
+        EmbeddedChannel encoderChannel = new EmbeddedChannel(
+                ZlibCodecFactory.newZlibEncoder(ZlibWrapper.NONE, 9, 15, 8));
+        EmbeddedChannel decoderChannel = new EmbeddedChannel(new PerFrameDeflateDecoder(false, ALWAYS_SKIP));
+
+        byte[] payload = new byte[300];
+        random.nextBytes(payload);
+
+        assertTrue(encoderChannel.writeOutbound(Unpooled.wrappedBuffer(payload)));
+        ByteBuf compressedPayload = encoderChannel.readOutbound();
+
+        BinaryWebSocketFrame compressedBinaryFrame = new BinaryWebSocketFrame(
+                true, WebSocketExtension.RSV1 | WebSocketExtension.RSV3, compressedPayload);
+
+        assertTrue(decoderChannel.writeInbound(compressedBinaryFrame));
+
+        BinaryWebSocketFrame inboundBinaryFrame = decoderChannel.readInbound();
+
+        assertNotNull(inboundBinaryFrame);
+        assertNotNull(inboundBinaryFrame.content());
+        assertEquals(compressedPayload, inboundBinaryFrame.content());
+        assertEquals(5, inboundBinaryFrame.rsv());
+
+        assertTrue(inboundBinaryFrame.release());
+
+        assertTrue(encoderChannel.finishAndReleaseAll());
+        assertFalse(decoderChannel.finish());
+    }
+
 }
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateEncoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateEncoderTest.java
index 5c085e9..4041f5c 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateEncoderTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerFrameDeflateEncoderTest.java
@@ -15,8 +15,8 @@
  */
 package io.netty.handler.codec.http.websocketx.extensions.compression;
 
-import static org.junit.Assert.*;
 import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
 import io.netty.buffer.Unpooled;
 import io.netty.channel.embedded.EmbeddedChannel;
 import io.netty.handler.codec.compression.ZlibCodecFactory;
@@ -24,11 +24,12 @@ import io.netty.handler.codec.compression.ZlibWrapper;
 import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
 import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame;
 import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtension;
+import org.junit.Test;
 
-import java.util.Arrays;
 import java.util.Random;
 
-import org.junit.Test;
+import static io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionFilter.*;
+import static org.junit.Assert.*;
 
 public class PerFrameDeflateEncoderTest {
 
@@ -47,23 +48,22 @@ public class PerFrameDeflateEncoderTest {
                 WebSocketExtension.RSV3, Unpooled.wrappedBuffer(payload));
 
         // execute
-        encoderChannel.writeOutbound(frame);
+        assertTrue(encoderChannel.writeOutbound(frame));
         BinaryWebSocketFrame compressedFrame = encoderChannel.readOutbound();
 
         // test
         assertNotNull(compressedFrame);
         assertNotNull(compressedFrame.content());
-        assertTrue(compressedFrame instanceof BinaryWebSocketFrame);
         assertEquals(WebSocketExtension.RSV1 | WebSocketExtension.RSV3, compressedFrame.rsv());
 
-        decoderChannel.writeInbound(compressedFrame.content());
-        decoderChannel.writeInbound(DeflateDecoder.FRAME_TAIL);
+        assertTrue(decoderChannel.writeInbound(compressedFrame.content()));
+        assertTrue(decoderChannel.writeInbound(DeflateDecoder.FRAME_TAIL.duplicate()));
         ByteBuf uncompressedPayload = decoderChannel.readInbound();
         assertEquals(300, uncompressedPayload.readableBytes());
 
         byte[] finalPayload = new byte[300];
         uncompressedPayload.readBytes(finalPayload);
-        assertTrue(Arrays.equals(finalPayload, payload));
+        assertArrayEquals(finalPayload, payload);
         uncompressedPayload.release();
     }
 
@@ -79,19 +79,18 @@ public class PerFrameDeflateEncoderTest {
                 WebSocketExtension.RSV3 | WebSocketExtension.RSV1, Unpooled.wrappedBuffer(payload));
 
         // execute
-        encoderChannel.writeOutbound(frame);
+        assertTrue(encoderChannel.writeOutbound(frame));
         BinaryWebSocketFrame newFrame = encoderChannel.readOutbound();
 
         // test
         assertNotNull(newFrame);
         assertNotNull(newFrame.content());
-        assertTrue(newFrame instanceof BinaryWebSocketFrame);
         assertEquals(WebSocketExtension.RSV3 | WebSocketExtension.RSV1, newFrame.rsv());
         assertEquals(300, newFrame.content().readableBytes());
 
         byte[] finalPayload = new byte[300];
         newFrame.content().readBytes(finalPayload);
-        assertTrue(Arrays.equals(finalPayload, payload));
+        assertArrayEquals(finalPayload, payload);
         newFrame.release();
     }
 
@@ -117,9 +116,9 @@ public class PerFrameDeflateEncoderTest {
                 WebSocketExtension.RSV3, Unpooled.wrappedBuffer(payload3));
 
         // execute
-        encoderChannel.writeOutbound(frame1);
-        encoderChannel.writeOutbound(frame2);
-        encoderChannel.writeOutbound(frame3);
+        assertTrue(encoderChannel.writeOutbound(frame1));
+        assertTrue(encoderChannel.writeOutbound(frame2));
+        assertTrue(encoderChannel.writeOutbound(frame3));
         BinaryWebSocketFrame compressedFrame1 = encoderChannel.readOutbound();
         ContinuationWebSocketFrame compressedFrame2 = encoderChannel.readOutbound();
         ContinuationWebSocketFrame compressedFrame3 = encoderChannel.readOutbound();
@@ -135,28 +134,52 @@ public class PerFrameDeflateEncoderTest {
         assertFalse(compressedFrame2.isFinalFragment());
         assertTrue(compressedFrame3.isFinalFragment());
 
-        decoderChannel.writeInbound(compressedFrame1.content());
-        decoderChannel.writeInbound(Unpooled.wrappedBuffer(DeflateDecoder.FRAME_TAIL));
+        assertTrue(decoderChannel.writeInbound(compressedFrame1.content()));
+        assertTrue(decoderChannel.writeInbound(DeflateDecoder.FRAME_TAIL.duplicate()));
         ByteBuf uncompressedPayload1 = decoderChannel.readInbound();
         byte[] finalPayload1 = new byte[100];
         uncompressedPayload1.readBytes(finalPayload1);
-        assertTrue(Arrays.equals(finalPayload1, payload1));
+        assertArrayEquals(finalPayload1, payload1);
         uncompressedPayload1.release();
 
-        decoderChannel.writeInbound(compressedFrame2.content());
-        decoderChannel.writeInbound(Unpooled.wrappedBuffer(DeflateDecoder.FRAME_TAIL));
+        assertTrue(decoderChannel.writeInbound(compressedFrame2.content()));
+        assertTrue(decoderChannel.writeInbound(DeflateDecoder.FRAME_TAIL.duplicate()));
         ByteBuf uncompressedPayload2 = decoderChannel.readInbound();
         byte[] finalPayload2 = new byte[100];
         uncompressedPayload2.readBytes(finalPayload2);
-        assertTrue(Arrays.equals(finalPayload2, payload2));
+        assertArrayEquals(finalPayload2, payload2);
         uncompressedPayload2.release();
 
-        decoderChannel.writeInbound(compressedFrame3.content());
-        decoderChannel.writeInbound(Unpooled.wrappedBuffer(DeflateDecoder.FRAME_TAIL));
+        assertTrue(decoderChannel.writeInbound(compressedFrame3.content()));
+        assertTrue(decoderChannel.writeInbound(DeflateDecoder.FRAME_TAIL.duplicate()));
         ByteBuf uncompressedPayload3 = decoderChannel.readInbound();
         byte[] finalPayload3 = new byte[100];
         uncompressedPayload3.readBytes(finalPayload3);
-        assertTrue(Arrays.equals(finalPayload3, payload3));
+        assertArrayEquals(finalPayload3, payload3);
         uncompressedPayload3.release();
     }
+
+    @Test
+    public void testCompressionSkip() {
+        EmbeddedChannel encoderChannel = new EmbeddedChannel(
+                new PerFrameDeflateEncoder(9, 15, false, ALWAYS_SKIP));
+        byte[] payload = new byte[300];
+        random.nextBytes(payload);
+        BinaryWebSocketFrame binaryFrame = new BinaryWebSocketFrame(true,
+                                                                    0, Unpooled.wrappedBuffer(payload));
+
+        // execute
+        assertTrue(encoderChannel.writeOutbound(binaryFrame.copy()));
+        BinaryWebSocketFrame outboundFrame = encoderChannel.readOutbound();
+
+        // test
+        assertNotNull(outboundFrame);
+        assertNotNull(outboundFrame.content());
+        assertArrayEquals(payload, ByteBufUtil.getBytes(outboundFrame.content()));
+        assertEquals(0, outboundFrame.rsv());
+        assertTrue(outboundFrame.release());
+
+        assertFalse(encoderChannel.finish());
+    }
+
 }
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateDecoderTest.java
index d5e5868..9529b44 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateDecoderTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateDecoderTest.java
@@ -15,20 +15,27 @@
  */
 package io.netty.handler.codec.http.websocketx.extensions.compression;
 
-import static org.junit.Assert.*;
 import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
 import io.netty.buffer.Unpooled;
 import io.netty.channel.embedded.EmbeddedChannel;
+import io.netty.handler.codec.DecoderException;
 import io.netty.handler.codec.compression.ZlibCodecFactory;
 import io.netty.handler.codec.compression.ZlibWrapper;
 import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
 import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.WebSocketFrame;
 import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtension;
+import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionFilter;
+import org.junit.Test;
 
-import java.util.Arrays;
 import java.util.Random;
 
-import org.junit.Test;
+import static io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionFilter.*;
+import static io.netty.handler.codec.http.websocketx.extensions.compression.DeflateDecoder.*;
+import static io.netty.util.CharsetUtil.*;
+import static org.junit.Assert.*;
 
 public class PerMessageDeflateDecoderTest {
 
@@ -44,7 +51,7 @@ public class PerMessageDeflateDecoderTest {
         byte[] payload = new byte[300];
         random.nextBytes(payload);
 
-        encoderChannel.writeOutbound(Unpooled.wrappedBuffer(payload));
+        assertTrue(encoderChannel.writeOutbound(Unpooled.wrappedBuffer(payload)));
         ByteBuf compressedPayload = encoderChannel.readOutbound();
 
         BinaryWebSocketFrame compressedFrame = new BinaryWebSocketFrame(true,
@@ -52,19 +59,18 @@ public class PerMessageDeflateDecoderTest {
                 compressedPayload.slice(0, compressedPayload.readableBytes() - 4));
 
         // execute
-        decoderChannel.writeInbound(compressedFrame);
+        assertTrue(decoderChannel.writeInbound(compressedFrame));
         BinaryWebSocketFrame uncompressedFrame = decoderChannel.readInbound();
 
         // test
         assertNotNull(uncompressedFrame);
         assertNotNull(uncompressedFrame.content());
-        assertTrue(uncompressedFrame instanceof BinaryWebSocketFrame);
         assertEquals(WebSocketExtension.RSV3, uncompressedFrame.rsv());
         assertEquals(300, uncompressedFrame.content().readableBytes());
 
         byte[] finalPayload = new byte[300];
         uncompressedFrame.content().readBytes(finalPayload);
-        assertTrue(Arrays.equals(finalPayload, payload));
+        assertArrayEquals(finalPayload, payload);
         uncompressedFrame.release();
     }
 
@@ -80,24 +86,23 @@ public class PerMessageDeflateDecoderTest {
                 WebSocketExtension.RSV3, Unpooled.wrappedBuffer(payload));
 
         // execute
-        decoderChannel.writeInbound(frame);
+        assertTrue(decoderChannel.writeInbound(frame));
         BinaryWebSocketFrame newFrame = decoderChannel.readInbound();
 
         // test
         assertNotNull(newFrame);
         assertNotNull(newFrame.content());
-        assertTrue(newFrame instanceof BinaryWebSocketFrame);
         assertEquals(WebSocketExtension.RSV3, newFrame.rsv());
         assertEquals(300, newFrame.content().readableBytes());
 
         byte[] finalPayload = new byte[300];
         newFrame.content().readBytes(finalPayload);
-        assertTrue(Arrays.equals(finalPayload, payload));
+        assertArrayEquals(finalPayload, payload);
         newFrame.release();
     }
 
     @Test
-    public void testFramementedFrame() {
+    public void testFragmentedFrame() {
         EmbeddedChannel encoderChannel = new EmbeddedChannel(
                 ZlibCodecFactory.newZlibEncoder(ZlibWrapper.NONE, 9, 15, 8));
         EmbeddedChannel decoderChannel = new EmbeddedChannel(new PerMessageDeflateDecoder(false));
@@ -106,7 +111,7 @@ public class PerMessageDeflateDecoderTest {
         byte[] payload = new byte[300];
         random.nextBytes(payload);
 
-        encoderChannel.writeOutbound(Unpooled.wrappedBuffer(payload));
+        assertTrue(encoderChannel.writeOutbound(Unpooled.wrappedBuffer(payload)));
         ByteBuf compressedPayload = encoderChannel.readOutbound();
         compressedPayload = compressedPayload.slice(0, compressedPayload.readableBytes() - 4);
 
@@ -121,9 +126,9 @@ public class PerMessageDeflateDecoderTest {
                         compressedPayload.readableBytes() - oneThird * 2));
 
         // execute
-        decoderChannel.writeInbound(compressedFrame1.retain());
-        decoderChannel.writeInbound(compressedFrame2.retain());
-        decoderChannel.writeInbound(compressedFrame3);
+        assertTrue(decoderChannel.writeInbound(compressedFrame1.retain()));
+        assertTrue(decoderChannel.writeInbound(compressedFrame2.retain()));
+        assertTrue(decoderChannel.writeInbound(compressedFrame3));
         BinaryWebSocketFrame uncompressedFrame1 = decoderChannel.readInbound();
         ContinuationWebSocketFrame uncompressedFrame2 = decoderChannel.readInbound();
         ContinuationWebSocketFrame uncompressedFrame3 = decoderChannel.readInbound();
@@ -142,7 +147,7 @@ public class PerMessageDeflateDecoderTest {
 
         byte[] finalPayload = new byte[300];
         finalPayloadWrapped.readBytes(finalPayload);
-        assertTrue(Arrays.equals(finalPayload, payload));
+        assertArrayEquals(finalPayload, payload);
         finalPayloadWrapped.release();
     }
 
@@ -158,9 +163,9 @@ public class PerMessageDeflateDecoderTest {
         byte[] payload2 = new byte[100];
         random.nextBytes(payload2);
 
-        encoderChannel.writeOutbound(Unpooled.wrappedBuffer(payload1));
+        assertTrue(encoderChannel.writeOutbound(Unpooled.wrappedBuffer(payload1)));
         ByteBuf compressedPayload1 = encoderChannel.readOutbound();
-        encoderChannel.writeOutbound(Unpooled.wrappedBuffer(payload2));
+        assertTrue(encoderChannel.writeOutbound(Unpooled.wrappedBuffer(payload2)));
         ByteBuf compressedPayload2 = encoderChannel.readOutbound();
 
         BinaryWebSocketFrame compressedFrame = new BinaryWebSocketFrame(true,
@@ -170,23 +175,215 @@ public class PerMessageDeflateDecoderTest {
                         compressedPayload2.slice(0, compressedPayload2.readableBytes() - 4)));
 
         // execute
-        decoderChannel.writeInbound(compressedFrame);
+        assertTrue(decoderChannel.writeInbound(compressedFrame));
         BinaryWebSocketFrame uncompressedFrame = decoderChannel.readInbound();
 
         // test
         assertNotNull(uncompressedFrame);
         assertNotNull(uncompressedFrame.content());
-        assertTrue(uncompressedFrame instanceof BinaryWebSocketFrame);
         assertEquals(WebSocketExtension.RSV3, uncompressedFrame.rsv());
         assertEquals(200, uncompressedFrame.content().readableBytes());
 
         byte[] finalPayload1 = new byte[100];
         uncompressedFrame.content().readBytes(finalPayload1);
-        assertTrue(Arrays.equals(finalPayload1, payload1));
+        assertArrayEquals(finalPayload1, payload1);
         byte[] finalPayload2 = new byte[100];
         uncompressedFrame.content().readBytes(finalPayload2);
-        assertTrue(Arrays.equals(finalPayload2, payload2));
+        assertArrayEquals(finalPayload2, payload2);
         uncompressedFrame.release();
     }
 
+    @Test
+    public void testDecompressionSkipForBinaryFrame() {
+        EmbeddedChannel encoderChannel = new EmbeddedChannel(
+                ZlibCodecFactory.newZlibEncoder(ZlibWrapper.NONE, 9, 15, 8));
+        EmbeddedChannel decoderChannel = new EmbeddedChannel(new PerMessageDeflateDecoder(false, ALWAYS_SKIP));
+
+        byte[] payload = new byte[300];
+        random.nextBytes(payload);
+
+        assertTrue(encoderChannel.writeOutbound(Unpooled.wrappedBuffer(payload)));
+        ByteBuf compressedPayload = encoderChannel.readOutbound();
+
+        BinaryWebSocketFrame compressedBinaryFrame = new BinaryWebSocketFrame(true, WebSocketExtension.RSV1,
+                                                                              compressedPayload);
+        assertTrue(decoderChannel.writeInbound(compressedBinaryFrame));
+
+        WebSocketFrame inboundFrame = decoderChannel.readInbound();
+
+        assertEquals(WebSocketExtension.RSV1, inboundFrame.rsv());
+        assertEquals(compressedPayload, inboundFrame.content());
+        assertTrue(inboundFrame.release());
+
+        assertTrue(encoderChannel.finishAndReleaseAll());
+        assertFalse(decoderChannel.finish());
+    }
+
+    @Test
+    public void testSelectivityDecompressionSkip() {
+        WebSocketExtensionFilter selectivityDecompressionFilter = new WebSocketExtensionFilter() {
+            @Override
+            public boolean mustSkip(WebSocketFrame frame) {
+                return frame instanceof TextWebSocketFrame && frame.content().readableBytes() < 100;
+            }
+        };
+        EmbeddedChannel encoderChannel = new EmbeddedChannel(
+                ZlibCodecFactory.newZlibEncoder(ZlibWrapper.NONE, 9, 15, 8));
+        EmbeddedChannel decoderChannel = new EmbeddedChannel(
+                new PerMessageDeflateDecoder(false, selectivityDecompressionFilter));
+
+        String textPayload = "compressed payload";
+        byte[] binaryPayload = new byte[300];
+        random.nextBytes(binaryPayload);
+
+        assertTrue(encoderChannel.writeOutbound(Unpooled.wrappedBuffer(textPayload.getBytes(UTF_8))));
+        assertTrue(encoderChannel.writeOutbound(Unpooled.wrappedBuffer(binaryPayload)));
+        ByteBuf compressedTextPayload = encoderChannel.readOutbound();
+        ByteBuf compressedBinaryPayload = encoderChannel.readOutbound();
+
+        TextWebSocketFrame compressedTextFrame = new TextWebSocketFrame(true, WebSocketExtension.RSV1,
+                                                                        compressedTextPayload);
+        BinaryWebSocketFrame compressedBinaryFrame = new BinaryWebSocketFrame(true, WebSocketExtension.RSV1,
+                                                                              compressedBinaryPayload);
+
+        assertTrue(decoderChannel.writeInbound(compressedTextFrame));
+        assertTrue(decoderChannel.writeInbound(compressedBinaryFrame));
+
+        TextWebSocketFrame inboundTextFrame = decoderChannel.readInbound();
+        BinaryWebSocketFrame inboundBinaryFrame = decoderChannel.readInbound();
+
+        assertEquals(WebSocketExtension.RSV1, inboundTextFrame.rsv());
+        assertEquals(compressedTextPayload, inboundTextFrame.content());
+        assertTrue(inboundTextFrame.release());
+
+        assertEquals(0, inboundBinaryFrame.rsv());
+        assertArrayEquals(binaryPayload, ByteBufUtil.getBytes(inboundBinaryFrame.content()));
+        assertTrue(inboundBinaryFrame.release());
+
+        assertTrue(encoderChannel.finishAndReleaseAll());
+        assertFalse(decoderChannel.finish());
+    }
+
+    @Test(expected = DecoderException.class)
+    public void testIllegalStateWhenDecompressionInProgress() {
+        WebSocketExtensionFilter selectivityDecompressionFilter = new WebSocketExtensionFilter() {
+            @Override
+            public boolean mustSkip(WebSocketFrame frame) {
+                return frame.content().readableBytes() < 100;
+            }
+        };
+
+        EmbeddedChannel encoderChannel = new EmbeddedChannel(
+                ZlibCodecFactory.newZlibEncoder(ZlibWrapper.NONE, 9, 15, 8));
+        EmbeddedChannel decoderChannel = new EmbeddedChannel(
+                new PerMessageDeflateDecoder(false, selectivityDecompressionFilter));
+
+        byte[] firstPayload = new byte[200];
+        random.nextBytes(firstPayload);
+
+        byte[] finalPayload = new byte[50];
+        random.nextBytes(finalPayload);
+
+        assertTrue(encoderChannel.writeOutbound(Unpooled.wrappedBuffer(firstPayload)));
+        assertTrue(encoderChannel.writeOutbound(Unpooled.wrappedBuffer(finalPayload)));
+        ByteBuf compressedFirstPayload = encoderChannel.readOutbound();
+        ByteBuf compressedFinalPayload = encoderChannel.readOutbound();
+        assertTrue(encoderChannel.finishAndReleaseAll());
+
+        BinaryWebSocketFrame firstPart = new BinaryWebSocketFrame(false, WebSocketExtension.RSV1,
+                                                                  compressedFirstPayload);
+        ContinuationWebSocketFrame finalPart = new ContinuationWebSocketFrame(true, WebSocketExtension.RSV1,
+                                                                              compressedFinalPayload);
+        assertTrue(decoderChannel.writeInbound(firstPart));
+
+        BinaryWebSocketFrame outboundFirstPart = decoderChannel.readInbound();
+        //first part is decompressed
+        assertEquals(0, outboundFirstPart.rsv());
+        assertArrayEquals(firstPayload, ByteBufUtil.getBytes(outboundFirstPart.content()));
+        assertTrue(outboundFirstPart.release());
+
+        //final part throwing exception
+        try {
+            decoderChannel.writeInbound(finalPart);
+        } finally {
+            assertTrue(finalPart.release());
+            assertFalse(encoderChannel.finishAndReleaseAll());
+        }
+    }
+
+    @Test
+    public void testEmptyFrameDecompression() {
+        EmbeddedChannel decoderChannel = new EmbeddedChannel(new PerMessageDeflateDecoder(false));
+
+        TextWebSocketFrame emptyDeflateBlockFrame = new TextWebSocketFrame(true, WebSocketExtension.RSV1,
+                                                                           EMPTY_DEFLATE_BLOCK);
+
+        assertTrue(decoderChannel.writeInbound(emptyDeflateBlockFrame));
+        TextWebSocketFrame emptyBufferFrame = decoderChannel.readInbound();
+
+        assertFalse(emptyBufferFrame.content().isReadable());
+
+        // Composite empty buffer
+        assertTrue(emptyBufferFrame.release());
+        assertFalse(decoderChannel.finish());
+    }
+
+    @Test
+    public void testFragmentedFrameWithLeftOverInLastFragment() {
+        String hexDump = "677170647a777a737574656b707a787a6f6a7561756578756f6b7868616371716c657a6d64697479766d726f6" +
+                         "269746c6376777464776f6f72767a726f64667278676764687775786f6762766d776d706b76697773777a7072" +
+                         "6a6a737279707a7078697a6c69616d7461656d646278626d786f66666e686e776a7a7461746d7a776668776b6" +
+                         "f6f736e73746575637a6d727a7175707a6e74627578687871767771697a71766c64626d78726d6d7675756877" +
+                         "62667963626b687a726d676e646263776e67797264706d6c6863626577616967706a78636a72697464756e627" +
+                         "977616f79736475676f76736f7178746a7a7479626c64636b6b6778637768746c62";
+        EmbeddedChannel encoderChannel = new EmbeddedChannel(
+                ZlibCodecFactory.newZlibEncoder(ZlibWrapper.NONE, 9, 15, 8));
+        EmbeddedChannel decoderChannel = new EmbeddedChannel(new PerMessageDeflateDecoder(false));
+
+        ByteBuf originPayload = Unpooled.wrappedBuffer(ByteBufUtil.decodeHexDump(hexDump));
+        assertTrue(encoderChannel.writeOutbound(originPayload.duplicate().retain()));
+
+        ByteBuf compressedPayload = encoderChannel.readOutbound();
+        compressedPayload = compressedPayload.slice(0, compressedPayload.readableBytes() - 4);
+
+        int oneThird = compressedPayload.readableBytes() / 3;
+
+        TextWebSocketFrame compressedFrame1 = new TextWebSocketFrame(
+                false, WebSocketExtension.RSV1, compressedPayload.slice(0, oneThird));
+        ContinuationWebSocketFrame compressedFrame2 = new ContinuationWebSocketFrame(
+                false, WebSocketExtension.RSV3, compressedPayload.slice(oneThird, oneThird));
+        ContinuationWebSocketFrame compressedFrame3 = new ContinuationWebSocketFrame(
+                false, WebSocketExtension.RSV3, compressedPayload.slice(oneThird * 2, oneThird));
+        int offset = oneThird * 3;
+        ContinuationWebSocketFrame compressedFrameWithExtraData = new ContinuationWebSocketFrame(
+                true, WebSocketExtension.RSV3, compressedPayload.slice(offset,
+                     compressedPayload.readableBytes() - offset));
+
+        // check that last fragment contains only one extra byte
+        assertEquals(1, compressedFrameWithExtraData.content().readableBytes());
+        assertEquals(1, compressedFrameWithExtraData.content().getByte(0));
+
+        // write compressed frames
+        assertTrue(decoderChannel.writeInbound(compressedFrame1.retain()));
+        assertTrue(decoderChannel.writeInbound(compressedFrame2.retain()));
+        assertTrue(decoderChannel.writeInbound(compressedFrame3.retain()));
+        assertTrue(decoderChannel.writeInbound(compressedFrameWithExtraData));
+
+        // read uncompressed frames
+        TextWebSocketFrame uncompressedFrame1 = decoderChannel.readInbound();
+        ContinuationWebSocketFrame uncompressedFrame2 = decoderChannel.readInbound();
+        ContinuationWebSocketFrame uncompressedFrame3 = decoderChannel.readInbound();
+        ContinuationWebSocketFrame uncompressedExtraData = decoderChannel.readInbound();
+        assertFalse(uncompressedExtraData.content().isReadable());
+
+        ByteBuf uncompressedPayload = Unpooled.wrappedBuffer(uncompressedFrame1.content(), uncompressedFrame2.content(),
+                                      uncompressedFrame3.content(), uncompressedExtraData.content());
+        assertEquals(originPayload, uncompressedPayload);
+
+        assertTrue(originPayload.release());
+        assertTrue(uncompressedPayload.release());
+
+        assertTrue(encoderChannel.finishAndReleaseAll());
+        assertFalse(decoderChannel.finish());
+    }
 }
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateEncoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateEncoderTest.java
index 66ae962..1f8b477 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateEncoderTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/websocketx/extensions/compression/PerMessageDeflateEncoderTest.java
@@ -15,20 +15,28 @@
  */
 package io.netty.handler.codec.http.websocketx.extensions.compression;
 
-import static org.junit.Assert.*;
 import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
 import io.netty.buffer.Unpooled;
 import io.netty.channel.embedded.EmbeddedChannel;
+import io.netty.handler.codec.EncoderException;
 import io.netty.handler.codec.compression.ZlibCodecFactory;
 import io.netty.handler.codec.compression.ZlibWrapper;
 import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
 import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.WebSocketFrame;
 import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtension;
+import io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionFilter;
+import org.junit.Test;
 
 import java.util.Arrays;
 import java.util.Random;
 
-import org.junit.Test;
+import static io.netty.handler.codec.http.websocketx.extensions.WebSocketExtensionFilter.*;
+import static io.netty.handler.codec.http.websocketx.extensions.compression.DeflateDecoder.*;
+import static io.netty.util.CharsetUtil.*;
+import static org.junit.Assert.*;
 
 public class PerMessageDeflateEncoderTest {
 
@@ -44,26 +52,25 @@ public class PerMessageDeflateEncoderTest {
         byte[] payload = new byte[300];
         random.nextBytes(payload);
         BinaryWebSocketFrame frame = new BinaryWebSocketFrame(true,
-                WebSocketExtension.RSV3, Unpooled.wrappedBuffer(payload));
+                                                              WebSocketExtension.RSV3, Unpooled.wrappedBuffer(payload));
 
         // execute
-        encoderChannel.writeOutbound(frame);
+        assertTrue(encoderChannel.writeOutbound(frame));
         BinaryWebSocketFrame compressedFrame = encoderChannel.readOutbound();
 
         // test
         assertNotNull(compressedFrame);
         assertNotNull(compressedFrame.content());
-        assertTrue(compressedFrame instanceof BinaryWebSocketFrame);
         assertEquals(WebSocketExtension.RSV1 | WebSocketExtension.RSV3, compressedFrame.rsv());
 
-        decoderChannel.writeInbound(compressedFrame.content());
-        decoderChannel.writeInbound(DeflateDecoder.FRAME_TAIL);
+        assertTrue(decoderChannel.writeInbound(compressedFrame.content()));
+        assertTrue(decoderChannel.writeInbound(DeflateDecoder.FRAME_TAIL.duplicate()));
         ByteBuf uncompressedPayload = decoderChannel.readInbound();
         assertEquals(300, uncompressedPayload.readableBytes());
 
         byte[] finalPayload = new byte[300];
         uncompressedPayload.readBytes(finalPayload);
-        assertTrue(Arrays.equals(finalPayload, payload));
+        assertArrayEquals(finalPayload, payload);
         uncompressedPayload.release();
     }
 
@@ -76,28 +83,29 @@ public class PerMessageDeflateEncoderTest {
         random.nextBytes(payload);
 
         BinaryWebSocketFrame frame = new BinaryWebSocketFrame(true,
-                WebSocketExtension.RSV3 | WebSocketExtension.RSV1, Unpooled.wrappedBuffer(payload));
+                                                              WebSocketExtension.RSV3 | WebSocketExtension.RSV1,
+                                                              Unpooled.wrappedBuffer(payload));
 
         // execute
-        encoderChannel.writeOutbound(frame);
+        assertTrue(encoderChannel.writeOutbound(frame));
         BinaryWebSocketFrame newFrame = encoderChannel.readOutbound();
 
         // test
         assertNotNull(newFrame);
         assertNotNull(newFrame.content());
-        assertTrue(newFrame instanceof BinaryWebSocketFrame);
         assertEquals(WebSocketExtension.RSV3 | WebSocketExtension.RSV1, newFrame.rsv());
         assertEquals(300, newFrame.content().readableBytes());
 
         byte[] finalPayload = new byte[300];
         newFrame.content().readBytes(finalPayload);
-        assertTrue(Arrays.equals(finalPayload, payload));
+        assertArrayEquals(finalPayload, payload);
         newFrame.release();
     }
 
     @Test
-    public void testFramementedFrame() {
-        EmbeddedChannel encoderChannel = new EmbeddedChannel(new PerMessageDeflateEncoder(9, 15, false));
+    public void testFragmentedFrame() {
+        EmbeddedChannel encoderChannel = new EmbeddedChannel(new PerMessageDeflateEncoder(9, 15, false,
+                                                                                          NEVER_SKIP));
         EmbeddedChannel decoderChannel = new EmbeddedChannel(
                 ZlibCodecFactory.newZlibDecoder(ZlibWrapper.NONE));
 
@@ -110,16 +118,19 @@ public class PerMessageDeflateEncoderTest {
         random.nextBytes(payload3);
 
         BinaryWebSocketFrame frame1 = new BinaryWebSocketFrame(false,
-                WebSocketExtension.RSV3, Unpooled.wrappedBuffer(payload1));
+                                                               WebSocketExtension.RSV3,
+                                                               Unpooled.wrappedBuffer(payload1));
         ContinuationWebSocketFrame frame2 = new ContinuationWebSocketFrame(false,
-                WebSocketExtension.RSV3, Unpooled.wrappedBuffer(payload2));
+                                                                           WebSocketExtension.RSV3,
+                                                                           Unpooled.wrappedBuffer(payload2));
         ContinuationWebSocketFrame frame3 = new ContinuationWebSocketFrame(true,
-                WebSocketExtension.RSV3, Unpooled.wrappedBuffer(payload3));
+                                                                           WebSocketExtension.RSV3,
+                                                                           Unpooled.wrappedBuffer(payload3));
 
         // execute
-        encoderChannel.writeOutbound(frame1);
-        encoderChannel.writeOutbound(frame2);
-        encoderChannel.writeOutbound(frame3);
+        assertTrue(encoderChannel.writeOutbound(frame1));
+        assertTrue(encoderChannel.writeOutbound(frame2));
+        assertTrue(encoderChannel.writeOutbound(frame3));
         BinaryWebSocketFrame compressedFrame1 = encoderChannel.readOutbound();
         ContinuationWebSocketFrame compressedFrame2 = encoderChannel.readOutbound();
         ContinuationWebSocketFrame compressedFrame3 = encoderChannel.readOutbound();
@@ -135,26 +146,163 @@ public class PerMessageDeflateEncoderTest {
         assertFalse(compressedFrame2.isFinalFragment());
         assertTrue(compressedFrame3.isFinalFragment());
 
-        decoderChannel.writeInbound(compressedFrame1.content());
+        assertTrue(decoderChannel.writeInbound(compressedFrame1.content()));
         ByteBuf uncompressedPayload1 = decoderChannel.readInbound();
         byte[] finalPayload1 = new byte[100];
         uncompressedPayload1.readBytes(finalPayload1);
-        assertTrue(Arrays.equals(finalPayload1, payload1));
+        assertArrayEquals(finalPayload1, payload1);
         uncompressedPayload1.release();
 
-        decoderChannel.writeInbound(compressedFrame2.content());
+        assertTrue(decoderChannel.writeInbound(compressedFrame2.content()));
         ByteBuf uncompressedPayload2 = decoderChannel.readInbound();
         byte[] finalPayload2 = new byte[100];
         uncompressedPayload2.readBytes(finalPayload2);
-        assertTrue(Arrays.equals(finalPayload2, payload2));
+        assertArrayEquals(finalPayload2, payload2);
         uncompressedPayload2.release();
 
-        decoderChannel.writeInbound(compressedFrame3.content());
-        decoderChannel.writeInbound(DeflateDecoder.FRAME_TAIL);
+        assertTrue(decoderChannel.writeInbound(compressedFrame3.content()));
+        assertTrue(decoderChannel.writeInbound(DeflateDecoder.FRAME_TAIL.duplicate()));
         ByteBuf uncompressedPayload3 = decoderChannel.readInbound();
         byte[] finalPayload3 = new byte[100];
         uncompressedPayload3.readBytes(finalPayload3);
-        assertTrue(Arrays.equals(finalPayload3, payload3));
+        assertArrayEquals(finalPayload3, payload3);
         uncompressedPayload3.release();
     }
+
+    @Test
+    public void testCompressionSkipForBinaryFrame() {
+        EmbeddedChannel encoderChannel = new EmbeddedChannel(new PerMessageDeflateEncoder(9, 15, false,
+                                                                                          ALWAYS_SKIP));
+        byte[] payload = new byte[300];
+        random.nextBytes(payload);
+
+        WebSocketFrame binaryFrame = new BinaryWebSocketFrame(Unpooled.wrappedBuffer(payload));
+
+        assertTrue(encoderChannel.writeOutbound(binaryFrame.copy()));
+        WebSocketFrame outboundFrame = encoderChannel.readOutbound();
+
+        assertEquals(0, outboundFrame.rsv());
+        assertArrayEquals(payload, ByteBufUtil.getBytes(outboundFrame.content()));
+        assertTrue(outboundFrame.release());
+
+        assertFalse(encoderChannel.finish());
+    }
+
+    @Test
+    public void testSelectivityCompressionSkip() {
+        WebSocketExtensionFilter selectivityCompressionFilter = new WebSocketExtensionFilter() {
+            @Override
+            public boolean mustSkip(WebSocketFrame frame) {
+                return  (frame instanceof TextWebSocketFrame || frame instanceof BinaryWebSocketFrame)
+                    && frame.content().readableBytes() < 100;
+            }
+        };
+        EmbeddedChannel encoderChannel = new EmbeddedChannel(
+                new PerMessageDeflateEncoder(9, 15, false, selectivityCompressionFilter));
+        EmbeddedChannel decoderChannel = new EmbeddedChannel(
+                ZlibCodecFactory.newZlibDecoder(ZlibWrapper.NONE));
+
+        String textPayload = "not compressed payload";
+        byte[] binaryPayload = new byte[101];
+        random.nextBytes(binaryPayload);
+
+        WebSocketFrame textFrame = new TextWebSocketFrame(textPayload);
+        BinaryWebSocketFrame binaryFrame = new BinaryWebSocketFrame(Unpooled.wrappedBuffer(binaryPayload));
+
+        assertTrue(encoderChannel.writeOutbound(textFrame));
+        assertTrue(encoderChannel.writeOutbound(binaryFrame));
+
+        WebSocketFrame outboundTextFrame = encoderChannel.readOutbound();
+
+        //compression skipped for textFrame
+        assertEquals(0, outboundTextFrame.rsv());
+        assertEquals(textPayload, outboundTextFrame.content().toString(UTF_8));
+        assertTrue(outboundTextFrame.release());
+
+        WebSocketFrame outboundBinaryFrame = encoderChannel.readOutbound();
+
+        //compression not skipped for binaryFrame
+        assertEquals(WebSocketExtension.RSV1, outboundBinaryFrame.rsv());
+
+        assertTrue(decoderChannel.writeInbound(outboundBinaryFrame.content().retain()));
+        ByteBuf uncompressedBinaryPayload = decoderChannel.readInbound();
+
+        assertArrayEquals(binaryPayload, ByteBufUtil.getBytes(uncompressedBinaryPayload));
+
+        assertTrue(outboundBinaryFrame.release());
+        assertTrue(uncompressedBinaryPayload.release());
+
+        assertFalse(encoderChannel.finish());
+        assertFalse(decoderChannel.finish());
+    }
+
+    @Test(expected = EncoderException.class)
+    public void testIllegalStateWhenCompressionInProgress() {
+        WebSocketExtensionFilter selectivityCompressionFilter = new WebSocketExtensionFilter() {
+            @Override
+            public boolean mustSkip(WebSocketFrame frame) {
+                return frame.content().readableBytes() < 100;
+            }
+        };
+        EmbeddedChannel encoderChannel = new EmbeddedChannel(
+                new PerMessageDeflateEncoder(9, 15, false, selectivityCompressionFilter));
+
+        byte[] firstPayload = new byte[200];
+        random.nextBytes(firstPayload);
+
+        byte[] finalPayload = new byte[90];
+        random.nextBytes(finalPayload);
+
+        BinaryWebSocketFrame firstPart = new BinaryWebSocketFrame(false, 0, Unpooled.wrappedBuffer(firstPayload));
+        ContinuationWebSocketFrame finalPart = new ContinuationWebSocketFrame(true, 0,
+                                                                              Unpooled.wrappedBuffer(finalPayload));
+        assertTrue(encoderChannel.writeOutbound(firstPart));
+
+        BinaryWebSocketFrame outboundFirstPart = encoderChannel.readOutbound();
+        //first part is compressed
+        assertEquals(WebSocketExtension.RSV1, outboundFirstPart.rsv());
+        assertFalse(Arrays.equals(firstPayload, ByteBufUtil.getBytes(outboundFirstPart.content())));
+        assertTrue(outboundFirstPart.release());
+
+        //final part throwing exception
+        try {
+            encoderChannel.writeOutbound(finalPart);
+        } finally {
+            assertTrue(finalPart.release());
+            assertFalse(encoderChannel.finishAndReleaseAll());
+        }
+    }
+
+    @Test
+    public void testEmptyFrameCompression() {
+        EmbeddedChannel encoderChannel = new EmbeddedChannel(new PerMessageDeflateEncoder(9, 15, false));
+
+        TextWebSocketFrame emptyFrame = new TextWebSocketFrame("");
+
+        assertTrue(encoderChannel.writeOutbound(emptyFrame));
+        TextWebSocketFrame emptyDeflateFrame = encoderChannel.readOutbound();
+
+        assertEquals(WebSocketExtension.RSV1, emptyDeflateFrame.rsv());
+        assertTrue(ByteBufUtil.equals(EMPTY_DEFLATE_BLOCK, emptyDeflateFrame.content()));
+        // Unreleasable buffer
+        assertFalse(emptyDeflateFrame.release());
+
+        assertFalse(encoderChannel.finish());
+    }
+
+    @Test(expected = EncoderException.class)
+    public void testCodecExceptionForNotFinEmptyFrame() {
+        EmbeddedChannel encoderChannel = new EmbeddedChannel(new PerMessageDeflateEncoder(9, 15, false));
+
+        TextWebSocketFrame emptyNotFinFrame = new TextWebSocketFrame(false, 0, "");
+
+        try {
+            encoderChannel.writeOutbound(emptyNotFinFrame);
+        } finally {
+            // EmptyByteBuf buffer
+            assertFalse(emptyNotFinFrame.release());
+            assertFalse(encoderChannel.finish());
+        }
+    }
+
 }
diff --git a/codec-http/src/test/java/io/netty/handler/codec/rtsp/RtspDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/rtsp/RtspDecoderTest.java
index d416720..631ba2a 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/rtsp/RtspDecoderTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/rtsp/RtspDecoderTest.java
@@ -65,7 +65,6 @@ public class RtspDecoderTest {
         ((FullHttpRequest) res1).release();
 
         HttpObject res2 = ch.readInbound();
-        System.out.println(res2);
         assertNotNull(res2);
         assertTrue(res2 instanceof FullHttpResponse);
         ((FullHttpResponse) res2).release();
diff --git a/codec-http/src/test/resources/file-03.txt b/codec-http/src/test/resources/file-03.txt
new file mode 100644
index 0000000..b545f1b
--- /dev/null
+++ b/codec-http/src/test/resources/file-03.txt
@@ -0,0 +1 @@
+File 03
diff --git a/codec-http2/pom.xml b/codec-http2/pom.xml
index 695f3b8..1818182 100644
--- a/codec-http2/pom.xml
+++ b/codec-http2/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>io.netty</groupId>
     <artifactId>netty-parent</artifactId>
-    <version>4.1.33.Final</version>
+    <version>4.1.48.Final</version>
   </parent>
 
   <artifactId>netty-codec-http2</artifactId>
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 7c52cd2..f262b11 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
@@ -16,17 +16,16 @@
 
 package io.netty.handler.codec.http2;
 
+import io.netty.channel.Channel;
 import io.netty.handler.codec.http2.Http2HeadersEncoder.SensitivityDetector;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.UnstableApi;
 
 import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_HEADER_LIST_SIZE;
-import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_INITIAL_HUFFMAN_DECODE_CAPACITY;
 import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_MAX_RESERVED_STREAMS;
+import static io.netty.handler.codec.http2.Http2PromisedRequestVerifier.ALWAYS_VERIFY;
 import static io.netty.util.internal.ObjectUtil.checkNotNull;
-import static io.netty.util.internal.ObjectUtil.checkPositive;
 import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
-import static java.util.concurrent.TimeUnit.SECONDS;
 
 /**
  * Abstract base class which defines commonly used features required to build {@link Http2ConnectionHandler} instances.
@@ -64,7 +63,6 @@ import static java.util.concurrent.TimeUnit.SECONDS;
  *   <li>{@link #headerSensitivityDetector(SensitivityDetector)}</li>
  *   <li>{@link #encoderEnforceMaxConcurrentStreams(boolean)}</li>
  *   <li>{@link #encoderIgnoreMaxHeaderListSize(boolean)}</li>
- *   <li>{@link #initialHuffmanDecodeCapacity(int)}</li>
  * </ul>
  *
  * <h3>Exposing necessary methods in a subclass</h3>
@@ -84,9 +82,10 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2Conne
     private Http2Settings initialSettings = Http2Settings.defaultSettings();
     private Http2FrameListener frameListener;
     private long gracefulShutdownTimeoutMillis = Http2CodecUtil.DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT_MILLIS;
+    private boolean decoupleCloseAndGoAway;
 
     // The property that will prohibit connection() and codec() if set by server(),
-    // because this property is used only when this builder creates a Http2Connection.
+    // because this property is used only when this builder creates an Http2Connection.
     private Boolean isServer;
     private Integer maxReservedStreams;
 
@@ -105,7 +104,11 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2Conne
     private SensitivityDetector headerSensitivityDetector;
     private Boolean encoderEnforceMaxConcurrentStreams;
     private Boolean encoderIgnoreMaxHeaderListSize;
-    private int initialHuffmanDecodeCapacity = DEFAULT_INITIAL_HUFFMAN_DECODE_CAPACITY;
+    private Http2PromisedRequestVerifier promisedRequestVerifier = ALWAYS_VERIFY;
+    private boolean autoAckSettingsFrame = true;
+    private boolean autoAckPingFrame = true;
+    private int maxQueuedControlFrames = Http2CodecUtil.DEFAULT_MAX_QUEUED_CONTROL_FRAMES;
+    private int maxConsecutiveEmptyFrames = 2;
 
     /**
      * Sets the {@link Http2Settings} to use for the initial connection settings exchange.
@@ -324,6 +327,30 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2Conne
         return self();
     }
 
+    /**
+     * Returns the maximum number of queued control frames that are allowed before the connection is closed.
+     * This allows to protected against various attacks that can lead to high CPU / memory usage if the remote-peer
+     * floods us with frames that would have us produce control frames, but stops to read from the underlying socket.
+     *
+     * {@code 0} means no protection is in place.
+     */
+    protected int encoderEnforceMaxQueuedControlFrames() {
+        return maxQueuedControlFrames;
+    }
+
+    /**
+     * Sets the maximum number of queued control frames that are allowed before the connection is closed.
+     * This allows to protected against various attacks that can lead to high CPU / memory usage if the remote-peer
+     * floods us with frames that would have us produce control frames, but stops to read from the underlying socket.
+     *
+     * {@code 0} means no protection should be applied.
+     */
+    protected B encoderEnforceMaxQueuedControlFrames(int maxQueuedControlFrames) {
+        enforceNonCodecConstraints("encoderEnforceMaxQueuedControlFrames");
+        this.maxQueuedControlFrames = ObjectUtil.checkPositiveOrZero(maxQueuedControlFrames, "maxQueuedControlFrames");
+        return self();
+    }
+
     /**
      * Returns the {@link SensitivityDetector} to use.
      */
@@ -354,16 +381,112 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2Conne
     }
 
     /**
-     * Sets the initial size of an intermediate buffer used during HPACK huffman decoding.
-     * @param initialHuffmanDecodeCapacity initial size of an intermediate buffer used during HPACK huffman decoding.
-     * @return this.
+     * Does nothing, do not call.
+     *
+     * @deprecated Huffman decoding no longer depends on having a decode capacity.
      */
+    @Deprecated
     protected B initialHuffmanDecodeCapacity(int initialHuffmanDecodeCapacity) {
-        enforceNonCodecConstraints("initialHuffmanDecodeCapacity");
-        this.initialHuffmanDecodeCapacity = checkPositive(initialHuffmanDecodeCapacity, "initialHuffmanDecodeCapacity");
         return self();
     }
 
+    /**
+     * Set the {@link Http2PromisedRequestVerifier} to use.
+     * @return this.
+     */
+    protected B promisedRequestVerifier(Http2PromisedRequestVerifier promisedRequestVerifier) {
+        enforceNonCodecConstraints("promisedRequestVerifier");
+        this.promisedRequestVerifier = checkNotNull(promisedRequestVerifier, "promisedRequestVerifier");
+        return self();
+    }
+
+    /**
+     * Get the {@link Http2PromisedRequestVerifier} to use.
+     * @return the {@link Http2PromisedRequestVerifier} to use.
+     */
+    protected Http2PromisedRequestVerifier promisedRequestVerifier() {
+        return promisedRequestVerifier;
+    }
+
+    /**
+     * Returns the maximum number of consecutive empty DATA frames (without end_of_stream flag) that are allowed before
+     * the connection is closed. This allows to protected against the remote peer flooding us with such frames and
+     * so use up a lot of CPU. There is no valid use-case for empty DATA frames without end_of_stream flag.
+     *
+     * {@code 0} means no protection is in place.
+     */
+    protected int decoderEnforceMaxConsecutiveEmptyDataFrames() {
+        return maxConsecutiveEmptyFrames;
+    }
+
+    /**
+     * Sets the maximum number of consecutive empty DATA frames (without end_of_stream flag) that are allowed before
+     * the connection is closed. This allows to protected against the remote peer flooding us with such frames and
+     * so use up a lot of CPU. There is no valid use-case for empty DATA frames without end_of_stream flag.
+     *
+     * {@code 0} means no protection should be applied.
+     */
+    protected B decoderEnforceMaxConsecutiveEmptyDataFrames(int maxConsecutiveEmptyFrames) {
+        enforceNonCodecConstraints("maxConsecutiveEmptyFrames");
+        this.maxConsecutiveEmptyFrames = ObjectUtil.checkPositiveOrZero(
+                maxConsecutiveEmptyFrames, "maxConsecutiveEmptyFrames");
+        return self();
+    }
+
+    /**
+     * Determine if settings frame should automatically be acknowledged and applied.
+     * @return this.
+     */
+    protected B autoAckSettingsFrame(boolean autoAckSettings) {
+        enforceNonCodecConstraints("autoAckSettingsFrame");
+        this.autoAckSettingsFrame = autoAckSettings;
+        return self();
+    }
+
+    /**
+     * Determine if the SETTINGS frames should be automatically acknowledged and applied.
+     * @return {@code true} if the SETTINGS frames should be automatically acknowledged and applied.
+     */
+    protected boolean isAutoAckSettingsFrame() {
+        return autoAckSettingsFrame;
+    }
+
+    /**
+     * Determine if PING frame should automatically be acknowledged or not.
+     * @return this.
+     */
+    protected B autoAckPingFrame(boolean autoAckPingFrame) {
+        enforceNonCodecConstraints("autoAckPingFrame");
+        this.autoAckPingFrame = autoAckPingFrame;
+        return self();
+    }
+
+    /**
+     * Determine if the PING frames should be automatically acknowledged or not.
+     * @return {@code true} if the PING frames should be automatically acknowledged.
+     */
+    protected boolean isAutoAckPingFrame() {
+        return autoAckPingFrame;
+    }
+
+    /**
+     * Determine if the {@link Channel#close()} should be coupled with goaway and graceful close.
+     * @param decoupleCloseAndGoAway {@code true} to make {@link Channel#close()} directly close the underlying
+     *   transport, and not attempt graceful closure via GOAWAY.
+     * @return {@code this}.
+     */
+    protected B decoupleCloseAndGoAway(boolean decoupleCloseAndGoAway) {
+        this.decoupleCloseAndGoAway = decoupleCloseAndGoAway;
+        return self();
+    }
+
+    /**
+     * Determine if the {@link Channel#close()} should be coupled with goaway and graceful close.
+     */
+    protected boolean decoupleCloseAndGoAway() {
+        return decoupleCloseAndGoAway;
+    }
+
     /**
      * Create a new {@link Http2ConnectionHandler}.
      */
@@ -385,7 +508,7 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2Conne
         Long maxHeaderListSize = initialSettings.maxHeaderListSize();
         Http2FrameReader reader = new DefaultHttp2FrameReader(new DefaultHttp2HeadersDecoder(isValidateHeaders(),
                 maxHeaderListSize == null ? DEFAULT_HEADER_LIST_SIZE : maxHeaderListSize,
-                initialHuffmanDecodeCapacity));
+                /* initialHuffmanDecodeCapacity= */ -1));
         Http2FrameWriter writer = encoderIgnoreMaxHeaderListSize == null ?
                 new DefaultHttp2FrameWriter(headerSensitivityDetector()) :
                 new DefaultHttp2FrameWriter(headerSensitivityDetector(), encoderIgnoreMaxHeaderListSize);
@@ -398,6 +521,9 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2Conne
         Http2ConnectionEncoder encoder = new DefaultHttp2ConnectionEncoder(connection, writer);
         boolean encoderEnforceMaxConcurrentStreams = encoderEnforceMaxConcurrentStreams();
 
+        if (maxQueuedControlFrames != 0) {
+            encoder = new Http2ControlFrameLimitEncoder(encoder, maxQueuedControlFrames);
+        }
         if (encoderEnforceMaxConcurrentStreams) {
             if (connection.isServer()) {
                 encoder.close();
@@ -409,11 +535,16 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2Conne
             encoder = new StreamBufferingEncoder(encoder);
         }
 
-        Http2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, reader);
+        DefaultHttp2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, reader,
+                promisedRequestVerifier(), isAutoAckSettingsFrame(), isAutoAckPingFrame());
         return buildFromCodec(decoder, encoder);
     }
 
     private T buildFromCodec(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder) {
+        int maxConsecutiveEmptyDataFrames = decoderEnforceMaxConsecutiveEmptyDataFrames();
+        if (maxConsecutiveEmptyDataFrames > 0) {
+            decoder = new Http2EmptyDataFrameConnectionDecoder(decoder, maxConsecutiveEmptyDataFrames);
+        }
         final T handler;
         try {
             // Call the abstract build method
@@ -421,7 +552,7 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2Conne
         } catch (Throwable t) {
             encoder.close();
             decoder.close();
-            throw new IllegalStateException("failed to build a Http2ConnectionHandler", t);
+            throw new IllegalStateException("failed to build an Http2ConnectionHandler", t);
         }
 
         // Setup post build options
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2StreamChannel.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2StreamChannel.java
new file mode 100644
index 0000000..92abf19
--- /dev/null
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2StreamChannel.java
@@ -0,0 +1,1106 @@
+/*
+ * Copyright 2019 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.buffer.ByteBufAllocator;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelConfig;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelId;
+import io.netty.channel.ChannelMetadata;
+import io.netty.channel.ChannelOutboundBuffer;
+import io.netty.channel.ChannelPipeline;
+import io.netty.channel.ChannelProgressivePromise;
+import io.netty.channel.ChannelPromise;
+import io.netty.channel.DefaultChannelConfig;
+import io.netty.channel.DefaultChannelPipeline;
+import io.netty.channel.EventLoop;
+import io.netty.channel.MessageSizeEstimator;
+import io.netty.channel.RecvByteBufAllocator;
+import io.netty.channel.VoidChannelPromise;
+import io.netty.channel.WriteBufferWaterMark;
+import io.netty.handler.codec.http2.Http2FrameCodec.DefaultHttp2FrameStream;
+import io.netty.util.DefaultAttributeMap;
+import io.netty.util.ReferenceCountUtil;
+import io.netty.util.internal.StringUtil;
+import io.netty.util.internal.logging.InternalLogger;
+import io.netty.util.internal.logging.InternalLoggerFactory;
+
+import java.io.IOException;
+import java.net.SocketAddress;
+import java.nio.channels.ClosedChannelException;
+import java.util.ArrayDeque;
+import java.util.Queue;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
+import java.util.concurrent.atomic.AtomicLongFieldUpdater;
+
+import static io.netty.handler.codec.http2.Http2CodecUtil.isStreamIdValid;
+import static java.lang.Math.min;
+
+abstract class AbstractHttp2StreamChannel extends DefaultAttributeMap implements Http2StreamChannel {
+
+    static final Http2FrameStreamVisitor WRITABLE_VISITOR = new Http2FrameStreamVisitor() {
+        @Override
+        public boolean visit(Http2FrameStream stream) {
+            final AbstractHttp2StreamChannel childChannel = (AbstractHttp2StreamChannel)
+                    ((DefaultHttp2FrameStream) stream).attachment;
+            childChannel.trySetWritable();
+            return true;
+        }
+    };
+
+    private static final InternalLogger logger = InternalLoggerFactory.getInstance(AbstractHttp2StreamChannel.class);
+
+    private static final ChannelMetadata METADATA = new ChannelMetadata(false, 16);
+
+    /**
+     * Number of bytes to consider non-payload messages. 9 is arbitrary, but also the minimum size of an HTTP/2 frame.
+     * Primarily is non-zero.
+     */
+    private static final int MIN_HTTP2_FRAME_SIZE = 9;
+
+    /**
+     * Returns the flow-control size for DATA frames, and {@value MIN_HTTP2_FRAME_SIZE} for all other frames.
+     */
+    private static final class FlowControlledFrameSizeEstimator implements MessageSizeEstimator {
+
+        static final FlowControlledFrameSizeEstimator INSTANCE = new FlowControlledFrameSizeEstimator();
+
+        private static final Handle HANDLE_INSTANCE = new Handle() {
+            @Override
+            public int size(Object msg) {
+                return msg instanceof Http2DataFrame ?
+                        // Guard against overflow.
+                        (int) min(Integer.MAX_VALUE, ((Http2DataFrame) msg).initialFlowControlledBytes() +
+                                (long) MIN_HTTP2_FRAME_SIZE) : MIN_HTTP2_FRAME_SIZE;
+            }
+        };
+
+        @Override
+        public Handle newHandle() {
+            return HANDLE_INSTANCE;
+        }
+    }
+
+    private static final AtomicLongFieldUpdater<AbstractHttp2StreamChannel> TOTAL_PENDING_SIZE_UPDATER =
+            AtomicLongFieldUpdater.newUpdater(AbstractHttp2StreamChannel.class, "totalPendingSize");
+
+    private static final AtomicIntegerFieldUpdater<AbstractHttp2StreamChannel> UNWRITABLE_UPDATER =
+            AtomicIntegerFieldUpdater.newUpdater(AbstractHttp2StreamChannel.class, "unwritable");
+
+    private static void windowUpdateFrameWriteComplete(ChannelFuture future, Channel streamChannel) {
+        Throwable cause = future.cause();
+        if (cause != null) {
+            Throwable unwrappedCause;
+            // Unwrap if needed
+            if (cause instanceof Http2FrameStreamException && ((unwrappedCause = cause.getCause()) != null)) {
+                cause = unwrappedCause;
+            }
+
+            // Notify the child-channel and close it.
+            streamChannel.pipeline().fireExceptionCaught(cause);
+            streamChannel.unsafe().close(streamChannel.unsafe().voidPromise());
+        }
+    }
+
+    private final ChannelFutureListener windowUpdateFrameWriteListener = new ChannelFutureListener() {
+        @Override
+        public void operationComplete(ChannelFuture future) {
+            windowUpdateFrameWriteComplete(future, AbstractHttp2StreamChannel.this);
+        }
+    };
+
+    /**
+     * The current status of the read-processing for a {@link AbstractHttp2StreamChannel}.
+     */
+    private enum ReadStatus {
+        /**
+         * No read in progress and no read was requested (yet)
+         */
+        IDLE,
+
+        /**
+         * Reading in progress
+         */
+        IN_PROGRESS,
+
+        /**
+         * A read operation was requested.
+         */
+        REQUESTED
+    }
+
+    private final AbstractHttp2StreamChannel.Http2StreamChannelConfig config = new Http2StreamChannelConfig(this);
+    private final AbstractHttp2StreamChannel.Http2ChannelUnsafe unsafe = new Http2ChannelUnsafe();
+    private final ChannelId channelId;
+    private final ChannelPipeline pipeline;
+    private final DefaultHttp2FrameStream stream;
+    private final ChannelPromise closePromise;
+
+    private volatile boolean registered;
+
+    private volatile long totalPendingSize;
+    private volatile int unwritable;
+
+    // Cached to reduce GC
+    private Runnable fireChannelWritabilityChangedTask;
+
+    private boolean outboundClosed;
+    private int flowControlledBytes;
+
+    /**
+     * This variable represents if a read is in progress for the current channel or was requested.
+     * Note that depending upon the {@link RecvByteBufAllocator} behavior a read may extend beyond the
+     * {@link Http2ChannelUnsafe#beginRead()} method scope. The {@link Http2ChannelUnsafe#beginRead()} loop may
+     * drain all pending data, and then if the parent channel is reading this channel may still accept frames.
+     */
+    private ReadStatus readStatus = ReadStatus.IDLE;
+
+    private Queue<Object> inboundBuffer;
+
+    /** {@code true} after the first HEADERS frame has been written **/
+    private boolean firstFrameWritten;
+    private boolean readCompletePending;
+
+    AbstractHttp2StreamChannel(DefaultHttp2FrameStream stream, int id, ChannelHandler inboundHandler) {
+        this.stream = stream;
+        stream.attachment = this;
+        pipeline = new DefaultChannelPipeline(this) {
+            @Override
+            protected void incrementPendingOutboundBytes(long size) {
+                AbstractHttp2StreamChannel.this.incrementPendingOutboundBytes(size, true);
+            }
+
+            @Override
+            protected void decrementPendingOutboundBytes(long size) {
+                AbstractHttp2StreamChannel.this.decrementPendingOutboundBytes(size, true);
+            }
+        };
+
+        closePromise = pipeline.newPromise();
+        channelId = new Http2StreamChannelId(parent().id(), id);
+
+        if (inboundHandler != null) {
+            // Add the handler to the pipeline now that we are registered.
+            pipeline.addLast(inboundHandler);
+        }
+    }
+
+    private void incrementPendingOutboundBytes(long size, boolean invokeLater) {
+        if (size == 0) {
+            return;
+        }
+
+        long newWriteBufferSize = TOTAL_PENDING_SIZE_UPDATER.addAndGet(this, size);
+        if (newWriteBufferSize > config().getWriteBufferHighWaterMark()) {
+            setUnwritable(invokeLater);
+        }
+    }
+
+    private void decrementPendingOutboundBytes(long size, boolean invokeLater) {
+        if (size == 0) {
+            return;
+        }
+
+        long newWriteBufferSize = TOTAL_PENDING_SIZE_UPDATER.addAndGet(this, -size);
+        // Once the totalPendingSize dropped below the low water-mark we can mark the child channel
+        // as writable again. Before doing so we also need to ensure the parent channel is writable to
+        // prevent excessive buffering in the parent outbound buffer. If the parent is not writable
+        // we will mark the child channel as writable once the parent becomes writable by calling
+        // trySetWritable() later.
+        if (newWriteBufferSize < config().getWriteBufferLowWaterMark() && parent().isWritable()) {
+            setWritable(invokeLater);
+        }
+    }
+
+    final void trySetWritable() {
+        // The parent is writable again but the child channel itself may still not be writable.
+        // Lets try to set the child channel writable to match the state of the parent channel
+        // if (and only if) the totalPendingSize is smaller then the low water-mark.
+        // If this is not the case we will try again later once we drop under it.
+        if (totalPendingSize < config().getWriteBufferLowWaterMark()) {
+            setWritable(false);
+        }
+    }
+
+    private void setWritable(boolean invokeLater) {
+        for (;;) {
+            final int oldValue = unwritable;
+            final int newValue = oldValue & ~1;
+            if (UNWRITABLE_UPDATER.compareAndSet(this, oldValue, newValue)) {
+                if (oldValue != 0 && newValue == 0) {
+                    fireChannelWritabilityChanged(invokeLater);
+                }
+                break;
+            }
+        }
+    }
+
+    private void setUnwritable(boolean invokeLater) {
+        for (;;) {
+            final int oldValue = unwritable;
+            final int newValue = oldValue | 1;
+            if (UNWRITABLE_UPDATER.compareAndSet(this, oldValue, newValue)) {
+                if (oldValue == 0 && newValue != 0) {
+                    fireChannelWritabilityChanged(invokeLater);
+                }
+                break;
+            }
+        }
+    }
+
+    private void fireChannelWritabilityChanged(boolean invokeLater) {
+        final ChannelPipeline pipeline = pipeline();
+        if (invokeLater) {
+            Runnable task = fireChannelWritabilityChangedTask;
+            if (task == null) {
+                fireChannelWritabilityChangedTask = task = new Runnable() {
+                    @Override
+                    public void run() {
+                        pipeline.fireChannelWritabilityChanged();
+                    }
+                };
+            }
+            eventLoop().execute(task);
+        } else {
+            pipeline.fireChannelWritabilityChanged();
+        }
+    }
+    @Override
+    public Http2FrameStream stream() {
+        return stream;
+    }
+
+    void closeOutbound() {
+        outboundClosed = true;
+    }
+
+    void streamClosed() {
+        unsafe.readEOS();
+        // Attempt to drain any queued data from the queue and deliver it to the application before closing this
+        // channel.
+        unsafe.doBeginRead();
+    }
+
+    @Override
+    public ChannelMetadata metadata() {
+        return METADATA;
+    }
+
+    @Override
+    public ChannelConfig config() {
+        return config;
+    }
+
+    @Override
+    public boolean isOpen() {
+        return !closePromise.isDone();
+    }
+
+    @Override
+    public boolean isActive() {
+        return isOpen();
+    }
+
+    @Override
+    public boolean isWritable() {
+        return unwritable == 0;
+    }
+
+    @Override
+    public ChannelId id() {
+        return channelId;
+    }
+
+    @Override
+    public EventLoop eventLoop() {
+        return parent().eventLoop();
+    }
+
+    @Override
+    public Channel parent() {
+        return parentContext().channel();
+    }
+
+    @Override
+    public boolean isRegistered() {
+        return registered;
+    }
+
+    @Override
+    public SocketAddress localAddress() {
+        return parent().localAddress();
+    }
+
+    @Override
+    public SocketAddress remoteAddress() {
+        return parent().remoteAddress();
+    }
+
+    @Override
+    public ChannelFuture closeFuture() {
+        return closePromise;
+    }
+
+    @Override
+    public long bytesBeforeUnwritable() {
+        long bytes = config().getWriteBufferHighWaterMark() - totalPendingSize;
+        // If bytes is negative we know we are not writable, but if bytes is non-negative we have to check
+        // writability. Note that totalPendingSize and isWritable() use different volatile variables that are not
+        // synchronized together. totalPendingSize will be updated before isWritable().
+        if (bytes > 0) {
+            return isWritable() ? bytes : 0;
+        }
+        return 0;
+    }
+
+    @Override
+    public long bytesBeforeWritable() {
+        long bytes = totalPendingSize - config().getWriteBufferLowWaterMark();
+        // If bytes is negative we know we are writable, but if bytes is non-negative we have to check writability.
+        // Note that totalPendingSize and isWritable() use different volatile variables that are not synchronized
+        // together. totalPendingSize will be updated before isWritable().
+        if (bytes > 0) {
+            return isWritable() ? 0 : bytes;
+        }
+        return 0;
+    }
+
+    @Override
+    public Unsafe unsafe() {
+        return unsafe;
+    }
+
+    @Override
+    public ChannelPipeline pipeline() {
+        return pipeline;
+    }
+
+    @Override
+    public ByteBufAllocator alloc() {
+        return config().getAllocator();
+    }
+
+    @Override
+    public Channel read() {
+        pipeline().read();
+        return this;
+    }
+
+    @Override
+    public Channel flush() {
+        pipeline().flush();
+        return this;
+    }
+
+    @Override
+    public ChannelFuture bind(SocketAddress localAddress) {
+        return pipeline().bind(localAddress);
+    }
+
+    @Override
+    public ChannelFuture connect(SocketAddress remoteAddress) {
+        return pipeline().connect(remoteAddress);
+    }
+
+    @Override
+    public ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress) {
+        return pipeline().connect(remoteAddress, localAddress);
+    }
+
+    @Override
+    public ChannelFuture disconnect() {
+        return pipeline().disconnect();
+    }
+
+    @Override
+    public ChannelFuture close() {
+        return pipeline().close();
+    }
+
+    @Override
+    public ChannelFuture deregister() {
+        return pipeline().deregister();
+    }
+
+    @Override
+    public ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) {
+        return pipeline().bind(localAddress, promise);
+    }
+
+    @Override
+    public ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) {
+        return pipeline().connect(remoteAddress, promise);
+    }
+
+    @Override
+    public ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) {
+        return pipeline().connect(remoteAddress, localAddress, promise);
+    }
+
+    @Override
+    public ChannelFuture disconnect(ChannelPromise promise) {
+        return pipeline().disconnect(promise);
+    }
+
+    @Override
+    public ChannelFuture close(ChannelPromise promise) {
+        return pipeline().close(promise);
+    }
+
+    @Override
+    public ChannelFuture deregister(ChannelPromise promise) {
+        return pipeline().deregister(promise);
+    }
+
+    @Override
+    public ChannelFuture write(Object msg) {
+        return pipeline().write(msg);
+    }
+
+    @Override
+    public ChannelFuture write(Object msg, ChannelPromise promise) {
+        return pipeline().write(msg, promise);
+    }
+
+    @Override
+    public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) {
+        return pipeline().writeAndFlush(msg, promise);
+    }
+
+    @Override
+    public ChannelFuture writeAndFlush(Object msg) {
+        return pipeline().writeAndFlush(msg);
+    }
+
+    @Override
+    public ChannelPromise newPromise() {
+        return pipeline().newPromise();
+    }
+
+    @Override
+    public ChannelProgressivePromise newProgressivePromise() {
+        return pipeline().newProgressivePromise();
+    }
+
+    @Override
+    public ChannelFuture newSucceededFuture() {
+        return pipeline().newSucceededFuture();
+    }
+
+    @Override
+    public ChannelFuture newFailedFuture(Throwable cause) {
+        return pipeline().newFailedFuture(cause);
+    }
+
+    @Override
+    public ChannelPromise voidPromise() {
+        return pipeline().voidPromise();
+    }
+
+    @Override
+    public int hashCode() {
+        return id().hashCode();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        return this == o;
+    }
+
+    @Override
+    public int compareTo(Channel o) {
+        if (this == o) {
+            return 0;
+        }
+
+        return id().compareTo(o.id());
+    }
+
+    @Override
+    public String toString() {
+        return parent().toString() + "(H2 - " + stream + ')';
+    }
+
+    /**
+     * Receive a read message. This does not notify handlers unless a read is in progress on the
+     * channel.
+     */
+    void fireChildRead(Http2Frame frame) {
+        assert eventLoop().inEventLoop();
+        if (!isActive()) {
+            ReferenceCountUtil.release(frame);
+        } else if (readStatus != ReadStatus.IDLE) {
+            // If a read is in progress or has been requested, there cannot be anything in the queue,
+            // otherwise we would have drained it from the queue and processed it during the read cycle.
+            assert inboundBuffer == null || inboundBuffer.isEmpty();
+            final RecvByteBufAllocator.Handle allocHandle = unsafe.recvBufAllocHandle();
+            unsafe.doRead0(frame, allocHandle);
+            // We currently don't need to check for readEOS because the parent channel and child channel are limited
+            // to the same EventLoop thread. There are a limited number of frame types that may come after EOS is
+            // read (unknown, reset) and the trade off is less conditionals for the hot path (headers/data) at the
+            // cost of additional readComplete notifications on the rare path.
+            if (allocHandle.continueReading()) {
+                maybeAddChannelToReadCompletePendingQueue();
+            } else {
+                unsafe.notifyReadComplete(allocHandle, true);
+            }
+        } else {
+            if (inboundBuffer == null) {
+                inboundBuffer = new ArrayDeque<Object>(4);
+            }
+            inboundBuffer.add(frame);
+        }
+    }
+
+    void fireChildReadComplete() {
+        assert eventLoop().inEventLoop();
+        assert readStatus != ReadStatus.IDLE || !readCompletePending;
+        unsafe.notifyReadComplete(unsafe.recvBufAllocHandle(), false);
+    }
+
+    private final class Http2ChannelUnsafe implements Unsafe {
+        private final VoidChannelPromise unsafeVoidPromise =
+                new VoidChannelPromise(AbstractHttp2StreamChannel.this, false);
+        @SuppressWarnings("deprecation")
+        private RecvByteBufAllocator.Handle recvHandle;
+        private boolean writeDoneAndNoFlush;
+        private boolean closeInitiated;
+        private boolean readEOS;
+
+        @Override
+        public void connect(final SocketAddress remoteAddress,
+                            SocketAddress localAddress, final ChannelPromise promise) {
+            if (!promise.setUncancellable()) {
+                return;
+            }
+            promise.setFailure(new UnsupportedOperationException());
+        }
+
+        @Override
+        public RecvByteBufAllocator.Handle recvBufAllocHandle() {
+            if (recvHandle == null) {
+                recvHandle = config().getRecvByteBufAllocator().newHandle();
+                recvHandle.reset(config());
+            }
+            return recvHandle;
+        }
+
+        @Override
+        public SocketAddress localAddress() {
+            return parent().unsafe().localAddress();
+        }
+
+        @Override
+        public SocketAddress remoteAddress() {
+            return parent().unsafe().remoteAddress();
+        }
+
+        @Override
+        public void register(EventLoop eventLoop, ChannelPromise promise) {
+            if (!promise.setUncancellable()) {
+                return;
+            }
+            if (registered) {
+                promise.setFailure(new UnsupportedOperationException("Re-register is not supported"));
+                return;
+            }
+
+            registered = true;
+
+            promise.setSuccess();
+
+            pipeline().fireChannelRegistered();
+            if (isActive()) {
+                pipeline().fireChannelActive();
+            }
+        }
+
+        @Override
+        public void bind(SocketAddress localAddress, ChannelPromise promise) {
+            if (!promise.setUncancellable()) {
+                return;
+            }
+            promise.setFailure(new UnsupportedOperationException());
+        }
+
+        @Override
+        public void disconnect(ChannelPromise promise) {
+            close(promise);
+        }
+
+        @Override
+        public void close(final ChannelPromise promise) {
+            if (!promise.setUncancellable()) {
+                return;
+            }
+            if (closeInitiated) {
+                if (closePromise.isDone()) {
+                    // Closed already.
+                    promise.setSuccess();
+                } else if (!(promise instanceof VoidChannelPromise)) { // Only needed if no VoidChannelPromise.
+                    // This means close() was called before so we just register a listener and return
+                    closePromise.addListener(new ChannelFutureListener() {
+                        @Override
+                        public void operationComplete(ChannelFuture future) {
+                            promise.setSuccess();
+                        }
+                    });
+                }
+                return;
+            }
+            closeInitiated = true;
+            // Just set to false as removing from an underlying queue would even be more expensive.
+            readCompletePending = false;
+
+            final boolean wasActive = isActive();
+
+            // There is no need to update the local window as once the stream is closed all the pending bytes will be
+            // given back to the connection window by the controller itself.
+
+            // Only ever send a reset frame if the connection is still alive and if the stream was created before
+            // as otherwise we may send a RST on a stream in an invalid state and cause a connection error.
+            if (parent().isActive() && !readEOS && Http2CodecUtil.isStreamIdValid(stream.id())) {
+                Http2StreamFrame resetFrame = new DefaultHttp2ResetFrame(Http2Error.CANCEL).stream(stream());
+                write(resetFrame, unsafe().voidPromise());
+                flush();
+            }
+
+            if (inboundBuffer != null) {
+                for (;;) {
+                    Object msg = inboundBuffer.poll();
+                    if (msg == null) {
+                        break;
+                    }
+                    ReferenceCountUtil.release(msg);
+                }
+                inboundBuffer = null;
+            }
+
+            // The promise should be notified before we call fireChannelInactive().
+            outboundClosed = true;
+            closePromise.setSuccess();
+            promise.setSuccess();
+
+            fireChannelInactiveAndDeregister(voidPromise(), wasActive);
+        }
+
+        @Override
+        public void closeForcibly() {
+            close(unsafe().voidPromise());
+        }
+
+        @Override
+        public void deregister(ChannelPromise promise) {
+            fireChannelInactiveAndDeregister(promise, false);
+        }
+
+        private void fireChannelInactiveAndDeregister(final ChannelPromise promise,
+                                                      final boolean fireChannelInactive) {
+            if (!promise.setUncancellable()) {
+                return;
+            }
+
+            if (!registered) {
+                promise.setSuccess();
+                return;
+            }
+
+            // As a user may call deregister() from within any method while doing processing in the ChannelPipeline,
+            // we need to ensure we do the actual deregister operation later. This is necessary to preserve the
+            // behavior of the AbstractChannel, which always invokes channelUnregistered and channelInactive
+            // events 'later' to ensure the current events in the handler are completed before these events.
+            //
+            // See:
+            // https://github.com/netty/netty/issues/4435
+            invokeLater(new Runnable() {
+                @Override
+                public void run() {
+                    if (fireChannelInactive) {
+                        pipeline.fireChannelInactive();
+                    }
+                    // The user can fire `deregister` events multiple times but we only want to fire the pipeline
+                    // event if the channel was actually registered.
+                    if (registered) {
+                        registered = false;
+                        pipeline.fireChannelUnregistered();
+                    }
+                    safeSetSuccess(promise);
+                }
+            });
+        }
+
+        private void safeSetSuccess(ChannelPromise promise) {
+            if (!(promise instanceof VoidChannelPromise) && !promise.trySuccess()) {
+                logger.warn("Failed to mark a promise as success because it is done already: {}", promise);
+            }
+        }
+
+        private void invokeLater(Runnable task) {
+            try {
+                // This method is used by outbound operation implementations to trigger an inbound event later.
+                // They do not trigger an inbound event immediately because an outbound operation might have been
+                // triggered by another inbound event handler method.  If fired immediately, the call stack
+                // will look like this for example:
+                //
+                //   handlerA.inboundBufferUpdated() - (1) an inbound handler method closes a connection.
+                //   -> handlerA.ctx.close()
+                //     -> channel.unsafe.close()
+                //       -> handlerA.channelInactive() - (2) another inbound handler method called while in (1) yet
+                //
+                // which means the execution of two inbound handler methods of the same handler overlap undesirably.
+                eventLoop().execute(task);
+            } catch (RejectedExecutionException e) {
+                logger.warn("Can't invoke task later as EventLoop rejected it", e);
+            }
+        }
+
+        @Override
+        public void beginRead() {
+            if (!isActive()) {
+                return;
+            }
+            updateLocalWindowIfNeeded();
+
+            switch (readStatus) {
+                case IDLE:
+                    readStatus = ReadStatus.IN_PROGRESS;
+                    doBeginRead();
+                    break;
+                case IN_PROGRESS:
+                    readStatus = ReadStatus.REQUESTED;
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        private Object pollQueuedMessage() {
+            return inboundBuffer == null ? null : inboundBuffer.poll();
+        }
+
+        void doBeginRead() {
+            // Process messages until there are none left (or the user stopped requesting) and also handle EOS.
+            while (readStatus != ReadStatus.IDLE) {
+                Object message = pollQueuedMessage();
+                if (message == null) {
+                    if (readEOS) {
+                        unsafe.closeForcibly();
+                    }
+                    // We need to double check that there is nothing left to flush such as a
+                    // window update frame.
+                    flush();
+                    break;
+                }
+                final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
+                allocHandle.reset(config());
+                boolean continueReading = false;
+                do {
+                    doRead0((Http2Frame) message, allocHandle);
+                } while ((readEOS || (continueReading = allocHandle.continueReading()))
+                        && (message = pollQueuedMessage()) != null);
+
+                if (continueReading && isParentReadInProgress() && !readEOS) {
+                    // Currently the parent and child channel are on the same EventLoop thread. If the parent is
+                    // currently reading it is possible that more frames will be delivered to this child channel. In
+                    // the case that this child channel still wants to read we delay the channelReadComplete on this
+                    // child channel until the parent is done reading.
+                    maybeAddChannelToReadCompletePendingQueue();
+                } else {
+                    notifyReadComplete(allocHandle, true);
+                }
+            }
+        }
+
+        void readEOS() {
+            readEOS = true;
+        }
+
+        private void updateLocalWindowIfNeeded() {
+            if (flowControlledBytes != 0) {
+                int bytes = flowControlledBytes;
+                flowControlledBytes = 0;
+                ChannelFuture future = write0(parentContext(), new DefaultHttp2WindowUpdateFrame(bytes).stream(stream));
+                // window update frames are commonly swallowed by the Http2FrameCodec and the promise is synchronously
+                // completed but the flow controller _may_ have generated a wire level WINDOW_UPDATE. Therefore we need,
+                // to assume there was a write done that needs to be flushed or we risk flow control starvation.
+                writeDoneAndNoFlush = true;
+                // Add a listener which will notify and teardown the stream
+                // when a window update fails if needed or check the result of the future directly if it was completed
+                // already.
+                // See https://github.com/netty/netty/issues/9663
+                if (future.isDone()) {
+                    windowUpdateFrameWriteComplete(future, AbstractHttp2StreamChannel.this);
+                } else {
+                    future.addListener(windowUpdateFrameWriteListener);
+                }
+            }
+        }
+
+        void notifyReadComplete(RecvByteBufAllocator.Handle allocHandle, boolean forceReadComplete) {
+            if (!readCompletePending && !forceReadComplete) {
+                return;
+            }
+            // Set to false just in case we added the channel multiple times before.
+            readCompletePending = false;
+
+            if (readStatus == ReadStatus.REQUESTED) {
+                readStatus = ReadStatus.IN_PROGRESS;
+            } else {
+                readStatus = ReadStatus.IDLE;
+            }
+
+            allocHandle.readComplete();
+            pipeline().fireChannelReadComplete();
+            // Reading data may result in frames being written (e.g. WINDOW_UPDATE, RST, etc..). If the parent
+            // channel is not currently reading we need to force a flush at the child channel, because we cannot
+            // rely upon flush occurring in channelReadComplete on the parent channel.
+            flush();
+            if (readEOS) {
+                unsafe.closeForcibly();
+            }
+        }
+
+        @SuppressWarnings("deprecation")
+        void doRead0(Http2Frame frame, RecvByteBufAllocator.Handle allocHandle) {
+            final int bytes;
+            if (frame instanceof Http2DataFrame) {
+                bytes = ((Http2DataFrame) frame).initialFlowControlledBytes();
+
+                // It is important that we increment the flowControlledBytes before we call fireChannelRead(...)
+                // as it may cause a read() that will call updateLocalWindowIfNeeded() and we need to ensure
+                // in this case that we accounted for it.
+                //
+                // See https://github.com/netty/netty/issues/9663
+                flowControlledBytes += bytes;
+            } else {
+                bytes = MIN_HTTP2_FRAME_SIZE;
+            }
+            // Update before firing event through the pipeline to be consistent with other Channel implementation.
+            allocHandle.attemptedBytesRead(bytes);
+            allocHandle.lastBytesRead(bytes);
+            allocHandle.incMessagesRead(1);
+
+            pipeline().fireChannelRead(frame);
+        }
+
+        @Override
+        public void write(Object msg, final ChannelPromise promise) {
+            // After this point its not possible to cancel a write anymore.
+            if (!promise.setUncancellable()) {
+                ReferenceCountUtil.release(msg);
+                return;
+            }
+
+            if (!isActive() ||
+                    // Once the outbound side was closed we should not allow header / data frames
+                    outboundClosed && (msg instanceof Http2HeadersFrame || msg instanceof Http2DataFrame)) {
+                ReferenceCountUtil.release(msg);
+                promise.setFailure(new ClosedChannelException());
+                return;
+            }
+
+            try {
+                if (msg instanceof Http2StreamFrame) {
+                    Http2StreamFrame frame = validateStreamFrame((Http2StreamFrame) msg).stream(stream());
+                    writeHttp2StreamFrame(frame, promise);
+                } else {
+                    String msgStr = msg.toString();
+                    ReferenceCountUtil.release(msg);
+                    promise.setFailure(new IllegalArgumentException(
+                            "Message must be an " + StringUtil.simpleClassName(Http2StreamFrame.class) +
+                                    ": " + msgStr));
+                }
+            } catch (Throwable t) {
+                promise.tryFailure(t);
+            }
+        }
+
+        private void writeHttp2StreamFrame(Http2StreamFrame frame, final ChannelPromise promise) {
+            if (!firstFrameWritten && !isStreamIdValid(stream().id()) && !(frame instanceof Http2HeadersFrame)) {
+                ReferenceCountUtil.release(frame);
+                promise.setFailure(
+                    new IllegalArgumentException("The first frame must be a headers frame. Was: "
+                        + frame.name()));
+                return;
+            }
+
+            final boolean firstWrite;
+            if (firstFrameWritten) {
+                firstWrite = false;
+            } else {
+                firstWrite = firstFrameWritten = true;
+            }
+
+            ChannelFuture f = write0(parentContext(), frame);
+            if (f.isDone()) {
+                if (firstWrite) {
+                    firstWriteComplete(f, promise);
+                } else {
+                    writeComplete(f, promise);
+                }
+            } else {
+                final long bytes = FlowControlledFrameSizeEstimator.HANDLE_INSTANCE.size(frame);
+                incrementPendingOutboundBytes(bytes, false);
+                f.addListener(new ChannelFutureListener() {
+                    @Override
+                    public void operationComplete(ChannelFuture future) {
+                        if (firstWrite) {
+                            firstWriteComplete(future, promise);
+                        } else {
+                            writeComplete(future, promise);
+                        }
+                        decrementPendingOutboundBytes(bytes, false);
+                    }
+                });
+                writeDoneAndNoFlush = true;
+            }
+        }
+
+        private void firstWriteComplete(ChannelFuture future, ChannelPromise promise) {
+            Throwable cause = future.cause();
+            if (cause == null) {
+                promise.setSuccess();
+            } else {
+                // If the first write fails there is not much we can do, just close
+                closeForcibly();
+                promise.setFailure(wrapStreamClosedError(cause));
+            }
+        }
+
+        private void writeComplete(ChannelFuture future, ChannelPromise promise) {
+            Throwable cause = future.cause();
+            if (cause == null) {
+                promise.setSuccess();
+            } else {
+                Throwable error = wrapStreamClosedError(cause);
+                // To make it more consistent with AbstractChannel we handle all IOExceptions here.
+                if (error instanceof IOException) {
+                    if (config.isAutoClose()) {
+                        // Close channel if needed.
+                        closeForcibly();
+                    } else {
+                        // TODO: Once Http2StreamChannel extends DuplexChannel we should call shutdownOutput(...)
+                        outboundClosed = true;
+                    }
+                }
+                promise.setFailure(error);
+            }
+        }
+
+        private Throwable wrapStreamClosedError(Throwable cause) {
+            // If the error was caused by STREAM_CLOSED we should use a ClosedChannelException to better
+            // mimic other transports and make it easier to reason about what exceptions to expect.
+            if (cause instanceof Http2Exception && ((Http2Exception) cause).error() == Http2Error.STREAM_CLOSED) {
+                return new ClosedChannelException().initCause(cause);
+            }
+            return cause;
+        }
+
+        private Http2StreamFrame validateStreamFrame(Http2StreamFrame frame) {
+            if (frame.stream() != null && frame.stream() != stream) {
+                String msgString = frame.toString();
+                ReferenceCountUtil.release(frame);
+                throw new IllegalArgumentException(
+                        "Stream " + frame.stream() + " must not be set on the frame: " + msgString);
+            }
+            return frame;
+        }
+
+        @Override
+        public void flush() {
+            // If we are currently in the parent channel's read loop we should just ignore the flush.
+            // We will ensure we trigger ctx.flush() after we processed all Channels later on and
+            // so aggregate the flushes. This is done as ctx.flush() is expensive when as it may trigger an
+            // write(...) or writev(...) operation on the socket.
+            if (!writeDoneAndNoFlush || isParentReadInProgress()) {
+                // There is nothing to flush so this is a NOOP.
+                return;
+            }
+            // We need to set this to false before we call flush0(...) as ChannelFutureListener may produce more data
+            // that are explicit flushed.
+            writeDoneAndNoFlush = false;
+            flush0(parentContext());
+        }
+
+        @Override
+        public ChannelPromise voidPromise() {
+            return unsafeVoidPromise;
+        }
+
+        @Override
+        public ChannelOutboundBuffer outboundBuffer() {
+            // Always return null as we not use the ChannelOutboundBuffer and not even support it.
+            return null;
+        }
+    }
+
+    /**
+     * {@link ChannelConfig} so that the high and low writebuffer watermarks can reflect the outbound flow control
+     * window, without having to create a new {@link WriteBufferWaterMark} object whenever the flow control window
+     * changes.
+     */
+    private static final class Http2StreamChannelConfig extends DefaultChannelConfig {
+        Http2StreamChannelConfig(Channel channel) {
+            super(channel);
+        }
+
+        @Override
+        public MessageSizeEstimator getMessageSizeEstimator() {
+            return FlowControlledFrameSizeEstimator.INSTANCE;
+        }
+
+        @Override
+        public ChannelConfig setMessageSizeEstimator(MessageSizeEstimator estimator) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public ChannelConfig setRecvByteBufAllocator(RecvByteBufAllocator allocator) {
+            if (!(allocator.newHandle() instanceof RecvByteBufAllocator.ExtendedHandle)) {
+                throw new IllegalArgumentException("allocator.newHandle() must return an object of type: " +
+                        RecvByteBufAllocator.ExtendedHandle.class);
+            }
+            super.setRecvByteBufAllocator(allocator);
+            return this;
+        }
+    }
+
+    private void maybeAddChannelToReadCompletePendingQueue() {
+        if (!readCompletePending) {
+            readCompletePending = true;
+            addChannelToReadCompletePendingQueue();
+        }
+    }
+
+    protected void flush0(ChannelHandlerContext ctx) {
+        ctx.flush();
+    }
+
+    protected ChannelFuture write0(ChannelHandlerContext ctx, Object msg) {
+        ChannelPromise promise = ctx.newPromise();
+        ctx.write(msg, promise);
+        return promise;
+    }
+
+    protected abstract boolean isParentReadInProgress();
+    protected abstract void addChannelToReadCompletePendingQueue();
+    protected abstract ChannelHandlerContext parentContext();
+}
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/CleartextHttp2ServerUpgradeHandler.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/CleartextHttp2ServerUpgradeHandler.java
index c70b343..fe9353b 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/CleartextHttp2ServerUpgradeHandler.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/CleartextHttp2ServerUpgradeHandler.java
@@ -18,7 +18,6 @@ package io.netty.handler.codec.http2;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.ByteBufUtil;
 import io.netty.channel.ChannelHandler;
-import io.netty.channel.ChannelHandlerAdapter;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.handler.codec.ByteToMessageDecoder;
 import io.netty.handler.codec.http.HttpServerCodec;
@@ -39,7 +38,7 @@ import static io.netty.util.internal.ObjectUtil.checkNotNull;
  * prior knowledge or not.
  */
 @UnstableApi
-public final class CleartextHttp2ServerUpgradeHandler extends ChannelHandlerAdapter {
+public final class CleartextHttp2ServerUpgradeHandler extends ByteToMessageDecoder {
     private static final ByteBuf CONNECTION_PREFACE = unreleasableBuffer(connectionPrefaceBuf());
 
     private final HttpServerCodec httpServerCodec;
@@ -66,36 +65,33 @@ public final class CleartextHttp2ServerUpgradeHandler extends ChannelHandlerAdap
     @Override
     public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
         ctx.pipeline()
-           .addBefore(ctx.name(), null, new PriorKnowledgeHandler())
-           .addBefore(ctx.name(), null, httpServerCodec)
-           .replace(this, null, httpServerUpgradeHandler);
+                .addAfter(ctx.name(), null, httpServerUpgradeHandler)
+                .addAfter(ctx.name(), null, httpServerCodec);
     }
 
     /**
      * Peek inbound message to determine current connection wants to start HTTP/2
      * by HTTP upgrade or prior knowledge
      */
-    private final class PriorKnowledgeHandler extends ByteToMessageDecoder {
-        @Override
-        protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
-            int prefaceLength = CONNECTION_PREFACE.readableBytes();
-            int bytesRead = Math.min(in.readableBytes(), prefaceLength);
+    @Override
+    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
+        int prefaceLength = CONNECTION_PREFACE.readableBytes();
+        int bytesRead = Math.min(in.readableBytes(), prefaceLength);
 
-            if (!ByteBufUtil.equals(CONNECTION_PREFACE, CONNECTION_PREFACE.readerIndex(),
-                                    in, in.readerIndex(), bytesRead)) {
-                ctx.pipeline().remove(this);
-            } else if (bytesRead == prefaceLength) {
-                // Full h2 preface match, removed source codec, using http2 codec to handle
-                // following network traffic
-                ctx.pipeline()
-                   .remove(httpServerCodec)
-                   .remove(httpServerUpgradeHandler);
+        if (!ByteBufUtil.equals(CONNECTION_PREFACE, CONNECTION_PREFACE.readerIndex(),
+                in, in.readerIndex(), bytesRead)) {
+            ctx.pipeline().remove(this);
+        } else if (bytesRead == prefaceLength) {
+            // Full h2 preface match, removed source codec, using http2 codec to handle
+            // following network traffic
+            ctx.pipeline()
+                    .remove(httpServerCodec)
+                    .remove(httpServerUpgradeHandler);
 
-                ctx.pipeline().addAfter(ctx.name(), null, http2ServerHandler);
-                ctx.pipeline().remove(this);
+            ctx.pipeline().addAfter(ctx.name(), null, http2ServerHandler);
+            ctx.pipeline().remove(this);
 
-                ctx.fireUserEventTriggered(PriorKnowledgeUpgradeEvent.INSTANCE);
-            }
+            ctx.fireUserEventTriggered(PriorKnowledgeUpgradeEvent.INSTANCE);
         }
     }
 
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/CompressorHttp2ConnectionEncoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/CompressorHttp2ConnectionEncoder.java
index 3137da2..213aa7b 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/CompressorHttp2ConnectionEncoder.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/CompressorHttp2ConnectionEncoder.java
@@ -108,7 +108,7 @@ public class CompressorHttp2ConnectionEncoder extends DecoratingHttp2ConnectionE
                 return promise;
             }
 
-            PromiseCombiner combiner = new PromiseCombiner();
+            PromiseCombiner combiner = new PromiseCombiner(ctx.executor());
             for (;;) {
                 ByteBuf nextBuf = nextReadableBuf(channel);
                 boolean compressedEndOfStream = nextBuf == null && endOfStream;
@@ -284,16 +284,7 @@ public class CompressorHttp2ConnectionEncoder extends DecoratingHttp2ConnectionE
      * @param compressor The compressor for {@code stream}
      */
     void cleanup(Http2Stream stream, EmbeddedChannel compressor) {
-        if (compressor.finish()) {
-            for (;;) {
-                final ByteBuf buf = compressor.readOutbound();
-                if (buf == null) {
-                    break;
-                }
-
-                buf.release();
-            }
-        }
+        compressor.finishAndReleaseAll();
         stream.removeProperty(propertyKey);
     }
 
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DecoratingHttp2ConnectionEncoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DecoratingHttp2ConnectionEncoder.java
index 9d591ab..43f3c3b 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DecoratingHttp2ConnectionEncoder.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DecoratingHttp2ConnectionEncoder.java
@@ -22,7 +22,8 @@ import static io.netty.util.internal.ObjectUtil.checkNotNull;
  * A decorator around another {@link Http2ConnectionEncoder} instance.
  */
 @UnstableApi
-public class DecoratingHttp2ConnectionEncoder extends DecoratingHttp2FrameWriter implements Http2ConnectionEncoder {
+public class DecoratingHttp2ConnectionEncoder extends DecoratingHttp2FrameWriter implements Http2ConnectionEncoder,
+        Http2SettingsReceivedConsumer {
     private final Http2ConnectionEncoder delegate;
 
     public DecoratingHttp2ConnectionEncoder(Http2ConnectionEncoder delegate) {
@@ -59,4 +60,14 @@ public class DecoratingHttp2ConnectionEncoder extends DecoratingHttp2FrameWriter
     public void remoteSettings(Http2Settings settings) throws Http2Exception {
         delegate.remoteSettings(settings);
     }
+
+    @Override
+    public void consumeReceivedSettings(Http2Settings settings) {
+        if (delegate instanceof Http2SettingsReceivedConsumer) {
+            ((Http2SettingsReceivedConsumer) delegate).consumeReceivedSettings(settings);
+        } else {
+            throw new IllegalStateException("delegate " + delegate + " is not an instance of " +
+                    Http2SettingsReceivedConsumer.class);
+        }
+    }
 }
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Connection.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Connection.java
index 8590e06..4d866eb 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Connection.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Connection.java
@@ -930,7 +930,7 @@ public class DefaultHttp2Connection implements Http2Connection {
         private final Set<Http2Stream> streams = new LinkedHashSet<Http2Stream>();
         private int pendingIterations;
 
-        public ActiveStreams(List<Listener> listeners) {
+        ActiveStreams(List<Listener> listeners) {
             this.listeners = listeners;
         }
 
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 2d78fc9..10da347 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
@@ -57,6 +57,8 @@ public class DefaultHttp2ConnectionDecoder implements Http2ConnectionDecoder {
     private final Http2FrameReader frameReader;
     private Http2FrameListener listener;
     private final Http2PromisedRequestVerifier requestVerifier;
+    private final Http2SettingsReceivedConsumer settingsReceivedConsumer;
+    private final boolean autoAckPing;
 
     public DefaultHttp2ConnectionDecoder(Http2Connection connection,
                                          Http2ConnectionEncoder encoder,
@@ -68,6 +70,60 @@ public class DefaultHttp2ConnectionDecoder implements Http2ConnectionDecoder {
                                          Http2ConnectionEncoder encoder,
                                          Http2FrameReader frameReader,
                                          Http2PromisedRequestVerifier requestVerifier) {
+        this(connection, encoder, frameReader, requestVerifier, true);
+    }
+
+    /**
+     * Create a new instance.
+     * @param connection The {@link Http2Connection} associated with this decoder.
+     * @param encoder The {@link Http2ConnectionEncoder} associated with this decoder.
+     * @param frameReader Responsible for reading/parsing the raw frames. As opposed to this object which applies
+     *                    h2 semantics on top of the frames.
+     * @param requestVerifier Determines if push promised streams are valid.
+     * @param autoAckSettings {@code false} to disable automatically applying and sending settings acknowledge frame.
+     *  The {@code Http2ConnectionEncoder} is expected to be an instance of {@link Http2SettingsReceivedConsumer} and
+     *  will apply the earliest received but not yet ACKed SETTINGS when writing the SETTINGS ACKs.
+     * {@code true} to enable automatically applying and sending settings acknowledge frame.
+     */
+    public DefaultHttp2ConnectionDecoder(Http2Connection connection,
+                                         Http2ConnectionEncoder encoder,
+                                         Http2FrameReader frameReader,
+                                         Http2PromisedRequestVerifier requestVerifier,
+                                         boolean autoAckSettings) {
+        this(connection, encoder, frameReader, requestVerifier, autoAckSettings, true);
+    }
+
+    /**
+     * Create a new instance.
+     * @param connection The {@link Http2Connection} associated with this decoder.
+     * @param encoder The {@link Http2ConnectionEncoder} associated with this decoder.
+     * @param frameReader Responsible for reading/parsing the raw frames. As opposed to this object which applies
+     *                    h2 semantics on top of the frames.
+     * @param requestVerifier Determines if push promised streams are valid.
+     * @param autoAckSettings {@code false} to disable automatically applying and sending settings acknowledge frame.
+     *                        The {@code Http2ConnectionEncoder} is expected to be an instance of
+     *                        {@link Http2SettingsReceivedConsumer} and will apply the earliest received but not yet
+     *                        ACKed SETTINGS when writing the SETTINGS ACKs. {@code true} to enable automatically
+     *                        applying and sending settings acknowledge frame.
+     * @param autoAckPing {@code false} to disable automatically sending ping acknowledge frame. {@code true} to enable
+     *                    automatically sending ping ack frame.
+     */
+    public DefaultHttp2ConnectionDecoder(Http2Connection connection,
+                                         Http2ConnectionEncoder encoder,
+                                         Http2FrameReader frameReader,
+                                         Http2PromisedRequestVerifier requestVerifier,
+                                         boolean autoAckSettings,
+                                         boolean autoAckPing) {
+        this.autoAckPing = autoAckPing;
+        if (autoAckSettings) {
+            settingsReceivedConsumer = null;
+        } else {
+            if (!(encoder instanceof Http2SettingsReceivedConsumer)) {
+                throw new IllegalArgumentException("disabling autoAckSettings requires the encoder to be a " +
+                        Http2SettingsReceivedConsumer.class);
+            }
+            settingsReceivedConsumer = (Http2SettingsReceivedConsumer) encoder;
+        }
         this.connection = checkNotNull(connection, "connection");
         this.frameReader = checkNotNull(frameReader, "frameReader");
         this.encoder = checkNotNull(encoder, "encoder");
@@ -158,8 +214,8 @@ public class DefaultHttp2ConnectionDecoder implements Http2ConnectionDecoder {
 
     void onGoAwayRead0(ChannelHandlerContext ctx, int lastStreamId, long errorCode, ByteBuf debugData)
             throws Http2Exception {
-        connection.goAwayReceived(lastStreamId, errorCode, debugData);
         listener.onGoAwayRead(ctx, lastStreamId, errorCode, debugData);
+        connection.goAwayReceived(lastStreamId, errorCode, debugData);
     }
 
     void onUnknownFrame0(ChannelHandlerContext ctx, byte frameType, int streamId, Http2Flags flags,
@@ -408,23 +464,27 @@ public class DefaultHttp2ConnectionDecoder implements Http2ConnectionDecoder {
         }
 
         @Override
-        public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings) throws Http2Exception {
-            // Acknowledge receipt of the settings. We should do this before we process the settings to ensure our
-            // remote peer applies these settings before any subsequent frames that we may send which depend upon these
-            // new settings. See https://github.com/netty/netty/issues/6520.
-            encoder.writeSettingsAck(ctx, ctx.newPromise());
-
-            encoder.remoteSettings(settings);
+        public void onSettingsRead(final ChannelHandlerContext ctx, Http2Settings settings) throws Http2Exception {
+            if (settingsReceivedConsumer == null) {
+                // Acknowledge receipt of the settings. We should do this before we process the settings to ensure our
+                // remote peer applies these settings before any subsequent frames that we may send which depend upon
+                // these new settings. See https://github.com/netty/netty/issues/6520.
+                encoder.writeSettingsAck(ctx, ctx.newPromise());
+
+                encoder.remoteSettings(settings);
+            } else {
+                settingsReceivedConsumer.consumeReceivedSettings(settings);
+            }
 
             listener.onSettingsRead(ctx, settings);
         }
 
         @Override
         public void onPingRead(ChannelHandlerContext ctx, long data) throws Http2Exception {
-            // Send an ack back to the remote client.
-            // Need to retain the buffer here since it will be released after the write completes.
-            encoder.writePing(ctx, true, data, ctx.newPromise());
-
+            if (autoAckPing) {
+                // Send an ack back to the remote client.
+                encoder.writePing(ctx, true, data, ctx.newPromise());
+            }
             listener.onPingRead(ctx, data);
         }
 
@@ -525,6 +585,11 @@ public class DefaultHttp2ConnectionDecoder implements Http2ConnectionDecoder {
                             ctx.channel(), frameName, streamId);
                     return true;
                 }
+
+                // Make sure it's not an out-of-order frame, like a rogue DATA frame, for a stream that could
+                // never have existed.
+                verifyStreamMayHaveExisted(streamId);
+
                 // Its possible that this frame would result in stream ID out of order creation (PROTOCOL ERROR) and its
                 // also possible that this frame is received on a CLOSED stream (STREAM_CLOSED after a RST_STREAM is
                 // sent). We don't have enough information to know for sure, so we choose the lesser of the two errors.
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionEncoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionEncoder.java
index ce90e24..c1bdbc6 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionEncoder.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionEncoder.java
@@ -21,15 +21,19 @@ import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelPromise;
 import io.netty.channel.CoalescingBufferQueue;
 import io.netty.handler.codec.http.HttpStatusClass;
+import io.netty.handler.codec.http2.Http2CodecUtil.SimpleChannelPromiseAggregator;
 import io.netty.util.internal.UnstableApi;
 
 import java.util.ArrayDeque;
+import java.util.Queue;
 
 import static io.netty.handler.codec.http.HttpStatusClass.INFORMATIONAL;
 import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_PRIORITY_WEIGHT;
+import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR;
 import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
 import static io.netty.handler.codec.http2.Http2Exception.connectionError;
 import static io.netty.util.internal.ObjectUtil.checkNotNull;
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
 import static java.lang.Integer.MAX_VALUE;
 import static java.lang.Math.min;
 
@@ -37,13 +41,14 @@ import static java.lang.Math.min;
  * Default implementation of {@link Http2ConnectionEncoder}.
  */
 @UnstableApi
-public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder {
+public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder, Http2SettingsReceivedConsumer {
     private final Http2FrameWriter frameWriter;
     private final Http2Connection connection;
     private Http2LifecycleManager lifecycleManager;
     // We prefer ArrayDeque to LinkedList because later will produce more GC.
     // This initial capacity is plenty for SETTINGS traffic.
-    private final ArrayDeque<Http2Settings> outstandingLocalSettingsQueue = new ArrayDeque<Http2Settings>(4);
+    private final Queue<Http2Settings> outstandingLocalSettingsQueue = new ArrayDeque<Http2Settings>(4);
+    private Queue<Http2Settings> outstandingRemoteSettingsQueue;
 
     public DefaultHttp2ConnectionEncoder(Http2Connection connection, Http2FrameWriter frameWriter) {
         this.connection = checkNotNull(connection, "connection");
@@ -143,7 +148,7 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder {
     @Override
     public ChannelFuture writeHeaders(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding,
             boolean endStream, ChannelPromise promise) {
-        return writeHeaders(ctx, streamId, headers, 0, DEFAULT_PRIORITY_WEIGHT, false, padding, endStream, promise);
+        return writeHeaders0(ctx, streamId, headers, false, 0, (short) 0, false, padding, endStream, promise);
     }
 
     private static boolean validateHeadersSentState(Http2Stream stream, Http2Headers headers, boolean isServer,
@@ -159,6 +164,31 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder {
     public ChannelFuture writeHeaders(final ChannelHandlerContext ctx, final int streamId,
             final Http2Headers headers, final int streamDependency, final short weight,
             final boolean exclusive, final int padding, final boolean endOfStream, ChannelPromise promise) {
+        return writeHeaders0(ctx, streamId, headers, true, streamDependency,
+                weight, exclusive, padding, endOfStream, promise);
+    }
+
+    /**
+     * Write headers via {@link Http2FrameWriter}. If {@code hasPriority} is {@code false} it will ignore the
+     * {@code streamDependency}, {@code weight} and {@code exclusive} parameters.
+     */
+    private static ChannelFuture sendHeaders(Http2FrameWriter frameWriter, ChannelHandlerContext ctx, int streamId,
+                                       Http2Headers headers, final boolean hasPriority,
+                                       int streamDependency, final short weight,
+                                       boolean exclusive, final int padding,
+                                       boolean endOfStream, ChannelPromise promise) {
+        if (hasPriority) {
+            return frameWriter.writeHeaders(ctx, streamId, headers, streamDependency,
+                    weight, exclusive, padding, endOfStream, promise);
+        }
+        return frameWriter.writeHeaders(ctx, streamId, headers, padding, endOfStream, promise);
+    }
+
+    private ChannelFuture writeHeaders0(final ChannelHandlerContext ctx, final int streamId,
+                                        final Http2Headers headers, final boolean hasPriority,
+                                        final int streamDependency, final short weight,
+                                        final boolean exclusive, final int padding,
+                                        final boolean endOfStream, ChannelPromise promise) {
         try {
             Http2Stream stream = connection.stream(streamId);
             if (stream == null) {
@@ -200,8 +230,9 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder {
                 promise = promise.unvoid();
                 boolean isInformational = validateHeadersSentState(stream, headers, connection.isServer(), endOfStream);
 
-                ChannelFuture future = frameWriter.writeHeaders(ctx, streamId, headers, streamDependency,
-                                                                weight, exclusive, padding, endOfStream, promise);
+                ChannelFuture future = sendHeaders(frameWriter, ctx, streamId, headers, hasPriority, streamDependency,
+                        weight, exclusive, padding, endOfStream, promise);
+
                 // Writing headers may fail during the encode state if they violate HPACK limits.
                 Throwable failureCause = future.cause();
                 if (failureCause == null) {
@@ -231,8 +262,8 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder {
             } else {
                 // Pass headers to the flow-controller so it can maintain their sequence relative to DATA frames.
                 flowController.addFlowControlled(stream,
-                        new FlowControlledHeaders(stream, headers, streamDependency, weight, exclusive, padding,
-                                                  true, promise));
+                        new FlowControlledHeaders(stream, headers, hasPriority, streamDependency,
+                                weight, exclusive, padding, true, promise));
                 return promise;
             }
         } catch (Throwable t) {
@@ -273,7 +304,32 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder {
 
     @Override
     public ChannelFuture writeSettingsAck(ChannelHandlerContext ctx, ChannelPromise promise) {
-        return frameWriter.writeSettingsAck(ctx, promise);
+        if (outstandingRemoteSettingsQueue == null) {
+            return frameWriter.writeSettingsAck(ctx, promise);
+        }
+        Http2Settings settings = outstandingRemoteSettingsQueue.poll();
+        if (settings == null) {
+            return promise.setFailure(new Http2Exception(INTERNAL_ERROR, "attempted to write a SETTINGS ACK with no " +
+                    " pending SETTINGS"));
+        }
+        SimpleChannelPromiseAggregator aggregator = new SimpleChannelPromiseAggregator(promise, ctx.channel(),
+                ctx.executor());
+        // Acknowledge receipt of the settings. We should do this before we process the settings to ensure our
+        // remote peer applies these settings before any subsequent frames that we may send which depend upon
+        // these new settings. See https://github.com/netty/netty/issues/6520.
+        frameWriter.writeSettingsAck(ctx, aggregator.newPromise());
+
+        // We create a "new promise" to make sure that status from both the write and the application are taken into
+        // account independently.
+        ChannelPromise applySettingsPromise = aggregator.newPromise();
+        try {
+            remoteSettings(settings);
+            applySettingsPromise.setSuccess();
+        } catch (Throwable e) {
+            applySettingsPromise.setFailure(e);
+            lifecycleManager.onError(ctx, true, e);
+        }
+        return aggregator.doneAllocatingPromises();
     }
 
     @Override
@@ -366,6 +422,14 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder {
         return stream;
     }
 
+    @Override
+    public void consumeReceivedSettings(Http2Settings settings) {
+        if (outstandingRemoteSettingsQueue == null) {
+            outstandingRemoteSettingsQueue = new ArrayDeque<Http2Settings>(2);
+        }
+        outstandingRemoteSettingsQueue.add(settings);
+    }
+
     /**
      * Wrap a DATA frame so it can be written subject to flow-control. Note that this implementation assumes it
      * only writes padding once for the entire payload as opposed to writing it once per-frame. This makes the
@@ -481,14 +545,17 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder {
      */
     private final class FlowControlledHeaders extends FlowControlledBase {
         private final Http2Headers headers;
+        private final boolean hasPriorty;
         private final int streamDependency;
         private final short weight;
         private final boolean exclusive;
 
-        FlowControlledHeaders(Http2Stream stream, Http2Headers headers, int streamDependency, short weight,
-                boolean exclusive, int padding, boolean endOfStream, ChannelPromise promise) {
+        FlowControlledHeaders(Http2Stream stream, Http2Headers headers, boolean hasPriority,
+                              int streamDependency, short weight, boolean exclusive,
+                              int padding, boolean endOfStream, ChannelPromise promise) {
             super(stream, padding, endOfStream, promise.unvoid());
             this.headers = headers;
+            this.hasPriorty = hasPriority;
             this.streamDependency = streamDependency;
             this.weight = weight;
             this.exclusive = exclusive;
@@ -514,8 +581,8 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder {
             // closeStreamLocal().
             promise.addListener(this);
 
-            ChannelFuture f = frameWriter.writeHeaders(ctx, stream.id(), headers, streamDependency, weight, exclusive,
-                                                       padding, endOfStream, promise);
+            ChannelFuture f = sendHeaders(frameWriter, ctx, stream.id(), headers, hasPriorty, streamDependency,
+                    weight, exclusive, padding, endOfStream, promise);
             // Writing headers may fail during the encode state if they violate HPACK limits.
             Throwable failureCause = f.cause();
             if (failureCause == null) {
@@ -543,9 +610,7 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder {
 
         FlowControlledBase(final Http2Stream stream, int padding, boolean endOfStream,
                 final ChannelPromise promise) {
-            if (padding < 0) {
-                throw new IllegalArgumentException("padding must be >= 0");
-            }
+            checkPositiveOrZero(padding, "padding");
             this.padding = padding;
             this.endOfStream = endOfStream;
             this.stream = stream;
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2FrameWriter.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2FrameWriter.java
index 1e4cdcd..71a6653 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2FrameWriter.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2FrameWriter.java
@@ -61,6 +61,8 @@ import static io.netty.handler.codec.http2.Http2FrameTypes.RST_STREAM;
 import static io.netty.handler.codec.http2.Http2FrameTypes.SETTINGS;
 import static io.netty.handler.codec.http2.Http2FrameTypes.WINDOW_UPDATE;
 import static io.netty.util.internal.ObjectUtil.checkNotNull;
+import static io.netty.util.internal.ObjectUtil.checkPositive;
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
 import static java.lang.Math.max;
 import static java.lang.Math.min;
 
@@ -384,7 +386,7 @@ public class DefaultHttp2FrameWriter implements Http2FrameWriter, Http2FrameSize
             }
 
             if (!flags.endOfHeaders()) {
-                writeContinuationFrames(ctx, streamId, headerBlock, padding, promiseAggregator);
+                writeContinuationFrames(ctx, streamId, headerBlock, promiseAggregator);
             }
         } catch (Http2Exception e) {
             promiseAggregator.setFailure(e);
@@ -531,7 +533,7 @@ public class DefaultHttp2FrameWriter implements Http2FrameWriter, Http2FrameSize
             }
 
             if (!flags.endOfHeaders()) {
-                writeContinuationFrames(ctx, streamId, headerBlock, padding, promiseAggregator);
+                writeContinuationFrames(ctx, streamId, headerBlock, promiseAggregator);
             }
         } catch (Http2Exception e) {
             promiseAggregator.setFailure(e);
@@ -551,28 +553,19 @@ public class DefaultHttp2FrameWriter implements Http2FrameWriter, Http2FrameSize
      * Writes as many continuation frames as needed until {@code padding} and {@code headerBlock} are consumed.
      */
     private ChannelFuture writeContinuationFrames(ChannelHandlerContext ctx, int streamId,
-            ByteBuf headerBlock, int padding, SimpleChannelPromiseAggregator promiseAggregator) {
-        Http2Flags flags = new Http2Flags().paddingPresent(padding > 0);
-        int maxFragmentLength = maxFrameSize - padding;
-        // TODO: same padding is applied to all frames, is this desired?
-        if (maxFragmentLength <= 0) {
-            return promiseAggregator.setFailure(new IllegalArgumentException(
-                    "Padding [" + padding + "] is too large for max frame size [" + maxFrameSize + "]"));
-        }
+            ByteBuf headerBlock, SimpleChannelPromiseAggregator promiseAggregator) {
+        Http2Flags flags = new Http2Flags();
 
         if (headerBlock.isReadable()) {
             // The frame header (and padding) only changes on the last frame, so allocate it once and re-use
-            int fragmentReadableBytes = min(headerBlock.readableBytes(), maxFragmentLength);
-            int payloadLength = fragmentReadableBytes + padding;
+            int fragmentReadableBytes = min(headerBlock.readableBytes(), maxFrameSize);
             ByteBuf buf = ctx.alloc().buffer(CONTINUATION_FRAME_HEADER_LENGTH);
-            writeFrameHeaderInternal(buf, payloadLength, CONTINUATION, flags, streamId);
-            writePaddingLength(buf, padding);
+            writeFrameHeaderInternal(buf, fragmentReadableBytes, CONTINUATION, flags, streamId);
 
             do {
-                fragmentReadableBytes = min(headerBlock.readableBytes(), maxFragmentLength);
+                fragmentReadableBytes = min(headerBlock.readableBytes(), maxFrameSize);
                 ByteBuf fragment = headerBlock.readRetainedSlice(fragmentReadableBytes);
 
-                payloadLength = fragmentReadableBytes + padding;
                 if (headerBlock.isReadable()) {
                     ctx.write(buf.retain(), promiseAggregator.newPromise());
                 } else {
@@ -580,18 +573,13 @@ public class DefaultHttp2FrameWriter implements Http2FrameWriter, Http2FrameSize
                     flags = flags.endOfHeaders(true);
                     buf.release();
                     buf = ctx.alloc().buffer(CONTINUATION_FRAME_HEADER_LENGTH);
-                    writeFrameHeaderInternal(buf, payloadLength, CONTINUATION, flags, streamId);
-                    writePaddingLength(buf, padding);
+                    writeFrameHeaderInternal(buf, fragmentReadableBytes, CONTINUATION, flags, streamId);
                     ctx.write(buf, promiseAggregator.newPromise());
                 }
 
                 ctx.write(fragment, promiseAggregator.newPromise());
 
-                // Write out the padding, if any.
-                if (paddingBytes(padding) > 0) {
-                    ctx.write(ZERO_BUFFER.slice(0, paddingBytes(padding)), promiseAggregator.newPromise());
-                }
-            } while(headerBlock.isReadable());
+            } while (headerBlock.isReadable());
         }
         return promiseAggregator;
     }
@@ -614,15 +602,11 @@ public class DefaultHttp2FrameWriter implements Http2FrameWriter, Http2FrameSize
     }
 
     private static void verifyStreamId(int streamId, String argumentName) {
-        if (streamId <= 0) {
-            throw new IllegalArgumentException(argumentName + " must be > 0");
-        }
+        checkPositive(streamId, "streamId");
     }
 
     private static void verifyStreamOrConnectionId(int streamId, String argumentName) {
-        if (streamId < 0) {
-            throw new IllegalArgumentException(argumentName + " must be >= 0");
-        }
+        checkPositiveOrZero(streamId, "streamId");
     }
 
     private static void verifyWeight(short weight) {
@@ -638,9 +622,7 @@ public class DefaultHttp2FrameWriter implements Http2FrameWriter, Http2FrameSize
     }
 
     private static void verifyWindowSizeIncrement(int windowSizeIncrement) {
-        if (windowSizeIncrement < 0) {
-            throw new IllegalArgumentException("WindowSizeIncrement must be >= 0");
-        }
+        checkPositiveOrZero(windowSizeIncrement, "windowSizeIncrement");
     }
 
     private static void verifyPingPayload(ByteBuf data) {
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2GoAwayFrame.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2GoAwayFrame.java
index 7720767..2dbd738 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2GoAwayFrame.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2GoAwayFrame.java
@@ -15,6 +15,8 @@
  */
 package io.netty.handler.codec.http2;
 
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
+
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.DefaultByteBufHolder;
 import io.netty.buffer.Unpooled;
@@ -98,9 +100,7 @@ public final class DefaultHttp2GoAwayFrame extends DefaultByteBufHolder implemen
 
     @Override
     public Http2GoAwayFrame setExtraStreamIds(int extraStreamIds) {
-        if (extraStreamIds < 0) {
-            throw new IllegalArgumentException("extraStreamIds must be non-negative");
-        }
+        checkPositiveOrZero(extraStreamIds, "extraStreamIds");
         this.extraStreamIds = extraStreamIds;
         return this;
     }
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoder.java
index 5d63209..593e948 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoder.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoder.java
@@ -20,7 +20,6 @@ import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.UnstableApi;
 
 import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_HEADER_LIST_SIZE;
-import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_INITIAL_HUFFMAN_DECODE_CAPACITY;
 import static io.netty.handler.codec.http2.Http2Error.COMPRESSION_ERROR;
 import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR;
 import static io.netty.handler.codec.http2.Http2Exception.connectionError;
@@ -57,7 +56,7 @@ public class DefaultHttp2HeadersDecoder implements Http2HeadersDecoder, Http2Hea
      *  (which is dangerous).
      */
     public DefaultHttp2HeadersDecoder(boolean validateHeaders, long maxHeaderListSize) {
-        this(validateHeaders, maxHeaderListSize, DEFAULT_INITIAL_HUFFMAN_DECODE_CAPACITY);
+        this(validateHeaders, maxHeaderListSize, /* initialHuffmanDecodeCapacity= */ -1);
     }
 
     /**
@@ -67,11 +66,11 @@ public class DefaultHttp2HeadersDecoder implements Http2HeadersDecoder, Http2Hea
      *  This is because <a href="https://tools.ietf.org/html/rfc7540#section-6.5.1">SETTINGS_MAX_HEADER_LIST_SIZE</a>
      *  allows a lower than advertised limit from being enforced, and the default limit is unlimited
      *  (which is dangerous).
-     * @param initialHuffmanDecodeCapacity Size of an intermediate buffer used during huffman decode.
+     * @param initialHuffmanDecodeCapacity Does nothing, do not use.
      */
     public DefaultHttp2HeadersDecoder(boolean validateHeaders, long maxHeaderListSize,
-                                      int initialHuffmanDecodeCapacity) {
-        this(validateHeaders, new HpackDecoder(maxHeaderListSize, initialHuffmanDecodeCapacity));
+                                      @Deprecated int initialHuffmanDecodeCapacity) {
+        this(validateHeaders, new HpackDecoder(maxHeaderListSize));
     }
 
     /**
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersEncoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersEncoder.java
index 5e0a9f6..7b48b96 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersEncoder.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersEncoder.java
@@ -43,7 +43,13 @@ public class DefaultHttp2HeadersEncoder implements Http2HeadersEncoder, Http2Hea
 
     public DefaultHttp2HeadersEncoder(SensitivityDetector sensitivityDetector, boolean ignoreMaxHeaderListSize,
                                       int dynamicTableArraySizeHint) {
-        this(sensitivityDetector, new HpackEncoder(ignoreMaxHeaderListSize, dynamicTableArraySizeHint));
+        this(sensitivityDetector, ignoreMaxHeaderListSize, dynamicTableArraySizeHint, HpackEncoder.HUFF_CODE_THRESHOLD);
+    }
+
+    public DefaultHttp2HeadersEncoder(SensitivityDetector sensitivityDetector, boolean ignoreMaxHeaderListSize,
+                                      int dynamicTableArraySizeHint, int huffCodeThreshold) {
+        this(sensitivityDetector,
+                new HpackEncoder(ignoreMaxHeaderListSize, dynamicTableArraySizeHint, huffCodeThreshold));
     }
 
     /**
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2LocalFlowController.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2LocalFlowController.java
index 74dc3ae..12116c8 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2LocalFlowController.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2LocalFlowController.java
@@ -24,12 +24,14 @@ import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR;
 import static io.netty.handler.codec.http2.Http2Exception.connectionError;
 import static io.netty.handler.codec.http2.Http2Exception.streamError;
 import static io.netty.util.internal.ObjectUtil.checkNotNull;
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
 import static java.lang.Math.max;
 import static java.lang.Math.min;
 import io.netty.buffer.ByteBuf;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.handler.codec.http2.Http2Exception.CompositeStreamException;
 import io.netty.handler.codec.http2.Http2Exception.StreamException;
+import io.netty.handler.codec.http2.Http2Stream.State;
 import io.netty.util.internal.PlatformDependent;
 import io.netty.util.internal.UnstableApi;
 
@@ -108,8 +110,11 @@ public class DefaultHttp2LocalFlowController implements Http2LocalFlowController
                     FlowState state = state(stream);
                     int unconsumedBytes = state.unconsumedBytes();
                     if (ctx != null && unconsumedBytes > 0) {
-                        connectionState().consumeBytes(unconsumedBytes);
-                        state.consumeBytes(unconsumedBytes);
+                        if (consumeAllBytes(state, unconsumedBytes)) {
+                            // As the user has no real control on when this callback is used we should better
+                            // call flush() if we produced any window update to ensure we not stale.
+                            ctx.flush();
+                        }
                     }
                 } catch (Http2Exception e) {
                     PlatformDependent.throwException(e);
@@ -173,9 +178,7 @@ public class DefaultHttp2LocalFlowController implements Http2LocalFlowController
     @Override
     public boolean consumeBytes(Http2Stream stream, int numBytes) throws Http2Exception {
         assert ctx != null && ctx.executor().inEventLoop();
-        if (numBytes < 0) {
-            throw new IllegalArgumentException("numBytes must not be negative");
-        }
+        checkPositiveOrZero(numBytes, "numBytes");
         if (numBytes == 0) {
             return false;
         }
@@ -187,13 +190,15 @@ public class DefaultHttp2LocalFlowController implements Http2LocalFlowController
                 throw new UnsupportedOperationException("Returning bytes for the connection window is not supported");
             }
 
-            boolean windowUpdateSent = connectionState().consumeBytes(numBytes);
-            windowUpdateSent |= state(stream).consumeBytes(numBytes);
-            return windowUpdateSent;
+            return consumeAllBytes(state(stream), numBytes);
         }
         return false;
     }
 
+    private boolean consumeAllBytes(FlowState state, int numBytes) throws Http2Exception {
+        return connectionState().consumeBytes(numBytes) | state.consumeBytes(numBytes);
+    }
+
     @Override
     public int unconsumedBytes(Http2Stream stream) {
         return state(stream).unconsumedBytes();
@@ -296,7 +301,7 @@ public class DefaultHttp2LocalFlowController implements Http2LocalFlowController
      * received.
      */
     private final class AutoRefillState extends DefaultState {
-        public AutoRefillState(Http2Stream stream, int initialWindowSize) {
+        AutoRefillState(Http2Stream stream, int initialWindowSize) {
             super(stream, initialWindowSize);
         }
 
@@ -349,7 +354,7 @@ public class DefaultHttp2LocalFlowController implements Http2LocalFlowController
         private int lowerBound;
         private boolean endOfStream;
 
-        public DefaultState(Http2Stream stream, int initialWindowSize) {
+        DefaultState(Http2Stream stream, int initialWindowSize) {
             this.stream = stream;
             window(initialWindowSize);
             streamWindowUpdateRatio = windowUpdateRatio;
@@ -449,7 +454,9 @@ public class DefaultHttp2LocalFlowController implements Http2LocalFlowController
 
         @Override
         public boolean writeWindowUpdateIfNeeded() throws Http2Exception {
-            if (endOfStream || initialStreamWindowSize <= 0) {
+            if (endOfStream || initialStreamWindowSize <= 0 ||
+                    // If the stream is already closed there is no need to try to write a window update for it.
+                    isClosed(stream)) {
                 return false;
             }
 
@@ -613,7 +620,7 @@ public class DefaultHttp2LocalFlowController implements Http2LocalFlowController
         private CompositeStreamException compositeException;
         private final int delta;
 
-        public WindowUpdateVisitor(int delta) {
+        WindowUpdateVisitor(int delta) {
             this.delta = delta;
         }
 
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2PingFrame.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2PingFrame.java
index be5f2da..984f65d 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2PingFrame.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2PingFrame.java
@@ -32,10 +32,7 @@ public class DefaultHttp2PingFrame implements Http2PingFrame {
         this(content, false);
     }
 
-    /**
-     * A user cannot send a ping ack, as this is done automatically when a ping is received.
-     */
-    DefaultHttp2PingFrame(long content, boolean ack) {
+    public DefaultHttp2PingFrame(long content, boolean ack) {
         this.content = content;
         this.ack = ack;
     }
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2RemoteFlowController.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2RemoteFlowController.java
index 217cf8d..ef6ec98 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2RemoteFlowController.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2RemoteFlowController.java
@@ -31,6 +31,7 @@ import static io.netty.handler.codec.http2.Http2Error.STREAM_CLOSED;
 import static io.netty.handler.codec.http2.Http2Exception.streamError;
 import static io.netty.handler.codec.http2.Http2Stream.State.HALF_CLOSED_LOCAL;
 import static io.netty.util.internal.ObjectUtil.checkNotNull;
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
 import static java.lang.Math.max;
 import static java.lang.Math.min;
 
@@ -635,9 +636,7 @@ public class DefaultHttp2RemoteFlowController implements Http2RemoteFlowControll
         }
 
         void initialWindowSize(int newWindowSize) throws Http2Exception {
-            if (newWindowSize < 0) {
-                throw new IllegalArgumentException("Invalid initial window size: " + newWindowSize);
-            }
+            checkPositiveOrZero(newWindowSize, "newWindowSize");
 
             final int delta = newWindowSize - initialWindowSize;
             initialWindowSize = newWindowSize;
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2SettingsAckFrame.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2SettingsAckFrame.java
new file mode 100644
index 0000000..259b4a0
--- /dev/null
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2SettingsAckFrame.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2019 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.util.internal.StringUtil;
+
+/**
+ * The default {@link Http2SettingsAckFrame} implementation.
+ */
+final class DefaultHttp2SettingsAckFrame implements Http2SettingsAckFrame {
+    @Override
+    public String name() {
+        return "SETTINGS(ACK)";
+    }
+
+    @Override
+    public String toString() {
+        return StringUtil.simpleClassName(this);
+    }
+}
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2SettingsFrame.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2SettingsFrame.java
index c60f59f..f0b6b94 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2SettingsFrame.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2SettingsFrame.java
@@ -42,6 +42,20 @@ public class DefaultHttp2SettingsFrame implements Http2SettingsFrame {
         return "SETTINGS";
     }
 
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof Http2SettingsFrame)) {
+            return false;
+        }
+        Http2SettingsFrame other = (Http2SettingsFrame) o;
+        return settings.equals(other.settings());
+    }
+
+    @Override
+    public int hashCode() {
+        return settings.hashCode();
+    }
+
     @Override
     public String toString() {
         return StringUtil.simpleClassName(this) + "(settings=" + settings + ')';
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2UnknownFrame.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2UnknownFrame.java
index 65289d4..4bc77f8 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2UnknownFrame.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2UnknownFrame.java
@@ -80,7 +80,7 @@ public final class DefaultHttp2UnknownFrame extends DefaultByteBufHolder impleme
 
     @Override
     public DefaultHttp2UnknownFrame replace(ByteBuf content) {
-        return new DefaultHttp2UnknownFrame(frameType, flags, content).stream(stream());
+        return new DefaultHttp2UnknownFrame(frameType, flags, content).stream(stream);
     }
 
     @Override
@@ -97,8 +97,8 @@ public final class DefaultHttp2UnknownFrame extends DefaultByteBufHolder impleme
 
     @Override
     public String toString() {
-        return StringUtil.simpleClassName(this) + "(frameType=" + frameType() + ", stream=" + stream() +
-                ", flags=" + flags() + ", content=" + contentToString() + ')';
+        return StringUtil.simpleClassName(this) + "(frameType=" + frameType + ", stream=" + stream +
+               ", flags=" + flags + ", content=" + contentToString() + ')';
     }
 
     @Override
@@ -119,18 +119,20 @@ public final class DefaultHttp2UnknownFrame extends DefaultByteBufHolder impleme
             return false;
         }
         DefaultHttp2UnknownFrame other = (DefaultHttp2UnknownFrame) o;
-        return super.equals(other) && flags().equals(other.flags())
-                && frameType() == other.frameType() && (stream() == null && other.stream() == null) ||
-                stream().equals(other.stream());
+        Http2FrameStream otherStream = other.stream();
+        return (stream == otherStream || otherStream != null && otherStream.equals(stream))
+               && flags.equals(other.flags())
+               && frameType == other.frameType()
+               && super.equals(other);
     }
 
     @Override
     public int hashCode() {
         int hash = super.hashCode();
-        hash = hash * 31 + frameType();
-        hash = hash * 31 + flags().hashCode();
-        if (stream() != null) {
-            hash = hash * 31 + stream().hashCode();
+        hash = hash * 31 + frameType;
+        hash = hash * 31 + flags.hashCode();
+        if (stream != null) {
+            hash = hash * 31 + stream.hashCode();
         }
 
         return hash;
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DelegatingDecompressorFrameListener.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DelegatingDecompressorFrameListener.java
index 78ef230..6793f28 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DelegatingDecompressorFrameListener.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DelegatingDecompressorFrameListener.java
@@ -33,9 +33,10 @@ import static io.netty.handler.codec.http.HttpHeaderValues.X_GZIP;
 import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR;
 import static io.netty.handler.codec.http2.Http2Exception.streamError;
 import static io.netty.util.internal.ObjectUtil.checkNotNull;
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
 
 /**
- * A HTTP2 frame listener that will decompress data frames according to the {@code content-encoding} header for each
+ * An HTTP2 frame listener that will decompress data frames according to the {@code content-encoding} header for each
  * stream. The decompression provided by this class will be applied to the data for the entire stream.
  */
 @UnstableApi
@@ -398,9 +399,7 @@ public class DelegatingDecompressorFrameListener extends Http2FrameListenerDecor
          * @return The number of pre-decompressed bytes that have been consumed.
          */
         int consumeBytes(int streamId, int decompressedBytes) throws Http2Exception {
-            if (decompressedBytes < 0) {
-                throw new IllegalArgumentException("decompressedBytes must not be negative: " + decompressedBytes);
-            }
+            checkPositiveOrZero(decompressedBytes, "decompressedBytes");
             if (decompressed - decompressedBytes < 0) {
                 throw streamError(streamId, INTERNAL_ERROR,
                         "Attempting to return too many bytes for stream %d. decompressed: %d " +
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 9c680e6..6070bc5 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
@@ -53,24 +53,31 @@ import static io.netty.util.internal.ThrowableUtil.unknownStackTrace;
 
 final class HpackDecoder {
     private static final Http2Exception DECODE_ULE_128_DECOMPRESSION_EXCEPTION = unknownStackTrace(
-            connectionError(COMPRESSION_ERROR, "HPACK - decompression failure"), HpackDecoder.class,
+            Http2Exception.newStatic(COMPRESSION_ERROR, "HPACK - decompression failure",
+                    Http2Exception.ShutdownHint.HARD_SHUTDOWN), HpackDecoder.class,
             "decodeULE128(..)");
     private static final Http2Exception DECODE_ULE_128_TO_LONG_DECOMPRESSION_EXCEPTION = unknownStackTrace(
-            connectionError(COMPRESSION_ERROR, "HPACK - long overflow"), HpackDecoder.class, "decodeULE128(..)");
+            Http2Exception.newStatic(COMPRESSION_ERROR, "HPACK - long overflow",
+                    Http2Exception.ShutdownHint.HARD_SHUTDOWN), HpackDecoder.class, "decodeULE128(..)");
     private static final Http2Exception DECODE_ULE_128_TO_INT_DECOMPRESSION_EXCEPTION = unknownStackTrace(
-            connectionError(COMPRESSION_ERROR, "HPACK - int overflow"), HpackDecoder.class, "decodeULE128ToInt(..)");
+            Http2Exception.newStatic(COMPRESSION_ERROR, "HPACK - int overflow",
+                    Http2Exception.ShutdownHint.HARD_SHUTDOWN), HpackDecoder.class, "decodeULE128ToInt(..)");
     private static final Http2Exception DECODE_ILLEGAL_INDEX_VALUE = unknownStackTrace(
-            connectionError(COMPRESSION_ERROR, "HPACK - illegal index value"), HpackDecoder.class, "decode(..)");
+            Http2Exception.newStatic(COMPRESSION_ERROR, "HPACK - illegal index value",
+                    Http2Exception.ShutdownHint.HARD_SHUTDOWN), HpackDecoder.class, "decode(..)");
     private static final Http2Exception INDEX_HEADER_ILLEGAL_INDEX_VALUE = unknownStackTrace(
-            connectionError(COMPRESSION_ERROR, "HPACK - illegal index value"), HpackDecoder.class, "indexHeader(..)");
+            Http2Exception.newStatic(COMPRESSION_ERROR, "HPACK - illegal index value",
+                    Http2Exception.ShutdownHint.HARD_SHUTDOWN), HpackDecoder.class, "indexHeader(..)");
     private static final Http2Exception READ_NAME_ILLEGAL_INDEX_VALUE = unknownStackTrace(
-            connectionError(COMPRESSION_ERROR, "HPACK - illegal index value"), HpackDecoder.class, "readName(..)");
+            Http2Exception.newStatic(COMPRESSION_ERROR, "HPACK - illegal index value",
+                    Http2Exception.ShutdownHint.HARD_SHUTDOWN), HpackDecoder.class, "readName(..)");
     private static final Http2Exception INVALID_MAX_DYNAMIC_TABLE_SIZE = unknownStackTrace(
-            connectionError(COMPRESSION_ERROR, "HPACK - invalid max dynamic table size"), HpackDecoder.class,
+            Http2Exception.newStatic(COMPRESSION_ERROR, "HPACK - invalid max dynamic table size",
+                    Http2Exception.ShutdownHint.HARD_SHUTDOWN), HpackDecoder.class,
             "setDynamicTableSize(..)");
     private static final Http2Exception MAX_DYNAMIC_TABLE_SIZE_CHANGE_REQUIRED = unknownStackTrace(
-            connectionError(COMPRESSION_ERROR, "HPACK - max dynamic table size change required"), HpackDecoder.class,
-            "decode(..)");
+            Http2Exception.newStatic(COMPRESSION_ERROR, "HPACK - max dynamic table size change required",
+                    Http2Exception.ShutdownHint.HARD_SHUTDOWN), HpackDecoder.class, "decode(..)");
     private static final byte READ_HEADER_REPRESENTATION = 0;
     private static final byte READ_MAX_DYNAMIC_TABLE_SIZE = 1;
     private static final byte READ_INDEXED_HEADER = 2;
@@ -82,8 +89,8 @@ final class HpackDecoder {
     private static final byte READ_LITERAL_HEADER_VALUE_LENGTH = 8;
     private static final byte READ_LITERAL_HEADER_VALUE = 9;
 
+    private final HpackHuffmanDecoder huffmanDecoder = new HpackHuffmanDecoder();
     private final HpackDynamicTable hpackDynamicTable;
-    private final HpackHuffmanDecoder hpackHuffmanDecoder;
     private long maxHeaderListSize;
     private long maxDynamicTableSize;
     private long encoderMaxDynamicTableSize;
@@ -95,23 +102,21 @@ final class HpackDecoder {
      *  This is because <a href="https://tools.ietf.org/html/rfc7540#section-6.5.1">SETTINGS_MAX_HEADER_LIST_SIZE</a>
      *  allows a lower than advertised limit from being enforced, and the default limit is unlimited
      *  (which is dangerous).
-     * @param initialHuffmanDecodeCapacity Size of an intermediate buffer used during huffman decode.
      */
-    HpackDecoder(long maxHeaderListSize, int initialHuffmanDecodeCapacity) {
-        this(maxHeaderListSize, initialHuffmanDecodeCapacity, DEFAULT_HEADER_TABLE_SIZE);
+    HpackDecoder(long maxHeaderListSize) {
+        this(maxHeaderListSize, DEFAULT_HEADER_TABLE_SIZE);
     }
 
     /**
      * Exposed Used for testing only! Default values used in the initial settings frame are overridden intentionally
      * for testing but violate the RFC if used outside the scope of testing.
      */
-    HpackDecoder(long maxHeaderListSize, int initialHuffmanDecodeCapacity, int maxHeaderTableSize) {
+    HpackDecoder(long maxHeaderListSize, int maxHeaderTableSize) {
         this.maxHeaderListSize = checkPositive(maxHeaderListSize, "maxHeaderListSize");
 
         maxDynamicTableSize = encoderMaxDynamicTableSize = maxHeaderTableSize;
         maxDynamicTableSizeChangeRequired = false;
         hpackDynamicTable = new HpackDynamicTable(maxHeaderTableSize);
-        hpackHuffmanDecoder = new HpackHuffmanDecoder(initialHuffmanDecodeCapacity);
     }
 
     /**
@@ -441,7 +446,7 @@ final class HpackDecoder {
 
     private CharSequence readStringLiteral(ByteBuf in, int length, boolean huffmanEncoded) throws Http2Exception {
         if (huffmanEncoded) {
-            return hpackHuffmanDecoder.decode(in, length);
+            return huffmanDecoder.decode(in, length);
         }
         byte[] buf = new byte[length];
         in.readBytes(buf);
@@ -528,7 +533,7 @@ final class HpackDecoder {
         private HeaderType previousType;
         private Http2Exception validationException;
 
-        public Http2HeadersSink(int streamId, Http2Headers headers, long maxHeaderListSize, boolean validate) {
+        Http2HeadersSink(int streamId, Http2Headers headers, long maxHeaderListSize, boolean validate) {
             this.headers = headers;
             this.maxHeaderListSize = maxHeaderListSize;
             this.streamId = streamId;
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackEncoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackEncoder.java
index 7719072..5dab8f8 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackEncoder.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackEncoder.java
@@ -41,6 +41,7 @@ import java.util.Arrays;
 import java.util.Map;
 
 import static io.netty.handler.codec.http2.HpackUtil.equalsConstantTime;
+import static io.netty.handler.codec.http2.HpackUtil.equalsVariableTime;
 import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_HEADER_TABLE_SIZE;
 import static io.netty.handler.codec.http2.Http2CodecUtil.MAX_HEADER_LIST_SIZE;
 import static io.netty.handler.codec.http2.Http2CodecUtil.MAX_HEADER_TABLE_SIZE;
@@ -53,7 +54,15 @@ import static io.netty.util.internal.MathUtil.findNextPositivePowerOfTwo;
 import static java.lang.Math.max;
 import static java.lang.Math.min;
 
+/**
+ * An HPACK encoder.
+ *
+ * <p>Implementation note:  This class is security sensitive, and depends on users correctly identifying their headers
+ * as security sensitive or not.  If a header is considered not sensitive, methods names "insensitive" are used which
+ * are fast, but don't provide any security guarantees.
+ */
 final class HpackEncoder {
+    static final int HUFF_CODE_THRESHOLD = 512;
     // a linked hash map of header fields
     private final HeaderEntry[] headerFields;
     private final HeaderEntry head = new HeaderEntry(-1, AsciiString.EMPTY_STRING,
@@ -61,6 +70,7 @@ final class HpackEncoder {
     private final HpackHuffmanEncoder hpackHuffmanEncoder = new HpackHuffmanEncoder();
     private final byte hashMask;
     private final boolean ignoreMaxHeaderListSize;
+    private final int huffCodeThreshold;
     private long size;
     private long maxHeaderTableSize;
     private long maxHeaderListSize;
@@ -75,14 +85,14 @@ final class HpackEncoder {
     /**
      * Creates a new encoder.
      */
-    public HpackEncoder(boolean ignoreMaxHeaderListSize) {
-        this(ignoreMaxHeaderListSize, 16);
+    HpackEncoder(boolean ignoreMaxHeaderListSize) {
+        this(ignoreMaxHeaderListSize, 16, HUFF_CODE_THRESHOLD);
     }
 
     /**
      * Creates a new encoder.
      */
-    public HpackEncoder(boolean ignoreMaxHeaderListSize, int arraySizeHint) {
+    HpackEncoder(boolean ignoreMaxHeaderListSize, int arraySizeHint, int huffCodeThreshold) {
         this.ignoreMaxHeaderListSize = ignoreMaxHeaderListSize;
         maxHeaderTableSize = DEFAULT_HEADER_TABLE_SIZE;
         maxHeaderListSize = MAX_HEADER_LIST_SIZE;
@@ -91,6 +101,7 @@ final class HpackEncoder {
         headerFields = new HeaderEntry[findNextPositivePowerOfTwo(max(2, min(arraySizeHint, 128)))];
         hashMask = (byte) (headerFields.length - 1);
         head.before = head.after = head;
+        this.huffCodeThreshold = huffCodeThreshold;
     }
 
     /**
@@ -150,7 +161,7 @@ final class HpackEncoder {
 
         // If the peer will only use the static table
         if (maxHeaderTableSize == 0) {
-            int staticTableIndex = HpackStaticTable.getIndex(name, value);
+            int staticTableIndex = HpackStaticTable.getIndexInsensitive(name, value);
             if (staticTableIndex == -1) {
                 int nameIndex = HpackStaticTable.getIndex(name);
                 encodeLiteral(out, name, value, IndexType.NONE, nameIndex);
@@ -167,13 +178,13 @@ final class HpackEncoder {
             return;
         }
 
-        HeaderEntry headerField = getEntry(name, value);
+        HeaderEntry headerField = getEntryInsensitive(name, value);
         if (headerField != null) {
             int index = getIndex(headerField.index) + HpackStaticTable.length;
             // Section 6.1. Indexed Header Field Representation
             encodeInteger(out, 0x80, 7, index);
         } else {
-            int staticTableIndex = HpackStaticTable.getIndex(name, value);
+            int staticTableIndex = HpackStaticTable.getIndexInsensitive(name, value);
             if (staticTableIndex != -1) {
                 // Section 6.1. Indexed Header Field Representation
                 encodeInteger(out, 0x80, 7, staticTableIndex);
@@ -250,8 +261,9 @@ final class HpackEncoder {
      * Encode string literal according to Section 5.2.
      */
     private void encodeStringLiteral(ByteBuf out, CharSequence string) {
-        int huffmanLength = hpackHuffmanEncoder.getEncodedLength(string);
-        if (huffmanLength < string.length()) {
+        int huffmanLength;
+        if (string.length() >= huffCodeThreshold
+                && (huffmanLength = hpackHuffmanEncoder.getEncodedLength(string)) < string.length()) {
             encodeInteger(out, 0x80, 7, huffmanLength);
             hpackHuffmanEncoder.encode(out, string);
         } else {
@@ -347,15 +359,16 @@ final class HpackEncoder {
      * Returns the header entry with the lowest index value for the header field. Returns null if
      * header field is not in the dynamic table.
      */
-    private HeaderEntry getEntry(CharSequence name, CharSequence value) {
+    private HeaderEntry getEntryInsensitive(CharSequence name, CharSequence value) {
         if (length() == 0 || name == null || value == null) {
             return null;
         }
         int h = AsciiString.hashCode(name);
         int i = index(h);
         for (HeaderEntry e = headerFields[i]; e != null; e = e.next) {
-            // To avoid short circuit behavior a bitwise operator is used instead of a boolean operator.
-            if (e.hash == h && (equalsConstantTime(name, e.name) & equalsConstantTime(value, e.value)) != 0) {
+            // Check the value before then name, as it is more likely the value will be different incase there is no
+            // match.
+            if (e.hash == h && equalsVariableTime(value, e.value) && equalsVariableTime(name, e.name)) {
                 return e;
             }
         }
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackHeaderField.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackHeaderField.java
index 0b0d646..0cfddba 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackHeaderField.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackHeaderField.java
@@ -31,6 +31,7 @@
  */
 package io.netty.handler.codec.http2;
 
+import static io.netty.handler.codec.http2.HpackUtil.equalsVariableTime;
 import static io.netty.util.internal.ObjectUtil.checkNotNull;
 
 class HpackHeaderField {
@@ -57,23 +58,8 @@ class HpackHeaderField {
         return name.length() + value.length() + HEADER_ENTRY_OVERHEAD;
     }
 
-    @Override
-    public final int hashCode() {
-        // TODO(nmittler): Netty's build rules require this. Probably need a better implementation.
-        return super.hashCode();
-    }
-
-    @Override
-    public final boolean equals(Object obj) {
-        if (obj == this) {
-            return true;
-        }
-        if (!(obj instanceof HpackHeaderField)) {
-            return false;
-        }
-        HpackHeaderField other = (HpackHeaderField) obj;
-        // To avoid short circuit behavior a bitwise operator is used instead of a boolean operator.
-        return (HpackUtil.equalsConstantTime(name, other.name) & HpackUtil.equalsConstantTime(value, other.value)) != 0;
+    public final boolean equalsForTest(HpackHeaderField other) {
+        return equalsVariableTime(name, other.name) && equalsVariableTime(value, other.value);
     }
 
     @Override
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackHuffmanDecoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackHuffmanDecoder.java
index 9549c66..0b4a42c 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackHuffmanDecoder.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackHuffmanDecoder.java
@@ -34,212 +34,4704 @@ package io.netty.handler.codec.http2;
 import io.netty.buffer.ByteBuf;
 import io.netty.util.AsciiString;
 import io.netty.util.ByteProcessor;
-import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.ThrowableUtil;
 
 import static io.netty.handler.codec.http2.Http2Error.COMPRESSION_ERROR;
-import static io.netty.handler.codec.http2.Http2Exception.connectionError;
 
-final class HpackHuffmanDecoder {
+final class HpackHuffmanDecoder implements ByteProcessor {
 
-    private static final Http2Exception EOS_DECODED = ThrowableUtil.unknownStackTrace(
-            connectionError(COMPRESSION_ERROR, "HPACK - EOS Decoded"), HpackHuffmanDecoder.class, "decode(..)");
-    private static final Http2Exception INVALID_PADDING = ThrowableUtil.unknownStackTrace(
-            connectionError(COMPRESSION_ERROR, "HPACK - Invalid Padding"), HpackHuffmanDecoder.class, "decode(..)");
+    /* Scroll to the bottom! */
 
-    private static final Node ROOT = buildTree(HpackUtil.HUFFMAN_CODES, HpackUtil.HUFFMAN_CODE_LENGTHS);
+    private static final byte HUFFMAN_COMPLETE = 1;
+    private static final byte HUFFMAN_EMIT_SYMBOL = 1 << 1;
+    private static final byte HUFFMAN_FAIL = 1 << 2;
 
-    private final DecoderProcessor processor;
-
-    HpackHuffmanDecoder(int initialCapacity) {
-        processor = new DecoderProcessor(initialCapacity);
-    }
+    private static final int HUFFMAN_COMPLETE_SHIFT = HUFFMAN_COMPLETE << 8;
+    private static final int HUFFMAN_EMIT_SYMBOL_SHIFT = HUFFMAN_EMIT_SYMBOL << 8;
+    private static final int HUFFMAN_FAIL_SHIFT = HUFFMAN_FAIL << 8;
 
     /**
-     * Decompresses the given Huffman coded string literal.
+     * A table of byte tuples (state, flags, output).   They are packed together as:
      *
-     * @param buf the string literal to be decoded
-     * @return the output stream for the compressed data
-     * @throws Http2Exception EOS Decoded
+     * state<<16 + flags<<8 + output
      */
-    public AsciiString decode(ByteBuf buf, int length) throws Http2Exception {
-        processor.reset();
-        buf.forEachByte(buf.readerIndex(), length, processor);
-        buf.skipBytes(length);
-        return processor.end();
-    }
+    private static final int[] HUFFS = new int[] {
+            // Node 0 (Root Node, never emits symbols.)
+            (4 << 16) + (0 << 8) + 0,
+            (5 << 16) + (0 << 8) + 0,
+            (7 << 16) + (0 << 8) + 0,
+            (8 << 16) + (0 << 8) + 0,
+            (11 << 16) + (0 << 8) + 0,
+            (12 << 16) + (0 << 8) + 0,
+            (16 << 16) + (0 << 8) + 0,
+            (19 << 16) + (0 << 8) + 0,
+            (25 << 16) + (0 << 8) + 0,
+            (28 << 16) + (0 << 8) + 0,
+            (32 << 16) + (0 << 8) + 0,
+            (35 << 16) + (0 << 8) + 0,
+            (42 << 16) + (0 << 8) + 0,
+            (49 << 16) + (0 << 8) + 0,
+            (57 << 16) + (0 << 8) + 0,
+            (64 << 16) + (HUFFMAN_COMPLETE << 8) + 0,
 
-    private static final class Node {
+            // Node 1
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 48,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 49,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 50,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 97,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 99,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 101,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 105,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 111,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 115,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 116,
+            (13 << 16) + (0 << 8) + 0,
+            (14 << 16) + (0 << 8) + 0,
+            (17 << 16) + (0 << 8) + 0,
+            (18 << 16) + (0 << 8) + 0,
+            (20 << 16) + (0 << 8) + 0,
+            (21 << 16) + (0 << 8) + 0,
 
-        private final int symbol;      // terminal nodes have a symbol
-        private final int bits;        // number of bits matched by the node
-        private final Node[] children; // internal nodes have children
+            // Node 2
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 48,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 48,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 49,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 49,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 50,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 50,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 97,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 97,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 99,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 99,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 101,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 101,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 105,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 105,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 111,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 111,
 
-        /**
-         * Construct an internal node
-         */
-        Node() {
-            symbol = 0;
-            bits = 8;
-            children = new Node[256];
-        }
+            // Node 3
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 48,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 48,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 48,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 48,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 49,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 49,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 49,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 49,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 50,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 50,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 50,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 50,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 97,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 97,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 97,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 97,
 
-        /**
-         * Construct a terminal node
-         *
-         * @param symbol the symbol the node represents
-         * @param bits the number of bits matched by this node
-         */
-        Node(int symbol, int bits) {
-            assert bits > 0 && bits <= 8;
-            this.symbol = symbol;
-            this.bits = bits;
-            children = null;
-        }
+            // Node 4
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 48,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 48,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 48,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 48,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 48,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 48,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 48,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 48,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 49,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 49,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 49,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 49,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 49,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 49,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 49,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 49,
 
-        private boolean isTerminal() {
-            return children == null;
-        }
-    }
+            // Node 5
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 50,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 50,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 50,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 50,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 50,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 50,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 50,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 50,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 97,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 97,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 97,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 97,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 97,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 97,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 97,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 97,
 
-    private static Node buildTree(int[] codes, byte[] lengths) {
-        Node root = new Node();
-        for (int i = 0; i < codes.length; i++) {
-            insert(root, i, codes[i], lengths[i]);
-        }
-        return root;
-    }
+            // Node 6
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 99,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 99,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 99,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 99,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 101,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 101,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 101,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 101,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 105,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 105,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 105,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 105,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 111,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 111,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 111,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 111,
 
-    private static void insert(Node root, int symbol, int code, byte length) {
-        // traverse tree using the most significant bytes of code
-        Node current = root;
-        while (length > 8) {
-            if (current.isTerminal()) {
-                throw new IllegalStateException("invalid Huffman code: prefix not unique");
-            }
-            length -= 8;
-            int i = (code >>> length) & 0xFF;
-            if (current.children[i] == null) {
-                current.children[i] = new Node();
-            }
-            current = current.children[i];
-        }
+            // Node 7
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 99,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 99,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 99,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 99,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 99,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 99,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 99,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 99,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 101,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 101,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 101,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 101,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 101,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 101,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 101,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 101,
 
-        Node terminal = new Node(symbol, length);
-        int shift = 8 - length;
-        int start = (code << shift) & 0xFF;
-        int end = 1 << shift;
-        for (int i = start; i < start + end; i++) {
-            current.children[i] = terminal;
-        }
-    }
+            // Node 8
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 105,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 105,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 105,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 105,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 105,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 105,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 105,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 105,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 111,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 111,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 111,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 111,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 111,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 111,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 111,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 111,
 
-    private static final class DecoderProcessor implements ByteProcessor {
-        private final int initialCapacity;
-        private byte[] bytes;
-        private int index;
-        private Node node;
-        private int current;
-        private int currentBits;
-        private int symbolBits;
-
-        DecoderProcessor(int initialCapacity) {
-            this.initialCapacity = ObjectUtil.checkPositive(initialCapacity, "initialCapacity");
-        }
+            // Node 9
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 115,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 115,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 116,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 116,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 32,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 37,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 45,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 46,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 47,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 51,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 52,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 53,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 54,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 55,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 56,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 57,
 
-        void reset() {
-            node = ROOT;
-            current = 0;
-            currentBits = 0;
-            symbolBits = 0;
-            bytes = new byte[initialCapacity];
-            index = 0;
-        }
+            // Node 10
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 115,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 115,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 115,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 115,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 116,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 116,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 116,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 116,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 32,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 32,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 37,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 37,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 45,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 45,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 46,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 46,
 
-        /*
-         * The idea here is to consume whole bytes at a time rather than individual bits. node
-         * represents the Huffman tree, with all bit patterns denormalized as 256 children. Each
-         * child represents the last 8 bits of the huffman code. The parents of each child each
-         * represent the successive 8 bit chunks that lead up to the last most part. 8 bit bytes
-         * from buf are used to traverse these tree until a terminal node is found.
-         *
-         * current is a bit buffer. The low order bits represent how much of the huffman code has
-         * not been used to traverse the tree. Thus, the high order bits are just garbage.
-         * currentBits represents how many of the low order bits of current are actually valid.
-         * currentBits will vary between 0 and 15.
-         *
-         * symbolBits is the number of bits of the symbol being decoded, *including* all those of
-         * the parent nodes. symbolBits tells how far down the tree we are. For example, when
-         * decoding the invalid sequence {0xff, 0xff}, currentBits will be 0, but symbolBits will be
-         * 16. This is used to know if buf ended early (before consuming a whole symbol) or if
-         * there is too much padding.
-         */
-        @Override
-        public boolean process(byte value) throws Http2Exception {
-            current = (current << 8) | (value & 0xFF);
-            currentBits += 8;
-            symbolBits += 8;
-            // While there are unconsumed bits in current, keep consuming symbols.
-            do {
-                node = node.children[(current >>> (currentBits - 8)) & 0xFF];
-                currentBits -= node.bits;
-                if (node.isTerminal()) {
-                    if (node.symbol == HpackUtil.HUFFMAN_EOS) {
-                        throw EOS_DECODED;
-                    }
-                    append(node.symbol);
-                    node = ROOT;
-                    // Upon consuming a whole symbol, reset the symbol bits to the number of bits
-                    // left over in the byte.
-                    symbolBits = currentBits;
-                }
-            } while (currentBits >= 8);
-            return true;
-        }
+            // Node 11
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 115,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 115,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 115,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 115,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 115,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 115,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 115,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 115,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 116,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 116,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 116,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 116,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 116,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 116,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 116,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 116,
 
-        AsciiString end() throws Http2Exception {
-            /*
-             * We have consumed all the bytes in buf, but haven't consumed all the symbols. We may be on
-             * a partial symbol, so consume until there is nothing left. This will loop at most 2 times.
-             */
-            while (currentBits > 0) {
-                node = node.children[(current << (8 - currentBits)) & 0xFF];
-                if (node.isTerminal() && node.bits <= currentBits) {
-                    if (node.symbol == HpackUtil.HUFFMAN_EOS) {
-                        throw EOS_DECODED;
-                    }
-                    currentBits -= node.bits;
-                    append(node.symbol);
-                    node = ROOT;
-                    symbolBits = currentBits;
-                } else {
-                    break;
-                }
-            }
+            // Node 12
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 32,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 32,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 32,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 32,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 37,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 37,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 37,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 37,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 45,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 45,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 45,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 45,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 46,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 46,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 46,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 46,
+
+            // Node 13
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 32,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 32,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 32,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 32,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 32,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 32,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 32,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 32,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 37,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 37,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 37,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 37,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 37,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 37,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 37,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 37,
+
+            // Node 14
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 45,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 45,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 45,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 45,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 45,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 45,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 45,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 45,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 46,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 46,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 46,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 46,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 46,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 46,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 46,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 46,
+
+            // Node 15
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 47,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 47,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 51,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 51,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 52,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 52,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 53,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 53,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 54,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 54,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 55,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 55,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 56,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 56,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 57,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 57,
+
+            // Node 16
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 47,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 47,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 47,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 47,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 51,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 51,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 51,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 51,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 52,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 52,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 52,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 52,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 53,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 53,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 53,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 53,
+
+            // Node 17
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 47,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 47,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 47,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 47,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 47,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 47,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 47,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 47,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 51,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 51,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 51,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 51,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 51,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 51,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 51,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 51,
+
+            // Node 18
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 52,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 52,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 52,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 52,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 52,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 52,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 52,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 52,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 53,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 53,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 53,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 53,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 53,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 53,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 53,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 53,
+
+            // Node 19
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 54,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 54,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 54,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 54,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 55,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 55,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 55,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 55,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 56,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 56,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 56,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 56,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 57,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 57,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 57,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 57,
+
+            // Node 20
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 54,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 54,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 54,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 54,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 54,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 54,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 54,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 54,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 55,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 55,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 55,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 55,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 55,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 55,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 55,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 55,
+
+            // Node 21
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 56,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 56,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 56,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 56,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 56,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 56,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 56,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 56,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 57,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 57,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 57,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 57,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 57,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 57,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 57,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 57,
+
+            // Node 22
+            (26 << 16) + (0 << 8) + 0,
+            (27 << 16) + (0 << 8) + 0,
+            (29 << 16) + (0 << 8) + 0,
+            (30 << 16) + (0 << 8) + 0,
+            (33 << 16) + (0 << 8) + 0,
+            (34 << 16) + (0 << 8) + 0,
+            (36 << 16) + (0 << 8) + 0,
+            (37 << 16) + (0 << 8) + 0,
+            (43 << 16) + (0 << 8) + 0,
+            (46 << 16) + (0 << 8) + 0,
+            (50 << 16) + (0 << 8) + 0,
+            (53 << 16) + (0 << 8) + 0,
+            (58 << 16) + (0 << 8) + 0,
+            (61 << 16) + (0 << 8) + 0,
+            (65 << 16) + (0 << 8) + 0,
+            (68 << 16) + (HUFFMAN_COMPLETE << 8) + 0,
+
+            // Node 23
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 61,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 65,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 95,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 98,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 100,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 102,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 103,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 104,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 108,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 109,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 110,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 112,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 114,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 117,
+            (38 << 16) + (0 << 8) + 0,
+            (39 << 16) + (0 << 8) + 0,
+
+            // Node 24
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 61,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 61,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 65,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 65,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 95,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 95,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 98,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 98,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 100,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 100,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 102,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 102,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 103,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 103,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 104,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 104,
+
+            // Node 25
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 61,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 61,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 61,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 61,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 65,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 65,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 65,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 65,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 95,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 95,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 95,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 95,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 98,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 98,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 98,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 98,
+
+            // Node 26
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 61,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 61,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 61,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 61,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 61,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 61,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 61,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 61,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 65,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 65,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 65,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 65,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 65,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 65,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 65,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 65,
+
+            // Node 27
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 95,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 95,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 95,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 95,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 95,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 95,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 95,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 95,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 98,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 98,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 98,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 98,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 98,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 98,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 98,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 98,
+
+            // Node 28
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 100,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 100,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 100,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 100,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 102,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 102,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 102,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 102,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 103,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 103,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 103,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 103,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 104,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 104,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 104,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 104,
+
+            // Node 29
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 100,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 100,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 100,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 100,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 100,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 100,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 100,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 100,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 102,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 102,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 102,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 102,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 102,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 102,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 102,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 102,
+
+            // Node 30
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 103,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 103,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 103,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 103,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 103,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 103,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 103,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 103,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 104,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 104,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 104,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 104,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 104,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 104,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 104,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 104,
+
+            // Node 31
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 108,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 108,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 109,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 109,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 110,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 110,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 112,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 112,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 114,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 114,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 117,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 117,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 58,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 66,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 67,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 68,
+
+            // Node 32
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 108,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 108,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 108,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 108,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 109,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 109,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 109,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 109,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 110,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 110,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 110,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 110,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 112,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 112,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 112,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 112,
+
+            // Node 33
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 108,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 108,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 108,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 108,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 108,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 108,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 108,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 108,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 109,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 109,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 109,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 109,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 109,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 109,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 109,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 109,
+
+            // Node 34
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 110,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 110,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 110,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 110,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 110,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 110,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 110,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 110,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 112,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 112,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 112,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 112,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 112,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 112,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 112,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 112,
+
+            // Node 35
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 114,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 114,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 114,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 114,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 117,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 117,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 117,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 117,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 58,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 58,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 66,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 66,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 67,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 67,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 68,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 68,
+
+            // Node 36
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 114,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 114,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 114,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 114,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 114,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 114,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 114,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 114,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 117,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 117,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 117,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 117,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 117,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 117,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 117,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 117,
+
+            // Node 37
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 58,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 58,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 58,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 58,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 66,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 66,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 66,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 66,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 67,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 67,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 67,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 67,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 68,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 68,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 68,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 68,
+
+            // Node 38
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 58,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 58,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 58,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 58,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 58,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 58,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 58,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 58,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 66,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 66,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 66,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 66,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 66,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 66,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 66,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 66,
+
+            // Node 39
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 67,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 67,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 67,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 67,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 67,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 67,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 67,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 67,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 68,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 68,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 68,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 68,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 68,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 68,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 68,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 68,
+
+            // Node 40
+            (44 << 16) + (0 << 8) + 0,
+            (45 << 16) + (0 << 8) + 0,
+            (47 << 16) + (0 << 8) + 0,
+            (48 << 16) + (0 << 8) + 0,
+            (51 << 16) + (0 << 8) + 0,
+            (52 << 16) + (0 << 8) + 0,
+            (54 << 16) + (0 << 8) + 0,
+            (55 << 16) + (0 << 8) + 0,
+            (59 << 16) + (0 << 8) + 0,
+            (60 << 16) + (0 << 8) + 0,
+            (62 << 16) + (0 << 8) + 0,
+            (63 << 16) + (0 << 8) + 0,
+            (66 << 16) + (0 << 8) + 0,
+            (67 << 16) + (0 << 8) + 0,
+            (69 << 16) + (0 << 8) + 0,
+            (72 << 16) + (HUFFMAN_COMPLETE << 8) + 0,
+
+            // Node 41
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 69,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 70,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 71,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 72,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 73,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 74,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 75,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 76,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 77,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 78,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 79,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 80,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 81,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 82,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 83,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 84,
+
+            // Node 42
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 69,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 69,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 70,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 70,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 71,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 71,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 72,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 72,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 73,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 73,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 74,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 74,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 75,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 75,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 76,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 76,
+
+            // Node 43
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 69,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 69,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 69,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 69,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 70,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 70,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 70,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 70,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 71,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 71,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 71,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 71,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 72,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 72,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 72,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 72,
+
+            // Node 44
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 69,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 69,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 69,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 69,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 69,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 69,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 69,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 69,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 70,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 70,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 70,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 70,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 70,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 70,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 70,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 70,
+
+            // Node 45
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 71,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 71,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 71,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 71,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 71,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 71,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 71,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 71,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 72,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 72,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 72,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 72,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 72,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 72,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 72,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 72,
+
+            // Node 46
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 73,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 73,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 73,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 73,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 74,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 74,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 74,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 74,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 75,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 75,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 75,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 75,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 76,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 76,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 76,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 76,
+
+            // Node 47
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 73,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 73,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 73,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 73,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 73,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 73,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 73,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 73,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 74,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 74,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 74,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 74,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 74,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 74,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 74,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 74,
+
+            // Node 48
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 75,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 75,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 75,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 75,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 75,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 75,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 75,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 75,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 76,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 76,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 76,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 76,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 76,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 76,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 76,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 76,
+
+            // Node 49
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 77,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 77,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 78,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 78,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 79,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 79,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 80,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 80,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 81,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 81,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 82,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 82,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 83,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 83,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 84,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 84,
+
+            // Node 50
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 77,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 77,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 77,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 77,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 78,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 78,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 78,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 78,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 79,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 79,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 79,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 79,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 80,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 80,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 80,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 80,
+
+            // Node 51
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 77,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 77,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 77,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 77,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 77,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 77,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 77,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 77,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 78,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 78,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 78,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 78,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 78,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 78,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 78,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 78,
+
+            // Node 52
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 79,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 79,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 79,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 79,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 79,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 79,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 79,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 79,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 80,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 80,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 80,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 80,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 80,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 80,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 80,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 80,
+
+            // Node 53
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 81,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 81,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 81,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 81,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 82,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 82,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 82,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 82,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 83,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 83,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 83,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 83,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 84,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 84,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 84,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 84,
+
+            // Node 54
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 81,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 81,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 81,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 81,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 81,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 81,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 81,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 81,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 82,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 82,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 82,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 82,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 82,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 82,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 82,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 82,
+
+            // Node 55
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 83,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 83,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 83,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 83,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 83,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 83,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 83,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 83,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 84,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 84,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 84,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 84,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 84,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 84,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 84,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 84,
+
+            // Node 56
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 85,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 86,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 87,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 89,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 106,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 107,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 113,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 118,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 119,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 120,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 121,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 122,
+            (70 << 16) + (0 << 8) + 0,
+            (71 << 16) + (0 << 8) + 0,
+            (73 << 16) + (0 << 8) + 0,
+            (74 << 16) + (HUFFMAN_COMPLETE << 8) + 0,
+
+            // Node 57
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 85,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 85,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 86,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 86,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 87,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 87,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 89,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 89,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 106,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 106,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 107,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 107,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 113,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 113,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 118,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 118,
+
+            // Node 58
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 85,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 85,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 85,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 85,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 86,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 86,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 86,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 86,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 87,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 87,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 87,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 87,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 89,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 89,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 89,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 89,
+
+            // Node 59
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 85,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 85,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 85,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 85,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 85,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 85,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 85,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 85,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 86,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 86,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 86,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 86,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 86,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 86,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 86,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 86,
+
+            // Node 60
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 87,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 87,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 87,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 87,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 87,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 87,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 87,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 87,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 89,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 89,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 89,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 89,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 89,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 89,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 89,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 89,
+
+            // Node 61
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 106,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 106,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 106,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 106,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 107,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 107,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 107,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 107,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 113,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 113,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 113,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 113,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 118,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 118,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 118,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 118,
+
+            // Node 62
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 106,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 106,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 106,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 106,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 106,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 106,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 106,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 106,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 107,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 107,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 107,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 107,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 107,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 107,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 107,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 107,
+
+            // Node 63
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 113,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 113,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 113,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 113,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 113,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 113,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 113,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 113,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 118,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 118,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 118,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 118,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 118,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 118,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 118,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 118,
+
+            // Node 64
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 119,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 119,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 120,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 120,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 121,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 121,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 122,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 122,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 38,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 42,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 44,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 59,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 88,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 90,
+            (75 << 16) + (0 << 8) + 0,
+            (78 << 16) + (0 << 8) + 0,
+
+            // Node 65
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 119,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 119,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 119,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 119,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 120,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 120,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 120,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 120,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 121,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 121,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 121,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 121,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 122,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 122,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 122,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 122,
+
+            // Node 66
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 119,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 119,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 119,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 119,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 119,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 119,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 119,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 119,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 120,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 120,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 120,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 120,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 120,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 120,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 120,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 120,
+
+            // Node 67
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 121,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 121,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 121,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 121,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 121,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 121,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 121,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 121,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 122,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 122,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 122,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 122,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 122,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 122,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 122,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 122,
+
+            // Node 68
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 38,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 38,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 42,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 42,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 44,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 44,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 59,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 59,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 88,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 88,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 90,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 90,
+            (76 << 16) + (0 << 8) + 0,
+            (77 << 16) + (0 << 8) + 0,
+            (79 << 16) + (0 << 8) + 0,
+            (81 << 16) + (0 << 8) + 0,
+
+            // Node 69
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 38,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 38,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 38,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 38,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 42,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 42,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 42,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 42,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 44,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 44,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 44,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 44,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 59,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 59,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 59,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 59,
+
+            // Node 70
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 38,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 38,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 38,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 38,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 38,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 38,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 38,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 38,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 42,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 42,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 42,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 42,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 42,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 42,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 42,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 42,
+
+            // Node 71
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 44,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 44,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 44,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 44,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 44,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 44,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 44,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 44,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 59,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 59,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 59,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 59,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 59,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 59,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 59,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 59,
+
+            // Node 72
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 88,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 88,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 88,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 88,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 90,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 90,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 90,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 90,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 33,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 34,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 40,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 41,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 63,
+            (80 << 16) + (0 << 8) + 0,
+            (82 << 16) + (0 << 8) + 0,
+            (84 << 16) + (0 << 8) + 0,
+
+            // Node 73
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 88,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 88,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 88,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 88,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 88,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 88,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 88,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 88,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 90,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 90,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 90,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 90,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 90,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 90,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 90,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 90,
+
+            // Node 74
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 33,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 33,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 34,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 34,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 40,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 40,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 41,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 41,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 63,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 63,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 39,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 43,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 124,
+            (83 << 16) + (0 << 8) + 0,
+            (85 << 16) + (0 << 8) + 0,
+            (88 << 16) + (0 << 8) + 0,
+
+            // Node 75
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 33,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 33,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 33,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 33,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 34,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 34,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 34,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 34,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 40,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 40,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 40,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 40,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 41,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 41,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 41,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 41,
+
+            // Node 76
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 33,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 33,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 33,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 33,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 33,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 33,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 33,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 33,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 34,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 34,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 34,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 34,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 34,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 34,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 34,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 34,
+
+            // Node 77
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 40,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 40,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 40,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 40,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 40,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 40,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 40,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 40,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 41,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 41,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 41,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 41,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 41,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 41,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 41,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 41,
+
+            // Node 78
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 63,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 63,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 63,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 63,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 39,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 39,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 43,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 43,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 124,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 124,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 35,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 62,
+            (86 << 16) + (0 << 8) + 0,
+            (87 << 16) + (0 << 8) + 0,
+            (89 << 16) + (0 << 8) + 0,
+            (90 << 16) + (0 << 8) + 0,
+
+            // Node 79
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 63,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 63,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 63,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 63,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 63,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 63,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 63,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 63,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 39,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 39,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 39,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 39,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 43,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 43,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 43,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 43,
+
+            // Node 80
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 39,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 39,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 39,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 39,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 39,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 39,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 39,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 39,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 43,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 43,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 43,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 43,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 43,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 43,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 43,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 43,
+
+            // Node 81
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 124,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 124,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 124,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 124,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 35,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 35,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 62,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 62,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 0,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 36,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 64,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 91,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 93,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 126,
+            (91 << 16) + (0 << 8) + 0,
+            (92 << 16) + (0 << 8) + 0,
+
+            // Node 82
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 124,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 124,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 124,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 124,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 124,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 124,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 124,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 124,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 35,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 35,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 35,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 35,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 62,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 62,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 62,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 62,
+
+            // Node 83
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 35,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 35,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 35,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 35,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 35,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 35,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 35,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 35,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 62,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 62,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 62,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 62,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 62,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 62,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 62,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 62,
+
+            // Node 84
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 0,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 0,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 36,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 36,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 64,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 64,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 91,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 91,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 93,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 93,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 126,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 126,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 94,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 125,
+            (93 << 16) + (0 << 8) + 0,
+            (94 << 16) + (0 << 8) + 0,
+
+            // Node 85
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 0,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 0,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 0,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 0,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 36,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 36,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 36,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 36,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 64,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 64,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 64,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 64,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 91,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 91,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 91,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 91,
+
+            // Node 86
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 0,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 0,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 0,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 0,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 0,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 0,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 0,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 0,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 36,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 36,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 36,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 36,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 36,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 36,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 36,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 36,
+
+            // Node 87
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 64,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 64,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 64,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 64,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 64,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 64,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 64,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 64,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 91,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 91,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 91,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 91,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 91,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 91,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 91,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 91,
+
+            // Node 88
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 93,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 93,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 93,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 93,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 126,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 126,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 126,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 126,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 94,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 94,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 125,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 125,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 60,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 96,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 123,
+            (95 << 16) + (0 << 8) + 0,
+
+            // Node 89
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 93,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 93,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 93,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 93,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 93,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 93,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 93,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 93,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 126,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 126,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 126,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 126,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 126,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 126,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 126,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 126,
+
+            // Node 90
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 94,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 94,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 94,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 94,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 125,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 125,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 125,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 125,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 60,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 60,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 96,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 96,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 123,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 123,
+            (96 << 16) + (0 << 8) + 0,
+            (110 << 16) + (0 << 8) + 0,
+
+            // Node 91
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 94,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 94,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 94,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 94,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 94,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 94,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 94,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 94,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 125,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 125,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 125,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 125,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 125,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 125,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 125,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 125,
+
+            // Node 92
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 60,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 60,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 60,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 60,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 96,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 96,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 96,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 96,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 123,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 123,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 123,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 123,
+            (97 << 16) + (0 << 8) + 0,
+            (101 << 16) + (0 << 8) + 0,
+            (111 << 16) + (0 << 8) + 0,
+            (133 << 16) + (0 << 8) + 0,
+
+            // Node 93
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 60,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 60,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 60,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 60,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 60,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 60,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 60,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 60,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 96,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 96,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 96,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 96,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 96,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 96,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 96,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 96,
+
+            // Node 94
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 123,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 123,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 123,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 123,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 123,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 123,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 123,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 123,
+            (98 << 16) + (0 << 8) + 0,
+            (99 << 16) + (0 << 8) + 0,
+            (102 << 16) + (0 << 8) + 0,
+            (105 << 16) + (0 << 8) + 0,
+            (112 << 16) + (0 << 8) + 0,
+            (119 << 16) + (0 << 8) + 0,
+            (134 << 16) + (0 << 8) + 0,
+            (153 << 16) + (0 << 8) + 0,
+
+            // Node 95
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 92,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 195,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 208,
+            (100 << 16) + (0 << 8) + 0,
+            (103 << 16) + (0 << 8) + 0,
+            (104 << 16) + (0 << 8) + 0,
+            (106 << 16) + (0 << 8) + 0,
+            (107 << 16) + (0 << 8) + 0,
+            (113 << 16) + (0 << 8) + 0,
+            (116 << 16) + (0 << 8) + 0,
+            (120 << 16) + (0 << 8) + 0,
+            (126 << 16) + (0 << 8) + 0,
+            (135 << 16) + (0 << 8) + 0,
+            (142 << 16) + (0 << 8) + 0,
+            (154 << 16) + (0 << 8) + 0,
+            (169 << 16) + (0 << 8) + 0,
+
+            // Node 96
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 92,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 92,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 195,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 195,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 208,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 208,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 128,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 130,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 131,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 162,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 184,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 194,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 224,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 226,
+            (108 << 16) + (0 << 8) + 0,
+            (109 << 16) + (0 << 8) + 0,
+
+            // Node 97
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 92,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 92,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 92,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 92,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 195,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 195,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 195,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 195,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 208,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 208,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 208,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 208,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 128,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 128,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 130,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 130,
+
+            // Node 98
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 92,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 92,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 92,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 92,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 92,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 92,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 92,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 92,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 195,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 195,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 195,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 195,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 195,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 195,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 195,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 195,
+
+            // Node 99
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 208,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 208,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 208,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 208,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 208,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 208,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 208,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 208,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 128,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 128,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 128,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 128,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 130,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 130,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 130,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 130,
+
+            // Node 100
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 128,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 128,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 128,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 128,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 128,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 128,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 128,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 128,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 130,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 130,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 130,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 130,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 130,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 130,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 130,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 130,
+
+            // Node 101
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 131,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 131,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 162,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 162,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 184,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 184,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 194,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 194,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 224,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 224,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 226,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 226,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 153,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 161,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 167,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 172,
+
+            // Node 102
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 131,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 131,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 131,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 131,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 162,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 162,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 162,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 162,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 184,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 184,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 184,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 184,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 194,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 194,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 194,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 194,
+
+            // Node 103
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 131,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 131,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 131,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 131,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 131,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 131,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 131,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 131,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 162,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 162,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 162,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 162,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 162,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 162,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 162,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 162,
+
+            // Node 104
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 184,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 184,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 184,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 184,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 184,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 184,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 184,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 184,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 194,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 194,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 194,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 194,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 194,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 194,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 194,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 194,
+
+            // Node 105
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 224,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 224,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 224,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 224,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 226,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 226,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 226,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 226,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 153,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 153,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 161,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 161,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 167,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 167,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 172,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 172,
+
+            // Node 106
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 224,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 224,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 224,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 224,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 224,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 224,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 224,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 224,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 226,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 226,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 226,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 226,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 226,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 226,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 226,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 226,
+
+            // Node 107
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 153,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 153,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 153,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 153,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 161,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 161,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 161,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 161,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 167,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 167,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 167,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 167,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 172,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 172,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 172,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 172,
+
+            // Node 108
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 153,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 153,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 153,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 153,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 153,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 153,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 153,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 153,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 161,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 161,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 161,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 161,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 161,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 161,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 161,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 161,
+
+            // Node 109
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 167,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 167,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 167,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 167,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 167,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 167,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 167,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 167,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 172,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 172,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 172,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 172,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 172,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 172,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 172,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 172,
+
+            // Node 110
+            (114 << 16) + (0 << 8) + 0,
+            (115 << 16) + (0 << 8) + 0,
+            (117 << 16) + (0 << 8) + 0,
+            (118 << 16) + (0 << 8) + 0,
+            (121 << 16) + (0 << 8) + 0,
+            (123 << 16) + (0 << 8) + 0,
+            (127 << 16) + (0 << 8) + 0,
+            (130 << 16) + (0 << 8) + 0,
+            (136 << 16) + (0 << 8) + 0,
+            (139 << 16) + (0 << 8) + 0,
+            (143 << 16) + (0 << 8) + 0,
+            (146 << 16) + (0 << 8) + 0,
+            (155 << 16) + (0 << 8) + 0,
+            (162 << 16) + (0 << 8) + 0,
+            (170 << 16) + (0 << 8) + 0,
+            (180 << 16) + (0 << 8) + 0,
+
+            // Node 111
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 176,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 177,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 179,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 209,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 216,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 217,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 227,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 229,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 230,
+            (122 << 16) + (0 << 8) + 0,
+            (124 << 16) + (0 << 8) + 0,
+            (125 << 16) + (0 << 8) + 0,
+            (128 << 16) + (0 << 8) + 0,
+            (129 << 16) + (0 << 8) + 0,
+            (131 << 16) + (0 << 8) + 0,
+            (132 << 16) + (0 << 8) + 0,
+
+            // Node 112
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 176,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 176,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 177,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 177,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 179,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 179,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 209,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 209,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 216,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 216,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 217,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 217,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 227,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 227,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 229,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 229,
+
+            // Node 113
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 176,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 176,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 176,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 176,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 177,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 177,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 177,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 177,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 179,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 179,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 179,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 179,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 209,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 209,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 209,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 209,
+
+            // Node 114
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 176,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 176,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 176,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 176,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 176,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 176,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 176,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 176,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 177,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 177,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 177,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 177,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 177,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 177,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 177,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 177,
+
+            // Node 115
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 179,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 179,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 179,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 179,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 179,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 179,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 179,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 179,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 209,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 209,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 209,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 209,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 209,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 209,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 209,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 209,
+
+            // Node 116
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 216,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 216,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 216,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 216,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 217,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 217,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 217,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 217,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 227,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 227,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 227,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 227,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 229,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 229,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 229,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 229,
+
+            // Node 117
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 216,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 216,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 216,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 216,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 216,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 216,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 216,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 216,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 217,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 217,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 217,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 217,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 217,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 217,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 217,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 217,
+
+            // Node 118
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 227,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 227,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 227,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 227,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 227,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 227,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 227,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 227,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 229,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 229,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 229,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 229,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 229,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 229,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 229,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 229,
+
+            // Node 119
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 230,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 230,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 129,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 132,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 133,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 134,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 136,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 146,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 154,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 156,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 160,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 163,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 164,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 169,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 170,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 173,
+
+            // Node 120
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 230,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 230,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 230,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 230,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 129,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 129,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 132,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 132,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 133,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 133,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 134,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 134,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 136,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 136,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 146,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 146,
+
+            // Node 121
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 230,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 230,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 230,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 230,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 230,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 230,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 230,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 230,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 129,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 129,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 129,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 129,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 132,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 132,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 132,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 132,
+
+            // Node 122
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 129,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 129,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 129,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 129,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 129,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 129,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 129,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 129,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 132,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 132,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 132,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 132,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 132,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 132,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 132,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 132,
+
+            // Node 123
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 133,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 133,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 133,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 133,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 134,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 134,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 134,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 134,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 136,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 136,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 136,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 136,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 146,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 146,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 146,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 146,
+
+            // Node 124
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 133,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 133,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 133,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 133,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 133,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 133,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 133,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 133,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 134,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 134,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 134,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 134,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 134,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 134,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 134,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 134,
+
+            // Node 125
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 136,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 136,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 136,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 136,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 136,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 136,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 136,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 136,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 146,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 146,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 146,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 146,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 146,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 146,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 146,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 146,
+
+            // Node 126
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 154,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 154,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 156,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 156,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 160,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 160,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 163,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 163,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 164,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 164,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 169,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 169,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 170,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 170,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 173,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 173,
+
+            // Node 127
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 154,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 154,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 154,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 154,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 156,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 156,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 156,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 156,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 160,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 160,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 160,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 160,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 163,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 163,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 163,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 163,
+
+            // Node 128
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 154,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 154,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 154,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 154,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 154,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 154,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 154,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 154,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 156,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 156,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 156,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 156,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 156,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 156,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 156,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 156,
+
+            // Node 129
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 160,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 160,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 160,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 160,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 160,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 160,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 160,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 160,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 163,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 163,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 163,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 163,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 163,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 163,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 163,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 163,
+
+            // Node 130
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 164,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 164,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 164,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 164,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 169,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 169,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 169,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 169,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 170,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 170,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 170,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 170,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 173,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 173,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 173,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 173,
+
+            // Node 131
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 164,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 164,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 164,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 164,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 164,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 164,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 164,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 164,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 169,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 169,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 169,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 169,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 169,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 169,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 169,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 169,
 
-            // Section 5.2. String Literal Representation
-            // A padding strictly longer than 7 bits MUST be treated as a decoding error.
-            // Padding not corresponding to the most significant bits of the code
-            // for the EOS symbol (0xFF) MUST be treated as a decoding error.
-            int mask = (1 << symbolBits) - 1;
-            if (symbolBits > 7 || (current & mask) != mask) {
-                throw INVALID_PADDING;
+            // Node 132
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 170,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 170,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 170,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 170,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 170,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 170,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 170,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 170,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 173,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 173,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 173,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 173,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 173,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 173,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 173,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 173,
+
+            // Node 133
+            (137 << 16) + (0 << 8) + 0,
+            (138 << 16) + (0 << 8) + 0,
+            (140 << 16) + (0 << 8) + 0,
+            (141 << 16) + (0 << 8) + 0,
+            (144 << 16) + (0 << 8) + 0,
+            (145 << 16) + (0 << 8) + 0,
+            (147 << 16) + (0 << 8) + 0,
+            (150 << 16) + (0 << 8) + 0,
+            (156 << 16) + (0 << 8) + 0,
+            (159 << 16) + (0 << 8) + 0,
+            (163 << 16) + (0 << 8) + 0,
+            (166 << 16) + (0 << 8) + 0,
+            (171 << 16) + (0 << 8) + 0,
+            (174 << 16) + (0 << 8) + 0,
+            (181 << 16) + (0 << 8) + 0,
+            (190 << 16) + (0 << 8) + 0,
+
+            // Node 134
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 178,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 181,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 185,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 186,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 187,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 189,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 190,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 196,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 198,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 228,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 232,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 233,
+            (148 << 16) + (0 << 8) + 0,
+            (149 << 16) + (0 << 8) + 0,
+            (151 << 16) + (0 << 8) + 0,
+            (152 << 16) + (0 << 8) + 0,
+
+            // Node 135
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 178,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 178,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 181,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 181,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 185,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 185,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 186,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 186,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 187,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 187,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 189,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 189,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 190,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 190,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 196,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 196,
+
+            // Node 136
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 178,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 178,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 178,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 178,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 181,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 181,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 181,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 181,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 185,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 185,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 185,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 185,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 186,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 186,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 186,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 186,
+
+            // Node 137
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 178,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 178,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 178,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 178,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 178,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 178,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 178,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 178,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 181,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 181,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 181,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 181,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 181,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 181,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 181,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 181,
+
+            // Node 138
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 185,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 185,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 185,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 185,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 185,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 185,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 185,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 185,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 186,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 186,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 186,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 186,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 186,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 186,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 186,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 186,
+
+            // Node 139
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 187,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 187,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 187,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 187,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 189,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 189,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 189,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 189,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 190,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 190,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 190,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 190,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 196,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 196,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 196,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 196,
+
+            // Node 140
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 187,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 187,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 187,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 187,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 187,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 187,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 187,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 187,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 189,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 189,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 189,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 189,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 189,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 189,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 189,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 189,
+
+            // Node 141
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 190,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 190,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 190,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 190,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 190,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 190,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 190,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 190,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 196,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 196,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 196,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 196,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 196,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 196,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 196,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 196,
+
+            // Node 142
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 198,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 198,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 228,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 228,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 232,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 232,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 233,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 233,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 1,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 135,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 137,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 138,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 139,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 140,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 141,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 143,
+
+            // Node 143
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 198,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 198,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 198,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 198,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 228,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 228,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 228,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 228,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 232,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 232,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 232,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 232,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 233,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 233,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 233,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 233,
+
+            // Node 144
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 198,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 198,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 198,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 198,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 198,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 198,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 198,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 198,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 228,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 228,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 228,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 228,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 228,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 228,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 228,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 228,
+
+            // Node 145
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 232,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 232,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 232,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 232,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 232,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 232,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 232,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 232,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 233,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 233,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 233,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 233,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 233,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 233,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 233,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 233,
+
+            // Node 146
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 1,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 1,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 135,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 135,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 137,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 137,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 138,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 138,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 139,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 139,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 140,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 140,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 141,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 141,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 143,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 143,
+
+            // Node 147
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 1,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 1,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 1,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 1,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 135,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 135,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 135,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 135,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 137,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 137,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 137,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 137,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 138,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 138,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 138,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 138,
+
+            // Node 148
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 1,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 1,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 1,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 1,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 1,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 1,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 1,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 1,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 135,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 135,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 135,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 135,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 135,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 135,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 135,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 135,
+
+            // Node 149
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 137,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 137,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 137,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 137,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 137,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 137,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 137,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 137,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 138,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 138,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 138,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 138,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 138,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 138,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 138,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 138,
+
+            // Node 150
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 139,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 139,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 139,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 139,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 140,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 140,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 140,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 140,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 141,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 141,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 141,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 141,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 143,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 143,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 143,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 143,
+
+            // Node 151
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 139,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 139,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 139,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 139,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 139,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 139,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 139,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 139,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 140,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 140,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 140,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 140,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 140,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 140,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 140,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 140,
+
+            // Node 152
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 141,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 141,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 141,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 141,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 141,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 141,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 141,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 141,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 143,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 143,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 143,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 143,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 143,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 143,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 143,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 143,
+
+            // Node 153
+            (157 << 16) + (0 << 8) + 0,
+            (158 << 16) + (0 << 8) + 0,
+            (160 << 16) + (0 << 8) + 0,
+            (161 << 16) + (0 << 8) + 0,
+            (164 << 16) + (0 << 8) + 0,
+            (165 << 16) + (0 << 8) + 0,
+            (167 << 16) + (0 << 8) + 0,
+            (168 << 16) + (0 << 8) + 0,
+            (172 << 16) + (0 << 8) + 0,
+            (173 << 16) + (0 << 8) + 0,
+            (175 << 16) + (0 << 8) + 0,
+            (177 << 16) + (0 << 8) + 0,
+            (182 << 16) + (0 << 8) + 0,
+            (185 << 16) + (0 << 8) + 0,
+            (191 << 16) + (0 << 8) + 0,
+            (207 << 16) + (0 << 8) + 0,
+
+            // Node 154
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 147,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 149,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 150,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 151,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 152,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 155,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 157,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 158,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 165,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 166,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 168,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 174,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 175,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 180,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 182,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 183,
+
+            // Node 155
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 147,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 147,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 149,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 149,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 150,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 150,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 151,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 151,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 152,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 152,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 155,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 155,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 157,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 157,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 158,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 158,
+
+            // Node 156
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 147,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 147,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 147,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 147,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 149,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 149,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 149,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 149,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 150,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 150,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 150,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 150,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 151,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 151,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 151,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 151,
+
+            // Node 157
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 147,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 147,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 147,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 147,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 147,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 147,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 147,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 147,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 149,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 149,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 149,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 149,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 149,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 149,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 149,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 149,
+
+            // Node 158
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 150,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 150,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 150,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 150,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 150,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 150,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 150,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 150,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 151,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 151,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 151,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 151,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 151,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 151,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 151,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 151,
+
+            // Node 159
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 152,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 152,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 152,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 152,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 155,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 155,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 155,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 155,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 157,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 157,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 157,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 157,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 158,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 158,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 158,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 158,
+
+            // Node 160
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 152,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 152,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 152,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 152,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 152,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 152,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 152,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 152,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 155,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 155,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 155,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 155,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 155,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 155,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 155,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 155,
+
+            // Node 161
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 157,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 157,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 157,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 157,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 157,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 157,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 157,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 157,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 158,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 158,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 158,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 158,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 158,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 158,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 158,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 158,
+
+            // Node 162
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 165,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 165,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 166,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 166,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 168,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 168,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 174,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 174,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 175,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 175,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 180,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 180,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 182,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 182,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 183,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 183,
+
+            // Node 163
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 165,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 165,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 165,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 165,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 166,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 166,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 166,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 166,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 168,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 168,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 168,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 168,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 174,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 174,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 174,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 174,
+
+            // Node 164
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 165,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 165,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 165,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 165,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 165,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 165,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 165,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 165,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 166,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 166,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 166,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 166,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 166,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 166,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 166,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 166,
+
+            // Node 165
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 168,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 168,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 168,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 168,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 168,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 168,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 168,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 168,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 174,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 174,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 174,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 174,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 174,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 174,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 174,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 174,
+
+            // Node 166
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 175,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 175,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 175,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 175,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 180,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 180,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 180,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 180,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 182,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 182,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 182,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 182,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 183,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 183,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 183,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 183,
+
+            // Node 167
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 175,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 175,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 175,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 175,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 175,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 175,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 175,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 175,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 180,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 180,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 180,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 180,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 180,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 180,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 180,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 180,
+
+            // Node 168
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 182,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 182,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 182,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 182,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 182,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 182,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 182,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 182,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 183,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 183,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 183,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 183,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 183,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 183,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 183,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 183,
+
+            // Node 169
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 188,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 191,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 197,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 231,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 239,
+            (176 << 16) + (0 << 8) + 0,
+            (178 << 16) + (0 << 8) + 0,
+            (179 << 16) + (0 << 8) + 0,
+            (183 << 16) + (0 << 8) + 0,
+            (184 << 16) + (0 << 8) + 0,
+            (186 << 16) + (0 << 8) + 0,
+            (187 << 16) + (0 << 8) + 0,
+            (192 << 16) + (0 << 8) + 0,
+            (199 << 16) + (0 << 8) + 0,
+            (208 << 16) + (0 << 8) + 0,
+            (223 << 16) + (0 << 8) + 0,
+
+            // Node 170
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 188,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 188,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 191,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 191,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 197,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 197,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 231,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 231,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 239,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 239,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 9,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 142,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 144,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 145,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 148,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 159,
+
+            // Node 171
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 188,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 188,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 188,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 188,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 191,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 191,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 191,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 191,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 197,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 197,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 197,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 197,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 231,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 231,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 231,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 231,
+
+            // Node 172
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 188,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 188,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 188,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 188,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 188,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 188,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 188,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 188,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 191,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 191,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 191,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 191,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 191,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 191,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 191,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 191,
+
+            // Node 173
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 197,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 197,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 197,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 197,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 197,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 197,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 197,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 197,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 231,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 231,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 231,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 231,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 231,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 231,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 231,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 231,
+
+            // Node 174
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 239,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 239,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 239,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 239,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 9,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 9,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 142,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 142,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 144,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 144,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 145,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 145,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 148,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 148,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 159,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 159,
+
+            // Node 175
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 239,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 239,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 239,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 239,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 239,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 239,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 239,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 239,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 9,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 9,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 9,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 9,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 142,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 142,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 142,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 142,
+
+            // Node 176
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 9,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 9,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 9,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 9,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 9,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 9,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 9,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 9,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 142,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 142,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 142,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 142,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 142,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 142,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 142,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 142,
+
+            // Node 177
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 144,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 144,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 144,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 144,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 145,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 145,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 145,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 145,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 148,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 148,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 148,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 148,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 159,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 159,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 159,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 159,
+
+            // Node 178
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 144,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 144,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 144,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 144,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 144,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 144,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 144,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 144,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 145,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 145,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 145,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 145,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 145,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 145,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 145,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 145,
+
+            // Node 179
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 148,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 148,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 148,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 148,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 148,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 148,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 148,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 148,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 159,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 159,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 159,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 159,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 159,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 159,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 159,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 159,
+
+            // Node 180
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 171,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 206,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 215,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 225,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 236,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 237,
+            (188 << 16) + (0 << 8) + 0,
+            (189 << 16) + (0 << 8) + 0,
+            (193 << 16) + (0 << 8) + 0,
+            (196 << 16) + (0 << 8) + 0,
+            (200 << 16) + (0 << 8) + 0,
+            (203 << 16) + (0 << 8) + 0,
+            (209 << 16) + (0 << 8) + 0,
+            (216 << 16) + (0 << 8) + 0,
+            (224 << 16) + (0 << 8) + 0,
+            (238 << 16) + (0 << 8) + 0,
+
+            // Node 181
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 171,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 171,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 206,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 206,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 215,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 215,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 225,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 225,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 236,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 236,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 237,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 237,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 199,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 207,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 234,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 235,
+
+            // Node 182
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 171,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 171,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 171,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 171,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 206,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 206,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 206,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 206,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 215,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 215,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 215,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 215,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 225,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 225,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 225,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 225,
+
+            // Node 183
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 171,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 171,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 171,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 171,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 171,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 171,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 171,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 171,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 206,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 206,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 206,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 206,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 206,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 206,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 206,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 206,
+
+            // Node 184
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 215,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 215,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 215,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 215,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 215,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 215,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 215,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 215,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 225,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 225,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 225,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 225,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 225,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 225,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 225,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 225,
+
+            // Node 185
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 236,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 236,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 236,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 236,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 237,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 237,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 237,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 237,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 199,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 199,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 207,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 207,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 234,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 234,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 235,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 235,
+
+            // Node 186
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 236,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 236,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 236,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 236,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 236,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 236,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 236,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 236,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 237,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 237,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 237,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 237,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 237,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 237,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 237,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 237,
+
+            // Node 187
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 199,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 199,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 199,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 199,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 207,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 207,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 207,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 207,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 234,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 234,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 234,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 234,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 235,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 235,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 235,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 235,
+
+            // Node 188
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 199,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 199,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 199,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 199,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 199,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 199,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 199,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 199,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 207,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 207,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 207,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 207,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 207,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 207,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 207,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 207,
+
+            // Node 189
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 234,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 234,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 234,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 234,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 234,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 234,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 234,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 234,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 235,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 235,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 235,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 235,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 235,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 235,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 235,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 235,
+
+            // Node 190
+            (194 << 16) + (0 << 8) + 0,
+            (195 << 16) + (0 << 8) + 0,
+            (197 << 16) + (0 << 8) + 0,
+            (198 << 16) + (0 << 8) + 0,
+            (201 << 16) + (0 << 8) + 0,
+            (202 << 16) + (0 << 8) + 0,
+            (204 << 16) + (0 << 8) + 0,
+            (205 << 16) + (0 << 8) + 0,
+            (210 << 16) + (0 << 8) + 0,
+            (213 << 16) + (0 << 8) + 0,
+            (217 << 16) + (0 << 8) + 0,
+            (220 << 16) + (0 << 8) + 0,
+            (225 << 16) + (0 << 8) + 0,
+            (231 << 16) + (0 << 8) + 0,
+            (239 << 16) + (0 << 8) + 0,
+            (246 << 16) + (0 << 8) + 0,
+
+            // Node 191
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 192,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 193,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 200,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 201,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 202,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 205,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 210,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 213,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 218,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 219,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 238,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 240,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 242,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 243,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 255,
+            (206 << 16) + (0 << 8) + 0,
+
+            // Node 192
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 192,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 192,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 193,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 193,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 200,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 200,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 201,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 201,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 202,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 202,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 205,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 205,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 210,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 210,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 213,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 213,
+
+            // Node 193
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 192,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 192,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 192,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 192,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 193,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 193,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 193,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 193,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 200,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 200,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 200,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 200,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 201,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 201,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 201,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 201,
+
+            // Node 194
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 192,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 192,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 192,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 192,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 192,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 192,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 192,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 192,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 193,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 193,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 193,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 193,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 193,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 193,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 193,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 193,
+
+            // Node 195
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 200,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 200,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 200,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 200,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 200,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 200,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 200,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 200,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 201,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 201,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 201,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 201,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 201,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 201,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 201,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 201,
+
+            // Node 196
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 202,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 202,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 202,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 202,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 205,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 205,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 205,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 205,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 210,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 210,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 210,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 210,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 213,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 213,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 213,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 213,
+
+            // Node 197
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 202,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 202,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 202,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 202,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 202,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 202,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 202,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 202,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 205,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 205,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 205,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 205,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 205,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 205,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 205,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 205,
+
+            // Node 198
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 210,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 210,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 210,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 210,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 210,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 210,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 210,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 210,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 213,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 213,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 213,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 213,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 213,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 213,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 213,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 213,
+
+            // Node 199
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 218,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 218,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 219,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 219,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 238,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 238,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 240,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 240,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 242,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 242,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 243,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 243,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 255,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 255,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 203,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 204,
+
+            // Node 200
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 218,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 218,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 218,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 218,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 219,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 219,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 219,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 219,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 238,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 238,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 238,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 238,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 240,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 240,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 240,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 240,
+
+            // Node 201
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 218,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 218,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 218,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 218,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 218,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 218,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 218,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 218,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 219,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 219,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 219,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 219,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 219,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 219,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 219,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 219,
+
+            // Node 202
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 238,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 238,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 238,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 238,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 238,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 238,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 238,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 238,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 240,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 240,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 240,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 240,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 240,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 240,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 240,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 240,
+
+            // Node 203
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 242,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 242,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 242,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 242,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 243,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 243,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 243,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 243,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 255,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 255,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 255,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 255,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 203,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 203,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 204,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 204,
+
+            // Node 204
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 242,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 242,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 242,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 242,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 242,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 242,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 242,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 242,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 243,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 243,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 243,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 243,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 243,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 243,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 243,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 243,
+
+            // Node 205
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 255,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 255,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 255,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 255,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 255,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 255,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 255,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 255,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 203,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 203,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 203,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 203,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 204,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 204,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 204,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 204,
+
+            // Node 206
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 203,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 203,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 203,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 203,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 203,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 203,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 203,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 203,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 204,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 204,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 204,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 204,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 204,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 204,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 204,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 204,
+
+            // Node 207
+            (211 << 16) + (0 << 8) + 0,
+            (212 << 16) + (0 << 8) + 0,
+            (214 << 16) + (0 << 8) + 0,
+            (215 << 16) + (0 << 8) + 0,
+            (218 << 16) + (0 << 8) + 0,
+            (219 << 16) + (0 << 8) + 0,
+            (221 << 16) + (0 << 8) + 0,
+            (222 << 16) + (0 << 8) + 0,
+            (226 << 16) + (0 << 8) + 0,
+            (228 << 16) + (0 << 8) + 0,
+            (232 << 16) + (0 << 8) + 0,
+            (235 << 16) + (0 << 8) + 0,
+            (240 << 16) + (0 << 8) + 0,
+            (243 << 16) + (0 << 8) + 0,
+            (247 << 16) + (0 << 8) + 0,
+            (250 << 16) + (0 << 8) + 0,
+
+            // Node 208
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 211,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 212,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 214,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 221,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 222,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 223,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 241,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 244,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 245,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 246,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 247,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 248,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 250,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 251,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 252,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 253,
+
+            // Node 209
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 211,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 211,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 212,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 212,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 214,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 214,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 221,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 221,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 222,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 222,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 223,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 223,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 241,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 241,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 244,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 244,
+
+            // Node 210
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 211,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 211,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 211,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 211,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 212,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 212,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 212,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 212,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 214,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 214,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 214,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 214,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 221,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 221,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 221,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 221,
+
+            // Node 211
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 211,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 211,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 211,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 211,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 211,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 211,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 211,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 211,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 212,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 212,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 212,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 212,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 212,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 212,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 212,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 212,
+
+            // Node 212
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 214,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 214,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 214,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 214,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 214,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 214,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 214,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 214,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 221,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 221,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 221,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 221,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 221,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 221,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 221,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 221,
+
+            // Node 213
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 222,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 222,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 222,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 222,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 223,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 223,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 223,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 223,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 241,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 241,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 241,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 241,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 244,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 244,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 244,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 244,
+
+            // Node 214
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 222,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 222,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 222,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 222,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 222,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 222,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 222,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 222,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 223,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 223,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 223,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 223,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 223,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 223,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 223,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 223,
+
+            // Node 215
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 241,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 241,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 241,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 241,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 241,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 241,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 241,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 241,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 244,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 244,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 244,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 244,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 244,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 244,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 244,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 244,
+
+            // Node 216
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 245,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 245,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 246,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 246,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 247,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 247,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 248,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 248,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 250,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 250,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 251,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 251,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 252,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 252,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 253,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 253,
+
+            // Node 217
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 245,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 245,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 245,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 245,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 246,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 246,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 246,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 246,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 247,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 247,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 247,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 247,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 248,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 248,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 248,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 248,
+
+            // Node 218
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 245,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 245,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 245,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 245,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 245,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 245,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 245,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 245,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 246,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 246,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 246,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 246,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 246,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 246,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 246,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 246,
+
+            // Node 219
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 247,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 247,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 247,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 247,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 247,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 247,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 247,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 247,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 248,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 248,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 248,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 248,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 248,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 248,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 248,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 248,
+
+            // Node 220
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 250,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 250,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 250,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 250,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 251,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 251,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 251,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 251,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 252,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 252,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 252,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 252,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 253,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 253,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 253,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 253,
+
+            // Node 221
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 250,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 250,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 250,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 250,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 250,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 250,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 250,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 250,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 251,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 251,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 251,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 251,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 251,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 251,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 251,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 251,
+
+            // Node 222
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 252,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 252,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 252,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 252,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 252,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 252,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 252,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 252,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 253,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 253,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 253,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 253,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 253,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 253,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 253,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 253,
+
+            // Node 223
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 254,
+            (227 << 16) + (0 << 8) + 0,
+            (229 << 16) + (0 << 8) + 0,
+            (230 << 16) + (0 << 8) + 0,
+            (233 << 16) + (0 << 8) + 0,
+            (234 << 16) + (0 << 8) + 0,
+            (236 << 16) + (0 << 8) + 0,
+            (237 << 16) + (0 << 8) + 0,
+            (241 << 16) + (0 << 8) + 0,
+            (242 << 16) + (0 << 8) + 0,
+            (244 << 16) + (0 << 8) + 0,
+            (245 << 16) + (0 << 8) + 0,
+            (248 << 16) + (0 << 8) + 0,
+            (249 << 16) + (0 << 8) + 0,
+            (251 << 16) + (0 << 8) + 0,
+            (252 << 16) + (0 << 8) + 0,
+
+            // Node 224
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 254,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 254,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 2,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 3,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 4,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 5,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 6,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 7,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 8,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 11,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 12,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 14,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 15,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 16,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 17,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 18,
+
+            // Node 225
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 254,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 254,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 254,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 254,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 2,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 2,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 3,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 3,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 4,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 4,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 5,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 5,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 6,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 6,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 7,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 7,
+
+            // Node 226
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 254,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 254,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 254,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 254,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 254,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 254,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 254,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 254,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 2,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 2,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 2,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 2,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 3,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 3,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 3,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 3,
+
+            // Node 227
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 2,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 2,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 2,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 2,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 2,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 2,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 2,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 2,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 3,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 3,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 3,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 3,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 3,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 3,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 3,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 3,
+
+            // Node 228
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 4,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 4,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 4,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 4,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 5,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 5,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 5,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 5,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 6,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 6,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 6,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 6,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 7,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 7,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 7,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 7,
+
+            // Node 229
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 4,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 4,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 4,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 4,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 4,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 4,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 4,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 4,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 5,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 5,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 5,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 5,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 5,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 5,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 5,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 5,
+
+            // Node 230
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 6,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 6,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 6,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 6,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 6,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 6,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 6,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 6,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 7,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 7,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 7,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 7,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 7,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 7,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 7,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 7,
+
+            // Node 231
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 8,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 8,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 11,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 11,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 12,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 12,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 14,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 14,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 15,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 15,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 16,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 16,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 17,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 17,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 18,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 18,
+
+            // Node 232
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 8,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 8,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 8,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 8,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 11,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 11,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 11,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 11,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 12,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 12,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 12,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 12,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 14,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 14,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 14,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 14,
+
+            // Node 233
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 8,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 8,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 8,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 8,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 8,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 8,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 8,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 8,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 11,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 11,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 11,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 11,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 11,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 11,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 11,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 11,
+
+            // Node 234
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 12,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 12,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 12,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 12,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 12,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 12,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 12,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 12,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 14,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 14,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 14,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 14,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 14,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 14,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 14,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 14,
+
+            // Node 235
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 15,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 15,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 15,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 15,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 16,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 16,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 16,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 16,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 17,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 17,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 17,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 17,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 18,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 18,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 18,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 18,
+
+            // Node 236
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 15,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 15,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 15,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 15,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 15,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 15,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 15,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 15,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 16,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 16,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 16,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 16,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 16,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 16,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 16,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 16,
+
+            // Node 237
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 17,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 17,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 17,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 17,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 17,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 17,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 17,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 17,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 18,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 18,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 18,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 18,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 18,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 18,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 18,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 18,
+
+            // Node 238
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 19,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 20,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 21,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 23,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 24,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 25,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 26,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 27,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 28,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 29,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 30,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 31,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 127,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 220,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 249,
+            (253 << 16) + (0 << 8) + 0,
+
+            // Node 239
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 19,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 19,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 20,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 20,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 21,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 21,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 23,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 23,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 24,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 24,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 25,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 25,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 26,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 26,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 27,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 27,
+
+            // Node 240
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 19,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 19,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 19,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 19,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 20,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 20,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 20,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 20,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 21,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 21,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 21,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 21,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 23,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 23,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 23,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 23,
+
+            // Node 241
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 19,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 19,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 19,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 19,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 19,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 19,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 19,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 19,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 20,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 20,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 20,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 20,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 20,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 20,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 20,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 20,
+
+            // Node 242
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 21,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 21,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 21,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 21,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 21,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 21,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 21,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 21,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 23,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 23,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 23,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 23,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 23,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 23,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 23,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 23,
+
+            // Node 243
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 24,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 24,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 24,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 24,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 25,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 25,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 25,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 25,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 26,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 26,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 26,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 26,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 27,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 27,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 27,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 27,
+
+            // Node 244
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 24,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 24,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 24,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 24,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 24,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 24,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 24,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 24,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 25,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 25,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 25,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 25,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 25,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 25,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 25,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 25,
+
+            // Node 245
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 26,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 26,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 26,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 26,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 26,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 26,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 26,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 26,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 27,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 27,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 27,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 27,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 27,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 27,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 27,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 27,
+
+            // Node 246
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 28,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 28,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 29,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 29,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 30,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 30,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 31,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 31,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 127,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 127,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 220,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 220,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 249,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 249,
+            (254 << 16) + (0 << 8) + 0,
+            (255 << 16) + (0 << 8) + 0,
+
+            // Node 247
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 28,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 28,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 28,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 28,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 29,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 29,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 29,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 29,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 30,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 30,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 30,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 30,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 31,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 31,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 31,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 31,
+
+            // Node 248
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 28,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 28,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 28,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 28,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 28,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 28,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 28,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 28,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 29,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 29,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 29,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 29,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 29,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 29,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 29,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 29,
+
+            // Node 249
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 30,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 30,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 30,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 30,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 30,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 30,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 30,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 30,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 31,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 31,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 31,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 31,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 31,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 31,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 31,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 31,
+
+            // Node 250
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 127,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 127,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 127,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 127,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 220,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 220,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 220,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 220,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 249,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 249,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 249,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 249,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 10,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 13,
+            (0 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 22,
+            (0 << 16) + (HUFFMAN_FAIL << 8) + 0,
+
+            // Node 251
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 127,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 127,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 127,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 127,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 127,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 127,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 127,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 127,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 220,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 220,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 220,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 220,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 220,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 220,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 220,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 220,
+
+            // Node 252
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 249,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 249,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 249,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 249,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 249,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 249,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 249,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 249,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 10,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 10,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 13,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 13,
+            (1 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 22,
+            (22 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 22,
+            (0 << 16) + (HUFFMAN_FAIL << 8) + 0,
+            (0 << 16) + (HUFFMAN_FAIL << 8) + 0,
+
+            // Node 253
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 10,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 10,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 10,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 10,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 13,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 13,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 13,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 13,
+            (2 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 22,
+            (9 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 22,
+            (23 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 22,
+            (40 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 22,
+            (0 << 16) + (HUFFMAN_FAIL << 8) + 0,
+            (0 << 16) + (HUFFMAN_FAIL << 8) + 0,
+            (0 << 16) + (HUFFMAN_FAIL << 8) + 0,
+            (0 << 16) + (HUFFMAN_FAIL << 8) + 0,
+
+            // Node 254
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 10,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 10,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 10,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 10,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 10,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 10,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 10,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 10,
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 13,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 13,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 13,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 13,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 13,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 13,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 13,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 13,
+
+            // Node 255
+            (3 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 22,
+            (6 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 22,
+            (10 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 22,
+            (15 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 22,
+            (24 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 22,
+            (31 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 22,
+            (41 << 16) + (HUFFMAN_EMIT_SYMBOL << 8) + 22,
+            (56 << 16) + ((HUFFMAN_COMPLETE | HUFFMAN_EMIT_SYMBOL) << 8) + 22,
+            (0 << 16) + (HUFFMAN_FAIL << 8) + 0,
+            (0 << 16) + (HUFFMAN_FAIL << 8) + 0,
+            (0 << 16) + (HUFFMAN_FAIL << 8) + 0,
+            (0 << 16) + (HUFFMAN_FAIL << 8) + 0,
+            (0 << 16) + (HUFFMAN_FAIL << 8) + 0,
+            (0 << 16) + (HUFFMAN_FAIL << 8) + 0,
+            (0 << 16) + (HUFFMAN_FAIL << 8) + 0,
+            (0 << 16) + (HUFFMAN_FAIL << 8) + 0,
+    };
+
+    private static final Http2Exception BAD_ENCODING = ThrowableUtil.unknownStackTrace(
+            Http2Exception.newStatic(COMPRESSION_ERROR, "HPACK - Bad Encoding",
+                    Http2Exception.ShutdownHint.HARD_SHUTDOWN), HpackHuffmanDecoder.class, "decode(..)");
+
+    private byte[] dest;
+    private int k;
+    private int state;
+
+    HpackHuffmanDecoder() { }
+
+    /**
+     * Decompresses the given Huffman coded string literal.
+     *
+     * @param buf the string literal to be decoded
+     * @return the output stream for the compressed data
+     * @throws Http2Exception EOS Decoded
+     */
+    public AsciiString decode(ByteBuf buf, int length) throws Http2Exception {
+        if (length == 0) {
+            return AsciiString.EMPTY_STRING;
+        }
+        dest = new byte[length * 8 / 5];
+        try {
+            int readerIndex = buf.readerIndex();
+            // Using ByteProcessor to reduce bounds-checking and reference-count checking during byte-by-byte
+            // processing of the ByteBuf.
+            int endIndex = buf.forEachByte(readerIndex, length, this);
+            if (endIndex == -1) {
+                // We did consume the requested length
+                buf.readerIndex(readerIndex + length);
+                if ((state & HUFFMAN_COMPLETE_SHIFT) != HUFFMAN_COMPLETE_SHIFT) {
+                    throw BAD_ENCODING;
+                }
+                return new AsciiString(dest, 0, k, false);
             }
 
-            return new AsciiString(bytes, 0, index, false);
+            // The process(...) method returned before the requested length was requested. This means there
+            // was a bad encoding detected.
+            buf.readerIndex(endIndex);
+            throw BAD_ENCODING;
+        } finally {
+            dest = null;
+            k = 0;
+            state = 0;
         }
+    }
 
-        private void append(int i) {
-            if (bytes.length == index) {
-                // Choose an expanding strategy depending on how big the buffer already is.
-                // 1024 was choosen as a good guess and we may be able to investigate more if there are better choices.
-                // See also https://github.com/netty/netty/issues/6846
-                final int newLength = bytes.length >= 1024 ? bytes.length + initialCapacity : bytes.length << 1;
-                byte[] newBytes = new byte[newLength];
-                System.arraycopy(bytes, 0, newBytes, 0, bytes.length);
-                bytes = newBytes;
-            }
-            bytes[index++] = (byte) i;
+    /**
+     * <strong>This should never be called from anything but this class itself!</strong>
+     */
+    @Override
+    public boolean process(byte input) {
+        return processNibble(input >> 4) && processNibble(input);
+    }
+
+    private boolean processNibble(int input) {
+        // The high nibble of the flags byte of each row is always zero
+        // (low nibble after shifting row by 12), since there are only 3 flag bits
+        int index = state >> 12 | (input & 0x0F);
+        state = HUFFS[index];
+        if ((state & HUFFMAN_FAIL_SHIFT) != 0) {
+            return false;
+        }
+        if ((state & HUFFMAN_EMIT_SYMBOL_SHIFT) != 0) {
+            // state is always positive so can cast without mask here
+            dest[k++] = (byte) state;
         }
+        return true;
     }
 }
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackStaticTable.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackStaticTable.java
index 9621daf..2386d03 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackStaticTable.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackStaticTable.java
@@ -38,6 +38,7 @@ import java.util.Arrays;
 import java.util.List;
 
 import static io.netty.handler.codec.http2.HpackUtil.equalsConstantTime;
+import static io.netty.handler.codec.http2.HpackUtil.equalsVariableTime;
 
 final class HpackStaticTable {
 
@@ -145,7 +146,7 @@ final class HpackStaticTable {
      * Returns the index value for the given header field in the static table. Returns -1 if the
      * header field is not in the static table.
      */
-    static int getIndex(CharSequence name, CharSequence value) {
+    static int getIndexInsensitive(CharSequence name, CharSequence value) {
         int index = getIndex(name);
         if (index == -1) {
             return -1;
@@ -154,10 +155,7 @@ final class HpackStaticTable {
         // Note this assumes all entries for a given header field are sequential.
         while (index <= length) {
             HpackHeaderField entry = getEntry(index);
-            if (equalsConstantTime(name, entry.name) == 0) {
-                break;
-            }
-            if (equalsConstantTime(value, entry.value) != 0) {
+            if (equalsVariableTime(name, entry.name) && equalsVariableTime(value, entry.value)) {
                 return index;
             }
             index++;
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackUtil.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackUtil.java
index 62c24aa..d0f0da0 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackUtil.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackUtil.java
@@ -65,6 +65,16 @@ final class HpackUtil {
         return ConstantTimeUtils.equalsConstantTime(s1, s2);
     }
 
+    /**
+     * Compare two {@link CharSequence}s.
+     * @param s1 the first value.
+     * @param s2 the second value.
+     * @return {@code false} if not equal. {@code true} if equal.
+     */
+    static boolean equalsVariableTime(CharSequence s1, CharSequence s2) {
+        return AsciiString.contentEquals(s1, s2);
+    }
+
     // Section 6.2. Literal Header Field Representation
     enum IndexType {
         INCREMENTAL, // Section 6.2.1. Literal Header Field with Incremental Indexing
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ClientUpgradeCodec.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ClientUpgradeCodec.java
index 6028a6f..6e5e55e 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ClientUpgradeCodec.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ClientUpgradeCodec.java
@@ -47,13 +47,14 @@ public class Http2ClientUpgradeCodec implements HttpClientUpgradeHandler.Upgrade
     private final String handlerName;
     private final Http2ConnectionHandler connectionHandler;
     private final ChannelHandler upgradeToHandler;
+    private final ChannelHandler http2MultiplexHandler;
 
     public Http2ClientUpgradeCodec(Http2FrameCodec frameCodec, ChannelHandler upgradeToHandler) {
         this(null, frameCodec, upgradeToHandler);
     }
 
     public Http2ClientUpgradeCodec(String handlerName, Http2FrameCodec frameCodec, ChannelHandler upgradeToHandler) {
-        this(handlerName, (Http2ConnectionHandler) frameCodec, upgradeToHandler);
+        this(handlerName, (Http2ConnectionHandler) frameCodec, upgradeToHandler, null);
     }
 
     /**
@@ -66,6 +67,18 @@ public class Http2ClientUpgradeCodec implements HttpClientUpgradeHandler.Upgrade
         this((String) null, connectionHandler);
     }
 
+    /**
+     * Creates the codec using a default name for the connection handler when adding to the
+     * pipeline.
+     *
+     * @param connectionHandler the HTTP/2 connection handler
+     * @param http2MultiplexHandler the Http2 Multiplexer handler to work with Http2FrameCodec
+     */
+    public Http2ClientUpgradeCodec(Http2ConnectionHandler connectionHandler,
+        Http2MultiplexHandler http2MultiplexHandler) {
+        this((String) null, connectionHandler, http2MultiplexHandler);
+    }
+
     /**
      * Creates the codec providing an upgrade to the given handler for HTTP/2.
      *
@@ -74,14 +87,27 @@ public class Http2ClientUpgradeCodec implements HttpClientUpgradeHandler.Upgrade
      * @param connectionHandler the HTTP/2 connection handler
      */
     public Http2ClientUpgradeCodec(String handlerName, Http2ConnectionHandler connectionHandler) {
-        this(handlerName, connectionHandler, connectionHandler);
+        this(handlerName, connectionHandler, connectionHandler, null);
+    }
+
+    /**
+     * Creates the codec providing an upgrade to the given handler for HTTP/2.
+     *
+     * @param handlerName the name of the HTTP/2 connection handler to be used in the pipeline,
+     *                    or {@code null} to auto-generate the name
+     * @param connectionHandler the HTTP/2 connection handler
+     */
+    public Http2ClientUpgradeCodec(String handlerName, Http2ConnectionHandler connectionHandler,
+        Http2MultiplexHandler http2MultiplexHandler) {
+        this(handlerName, connectionHandler, connectionHandler, http2MultiplexHandler);
     }
 
     private Http2ClientUpgradeCodec(String handlerName, Http2ConnectionHandler connectionHandler, ChannelHandler
-                                    upgradeToHandler) {
+        upgradeToHandler, Http2MultiplexHandler http2MultiplexHandler) {
         this.handlerName = handlerName;
         this.connectionHandler = checkNotNull(connectionHandler, "connectionHandler");
         this.upgradeToHandler = checkNotNull(upgradeToHandler, "upgradeToHandler");
+        this.http2MultiplexHandler = http2MultiplexHandler;
     }
 
     @Override
@@ -91,7 +117,7 @@ public class Http2ClientUpgradeCodec implements HttpClientUpgradeHandler.Upgrade
 
     @Override
     public Collection<CharSequence> setUpgradeHeaders(ChannelHandlerContext ctx,
-            HttpRequest upgradeRequest) {
+        HttpRequest upgradeRequest) {
         CharSequence settingsValue = getSettingsHeaderValue(ctx);
         upgradeRequest.headers().set(HTTP_UPGRADE_SETTINGS_HEADER, settingsValue);
         return UPGRADE_HEADERS;
@@ -99,12 +125,24 @@ public class Http2ClientUpgradeCodec implements HttpClientUpgradeHandler.Upgrade
 
     @Override
     public void upgradeTo(ChannelHandlerContext ctx, FullHttpResponse upgradeResponse)
-            throws Exception {
-        // Add the handler to the pipeline.
-        ctx.pipeline().addAfter(ctx.name(), handlerName, upgradeToHandler);
+        throws Exception {
+        try {
+            // Add the handler to the pipeline.
+            ctx.pipeline().addAfter(ctx.name(), handlerName, upgradeToHandler);
+
+            // Add the Http2 Multiplex handler as this handler handle events produced by the connectionHandler.
+            // See https://github.com/netty/netty/issues/9495
+            if (http2MultiplexHandler != null) {
+                final String name = ctx.pipeline().context(connectionHandler).name();
+                ctx.pipeline().addAfter(name, null, http2MultiplexHandler);
+            }
 
-        // Reserve local stream 1 for the response.
-        connectionHandler.onHttpClientUpgrade();
+            // Reserve local stream 1 for the response.
+            connectionHandler.onHttpClientUpgrade();
+        } catch (Http2Exception e) {
+            ctx.fireExceptionCaught(e);
+            ctx.close();
+        }
     }
 
     /**
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2CodecUtil.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2CodecUtil.java
index 7ebc8fd..303527c 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2CodecUtil.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2CodecUtil.java
@@ -117,7 +117,6 @@ public final class Http2CodecUtil {
     public static final int SMALLEST_MAX_CONCURRENT_STREAMS = 100;
     static final int DEFAULT_MAX_RESERVED_STREAMS = SMALLEST_MAX_CONCURRENT_STREAMS;
     static final int DEFAULT_MIN_ALLOCATION_CHUNK = 1024;
-    static final int DEFAULT_INITIAL_HUFFMAN_DECODE_CAPACITY = 32;
 
     /**
      * Calculate the threshold in bytes which should trigger a {@code GO_AWAY} if a set of headers exceeds this amount.
@@ -133,6 +132,8 @@ public final class Http2CodecUtil {
 
     public static final long DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT_MILLIS = MILLISECONDS.convert(30, SECONDS);
 
+    public static final int DEFAULT_MAX_QUEUED_CONTROL_FRAMES = 10000;
+
     /**
      * Returns {@code true} if the stream is an outbound stream.
      *
@@ -151,6 +152,10 @@ public final class Http2CodecUtil {
         return streamId >= 0;
     }
 
+    static boolean isStreamIdValid(int streamId, boolean server) {
+        return isStreamIdValid(streamId) && server == ((streamId & 1) == 0);
+    }
+
     /**
      * Indicates whether or not the given value for max frame size falls within the valid range.
      */
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ConnectionHandler.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ConnectionHandler.java
index 4f66e91..909ca74 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ConnectionHandler.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ConnectionHandler.java
@@ -76,39 +76,27 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http
     private final Http2ConnectionDecoder decoder;
     private final Http2ConnectionEncoder encoder;
     private final Http2Settings initialSettings;
+    private final boolean decoupleCloseAndGoAway;
     private ChannelFutureListener closeListener;
     private BaseDecoder byteDecoder;
     private long gracefulShutdownTimeoutMillis;
 
     protected Http2ConnectionHandler(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder,
                                      Http2Settings initialSettings) {
+        this(decoder, encoder, initialSettings, false);
+    }
+
+    protected Http2ConnectionHandler(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder,
+                                     Http2Settings initialSettings, boolean decoupleCloseAndGoAway) {
         this.initialSettings = checkNotNull(initialSettings, "initialSettings");
         this.decoder = checkNotNull(decoder, "decoder");
         this.encoder = checkNotNull(encoder, "encoder");
+        this.decoupleCloseAndGoAway = decoupleCloseAndGoAway;
         if (encoder.connection() != decoder.connection()) {
             throw new IllegalArgumentException("Encoder and Decoder do not share the same connection object");
         }
     }
 
-    Http2ConnectionHandler(boolean server, Http2FrameWriter frameWriter, Http2FrameLogger frameLogger,
-                    Http2Settings initialSettings) {
-        this.initialSettings = checkNotNull(initialSettings, "initialSettings");
-
-        Http2Connection connection = new DefaultHttp2Connection(server);
-
-        Long maxHeaderListSize = initialSettings.maxHeaderListSize();
-        Http2FrameReader frameReader = new DefaultHttp2FrameReader(maxHeaderListSize == null ?
-                new DefaultHttp2HeadersDecoder(true) :
-                new DefaultHttp2HeadersDecoder(true, maxHeaderListSize));
-
-        if (frameLogger != null) {
-            frameWriter = new Http2OutboundFrameLogger(frameWriter, frameLogger);
-            frameReader = new Http2InboundFrameLogger(frameReader, frameLogger);
-        }
-        encoder = new DefaultHttp2ConnectionEncoder(connection, frameWriter);
-        decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, frameReader);
-    }
-
     /**
      * Get the amount of time (in milliseconds) this endpoint will wait for all streams to be closed before closing
      * the connection during the graceful shutdown process. Returns -1 if this connection is configured to wait
@@ -233,7 +221,7 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http
         private ByteBuf clientPrefaceString;
         private boolean prefaceSent;
 
-        public PrefaceDecoder(ChannelHandlerContext ctx) throws Exception {
+        PrefaceDecoder(ChannelHandlerContext ctx) throws Exception {
             clientPrefaceString = clientPrefaceString(encoder.connection());
             // This handler was just added to the context. In case it was handled after
             // the connection became active, send the connection preface now.
@@ -334,7 +322,7 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http
          * Peeks at that the next frame in the buffer and verifies that it is a non-ack {@code SETTINGS} frame.
          *
          * @param in the inbound buffer.
-         * @return {@code} true if the next frame is a non-ack {@code SETTINGS} frame, {@code false} if more
+         * @return {@code true} if the next frame is a non-ack {@code SETTINGS} frame, {@code false} if more
          * data is required before we can determine the next frame type.
          * @throws Http2Exception thrown if the next frame is NOT a non-ack {@code SETTINGS} frame.
          */
@@ -468,6 +456,10 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http
 
     @Override
     public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
+        if (decoupleCloseAndGoAway) {
+            ctx.close(promise);
+            return;
+        }
         promise = promise.unvoid();
         // Avoid NotYetConnectedException
         if (!ctx.channel().isActive()) {
@@ -480,22 +472,44 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http
         // a GO_AWAY has been sent we send a empty buffer just so we can wait to close until all other data has been
         // flushed to the OS.
         // https://github.com/netty/netty/issues/5307
-        final ChannelFuture future = connection().goAwaySent() ? ctx.write(EMPTY_BUFFER) : goAway(ctx, null);
+        ChannelFuture f = connection().goAwaySent() ? ctx.write(EMPTY_BUFFER) : goAway(ctx, null, ctx.newPromise());
         ctx.flush();
-        doGracefulShutdown(ctx, future, promise);
+        doGracefulShutdown(ctx, f, promise);
     }
 
-    private void doGracefulShutdown(ChannelHandlerContext ctx, ChannelFuture future, ChannelPromise promise) {
+    private ChannelFutureListener newClosingChannelFutureListener(
+            ChannelHandlerContext ctx, ChannelPromise promise) {
+        long gracefulShutdownTimeoutMillis = this.gracefulShutdownTimeoutMillis;
+        return gracefulShutdownTimeoutMillis < 0 ?
+                new ClosingChannelFutureListener(ctx, promise) :
+                new ClosingChannelFutureListener(ctx, promise, gracefulShutdownTimeoutMillis, MILLISECONDS);
+    }
+
+    private void doGracefulShutdown(ChannelHandlerContext ctx, ChannelFuture future, final ChannelPromise promise) {
+        final ChannelFutureListener listener = newClosingChannelFutureListener(ctx, promise);
         if (isGracefulShutdownComplete()) {
-            // If there are no active streams, close immediately after the GO_AWAY write completes.
-            future.addListener(new ClosingChannelFutureListener(ctx, promise));
+            // If there are no active streams, close immediately after the GO_AWAY write completes or the timeout
+            // elapsed.
+            future.addListener(listener);
         } else {
             // If there are active streams we should wait until they are all closed before closing the connection.
-            if (gracefulShutdownTimeoutMillis < 0) {
-                closeListener = new ClosingChannelFutureListener(ctx, promise);
-            } else {
-                closeListener = new ClosingChannelFutureListener(ctx, promise,
-                                                                 gracefulShutdownTimeoutMillis, MILLISECONDS);
+
+            // The ClosingChannelFutureListener will cascade promise completion. We need to always notify the
+            // new ClosingChannelFutureListener when the graceful close completes if the promise is not null.
+            if (closeListener == null) {
+                closeListener = listener;
+            } else if (promise != null) {
+                final ChannelFutureListener oldCloseListener = closeListener;
+                closeListener = new ChannelFutureListener() {
+                    @Override
+                    public void operationComplete(ChannelFuture future) throws Exception {
+                        try {
+                            oldCloseListener.operationComplete(future);
+                        } finally {
+                            listener.operationComplete(future);
+                        }
+                    }
+                };
             }
         }
     }
@@ -655,14 +669,11 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http
         }
 
         ChannelPromise promise = ctx.newPromise();
-        ChannelFuture future = goAway(ctx, http2Ex);
-        switch (http2Ex.shutdownHint()) {
-        case GRACEFUL_SHUTDOWN:
+        ChannelFuture future = goAway(ctx, http2Ex, ctx.newPromise());
+        if (http2Ex.shutdownHint() == Http2Exception.ShutdownHint.GRACEFUL_SHUTDOWN) {
             doGracefulShutdown(ctx, future, promise);
-            break;
-        default:
-            future.addListener(new ClosingChannelFutureListener(ctx, promise));
-            break;
+        } else {
+            future.addListener(newClosingChannelFutureListener(ctx, promise));
         }
     }
 
@@ -773,6 +784,13 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http
             // Don't write a RST_STREAM frame if we have already written one.
             return promise.setSuccess();
         }
+        // Synchronously set the resetSent flag to prevent any subsequent calls
+        // from resulting in multiple reset frames being sent.
+        //
+        // This needs to be done before we notify the promise as the promise may have a listener attached that
+        // call resetStream(...) again.
+        stream.resetSent();
+
         final ChannelFuture future;
         // If the remote peer is not aware of the steam, then we are not allowed to send a RST_STREAM
         // https://tools.ietf.org/html/rfc7540#section-6.4.
@@ -782,11 +800,6 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http
         } else {
             future = frameWriter().writeRstStream(ctx, stream.id(), errorCode, promise);
         }
-
-        // Synchronously set the resetSent flag to prevent any subsequent calls
-        // from resulting in multiple reset frames being sent.
-        stream.resetSent();
-
         if (future.isDone()) {
             processRstStreamWriteResult(ctx, stream, future);
         } else {
@@ -861,10 +874,10 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http
      * Close the remote endpoint with with a {@code GO_AWAY} frame. Does <strong>not</strong> flush
      * immediately, this is the responsibility of the caller.
      */
-    private ChannelFuture goAway(ChannelHandlerContext ctx, Http2Exception cause) {
+    private ChannelFuture goAway(ChannelHandlerContext ctx, Http2Exception cause, ChannelPromise promise) {
         long errorCode = cause != null ? cause.error().code() : NO_ERROR.code();
         int lastKnownStream = connection().remote().lastStreamCreated();
-        return goAway(ctx, lastKnownStream, errorCode, Http2CodecUtil.toByteBuf(ctx, cause), ctx.newPromise());
+        return goAway(ctx, lastKnownStream, errorCode, Http2CodecUtil.toByteBuf(ctx, cause), promise);
     }
 
     private void processRstStreamWriteResult(ChannelHandlerContext ctx, Http2Stream stream, ChannelFuture future) {
@@ -936,17 +949,25 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http
             timeoutTask = ctx.executor().schedule(new Runnable() {
                 @Override
                 public void run() {
-                    ctx.close(promise);
+                    doClose();
                 }
             }, timeout, unit);
         }
 
         @Override
-        public void operationComplete(ChannelFuture sentGoAwayFuture) throws Exception {
+        public void operationComplete(ChannelFuture sentGoAwayFuture) {
             if (timeoutTask != null) {
                 timeoutTask.cancel(false);
             }
-            ctx.close(promise);
+            doClose();
+        }
+
+        private void doClose() {
+            if (promise == null) {
+                ctx.close();
+            } else {
+                ctx.close(promise);
+            }
         }
     }
 }
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ConnectionHandlerBuilder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ConnectionHandlerBuilder.java
index 3e30200..c6d1ce7 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ConnectionHandlerBuilder.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ConnectionHandlerBuilder.java
@@ -88,10 +88,16 @@ public final class Http2ConnectionHandlerBuilder
     }
 
     @Override
+    @Deprecated
     public Http2ConnectionHandlerBuilder initialHuffmanDecodeCapacity(int initialHuffmanDecodeCapacity) {
         return super.initialHuffmanDecodeCapacity(initialHuffmanDecodeCapacity);
     }
 
+    @Override
+    public Http2ConnectionHandlerBuilder decoupleCloseAndGoAway(boolean decoupleCloseAndGoAway) {
+        return super.decoupleCloseAndGoAway(decoupleCloseAndGoAway);
+    }
+
     @Override
     public Http2ConnectionHandler build() {
         return super.build();
@@ -100,6 +106,6 @@ public final class Http2ConnectionHandlerBuilder
     @Override
     protected Http2ConnectionHandler build(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder,
                                            Http2Settings initialSettings) {
-        return new Http2ConnectionHandler(decoder, encoder, initialSettings);
+        return new Http2ConnectionHandler(decoder, encoder, initialSettings, decoupleCloseAndGoAway());
     }
 }
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ControlFrameLimitEncoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ControlFrameLimitEncoder.java
new file mode 100644
index 0000000..0d25123
--- /dev/null
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ControlFrameLimitEncoder.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2019 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.channel.ChannelFuture;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelPromise;
+import io.netty.util.internal.ObjectUtil;
+import io.netty.util.internal.logging.InternalLogger;
+import io.netty.util.internal.logging.InternalLoggerFactory;
+
+/**
+ * {@link DecoratingHttp2ConnectionEncoder} which guards against a remote peer that will trigger a massive amount
+ * of control frames but will not consume our responses to these.
+ * This encoder will tear-down the connection once we reached the configured limit to reduce the risk of DDOS.
+ */
+final class Http2ControlFrameLimitEncoder extends DecoratingHttp2ConnectionEncoder {
+    private static final InternalLogger logger = InternalLoggerFactory.getInstance(Http2ControlFrameLimitEncoder.class);
+
+    private final int maxOutstandingControlFrames;
+    private final ChannelFutureListener outstandingControlFramesListener = new ChannelFutureListener() {
+        @Override
+        public void operationComplete(ChannelFuture future) {
+            outstandingControlFrames--;
+        }
+    };
+    private Http2LifecycleManager lifecycleManager;
+    private int outstandingControlFrames;
+    private boolean limitReached;
+
+    Http2ControlFrameLimitEncoder(Http2ConnectionEncoder delegate, int maxOutstandingControlFrames) {
+        super(delegate);
+        this.maxOutstandingControlFrames = ObjectUtil.checkPositive(maxOutstandingControlFrames,
+                "maxOutstandingControlFrames");
+    }
+
+    @Override
+    public void lifecycleManager(Http2LifecycleManager lifecycleManager) {
+        this.lifecycleManager = lifecycleManager;
+        super.lifecycleManager(lifecycleManager);
+    }
+
+    @Override
+    public ChannelFuture writeSettingsAck(ChannelHandlerContext ctx, ChannelPromise promise) {
+        ChannelPromise newPromise = handleOutstandingControlFrames(ctx, promise);
+        if (newPromise == null) {
+            return promise;
+        }
+        return super.writeSettingsAck(ctx, newPromise);
+    }
+
+    @Override
+    public ChannelFuture writePing(ChannelHandlerContext ctx, boolean ack, long data, ChannelPromise promise) {
+        // Only apply the limit to ping acks.
+        if (ack) {
+            ChannelPromise newPromise = handleOutstandingControlFrames(ctx, promise);
+            if (newPromise == null) {
+                return promise;
+            }
+            return super.writePing(ctx, ack, data, newPromise);
+        }
+        return super.writePing(ctx, ack, data, promise);
+    }
+
+    @Override
+    public ChannelFuture writeRstStream(
+            ChannelHandlerContext ctx, int streamId, long errorCode, ChannelPromise promise) {
+        ChannelPromise newPromise = handleOutstandingControlFrames(ctx, promise);
+        if (newPromise == null) {
+            return promise;
+        }
+        return super.writeRstStream(ctx, streamId, errorCode, newPromise);
+    }
+
+    private ChannelPromise handleOutstandingControlFrames(ChannelHandlerContext ctx, ChannelPromise promise) {
+        if (!limitReached) {
+            if (outstandingControlFrames == maxOutstandingControlFrames) {
+                // Let's try to flush once as we may be able to flush some of the control frames.
+                ctx.flush();
+            }
+            if (outstandingControlFrames == maxOutstandingControlFrames) {
+                limitReached = true;
+                Http2Exception exception = Http2Exception.connectionError(Http2Error.ENHANCE_YOUR_CALM,
+                        "Maximum number %d of outstanding control frames reached", maxOutstandingControlFrames);
+                logger.info("Maximum number {} of outstanding control frames reached. Closing channel {}",
+                        maxOutstandingControlFrames, ctx.channel(), exception);
+
+                // First notify the Http2LifecycleManager and then close the connection.
+                lifecycleManager.onError(ctx, true, exception);
+                ctx.close();
+            }
+            outstandingControlFrames++;
+
+            // We did not reach the limit yet, add the listener to decrement the number of outstanding control frames
+            // once the promise was completed
+            return promise.unvoid().addListener(outstandingControlFramesListener);
+        }
+        return promise;
+    }
+}
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2EmptyDataFrameConnectionDecoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2EmptyDataFrameConnectionDecoder.java
new file mode 100644
index 0000000..4f0e155
--- /dev/null
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2EmptyDataFrameConnectionDecoder.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2019 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.util.internal.ObjectUtil;
+
+/**
+ * Enforce a limit on the maximum number of consecutive empty DATA frames (without end_of_stream flag) that are allowed
+ * before the connection will be closed.
+ */
+final class Http2EmptyDataFrameConnectionDecoder extends DecoratingHttp2ConnectionDecoder {
+
+    private final int maxConsecutiveEmptyFrames;
+
+    Http2EmptyDataFrameConnectionDecoder(Http2ConnectionDecoder delegate, int maxConsecutiveEmptyFrames) {
+        super(delegate);
+        this.maxConsecutiveEmptyFrames = ObjectUtil.checkPositive(
+                maxConsecutiveEmptyFrames, "maxConsecutiveEmptyFrames");
+    }
+
+    @Override
+    public void frameListener(Http2FrameListener listener) {
+        if (listener != null) {
+            super.frameListener(new Http2EmptyDataFrameListener(listener, maxConsecutiveEmptyFrames));
+        } else {
+            super.frameListener(null);
+        }
+    }
+
+    @Override
+    public Http2FrameListener frameListener() {
+        Http2FrameListener frameListener = frameListener0();
+        // Unwrap the original Http2FrameListener as we add this decoder under the hood.
+        if (frameListener instanceof Http2EmptyDataFrameListener) {
+            return ((Http2EmptyDataFrameListener) frameListener).listener;
+        }
+        return frameListener;
+    }
+
+    // Package-private for testing
+    Http2FrameListener frameListener0() {
+        return super.frameListener();
+    }
+}
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2EmptyDataFrameListener.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2EmptyDataFrameListener.java
new file mode 100644
index 0000000..dcbd987
--- /dev/null
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2EmptyDataFrameListener.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2019 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.buffer.ByteBuf;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.util.internal.ObjectUtil;
+
+/**
+ * Enforce a limit on the maximum number of consecutive empty DATA frames (without end_of_stream flag) that are allowed
+ * before the connection will be closed.
+ */
+final class Http2EmptyDataFrameListener extends Http2FrameListenerDecorator {
+    private final int maxConsecutiveEmptyFrames;
+
+    private boolean violationDetected;
+    private int emptyDataFrames;
+
+    Http2EmptyDataFrameListener(Http2FrameListener listener, int maxConsecutiveEmptyFrames) {
+        super(listener);
+        this.maxConsecutiveEmptyFrames = ObjectUtil.checkPositive(
+                maxConsecutiveEmptyFrames, "maxConsecutiveEmptyFrames");
+    }
+
+    @Override
+    public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream)
+            throws Http2Exception {
+        if (endOfStream || data.isReadable()) {
+            emptyDataFrames = 0;
+        } else if (emptyDataFrames++ == maxConsecutiveEmptyFrames && !violationDetected) {
+            violationDetected = true;
+            throw Http2Exception.connectionError(Http2Error.ENHANCE_YOUR_CALM,
+                    "Maximum number %d of empty data frames without end_of_stream flag received",
+                    maxConsecutiveEmptyFrames);
+        }
+
+        return super.onDataRead(ctx, streamId, data, padding, endOfStream);
+    }
+
+    @Override
+    public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers,
+                              int padding, boolean endStream) throws Http2Exception {
+        emptyDataFrames = 0;
+        super.onHeadersRead(ctx, streamId, headers, padding, endStream);
+    }
+
+    @Override
+    public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency,
+                              short weight, boolean exclusive, int padding, boolean endStream) throws Http2Exception {
+        emptyDataFrames = 0;
+        super.onHeadersRead(ctx, streamId, headers, streamDependency, weight, exclusive, padding, endStream);
+    }
+}
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2Exception.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2Exception.java
index 258f871..cedfe0b 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2Exception.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2Exception.java
@@ -15,6 +15,8 @@
 
 package io.netty.handler.codec.http2;
 
+import io.netty.util.internal.PlatformDependent;
+import io.netty.util.internal.SuppressJava6Requirement;
 import io.netty.util.internal.UnstableApi;
 
 import java.util.ArrayList;
@@ -62,6 +64,22 @@ public class Http2Exception extends Exception {
         this.shutdownHint = checkNotNull(shutdownHint, "shutdownHint");
     }
 
+    static Http2Exception newStatic(Http2Error error, String message, ShutdownHint shutdownHint) {
+        if (PlatformDependent.javaVersion() >= 7) {
+            return new Http2Exception(error, message, shutdownHint, true);
+        }
+        return new Http2Exception(error, message, shutdownHint);
+    }
+
+    @SuppressJava6Requirement(reason = "uses Java 7+ Exception.<init>(String, Throwable, boolean, boolean)" +
+            " but is guarded by version checks")
+    private Http2Exception(Http2Error error, String message, ShutdownHint shutdownHint, boolean shared) {
+        super(message, null, false, true);
+        assert shared;
+        this.error = checkNotNull(error, "error");
+        this.shutdownHint = checkNotNull(shutdownHint, "shutdownHint");
+    }
+
     public Http2Error error() {
         return error;
     }
@@ -79,7 +97,7 @@ public class Http2Exception extends Exception {
      * @param error The type of error as defined by the HTTP/2 specification.
      * @param fmt String with the content and format for the additional debug data.
      * @param args Objects which fit into the format defined by {@code fmt}.
-     * @return An exception which can be translated into a HTTP/2 error.
+     * @return An exception which can be translated into an HTTP/2 error.
      */
     public static Http2Exception connectionError(Http2Error error, String fmt, Object... args) {
         return new Http2Exception(error, String.format(fmt, args));
@@ -92,7 +110,7 @@ public class Http2Exception extends Exception {
      * @param cause The object which caused the error.
      * @param fmt String with the content and format for the additional debug data.
      * @param args Objects which fit into the format defined by {@code fmt}.
-     * @return An exception which can be translated into a HTTP/2 error.
+     * @return An exception which can be translated into an HTTP/2 error.
      */
     public static Http2Exception connectionError(Http2Error error, Throwable cause,
             String fmt, Object... args) {
@@ -105,7 +123,7 @@ public class Http2Exception extends Exception {
      * @param error The type of error as defined by the HTTP/2 specification.
      * @param fmt String with the content and format for the additional debug data.
      * @param args Objects which fit into the format defined by {@code fmt}.
-     * @return An exception which can be translated into a HTTP/2 error.
+     * @return An exception which can be translated into an HTTP/2 error.
      */
     public static Http2Exception closedStreamError(Http2Error error, String fmt, Object... args) {
         return new ClosedStreamCreationException(error, String.format(fmt, args));
@@ -194,7 +212,7 @@ public class Http2Exception extends Exception {
     /**
      * Provides a hint as to if shutdown is justified, what type of shutdown should be executed.
      */
-    public static enum ShutdownHint {
+    public enum ShutdownHint {
         /**
          * Do not shutdown the underlying channel.
          */
@@ -207,7 +225,7 @@ public class Http2Exception extends Exception {
         /**
          * Close the channel immediately after a {@code GOAWAY} is sent.
          */
-        HARD_SHUTDOWN;
+        HARD_SHUTDOWN
     }
 
     /**
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameCodec.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameCodec.java
index cf756ca..d5fa758 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameCodec.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameCodec.java
@@ -16,6 +16,7 @@
 package io.netty.handler.codec.http2;
 
 import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
 import io.netty.channel.ChannelFuture;
 import io.netty.channel.ChannelFutureListener;
 import io.netty.channel.ChannelHandlerContext;
@@ -32,18 +33,21 @@ import io.netty.util.ReferenceCounted;
 import io.netty.util.collection.IntObjectHashMap;
 import io.netty.util.collection.IntObjectMap;
 import io.netty.util.internal.UnstableApi;
+import io.netty.util.internal.logging.InternalLogLevel;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 
+import static io.netty.buffer.ByteBufUtil.writeAscii;
 import static io.netty.handler.codec.http2.Http2CodecUtil.HTTP_UPGRADE_STREAM_ID;
 import static io.netty.handler.codec.http2.Http2CodecUtil.isStreamIdValid;
+import static io.netty.handler.codec.http2.Http2Error.NO_ERROR;
 
 /**
  * <p><em>This API is very immature.</em> The Http2Connection-based API is currently preferred over this API.
  * This API is targeted to eventually replace or reduce the need for the {@link Http2ConnectionHandler} API.
  *
- * <p>A HTTP/2 handler that maps HTTP/2 frames to {@link Http2Frame} objects and vice versa. For every incoming HTTP/2
- * frame, a {@link Http2Frame} object is created and propagated via {@link #channelRead}. Outbound {@link Http2Frame}
+ * <p>An HTTP/2 handler that maps HTTP/2 frames to {@link Http2Frame} objects and vice versa. For every incoming HTTP/2
+ * frame, an {@link Http2Frame} object is created and propagated via {@link #channelRead}. Outbound {@link Http2Frame}
  * objects received via {@link #write} are converted to the HTTP/2 wire format. HTTP/2 frames specific to a stream
  * implement the {@link Http2StreamFrame} interface. The {@link Http2FrameCodec} is instantiated using the
  * {@link Http2FrameCodecBuilder}. It's recommended for channel handlers to inherit from the
@@ -76,7 +80,7 @@ import static io.netty.handler.codec.http2.Http2CodecUtil.isStreamIdValid;
  *
  * <h3>New inbound Streams</h3>
  *
- * The first frame of a HTTP/2 stream must be a {@link Http2HeadersFrame}, which will have a {@link Http2FrameStream}
+ * The first frame of an HTTP/2 stream must be an {@link Http2HeadersFrame}, which will have an {@link Http2FrameStream}
  * object attached.
  *
  * <h3>New outbound Streams</h3>
@@ -133,7 +137,7 @@ import static io.netty.handler.codec.http2.Http2CodecUtil.isStreamIdValid;
  * reference counted objects (e.g. {@link ByteBuf}s). The frame codec will call {@link ReferenceCounted#retain()} before
  * propagating a reference counted object through the pipeline, and thus an application handler needs to release such
  * an object after having consumed it. For more information on reference counting take a look at
- * http://netty.io/wiki/reference-counted-objects.html
+ * https://netty.io/wiki/reference-counted-objects.html
  *
  * <h3>HTTP Upgrade</h3>
  *
@@ -157,8 +161,9 @@ public class Http2FrameCodec extends Http2ConnectionHandler {
     private final IntObjectMap<DefaultHttp2FrameStream> frameStreamToInitializeMap =
             new IntObjectHashMap<DefaultHttp2FrameStream>(8);
 
-    Http2FrameCodec(Http2ConnectionEncoder encoder, Http2ConnectionDecoder decoder, Http2Settings initialSettings) {
-        super(decoder, encoder, initialSettings);
+    Http2FrameCodec(Http2ConnectionEncoder encoder, Http2ConnectionDecoder decoder, Http2Settings initialSettings,
+                    boolean decoupleCloseAndGoAway) {
+        super(decoder, encoder, initialSettings, decoupleCloseAndGoAway);
 
         decoder.frameListener(new FrameListener());
         connection().addListener(new ConnectionListener());
@@ -182,18 +187,28 @@ public class Http2FrameCodec extends Http2ConnectionHandler {
      */
     final void forEachActiveStream(final Http2FrameStreamVisitor streamVisitor) throws Http2Exception {
         assert ctx.executor().inEventLoop();
-
-        connection().forEachActiveStream(new Http2StreamVisitor() {
-            @Override
-            public boolean visit(Http2Stream stream) {
-                try {
-                    return streamVisitor.visit((Http2FrameStream) stream.getProperty(streamKey));
-                } catch (Throwable cause) {
-                    onError(ctx, false, cause);
-                    return false;
+        if (connection().numActiveStreams() > 0) {
+            connection().forEachActiveStream(new Http2StreamVisitor() {
+                @Override
+                public boolean visit(Http2Stream stream) {
+                    try {
+                        return streamVisitor.visit((Http2FrameStream) stream.getProperty(streamKey));
+                    } catch (Throwable cause) {
+                        onError(ctx, false, cause);
+                        return false;
+                    }
                 }
-            }
-        });
+            });
+        }
+    }
+
+    /**
+     * Retrieve the number of streams currently in the process of being initialized.
+     *
+     * This is package-private for testing only.
+     */
+    int numInitializingStreams() {
+        return frameStreamToInitializeMap.size();
     }
 
     @Override
@@ -234,10 +249,20 @@ public class Http2FrameCodec extends Http2ConnectionHandler {
      * HTTP/2 on stream 1 (the stream specifically reserved for cleartext HTTP upgrade).
      */
     @Override
-    public final void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
+    public final void userEventTriggered(final ChannelHandlerContext ctx, final Object evt) throws Exception {
         if (evt == Http2ConnectionPrefaceAndSettingsFrameWrittenEvent.INSTANCE) {
             // The user event implies that we are on the client.
             tryExpandConnectionFlowControlWindow(connection());
+
+            // We schedule this on the EventExecutor to allow to have any extra handlers added to the pipeline
+            // before we pass the event to the next handler. This is needed as the event may be called from within
+            // handlerAdded(...) which will be run before other handlers will be added to the pipeline.
+            ctx.executor().execute(new Runnable() {
+                @Override
+                public void run() {
+                    ctx.fireUserEventTriggered(evt);
+                }
+            });
         } else if (evt instanceof UpgradeEvent) {
             UpgradeEvent upgrade = (UpgradeEvent) evt;
             try {
@@ -257,9 +282,9 @@ public class Http2FrameCodec extends Http2ConnectionHandler {
             } finally {
                 upgrade.release();
             }
-            return;
+        } else {
+            ctx.fireUserEventTriggered(evt);
         }
-        super.userEventTriggered(ctx, evt);
     }
 
     /**
@@ -291,12 +316,25 @@ public class Http2FrameCodec extends Http2ConnectionHandler {
             }
         } else if (msg instanceof Http2ResetFrame) {
             Http2ResetFrame rstFrame = (Http2ResetFrame) msg;
-            encoder().writeRstStream(ctx, rstFrame.stream().id(), rstFrame.errorCode(), promise);
+            int id = rstFrame.stream().id();
+            // Only ever send a reset frame if stream may have existed before as otherwise we may send a RST on a
+            // stream in an invalid state and cause a connection error.
+            if (connection().streamMayHaveExisted(id)) {
+                encoder().writeRstStream(ctx, rstFrame.stream().id(), rstFrame.errorCode(), promise);
+            } else {
+                ReferenceCountUtil.release(rstFrame);
+                promise.setFailure(Http2Exception.streamError(
+                        rstFrame.stream().id(), Http2Error.PROTOCOL_ERROR, "Stream never existed"));
+            }
         } else if (msg instanceof Http2PingFrame) {
             Http2PingFrame frame = (Http2PingFrame) msg;
             encoder().writePing(ctx, frame.ack(), frame.content(), promise);
         } else if (msg instanceof Http2SettingsFrame) {
             encoder().writeSettings(ctx, ((Http2SettingsFrame) msg).settings(), promise);
+        } else if (msg instanceof Http2SettingsAckFrame) {
+            // In the event of manual SETTINGS ACK is is assumed the encoder will apply the earliest received but not
+            // yet ACKed settings.
+            encoder().writeSettingsAck(ctx, promise);
         } else if (msg instanceof Http2GoAwayFrame) {
             writeGoAwayFrame(ctx, (Http2GoAwayFrame) msg, promise);
         } else if (msg instanceof Http2UnknownFrame) {
@@ -357,6 +395,12 @@ public class Http2FrameCodec extends Http2ConnectionHandler {
             final int streamId = connection.local().incrementAndGetNextStreamId();
             if (streamId < 0) {
                 promise.setFailure(new Http2NoMoreStreamIdsException());
+
+                // Simulate a GOAWAY being received due to stream exhaustion on this connection. We use the maximum
+                // valid stream ID for the current peer.
+                onHttp2Frame(ctx, new DefaultHttp2GoAwayFrame(connection.isServer() ? Integer.MAX_VALUE :
+                        Integer.MAX_VALUE - 1, NO_ERROR.code(),
+                        writeAscii(ctx.alloc(), "Stream IDs exhausted on local stream creation")));
                 return;
             }
             stream.id = streamId;
@@ -371,56 +415,52 @@ public class Http2FrameCodec extends Http2ConnectionHandler {
             // We should not re-use ids.
             assert old == null;
 
-            // TODO(buchgr): Once Http2FrameStream and Http2Stream are merged this is no longer necessary.
-            final ChannelPromise writePromise = ctx.newPromise();
-
             encoder().writeHeaders(ctx, streamId, headersFrame.headers(), headersFrame.padding(),
-                    headersFrame.isEndStream(), writePromise);
-            if (writePromise.isDone()) {
-                notifyHeaderWritePromise(writePromise, promise);
-            } else {
-                numBufferedStreams++;
+                    headersFrame.isEndStream(), promise);
 
-                writePromise.addListener(new ChannelFutureListener() {
+            if (!promise.isDone()) {
+                numBufferedStreams++;
+                // Clean up the stream being initialized if writing the headers fails and also
+                // decrement the number of buffered streams.
+                promise.addListener(new ChannelFutureListener() {
                     @Override
-                    public void operationComplete(ChannelFuture future) throws Exception {
+                    public void operationComplete(ChannelFuture channelFuture) {
                         numBufferedStreams--;
 
-                        notifyHeaderWritePromise(future, promise);
+                        handleHeaderFuture(channelFuture, streamId);
                     }
                 });
+            } else {
+                handleHeaderFuture(promise, streamId);
             }
         }
     }
 
-    private static void notifyHeaderWritePromise(ChannelFuture future, ChannelPromise promise) {
-        Throwable cause = future.cause();
-        if (cause == null) {
-            promise.setSuccess();
-        } else {
-            promise.setFailure(cause);
+    private void handleHeaderFuture(ChannelFuture channelFuture, int streamId) {
+        if (!channelFuture.isSuccess()) {
+            frameStreamToInitializeMap.remove(streamId);
         }
     }
 
     private void onStreamActive0(Http2Stream stream) {
-        if (connection().local().isValidStreamId(stream.id())) {
+        if (stream.id() != Http2CodecUtil.HTTP_UPGRADE_STREAM_ID &&
+                connection().local().isValidStreamId(stream.id())) {
             return;
         }
 
-        Http2FrameStream stream2 = newStream().setStreamAndProperty(streamKey, stream);
+        DefaultHttp2FrameStream stream2 = newStream().setStreamAndProperty(streamKey, stream);
         onHttp2StreamStateChanged(ctx, stream2);
     }
 
     private final class ConnectionListener extends Http2ConnectionAdapter {
-
         @Override
         public void onStreamAdded(Http2Stream stream) {
             DefaultHttp2FrameStream frameStream = frameStreamToInitializeMap.remove(stream.id());
 
-             if (frameStream != null) {
-                 frameStream.setStreamAndProperty(streamKey, stream);
-             }
-         }
+            if (frameStream != null) {
+                frameStream.setStreamAndProperty(streamKey, stream);
+            }
+        }
 
         @Override
         public void onStreamActive(Http2Stream stream) {
@@ -429,15 +469,16 @@ public class Http2FrameCodec extends Http2ConnectionHandler {
 
         @Override
         public void onStreamClosed(Http2Stream stream) {
-            Http2FrameStream stream2 = stream.getProperty(streamKey);
-            if (stream2 != null) {
-                onHttp2StreamStateChanged(ctx, stream2);
-            }
+            onHttp2StreamStateChanged0(stream);
         }
 
         @Override
         public void onStreamHalfClosed(Http2Stream stream) {
-            Http2FrameStream stream2 = stream.getProperty(streamKey);
+            onHttp2StreamStateChanged0(stream);
+        }
+
+        private void onHttp2StreamStateChanged0(Http2Stream stream) {
+            DefaultHttp2FrameStream stream2 = stream.getProperty(streamKey);
             if (stream2 != null) {
                 onHttp2StreamStateChanged(ctx, stream2);
             }
@@ -487,10 +528,14 @@ public class Http2FrameCodec extends Http2ConnectionHandler {
         }
     }
 
-    void onHttp2UnknownStreamError(@SuppressWarnings("unused") ChannelHandlerContext ctx, Throwable cause,
+    private void onHttp2UnknownStreamError(@SuppressWarnings("unused") ChannelHandlerContext ctx, Throwable cause,
                                    Http2Exception.StreamException streamException) {
-        // Just log....
-        LOG.warn("Stream exception thrown for unkown stream {}.", streamException.streamId(), cause);
+        // It is normal to hit a race condition where we still receive frames for a stream that this
+        // peer has deemed closed, such as if this peer sends a RST(CANCEL) to discard the request.
+        // Since this is likely to be normal we log at DEBUG level.
+        InternalLogLevel level =
+            streamException.error() == Http2Error.STREAM_CLOSED ? InternalLogLevel.DEBUG : InternalLogLevel.WARN;
+        LOG.log(level, "Stream exception thrown for unknown stream {}.", streamException.streamId(), cause);
     }
 
     @Override
@@ -572,7 +617,7 @@ public class Http2FrameCodec extends Http2ConnectionHandler {
 
         @Override
         public void onSettingsAckRead(ChannelHandlerContext ctx) {
-            // TODO: Maybe handle me
+            onHttp2Frame(ctx, Http2SettingsAckFrame.INSTANCE);
         }
 
         @Override
@@ -590,17 +635,17 @@ public class Http2FrameCodec extends Http2ConnectionHandler {
         }
     }
 
-    void onUpgradeEvent(ChannelHandlerContext ctx, UpgradeEvent evt) {
+    private void onUpgradeEvent(ChannelHandlerContext ctx, UpgradeEvent evt) {
         ctx.fireUserEventTriggered(evt);
     }
 
-    void onHttp2StreamWritabilityChanged(ChannelHandlerContext ctx, Http2FrameStream stream,
+    private void onHttp2StreamWritabilityChanged(ChannelHandlerContext ctx, DefaultHttp2FrameStream stream,
                                          @SuppressWarnings("unused") boolean writable) {
-        ctx.fireUserEventTriggered(Http2FrameStreamEvent.writabilityChanged(stream));
+        ctx.fireUserEventTriggered(stream.writabilityChanged);
     }
 
-    void onHttp2StreamStateChanged(ChannelHandlerContext ctx, Http2FrameStream stream) {
-        ctx.fireUserEventTriggered(Http2FrameStreamEvent.stateChanged(stream));
+    void onHttp2StreamStateChanged(ChannelHandlerContext ctx, DefaultHttp2FrameStream stream) {
+        ctx.fireUserEventTriggered(stream.stateChanged);
     }
 
     void onHttp2Frame(ChannelHandlerContext ctx, Http2Frame frame) {
@@ -611,15 +656,10 @@ public class Http2FrameCodec extends Http2ConnectionHandler {
         ctx.fireExceptionCaught(cause);
     }
 
-    final boolean isWritable(DefaultHttp2FrameStream stream) {
-        Http2Stream s = stream.stream;
-        return s != null && connection().remote().flowController().isWritable(s);
-    }
-
     private final class Http2RemoteFlowControllerListener implements Http2RemoteFlowController.Listener {
         @Override
         public void writabilityChanged(Http2Stream stream) {
-            Http2FrameStream frameStream = stream.getProperty(streamKey);
+            DefaultHttp2FrameStream frameStream = stream.getProperty(streamKey);
             if (frameStream == null) {
                 return;
             }
@@ -637,6 +677,11 @@ public class Http2FrameCodec extends Http2ConnectionHandler {
         private volatile int id = -1;
         volatile Http2Stream stream;
 
+        final Http2FrameStreamEvent stateChanged = Http2FrameStreamEvent.stateChanged(this);
+        final Http2FrameStreamEvent writabilityChanged = Http2FrameStreamEvent.writabilityChanged(this);
+
+        Channel attachment;
+
         DefaultHttp2FrameStream setStreamAndProperty(PropertyKey streamKey, Http2Stream stream) {
             assert id == -1 || stream.id() == id;
             this.stream = stream;
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameCodecBuilder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameCodecBuilder.java
index eb45723..fad31b2 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameCodecBuilder.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameCodecBuilder.java
@@ -31,17 +31,19 @@ public class Http2FrameCodecBuilder extends
 
     Http2FrameCodecBuilder(boolean server) {
         server(server);
+        // For backwards compatibility we should disable to timeout by default at this layer.
+        gracefulShutdownTimeoutMillis(0);
     }
 
     /**
-     * Creates a builder for a HTTP/2 client.
+     * Creates a builder for an HTTP/2 client.
      */
     public static Http2FrameCodecBuilder forClient() {
         return new Http2FrameCodecBuilder(false);
     }
 
     /**
-     * Creates a builder for a HTTP/2 server.
+     * Creates a builder for an HTTP/2 server.
      */
     public static Http2FrameCodecBuilder forServer() {
         return new Http2FrameCodecBuilder(true);
@@ -118,6 +120,16 @@ public class Http2FrameCodecBuilder extends
         return super.encoderEnforceMaxConcurrentStreams(encoderEnforceMaxConcurrentStreams);
     }
 
+    @Override
+    public int encoderEnforceMaxQueuedControlFrames() {
+        return super.encoderEnforceMaxQueuedControlFrames();
+    }
+
+    @Override
+    public Http2FrameCodecBuilder encoderEnforceMaxQueuedControlFrames(int maxQueuedControlFrames) {
+        return super.encoderEnforceMaxQueuedControlFrames(maxQueuedControlFrames);
+    }
+
     @Override
     public Http2HeadersEncoder.SensitivityDetector headerSensitivityDetector() {
         return super.headerSensitivityDetector();
@@ -135,10 +147,36 @@ public class Http2FrameCodecBuilder extends
     }
 
     @Override
+    @Deprecated
     public Http2FrameCodecBuilder initialHuffmanDecodeCapacity(int initialHuffmanDecodeCapacity) {
         return super.initialHuffmanDecodeCapacity(initialHuffmanDecodeCapacity);
     }
 
+    @Override
+    public Http2FrameCodecBuilder autoAckSettingsFrame(boolean autoAckSettings) {
+        return super.autoAckSettingsFrame(autoAckSettings);
+    }
+
+    @Override
+    public Http2FrameCodecBuilder autoAckPingFrame(boolean autoAckPingFrame) {
+        return super.autoAckPingFrame(autoAckPingFrame);
+    }
+
+    @Override
+    public Http2FrameCodecBuilder decoupleCloseAndGoAway(boolean decoupleCloseAndGoAway) {
+        return super.decoupleCloseAndGoAway(decoupleCloseAndGoAway);
+    }
+
+    @Override
+    public int decoderEnforceMaxConsecutiveEmptyDataFrames() {
+        return super.decoderEnforceMaxConsecutiveEmptyDataFrames();
+    }
+
+    @Override
+    public Http2FrameCodecBuilder decoderEnforceMaxConsecutiveEmptyDataFrames(int maxConsecutiveEmptyFrames) {
+        return super.decoderEnforceMaxConsecutiveEmptyDataFrames(maxConsecutiveEmptyFrames);
+    }
+
     /**
      * Build a {@link Http2FrameCodec} object.
      */
@@ -151,8 +189,8 @@ public class Http2FrameCodecBuilder extends
             DefaultHttp2Connection connection = new DefaultHttp2Connection(isServer(), maxReservedStreams());
             Long maxHeaderListSize = initialSettings().maxHeaderListSize();
             Http2FrameReader frameReader = new DefaultHttp2FrameReader(maxHeaderListSize == null ?
-                    new DefaultHttp2HeadersDecoder(true) :
-                    new DefaultHttp2HeadersDecoder(true, maxHeaderListSize));
+                    new DefaultHttp2HeadersDecoder(isValidateHeaders()) :
+                    new DefaultHttp2HeadersDecoder(isValidateHeaders(), maxHeaderListSize));
 
             if (frameLogger() != null) {
                 frameWriter = new Http2OutboundFrameLogger(frameWriter, frameLogger());
@@ -162,8 +200,12 @@ public class Http2FrameCodecBuilder extends
             if (encoderEnforceMaxConcurrentStreams()) {
                 encoder = new StreamBufferingEncoder(encoder);
             }
-            Http2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, frameReader);
-
+            Http2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, frameReader,
+                    promisedRequestVerifier(), isAutoAckSettingsFrame(), isAutoAckPingFrame());
+            int maxConsecutiveEmptyDataFrames = decoderEnforceMaxConsecutiveEmptyDataFrames();
+            if (maxConsecutiveEmptyDataFrames > 0) {
+                decoder = new Http2EmptyDataFrameConnectionDecoder(decoder, maxConsecutiveEmptyDataFrames);
+            }
             return build(decoder, encoder, initialSettings());
         }
         return super.build();
@@ -172,6 +214,8 @@ public class Http2FrameCodecBuilder extends
     @Override
     protected Http2FrameCodec build(
             Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder, Http2Settings initialSettings) {
-        return new Http2FrameCodec(encoder, decoder, initialSettings);
+        Http2FrameCodec codec = new Http2FrameCodec(encoder, decoder, initialSettings, decoupleCloseAndGoAway());
+        codec.gracefulShutdownTimeoutMillis(gracefulShutdownTimeoutMillis());
+        return codec;
     }
 }
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameListener.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameListener.java
index e48923b..c8d3518 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameListener.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameListener.java
@@ -160,7 +160,7 @@ public interface Http2FrameListener {
      * Handles an inbound {@code PUSH_PROMISE} frame. Only called if {@code END_HEADERS} encountered.
      * <p>
      * Promised requests MUST be authoritative, cacheable, and safe.
-     * See <a href="https://tools.ietf.org/html/draft-ietf-httpbis-http2-17#section-8.2">[RFC http2], Section 8.2</a>.
+     * See <a href="https://tools.ietf.org/html/rfc7540#section-8.2">[RFC 7540], Section 8.2</a>.
      * <p>
      * Only one of the following methods will be called for each {@code HEADERS} frame sequence.
      * One will be called when the {@code END_HEADERS} flag has been received.
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameStream.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameStream.java
index 13f8634..533800f 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameStream.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameStream.java
@@ -20,7 +20,7 @@ import io.netty.handler.codec.http2.Http2Stream.State;
 import io.netty.util.internal.UnstableApi;
 
 /**
- * A single stream within a HTTP/2 connection. To be used with the {@link Http2FrameCodec}.
+ * A single stream within an HTTP/2 connection. To be used with the {@link Http2FrameCodec}.
  */
 @UnstableApi
 public interface Http2FrameStream {
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameStreamException.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameStreamException.java
index e70a0f1..bd65a4a 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameStreamException.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameStreamException.java
@@ -21,7 +21,7 @@ import io.netty.util.internal.UnstableApi;
 import static io.netty.util.internal.ObjectUtil.checkNotNull;
 
 /**
- * A HTTP/2 exception for a specific {@link Http2FrameStream}.
+ * An HTTP/2 exception for a specific {@link Http2FrameStream}.
  */
 @UnstableApi
 public final class Http2FrameStreamException extends Exception {
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2Headers.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2Headers.java
index f0999bf..fc5e11f 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2Headers.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2Headers.java
@@ -136,7 +136,7 @@ public interface Http2Headers extends Headers<CharSequence, CharSequence, Http2H
     Iterator<CharSequence> valueIterator(CharSequence name);
 
     /**
-     * Sets the {@link PseudoHeaderName#METHOD} header or {@code null} if there is no such header
+     * Sets the {@link PseudoHeaderName#METHOD} header
      */
     Http2Headers method(CharSequence value);
 
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2HeadersEncoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2HeadersEncoder.java
index 9e88efc..9e96a79 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2HeadersEncoder.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2HeadersEncoder.java
@@ -57,17 +57,18 @@ public interface Http2HeadersEncoder {
 
     /**
      * Determine if a header name/value pair is treated as
-     * <a href="http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#section-7.1.3">sensitive</a>.
+     * <a href="https://tools.ietf.org/html/rfc7541#section-7.1.3">sensitive</a>.
      * If the object can be dynamically modified and shared across multiple connections it may need to be thread safe.
      */
     interface SensitivityDetector {
         /**
          * Determine if a header {@code name}/{@code value} pair should be treated as
-         * <a href="http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#section-7.1.3">sensitive</a>.
+         * <a href="https://tools.ietf.org/html/rfc7541#section-7.1.3">sensitive</a>.
+         *
          * @param name The name for the header.
          * @param value The value of the header.
          * @return {@code true} if a header {@code name}/{@code value} pair should be treated as
-         * <a href="http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#section-7.1.3">sensitive</a>.
+         * <a href="https://tools.ietf.org/html/rfc7541#section-7.1.3">sensitive</a>.
          * {@code false} otherwise.
          */
         boolean isSensitive(CharSequence name, CharSequence value);
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MultiplexCodec.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MultiplexCodec.java
index d19ce2b..07c8fca 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MultiplexCodec.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MultiplexCodec.java
@@ -16,47 +16,23 @@
 package io.netty.handler.codec.http2;
 
 import io.netty.buffer.ByteBuf;
-import io.netty.buffer.ByteBufAllocator;
 import io.netty.channel.Channel;
 import io.netty.channel.ChannelConfig;
 import io.netty.channel.ChannelFuture;
-import io.netty.channel.ChannelFutureListener;
 import io.netty.channel.ChannelHandler;
 import io.netty.channel.ChannelHandlerContext;
-import io.netty.channel.ChannelId;
-import io.netty.channel.ChannelMetadata;
-import io.netty.channel.ChannelOutboundBuffer;
-import io.netty.channel.ChannelPipeline;
-import io.netty.channel.ChannelProgressivePromise;
 import io.netty.channel.ChannelPromise;
-import io.netty.channel.DefaultChannelConfig;
-import io.netty.channel.DefaultChannelPipeline;
 import io.netty.channel.EventLoop;
-import io.netty.channel.MessageSizeEstimator;
-import io.netty.channel.RecvByteBufAllocator;
-import io.netty.channel.RecvByteBufAllocator.Handle;
-import io.netty.channel.VoidChannelPromise;
-import io.netty.channel.WriteBufferWaterMark;
-import io.netty.util.DefaultAttributeMap;
-import io.netty.util.ReferenceCountUtil;
 import io.netty.util.ReferenceCounted;
-import io.netty.util.internal.StringUtil;
-import io.netty.util.internal.ThrowableUtil;
+
 import io.netty.util.internal.UnstableApi;
-import io.netty.util.internal.logging.InternalLogger;
-import io.netty.util.internal.logging.InternalLoggerFactory;
 
-import java.net.SocketAddress;
-import java.nio.channels.ClosedChannelException;
 import java.util.ArrayDeque;
 import java.util.Queue;
-import java.util.concurrent.RejectedExecutionException;
 
 import static io.netty.handler.codec.http2.Http2CodecUtil.HTTP_UPGRADE_STREAM_ID;
-import static io.netty.handler.codec.http2.Http2CodecUtil.isStreamIdValid;
 import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR;
 import static io.netty.handler.codec.http2.Http2Exception.connectionError;
-import static java.lang.Math.min;
 
 /**
  * An HTTP/2 handler that creates child channels for each stream.
@@ -85,7 +61,7 @@ import static java.lang.Math.min;
  * reference counted objects (e.g. {@link ByteBuf}s). The multiplex codec will call {@link ReferenceCounted#retain()}
  * before propagating a reference counted object through the pipeline, and thus an application handler needs to release
  * such an object after having consumed it. For more information on reference counting take a look at
- * http://netty.io/wiki/reference-counted-objects.html
+ * https://netty.io/wiki/reference-counted-objects.html
  *
  * <h3>Channel Events</h3>
  *
@@ -102,71 +78,32 @@ import static java.lang.Math.min;
  * does not know about the connection-level flow control window. {@link ChannelHandler}s are free to ignore the
  * channel's writability, in which case the excessive writes will be buffered by the parent channel. It's important to
  * note that only {@link Http2DataFrame}s are subject to HTTP/2 flow control.
+ *
+ * @deprecated use {@link Http2FrameCodecBuilder} together with {@link Http2MultiplexHandler}.
  */
+@Deprecated
 @UnstableApi
 public class Http2MultiplexCodec extends Http2FrameCodec {
 
-    private static final InternalLogger logger = InternalLoggerFactory.getInstance(DefaultHttp2StreamChannel.class);
-
-    private static final ChannelFutureListener CHILD_CHANNEL_REGISTRATION_LISTENER = new ChannelFutureListener() {
-        @Override
-        public void operationComplete(ChannelFuture future) {
-            registerDone(future);
-        }
-    };
-
-    private static final ChannelMetadata METADATA = new ChannelMetadata(false, 16);
-    private static final ClosedChannelException CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace(
-            new ClosedChannelException(), DefaultHttp2StreamChannel.Http2ChannelUnsafe.class, "write(...)");
-    /**
-     * Number of bytes to consider non-payload messages. 9 is arbitrary, but also the minimum size of an HTTP/2 frame.
-     * Primarily is non-zero.
-     */
-    private static final int MIN_HTTP2_FRAME_SIZE = 9;
-
-    /**
-     * Returns the flow-control size for DATA frames, and 0 for all other frames.
-     */
-    private static final class FlowControlledFrameSizeEstimator implements MessageSizeEstimator {
-
-        static final FlowControlledFrameSizeEstimator INSTANCE = new FlowControlledFrameSizeEstimator();
-
-        static final MessageSizeEstimator.Handle HANDLE_INSTANCE = new MessageSizeEstimator.Handle() {
-            @Override
-            public int size(Object msg) {
-                return msg instanceof Http2DataFrame ?
-                        // Guard against overflow.
-                        (int) min(Integer.MAX_VALUE, ((Http2DataFrame) msg).initialFlowControlledBytes() +
-                                (long) MIN_HTTP2_FRAME_SIZE) : MIN_HTTP2_FRAME_SIZE;
-            }
-        };
-
-        @Override
-        public Handle newHandle() {
-            return HANDLE_INSTANCE;
-        }
-    }
-
     private final ChannelHandler inboundStreamHandler;
     private final ChannelHandler upgradeStreamHandler;
+    private final Queue<AbstractHttp2StreamChannel> readCompletePendingQueue =
+            new MaxCapacityQueue<AbstractHttp2StreamChannel>(new ArrayDeque<AbstractHttp2StreamChannel>(8),
+                    // Choose 100 which is what is used most of the times as default.
+                    Http2CodecUtil.SMALLEST_MAX_CONCURRENT_STREAMS);
 
-    private int initialOutboundStreamWindow = Http2CodecUtil.DEFAULT_WINDOW_SIZE;
     private boolean parentReadInProgress;
     private int idCount;
 
-    // Linked-List for DefaultHttp2StreamChannel instances that need to be processed by channelReadComplete(...)
-    private DefaultHttp2StreamChannel head;
-    private DefaultHttp2StreamChannel tail;
-
-    // Need to be volatile as accessed from within the DefaultHttp2StreamChannel in a multi-threaded fashion.
+    // Need to be volatile as accessed from within the Http2MultiplexCodecStreamChannel in a multi-threaded fashion.
     volatile ChannelHandlerContext ctx;
 
     Http2MultiplexCodec(Http2ConnectionEncoder encoder,
                         Http2ConnectionDecoder decoder,
                         Http2Settings initialSettings,
                         ChannelHandler inboundStreamHandler,
-                        ChannelHandler upgradeStreamHandler) {
-        super(encoder, decoder, initialSettings);
+                        ChannelHandler upgradeStreamHandler, boolean decoupleCloseAndGoAway) {
+        super(encoder, decoder, initialSettings, decoupleCloseAndGoAway);
         this.inboundStreamHandler = inboundStreamHandler;
         this.upgradeStreamHandler = upgradeStreamHandler;
     }
@@ -179,24 +116,6 @@ public class Http2MultiplexCodec extends Http2FrameCodec {
         }
         // Creates the Http2Stream in the Connection.
         super.onHttpClientUpgrade();
-        // Now make a new FrameStream, set it's underlying Http2Stream, and initialize it.
-        Http2MultiplexCodecStream codecStream = newStream();
-        codecStream.setStreamAndProperty(streamKey, connection().stream(HTTP_UPGRADE_STREAM_ID));
-        onHttp2UpgradeStreamInitialized(ctx, codecStream);
-    }
-
-    private static void registerDone(ChannelFuture future) {
-        // Handle any errors that occurred on the local thread while registering. Even though
-        // failures can happen after this point, they will be handled by the channel by closing the
-        // childChannel.
-        if (!future.isSuccess()) {
-            Channel childChannel = future.channel();
-            if (childChannel.isRegistered()) {
-                childChannel.close();
-            } else {
-                childChannel.unsafe().closeForcibly();
-            }
-        }
     }
 
     @Override
@@ -211,80 +130,61 @@ public class Http2MultiplexCodec extends Http2FrameCodec {
     public final void handlerRemoved0(ChannelHandlerContext ctx) throws Exception {
         super.handlerRemoved0(ctx);
 
-        // Unlink the linked list to guard against GC nepotism.
-        DefaultHttp2StreamChannel ch = head;
-        while (ch != null) {
-            DefaultHttp2StreamChannel curr = ch;
-            ch = curr.next;
-            curr.next = curr.previous = null;
-        }
-        head = tail = null;
-    }
-
-    @Override
-    Http2MultiplexCodecStream newStream() {
-        return new Http2MultiplexCodecStream();
+        readCompletePendingQueue.clear();
     }
 
     @Override
     final void onHttp2Frame(ChannelHandlerContext ctx, Http2Frame frame) {
         if (frame instanceof Http2StreamFrame) {
             Http2StreamFrame streamFrame = (Http2StreamFrame) frame;
-            ((Http2MultiplexCodecStream) streamFrame.stream()).channel.fireChildRead(streamFrame);
-        } else if (frame instanceof Http2GoAwayFrame) {
-            onHttp2GoAwayFrame(ctx, (Http2GoAwayFrame) frame);
-            // Allow other handlers to act on GOAWAY frame
-            ctx.fireChannelRead(frame);
-        } else if (frame instanceof Http2SettingsFrame) {
-            Http2Settings settings = ((Http2SettingsFrame) frame).settings();
-            if (settings.initialWindowSize() != null) {
-                initialOutboundStreamWindow = settings.initialWindowSize();
-            }
-            // Allow other handlers to act on SETTINGS frame
-            ctx.fireChannelRead(frame);
-        } else {
-            // Send any other frames down the pipeline
-            ctx.fireChannelRead(frame);
+            AbstractHttp2StreamChannel channel  = (AbstractHttp2StreamChannel)
+                    ((DefaultHttp2FrameStream) streamFrame.stream()).attachment;
+            channel.fireChildRead(streamFrame);
+            return;
         }
-    }
-
-    private void onHttp2UpgradeStreamInitialized(ChannelHandlerContext ctx, Http2MultiplexCodecStream stream) {
-        assert stream.state() == Http2Stream.State.HALF_CLOSED_LOCAL;
-        DefaultHttp2StreamChannel ch = new DefaultHttp2StreamChannel(stream, true);
-        ch.outboundClosed = true;
-
-        // Add our upgrade handler to the channel and then register the channel.
-        // The register call fires the channelActive, etc.
-        ch.pipeline().addLast(upgradeStreamHandler);
-        ChannelFuture future = ctx.channel().eventLoop().register(ch);
-        if (future.isDone()) {
-            registerDone(future);
-        } else {
-            future.addListener(CHILD_CHANNEL_REGISTRATION_LISTENER);
+        if (frame instanceof Http2GoAwayFrame) {
+            onHttp2GoAwayFrame(ctx, (Http2GoAwayFrame) frame);
         }
+        // Send frames down the pipeline
+        ctx.fireChannelRead(frame);
     }
 
     @Override
-    final void onHttp2StreamStateChanged(ChannelHandlerContext ctx, Http2FrameStream stream) {
-        Http2MultiplexCodecStream s = (Http2MultiplexCodecStream) stream;
-
+    final void onHttp2StreamStateChanged(ChannelHandlerContext ctx, DefaultHttp2FrameStream stream) {
         switch (stream.state()) {
+            case HALF_CLOSED_LOCAL:
+                if (stream.id() != HTTP_UPGRADE_STREAM_ID) {
+                    // Ignore everything which was not caused by an upgrade
+                    break;
+                }
+                // fall-through
             case HALF_CLOSED_REMOTE:
+                // fall-through
             case OPEN:
-                if (s.channel != null) {
+                if (stream.attachment != null) {
                     // ignore if child channel was already created.
                     break;
                 }
-                // fall-trough
-                ChannelFuture future = ctx.channel().eventLoop().register(new DefaultHttp2StreamChannel(s, false));
+                final Http2MultiplexCodecStreamChannel streamChannel;
+                // We need to handle upgrades special when on the client side.
+                if (stream.id() == HTTP_UPGRADE_STREAM_ID && !connection().isServer()) {
+                    // Add our upgrade handler to the channel and then register the channel.
+                    // The register call fires the channelActive, etc.
+                    assert upgradeStreamHandler != null;
+                    streamChannel = new Http2MultiplexCodecStreamChannel(stream, upgradeStreamHandler);
+                    streamChannel.closeOutbound();
+                } else {
+                    streamChannel = new Http2MultiplexCodecStreamChannel(stream, inboundStreamHandler);
+                }
+                ChannelFuture future = ctx.channel().eventLoop().register(streamChannel);
                 if (future.isDone()) {
-                    registerDone(future);
+                    Http2MultiplexHandler.registerDone(future);
                 } else {
-                    future.addListener(CHILD_CHANNEL_REGISTRATION_LISTENER);
+                    future.addListener(Http2MultiplexHandler.CHILD_CHANNEL_REGISTRATION_LISTENER);
                 }
                 break;
             case CLOSED:
-                DefaultHttp2StreamChannel channel = s.channel;
+                AbstractHttp2StreamChannel channel = (AbstractHttp2StreamChannel) stream.attachment;
                 if (channel != null) {
                     channel.streamClosed();
                 }
@@ -295,79 +195,33 @@ public class Http2MultiplexCodec extends Http2FrameCodec {
         }
     }
 
-    @Override
-    final void onHttp2StreamWritabilityChanged(ChannelHandlerContext ctx, Http2FrameStream stream, boolean writable) {
-        (((Http2MultiplexCodecStream) stream).channel).writabilityChanged(writable);
-    }
-
     // TODO: This is most likely not the best way to expose this, need to think more about it.
     final Http2StreamChannel newOutboundStream() {
-        return new DefaultHttp2StreamChannel(newStream(), true);
+        return new Http2MultiplexCodecStreamChannel(newStream(), null);
     }
 
     @Override
     final void onHttp2FrameStreamException(ChannelHandlerContext ctx, Http2FrameStreamException cause) {
         Http2FrameStream stream = cause.stream();
-        DefaultHttp2StreamChannel childChannel = ((Http2MultiplexCodecStream) stream).channel;
+        AbstractHttp2StreamChannel channel = (AbstractHttp2StreamChannel) ((DefaultHttp2FrameStream) stream).attachment;
 
         try {
-            childChannel.pipeline().fireExceptionCaught(cause.getCause());
+            channel.pipeline().fireExceptionCaught(cause.getCause());
         } finally {
-            childChannel.unsafe().closeForcibly();
+            channel.unsafe().closeForcibly();
         }
     }
 
-    private boolean isChildChannelInReadPendingQueue(DefaultHttp2StreamChannel childChannel) {
-        return childChannel.previous != null || childChannel.next != null || head == childChannel;
-    }
-
-    final void tryAddChildChannelToReadPendingQueue(DefaultHttp2StreamChannel childChannel) {
-        if (!isChildChannelInReadPendingQueue(childChannel)) {
-            addChildChannelToReadPendingQueue(childChannel);
-        }
-    }
-
-    final void addChildChannelToReadPendingQueue(DefaultHttp2StreamChannel childChannel) {
-        if (tail == null) {
-            assert head == null;
-            tail = head = childChannel;
-        } else {
-            childChannel.previous = tail;
-            tail.next = childChannel;
-            tail = childChannel;
-        }
-    }
-
-    private void tryRemoveChildChannelFromReadPendingQueue(DefaultHttp2StreamChannel childChannel) {
-        if (isChildChannelInReadPendingQueue(childChannel)) {
-            removeChildChannelFromReadPendingQueue(childChannel);
-        }
-    }
-
-    private void removeChildChannelFromReadPendingQueue(DefaultHttp2StreamChannel childChannel) {
-        DefaultHttp2StreamChannel previous = childChannel.previous;
-        if (childChannel.next != null) {
-            childChannel.next.previous = previous;
-        } else {
-            tail = tail.previous; // If there is no next, this childChannel is the tail, so move the tail back.
-        }
-        if (previous != null) {
-            previous.next = childChannel.next;
-        } else {
-            head = head.next; // If there is no previous, this childChannel is the head, so move the tail forward.
-        }
-        childChannel.next = childChannel.previous = null;
-    }
-
     private void onHttp2GoAwayFrame(ChannelHandlerContext ctx, final Http2GoAwayFrame goAwayFrame) {
         try {
             forEachActiveStream(new Http2FrameStreamVisitor() {
                 @Override
                 public boolean visit(Http2FrameStream stream) {
                     final int streamId = stream.id();
-                    final DefaultHttp2StreamChannel childChannel = ((Http2MultiplexCodecStream) stream).channel;
+                    AbstractHttp2StreamChannel channel = (AbstractHttp2StreamChannel)
+                            ((DefaultHttp2FrameStream) stream).attachment;
                     if (streamId > goAwayFrame.lastStreamId() && connection().local().isValidStreamId(streamId)) {
-                        childChannel.pipeline().fireUserEventTriggered(goAwayFrame.retainedDuplicate());
+                        channel.pipeline().fireUserEventTriggered(goAwayFrame.retainedDuplicate());
                     }
                     return true;
                 }
@@ -383,917 +237,86 @@ public class Http2MultiplexCodec extends Http2FrameCodec {
      */
     @Override
     public final void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
+        processPendingReadCompleteQueue();
+        channelReadComplete0(ctx);
+    }
+
+    private void processPendingReadCompleteQueue() {
+        parentReadInProgress = true;
         try {
-            onChannelReadComplete(ctx);
+            // If we have many child channel we can optimize for the case when multiple call flush() in
+            // channelReadComplete(...) callbacks and only do it once as otherwise we will end-up with multiple
+            // write calls on the socket which is expensive.
+            for (;;) {
+                AbstractHttp2StreamChannel childChannel = readCompletePendingQueue.poll();
+                if (childChannel == null) {
+                    break;
+                }
+                childChannel.fireChildReadComplete();
+            }
         } finally {
             parentReadInProgress = false;
-            tail = head = null;
+            readCompletePendingQueue.clear();
             // We always flush as this is what Http2ConnectionHandler does for now.
             flush0(ctx);
         }
-        channelReadComplete0(ctx);
     }
-
     @Override
     public final void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
         parentReadInProgress = true;
         super.channelRead(ctx, msg);
     }
 
-    final void onChannelReadComplete(ChannelHandlerContext ctx)  {
-        // If we have many child channel we can optimize for the case when multiple call flush() in
-        // channelReadComplete(...) callbacks and only do it once as otherwise we will end-up with multiple
-        // write calls on the socket which is expensive.
-        DefaultHttp2StreamChannel current = head;
-        while (current != null) {
-            DefaultHttp2StreamChannel childChannel = current;
-            // Clear early in case fireChildReadComplete() causes it to need to be re-processed
-            current = current.next;
-            childChannel.next = childChannel.previous = null;
-            childChannel.fireChildReadComplete();
+    @Override
+    public final void channelWritabilityChanged(final ChannelHandlerContext ctx) throws Exception {
+        if (ctx.channel().isWritable()) {
+            // While the writability state may change during iterating of the streams we just set all of the streams
+            // to writable to not affect fairness. These will be "limited" by their own watermarks in any case.
+            forEachActiveStream(AbstractHttp2StreamChannel.WRITABLE_VISITOR);
         }
+
+        super.channelWritabilityChanged(ctx);
     }
 
     final void flush0(ChannelHandlerContext ctx) {
         flush(ctx);
     }
 
-    static final class Http2MultiplexCodecStream extends DefaultHttp2FrameStream {
-        DefaultHttp2StreamChannel channel;
-    }
-
-    private boolean initialWritability(DefaultHttp2FrameStream stream) {
-        // If the stream id is not valid yet we will just mark the channel as writable as we will be notified
-        // about non-writability state as soon as the first Http2HeaderFrame is written (if needed).
-        // This should be good enough and simplify things a lot.
-        return !isStreamIdValid(stream.id()) || isWritable(stream);
-    }
-
-    /**
-     * The current status of the read-processing for a {@link Http2StreamChannel}.
-     */
-    private enum ReadStatus {
-        /**
-         * No read in progress and no read was requested (yet)
-         */
-        IDLE,
-
-        /**
-         * Reading in progress
-         */
-        IN_PROGRESS,
-
-        /**
-         * A read operation was requested.
-         */
-        REQUESTED
-    }
-
-    // TODO: Handle writability changes due writing from outside the eventloop.
-    private final class DefaultHttp2StreamChannel extends DefaultAttributeMap implements Http2StreamChannel {
-        private final Http2StreamChannelConfig config = new Http2StreamChannelConfig(this);
-        private final Http2ChannelUnsafe unsafe = new Http2ChannelUnsafe();
-        private final ChannelId channelId;
-        private final ChannelPipeline pipeline;
-        private final DefaultHttp2FrameStream stream;
-        private final ChannelPromise closePromise;
-        private final boolean outbound;
-
-        private volatile boolean registered;
-        // We start with the writability of the channel when creating the StreamChannel.
-        private volatile boolean writable;
-
-        private boolean outboundClosed;
-
-        /**
-         * This variable represents if a read is in progress for the current channel or was requested.
-         * Note that depending upon the {@link RecvByteBufAllocator} behavior a read may extend beyond the
-         * {@link Http2ChannelUnsafe#beginRead()} method scope. The {@link Http2ChannelUnsafe#beginRead()} loop may
-         * drain all pending data, and then if the parent channel is reading this channel may still accept frames.
-         */
-        private ReadStatus readStatus = ReadStatus.IDLE;
-
-        private Queue<Object> inboundBuffer;
-
-        /** {@code true} after the first HEADERS frame has been written **/
-        private boolean firstFrameWritten;
-
-        // Currently the child channel and parent channel are always on the same EventLoop thread. This allows us to
-        // extend the read loop of a child channel if the child channel drains its queued data during read, and the
-        // parent channel is still in its read loop. The next/previous links build a doubly linked list that the parent
-        // channel will iterate in its channelReadComplete to end the read cycle for each child channel in the list.
-        DefaultHttp2StreamChannel next;
-        DefaultHttp2StreamChannel previous;
-
-        DefaultHttp2StreamChannel(DefaultHttp2FrameStream stream, boolean outbound) {
-            this.stream = stream;
-            this.outbound = outbound;
-            writable = initialWritability(stream);
-            ((Http2MultiplexCodecStream) stream).channel = this;
-            pipeline = new DefaultChannelPipeline(this) {
-                @Override
-                protected void incrementPendingOutboundBytes(long size) {
-                    // Do thing for now
-                }
-
-                @Override
-                protected void decrementPendingOutboundBytes(long size) {
-                    // Do thing for now
-                }
-            };
-            closePromise = pipeline.newPromise();
-            channelId = new Http2StreamChannelId(parent().id(), ++idCount);
-        }
-
-        @Override
-        public Http2FrameStream stream() {
-            return stream;
-        }
-
-        void streamClosed() {
-            unsafe.readEOS();
-            // Attempt to drain any queued data from the queue and deliver it to the application before closing this
-            // channel.
-            unsafe.doBeginRead();
-        }
-
-        @Override
-        public ChannelMetadata metadata() {
-            return METADATA;
-        }
-
-        @Override
-        public ChannelConfig config() {
-            return config;
-        }
-
-        @Override
-        public boolean isOpen() {
-            return !closePromise.isDone();
-        }
-
-        @Override
-        public boolean isActive() {
-            return isOpen();
-        }
-
-        @Override
-        public boolean isWritable() {
-            return writable;
-        }
-
-        @Override
-        public ChannelId id() {
-            return channelId;
-        }
-
-        @Override
-        public EventLoop eventLoop() {
-            return parent().eventLoop();
-        }
-
-        @Override
-        public Channel parent() {
-            return ctx.channel();
-        }
-
-        @Override
-        public boolean isRegistered() {
-            return registered;
-        }
-
-        @Override
-        public SocketAddress localAddress() {
-            return parent().localAddress();
-        }
-
-        @Override
-        public SocketAddress remoteAddress() {
-            return parent().remoteAddress();
-        }
-
-        @Override
-        public ChannelFuture closeFuture() {
-            return closePromise;
-        }
-
-        @Override
-        public long bytesBeforeUnwritable() {
-            // TODO: Do a proper impl
-            return config().getWriteBufferHighWaterMark();
-        }
-
-        @Override
-        public long bytesBeforeWritable() {
-            // TODO: Do a proper impl
-            return 0;
-        }
-
-        @Override
-        public Unsafe unsafe() {
-            return unsafe;
-        }
-
-        @Override
-        public ChannelPipeline pipeline() {
-            return pipeline;
-        }
-
-        @Override
-        public ByteBufAllocator alloc() {
-            return config().getAllocator();
-        }
-
-        @Override
-        public Channel read() {
-            pipeline().read();
-            return this;
-        }
-
-        @Override
-        public Channel flush() {
-            pipeline().flush();
-            return this;
-        }
-
-        @Override
-        public ChannelFuture bind(SocketAddress localAddress) {
-            return pipeline().bind(localAddress);
-        }
-
-        @Override
-        public ChannelFuture connect(SocketAddress remoteAddress) {
-            return pipeline().connect(remoteAddress);
-        }
-
-        @Override
-        public ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress) {
-            return pipeline().connect(remoteAddress, localAddress);
-        }
-
-        @Override
-        public ChannelFuture disconnect() {
-            return pipeline().disconnect();
-        }
-
-        @Override
-        public ChannelFuture close() {
-            return pipeline().close();
-        }
-
-        @Override
-        public ChannelFuture deregister() {
-            return pipeline().deregister();
-        }
-
-        @Override
-        public ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) {
-            return pipeline().bind(localAddress, promise);
-        }
-
-        @Override
-        public ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) {
-            return pipeline().connect(remoteAddress, promise);
-        }
-
-        @Override
-        public ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) {
-            return pipeline().connect(remoteAddress, localAddress, promise);
-        }
-
-        @Override
-        public ChannelFuture disconnect(ChannelPromise promise) {
-            return pipeline().disconnect(promise);
-        }
-
-        @Override
-        public ChannelFuture close(ChannelPromise promise) {
-            return pipeline().close(promise);
-        }
-
-        @Override
-        public ChannelFuture deregister(ChannelPromise promise) {
-            return pipeline().deregister(promise);
-        }
-
-        @Override
-        public ChannelFuture write(Object msg) {
-            return pipeline().write(msg);
-        }
-
-        @Override
-        public ChannelFuture write(Object msg, ChannelPromise promise) {
-            return pipeline().write(msg, promise);
-        }
-
-        @Override
-        public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) {
-            return pipeline().writeAndFlush(msg, promise);
-        }
-
-        @Override
-        public ChannelFuture writeAndFlush(Object msg) {
-            return pipeline().writeAndFlush(msg);
-        }
-
-        @Override
-        public ChannelPromise newPromise() {
-            return pipeline().newPromise();
-        }
-
-        @Override
-        public ChannelProgressivePromise newProgressivePromise() {
-            return pipeline().newProgressivePromise();
-        }
-
-        @Override
-        public ChannelFuture newSucceededFuture() {
-            return pipeline().newSucceededFuture();
-        }
+    private final class Http2MultiplexCodecStreamChannel extends AbstractHttp2StreamChannel {
 
-        @Override
-        public ChannelFuture newFailedFuture(Throwable cause) {
-            return pipeline().newFailedFuture(cause);
+        Http2MultiplexCodecStreamChannel(DefaultHttp2FrameStream stream, ChannelHandler inboundHandler) {
+            super(stream, ++idCount, inboundHandler);
         }
 
         @Override
-        public ChannelPromise voidPromise() {
-            return pipeline().voidPromise();
+        protected boolean isParentReadInProgress() {
+            return parentReadInProgress;
         }
 
         @Override
-        public int hashCode() {
-            return id().hashCode();
+        protected void addChannelToReadCompletePendingQueue() {
+            // If there is no space left in the queue, just keep on processing everything that is already
+            // stored there and try again.
+            while (!readCompletePendingQueue.offer(this)) {
+                processPendingReadCompleteQueue();
+            }
         }
 
         @Override
-        public boolean equals(Object o) {
-            return this == o;
+        protected ChannelHandlerContext parentContext() {
+            return ctx;
         }
 
         @Override
-        public int compareTo(Channel o) {
-            if (this == o) {
-                return 0;
-            }
-
-            return id().compareTo(o.id());
+        protected ChannelFuture write0(ChannelHandlerContext ctx, Object msg) {
+            ChannelPromise promise = ctx.newPromise();
+            Http2MultiplexCodec.this.write(ctx, msg, promise);
+            return promise;
         }
 
         @Override
-        public String toString() {
-            return parent().toString() + "(H2 - " + stream + ')';
-        }
-
-        void writabilityChanged(boolean writable) {
-            assert eventLoop().inEventLoop();
-            if (writable != this.writable && isActive()) {
-                // Only notify if we received a state change.
-                this.writable = writable;
-                pipeline().fireChannelWritabilityChanged();
-            }
-        }
-
-        /**
-         * Receive a read message. This does not notify handlers unless a read is in progress on the
-         * channel.
-         */
-        void fireChildRead(Http2Frame frame) {
-            assert eventLoop().inEventLoop();
-            if (!isActive()) {
-                ReferenceCountUtil.release(frame);
-            } else if (readStatus != ReadStatus.IDLE) {
-                // If a read is in progress or has been requested, there cannot be anything in the queue,
-                // otherwise we would have drained it from the queue and processed it during the read cycle.
-                assert inboundBuffer == null || inboundBuffer.isEmpty();
-                final Handle allocHandle = unsafe.recvBufAllocHandle();
-                unsafe.doRead0(frame, allocHandle);
-                // We currently don't need to check for readEOS because the parent channel and child channel are limited
-                // to the same EventLoop thread. There are a limited number of frame types that may come after EOS is
-                // read (unknown, reset) and the trade off is less conditionals for the hot path (headers/data) at the
-                // cost of additional readComplete notifications on the rare path.
-                if (allocHandle.continueReading()) {
-                    tryAddChildChannelToReadPendingQueue(this);
-                } else {
-                    tryRemoveChildChannelFromReadPendingQueue(this);
-                    unsafe.notifyReadComplete(allocHandle);
-                }
-            } else {
-                if (inboundBuffer == null) {
-                    inboundBuffer = new ArrayDeque<Object>(4);
-                }
-                inboundBuffer.add(frame);
-            }
-        }
-
-        void fireChildReadComplete() {
-            assert eventLoop().inEventLoop();
-            assert readStatus != ReadStatus.IDLE;
-            unsafe.notifyReadComplete(unsafe.recvBufAllocHandle());
-        }
-
-        private final class Http2ChannelUnsafe implements Unsafe {
-            private final VoidChannelPromise unsafeVoidPromise =
-                    new VoidChannelPromise(DefaultHttp2StreamChannel.this, false);
-            @SuppressWarnings("deprecation")
-            private Handle recvHandle;
-            private boolean writeDoneAndNoFlush;
-            private boolean closeInitiated;
-            private boolean readEOS;
-
-            @Override
-            public void connect(final SocketAddress remoteAddress,
-                                SocketAddress localAddress, final ChannelPromise promise) {
-                if (!promise.setUncancellable()) {
-                    return;
-                }
-                promise.setFailure(new UnsupportedOperationException());
-            }
-
-            @Override
-            public Handle recvBufAllocHandle() {
-                if (recvHandle == null) {
-                    recvHandle = config().getRecvByteBufAllocator().newHandle();
-                    recvHandle.reset(config());
-                }
-                return recvHandle;
-            }
-
-            @Override
-            public SocketAddress localAddress() {
-                return parent().unsafe().localAddress();
-            }
-
-            @Override
-            public SocketAddress remoteAddress() {
-                return parent().unsafe().remoteAddress();
-            }
-
-            @Override
-            public void register(EventLoop eventLoop, ChannelPromise promise) {
-                if (!promise.setUncancellable()) {
-                    return;
-                }
-                if (registered) {
-                    throw new UnsupportedOperationException("Re-register is not supported");
-                }
-
-                registered = true;
-
-                if (!outbound) {
-                    // Add the handler to the pipeline now that we are registered.
-                    pipeline().addLast(inboundStreamHandler);
-                }
-
-                promise.setSuccess();
-
-                pipeline().fireChannelRegistered();
-                if (isActive()) {
-                    pipeline().fireChannelActive();
-                }
-            }
-
-            @Override
-            public void bind(SocketAddress localAddress, ChannelPromise promise) {
-                if (!promise.setUncancellable()) {
-                    return;
-                }
-                promise.setFailure(new UnsupportedOperationException());
-            }
-
-            @Override
-            public void disconnect(ChannelPromise promise) {
-                close(promise);
-            }
-
-            @Override
-            public void close(final ChannelPromise promise) {
-                if (!promise.setUncancellable()) {
-                    return;
-                }
-                if (closeInitiated) {
-                    if (closePromise.isDone()) {
-                        // Closed already.
-                        promise.setSuccess();
-                    } else if (!(promise instanceof VoidChannelPromise)) { // Only needed if no VoidChannelPromise.
-                        // This means close() was called before so we just register a listener and return
-                        closePromise.addListener(new ChannelFutureListener() {
-                            @Override
-                            public void operationComplete(ChannelFuture future) {
-                                promise.setSuccess();
-                            }
-                        });
-                    }
-                    return;
-                }
-                closeInitiated = true;
-
-                tryRemoveChildChannelFromReadPendingQueue(DefaultHttp2StreamChannel.this);
-
-                final boolean wasActive = isActive();
-
-                // Only ever send a reset frame if the connection is still alive and if the stream may have existed
-                // as otherwise we may send a RST on a stream in an invalid state and cause a connection error.
-                if (parent().isActive() && !readEOS && connection().streamMayHaveExisted(stream().id())) {
-                    Http2StreamFrame resetFrame = new DefaultHttp2ResetFrame(Http2Error.CANCEL).stream(stream());
-                    write(resetFrame, unsafe().voidPromise());
-                    flush();
-                }
-
-                if (inboundBuffer != null) {
-                    for (;;) {
-                        Object msg = inboundBuffer.poll();
-                        if (msg == null) {
-                            break;
-                        }
-                        ReferenceCountUtil.release(msg);
-                    }
-                }
-
-                // The promise should be notified before we call fireChannelInactive().
-                outboundClosed = true;
-                closePromise.setSuccess();
-                promise.setSuccess();
-
-                fireChannelInactiveAndDeregister(voidPromise(), wasActive);
-            }
-
-            @Override
-            public void closeForcibly() {
-                close(unsafe().voidPromise());
-            }
-
-            @Override
-            public void deregister(ChannelPromise promise) {
-                fireChannelInactiveAndDeregister(promise, false);
-            }
-
-            private void fireChannelInactiveAndDeregister(final ChannelPromise promise,
-                                                          final boolean fireChannelInactive) {
-                if (!promise.setUncancellable()) {
-                    return;
-                }
-
-                if (!registered) {
-                    promise.setSuccess();
-                    return;
-                }
-
-                // As a user may call deregister() from within any method while doing processing in the ChannelPipeline,
-                // we need to ensure we do the actual deregister operation later. This is necessary to preserve the
-                // behavior of the AbstractChannel, which always invokes channelUnregistered and channelInactive
-                // events 'later' to ensure the current events in the handler are completed before these events.
-                //
-                // See:
-                // https://github.com/netty/netty/issues/4435
-                invokeLater(new Runnable() {
-                    @Override
-                    public void run() {
-                        if (fireChannelInactive) {
-                            pipeline.fireChannelInactive();
-                        }
-                        // The user can fire `deregister` events multiple times but we only want to fire the pipeline
-                        // event if the channel was actually registered.
-                        if (registered) {
-                            registered = false;
-                            pipeline.fireChannelUnregistered();
-                        }
-                        safeSetSuccess(promise);
-                    }
-                });
-            }
-
-            private void safeSetSuccess(ChannelPromise promise) {
-                if (!(promise instanceof VoidChannelPromise) && !promise.trySuccess()) {
-                    logger.warn("Failed to mark a promise as success because it is done already: {}", promise);
-                }
-            }
-
-            private void invokeLater(Runnable task) {
-                try {
-                    // This method is used by outbound operation implementations to trigger an inbound event later.
-                    // They do not trigger an inbound event immediately because an outbound operation might have been
-                    // triggered by another inbound event handler method.  If fired immediately, the call stack
-                    // will look like this for example:
-                    //
-                    //   handlerA.inboundBufferUpdated() - (1) an inbound handler method closes a connection.
-                    //   -> handlerA.ctx.close()
-                    //     -> channel.unsafe.close()
-                    //       -> handlerA.channelInactive() - (2) another inbound handler method called while in (1) yet
-                    //
-                    // which means the execution of two inbound handler methods of the same handler overlap undesirably.
-                    eventLoop().execute(task);
-                } catch (RejectedExecutionException e) {
-                    logger.warn("Can't invoke task later as EventLoop rejected it", e);
-                }
-            }
-
-            @Override
-            public void beginRead() {
-                if (!isActive()) {
-                    return;
-                }
-                switch (readStatus) {
-                    case IDLE:
-                        readStatus = ReadStatus.IN_PROGRESS;
-                        doBeginRead();
-                        break;
-                    case IN_PROGRESS:
-                        readStatus = ReadStatus.REQUESTED;
-                        break;
-                    default:
-                        break;
-                }
-            }
-
-            void doBeginRead() {
-                Object message;
-                if (inboundBuffer == null || (message = inboundBuffer.poll()) == null) {
-                    if (readEOS) {
-                        unsafe.closeForcibly();
-                    }
-                } else {
-                    final Handle allocHandle = recvBufAllocHandle();
-                    allocHandle.reset(config());
-                    boolean continueReading = false;
-                    do {
-                        doRead0((Http2Frame) message, allocHandle);
-                    } while ((readEOS || (continueReading = allocHandle.continueReading())) &&
-                             (message = inboundBuffer.poll()) != null);
-
-                    if (continueReading && parentReadInProgress && !readEOS) {
-                        // Currently the parent and child channel are on the same EventLoop thread. If the parent is
-                        // currently reading it is possile that more frames will be delivered to this child channel. In
-                        // the case that this child channel still wants to read we delay the channelReadComplete on this
-                        // child channel until the parent is done reading.
-                        assert !isChildChannelInReadPendingQueue(DefaultHttp2StreamChannel.this);
-                        addChildChannelToReadPendingQueue(DefaultHttp2StreamChannel.this);
-                    } else {
-                        notifyReadComplete(allocHandle);
-                    }
-                }
-            }
-
-            void readEOS() {
-                readEOS = true;
-            }
-
-            void notifyReadComplete(Handle allocHandle) {
-                assert next == null && previous == null;
-                if (readStatus == ReadStatus.REQUESTED) {
-                    readStatus = ReadStatus.IN_PROGRESS;
-                } else {
-                    readStatus = ReadStatus.IDLE;
-                }
-                allocHandle.readComplete();
-                pipeline().fireChannelReadComplete();
-                // Reading data may result in frames being written (e.g. WINDOW_UPDATE, RST, etc..). If the parent
-                // channel is not currently reading we need to force a flush at the child channel, because we cannot
-                // rely upon flush occurring in channelReadComplete on the parent channel.
-                flush();
-                if (readEOS) {
-                    unsafe.closeForcibly();
-                }
-            }
-
-            @SuppressWarnings("deprecation")
-            void doRead0(Http2Frame frame, Handle allocHandle) {
-                pipeline().fireChannelRead(frame);
-                allocHandle.incMessagesRead(1);
-
-                if (frame instanceof Http2DataFrame) {
-                    final int numBytesToBeConsumed = ((Http2DataFrame) frame).initialFlowControlledBytes();
-                    allocHandle.attemptedBytesRead(numBytesToBeConsumed);
-                    allocHandle.lastBytesRead(numBytesToBeConsumed);
-                    if (numBytesToBeConsumed != 0) {
-                        try {
-                            writeDoneAndNoFlush |= consumeBytes(stream.id(), numBytesToBeConsumed);
-                        } catch (Http2Exception e) {
-                            pipeline().fireExceptionCaught(e);
-                        }
-                    }
-                } else {
-                    allocHandle.attemptedBytesRead(MIN_HTTP2_FRAME_SIZE);
-                    allocHandle.lastBytesRead(MIN_HTTP2_FRAME_SIZE);
-                }
-            }
-
-            @Override
-            public void write(Object msg, final ChannelPromise promise) {
-                // After this point its not possible to cancel a write anymore.
-                if (!promise.setUncancellable()) {
-                    ReferenceCountUtil.release(msg);
-                    return;
-                }
-
-                if (!isActive() ||
-                        // Once the outbound side was closed we should not allow header / data frames
-                        outboundClosed && (msg instanceof Http2HeadersFrame || msg instanceof Http2DataFrame)) {
-                    ReferenceCountUtil.release(msg);
-                    promise.setFailure(CLOSED_CHANNEL_EXCEPTION);
-                    return;
-                }
-
-                try {
-                    if (msg instanceof Http2StreamFrame) {
-                        Http2StreamFrame frame = validateStreamFrame((Http2StreamFrame) msg).stream(stream());
-                        if (!firstFrameWritten && !isStreamIdValid(stream().id())) {
-                            if (!(frame instanceof Http2HeadersFrame)) {
-                                ReferenceCountUtil.release(frame);
-                                promise.setFailure(
-                                        new IllegalArgumentException("The first frame must be a headers frame. Was: "
-                                        + frame.name()));
-                                return;
-                            }
-                            firstFrameWritten = true;
-                            ChannelFuture future = write0(frame);
-                            if (future.isDone()) {
-                                firstWriteComplete(future, promise);
-                            } else {
-                                future.addListener(new ChannelFutureListener() {
-                                    @Override
-                                    public void operationComplete(ChannelFuture future) {
-                                        firstWriteComplete(future, promise);
-                                    }
-                                });
-                            }
-                            return;
-                        }
-                    } else  {
-                        String msgStr = msg.toString();
-                        ReferenceCountUtil.release(msg);
-                        promise.setFailure(new IllegalArgumentException(
-                                "Message must be an " + StringUtil.simpleClassName(Http2StreamFrame.class) +
-                                        ": " + msgStr));
-                        return;
-                    }
-
-                    ChannelFuture future = write0(msg);
-                    if (future.isDone()) {
-                        writeComplete(future, promise);
-                    } else {
-                        future.addListener(new ChannelFutureListener() {
-                            @Override
-                            public void operationComplete(ChannelFuture future) {
-                                writeComplete(future, promise);
-                            }
-                        });
-                    }
-                } catch (Throwable t) {
-                    promise.tryFailure(t);
-                } finally {
-                    writeDoneAndNoFlush = true;
-                }
-            }
-
-            private void firstWriteComplete(ChannelFuture future, ChannelPromise promise) {
-                Throwable cause = future.cause();
-                if (cause == null) {
-                    // As we just finished our first write which made the stream-id valid we need to re-evaluate
-                    // the writability of the channel.
-                    writabilityChanged(Http2MultiplexCodec.this.isWritable(stream));
-                    promise.setSuccess();
-                } else {
-                    // If the first write fails there is not much we can do, just close
-                    closeForcibly();
-                    promise.setFailure(wrapStreamClosedError(cause));
-                }
-            }
-
-            private void writeComplete(ChannelFuture future, ChannelPromise promise) {
-                Throwable cause = future.cause();
-                if (cause == null) {
-                    promise.setSuccess();
-                } else {
-                    Throwable error = wrapStreamClosedError(cause);
-                    if (error instanceof ClosedChannelException) {
-                        if (config.isAutoClose()) {
-                            // Close channel if needed.
-                            closeForcibly();
-                        } else {
-                            outboundClosed = true;
-                        }
-                    }
-                    promise.setFailure(error);
-                }
-            }
-
-            private Throwable wrapStreamClosedError(Throwable cause) {
-                // If the error was caused by STREAM_CLOSED we should use a ClosedChannelException to better
-                // mimic other transports and make it easier to reason about what exceptions to expect.
-                if (cause instanceof Http2Exception && ((Http2Exception) cause).error() == Http2Error.STREAM_CLOSED) {
-                    return new ClosedChannelException().initCause(cause);
-                }
-                return cause;
-            }
-
-            private Http2StreamFrame validateStreamFrame(Http2StreamFrame frame) {
-                if (frame.stream() != null && frame.stream() != stream) {
-                    String msgString = frame.toString();
-                    ReferenceCountUtil.release(frame);
-                    throw new IllegalArgumentException(
-                            "Stream " + frame.stream() + " must not be set on the frame: " + msgString);
-                }
-                return frame;
-            }
-
-            private ChannelFuture write0(Object msg) {
-                ChannelPromise promise = ctx.newPromise();
-                Http2MultiplexCodec.this.write(ctx, msg, promise);
-                return promise;
-            }
-
-            @Override
-            public void flush() {
-                // If we are currently in the parent channel's read loop we should just ignore the flush.
-                // We will ensure we trigger ctx.flush() after we processed all Channels later on and
-                // so aggregate the flushes. This is done as ctx.flush() is expensive when as it may trigger an
-                // write(...) or writev(...) operation on the socket.
-                if (!writeDoneAndNoFlush || parentReadInProgress) {
-                    // There is nothing to flush so this is a NOOP.
-                    return;
-                }
-                try {
-                    flush0(ctx);
-                } finally {
-                    writeDoneAndNoFlush = false;
-                }
-            }
-
-            @Override
-            public ChannelPromise voidPromise() {
-                return unsafeVoidPromise;
-            }
-
-            @Override
-            public ChannelOutboundBuffer outboundBuffer() {
-                // Always return null as we not use the ChannelOutboundBuffer and not even support it.
-                return null;
-            }
-        }
-
-        /**
-         * {@link ChannelConfig} so that the high and low writebuffer watermarks can reflect the outbound flow control
-         * window, without having to create a new {@link WriteBufferWaterMark} object whenever the flow control window
-         * changes.
-         */
-        private final class Http2StreamChannelConfig extends DefaultChannelConfig {
-            Http2StreamChannelConfig(Channel channel) {
-                super(channel);
-            }
-
-            @Override
-            public int getWriteBufferHighWaterMark() {
-                return min(parent().config().getWriteBufferHighWaterMark(), initialOutboundStreamWindow);
-            }
-
-            @Override
-            public int getWriteBufferLowWaterMark() {
-                return min(parent().config().getWriteBufferLowWaterMark(), initialOutboundStreamWindow);
-            }
-
-            @Override
-            public MessageSizeEstimator getMessageSizeEstimator() {
-                return FlowControlledFrameSizeEstimator.INSTANCE;
-            }
-
-            @Override
-            public WriteBufferWaterMark getWriteBufferWaterMark() {
-                int mark = getWriteBufferHighWaterMark();
-                return new WriteBufferWaterMark(mark, mark);
-            }
-
-            @Override
-            public ChannelConfig setMessageSizeEstimator(MessageSizeEstimator estimator) {
-                throw new UnsupportedOperationException();
-            }
-
-            @Override
-            @Deprecated
-            public ChannelConfig setWriteBufferHighWaterMark(int writeBufferHighWaterMark) {
-                throw new UnsupportedOperationException();
-            }
-
-            @Override
-            @Deprecated
-            public ChannelConfig setWriteBufferLowWaterMark(int writeBufferLowWaterMark) {
-                throw new UnsupportedOperationException();
-            }
-
-            @Override
-            public ChannelConfig setWriteBufferWaterMark(WriteBufferWaterMark writeBufferWaterMark) {
-                throw new UnsupportedOperationException();
-            }
-
-            @Override
-            public ChannelConfig setRecvByteBufAllocator(RecvByteBufAllocator allocator) {
-                if (!(allocator.newHandle() instanceof RecvByteBufAllocator.ExtendedHandle)) {
-                    throw new IllegalArgumentException("allocator.newHandle() must return an object of type: " +
-                            RecvByteBufAllocator.ExtendedHandle.class);
-                }
-                super.setRecvByteBufAllocator(allocator);
-                return this;
-            }
+        protected void flush0(ChannelHandlerContext ctx) {
+            Http2MultiplexCodec.this.flush0(ctx);
         }
     }
 }
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MultiplexCodecBuilder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MultiplexCodecBuilder.java
index c5732ec..5d0829e 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MultiplexCodecBuilder.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MultiplexCodecBuilder.java
@@ -23,7 +23,10 @@ import static io.netty.util.internal.ObjectUtil.checkNotNull;
 
 /**
  * A builder for {@link Http2MultiplexCodec}.
+ *
+ * @deprecated use {@link Http2FrameCodecBuilder} together with {@link Http2MultiplexHandler}.
  */
+@Deprecated
 @UnstableApi
 public class Http2MultiplexCodecBuilder
         extends AbstractHttp2ConnectionHandlerBuilder<Http2MultiplexCodec, Http2MultiplexCodecBuilder> {
@@ -35,6 +38,8 @@ public class Http2MultiplexCodecBuilder
     Http2MultiplexCodecBuilder(boolean server, ChannelHandler childHandler) {
         server(server);
         this.childHandler = checkSharable(checkNotNull(childHandler, "childHandler"));
+        // For backwards compatibility we should disable to timeout by default at this layer.
+        gracefulShutdownTimeoutMillis(0);
     }
 
     private static ChannelHandler checkSharable(ChannelHandler handler) {
@@ -52,7 +57,7 @@ public class Http2MultiplexCodecBuilder
     }
 
     /**
-     * Creates a builder for a HTTP/2 client.
+     * Creates a builder for an HTTP/2 client.
      *
      * @param childHandler the handler added to channels for remotely-created streams. It must be
      *     {@link ChannelHandler.Sharable}.
@@ -62,7 +67,7 @@ public class Http2MultiplexCodecBuilder
     }
 
     /**
-     * Creates a builder for a HTTP/2 server.
+     * Creates a builder for an HTTP/2 server.
      *
      * @param childHandler the handler added to channels for remotely-created streams. It must be
      *     {@link ChannelHandler.Sharable}.
@@ -71,6 +76,14 @@ public class Http2MultiplexCodecBuilder
         return new Http2MultiplexCodecBuilder(true, childHandler);
     }
 
+    public Http2MultiplexCodecBuilder withUpgradeStreamHandler(ChannelHandler upgradeStreamHandler) {
+        if (this.isServer()) {
+            throw new IllegalArgumentException("Server codecs don't use an extra handler for the upgrade stream");
+        }
+        this.upgradeStreamHandler = upgradeStreamHandler;
+        return this;
+    }
+
     @Override
     public Http2Settings initialSettings() {
         return super.initialSettings();
@@ -91,14 +104,6 @@ public class Http2MultiplexCodecBuilder
         return super.gracefulShutdownTimeoutMillis(gracefulShutdownTimeoutMillis);
     }
 
-    public Http2MultiplexCodecBuilder withUpgradeStreamHandler(ChannelHandler upgradeStreamHandler) {
-        if (this.isServer()) {
-            throw new IllegalArgumentException("Server codecs don't use an extra handler for the upgrade stream");
-        }
-        this.upgradeStreamHandler = upgradeStreamHandler;
-        return this;
-    }
-
     @Override
     public boolean isServer() {
         return super.isServer();
@@ -144,6 +149,16 @@ public class Http2MultiplexCodecBuilder
         return super.encoderEnforceMaxConcurrentStreams(encoderEnforceMaxConcurrentStreams);
     }
 
+    @Override
+    public int encoderEnforceMaxQueuedControlFrames() {
+        return super.encoderEnforceMaxQueuedControlFrames();
+    }
+
+    @Override
+    public Http2MultiplexCodecBuilder encoderEnforceMaxQueuedControlFrames(int maxQueuedControlFrames) {
+        return super.encoderEnforceMaxQueuedControlFrames(maxQueuedControlFrames);
+    }
+
     @Override
     public Http2HeadersEncoder.SensitivityDetector headerSensitivityDetector() {
         return super.headerSensitivityDetector();
@@ -161,10 +176,36 @@ public class Http2MultiplexCodecBuilder
     }
 
     @Override
+    @Deprecated
     public Http2MultiplexCodecBuilder initialHuffmanDecodeCapacity(int initialHuffmanDecodeCapacity) {
         return super.initialHuffmanDecodeCapacity(initialHuffmanDecodeCapacity);
     }
 
+    @Override
+    public Http2MultiplexCodecBuilder autoAckSettingsFrame(boolean autoAckSettings) {
+        return super.autoAckSettingsFrame(autoAckSettings);
+    }
+
+    @Override
+    public Http2MultiplexCodecBuilder autoAckPingFrame(boolean autoAckPingFrame) {
+        return super.autoAckPingFrame(autoAckPingFrame);
+    }
+
+    @Override
+    public Http2MultiplexCodecBuilder decoupleCloseAndGoAway(boolean decoupleCloseAndGoAway) {
+        return super.decoupleCloseAndGoAway(decoupleCloseAndGoAway);
+    }
+
+    @Override
+    public int decoderEnforceMaxConsecutiveEmptyDataFrames() {
+        return super.decoderEnforceMaxConsecutiveEmptyDataFrames();
+    }
+
+    @Override
+    public Http2MultiplexCodecBuilder decoderEnforceMaxConsecutiveEmptyDataFrames(int maxConsecutiveEmptyFrames) {
+        return super.decoderEnforceMaxConsecutiveEmptyDataFrames(maxConsecutiveEmptyFrames);
+    }
+
     @Override
     public Http2MultiplexCodec build() {
         Http2FrameWriter frameWriter = this.frameWriter;
@@ -174,8 +215,8 @@ public class Http2MultiplexCodecBuilder
             DefaultHttp2Connection connection = new DefaultHttp2Connection(isServer(), maxReservedStreams());
             Long maxHeaderListSize = initialSettings().maxHeaderListSize();
             Http2FrameReader frameReader = new DefaultHttp2FrameReader(maxHeaderListSize == null ?
-                    new DefaultHttp2HeadersDecoder(true) :
-                    new DefaultHttp2HeadersDecoder(true, maxHeaderListSize));
+                    new DefaultHttp2HeadersDecoder(isValidateHeaders()) :
+                    new DefaultHttp2HeadersDecoder(isValidateHeaders(), maxHeaderListSize));
 
             if (frameLogger() != null) {
                 frameWriter = new Http2OutboundFrameLogger(frameWriter, frameLogger());
@@ -185,7 +226,13 @@ public class Http2MultiplexCodecBuilder
             if (encoderEnforceMaxConcurrentStreams()) {
                 encoder = new StreamBufferingEncoder(encoder);
             }
-            Http2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, frameReader);
+            Http2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, frameReader,
+                    promisedRequestVerifier(), isAutoAckSettingsFrame(), isAutoAckPingFrame());
+
+            int maxConsecutiveEmptyDataFrames = decoderEnforceMaxConsecutiveEmptyDataFrames();
+            if (maxConsecutiveEmptyDataFrames > 0) {
+                decoder = new Http2EmptyDataFrameConnectionDecoder(decoder, maxConsecutiveEmptyDataFrames);
+            }
 
             return build(decoder, encoder, initialSettings());
         }
@@ -195,6 +242,9 @@ public class Http2MultiplexCodecBuilder
     @Override
     protected Http2MultiplexCodec build(
             Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder, Http2Settings initialSettings) {
-        return new Http2MultiplexCodec(encoder, decoder, initialSettings, childHandler, upgradeStreamHandler);
+        Http2MultiplexCodec codec = new Http2MultiplexCodec(encoder, decoder, initialSettings, childHandler,
+                upgradeStreamHandler, decoupleCloseAndGoAway());
+        codec.gracefulShutdownTimeoutMillis(gracefulShutdownTimeoutMillis());
+        return codec;
     }
 }
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MultiplexHandler.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MultiplexHandler.java
new file mode 100644
index 0000000..f9e8b11
--- /dev/null
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MultiplexHandler.java
@@ -0,0 +1,365 @@
+/*
+ * Copyright 2019 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.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelConfig;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelPipeline;
+import io.netty.channel.EventLoop;
+import io.netty.channel.ServerChannel;
+import io.netty.handler.codec.http2.Http2FrameCodec.DefaultHttp2FrameStream;
+import io.netty.util.ReferenceCounted;
+import io.netty.util.internal.ObjectUtil;
+import io.netty.util.internal.UnstableApi;
+
+import java.util.ArrayDeque;
+import java.util.Queue;
+
+import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR;
+import static io.netty.handler.codec.http2.Http2Exception.connectionError;
+
+/**
+ * An HTTP/2 handler that creates child channels for each stream. This handler must be used in combination
+ * with {@link Http2FrameCodec}.
+ *
+ * <p>When a new stream is created, a new {@link Channel} is created for it. Applications send and
+ * receive {@link Http2StreamFrame}s on the created channel. {@link ByteBuf}s cannot be processed by the channel;
+ * all writes that reach the head of the pipeline must be an instance of {@link Http2StreamFrame}. Writes that reach
+ * the head of the pipeline are processed directly by this handler and cannot be intercepted.
+ *
+ * <p>The child channel will be notified of user events that impact the stream, such as {@link
+ * Http2GoAwayFrame} and {@link Http2ResetFrame}, as soon as they occur. Although {@code
+ * Http2GoAwayFrame} and {@code Http2ResetFrame} signify that the remote is ignoring further
+ * communication, closing of the channel is delayed until any inbound queue is drained with {@link
+ * Channel#read()}, which follows the default behavior of channels in Netty. Applications are
+ * free to close the channel in response to such events if they don't have use for any queued
+ * messages. Any connection level events like {@link Http2SettingsFrame} and {@link Http2GoAwayFrame}
+ * will be processed internally and also propagated down the pipeline for other handlers to act on.
+ *
+ * <p>Outbound streams are supported via the {@link Http2StreamChannelBootstrap}.
+ *
+ * <p>{@link ChannelConfig#setMaxMessagesPerRead(int)} and {@link ChannelConfig#setAutoRead(boolean)} are supported.
+ *
+ * <h3>Reference Counting</h3>
+ *
+ * Some {@link Http2StreamFrame}s implement the {@link ReferenceCounted} interface, as they carry
+ * reference counted objects (e.g. {@link ByteBuf}s). The multiplex codec will call {@link ReferenceCounted#retain()}
+ * before propagating a reference counted object through the pipeline, and thus an application handler needs to release
+ * such an object after having consumed it. For more information on reference counting take a look at
+ * https://netty.io/wiki/reference-counted-objects.html
+ *
+ * <h3>Channel Events</h3>
+ *
+ * A child channel becomes active as soon as it is registered to an {@link EventLoop}. Therefore, an active channel
+ * does not map to an active HTTP/2 stream immediately. Only once a {@link Http2HeadersFrame} has been successfully sent
+ * or received, does the channel map to an active HTTP/2 stream. In case it is not possible to open a new HTTP/2 stream
+ * (i.e. due to the maximum number of active streams being exceeded), the child channel receives an exception
+ * indicating the cause and is closed immediately thereafter.
+ *
+ * <h3>Writability and Flow Control</h3>
+ *
+ * A child channel observes outbound/remote flow control via the channel's writability. A channel only becomes writable
+ * when it maps to an active HTTP/2 stream . A child channel does not know about the connection-level flow control
+ * window. {@link ChannelHandler}s are free to ignore the channel's writability, in which case the excessive writes will
+ * be buffered by the parent channel. It's important to note that only {@link Http2DataFrame}s are subject to
+ * HTTP/2 flow control.
+ */
+@UnstableApi
+public final class Http2MultiplexHandler extends Http2ChannelDuplexHandler {
+
+    static final ChannelFutureListener CHILD_CHANNEL_REGISTRATION_LISTENER = new ChannelFutureListener() {
+        @Override
+        public void operationComplete(ChannelFuture future) {
+            registerDone(future);
+        }
+    };
+
+    private final ChannelHandler inboundStreamHandler;
+    private final ChannelHandler upgradeStreamHandler;
+    private final Queue<AbstractHttp2StreamChannel> readCompletePendingQueue =
+            new MaxCapacityQueue<AbstractHttp2StreamChannel>(new ArrayDeque<AbstractHttp2StreamChannel>(8),
+                    // Choose 100 which is what is used most of the times as default.
+                    Http2CodecUtil.SMALLEST_MAX_CONCURRENT_STREAMS);
+
+    private boolean parentReadInProgress;
+    private int idCount;
+
+    // Need to be volatile as accessed from within the Http2MultiplexHandlerStreamChannel in a multi-threaded fashion.
+    private volatile ChannelHandlerContext ctx;
+
+    /**
+     * Creates a new instance
+     *
+     * @param inboundStreamHandler the {@link ChannelHandler} that will be added to the {@link ChannelPipeline} of
+     *                             the {@link Channel}s created for new inbound streams.
+     */
+    public Http2MultiplexHandler(ChannelHandler inboundStreamHandler) {
+        this(inboundStreamHandler, null);
+    }
+
+    /**
+     * Creates a new instance
+     *
+     * @param inboundStreamHandler the {@link ChannelHandler} that will be added to the {@link ChannelPipeline} of
+     *                             the {@link Channel}s created for new inbound streams.
+     * @param upgradeStreamHandler the {@link ChannelHandler} that will be added to the {@link ChannelPipeline} of the
+     *                             upgraded {@link Channel}.
+     */
+    public Http2MultiplexHandler(ChannelHandler inboundStreamHandler, ChannelHandler upgradeStreamHandler) {
+        this.inboundStreamHandler = ObjectUtil.checkNotNull(inboundStreamHandler, "inboundStreamHandler");
+        this.upgradeStreamHandler = upgradeStreamHandler;
+    }
+
+    static void registerDone(ChannelFuture future) {
+        // Handle any errors that occurred on the local thread while registering. Even though
+        // failures can happen after this point, they will be handled by the channel by closing the
+        // childChannel.
+        if (!future.isSuccess()) {
+            Channel childChannel = future.channel();
+            if (childChannel.isRegistered()) {
+                childChannel.close();
+            } else {
+                childChannel.unsafe().closeForcibly();
+            }
+        }
+    }
+
+    @Override
+    protected void handlerAdded0(ChannelHandlerContext ctx) {
+        if (ctx.executor() != ctx.channel().eventLoop()) {
+            throw new IllegalStateException("EventExecutor must be EventLoop of Channel");
+        }
+        this.ctx = ctx;
+    }
+
+    @Override
+    protected void handlerRemoved0(ChannelHandlerContext ctx) {
+        readCompletePendingQueue.clear();
+    }
+
+    @Override
+    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+        parentReadInProgress = true;
+        if (msg instanceof Http2StreamFrame) {
+            if (msg instanceof Http2WindowUpdateFrame) {
+                // We dont want to propagate update frames to the user
+                return;
+            }
+            Http2StreamFrame streamFrame = (Http2StreamFrame) msg;
+            DefaultHttp2FrameStream s =
+                    (DefaultHttp2FrameStream) streamFrame.stream();
+
+            AbstractHttp2StreamChannel channel = (AbstractHttp2StreamChannel) s.attachment;
+            if (msg instanceof Http2ResetFrame) {
+                // Reset frames needs to be propagated via user events as these are not flow-controlled and so
+                // must not be controlled by suppressing channel.read() on the child channel.
+                channel.pipeline().fireUserEventTriggered(msg);
+
+                // RST frames will also trigger closing of the streams which then will call
+                // AbstractHttp2StreamChannel.streamClosed()
+            } else {
+                channel.fireChildRead(streamFrame);
+            }
+            return;
+        }
+
+        if (msg instanceof Http2GoAwayFrame) {
+            // goaway frames will also trigger closing of the streams which then will call
+            // AbstractHttp2StreamChannel.streamClosed()
+            onHttp2GoAwayFrame(ctx, (Http2GoAwayFrame) msg);
+        }
+
+        // Send everything down the pipeline
+        ctx.fireChannelRead(msg);
+    }
+
+    @Override
+    public void channelWritabilityChanged(final ChannelHandlerContext ctx) throws Exception {
+        if (ctx.channel().isWritable()) {
+            // While the writability state may change during iterating of the streams we just set all of the streams
+            // to writable to not affect fairness. These will be "limited" by their own watermarks in any case.
+            forEachActiveStream(AbstractHttp2StreamChannel.WRITABLE_VISITOR);
+        }
+
+        ctx.fireChannelWritabilityChanged();
+    }
+
+    @Override
+    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
+        if (evt instanceof Http2FrameStreamEvent) {
+            Http2FrameStreamEvent event = (Http2FrameStreamEvent) evt;
+            DefaultHttp2FrameStream stream = (DefaultHttp2FrameStream) event.stream();
+            if (event.type() == Http2FrameStreamEvent.Type.State) {
+                switch (stream.state()) {
+                    case HALF_CLOSED_LOCAL:
+                        if (stream.id() != Http2CodecUtil.HTTP_UPGRADE_STREAM_ID) {
+                            // Ignore everything which was not caused by an upgrade
+                            break;
+                        }
+                        // fall-through
+                    case HALF_CLOSED_REMOTE:
+                        // fall-through
+                    case OPEN:
+                        if (stream.attachment != null) {
+                            // ignore if child channel was already created.
+                            break;
+                        }
+                        final AbstractHttp2StreamChannel ch;
+                        // We need to handle upgrades special when on the client side.
+                        if (stream.id() == Http2CodecUtil.HTTP_UPGRADE_STREAM_ID && !isServer(ctx)) {
+                            // We must have an upgrade handler or else we can't handle the stream
+                            if (upgradeStreamHandler == null) {
+                                throw connectionError(INTERNAL_ERROR,
+                                        "Client is misconfigured for upgrade requests");
+                            }
+                            ch = new Http2MultiplexHandlerStreamChannel(stream, upgradeStreamHandler);
+                            ch.closeOutbound();
+                        } else {
+                            ch = new Http2MultiplexHandlerStreamChannel(stream, inboundStreamHandler);
+                        }
+                        ChannelFuture future = ctx.channel().eventLoop().register(ch);
+                        if (future.isDone()) {
+                            registerDone(future);
+                        } else {
+                            future.addListener(CHILD_CHANNEL_REGISTRATION_LISTENER);
+                        }
+                        break;
+                    case CLOSED:
+                        AbstractHttp2StreamChannel channel = (AbstractHttp2StreamChannel) stream.attachment;
+                        if (channel != null) {
+                            channel.streamClosed();
+                        }
+                        break;
+                    default:
+                        // ignore for now
+                        break;
+                }
+            }
+            return;
+        }
+        ctx.fireUserEventTriggered(evt);
+    }
+
+    // TODO: This is most likely not the best way to expose this, need to think more about it.
+    Http2StreamChannel newOutboundStream() {
+        return new Http2MultiplexHandlerStreamChannel((DefaultHttp2FrameStream) newStream(), null);
+    }
+
+    @Override
+    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+        if (cause instanceof Http2FrameStreamException) {
+            Http2FrameStreamException exception = (Http2FrameStreamException) cause;
+            Http2FrameStream stream = exception.stream();
+            AbstractHttp2StreamChannel childChannel = (AbstractHttp2StreamChannel)
+                    ((DefaultHttp2FrameStream) stream).attachment;
+            try {
+                childChannel.pipeline().fireExceptionCaught(cause.getCause());
+            } finally {
+                childChannel.unsafe().closeForcibly();
+            }
+            return;
+        }
+        ctx.fireExceptionCaught(cause);
+    }
+
+    private static boolean isServer(ChannelHandlerContext ctx) {
+        return ctx.channel().parent() instanceof ServerChannel;
+    }
+
+    private void onHttp2GoAwayFrame(ChannelHandlerContext ctx, final Http2GoAwayFrame goAwayFrame) {
+        try {
+            final boolean server = isServer(ctx);
+            forEachActiveStream(new Http2FrameStreamVisitor() {
+                @Override
+                public boolean visit(Http2FrameStream stream) {
+                    final int streamId = stream.id();
+                    if (streamId > goAwayFrame.lastStreamId() && Http2CodecUtil.isStreamIdValid(streamId, server)) {
+                        final AbstractHttp2StreamChannel childChannel = (AbstractHttp2StreamChannel)
+                                ((DefaultHttp2FrameStream) stream).attachment;
+                        childChannel.pipeline().fireUserEventTriggered(goAwayFrame.retainedDuplicate());
+                    }
+                    return true;
+                }
+            });
+        } catch (Http2Exception e) {
+            ctx.fireExceptionCaught(e);
+            ctx.close();
+        }
+    }
+
+    /**
+     * Notifies any child streams of the read completion.
+     */
+    @Override
+    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
+        processPendingReadCompleteQueue();
+        ctx.fireChannelReadComplete();
+    }
+
+    private void processPendingReadCompleteQueue() {
+        parentReadInProgress = true;
+        // If we have many child channel we can optimize for the case when multiple call flush() in
+        // channelReadComplete(...) callbacks and only do it once as otherwise we will end-up with multiple
+        // write calls on the socket which is expensive.
+        AbstractHttp2StreamChannel childChannel = readCompletePendingQueue.poll();
+        if (childChannel != null) {
+            try {
+                do {
+                    childChannel.fireChildReadComplete();
+                    childChannel = readCompletePendingQueue.poll();
+                } while (childChannel != null);
+            } finally {
+                parentReadInProgress = false;
+                readCompletePendingQueue.clear();
+                ctx.flush();
+            }
+        } else {
+            parentReadInProgress = false;
+        }
+    }
+
+    private final class Http2MultiplexHandlerStreamChannel extends AbstractHttp2StreamChannel {
+
+        Http2MultiplexHandlerStreamChannel(DefaultHttp2FrameStream stream, ChannelHandler inboundHandler) {
+            super(stream, ++idCount, inboundHandler);
+        }
+
+        @Override
+        protected boolean isParentReadInProgress() {
+            return parentReadInProgress;
+        }
+
+        @Override
+        protected void addChannelToReadCompletePendingQueue() {
+            // If there is no space left in the queue, just keep on processing everything that is already
+            // stored there and try again.
+            while (!readCompletePendingQueue.offer(this)) {
+                processPendingReadCompleteQueue();
+            }
+        }
+
+        @Override
+        protected ChannelHandlerContext parentContext() {
+            return ctx;
+        }
+    }
+}
+
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2PromisedRequestVerifier.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2PromisedRequestVerifier.java
index 43b8f07..fc8e65c 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2PromisedRequestVerifier.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2PromisedRequestVerifier.java
@@ -19,7 +19,7 @@ import io.netty.util.internal.UnstableApi;
 
 /**
  * Provides an extensibility point for users to define the validity of push requests.
- * @see <a href="https://tools.ietf.org/html/draft-ietf-httpbis-http2-17#section-8.2">[RFC http2], Section 8.2</a>.
+ * @see <a href="https://tools.ietf.org/html/rfc7540#section-8.2">[RFC 7540], Section 8.2</a>.
  */
 @UnstableApi
 public interface Http2PromisedRequestVerifier {
@@ -29,7 +29,7 @@ public interface Http2PromisedRequestVerifier {
      * @param headers The headers to be verified.
      * @return {@code true} if the {@code ctx} is authoritative for the {@code headers}, {@code false} otherwise.
      * @see
-     * <a href="https://tools.ietf.org/html/draft-ietf-httpbis-http2-17#section-10.1">[RFC http2], Section 10.1</a>.
+     * <a href="https://tools.ietf.org/html/rfc7540#section-10.1">[RFC 7540], Section 10.1</a>.
      */
     boolean isAuthoritative(ChannelHandlerContext ctx, Http2Headers headers);
 
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2SecurityUtil.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2SecurityUtil.java
index 6c08fee..897c771 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2SecurityUtil.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2SecurityUtil.java
@@ -33,7 +33,7 @@ public final class Http2SecurityUtil {
      * Ciphers</a> and <a
      * href="https://wiki.mozilla.org/Security/Server_Side_TLS#Modern_compatibility">Mozilla Modern Cipher
      * Suites</a> in accordance with the <a
-     * href="https://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-9.2.2">HTTP/2 Specification</a>.
+     * href="https://tools.ietf.org/html/rfc7540#section-9.2.2">HTTP/2 Specification</a>.
      *
      * According to the <a href="http://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html">
      * JSSE documentation</a> "the names mentioned in the TLS RFCs prefixed with TLS_ are functionally equivalent
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ServerUpgradeCodec.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ServerUpgradeCodec.java
index f61dae3..84911ec 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ServerUpgradeCodec.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ServerUpgradeCodec.java
@@ -17,7 +17,6 @@ package io.netty.handler.codec.http2;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.ByteBufUtil;
 import io.netty.channel.ChannelHandler;
-import io.netty.channel.ChannelHandlerAdapter;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.handler.codec.base64.Base64;
 import io.netty.handler.codec.http.FullHttpRequest;
@@ -38,7 +37,6 @@ import static io.netty.handler.codec.http2.Http2CodecUtil.FRAME_HEADER_LENGTH;
 import static io.netty.handler.codec.http2.Http2CodecUtil.HTTP_UPGRADE_SETTINGS_HEADER;
 import static io.netty.handler.codec.http2.Http2CodecUtil.writeFrameHeader;
 import static io.netty.handler.codec.http2.Http2FrameTypes.SETTINGS;
-import static io.netty.util.internal.ObjectUtil.checkNotNull;
 
 /**
  * Server-side codec for performing a cleartext upgrade from HTTP/1.x to HTTP/2.
@@ -148,19 +146,19 @@ public class Http2ServerUpgradeCodec implements HttpServerUpgradeHandler.Upgrade
         try {
             // Add the HTTP/2 connection handler to the pipeline immediately following the current handler.
             ctx.pipeline().addAfter(ctx.name(), handlerName, connectionHandler);
-            connectionHandler.onHttpServerUpgrade(settings);
 
+            // Add also all extra handlers as these may handle events / messages produced by the connectionHandler.
+            // See https://github.com/netty/netty/issues/9314
+            if (handlers != null) {
+                final String name = ctx.pipeline().context(connectionHandler).name();
+                for (int i = handlers.length - 1; i >= 0; i--) {
+                    ctx.pipeline().addAfter(name, null, handlers[i]);
+                }
+            }
+            connectionHandler.onHttpServerUpgrade(settings);
         } catch (Http2Exception e) {
             ctx.fireExceptionCaught(e);
             ctx.close();
-            return;
-        }
-
-        if (handlers != null) {
-            final String name = ctx.pipeline().context(connectionHandler).name();
-            for (int i = handlers.length - 1; i >= 0; i--) {
-                ctx.pipeline().addAfter(name, null, handlers[i]);
-            }
         }
     }
 
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2SettingsAckFrame.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2SettingsAckFrame.java
new file mode 100644
index 0000000..fc46ce5
--- /dev/null
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2SettingsAckFrame.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2019 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;
+
+/**
+ * An ack for a previously received {@link Http2SettingsFrame}.
+ * <p>
+ * The <a href="https://tools.ietf.org/html/rfc7540#section-6.5">HTTP/2 protocol</a> enforces that ACKs are applied in
+ * order, so this ACK will apply to the earliest received and not yet ACKed {@link Http2SettingsFrame} frame.
+ */
+public interface Http2SettingsAckFrame extends Http2Frame {
+    Http2SettingsAckFrame INSTANCE = new DefaultHttp2SettingsAckFrame();
+
+    @Override
+    String name();
+}
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2SettingsReceivedConsumer.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2SettingsReceivedConsumer.java
new file mode 100644
index 0000000..97dfd39
--- /dev/null
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2SettingsReceivedConsumer.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2019 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;
+
+/**
+ * Provides a Consumer like interface to consume remote settings received but not yet ACKed.
+ */
+public interface Http2SettingsReceivedConsumer {
+    /**
+     * Consume the most recently received but not yet ACKed settings.
+     */
+    void consumeReceivedSettings(Http2Settings settings);
+}
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2StreamChannelBootstrap.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2StreamChannelBootstrap.java
index 6e9b797..f3007ff 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2StreamChannelBootstrap.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2StreamChannelBootstrap.java
@@ -35,16 +35,26 @@ import io.netty.util.internal.logging.InternalLoggerFactory;
 import java.nio.channels.ClosedChannelException;
 import java.util.LinkedHashMap;
 import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 
 @UnstableApi
 public final class Http2StreamChannelBootstrap {
     private static final InternalLogger logger = InternalLoggerFactory.getInstance(Http2StreamChannelBootstrap.class);
+    @SuppressWarnings("unchecked")
+    private static final Map.Entry<ChannelOption<?>, Object>[] EMPTY_OPTION_ARRAY = new Map.Entry[0];
+    @SuppressWarnings("unchecked")
+    private static final Map.Entry<AttributeKey<?>, Object>[] EMPTY_ATTRIBUTE_ARRAY = new Map.Entry[0];
 
+    // The order in which ChannelOptions are applied is important they may depend on each other for validation
+    // purposes.
     private final Map<ChannelOption<?>, Object> options = new LinkedHashMap<ChannelOption<?>, Object>();
-    private final Map<AttributeKey<?>, Object> attrs = new LinkedHashMap<AttributeKey<?>, Object>();
+    private final Map<AttributeKey<?>, Object> attrs = new ConcurrentHashMap<AttributeKey<?>, Object>();
     private final Channel channel;
     private volatile ChannelHandler handler;
 
+    // Cache the ChannelHandlerContext to speed up open(...) operations.
+    private volatile ChannelHandlerContext multiplexCtx;
+
     public Http2StreamChannelBootstrap(Channel channel) {
         this.channel = ObjectUtil.checkNotNull(channel, "channel");
     }
@@ -55,15 +65,12 @@ public final class Http2StreamChannelBootstrap {
      */
     @SuppressWarnings("unchecked")
     public <T> Http2StreamChannelBootstrap option(ChannelOption<T> option, T value) {
-        if (option == null) {
-            throw new NullPointerException("option");
-        }
-        if (value == null) {
-            synchronized (options) {
+        ObjectUtil.checkNotNull(option, "option");
+
+        synchronized (options) {
+            if (value == null) {
                 options.remove(option);
-            }
-        } else {
-            synchronized (options) {
+            } else {
                 options.put(option, value);
             }
         }
@@ -76,17 +83,11 @@ public final class Http2StreamChannelBootstrap {
      */
     @SuppressWarnings("unchecked")
     public <T> Http2StreamChannelBootstrap attr(AttributeKey<T> key, T value) {
-        if (key == null) {
-            throw new NullPointerException("key");
-        }
+        ObjectUtil.checkNotNull(key, "key");
         if (value == null) {
-            synchronized (attrs) {
-                attrs.remove(key);
-            }
+            attrs.remove(key);
         } else {
-            synchronized (attrs) {
-                attrs.put(key, value);
-            }
+            attrs.put(key, value);
         }
         return this;
     }
@@ -94,44 +95,84 @@ public final class Http2StreamChannelBootstrap {
     /**
      * the {@link ChannelHandler} to use for serving the requests.
      */
-    @SuppressWarnings("unchecked")
     public Http2StreamChannelBootstrap handler(ChannelHandler handler) {
         this.handler = ObjectUtil.checkNotNull(handler, "handler");
         return this;
     }
 
+    /**
+     * Open a new {@link Http2StreamChannel} to use.
+     * @return the {@link Future} that will be notified once the channel was opened successfully or it failed.
+     */
     public Future<Http2StreamChannel> open() {
         return open(channel.eventLoop().<Http2StreamChannel>newPromise());
     }
 
+    /**
+     * Open a new {@link Http2StreamChannel} to use and notifies the given {@link Promise}.
+     * @return the {@link Future} that will be notified once the channel was opened successfully or it failed.
+     */
+    @SuppressWarnings("deprecation")
     public Future<Http2StreamChannel> open(final Promise<Http2StreamChannel> promise) {
-        final ChannelHandlerContext ctx = channel.pipeline().context(Http2MultiplexCodec.class);
-        if (ctx == null) {
-            if (channel.isActive()) {
-                promise.setFailure(new IllegalStateException(StringUtil.simpleClassName(Http2MultiplexCodec.class) +
-                        " must be in the ChannelPipeline of Channel " + channel));
-            } else {
-                promise.setFailure(new ClosedChannelException());
-            }
-        } else {
+        try {
+            ChannelHandlerContext ctx = findCtx();
             EventExecutor executor = ctx.executor();
             if (executor.inEventLoop()) {
                 open0(ctx, promise);
             } else {
+                final ChannelHandlerContext finalCtx = ctx;
                 executor.execute(new Runnable() {
                     @Override
                     public void run() {
-                        open0(ctx, promise);
+                        open0(finalCtx, promise);
                     }
                 });
             }
+        } catch (Throwable cause) {
+            promise.setFailure(cause);
         }
         return promise;
     }
 
+    private ChannelHandlerContext findCtx() throws ClosedChannelException {
+        // First try to use cached context and if this not work lets try to lookup the context.
+        ChannelHandlerContext ctx = this.multiplexCtx;
+        if (ctx != null && !ctx.isRemoved()) {
+            return ctx;
+        }
+        ChannelPipeline pipeline = channel.pipeline();
+        ctx = pipeline.context(Http2MultiplexCodec.class);
+        if (ctx == null) {
+            ctx = pipeline.context(Http2MultiplexHandler.class);
+        }
+        if (ctx == null) {
+            if (channel.isActive()) {
+                throw new IllegalStateException(StringUtil.simpleClassName(Http2MultiplexCodec.class) + " or "
+                        + StringUtil.simpleClassName(Http2MultiplexHandler.class)
+                        + " must be in the ChannelPipeline of Channel " + channel);
+            } else {
+                throw new ClosedChannelException();
+            }
+        }
+        this.multiplexCtx = ctx;
+        return ctx;
+    }
+
+    /**
+     * @deprecated should not be used directly. Use {@link #open()} or {@link #open(Promise)}
+     */
+    @Deprecated
     public void open0(ChannelHandlerContext ctx, final Promise<Http2StreamChannel> promise) {
         assert ctx.executor().inEventLoop();
-        final Http2StreamChannel streamChannel = ((Http2MultiplexCodec) ctx.handler()).newOutboundStream();
+        if (!promise.setUncancellable()) {
+            return;
+        }
+        final Http2StreamChannel streamChannel;
+        if (ctx.handler() instanceof Http2MultiplexCodec) {
+            streamChannel = ((Http2MultiplexCodec) ctx.handler()).newOutboundStream();
+        } else {
+            streamChannel = ((Http2MultiplexHandler) ctx.handler()).newOutboundStream();
+        }
         try {
             init(streamChannel);
         } catch (Exception e) {
@@ -143,7 +184,7 @@ public final class Http2StreamChannelBootstrap {
         ChannelFuture future = ctx.channel().eventLoop().register(streamChannel);
         future.addListener(new ChannelFutureListener() {
             @Override
-            public void operationComplete(ChannelFuture future) throws Exception {
+            public void operationComplete(ChannelFuture future) {
                 if (future.isSuccess()) {
                     promise.setSuccess(streamChannel);
                 } else if (future.isCancelled()) {
@@ -161,36 +202,34 @@ public final class Http2StreamChannelBootstrap {
         });
     }
 
-    @SuppressWarnings("unchecked")
-    private void init(Channel channel) throws Exception {
+    private void init(Channel channel) {
         ChannelPipeline p = channel.pipeline();
         ChannelHandler handler = this.handler;
         if (handler != null) {
             p.addLast(handler);
         }
+        final Map.Entry<ChannelOption<?>, Object> [] optionArray;
         synchronized (options) {
-            setChannelOptions(channel, options, logger);
+            optionArray = options.entrySet().toArray(EMPTY_OPTION_ARRAY);
         }
 
-        synchronized (attrs) {
-            for (Map.Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
-                channel.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
-            }
-        }
+        setChannelOptions(channel, optionArray);
+        setAttributes(channel, attrs.entrySet().toArray(EMPTY_ATTRIBUTE_ARRAY));
     }
 
     private static void setChannelOptions(
-            Channel channel, Map<ChannelOption<?>, Object> options, InternalLogger logger) {
-        for (Map.Entry<ChannelOption<?>, Object> e: options.entrySet()) {
-            setChannelOption(channel, e.getKey(), e.getValue(), logger);
+            Channel channel, Map.Entry<ChannelOption<?>, Object>[] options) {
+        for (Map.Entry<ChannelOption<?>, Object> e: options) {
+            setChannelOption(channel, e.getKey(), e.getValue());
         }
     }
 
-    @SuppressWarnings("unchecked")
     private static void setChannelOption(
-            Channel channel, ChannelOption<?> option, Object value, InternalLogger logger) {
+            Channel channel, ChannelOption<?> option, Object value) {
         try {
-            if (!channel.config().setOption((ChannelOption<Object>) option, value)) {
+            @SuppressWarnings("unchecked")
+            ChannelOption<Object> opt = (ChannelOption<Object>) option;
+            if (!channel.config().setOption(opt, value)) {
                 logger.warn("Unknown channel option '{}' for channel '{}'", option, channel);
             }
         } catch (Throwable t) {
@@ -198,4 +237,13 @@ public final class Http2StreamChannelBootstrap {
                     "Failed to set channel option '{}' with value '{}' for channel '{}'", option, value, channel, t);
         }
     }
+
+    private static void setAttributes(
+            Channel channel, Map.Entry<AttributeKey<?>, Object>[] options) {
+        for (Map.Entry<AttributeKey<?>, Object> e: options) {
+            @SuppressWarnings("unchecked")
+            AttributeKey<Object> key = (AttributeKey<Object>) e.getKey();
+            channel.attr(key).set(e.getValue());
+        }
+    }
 }
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 5f808db..8ffbd28 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
@@ -33,6 +33,7 @@ import io.netty.handler.codec.http.HttpResponseStatus;
 import io.netty.handler.codec.http.HttpUtil;
 import io.netty.handler.codec.http.HttpVersion;
 import io.netty.util.AsciiString;
+import io.netty.util.internal.InternalThreadLocalMap;
 import io.netty.util.internal.UnstableApi;
 
 import java.net.URI;
@@ -91,24 +92,24 @@ public final class HttpConversionUtil {
 
     /**
      * This will be the method used for {@link HttpRequest} objects generated out of the HTTP message flow defined in <a
-     * href="http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-8.1.">HTTP/2 Spec Message Flow</a>
+     * href="https://tools.ietf.org/html/rfc7540#section-8.1">[RFC 7540], Section 8.1</a>
      */
     public static final HttpMethod OUT_OF_MESSAGE_SEQUENCE_METHOD = HttpMethod.OPTIONS;
 
     /**
      * This will be the path used for {@link HttpRequest} objects generated out of the HTTP message flow defined in <a
-     * href="http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-8.1.">HTTP/2 Spec Message Flow</a>
+     * href="https://tools.ietf.org/html/rfc7540#section-8.1">[RFC 7540], Section 8.1</a>
      */
     public static final String OUT_OF_MESSAGE_SEQUENCE_PATH = "";
 
     /**
      * This will be the status code used for {@link HttpResponse} objects generated out of the HTTP message flow defined
-     * in <a href="http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-8.1.">HTTP/2 Spec Message Flow</a>
+     * in <a href="https://tools.ietf.org/html/rfc7540#section-8.1">[RFC 7540], Section 8.1</a>
      */
     public static final HttpResponseStatus OUT_OF_MESSAGE_SEQUENCE_RETURN_CODE = HttpResponseStatus.OK;
 
     /**
-     * <a href="https://tools.ietf.org/html/rfc7540#section-8.1.2.3">rfc7540, 8.1.2.3</a> states the path must not
+     * <a href="https://tools.ietf.org/html/rfc7540#section-8.1.2.3">[RFC 7540], 8.1.2.3</a> states the path must not
      * be empty, and instead should be {@code /}.
      */
     private static final AsciiString EMPTY_REQUEST_PATH = AsciiString.cached("/");
@@ -121,28 +122,28 @@ public final class HttpConversionUtil {
      */
     public enum ExtensionHeaderNames {
         /**
-         * HTTP extension header which will identify the stream id from the HTTP/2 event(s) responsible for generating a
-         * {@code HttpObject}
+         * HTTP extension header which will identify the stream id from the HTTP/2 event(s) responsible for
+         * generating an {@code HttpObject}
          * <p>
          * {@code "x-http2-stream-id"}
          */
         STREAM_ID("x-http2-stream-id"),
         /**
          * HTTP extension header which will identify the scheme pseudo header from the HTTP/2 event(s) responsible for
-         * generating a {@code HttpObject}
+         * generating an {@code HttpObject}
          * <p>
          * {@code "x-http2-scheme"}
          */
         SCHEME("x-http2-scheme"),
         /**
          * HTTP extension header which will identify the path pseudo header from the HTTP/2 event(s) responsible for
-         * generating a {@code HttpObject}
+         * generating an {@code HttpObject}
          * <p>
          * {@code "x-http2-path"}
          */
         PATH("x-http2-path"),
         /**
-         * HTTP extension header which will identify the stream id used to create this stream in a HTTP/2 push promise
+         * HTTP extension header which will identify the stream id used to create this stream in an HTTP/2 push promise
          * frame
          * <p>
          * {@code "x-http2-stream-promise-id"}
@@ -157,7 +158,7 @@ public final class HttpConversionUtil {
         STREAM_DEPENDENCY_ID("x-http2-stream-dependency-id"),
         /**
          * HTTP extension header which will identify the weight (if non-default and the priority is not on the default
-         * stream) of the associated HTTP/2 stream responsible responsible for generating a {@code HttpObject}
+         * stream) of the associated HTTP/2 stream responsible responsible for generating an {@code HttpObject}
          * <p>
          * {@code "x-http2-stream-weight"}
          */
@@ -360,9 +361,7 @@ public final class HttpConversionUtil {
             HttpVersion httpVersion, boolean isTrailer, boolean isRequest) throws Http2Exception {
         Http2ToHttpHeaderTranslator translator = new Http2ToHttpHeaderTranslator(streamId, outputHeaders, isRequest);
         try {
-            for (Entry<CharSequence, CharSequence> entry : inputHeaders) {
-                translator.translate(entry);
-            }
+            translator.translateHeaders(inputHeaders);
         } catch (Http2Exception ex) {
             throw ex;
         } catch (Throwable t) {
@@ -520,7 +519,7 @@ public final class HttpConversionUtil {
     }
 
     /**
-     * Generate a HTTP/2 {code :path} from a URI in accordance with
+     * Generate an HTTP/2 {code :path} from a URI in accordance with
      * <a href="https://tools.ietf.org/html/rfc7230#section-5.3">rfc7230, 5.3</a>.
      */
     private static AsciiString toHttp2Path(URI uri) {
@@ -620,29 +619,43 @@ public final class HttpConversionUtil {
             translations = request ? REQUEST_HEADER_TRANSLATIONS : RESPONSE_HEADER_TRANSLATIONS;
         }
 
-        public void translate(Entry<CharSequence, CharSequence> entry) throws Http2Exception {
-            final CharSequence name = entry.getKey();
-            final CharSequence value = entry.getValue();
-            AsciiString translatedName = translations.get(name);
-            if (translatedName != null) {
-                output.add(translatedName, AsciiString.of(value));
-            } else if (!Http2Headers.PseudoHeaderName.isPseudoHeader(name)) {
-                // https://tools.ietf.org/html/rfc7540#section-8.1.2.3
-                // All headers that start with ':' are only valid in HTTP/2 context
-                if (name.length() == 0 || name.charAt(0) == ':') {
-                    throw streamError(streamId, PROTOCOL_ERROR,
-                            "Invalid HTTP/2 header '%s' encountered in translation to HTTP/1.x", name);
-                }
-                if (COOKIE.equals(name)) {
-                    // combine the cookie values into 1 header entry.
-                    // https://tools.ietf.org/html/rfc7540#section-8.1.2.5
-                    String existingCookie = output.get(COOKIE);
-                    output.set(COOKIE,
-                               (existingCookie != null) ? (existingCookie + "; " + value) : value);
-                } else {
-                    output.add(name, value);
+        public void translateHeaders(Iterable<Entry<CharSequence, CharSequence>> inputHeaders) throws Http2Exception {
+            // lazily created as needed
+            StringBuilder cookies = null;
+
+            for (Entry<CharSequence, CharSequence> entry : inputHeaders) {
+                final CharSequence name = entry.getKey();
+                final CharSequence value = entry.getValue();
+                AsciiString translatedName = translations.get(name);
+                if (translatedName != null) {
+                    output.add(translatedName, AsciiString.of(value));
+                } else if (!Http2Headers.PseudoHeaderName.isPseudoHeader(name)) {
+                    // https://tools.ietf.org/html/rfc7540#section-8.1.2.3
+                    // All headers that start with ':' are only valid in HTTP/2 context
+                    if (name.length() == 0 || name.charAt(0) == ':') {
+                        throw streamError(streamId, PROTOCOL_ERROR,
+                                "Invalid HTTP/2 header '%s' encountered in translation to HTTP/1.x", name);
+                    }
+                    if (COOKIE.equals(name)) {
+                        // combine the cookie values into 1 header entry.
+                        // https://tools.ietf.org/html/rfc7540#section-8.1.2.5
+                        if (cookies == null) {
+                            cookies = InternalThreadLocalMap.get().stringBuilder();
+                        } else if (cookies.length() > 0) {
+                            cookies.append("; ");
+                        }
+                        cookies.append(value);
+                    } else {
+                        output.add(name, value);
+                    }
                 }
             }
+            if (cookies != null) {
+                output.add(COOKIE, cookies.toString());
+            }
+        }
+
+        private void translateHeader(Entry<CharSequence, CharSequence> entry) throws Http2Exception {
         }
     }
 }
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpToHttp2ConnectionHandler.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpToHttp2ConnectionHandler.java
index bdec231..84998c2 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpToHttp2ConnectionHandler.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpToHttp2ConnectionHandler.java
@@ -45,6 +45,13 @@ public class HttpToHttp2ConnectionHandler extends Http2ConnectionHandler {
         this.validateHeaders = validateHeaders;
     }
 
+    protected HttpToHttp2ConnectionHandler(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder,
+                                           Http2Settings initialSettings, boolean validateHeaders,
+                                           boolean decoupleCloseAndGoAway) {
+        super(decoder, encoder, initialSettings, decoupleCloseAndGoAway);
+        this.validateHeaders = validateHeaders;
+    }
+
     /**
      * Get the next stream id either from the {@link HttpHeaders} object or HTTP/2 codec
      *
@@ -103,8 +110,8 @@ public class HttpToHttp2ConnectionHandler extends Http2ConnectionHandler {
                 // Write the data
                 final ByteBuf content = ((HttpContent) msg).content();
                 endStream = isLastContent && trailers.isEmpty();
-                release = false;
                 encoder.writeData(ctx, currentStreamId, content, 0, endStream, promiseAggregator.newPromise());
+                release = false;
 
                 if (!trailers.isEmpty()) {
                     // Write trailing headers.
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpToHttp2ConnectionHandlerBuilder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpToHttp2ConnectionHandlerBuilder.java
index 877c958..8d1df17 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpToHttp2ConnectionHandlerBuilder.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpToHttp2ConnectionHandlerBuilder.java
@@ -80,10 +80,16 @@ public final class HttpToHttp2ConnectionHandlerBuilder extends
     }
 
     @Override
+    @Deprecated
     public HttpToHttp2ConnectionHandlerBuilder initialHuffmanDecodeCapacity(int initialHuffmanDecodeCapacity) {
         return super.initialHuffmanDecodeCapacity(initialHuffmanDecodeCapacity);
     }
 
+    @Override
+    public HttpToHttp2ConnectionHandlerBuilder decoupleCloseAndGoAway(boolean decoupleCloseAndGoAway) {
+        return super.decoupleCloseAndGoAway(decoupleCloseAndGoAway);
+    }
+
     @Override
     public HttpToHttp2ConnectionHandler build() {
         return super.build();
@@ -92,6 +98,7 @@ public final class HttpToHttp2ConnectionHandlerBuilder extends
     @Override
     protected HttpToHttp2ConnectionHandler build(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder,
                                                  Http2Settings initialSettings) {
-        return new HttpToHttp2ConnectionHandler(decoder, encoder, initialSettings, isValidateHeaders());
+        return new HttpToHttp2ConnectionHandler(decoder, encoder, initialSettings, isValidateHeaders(),
+                decoupleCloseAndGoAway());
     }
 }
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/InboundHttp2ToHttpAdapter.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/InboundHttp2ToHttpAdapter.java
index b5adf25..d8d5768 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/InboundHttp2ToHttpAdapter.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/InboundHttp2ToHttpAdapter.java
@@ -16,7 +16,6 @@ package io.netty.handler.codec.http2;
 
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.ByteBufAllocator;
-import io.netty.buffer.Unpooled;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.handler.codec.http.FullHttpMessage;
 import io.netty.handler.codec.http.FullHttpRequest;
@@ -34,7 +33,7 @@ import static io.netty.util.internal.ObjectUtil.checkNotNull;
 
 /**
  * This adapter provides just header/data events from the HTTP message flow defined
- * here <a href="http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-8.1.">HTTP/2 Spec Message Flow</a>.
+ * in <a href="https://tools.ietf.org/html/rfc7540#section-8.1">[RFC 7540], Section 8.1</a>.
  * <p>
  * See {@link HttpToHttp2ConnectionHandler} to get translation from HTTP/1.x objects to HTTP/2 frames for writes.
  */
@@ -53,9 +52,9 @@ public class InboundHttp2ToHttpAdapter extends Http2EventAdapter {
         }
 
         @Override
-        public FullHttpMessage copyIfNeeded(FullHttpMessage msg) {
+        public FullHttpMessage copyIfNeeded(ByteBufAllocator allocator, FullHttpMessage msg) {
             if (msg instanceof FullHttpRequest) {
-                FullHttpRequest copy = ((FullHttpRequest) msg).replace(Unpooled.buffer(0));
+                FullHttpRequest copy = ((FullHttpRequest) msg).replace(allocator.buffer(0));
                 copy.headers().remove(HttpHeaderNames.EXPECT);
                 return copy;
             }
@@ -73,11 +72,10 @@ public class InboundHttp2ToHttpAdapter extends Http2EventAdapter {
     protected InboundHttp2ToHttpAdapter(Http2Connection connection, int maxContentLength,
                                         boolean validateHttpHeaders, boolean propagateSettings) {
 
-        checkNotNull(connection, "connection");
         if (maxContentLength <= 0) {
             throw new IllegalArgumentException("maxContentLength: " + maxContentLength + " (expected: > 0)");
         }
-        this.connection = connection;
+        this.connection = checkNotNull(connection, "connection");
         this.maxContentLength = maxContentLength;
         this.validateHttpHeaders = validateHttpHeaders;
         this.propagateSettings = propagateSettings;
@@ -200,7 +198,7 @@ public class InboundHttp2ToHttpAdapter extends Http2EventAdapter {
         if (sendDetector.mustSendImmediately(msg)) {
             // Copy the message (if necessary) before sending. The content is not expected to be copied (or used) in
             // this operation but just in case it is used do the copy before sending and the resource may be released
-            final FullHttpMessage copy = endOfStream ? null : sendDetector.copyIfNeeded(msg);
+            final FullHttpMessage copy = endOfStream ? null : sendDetector.copyIfNeeded(ctx.alloc(), msg);
             fireChannelRead(ctx, msg, release, stream);
             return copy;
         }
@@ -354,9 +352,10 @@ public class InboundHttp2ToHttpAdapter extends Http2EventAdapter {
          * with a 'Expect: 100-continue' header. The message will be sent immediately,
          * and the data will be queued and sent at the end of the stream.
          *
+         * @param allocator The {@link ByteBufAllocator} that can be used to allocate
          * @param msg The message which has just been sent due to {@link #mustSendImmediately(FullHttpMessage)}
          * @return A modified copy of the {@code msg} or {@code null} if a copy is not needed.
          */
-        FullHttpMessage copyIfNeeded(FullHttpMessage msg);
+        FullHttpMessage copyIfNeeded(ByteBufAllocator allocator, FullHttpMessage msg);
     }
 }
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/MaxCapacityQueue.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/MaxCapacityQueue.java
new file mode 100644
index 0000000..7f72511
--- /dev/null
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/MaxCapacityQueue.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2019 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 java.util.Collection;
+import java.util.Iterator;
+import java.util.Queue;
+
+final class MaxCapacityQueue<E> implements Queue<E> {
+    private final Queue<E> queue;
+    private final int maxCapacity;
+
+    MaxCapacityQueue(Queue<E> queue, int maxCapacity) {
+        this.queue = queue;
+        this.maxCapacity = maxCapacity;
+    }
+
+    @Override
+    public boolean add(E element) {
+        if (offer(element)) {
+            return true;
+        }
+        throw new IllegalStateException();
+    }
+
+    @Override
+    public boolean offer(E element) {
+        if (maxCapacity <= queue.size()) {
+            return false;
+        }
+        return queue.offer(element);
+    }
+
+    @Override
+    public E remove() {
+        return queue.remove();
+    }
+
+    @Override
+    public E poll() {
+        return queue.poll();
+    }
+
+    @Override
+    public E element() {
+        return queue.element();
+    }
+
+    @Override
+    public E peek() {
+        return queue.peek();
+    }
+
+    @Override
+    public int size() {
+        return queue.size();
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return queue.isEmpty();
+    }
+
+    @Override
+    public boolean contains(Object o) {
+        return queue.contains(o);
+    }
+
+    @Override
+    public Iterator<E> iterator() {
+        return queue.iterator();
+    }
+
+    @Override
+    public Object[] toArray() {
+        return queue.toArray();
+    }
+
+    @Override
+    public <T> T[] toArray(T[] a) {
+        return queue.toArray(a);
+    }
+
+    @Override
+    public boolean remove(Object o) {
+        return queue.remove(o);
+    }
+
+    @Override
+    public boolean containsAll(Collection<?> c) {
+        return queue.containsAll(c);
+    }
+
+    @Override
+    public boolean addAll(Collection<? extends E> c) {
+        if (maxCapacity >= size() + c.size()) {
+            return queue.addAll(c);
+        }
+        throw new IllegalStateException();
+    }
+
+    @Override
+    public boolean removeAll(Collection<?> c) {
+        return queue.removeAll(c);
+    }
+
+    @Override
+    public boolean retainAll(Collection<?> c) {
+        return queue.retainAll(c);
+    }
+
+    @Override
+    public void clear() {
+        queue.clear();
+    }
+}
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/UniformStreamByteDistributor.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/UniformStreamByteDistributor.java
index 421d348..c7e5791 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/UniformStreamByteDistributor.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/UniformStreamByteDistributor.java
@@ -24,6 +24,7 @@ import static io.netty.handler.codec.http2.Http2CodecUtil.streamableBytes;
 import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR;
 import static io.netty.handler.codec.http2.Http2Exception.connectionError;
 import static io.netty.util.internal.ObjectUtil.checkNotNull;
+import static io.netty.util.internal.ObjectUtil.checkPositive;
 import static java.lang.Math.max;
 import static java.lang.Math.min;
 
@@ -72,9 +73,7 @@ public final class UniformStreamByteDistributor implements StreamByteDistributor
      * Must be > 0.
      */
     public void minAllocationChunk(int minAllocationChunk) {
-        if (minAllocationChunk <= 0) {
-            throw new IllegalArgumentException("minAllocationChunk must be > 0");
-        }
+        checkPositive(minAllocationChunk, "minAllocationChunk");
         this.minAllocationChunk = minAllocationChunk;
     }
 
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/WeightedFairQueueByteDistributor.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/WeightedFairQueueByteDistributor.java
index b7c2cdd..b0f3a25 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/WeightedFairQueueByteDistributor.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/WeightedFairQueueByteDistributor.java
@@ -37,6 +37,8 @@ import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_PRIORITY_WEIGH
 import static io.netty.handler.codec.http2.Http2CodecUtil.streamableBytes;
 import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR;
 import static io.netty.handler.codec.http2.Http2Exception.connectionError;
+import static io.netty.util.internal.ObjectUtil.checkPositive;
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
 import static java.lang.Integer.MAX_VALUE;
 import static java.lang.Math.max;
 import static java.lang.Math.min;
@@ -96,9 +98,8 @@ public final class WeightedFairQueueByteDistributor implements StreamByteDistrib
     }
 
     public WeightedFairQueueByteDistributor(Http2Connection connection, int maxStateOnlySize) {
-        if (maxStateOnlySize < 0) {
-            throw new IllegalArgumentException("maxStateOnlySize: " + maxStateOnlySize + " (expected: >0)");
-        } else if (maxStateOnlySize == 0) {
+        checkPositiveOrZero(maxStateOnlySize, "maxStateOnlySize");
+        if (maxStateOnlySize == 0) {
             stateOnlyMap = IntCollections.emptyMap();
             stateOnlyRemovalQueue = EmptyPriorityQueue.instance();
         } else {
@@ -281,9 +282,7 @@ public final class WeightedFairQueueByteDistributor implements StreamByteDistrib
      * @param allocationQuantum the amount of bytes that will be allocated to each stream. Must be &gt; 0.
      */
     public void allocationQuantum(int allocationQuantum) {
-        if (allocationQuantum <= 0) {
-            throw new IllegalArgumentException("allocationQuantum must be > 0");
-        }
+        checkPositive(allocationQuantum, "allocationQuantum");
         this.allocationQuantum = allocationQuantum;
     }
 
diff --git a/codec-http2/src/main/resources/META-INF/native-image/io.netty/codec-http2/native-image.properties b/codec-http2/src/main/resources/META-INF/native-image/io.netty/codec-http2/native-image.properties
new file mode 100644
index 0000000..ed5e345
--- /dev/null
+++ b/codec-http2/src/main/resources/META-INF/native-image/io.netty/codec-http2/native-image.properties
@@ -0,0 +1,16 @@
+# Copyright 2019 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.
+
+Args = --initialize-at-build-time=io.netty \
+       --initialize-at-run-time=io.netty.handler.codec.http2.Http2CodecUtil,io.netty.handler.codec.http2.Http2ClientUpgradeCodec,io.netty.handler.codec.http2.Http2ConnectionHandler,io.netty.handler.codec.http2.DefaultHttp2FrameWriter
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/CleartextHttp2ServerUpgradeHandlerTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/CleartextHttp2ServerUpgradeHandlerTest.java
index a544054..129f62d 100644
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/CleartextHttp2ServerUpgradeHandlerTest.java
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/CleartextHttp2ServerUpgradeHandlerTest.java
@@ -42,8 +42,15 @@ import org.junit.Test;
 import java.util.ArrayList;
 import java.util.List;
 
-import static org.junit.Assert.*;
-import static org.mockito.Mockito.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 
 /**
  * Tests for {@link CleartextHttp2ServerUpgradeHandler}
@@ -112,47 +119,35 @@ public class CleartextHttp2ServerUpgradeHandlerTest {
 
     @Test
     public void upgrade() throws Exception {
-        setUpServerChannel();
-
         String upgradeString = "GET / HTTP/1.1\r\n" +
-                               "Host: example.com\r\n" +
-                               "Connection: Upgrade, HTTP2-Settings\r\n" +
-                               "Upgrade: h2c\r\n" +
-                               "HTTP2-Settings: AAMAAABkAAQAAP__\r\n\r\n";
-        ByteBuf upgrade = Unpooled.copiedBuffer(upgradeString, CharsetUtil.US_ASCII);
-
-        assertFalse(channel.writeInbound(upgrade));
-
-        assertEquals(1, userEvents.size());
-
-        Object userEvent = userEvents.get(0);
-        assertTrue(userEvent instanceof UpgradeEvent);
-        assertEquals("h2c", ((UpgradeEvent) userEvent).protocol());
-        ReferenceCountUtil.release(userEvent);
-
-        assertEquals(100, http2ConnectionHandler.connection().local().maxActiveStreams());
-        assertEquals(65535, http2ConnectionHandler.connection().local().flowController().initialWindowSize());
-
-        assertEquals(1, http2ConnectionHandler.connection().numActiveStreams());
-        assertNotNull(http2ConnectionHandler.connection().stream(1));
-
-        Http2Stream stream = http2ConnectionHandler.connection().stream(1);
-        assertEquals(State.HALF_CLOSED_REMOTE, stream.state());
-        assertFalse(stream.isHeadersSent());
-
-        String expectedHttpResponse = "HTTP/1.1 101 Switching Protocols\r\n" +
-                "connection: upgrade\r\n" +
-                "upgrade: h2c\r\n\r\n";
-        ByteBuf responseBuffer = channel.readOutbound();
-        assertEquals(expectedHttpResponse, responseBuffer.toString(CharsetUtil.UTF_8));
-        responseBuffer.release();
+                "Host: example.com\r\n" +
+                "Connection: Upgrade, HTTP2-Settings\r\n" +
+                "Upgrade: h2c\r\n" +
+                "HTTP2-Settings: AAMAAABkAAQAAP__\r\n\r\n";
+        validateClearTextUpgrade(upgradeString);
+    }
 
-        // Check that the preface was send (a.k.a the settings frame)
-        ByteBuf settingsBuffer = channel.readOutbound();
-        assertNotNull(settingsBuffer);
-        settingsBuffer.release();
+    @Test
+    public void upgradeWithMultipleConnectionHeaders() {
+        String upgradeString = "GET / HTTP/1.1\r\n" +
+                "Host: example.com\r\n" +
+                "Connection: keep-alive\r\n" +
+                "Connection: Upgrade, HTTP2-Settings\r\n" +
+                "Upgrade: h2c\r\n" +
+                "HTTP2-Settings: AAMAAABkAAQAAP__\r\n\r\n";
+        validateClearTextUpgrade(upgradeString);
+    }
 
-        assertNull(channel.readOutbound());
+    @Test
+    public void requiredHeadersInSeparateConnectionHeaders() {
+        String upgradeString = "GET / HTTP/1.1\r\n" +
+                "Host: example.com\r\n" +
+                "Connection: keep-alive\r\n" +
+                "Connection: HTTP2-Settings\r\n" +
+                "Connection: Upgrade\r\n" +
+                "Upgrade: h2c\r\n" +
+                "HTTP2-Settings: AAMAAABkAAQAAP__\r\n\r\n";
+        validateClearTextUpgrade(upgradeString);
     }
 
     @Test
@@ -254,4 +249,43 @@ public class CleartextHttp2ServerUpgradeHandlerTest {
     private static Http2Settings expectedSettings() {
         return new Http2Settings().maxConcurrentStreams(100).initialWindowSize(65535);
     }
+
+    private void validateClearTextUpgrade(String upgradeString) {
+        setUpServerChannel();
+
+        ByteBuf upgrade = Unpooled.copiedBuffer(upgradeString, CharsetUtil.US_ASCII);
+
+        assertFalse(channel.writeInbound(upgrade));
+
+        assertEquals(1, userEvents.size());
+
+        Object userEvent = userEvents.get(0);
+        assertTrue(userEvent instanceof UpgradeEvent);
+        assertEquals("h2c", ((UpgradeEvent) userEvent).protocol());
+        ReferenceCountUtil.release(userEvent);
+
+        assertEquals(100, http2ConnectionHandler.connection().local().maxActiveStreams());
+        assertEquals(65535, http2ConnectionHandler.connection().local().flowController().initialWindowSize());
+
+        assertEquals(1, http2ConnectionHandler.connection().numActiveStreams());
+        assertNotNull(http2ConnectionHandler.connection().stream(1));
+
+        Http2Stream stream = http2ConnectionHandler.connection().stream(1);
+        assertEquals(State.HALF_CLOSED_REMOTE, stream.state());
+        assertFalse(stream.isHeadersSent());
+
+        String expectedHttpResponse = "HTTP/1.1 101 Switching Protocols\r\n" +
+                "connection: upgrade\r\n" +
+                "upgrade: h2c\r\n\r\n";
+        ByteBuf responseBuffer = channel.readOutbound();
+        assertEquals(expectedHttpResponse, responseBuffer.toString(CharsetUtil.UTF_8));
+        responseBuffer.release();
+
+        // Check that the preface was send (a.k.a the settings frame)
+        ByteBuf settingsBuffer = channel.readOutbound();
+        assertNotNull(settingsBuffer);
+        settingsBuffer.release();
+
+        assertNull(channel.readOutbound());
+    }
 }
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DecoratingHttp2ConnectionEncoderTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DecoratingHttp2ConnectionEncoderTest.java
new file mode 100644
index 0000000..0eb9a73
--- /dev/null
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/DecoratingHttp2ConnectionEncoderTest.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2019 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 org.junit.Test;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.times;
+
+public class DecoratingHttp2ConnectionEncoderTest {
+
+    @Test(expected = IllegalStateException.class)
+    public void testConsumeReceivedSettingsThrows() {
+        Http2ConnectionEncoder encoder = mock(Http2ConnectionEncoder.class);
+        DecoratingHttp2ConnectionEncoder decoratingHttp2ConnectionEncoder =
+                new DecoratingHttp2ConnectionEncoder(encoder);
+        decoratingHttp2ConnectionEncoder.consumeReceivedSettings(Http2Settings.defaultSettings());
+    }
+
+    @Test
+    public void testConsumeReceivedSettingsDelegate() {
+        TestHttp2ConnectionEncoder encoder = mock(TestHttp2ConnectionEncoder.class);
+        DecoratingHttp2ConnectionEncoder decoratingHttp2ConnectionEncoder =
+                new DecoratingHttp2ConnectionEncoder(encoder);
+
+        Http2Settings settings = Http2Settings.defaultSettings();
+        decoratingHttp2ConnectionEncoder.consumeReceivedSettings(Http2Settings.defaultSettings());
+        verify(encoder, times(1)).consumeReceivedSettings(eq(settings));
+    }
+
+    private interface TestHttp2ConnectionEncoder extends Http2ConnectionEncoder, Http2SettingsReceivedConsumer { }
+}
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 7e87d52..9eb66bc 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
@@ -42,7 +42,11 @@ 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.util.CharsetUtil.UTF_8;
+
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.not;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.Mockito.any;
@@ -249,9 +253,9 @@ public class DefaultHttp2ConnectionDecoderTest {
         }
     }
 
-    @Test(expected = Http2Exception.class)
+    @Test(expected = Http2Exception.StreamException.class)
     public void dataReadForUnknownStreamShouldApplyFlowControlAndFail() throws Exception {
-        when(connection.streamMayHaveExisted(STREAM_ID)).thenReturn(false);
+        when(connection.streamMayHaveExisted(STREAM_ID)).thenReturn(true);
         when(connection.stream(STREAM_ID)).thenReturn(null);
         final ByteBuf data = dummyData();
         int padding = 10;
@@ -272,6 +276,32 @@ public class DefaultHttp2ConnectionDecoderTest {
         }
     }
 
+    @Test(expected = Http2Exception.class)
+    public void dataReadForUnknownStreamThatCouldntExistFail() throws Exception {
+        when(connection.streamMayHaveExisted(STREAM_ID)).thenReturn(false);
+        when(connection.stream(STREAM_ID)).thenReturn(null);
+        final ByteBuf data = dummyData();
+        int padding = 10;
+        int processedBytes = data.readableBytes() + padding;
+        try {
+            decode().onDataRead(ctx, STREAM_ID, data, padding, true);
+        } catch (Http2Exception ex) {
+            assertThat(ex, not(instanceOf(Http2Exception.StreamException.class)));
+            throw ex;
+        } finally {
+            try {
+                verify(localFlow)
+                    .receiveFlowControlledFrame(eq((Http2Stream) null), eq(data), eq(padding), eq(true));
+                verify(localFlow).consumeBytes(eq((Http2Stream) null), eq(processedBytes));
+                verify(localFlow).frameWriter(any(Http2FrameWriter.class));
+                verifyNoMoreInteractions(localFlow);
+                verify(listener, never()).onDataRead(eq(ctx), anyInt(), any(ByteBuf.class), anyInt(), anyBoolean());
+            } finally {
+                data.release();
+            }
+        }
+    }
+
     @Test
     public void dataReadForUnknownStreamShouldApplyFlowControl() throws Exception {
         when(connection.stream(STREAM_ID)).thenReturn(null);
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionEncoderTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionEncoderTest.java
index 9ca7b1f..aa4ffc3 100644
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionEncoderTest.java
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionEncoderTest.java
@@ -177,12 +177,26 @@ public class DefaultHttp2ConnectionEncoderTest {
                 anyInt(), anyBoolean(), any(ChannelPromise.class)))
                 .then(new Answer<ChannelFuture>() {
                     @Override
-                    public ChannelFuture answer(InvocationOnMock invocationOnMock) throws Throwable {
-                        ChannelPromise promise = (ChannelPromise) invocationOnMock.getArguments()[8];
+                    public ChannelFuture answer(InvocationOnMock invocationOnMock) {
+                        ChannelPromise promise = invocationOnMock.getArgument(8);
                         if (streamClosed) {
                             fail("Stream already closed");
                         } else {
-                            streamClosed = (Boolean) invocationOnMock.getArguments()[5];
+                            streamClosed = invocationOnMock.getArgument(5);
+                        }
+                        return promise.setSuccess();
+                    }
+                });
+        when(writer.writeHeaders(eq(ctx), anyInt(), any(Http2Headers.class),
+                anyInt(), anyBoolean(), any(ChannelPromise.class)))
+                .then(new Answer<ChannelFuture>() {
+                    @Override
+                    public ChannelFuture answer(InvocationOnMock invocationOnMock) {
+                        ChannelPromise promise = invocationOnMock.getArgument(5);
+                        if (streamClosed) {
+                            fail("Stream already closed");
+                        } else {
+                            streamClosed = invocationOnMock.getArgument(4);
                         }
                         return promise.setSuccess();
                     }
@@ -335,12 +349,12 @@ public class DefaultHttp2ConnectionEncoderTest {
     @Test
     public void writeHeadersUsingVoidPromise() throws Exception {
         final Throwable cause = new RuntimeException("fake exception");
-        when(writer.writeHeaders(eq(ctx), eq(STREAM_ID), any(Http2Headers.class), anyInt(), anyShort(), anyBoolean(),
+        when(writer.writeHeaders(eq(ctx), eq(STREAM_ID), any(Http2Headers.class),
                                  anyInt(), anyBoolean(), any(ChannelPromise.class)))
                 .then(new Answer<ChannelFuture>() {
                     @Override
                     public ChannelFuture answer(InvocationOnMock invocationOnMock) throws Throwable {
-                        ChannelPromise promise = invocationOnMock.getArgument(8);
+                        ChannelPromise promise = invocationOnMock.getArgument(5);
                         assertFalse(promise.isVoid());
                         return promise.setFailure(cause);
                     }
@@ -349,7 +363,7 @@ public class DefaultHttp2ConnectionEncoderTest {
         // END_STREAM flag, so that a listener is added to the future.
         encoder.writeHeaders(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, true, newVoidPromise(channel));
 
-        verify(writer).writeHeaders(eq(ctx), eq(STREAM_ID), any(Http2Headers.class), anyInt(), anyShort(), anyBoolean(),
+        verify(writer).writeHeaders(eq(ctx), eq(STREAM_ID), any(Http2Headers.class),
                                     anyInt(), anyBoolean(), any(ChannelPromise.class));
         // When using a void promise, the error should be propagated via the channel pipeline.
         verify(pipeline).fireExceptionCaught(cause);
@@ -377,7 +391,7 @@ public class DefaultHttp2ConnectionEncoderTest {
         ChannelPromise promise = newPromise();
         encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, false, promise);
         verify(writer).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0),
-                eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise));
+                eq(false), eq(promise));
         assertTrue(promise.isSuccess());
     }
 
@@ -390,8 +404,8 @@ public class DefaultHttp2ConnectionEncoderTest {
         ChannelPromise promise = newPromise();
         encoder.writeHeaders(ctx, PUSH_STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, false, promise);
         assertEquals(HALF_CLOSED_REMOTE, stream(PUSH_STREAM_ID).state());
-        verify(writer).writeHeaders(eq(ctx), eq(PUSH_STREAM_ID), eq(EmptyHttp2Headers.INSTANCE), eq(0),
-                eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise));
+        verify(writer).writeHeaders(eq(ctx), eq(PUSH_STREAM_ID), eq(EmptyHttp2Headers.INSTANCE),
+                eq(0), eq(false), eq(promise));
     }
 
     @Test
@@ -406,8 +420,8 @@ public class DefaultHttp2ConnectionEncoderTest {
         assertTrue(future.isDone());
         assertFalse(future.isSuccess());
 
-        verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0),
-                eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise));
+        verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE),
+                eq(0), eq(false), eq(promise));
     }
 
     @Test
@@ -425,8 +439,8 @@ public class DefaultHttp2ConnectionEncoderTest {
         assertTrue(future.isDone());
         assertFalse(future.isSuccess());
 
-        verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0),
-                eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise));
+        verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE),
+                eq(0), eq(false), eq(promise));
     }
 
     @Test
@@ -452,10 +466,10 @@ public class DefaultHttp2ConnectionEncoderTest {
         assertTrue(future.isDone());
         assertFalse(future.isSuccess());
 
-        verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0),
-                eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise));
-        verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0),
-                eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(true), eq(promise2));
+        verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE),
+                eq(0), eq(false), eq(promise));
+        verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE),
+                eq(0), eq(true), eq(promise2));
     }
 
     @Test
@@ -493,13 +507,13 @@ public class DefaultHttp2ConnectionEncoderTest {
         assertTrue(future.isDone());
         assertEquals(eos, future.isSuccess());
 
-        verify(writer, times(infoHeaderCount)).writeHeaders(eq(ctx), eq(streamId), eq(infoHeaders), eq(0),
-                eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), any(ChannelPromise.class));
-        verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0),
-                eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise2));
+        verify(writer, times(infoHeaderCount)).writeHeaders(eq(ctx), eq(streamId), eq(infoHeaders),
+                eq(0), eq(false), any(ChannelPromise.class));
+        verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE),
+                eq(0), eq(false), eq(promise2));
         if (eos) {
-            verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0),
-                    eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(true), eq(promise3));
+            verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE),
+                    eq(0), eq(true), eq(promise3));
         }
     }
 
@@ -536,10 +550,10 @@ public class DefaultHttp2ConnectionEncoderTest {
         assertTrue(future.isDone());
         assertFalse(future.isSuccess());
 
-        verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0),
-                eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise));
-        verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0),
-                eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(true), eq(promise2));
+        verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE),
+                eq(0), eq(false), eq(promise));
+        verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE),
+                eq(0), eq(true), eq(promise2));
     }
 
     @Test
@@ -581,13 +595,13 @@ public class DefaultHttp2ConnectionEncoderTest {
         assertTrue(future.isDone());
         assertEquals(eos, future.isSuccess());
 
-        verify(writer, times(infoHeaderCount)).writeHeaders(eq(ctx), eq(streamId), eq(infoHeaders), eq(0),
-                eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), any(ChannelPromise.class));
-        verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0),
-                eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise2));
+        verify(writer, times(infoHeaderCount)).writeHeaders(eq(ctx), eq(streamId), eq(infoHeaders),
+                eq(0), eq(false), any(ChannelPromise.class));
+        verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE),
+                eq(0), eq(false), eq(promise2));
         if (eos) {
-            verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0),
-                    eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(true), eq(promise3));
+            verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE),
+                    eq(0), eq(true), eq(promise3));
         }
     }
 
@@ -752,8 +766,7 @@ public class DefaultHttp2ConnectionEncoderTest {
         final ChannelPromise promise = newPromise();
         final Throwable ex = new RuntimeException();
         // Fake an encoding error, like HPACK's HeaderListSizeException
-        when(writer.writeHeaders(eq(ctx), eq(STREAM_ID), eq(EmptyHttp2Headers.INSTANCE), eq(0),
-                eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(true), eq(promise)))
+        when(writer.writeHeaders(eq(ctx), eq(STREAM_ID), eq(EmptyHttp2Headers.INSTANCE), eq(0), eq(true), eq(promise)))
             .thenAnswer(new Answer<ChannelFuture>() {
                 @Override
                 public ChannelFuture answer(InvocationOnMock invocation) {
@@ -779,8 +792,7 @@ public class DefaultHttp2ConnectionEncoderTest {
         final ChannelPromise promise = newPromise();
         final Throwable ex = new RuntimeException();
         // Fake an encoding error, like HPACK's HeaderListSizeException
-        when(writer.writeHeaders(eq(ctx), eq(STREAM_ID), eq(EmptyHttp2Headers.INSTANCE), eq(0),
-            eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(true), eq(promise)))
+        when(writer.writeHeaders(eq(ctx), eq(STREAM_ID), eq(EmptyHttp2Headers.INSTANCE), eq(0), eq(true), eq(promise)))
             .thenAnswer(new Answer<ChannelFuture>() {
                 @Override
                 public ChannelFuture answer(InvocationOnMock invocation) {
@@ -849,8 +861,8 @@ public class DefaultHttp2ConnectionEncoderTest {
         goAwaySent(0);
         ChannelPromise promise = newPromise();
         encoder.writeHeaders(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, false, promise);
-        verify(writer).writeHeaders(eq(ctx), eq(STREAM_ID), eq(EmptyHttp2Headers.INSTANCE), eq(0),
-                eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise));
+        verify(writer).writeHeaders(eq(ctx), eq(STREAM_ID), eq(EmptyHttp2Headers.INSTANCE),
+                eq(0), eq(false), eq(promise));
     }
 
     @Test
@@ -868,8 +880,29 @@ public class DefaultHttp2ConnectionEncoderTest {
         goAwayReceived(STREAM_ID);
         ChannelPromise promise = newPromise();
         encoder.writeHeaders(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, false, promise);
-        verify(writer).writeHeaders(eq(ctx), eq(STREAM_ID), eq(EmptyHttp2Headers.INSTANCE), eq(0),
-                eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise));
+        verify(writer).writeHeaders(eq(ctx), eq(STREAM_ID), eq(EmptyHttp2Headers.INSTANCE),
+                eq(0), eq(false), eq(promise));
+    }
+
+    @Test
+    public void headersWithNoPriority() {
+        writeAllFlowControlledFrames();
+        final int streamId = 6;
+        ChannelPromise promise = newPromise();
+        encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, false, promise);
+        verify(writer).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE),
+                eq(0), eq(false), eq(promise));
+    }
+
+    @Test
+    public void headersWithPriority() {
+        writeAllFlowControlledFrames();
+        final int streamId = 6;
+        ChannelPromise promise = newPromise();
+        encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 10, DEFAULT_PRIORITY_WEIGHT,
+                true, 1, false, promise);
+        verify(writer).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(10),
+                eq(DEFAULT_PRIORITY_WEIGHT), eq(true), eq(1), eq(false), eq(promise));
     }
 
     private void writeAllFlowControlledFrames() {
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionTest.java
index 69183e0..b43c000 100644
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionTest.java
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2ConnectionTest.java
@@ -624,7 +624,7 @@ public class DefaultHttp2ConnectionTest {
         private final boolean[] array;
         private final int index;
 
-        public ListenerExceptionThrower(boolean[] array, int index) {
+        ListenerExceptionThrower(boolean[] array, int index) {
             this.array = array;
             this.index = index;
         }
@@ -640,7 +640,7 @@ public class DefaultHttp2ConnectionTest {
         private final boolean[] array;
         private final int index;
 
-        public ListenerVerifyCallAnswer(boolean[] array, int index) {
+        ListenerVerifyCallAnswer(boolean[] array, int index) {
             this.array = array;
             this.index = index;
         }
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2FrameWriterTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2FrameWriterTest.java
index b5a40d6..6d53c40 100644
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2FrameWriterTest.java
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2FrameWriterTest.java
@@ -15,6 +15,7 @@
 package io.netty.handler.codec.http2;
 
 import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
 import io.netty.buffer.Unpooled;
 import io.netty.buffer.UnpooledByteBufAllocator;
 import io.netty.channel.Channel;
@@ -37,7 +38,6 @@ import java.io.IOException;
 import java.util.Arrays;
 
 import static org.junit.Assert.*;
-import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.*;
 
 /**
@@ -66,8 +66,11 @@ public class DefaultHttp2FrameWriterTest {
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
+        http2HeadersEncoder = new DefaultHttp2HeadersEncoder(
+                Http2HeadersEncoder.NEVER_SENSITIVE, new HpackEncoder(false, 16, 0));
 
-        frameWriter = new DefaultHttp2FrameWriter();
+        frameWriter = new DefaultHttp2FrameWriter(new DefaultHttp2HeadersEncoder(
+                Http2HeadersEncoder.NEVER_SENSITIVE, new HpackEncoder(false, 16, 0)));
 
         outbound = Unpooled.buffer();
 
@@ -75,8 +78,6 @@ public class DefaultHttp2FrameWriterTest {
 
         promise = new DefaultChannelPromise(channel, ImmediateEventExecutor.INSTANCE);
 
-        http2HeadersEncoder = new DefaultHttp2HeadersEncoder();
-
         Answer<Object> answer = new Answer<Object>() {
             @Override
             public Object answer(InvocationOnMock var1) throws Throwable {
@@ -204,6 +205,48 @@ public class DefaultHttp2FrameWriterTest {
                           secondPayload);
     }
 
+    @Test
+    public void writeLargeHeaderWithPadding() throws Exception {
+        int streamId = 1;
+        Http2Headers headers = new DefaultHttp2Headers()
+                .method("GET").path("/").authority("foo.com").scheme("https");
+        headers = dummyHeaders(headers, 20);
+
+        http2HeadersEncoder.configuration().maxHeaderListSize(Integer.MAX_VALUE);
+        frameWriter.headersConfiguration().maxHeaderListSize(Integer.MAX_VALUE);
+        frameWriter.maxFrameSize(Http2CodecUtil.MAX_FRAME_SIZE_LOWER_BOUND);
+        frameWriter.writeHeaders(ctx, streamId, headers, 5, true, promise);
+
+        byte[] expectedPayload = buildLargeHeaderPayload(streamId, headers, (byte) 4,
+                Http2CodecUtil.MAX_FRAME_SIZE_LOWER_BOUND);
+
+        // First frame: HEADER(length=0x4000, flags=0x09)
+        assertEquals(Http2CodecUtil.MAX_FRAME_SIZE_LOWER_BOUND,
+                outbound.readUnsignedMedium());
+        assertEquals(0x01, outbound.readByte());
+        assertEquals(0x09, outbound.readByte()); // 0x01 + 0x08
+        assertEquals(streamId, outbound.readInt());
+
+        byte[] firstPayload = new byte[Http2CodecUtil.MAX_FRAME_SIZE_LOWER_BOUND];
+        outbound.readBytes(firstPayload);
+
+        int remainPayloadLength = expectedPayload.length - Http2CodecUtil.MAX_FRAME_SIZE_LOWER_BOUND;
+        // Second frame: CONTINUATION(length=remainPayloadLength, flags=0x04)
+        assertEquals(remainPayloadLength, outbound.readUnsignedMedium());
+        assertEquals(0x09, outbound.readByte());
+        assertEquals(0x04, outbound.readByte());
+        assertEquals(streamId, outbound.readInt());
+
+        byte[] secondPayload = new byte[remainPayloadLength];
+        outbound.readBytes(secondPayload);
+
+        assertArrayEquals(Arrays.copyOfRange(expectedPayload, 0, firstPayload.length),
+                firstPayload);
+        assertArrayEquals(Arrays.copyOfRange(expectedPayload, firstPayload.length,
+                expectedPayload.length),
+                secondPayload);
+    }
+
     @Test
     public void writeFrameZeroPayload() throws Exception {
         frameWriter.writeFrame(ctx, (byte) 0xf, 0, new Http2Flags(), Unpooled.EMPTY_BUFFER, promise);
@@ -265,6 +308,22 @@ public class DefaultHttp2FrameWriterTest {
         }
     }
 
+    private byte[] buildLargeHeaderPayload(int streamId, Http2Headers headers, byte padding, int maxFrameSize)
+            throws Http2Exception, IOException {
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        try {
+            outputStream.write(padding);
+            byte[] payload = headerPayload(streamId, headers);
+            int firstPayloadSize = maxFrameSize - (padding + 1); //1 for padding length
+            outputStream.write(payload, 0, firstPayloadSize);
+            outputStream.write(new byte[padding]);
+            outputStream.write(payload, firstPayloadSize, payload.length - firstPayloadSize);
+            return outputStream.toByteArray();
+        } finally {
+            outputStream.close();
+        }
+    }
+
     private static Http2Headers dummyHeaders(Http2Headers headers, int times) {
         final String largeValue = repeat("dummy-value", 100);
         for (int i = 0; i < times; i++) {
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2LocalFlowControllerTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2LocalFlowControllerTest.java
index 6a3b80a..cea7793 100644
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2LocalFlowControllerTest.java
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2LocalFlowControllerTest.java
@@ -20,12 +20,14 @@ import static io.netty.handler.codec.http2.Http2CodecUtil.CONNECTION_STREAM_ID;
 import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_WINDOW_SIZE;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
@@ -34,12 +36,15 @@ import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelPromise;
+import io.netty.handler.codec.http2.Http2Stream.State;
 import io.netty.util.concurrent.EventExecutor;
 import junit.framework.AssertionFailedError;
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
 
 /**
  * Tests for {@link DefaultHttp2LocalFlowController}.
@@ -66,15 +71,28 @@ public class DefaultHttp2LocalFlowControllerTest {
     @Before
     public void setup() throws Http2Exception {
         MockitoAnnotations.initMocks(this);
-
-        when(ctx.newPromise()).thenReturn(promise);
-        when(ctx.flush()).thenThrow(new AssertionFailedError("forbidden"));
-        when(ctx.executor()).thenReturn(executor);
+        setupChannelHandlerContext(false);
         when(executor.inEventLoop()).thenReturn(true);
 
         initController(false);
     }
 
+    private void setupChannelHandlerContext(boolean allowFlush) {
+        reset(ctx);
+        when(ctx.newPromise()).thenReturn(promise);
+        if (allowFlush) {
+            when(ctx.flush()).then(new Answer<ChannelHandlerContext>() {
+                @Override
+                public ChannelHandlerContext answer(InvocationOnMock invocationOnMock) {
+                    return ctx;
+                }
+            });
+        } else {
+            when(ctx.flush()).thenThrow(new AssertionFailedError("forbidden"));
+        }
+        when(ctx.executor()).thenReturn(executor);
+    }
+
     @Test
     public void dataFrameShouldBeAccepted() throws Http2Exception {
         receiveFlowControlledFrame(STREAM_ID, 10, 0, false);
@@ -138,6 +156,46 @@ public class DefaultHttp2LocalFlowControllerTest {
         verifyWindowUpdateNotSent(STREAM_ID);
     }
 
+    @Test
+    public void windowUpdateShouldNotBeSentAfterStreamIsClosedForUnconsumedBytes() throws Http2Exception {
+        int dataSize = (int) (DEFAULT_WINDOW_SIZE * DEFAULT_WINDOW_UPDATE_RATIO) + 1;
+
+        // Don't set end-of-stream on the frame as we want to verify that we not return the unconsumed bytes in this
+        // case once the stream was closed,
+        receiveFlowControlledFrame(STREAM_ID, dataSize, 0, false);
+        verifyWindowUpdateNotSent(CONNECTION_STREAM_ID);
+        verifyWindowUpdateNotSent(STREAM_ID);
+
+        // Close the stream
+        Http2Stream stream = connection.stream(STREAM_ID);
+        stream.close();
+        assertEquals(State.CLOSED, stream.state());
+        assertNull(connection.stream(STREAM_ID));
+
+        // The window update for the connection should made it through but not the update for the already closed
+        // stream
+        verifyWindowUpdateSent(CONNECTION_STREAM_ID, dataSize);
+        verifyWindowUpdateNotSent(STREAM_ID);
+    }
+
+    @Test
+    public void windowUpdateShouldBeWrittenWhenStreamIsClosedAndFlushed() throws Http2Exception {
+        int dataSize = (int) (DEFAULT_WINDOW_SIZE * DEFAULT_WINDOW_UPDATE_RATIO) + 1;
+
+        setupChannelHandlerContext(true);
+
+        receiveFlowControlledFrame(STREAM_ID, dataSize, 0, false);
+        verifyWindowUpdateNotSent(CONNECTION_STREAM_ID);
+        verifyWindowUpdateNotSent(STREAM_ID);
+
+        connection.stream(STREAM_ID).close();
+
+        verifyWindowUpdateSent(CONNECTION_STREAM_ID, dataSize);
+
+        // Verify we saw one flush.
+        verify(ctx).flush();
+    }
+
     @Test
     public void halfWindowRemainingShouldUpdateAllWindows() throws Http2Exception {
         int dataSize = (int) (DEFAULT_WINDOW_SIZE * DEFAULT_WINDOW_UPDATE_RATIO) + 1;
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 994fef6..7b26d90 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
@@ -79,7 +79,7 @@ public class HpackDecoderTest {
 
     @Before
     public void setUp() {
-        hpackDecoder = new HpackDecoder(8192, 32);
+        hpackDecoder = new HpackDecoder(8192);
         mockHeaders = mock(Http2Headers.class);
     }
 
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackEncoderTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackEncoderTest.java
index de049a6..b650035 100644
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackEncoderTest.java
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackEncoderTest.java
@@ -33,7 +33,7 @@ public class HpackEncoderTest {
     @Before
     public void setUp() {
         hpackEncoder = new HpackEncoder();
-        hpackDecoder = new HpackDecoder(DEFAULT_HEADER_LIST_SIZE, 32);
+        hpackDecoder = new HpackDecoder(DEFAULT_HEADER_LIST_SIZE);
         mockHeaders = mock(Http2Headers.class);
     }
 
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackHuffmanTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackHuffmanTest.java
index 06e540f..f056436 100644
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackHuffmanTest.java
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackHuffmanTest.java
@@ -61,56 +61,56 @@ public class HpackHuffmanTest {
         for (int i = 0; i < 4; i++) {
             buf[i] = (byte) 0xFF;
         }
-        decode(newHuffmanDecoder(), buf);
+        decode(buf);
     }
 
     @Test(expected = Http2Exception.class)
     public void testDecodeIllegalPadding() throws Http2Exception {
         byte[] buf = new byte[1];
         buf[0] = 0x00; // '0', invalid padding
-        decode(newHuffmanDecoder(), buf);
+        decode(buf);
     }
 
     @Test(expected = Http2Exception.class)
     public void testDecodeExtraPadding() throws Http2Exception {
         byte[] buf = makeBuf(0x0f, 0xFF); // '1', 'EOS'
-        decode(newHuffmanDecoder(), buf);
+        decode(buf);
     }
 
     @Test(expected = Http2Exception.class)
     public void testDecodeExtraPadding1byte() throws Http2Exception {
         byte[] buf = makeBuf(0xFF);
-        decode(newHuffmanDecoder(), buf);
+        decode(buf);
     }
 
     @Test(expected = Http2Exception.class)
     public void testDecodeExtraPadding2byte() throws Http2Exception {
         byte[] buf = makeBuf(0x1F, 0xFF); // 'a'
-        decode(newHuffmanDecoder(), buf);
+        decode(buf);
     }
 
     @Test(expected = Http2Exception.class)
     public void testDecodeExtraPadding3byte() throws Http2Exception {
         byte[] buf = makeBuf(0x1F, 0xFF, 0xFF); // 'a'
-        decode(newHuffmanDecoder(), buf);
+        decode(buf);
     }
 
     @Test(expected = Http2Exception.class)
     public void testDecodeExtraPadding4byte() throws Http2Exception {
         byte[] buf = makeBuf(0x1F, 0xFF, 0xFF, 0xFF); // 'a'
-        decode(newHuffmanDecoder(), buf);
+        decode(buf);
     }
 
     @Test(expected = Http2Exception.class)
     public void testDecodeExtraPadding29bit() throws Http2Exception {
         byte[] buf = makeBuf(0xFF, 0x9F, 0xFF, 0xFF, 0xFF);  // '|'
-        decode(newHuffmanDecoder(), buf);
+        decode(buf);
     }
 
     @Test(expected = Http2Exception.class)
     public void testDecodePartialSymbol() throws Http2Exception {
         byte[] buf = makeBuf(0x52, 0xBC, 0x30, 0xFF, 0xFF, 0xFF, 0xFF); // " pFA\x00", 31 bits of padding, a.k.a. EOS
-        decode(newHuffmanDecoder(), buf);
+        decode(buf);
     }
 
     private static byte[] makeBuf(int ... bytes) {
@@ -122,19 +122,19 @@ public class HpackHuffmanTest {
     }
 
     private static void roundTrip(String s) throws Http2Exception {
-        roundTrip(new HpackHuffmanEncoder(), newHuffmanDecoder(), s);
+        roundTrip(new HpackHuffmanEncoder(), s);
     }
 
-    private static void roundTrip(HpackHuffmanEncoder encoder, HpackHuffmanDecoder decoder, String s)
+    private static void roundTrip(HpackHuffmanEncoder encoder, String s)
             throws Http2Exception {
-        roundTrip(encoder, decoder, s.getBytes());
+        roundTrip(encoder, s.getBytes());
     }
 
     private static void roundTrip(byte[] buf) throws Http2Exception {
-        roundTrip(new HpackHuffmanEncoder(), newHuffmanDecoder(), buf);
+        roundTrip(new HpackHuffmanEncoder(), buf);
     }
 
-    private static void roundTrip(HpackHuffmanEncoder encoder, HpackHuffmanDecoder decoder, byte[] buf)
+    private static void roundTrip(HpackHuffmanEncoder encoder, byte[] buf)
             throws Http2Exception {
         ByteBuf buffer = Unpooled.buffer();
         try {
@@ -142,7 +142,7 @@ public class HpackHuffmanTest {
             byte[] bytes = new byte[buffer.readableBytes()];
             buffer.readBytes(bytes);
 
-            byte[] actualBytes = decode(decoder, bytes);
+            byte[] actualBytes = decode(bytes);
 
             Assert.assertTrue(Arrays.equals(buf, actualBytes));
         } finally {
@@ -150,18 +150,14 @@ public class HpackHuffmanTest {
         }
     }
 
-    private static byte[] decode(HpackHuffmanDecoder decoder, byte[] bytes) throws Http2Exception {
+    private static byte[] decode(byte[] bytes) throws Http2Exception {
         ByteBuf buffer = Unpooled.wrappedBuffer(bytes);
         try {
-            AsciiString decoded = decoder.decode(buffer, buffer.readableBytes());
+            AsciiString decoded = new HpackHuffmanDecoder().decode(buffer, buffer.readableBytes());
             Assert.assertFalse(buffer.isReadable());
             return decoded.toByteArray();
         } finally {
             buffer.release();
         }
     }
-
-    private static HpackHuffmanDecoder newHuffmanDecoder() {
-        return new HpackHuffmanDecoder(32);
-    }
 }
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackTest.java
index fe9fa32..5a7d45f 100644
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackTest.java
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackTest.java
@@ -31,6 +31,7 @@
  */
 package io.netty.handler.codec.http2;
 
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.ResourcesUtil;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -57,9 +58,7 @@ public class HpackTest {
     @Parameters(name = "{0}")
     public static Collection<Object[]> data() {
         File[] files = ResourcesUtil.getFile(HpackTest.class, TEST_DIR).listFiles();
-        if (files == null) {
-            throw new NullPointerException("files");
-        }
+        ObjectUtil.checkNotNull(files, "files");
 
         ArrayList<Object[]> data = new ArrayList<Object[]>();
         for (File file : files) {
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackTestCase.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackTestCase.java
index 33faa50..44c8231 100644
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackTestCase.java
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackTestCase.java
@@ -102,7 +102,7 @@ final class HpackTestCase {
 
             List<HpackHeaderField> expectedDynamicTable = headerBlock.getDynamicTable();
 
-            if (!expectedDynamicTable.equals(actualDynamicTable)) {
+            if (!headersEqual(expectedDynamicTable, actualDynamicTable)) {
                 throw new AssertionError(
                         "\nEXPECTED DYNAMIC TABLE:\n" + expectedDynamicTable +
                                 "\nACTUAL DYNAMIC TABLE:\n" + actualDynamicTable);
@@ -128,7 +128,7 @@ final class HpackTestCase {
                 expectedHeaders.add(new HpackHeaderField(h.name, h.value));
             }
 
-            if (!expectedHeaders.equals(actualHeaders)) {
+            if (!headersEqual(expectedHeaders, actualHeaders)) {
                 throw new AssertionError(
                         "\nEXPECTED:\n" + expectedHeaders +
                                 "\nACTUAL:\n" + actualHeaders);
@@ -141,7 +141,7 @@ final class HpackTestCase {
 
             List<HpackHeaderField> expectedDynamicTable = headerBlock.getDynamicTable();
 
-            if (!expectedDynamicTable.equals(actualDynamicTable)) {
+            if (!headersEqual(expectedDynamicTable, actualDynamicTable)) {
                 throw new AssertionError(
                         "\nEXPECTED DYNAMIC TABLE:\n" + expectedDynamicTable +
                                 "\nACTUAL DYNAMIC TABLE:\n" + actualDynamicTable);
@@ -174,7 +174,7 @@ final class HpackTestCase {
             maxHeaderTableSize = Integer.MAX_VALUE;
         }
 
-        return new HpackDecoder(DEFAULT_HEADER_LIST_SIZE, 32, maxHeaderTableSize);
+        return new HpackDecoder(DEFAULT_HEADER_LIST_SIZE, maxHeaderTableSize);
     }
 
     private static byte[] encode(HpackEncoder hpackEncoder, List<HpackHeaderField> headers, int maxHeaderTableSize,
@@ -229,6 +229,18 @@ final class HpackTestCase {
         return ret.toString();
     }
 
+    private static boolean headersEqual(List<HpackHeaderField> expected, List<HpackHeaderField> actual) {
+        if (expected.size() != actual.size()) {
+            return false;
+        }
+        for (int i = 0; i < expected.size(); i++) {
+            if (!expected.get(i).equalsForTest(actual.get(i))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
     static class HeaderBlock {
         private int maxHeaderTableSize = -1;
         private byte[] encodedBytes;
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ClientUpgradeCodecTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ClientUpgradeCodecTest.java
index fcfdb4b..f0b4a58 100644
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ClientUpgradeCodecTest.java
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ClientUpgradeCodecTest.java
@@ -14,11 +14,9 @@
  */
 package io.netty.handler.codec.http2;
 
-import io.netty.channel.Channel;
 import io.netty.channel.ChannelHandler;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelInboundHandlerAdapter;
-import io.netty.channel.ChannelInitializer;
 import io.netty.channel.embedded.EmbeddedChannel;
 import io.netty.handler.codec.http.DefaultFullHttpRequest;
 import io.netty.handler.codec.http.FullHttpRequest;
@@ -35,26 +33,42 @@ public class Http2ClientUpgradeCodecTest {
 
     @Test
     public void testUpgradeToHttp2ConnectionHandler() throws Exception {
-        testUpgrade(new Http2ConnectionHandlerBuilder().server(false).frameListener(new Http2FrameAdapter()).build());
+        testUpgrade(new Http2ConnectionHandlerBuilder().server(false).frameListener(
+            new Http2FrameAdapter()).build(), null);
     }
 
     @Test
     public void testUpgradeToHttp2FrameCodec() throws Exception {
-        testUpgrade(Http2FrameCodecBuilder.forClient().build());
+        testUpgrade(Http2FrameCodecBuilder.forClient().build(), null);
     }
 
     @Test
     public void testUpgradeToHttp2MultiplexCodec() throws Exception {
         testUpgrade(Http2MultiplexCodecBuilder.forClient(new HttpInboundHandler())
-            .withUpgradeStreamHandler(new ChannelInboundHandlerAdapter()).build());
+            .withUpgradeStreamHandler(new ChannelInboundHandlerAdapter()).build(), null);
     }
 
-    private static void testUpgrade(Http2ConnectionHandler handler) throws Exception {
+    @Test
+    public void testUpgradeToHttp2FrameCodecWithMultiplexer() throws Exception {
+        testUpgrade(Http2FrameCodecBuilder.forClient().build(),
+            new Http2MultiplexHandler(new HttpInboundHandler(), new HttpInboundHandler()));
+    }
+
+    private static void testUpgrade(Http2ConnectionHandler handler, Http2MultiplexHandler multiplexer)
+            throws Exception {
         FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.OPTIONS, "*");
 
         EmbeddedChannel channel = new EmbeddedChannel(new ChannelInboundHandlerAdapter());
         ChannelHandlerContext ctx = channel.pipeline().firstContext();
-        Http2ClientUpgradeCodec codec = new Http2ClientUpgradeCodec("connectionHandler", handler);
+
+        Http2ClientUpgradeCodec codec;
+
+        if (multiplexer == null) {
+            codec = new Http2ClientUpgradeCodec("connectionHandler", handler);
+        } else {
+            codec = new Http2ClientUpgradeCodec("connectionHandler", handler, multiplexer);
+        }
+
         codec.setUpgradeHeaders(ctx, request);
         // Flush the channel to ensure we write out all buffered data
         channel.flush();
@@ -62,6 +76,10 @@ public class Http2ClientUpgradeCodecTest {
         codec.upgradeTo(ctx, null);
         assertNotNull(channel.pipeline().get("connectionHandler"));
 
+        if (multiplexer != null) {
+            assertNotNull(channel.pipeline().get(Http2MultiplexHandler.class));
+        }
+
         assertTrue(channel.finishAndReleaseAll());
     }
 
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ConnectionHandlerTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ConnectionHandlerTest.java
index 9b8c624..0143edc 100644
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ConnectionHandlerTest.java
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ConnectionHandlerTest.java
@@ -29,6 +29,7 @@ import io.netty.channel.DefaultChannelConfig;
 import io.netty.channel.DefaultChannelPromise;
 import io.netty.handler.codec.http.HttpResponseStatus;
 import io.netty.handler.codec.http2.Http2CodecUtil.SimpleChannelPromiseAggregator;
+import io.netty.handler.codec.http2.Http2Exception.ShutdownHint;
 import io.netty.util.ReferenceCountUtil;
 import io.netty.util.concurrent.EventExecutor;
 import io.netty.util.concurrent.GenericFutureListener;
@@ -40,6 +41,7 @@ import org.junit.Test;
 import org.mockito.ArgumentCaptor;
 import org.mockito.ArgumentMatchers;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.stubbing.Answer;
@@ -48,9 +50,11 @@ import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
 
 import static io.netty.buffer.Unpooled.copiedBuffer;
 import static io.netty.handler.codec.http2.Http2CodecUtil.connectionPrefaceBuf;
+import static io.netty.handler.codec.http2.Http2Error.CANCEL;
 import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
 import static io.netty.handler.codec.http2.Http2Error.STREAM_CLOSED;
 import static io.netty.handler.codec.http2.Http2Stream.State.CLOSED;
@@ -721,13 +725,42 @@ public class Http2ConnectionHandlerTest {
         writeRstStreamUsingVoidPromise(STREAM_ID);
     }
 
+    @Test
+    public void gracefulShutdownTimeoutWhenConnectionErrorHardShutdownTest() throws Exception {
+        gracefulShutdownTimeoutWhenConnectionErrorTest0(ShutdownHint.HARD_SHUTDOWN);
+    }
+
+    @Test
+    public void gracefulShutdownTimeoutWhenConnectionErrorGracefulShutdownTest() throws Exception {
+        gracefulShutdownTimeoutWhenConnectionErrorTest0(ShutdownHint.GRACEFUL_SHUTDOWN);
+    }
+
+    private void gracefulShutdownTimeoutWhenConnectionErrorTest0(ShutdownHint hint) throws Exception {
+        handler = newHandler();
+        final long expectedMillis = 1234;
+        handler.gracefulShutdownTimeoutMillis(expectedMillis);
+        Http2Exception exception = new Http2Exception(PROTOCOL_ERROR, "Test error", hint);
+        handler.onConnectionError(ctx, false, exception, exception);
+        verify(executor, atLeastOnce()).schedule(any(Runnable.class), eq(expectedMillis), eq(TimeUnit.MILLISECONDS));
+    }
+
     @Test
     public void gracefulShutdownTimeoutTest() throws Exception {
         handler = newHandler();
         final long expectedMillis = 1234;
         handler.gracefulShutdownTimeoutMillis(expectedMillis);
         handler.close(ctx, promise);
-        verify(executor).schedule(any(Runnable.class), eq(expectedMillis), eq(TimeUnit.MILLISECONDS));
+        verify(executor, atLeastOnce()).schedule(any(Runnable.class), eq(expectedMillis), eq(TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void gracefulShutdownTimeoutNoActiveStreams() throws Exception {
+        handler = newHandler();
+        when(connection.numActiveStreams()).thenReturn(0);
+        final long expectedMillis = 1234;
+        handler.gracefulShutdownTimeoutMillis(expectedMillis);
+        handler.close(ctx, promise);
+        verify(executor, atLeastOnce()).schedule(any(Runnable.class), eq(expectedMillis), eq(TimeUnit.MILLISECONDS));
     }
 
     @Test
@@ -738,6 +771,51 @@ public class Http2ConnectionHandlerTest {
         verify(executor, never()).schedule(any(Runnable.class), anyLong(), any(TimeUnit.class));
     }
 
+    @Test
+    public void writeMultipleRstFramesForSameStream() throws Exception {
+        handler = newHandler();
+        when(stream.id()).thenReturn(STREAM_ID);
+
+        final AtomicBoolean resetSent = new AtomicBoolean();
+        when(stream.resetSent()).then(new Answer<Http2Stream>() {
+            @Override
+            public Http2Stream answer(InvocationOnMock invocationOnMock) {
+                resetSent.set(true);
+                return stream;
+            }
+        });
+        when(stream.isResetSent()).then(new Answer<Boolean>() {
+            @Override
+            public Boolean answer(InvocationOnMock invocationOnMock) {
+                return resetSent.get();
+            }
+        });
+        when(frameWriter.writeRstStream(eq(ctx), eq(STREAM_ID), anyLong(), any(ChannelPromise.class)))
+                .then(new Answer<ChannelFuture>() {
+                    @Override
+                    public ChannelFuture answer(InvocationOnMock invocationOnMock) throws Throwable {
+                        ChannelPromise promise = invocationOnMock.getArgument(3);
+                        return promise.setSuccess();
+                    }
+                });
+
+        ChannelPromise promise =
+                new DefaultChannelPromise(channel, ImmediateEventExecutor.INSTANCE);
+        final ChannelPromise promise2 =
+                new DefaultChannelPromise(channel, ImmediateEventExecutor.INSTANCE);
+        promise.addListener(new ChannelFutureListener() {
+            @Override
+            public void operationComplete(ChannelFuture future) {
+                handler.resetStream(ctx, STREAM_ID, STREAM_CLOSED.code(), promise2);
+            }
+        });
+
+        handler.resetStream(ctx, STREAM_ID, CANCEL.code(), promise);
+        verify(frameWriter).writeRstStream(eq(ctx), eq(STREAM_ID), anyLong(), any(ChannelPromise.class));
+        assertTrue(promise.isSuccess());
+        assertTrue(promise2.isSuccess());
+    }
+
     private void writeRstStreamUsingVoidPromise(int streamId) throws Exception {
         handler = newHandler();
         final Throwable cause = new RuntimeException("fake exception");
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ConnectionRoundtripTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ConnectionRoundtripTest.java
index 32febac..f9dee00 100644
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ConnectionRoundtripTest.java
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ConnectionRoundtripTest.java
@@ -52,6 +52,8 @@ import java.io.ByteArrayOutputStream;
 import java.util.Random;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 
 import static io.netty.buffer.Unpooled.EMPTY_BUFFER;
@@ -522,18 +524,7 @@ public class Http2ConnectionRoundtripTest {
         final CountDownLatch serverWriteHeadersLatch = new CountDownLatch(1);
         final AtomicReference<Throwable> serverWriteHeadersCauseRef = new AtomicReference<Throwable>();
 
-        final Http2Headers headers = dummyHeaders();
         final int streamId = 3;
-        runInChannel(clientChannel, new Http2Runnable() {
-            @Override
-            public void run() throws Http2Exception {
-                http2Client.encoder().writeHeaders(ctx(), streamId, headers, CONNECTION_STREAM_ID,
-                        DEFAULT_PRIORITY_WEIGHT, false, 0, false, newPromise());
-                http2Client.encoder().writeRstStream(ctx(), streamId, Http2Error.CANCEL.code(), newPromise());
-                http2Client.flush(ctx());
-            }
-        });
-
         doAnswer(new Answer<Void>() {
             @Override
             public Void answer(InvocationOnMock invocationOnMock) throws Throwable {
@@ -544,6 +535,17 @@ public class Http2ConnectionRoundtripTest {
             }
         }).when(serverListener).onRstStreamRead(any(ChannelHandlerContext.class), eq(streamId), anyLong());
 
+        final Http2Headers headers = dummyHeaders();
+        runInChannel(clientChannel, new Http2Runnable() {
+            @Override
+            public void run() throws Http2Exception {
+                http2Client.encoder().writeHeaders(ctx(), streamId, headers, CONNECTION_STREAM_ID,
+                        DEFAULT_PRIORITY_WEIGHT, false, 0, false, newPromise());
+                http2Client.encoder().writeRstStream(ctx(), streamId, Http2Error.CANCEL.code(), newPromise());
+                http2Client.flush(ctx());
+            }
+        });
+
         assertTrue(serverSettingsAckLatch.await(DEFAULT_AWAIT_TIMEOUT_SECONDS, SECONDS));
         assertTrue(serverGotRstLatch.await(DEFAULT_AWAIT_TIMEOUT_SECONDS, SECONDS));
 
@@ -945,47 +947,33 @@ public class Http2ConnectionRoundtripTest {
     }
 
     @Test
-    public void createStreamSynchronouslyAfterGoAwayReceivedShouldFailLocally() throws Exception {
+    public void listenerIsNotifiedOfGoawayBeforeStreamsAreRemovedFromTheConnection() throws Exception {
         bootstrapEnv(1, 1, 2, 1, 1);
 
-        final CountDownLatch clientGoAwayLatch = new CountDownLatch(1);
-        doAnswer(new Answer<Void>() {
-            @Override
-            public Void answer(InvocationOnMock invocationOnMock) throws Throwable {
-                clientGoAwayLatch.countDown();
-                return null;
-            }
-        }).when(clientListener).onGoAwayRead(any(ChannelHandlerContext.class), anyInt(), anyLong(), any(ByteBuf.class));
-
         // We want both sides to do graceful shutdown during the test.
         setClientGracefulShutdownTime(10000);
         setServerGracefulShutdownTime(10000);
 
-        final Http2Headers headers = dummyHeaders();
-        final AtomicReference<ChannelFuture> clientWriteAfterGoAwayFutureRef = new AtomicReference<ChannelFuture>();
-        final CountDownLatch clientWriteAfterGoAwayLatch = new CountDownLatch(1);
+        final AtomicReference<Http2Stream.State> clientStream3State = new AtomicReference<Http2Stream.State>();
+        final CountDownLatch clientGoAwayLatch = new CountDownLatch(1);
         doAnswer(new Answer<Void>() {
             @Override
             public Void answer(InvocationOnMock invocationOnMock) throws Throwable {
-                ChannelFuture f = http2Client.encoder().writeHeaders(ctx(), 5, headers, 0, (short) 16, false, 0,
-                        true, newPromise());
-                clientWriteAfterGoAwayFutureRef.set(f);
-                f.addListener(new ChannelFutureListener() {
-                    @Override
-                    public void operationComplete(ChannelFuture future) throws Exception {
-                        clientWriteAfterGoAwayLatch.countDown();
-                    }
-                });
-                http2Client.flush(ctx());
+                clientStream3State.set(http2Client.connection().stream(3).state());
+                clientGoAwayLatch.countDown();
                 return null;
             }
         }).when(clientListener).onGoAwayRead(any(ChannelHandlerContext.class), anyInt(), anyLong(), any(ByteBuf.class));
 
+        // Create a single stream by sending a HEADERS frame to the server.
+        final Http2Headers headers = dummyHeaders();
         runInChannel(clientChannel, new Http2Runnable() {
             @Override
             public void run() throws Http2Exception {
+                http2Client.encoder().writeHeaders(ctx(), 1, headers, 0, (short) 16, false, 0,
+                    false, newPromise());
                 http2Client.encoder().writeHeaders(ctx(), 3, headers, 0, (short) 16, false, 0,
-                        true, newPromise());
+                    false, newPromise());
                 http2Client.flush(ctx());
             }
         });
@@ -998,24 +986,38 @@ public class Http2ConnectionRoundtripTest {
         runInChannel(serverChannel, new Http2Runnable() {
             @Override
             public void run() throws Http2Exception {
-                http2Server.encoder().writeGoAway(serverCtx(), 3, NO_ERROR.code(), EMPTY_BUFFER, serverNewPromise());
+                http2Server.encoder().writeGoAway(serverCtx(), 1, NO_ERROR.code(), EMPTY_BUFFER, serverNewPromise());
                 http2Server.flush(serverCtx());
             }
         });
 
-        // Wait for the client's write operation to complete.
-        assertTrue(clientWriteAfterGoAwayLatch.await(DEFAULT_AWAIT_TIMEOUT_SECONDS, SECONDS));
-
-        ChannelFuture clientWriteAfterGoAwayFuture = clientWriteAfterGoAwayFutureRef.get();
-        assertNotNull(clientWriteAfterGoAwayFuture);
-        Throwable clientCause = clientWriteAfterGoAwayFuture.cause();
-        assertThat(clientCause, is(instanceOf(Http2Exception.StreamException.class)));
-        assertEquals(Http2Error.REFUSED_STREAM.code(), ((Http2Exception.StreamException) clientCause).error().code());
+        // wait for the client to receive the GO_AWAY.
+        assertTrue(clientGoAwayLatch.await(DEFAULT_AWAIT_TIMEOUT_SECONDS, SECONDS));
+        verify(clientListener).onGoAwayRead(any(ChannelHandlerContext.class), eq(1), eq(NO_ERROR.code()),
+            any(ByteBuf.class));
+        assertEquals(Http2Stream.State.OPEN, clientStream3State.get());
+
+        // Make sure that stream 3 has been closed which is true if it's gone.
+        final CountDownLatch probeStreamCount = new CountDownLatch(1);
+        final AtomicBoolean stream3Exists = new AtomicBoolean();
+        final AtomicInteger streamCount = new AtomicInteger();
+        runInChannel(this.clientChannel, new Http2Runnable() {
+            @Override
+            public void run() throws Http2Exception {
+                stream3Exists.set(http2Client.connection().stream(3) != null);
+                streamCount.set(http2Client.connection().numActiveStreams());
+                probeStreamCount.countDown();
+            }
+        });
+        // The stream should be closed right after
+        assertTrue(probeStreamCount.await(DEFAULT_AWAIT_TIMEOUT_SECONDS, SECONDS));
+        assertEquals(1, streamCount.get());
+        assertFalse(stream3Exists.get());
 
         // Wait for the server to receive a GO_AWAY, but this is expected to timeout!
         assertFalse(goAwayLatch.await(1, SECONDS));
         verify(serverListener, never()).onGoAwayRead(any(ChannelHandlerContext.class), anyInt(), anyLong(),
-                any(ByteBuf.class));
+            any(ByteBuf.class));
 
         // Shutdown shouldn't wait for the server to close streams
         setClientGracefulShutdownTime(0);
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ControlFrameLimitEncoderTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ControlFrameLimitEncoderTest.java
new file mode 100644
index 0000000..bbae3af
--- /dev/null
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ControlFrameLimitEncoderTest.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright 2019 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.buffer.ByteBuf;
+import io.netty.buffer.UnpooledByteBufAllocator;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelConfig;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelMetadata;
+import io.netty.channel.ChannelPromise;
+import io.netty.channel.DefaultChannelPromise;
+import io.netty.channel.DefaultMessageSizeEstimator;
+import io.netty.handler.codec.http2.Http2Exception.ShutdownHint;
+import io.netty.util.ReferenceCountUtil;
+import io.netty.util.concurrent.EventExecutor;
+import io.netty.util.concurrent.ImmediateEventExecutor;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+
+import java.util.ArrayDeque;
+import java.util.Queue;
+
+import static io.netty.handler.codec.http2.Http2CodecUtil.*;
+import static io.netty.handler.codec.http2.Http2Error.CANCEL;
+import static io.netty.handler.codec.http2.Http2Error.ENHANCE_YOUR_CALM;
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * Tests for {@link Http2ControlFrameLimitEncoder}.
+ */
+public class Http2ControlFrameLimitEncoderTest {
+
+    private Http2ControlFrameLimitEncoder encoder;
+
+    @Mock
+    private Http2FrameWriter writer;
+
+    @Mock
+    private ChannelHandlerContext ctx;
+
+    @Mock
+    private Channel channel;
+
+    @Mock
+    private Channel.Unsafe unsafe;
+
+    @Mock
+    private ChannelConfig config;
+
+    @Mock
+    private EventExecutor executor;
+
+    private int numWrites;
+
+    private Queue<ChannelPromise> goAwayPromises = new ArrayDeque<ChannelPromise>();
+
+    /**
+     * Init fields and do mocking.
+     */
+    @Before
+    public void setup() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        numWrites = 0;
+
+        Http2FrameWriter.Configuration configuration = mock(Http2FrameWriter.Configuration.class);
+        Http2FrameSizePolicy frameSizePolicy = mock(Http2FrameSizePolicy.class);
+        when(writer.configuration()).thenReturn(configuration);
+        when(configuration.frameSizePolicy()).thenReturn(frameSizePolicy);
+        when(frameSizePolicy.maxFrameSize()).thenReturn(DEFAULT_MAX_FRAME_SIZE);
+
+        when(writer.writeRstStream(eq(ctx), anyInt(), anyLong(), any(ChannelPromise.class)))
+                .thenAnswer(new Answer<ChannelFuture>() {
+                    @Override
+                    public ChannelFuture answer(InvocationOnMock invocationOnMock) {
+                        return handlePromise(invocationOnMock, 3);
+                    }
+                });
+        when(writer.writeSettingsAck(any(ChannelHandlerContext.class), any(ChannelPromise.class)))
+                .thenAnswer(new Answer<ChannelFuture>() {
+                    @Override
+                    public ChannelFuture answer(InvocationOnMock invocationOnMock) {
+                        return handlePromise(invocationOnMock, 1);
+                    }
+        });
+        when(writer.writePing(any(ChannelHandlerContext.class), anyBoolean(), anyLong(), any(ChannelPromise.class)))
+                .thenAnswer(new Answer<ChannelFuture>() {
+                    @Override
+                    public ChannelFuture answer(InvocationOnMock invocationOnMock) {
+                        ChannelPromise promise = handlePromise(invocationOnMock, 3);
+                        if (invocationOnMock.getArgument(1) == Boolean.FALSE) {
+                            promise.trySuccess();
+                        }
+                        return promise;
+                    }
+                });
+        when(writer.writeGoAway(any(ChannelHandlerContext.class), anyInt(), anyLong(), any(ByteBuf.class),
+                any(ChannelPromise.class))).thenAnswer(new Answer<ChannelFuture>() {
+            @Override
+            public ChannelFuture answer(InvocationOnMock invocationOnMock) {
+                ReferenceCountUtil.release(invocationOnMock.getArgument(3));
+                ChannelPromise promise = invocationOnMock.getArgument(4);
+                goAwayPromises.offer(promise);
+                return promise;
+            }
+        });
+        Http2Connection connection = new DefaultHttp2Connection(false);
+        connection.remote().flowController(new DefaultHttp2RemoteFlowController(connection));
+        connection.local().flowController(new DefaultHttp2LocalFlowController(connection).frameWriter(writer));
+
+        DefaultHttp2ConnectionEncoder defaultEncoder =
+                new DefaultHttp2ConnectionEncoder(connection, writer);
+        encoder = new Http2ControlFrameLimitEncoder(defaultEncoder, 2);
+        DefaultHttp2ConnectionDecoder decoder =
+                new DefaultHttp2ConnectionDecoder(connection, encoder, mock(Http2FrameReader.class));
+        Http2ConnectionHandler handler = new Http2ConnectionHandlerBuilder()
+                .frameListener(mock(Http2FrameListener.class))
+                .codec(decoder, encoder).build();
+
+        // Set LifeCycleManager on encoder and decoder
+        when(ctx.channel()).thenReturn(channel);
+        when(ctx.alloc()).thenReturn(UnpooledByteBufAllocator.DEFAULT);
+        when(channel.alloc()).thenReturn(UnpooledByteBufAllocator.DEFAULT);
+        when(executor.inEventLoop()).thenReturn(true);
+        doAnswer(new Answer<ChannelPromise>() {
+            @Override
+            public ChannelPromise answer(InvocationOnMock invocation) throws Throwable {
+                return newPromise();
+            }
+        }).when(ctx).newPromise();
+        when(ctx.executor()).thenReturn(executor);
+        when(channel.isActive()).thenReturn(false);
+        when(channel.config()).thenReturn(config);
+        when(channel.isWritable()).thenReturn(true);
+        when(channel.bytesBeforeUnwritable()).thenReturn(Long.MAX_VALUE);
+        when(config.getWriteBufferHighWaterMark()).thenReturn(Integer.MAX_VALUE);
+        when(config.getMessageSizeEstimator()).thenReturn(DefaultMessageSizeEstimator.DEFAULT);
+        ChannelMetadata metadata = new ChannelMetadata(false, 16);
+        when(channel.metadata()).thenReturn(metadata);
+        when(channel.unsafe()).thenReturn(unsafe);
+        handler.handlerAdded(ctx);
+    }
+
+    private ChannelPromise handlePromise(InvocationOnMock invocationOnMock, int promiseIdx) {
+        ChannelPromise promise = invocationOnMock.getArgument(promiseIdx);
+        if (++numWrites == 2) {
+            promise.setSuccess();
+        }
+        return promise;
+    }
+
+    @After
+    public void teardown() {
+        // Close and release any buffered frames.
+        encoder.close();
+
+        // Notify all goAway ChannelPromise instances now as these will also release the retained ByteBuf for the
+        // debugData.
+        for (;;) {
+            ChannelPromise promise = goAwayPromises.poll();
+            if (promise == null) {
+                break;
+            }
+            promise.setSuccess();
+        }
+    }
+
+    @Test
+    public void testLimitSettingsAck() {
+        assertFalse(encoder.writeSettingsAck(ctx, newPromise()).isDone());
+        // The second write is always marked as success by our mock, which means it will also not be queued and so
+        // not count to the number of queued frames.
+        assertTrue(encoder.writeSettingsAck(ctx, newPromise()).isSuccess());
+        assertFalse(encoder.writeSettingsAck(ctx, newPromise()).isDone());
+
+        verifyFlushAndClose(0, false);
+
+        assertFalse(encoder.writeSettingsAck(ctx, newPromise()).isDone());
+        assertFalse(encoder.writeSettingsAck(ctx, newPromise()).isDone());
+
+        verifyFlushAndClose(1, true);
+    }
+
+    @Test
+    public void testLimitPingAck() {
+        assertFalse(encoder.writePing(ctx, true, 8, newPromise()).isDone());
+        // The second write is always marked as success by our mock, which means it will also not be queued and so
+        // not count to the number of queued frames.
+        assertTrue(encoder.writePing(ctx, true, 8, newPromise()).isSuccess());
+        assertFalse(encoder.writePing(ctx, true, 8, newPromise()).isDone());
+
+        verifyFlushAndClose(0, false);
+
+        assertFalse(encoder.writePing(ctx, true, 8, newPromise()).isDone());
+        assertFalse(encoder.writePing(ctx, true, 8, newPromise()).isDone());
+
+        verifyFlushAndClose(1, true);
+    }
+
+    @Test
+    public void testNotLimitPing() {
+        assertTrue(encoder.writePing(ctx, false, 8, newPromise()).isSuccess());
+        assertTrue(encoder.writePing(ctx, false, 8, newPromise()).isSuccess());
+        assertTrue(encoder.writePing(ctx, false, 8, newPromise()).isSuccess());
+        assertTrue(encoder.writePing(ctx, false, 8, newPromise()).isSuccess());
+
+        verifyFlushAndClose(0, false);
+    }
+
+    @Test
+    public void testLimitRst() {
+        assertFalse(encoder.writeRstStream(ctx, 1, CANCEL.code(), newPromise()).isDone());
+        // The second write is always marked as success by our mock, which means it will also not be queued and so
+        // not count to the number of queued frames.
+        assertTrue(encoder.writeRstStream(ctx, 1, CANCEL.code(), newPromise()).isSuccess());
+        assertFalse(encoder.writeRstStream(ctx, 1, CANCEL.code(), newPromise()).isDone());
+
+        verifyFlushAndClose(0, false);
+
+        assertFalse(encoder.writeRstStream(ctx, 1, CANCEL.code(), newPromise()).isDone());
+        assertFalse(encoder.writeRstStream(ctx, 1, CANCEL.code(), newPromise()).isDone());
+
+        verifyFlushAndClose(1, true);
+    }
+
+    @Test
+    public void testLimit() {
+        assertFalse(encoder.writeRstStream(ctx, 1, CANCEL.code(), newPromise()).isDone());
+        // The second write is always marked as success by our mock, which means it will also not be queued and so
+        // not count to the number of queued frames.
+        assertTrue(encoder.writePing(ctx, false, 8, newPromise()).isSuccess());
+        assertFalse(encoder.writePing(ctx, true, 8, newPromise()).isSuccess());
+
+        verifyFlushAndClose(0, false);
+
+        assertFalse(encoder.writeSettingsAck(ctx, newPromise()).isDone());
+        assertFalse(encoder.writeRstStream(ctx, 1, CANCEL.code(), newPromise()).isDone());
+        assertFalse(encoder.writePing(ctx, true, 8, newPromise()).isSuccess());
+
+        verifyFlushAndClose(1, true);
+    }
+
+    private void verifyFlushAndClose(int invocations, boolean failed) {
+        verify(ctx, atLeast(invocations)).flush();
+        verify(ctx, times(invocations)).close();
+        if (failed) {
+            verify(writer, times(1)).writeGoAway(eq(ctx), eq(0), eq(ENHANCE_YOUR_CALM.code()),
+                    any(ByteBuf.class), any(ChannelPromise.class));
+        }
+    }
+
+    private ChannelPromise newPromise() {
+        return new DefaultChannelPromise(channel, ImmediateEventExecutor.INSTANCE);
+    }
+}
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2DefaultFramesTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2DefaultFramesTest.java
new file mode 100644
index 0000000..86457d4
--- /dev/null
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2DefaultFramesTest.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2019 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.buffer.DefaultByteBufHolder;
+import io.netty.buffer.Unpooled;
+import org.junit.Test;
+
+import static org.junit.Assert.assertFalse;
+
+public class Http2DefaultFramesTest {
+
+    @SuppressWarnings("SimplifiableJUnitAssertion")
+    @Test
+    public void testEqualOperation() {
+        // in this case, 'goAwayFrame' and 'unknownFrame' will also have an EMPTY_BUFFER data
+        // so we want to check that 'dflt' will not consider them equal.
+        DefaultHttp2GoAwayFrame goAwayFrame = new DefaultHttp2GoAwayFrame(1);
+        DefaultHttp2UnknownFrame unknownFrame = new DefaultHttp2UnknownFrame((byte) 1, new Http2Flags((short) 1));
+        DefaultByteBufHolder dflt = new DefaultByteBufHolder(Unpooled.EMPTY_BUFFER);
+        try {
+            // not using 'assertNotEquals' to be explicit about which object we are calling .equals() on
+            assertFalse(dflt.equals(goAwayFrame));
+            assertFalse(dflt.equals(unknownFrame));
+        } finally {
+            goAwayFrame.release();
+            unknownFrame.release();
+            dflt.release();
+        }
+    }
+}
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2EmptyDataFrameConnectionDecoderTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2EmptyDataFrameConnectionDecoderTest.java
new file mode 100644
index 0000000..346a09a
--- /dev/null
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2EmptyDataFrameConnectionDecoderTest.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2019 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 org.hamcrest.CoreMatchers;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class Http2EmptyDataFrameConnectionDecoderTest {
+
+    @Test
+    public void testDecoration() {
+        Http2ConnectionDecoder delegate = mock(Http2ConnectionDecoder.class);
+        final ArgumentCaptor<Http2FrameListener> listenerArgumentCaptor =
+                ArgumentCaptor.forClass(Http2FrameListener.class);
+        when(delegate.frameListener()).then(new Answer<Http2FrameListener>() {
+            @Override
+            public Http2FrameListener answer(InvocationOnMock invocationOnMock) {
+                return listenerArgumentCaptor.getValue();
+            }
+        });
+        Http2FrameListener listener = mock(Http2FrameListener.class);
+        Http2EmptyDataFrameConnectionDecoder decoder = new Http2EmptyDataFrameConnectionDecoder(delegate, 2);
+        decoder.frameListener(listener);
+        verify(delegate).frameListener(listenerArgumentCaptor.capture());
+
+        assertThat(decoder.frameListener(),
+                CoreMatchers.not(CoreMatchers.instanceOf(Http2EmptyDataFrameListener.class)));
+        assertThat(decoder.frameListener0(), CoreMatchers.instanceOf(Http2EmptyDataFrameListener.class));
+    }
+
+    @Test
+    public void testDecorationWithNull() {
+        Http2ConnectionDecoder delegate = mock(Http2ConnectionDecoder.class);
+
+        Http2EmptyDataFrameConnectionDecoder decoder = new Http2EmptyDataFrameConnectionDecoder(delegate, 2);
+        decoder.frameListener(null);
+        assertNull(decoder.frameListener());
+    }
+}
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2EmptyDataFrameListenerTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2EmptyDataFrameListenerTest.java
new file mode 100644
index 0000000..e7b2ac4
--- /dev/null
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2EmptyDataFrameListenerTest.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2019 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.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.ChannelHandlerContext;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+public class Http2EmptyDataFrameListenerTest {
+
+    @Mock
+    private Http2FrameListener frameListener;
+    @Mock
+    private ChannelHandlerContext ctx;
+
+    @Mock
+    private ByteBuf nonEmpty;
+
+    private Http2EmptyDataFrameListener listener;
+
+    @Before
+    public void setUp() {
+        initMocks(this);
+        when(nonEmpty.isReadable()).thenReturn(true);
+        listener = new Http2EmptyDataFrameListener(frameListener, 2);
+    }
+
+    @Test
+    public void testEmptyDataFrames() throws Http2Exception {
+        listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
+        listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
+
+        try {
+            listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
+            fail();
+        } catch (Http2Exception expected) {
+            // expected
+        }
+        verify(frameListener, times(2)).onDataRead(eq(ctx), eq(1), any(ByteBuf.class), eq(0), eq(false));
+    }
+
+    @Test
+    public void testEmptyDataFramesWithNonEmptyInBetween() throws Http2Exception {
+        Http2EmptyDataFrameListener listener = new Http2EmptyDataFrameListener(frameListener, 2);
+        listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
+        listener.onDataRead(ctx, 1, nonEmpty, 0, false);
+
+        listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
+        listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
+
+        try {
+            listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
+            fail();
+        } catch (Http2Exception expected) {
+            // expected
+        }
+        verify(frameListener, times(4)).onDataRead(eq(ctx), eq(1), any(ByteBuf.class), eq(0), eq(false));
+    }
+
+    @Test
+    public void testEmptyDataFramesWithEndOfStreamInBetween() throws Http2Exception {
+        Http2EmptyDataFrameListener listener = new Http2EmptyDataFrameListener(frameListener, 2);
+        listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
+        listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, true);
+
+        listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
+        listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
+
+        try {
+            listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
+            fail();
+        } catch (Http2Exception expected) {
+            // expected
+        }
+        verify(frameListener, times(1)).onDataRead(eq(ctx), eq(1), any(ByteBuf.class), eq(0), eq(true));
+        verify(frameListener, times(3)).onDataRead(eq(ctx), eq(1), any(ByteBuf.class), eq(0), eq(false));
+    }
+
+    @Test
+    public void testEmptyDataFramesWithHeaderFrameInBetween() throws Http2Exception {
+        Http2EmptyDataFrameListener listener = new Http2EmptyDataFrameListener(frameListener, 2);
+        listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
+        listener.onHeadersRead(ctx, 1, EmptyHttp2Headers.INSTANCE, 0, true);
+
+        listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
+        listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
+
+        try {
+            listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
+            fail();
+        } catch (Http2Exception expected) {
+            // expected
+        }
+
+        verify(frameListener, times(1)).onHeadersRead(eq(ctx), eq(1), eq(EmptyHttp2Headers.INSTANCE), eq(0), eq(true));
+        verify(frameListener, times(3)).onDataRead(eq(ctx), eq(1), any(ByteBuf.class), eq(0), eq(false));
+    }
+
+    @Test
+    public void testEmptyDataFramesWithHeaderFrameInBetween2() throws Http2Exception {
+        Http2EmptyDataFrameListener listener = new Http2EmptyDataFrameListener(frameListener, 2);
+        listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
+        listener.onHeadersRead(ctx, 1, EmptyHttp2Headers.INSTANCE, 0, (short) 0, false, 0, true);
+
+        listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
+        listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
+
+        try {
+            listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
+            fail();
+        } catch (Http2Exception expected) {
+            // expected
+        }
+
+        verify(frameListener, times(1)).onHeadersRead(eq(ctx), eq(1),
+                eq(EmptyHttp2Headers.INSTANCE), eq(0), eq((short) 0), eq(false), eq(0), eq(true));
+        verify(frameListener, times(3)).onDataRead(eq(ctx), eq(1), any(ByteBuf.class), eq(0), eq(false));
+    }
+}
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2FrameCodecTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2FrameCodecTest.java
index 27d13cf..f2e3c93 100644
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2FrameCodecTest.java
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2FrameCodecTest.java
@@ -55,11 +55,11 @@ import java.util.Set;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 import static io.netty.handler.codec.http2.Http2CodecUtil.isStreamIdValid;
+import static io.netty.handler.codec.http2.Http2Error.NO_ERROR;
 import static io.netty.handler.codec.http2.Http2TestUtil.anyChannelPromise;
 import static io.netty.handler.codec.http2.Http2TestUtil.anyHttp2Settings;
 import static io.netty.handler.codec.http2.Http2TestUtil.assertEqualsAndRelease;
 import static io.netty.handler.codec.http2.Http2TestUtil.bb;
-
 import static org.hamcrest.Matchers.instanceOf;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -152,6 +152,8 @@ public class Http2FrameCodecTest {
 
         Http2SettingsFrame settingsFrame = inboundHandler.readInbound();
         assertNotNull(settingsFrame);
+        Http2SettingsAckFrame settingsAckFrame = inboundHandler.readInbound();
+        assertNotNull(settingsAckFrame);
     }
 
     @Test
@@ -174,7 +176,7 @@ public class Http2FrameCodecTest {
 
         channel.writeOutbound(new DefaultHttp2HeadersFrame(response, true, 27).stream(stream2));
         verify(frameWriter).writeHeaders(
-                eqFrameCodecCtx(), eq(1), eq(response), anyInt(), anyShort(), anyBoolean(),
+                eqFrameCodecCtx(), eq(1), eq(response),
                 eq(27), eq(true), anyChannelPromise());
         verify(frameWriter, never()).writeRstStream(
                 eqFrameCodecCtx(), anyInt(), anyLong(), anyChannelPromise());
@@ -203,7 +205,7 @@ public class Http2FrameCodecTest {
 
         channel.writeOutbound(new DefaultHttp2HeadersFrame(response, true, 27).stream(stream2));
         verify(frameWriter).writeHeaders(
-                eqFrameCodecCtx(), eq(1), eq(response), anyInt(), anyShort(), anyBoolean(),
+                eqFrameCodecCtx(), eq(1), eq(response),
                 eq(27), eq(true), anyChannelPromise());
         verify(frameWriter, never()).writeRstStream(
                 eqFrameCodecCtx(), anyInt(), anyLong(), anyChannelPromise());
@@ -217,7 +219,7 @@ public class Http2FrameCodecTest {
         Http2Connection conn = new DefaultHttp2Connection(true);
         Http2ConnectionEncoder enc = new DefaultHttp2ConnectionEncoder(conn, new DefaultHttp2FrameWriter());
         Http2ConnectionDecoder dec = new DefaultHttp2ConnectionDecoder(conn, enc, new DefaultHttp2FrameReader());
-        Http2FrameCodec codec = new Http2FrameCodec(enc, dec, new Http2Settings());
+        Http2FrameCodec codec = new Http2FrameCodec(enc, dec, new Http2Settings(), false);
         EmbeddedChannel em = new EmbeddedChannel(codec);
 
         // We call #consumeBytes on a stream id which has not been seen yet to emulate the case
@@ -250,8 +252,8 @@ public class Http2FrameCodecTest {
         assertNull(inboundHandler.readInbound());
 
         channel.writeOutbound(new DefaultHttp2HeadersFrame(response, false).stream(stream2));
-        verify(frameWriter).writeHeaders(eqFrameCodecCtx(), eq(1), eq(response), anyInt(),
-                                         anyShort(), anyBoolean(), eq(0), eq(false), anyChannelPromise());
+        verify(frameWriter).writeHeaders(eqFrameCodecCtx(), eq(1), eq(response),
+                eq(0), eq(false), anyChannelPromise());
 
         channel.writeOutbound(new DefaultHttp2DataFrame(bb("world"), true, 27).stream(stream2));
         ArgumentCaptor<ByteBuf> outboundData = ArgumentCaptor.forClass(ByteBuf.class);
@@ -302,9 +304,9 @@ public class Http2FrameCodecTest {
         Http2HeadersFrame actualHeaders = inboundHandler.readInbound();
         assertEquals(expectedHeaders.stream(actualHeaders.stream()), actualHeaders);
 
-        frameInboundWriter.writeInboundRstStream(3, Http2Error.NO_ERROR.code());
+        frameInboundWriter.writeInboundRstStream(3, NO_ERROR.code());
 
-        Http2ResetFrame expectedRst = new DefaultHttp2ResetFrame(Http2Error.NO_ERROR).stream(actualHeaders.stream());
+        Http2ResetFrame expectedRst = new DefaultHttp2ResetFrame(NO_ERROR).stream(actualHeaders.stream());
         Http2ResetFrame actualRst = inboundHandler.readInbound();
         assertEquals(expectedRst, actualRst);
 
@@ -321,13 +323,13 @@ public class Http2FrameCodecTest {
         ByteBuf debugData = bb("debug");
         ByteBuf expected = debugData.copy();
 
-        Http2GoAwayFrame goAwayFrame = new DefaultHttp2GoAwayFrame(Http2Error.NO_ERROR.code(), debugData);
+        Http2GoAwayFrame goAwayFrame = new DefaultHttp2GoAwayFrame(NO_ERROR.code(),
+                debugData.retainedDuplicate());
         goAwayFrame.setExtraStreamIds(2);
 
         channel.writeOutbound(goAwayFrame);
         verify(frameWriter).writeGoAway(eqFrameCodecCtx(), eq(7),
-                eq(Http2Error.NO_ERROR.code()), eq(expected), anyChannelPromise());
-        assertEquals(1, debugData.refCnt());
+                eq(NO_ERROR.code()), eq(expected), anyChannelPromise());
         assertEquals(State.OPEN, stream.state());
         assertTrue(channel.isActive());
         expected.release();
@@ -337,8 +339,8 @@ public class Http2FrameCodecTest {
     @Test
     public void receiveGoaway() throws Exception {
         ByteBuf debugData = bb("foo");
-        frameInboundWriter.writeInboundGoAway(2, Http2Error.NO_ERROR.code(), debugData);
-        Http2GoAwayFrame expectedFrame = new DefaultHttp2GoAwayFrame(2, Http2Error.NO_ERROR.code(), bb("foo"));
+        frameInboundWriter.writeInboundGoAway(2, NO_ERROR.code(), debugData);
+        Http2GoAwayFrame expectedFrame = new DefaultHttp2GoAwayFrame(2, NO_ERROR.code(), bb("foo"));
         Http2GoAwayFrame actualFrame = inboundHandler.readInbound();
 
         assertEqualsAndRelease(expectedFrame, actualFrame);
@@ -384,14 +386,15 @@ public class Http2FrameCodecTest {
         assertEquals(State.OPEN, stream.state());
 
         ByteBuf debugData = bb("debug");
-        Http2GoAwayFrame goAwayFrame = new DefaultHttp2GoAwayFrame(Http2Error.NO_ERROR.code(), debugData.slice());
+        Http2GoAwayFrame goAwayFrame = new DefaultHttp2GoAwayFrame(NO_ERROR.code(),
+                debugData.retainedDuplicate());
         goAwayFrame.setExtraStreamIds(Integer.MAX_VALUE);
 
         channel.writeOutbound(goAwayFrame);
         // When the last stream id computation overflows, the last stream id should just be set to 2^31 - 1.
         verify(frameWriter).writeGoAway(eqFrameCodecCtx(), eq(Integer.MAX_VALUE),
-                eq(Http2Error.NO_ERROR.code()), eq(debugData), anyChannelPromise());
-        assertEquals(1, debugData.refCnt());
+                eq(NO_ERROR.code()), eq(debugData), anyChannelPromise());
+        debugData.release();
         assertEquals(State.OPEN, stream.state());
         assertTrue(channel.isActive());
     }
@@ -662,6 +665,33 @@ public class Http2FrameCodecTest {
         assertFalse(channel.finishAndReleaseAll());
     }
 
+    @Test
+    public void doNotLeakOnFailedInitializationForChannels() throws Exception {
+        setUp(Http2FrameCodecBuilder.forServer(), new Http2Settings().maxConcurrentStreams(2));
+
+        Http2FrameStream stream1 = frameCodec.newStream();
+        Http2FrameStream stream2 = frameCodec.newStream();
+
+        ChannelPromise stream1HeaderPromise = channel.newPromise();
+        ChannelPromise stream2HeaderPromise = channel.newPromise();
+
+        channel.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers()).stream(stream1),
+                              stream1HeaderPromise);
+        channel.runPendingTasks();
+
+        frameInboundWriter.writeInboundGoAway(stream1.id(), 0L, Unpooled.EMPTY_BUFFER);
+
+        channel.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers()).stream(stream2),
+                              stream2HeaderPromise);
+        channel.runPendingTasks();
+
+        assertTrue(stream1HeaderPromise.syncUninterruptibly().isSuccess());
+        assertTrue(stream2HeaderPromise.isDone());
+
+        assertEquals(0, frameCodec.numInitializingStreams());
+        assertFalse(channel.finishAndReleaseAll());
+    }
+
     @Test
     public void streamIdentifiersExhausted() throws Http2Exception {
         int maxServerStreamId = Integer.MAX_VALUE - 1;
@@ -674,6 +704,11 @@ public class Http2FrameCodecTest {
         ChannelPromise writePromise = channel.newPromise();
         channel.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers()).stream(stream), writePromise);
 
+        Http2GoAwayFrame goAwayFrame = inboundHandler.readInbound();
+        assertNotNull(goAwayFrame);
+        assertEquals(NO_ERROR.code(), goAwayFrame.errorCode());
+        assertEquals(Integer.MAX_VALUE, goAwayFrame.lastStreamId());
+        goAwayFrame.release();
         assertThat(writePromise.cause(), instanceOf(Http2NoMoreStreamIdsException.class));
     }
 
@@ -751,6 +786,30 @@ public class Http2FrameCodecTest {
         assertEquals(expectedStreams, activeStreams);
     }
 
+    @Test
+    public void autoAckPingTrue() throws Exception {
+        setUp(Http2FrameCodecBuilder.forServer().autoAckPingFrame(true), new Http2Settings());
+        frameInboundWriter.writeInboundPing(false, 8);
+        Http2PingFrame frame = inboundHandler.readInbound();
+        assertFalse(frame.ack());
+        assertEquals(8, frame.content());
+        verify(frameWriter).writePing(eqFrameCodecCtx(), eq(true), eq(8L), anyChannelPromise());
+    }
+
+    @Test
+    public void autoAckPingFalse() throws Exception {
+        setUp(Http2FrameCodecBuilder.forServer().autoAckPingFrame(false), new Http2Settings());
+        frameInboundWriter.writeInboundPing(false, 8);
+        verify(frameWriter, never()).writePing(eqFrameCodecCtx(), eq(true), eq(8L), anyChannelPromise());
+        Http2PingFrame frame = inboundHandler.readInbound();
+        assertFalse(frame.ack());
+        assertEquals(8, frame.content());
+
+        // Now ack the frame manually.
+        channel.writeAndFlush(new DefaultHttp2PingFrame(8, true));
+        verify(frameWriter).writePing(eqFrameCodecCtx(), eq(true), eq(8L), anyChannelPromise());
+    }
+
     @Test
     public void streamShouldBeOpenInListener() {
         final Http2FrameStream stream2 = frameCodec.newStream();
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexClientUpgradeTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexClientUpgradeTest.java
new file mode 100644
index 0000000..dcbece6
--- /dev/null
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexClientUpgradeTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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 org.junit.Test;
+
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.channel.embedded.EmbeddedChannel;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+public abstract class Http2MultiplexClientUpgradeTest<C extends Http2FrameCodec> {
+
+    @ChannelHandler.Sharable
+    static final class NoopHandler extends ChannelInboundHandlerAdapter {
+        @Override
+        public void channelActive(ChannelHandlerContext ctx) {
+            ctx.channel().close();
+        }
+    }
+
+    private static final class UpgradeHandler extends ChannelInboundHandlerAdapter {
+        Http2Stream.State stateOnActive;
+        int streamId;
+        boolean channelInactiveCalled;
+
+        @Override
+        public void channelActive(ChannelHandlerContext ctx) throws Exception {
+            Http2StreamChannel ch = (Http2StreamChannel) ctx.channel();
+            stateOnActive = ch.stream().state();
+            streamId = ch.stream().id();
+            super.channelActive(ctx);
+        }
+
+        @Override
+        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
+            channelInactiveCalled = true;
+            super.channelInactive(ctx);
+        }
+    }
+
+    protected abstract C newCodec(ChannelHandler upgradeHandler);
+
+    protected abstract ChannelHandler newMultiplexer(ChannelHandler upgradeHandler);
+
+    @Test
+    public void upgradeHandlerGetsActivated() throws Exception {
+        UpgradeHandler upgradeHandler = new UpgradeHandler();
+        C codec = newCodec(upgradeHandler);
+        EmbeddedChannel ch = new EmbeddedChannel(codec, newMultiplexer(upgradeHandler));
+
+        codec.onHttpClientUpgrade();
+
+        assertFalse(upgradeHandler.stateOnActive.localSideOpen());
+        assertTrue(upgradeHandler.stateOnActive.remoteSideOpen());
+        assertNotNull(codec.connection().stream(Http2CodecUtil.HTTP_UPGRADE_STREAM_ID).getProperty(codec.streamKey));
+        assertEquals(Http2CodecUtil.HTTP_UPGRADE_STREAM_ID, upgradeHandler.streamId);
+        assertTrue(ch.finishAndReleaseAll());
+        assertTrue(upgradeHandler.channelInactiveCalled);
+    }
+
+    @Test(expected = Http2Exception.class)
+    public void clientUpgradeWithoutUpgradeHandlerThrowsHttp2Exception() throws Http2Exception {
+        C codec = newCodec(null);
+        EmbeddedChannel ch = new EmbeddedChannel(codec, newMultiplexer(null));
+        try {
+            codec.onHttpClientUpgrade();
+        } finally {
+            assertTrue(ch.finishAndReleaseAll());
+        }
+    }
+}
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexCodecClientUpgradeTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexCodecClientUpgradeTest.java
index 26b63ed..b917feb 100644
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexCodecClientUpgradeTest.java
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexCodecClientUpgradeTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018 The Netty Project
+ * Copyright 2019 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
@@ -14,68 +14,21 @@
  */
 package io.netty.handler.codec.http2;
 
-import org.junit.Test;
-
 import io.netty.channel.ChannelHandler;
-import io.netty.channel.ChannelHandlerContext;
-import io.netty.channel.ChannelInboundHandlerAdapter;
-import io.netty.channel.embedded.EmbeddedChannel;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-public class Http2MultiplexCodecClientUpgradeTest {
-
-    @ChannelHandler.Sharable
-    private final class NoopHandler extends ChannelInboundHandlerAdapter {
-        @Override
-        public void channelActive(ChannelHandlerContext ctx) {
-            ctx.channel().close();
-        }
-    }
 
-    private final class UpgradeHandler extends ChannelInboundHandlerAdapter {
-        Http2Stream.State stateOnActive;
-        int streamId;
+public class Http2MultiplexCodecClientUpgradeTest extends Http2MultiplexClientUpgradeTest<Http2MultiplexCodec> {
 
-        @Override
-        public void channelActive(ChannelHandlerContext ctx) throws Exception {
-            Http2StreamChannel ch = (Http2StreamChannel) ctx.channel();
-            stateOnActive = ch.stream().state();
-            streamId = ch.stream().id();
-            super.channelActive(ctx);
-        }
-    }
-
-    private Http2MultiplexCodec newCodec(ChannelHandler upgradeHandler) {
+    @Override
+    protected Http2MultiplexCodec newCodec(ChannelHandler upgradeHandler) {
         Http2MultiplexCodecBuilder builder = Http2MultiplexCodecBuilder.forClient(new NoopHandler());
-        builder.withUpgradeStreamHandler(upgradeHandler);
+        if (upgradeHandler != null) {
+            builder.withUpgradeStreamHandler(upgradeHandler);
+        }
         return builder.build();
     }
 
-    @Test
-    public void upgradeHandlerGetsActivated() throws Exception {
-        UpgradeHandler upgradeHandler = new UpgradeHandler();
-        Http2MultiplexCodec codec = newCodec(upgradeHandler);
-        EmbeddedChannel ch = new EmbeddedChannel(codec);
-
-        codec.onHttpClientUpgrade();
-
-        assertFalse(upgradeHandler.stateOnActive.localSideOpen());
-        assertTrue(upgradeHandler.stateOnActive.remoteSideOpen());
-        assertEquals(1, upgradeHandler.streamId);
-        assertTrue(ch.finishAndReleaseAll());
-    }
-
-    @Test(expected = Http2Exception.class)
-    public void clientUpgradeWithoutUpgradeHandlerThrowsHttp2Exception() throws Http2Exception {
-        Http2MultiplexCodec codec = Http2MultiplexCodecBuilder.forClient(new NoopHandler()).build();
-        EmbeddedChannel ch = new EmbeddedChannel(codec);
-        try {
-            codec.onHttpClientUpgrade();
-        } finally {
-            assertTrue(ch.finishAndReleaseAll());
-        }
+    @Override
+    protected ChannelHandler newMultiplexer(ChannelHandler upgradeHandler) {
+        return null;
     }
 }
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexCodecTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexCodecTest.java
index 7788e6d..d9c20f9 100644
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexCodecTest.java
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexCodecTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2016 The Netty Project
+ * Copyright 2019 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
@@ -14,994 +14,27 @@
  */
 package io.netty.handler.codec.http2;
 
-import io.netty.buffer.ByteBuf;
-import io.netty.buffer.Unpooled;
-import io.netty.channel.Channel;
-import io.netty.channel.ChannelFuture;
-import io.netty.channel.ChannelFutureListener;
 import io.netty.channel.ChannelHandler;
-import io.netty.channel.ChannelHandlerContext;
-import io.netty.channel.ChannelInboundHandlerAdapter;
-import io.netty.channel.ChannelPromise;
-import io.netty.channel.embedded.EmbeddedChannel;
-import io.netty.handler.codec.http.HttpMethod;
-import io.netty.handler.codec.http.HttpScheme;
-import io.netty.handler.codec.http2.Http2Exception.StreamException;
-import io.netty.handler.codec.http2.LastInboundHandler.Consumer;
-import io.netty.util.AsciiString;
-import io.netty.util.AttributeKey;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.ArgumentMatcher;
-import org.mockito.Mockito;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
 
-import java.net.InetSocketAddress;
-import java.nio.channels.ClosedChannelException;
-import java.util.ArrayDeque;
-import java.util.Queue;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.atomic.AtomicReference;
+public class Http2MultiplexCodecTest extends Http2MultiplexTest<Http2FrameCodec> {
 
-import static io.netty.util.ReferenceCountUtil.release;
-import static io.netty.handler.codec.http2.Http2TestUtil.anyChannelPromise;
-import static io.netty.handler.codec.http2.Http2TestUtil.anyHttp2Settings;
-import static io.netty.handler.codec.http2.Http2TestUtil.assertEqualsAndRelease;
-import static io.netty.handler.codec.http2.Http2TestUtil.bb;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.anyShort;
-import static org.mockito.ArgumentMatchers.argThat;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-/**
- * Unit tests for {@link Http2MultiplexCodec}.
- */
-public class Http2MultiplexCodecTest {
-    private final Http2Headers request = new DefaultHttp2Headers()
-            .method(HttpMethod.GET.asciiName()).scheme(HttpScheme.HTTPS.name())
-            .authority(new AsciiString("example.org")).path(new AsciiString("/foo"));
-
-    private EmbeddedChannel parentChannel;
-    private Http2FrameWriter frameWriter;
-    private Http2FrameInboundWriter frameInboundWriter;
-    private TestChannelInitializer childChannelInitializer;
-    private Http2MultiplexCodec codec;
-
-    private static final int initialRemoteStreamWindow = 1024;
-
-    @Before
-    public void setUp() {
-        childChannelInitializer = new TestChannelInitializer();
-        parentChannel = new EmbeddedChannel();
-        frameInboundWriter = new Http2FrameInboundWriter(parentChannel);
-        parentChannel.connect(new InetSocketAddress(0));
-        frameWriter = Http2TestUtil.mockedFrameWriter();
-        codec = new Http2MultiplexCodecBuilder(true, childChannelInitializer).frameWriter(frameWriter).build();
-        parentChannel.pipeline().addLast(codec);
-        parentChannel.runPendingTasks();
-        parentChannel.pipeline().fireChannelActive();
-
-        parentChannel.writeInbound(Http2CodecUtil.connectionPrefaceBuf());
-
-        Http2Settings settings = new Http2Settings().initialWindowSize(initialRemoteStreamWindow);
-        frameInboundWriter.writeInboundSettings(settings);
-
-        verify(frameWriter).writeSettingsAck(eqMultiplexCodecCtx(), anyChannelPromise());
-
-        frameInboundWriter.writeInboundSettingsAck();
-
-        Http2SettingsFrame settingsFrame = parentChannel.readInbound();
-        assertNotNull(settingsFrame);
-
-        // Handshake
-        verify(frameWriter).writeSettings(eqMultiplexCodecCtx(),
-                anyHttp2Settings(), anyChannelPromise());
-    }
-
-    private ChannelHandlerContext eqMultiplexCodecCtx() {
-        return eq(codec.ctx);
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        if (childChannelInitializer.handler instanceof LastInboundHandler) {
-            ((LastInboundHandler) childChannelInitializer.handler).finishAndReleaseAll();
-        }
-        parentChannel.finishAndReleaseAll();
-        codec = null;
-    }
-
-    // TODO(buchgr): Flush from child channel
-    // TODO(buchgr): ChildChannel.childReadComplete()
-    // TODO(buchgr): GOAWAY Logic
-    // TODO(buchgr): Test ChannelConfig.setMaxMessagesPerRead
-
-    @Test
-    public void writeUnknownFrame() {
-        Http2StreamChannel childChannel = newOutboundStream(new ChannelInboundHandlerAdapter() {
-            @Override
-            public void channelActive(ChannelHandlerContext ctx) {
-                ctx.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers()));
-                ctx.writeAndFlush(new DefaultHttp2UnknownFrame((byte) 99, new Http2Flags()));
-                ctx.fireChannelActive();
-            }
-        });
-        assertTrue(childChannel.isActive());
-
-        parentChannel.runPendingTasks();
-
-        verify(frameWriter).writeFrame(eq(codec.ctx), eq((byte) 99), eqStreamId(childChannel), any(Http2Flags.class),
-                any(ByteBuf.class), any(ChannelPromise.class));
-    }
-
-    private Http2StreamChannel newInboundStream(int streamId, boolean endStream, final ChannelHandler childHandler) {
-        return newInboundStream(streamId, endStream, null, childHandler);
-    }
-
-    private Http2StreamChannel newInboundStream(int streamId, boolean endStream,
-                                                AtomicInteger maxReads, final ChannelHandler childHandler) {
-        final AtomicReference<Http2StreamChannel> streamChannelRef = new AtomicReference<Http2StreamChannel>();
-        childChannelInitializer.maxReads = maxReads;
-        childChannelInitializer.handler = new ChannelInboundHandlerAdapter() {
-            @Override
-            public void channelRegistered(ChannelHandlerContext ctx) {
-                assertNull(streamChannelRef.get());
-                streamChannelRef.set((Http2StreamChannel) ctx.channel());
-                ctx.pipeline().addLast(childHandler);
-                ctx.fireChannelRegistered();
-            }
-        };
-
-        frameInboundWriter.writeInboundHeaders(streamId, request, 0, endStream);
-        parentChannel.runPendingTasks();
-        Http2StreamChannel channel = streamChannelRef.get();
-        assertEquals(streamId, channel.stream().id());
-        return channel;
-    }
-
-    @Test
-    public void readUnkownFrame() {
-        LastInboundHandler handler = new LastInboundHandler();
-
-        Http2StreamChannel channel = newInboundStream(3, true, handler);
-        frameInboundWriter.writeInboundFrame((byte) 99, channel.stream().id(), new Http2Flags(), Unpooled.EMPTY_BUFFER);
-
-        // header frame and unknown frame
-        verifyFramesMultiplexedToCorrectChannel(channel, handler, 2);
-
-        Channel childChannel = newOutboundStream(new ChannelInboundHandlerAdapter());
-        assertTrue(childChannel.isActive());
-    }
-
-    @Test
-    public void headerAndDataFramesShouldBeDelivered() {
-        LastInboundHandler inboundHandler = new LastInboundHandler();
-
-        Http2StreamChannel channel = newInboundStream(3, false, inboundHandler);
-        Http2HeadersFrame headersFrame = new DefaultHttp2HeadersFrame(request).stream(channel.stream());
-        Http2DataFrame dataFrame1 = new DefaultHttp2DataFrame(bb("hello")).stream(channel.stream());
-        Http2DataFrame dataFrame2 = new DefaultHttp2DataFrame(bb("world")).stream(channel.stream());
-
-        assertTrue(inboundHandler.isChannelActive());
-        frameInboundWriter.writeInboundData(channel.stream().id(), bb("hello"), 0, false);
-        frameInboundWriter.writeInboundData(channel.stream().id(), bb("world"), 0, false);
-
-        assertEquals(headersFrame, inboundHandler.readInbound());
-
-        assertEqualsAndRelease(dataFrame1, inboundHandler.<Http2Frame>readInbound());
-        assertEqualsAndRelease(dataFrame2, inboundHandler.<Http2Frame>readInbound());
-
-        assertNull(inboundHandler.readInbound());
-    }
-
-    @Test
-    public void framesShouldBeMultiplexed() {
-        LastInboundHandler handler1 = new LastInboundHandler();
-        Http2StreamChannel channel1 = newInboundStream(3, false, handler1);
-        LastInboundHandler handler2 = new LastInboundHandler();
-        Http2StreamChannel channel2 = newInboundStream(5, false, handler2);
-        LastInboundHandler handler3 = new LastInboundHandler();
-        Http2StreamChannel channel3 = newInboundStream(11, false, handler3);
-
-        verifyFramesMultiplexedToCorrectChannel(channel1, handler1, 1);
-        verifyFramesMultiplexedToCorrectChannel(channel2, handler2, 1);
-        verifyFramesMultiplexedToCorrectChannel(channel3, handler3, 1);
-
-        frameInboundWriter.writeInboundData(channel2.stream().id(), bb("hello"), 0, false);
-        frameInboundWriter.writeInboundData(channel1.stream().id(), bb("foo"), 0, true);
-        frameInboundWriter.writeInboundData(channel2.stream().id(), bb("world"), 0, true);
-        frameInboundWriter.writeInboundData(channel3.stream().id(), bb("bar"), 0, true);
-
-        verifyFramesMultiplexedToCorrectChannel(channel1, handler1, 1);
-        verifyFramesMultiplexedToCorrectChannel(channel2, handler2, 2);
-        verifyFramesMultiplexedToCorrectChannel(channel3, handler3, 1);
-    }
-
-    @Test
-    public void inboundDataFrameShouldUpdateLocalFlowController() throws Http2Exception {
-        Http2LocalFlowController flowController = Mockito.mock(Http2LocalFlowController.class);
-        codec.connection().local().flowController(flowController);
-
-        LastInboundHandler handler = new LastInboundHandler();
-        final Http2StreamChannel channel = newInboundStream(3, false, handler);
-
-        ByteBuf tenBytes = bb("0123456789");
-
-        frameInboundWriter.writeInboundData(channel.stream().id(), tenBytes, 0, true);
-
-        // Verify we marked the bytes as consumed
-        verify(flowController).consumeBytes(argThat(new ArgumentMatcher<Http2Stream>() {
-            @Override
-            public boolean matches(Http2Stream http2Stream) {
-                return http2Stream.id() == channel.stream().id();
-            }
-        }), eq(10));
-
-        // headers and data frame
-        verifyFramesMultiplexedToCorrectChannel(channel, handler, 2);
-    }
-
-    @Test
-    public void unhandledHttp2FramesShouldBePropagated() {
-        Http2PingFrame pingFrame = new DefaultHttp2PingFrame(0);
-        frameInboundWriter.writeInboundPing(false, 0);
-        assertEquals(parentChannel.readInbound(), pingFrame);
-
-        DefaultHttp2GoAwayFrame goAwayFrame = new DefaultHttp2GoAwayFrame(1,
-                parentChannel.alloc().buffer().writeLong(8));
-        frameInboundWriter.writeInboundGoAway(0, goAwayFrame.errorCode(), goAwayFrame.content().retainedDuplicate());
-
-        Http2GoAwayFrame frame = parentChannel.readInbound();
-        assertEqualsAndRelease(frame, goAwayFrame);
-    }
-
-    @Test
-    public void channelReadShouldRespectAutoRead() {
-        LastInboundHandler inboundHandler = new LastInboundHandler();
-        Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler);
-        assertTrue(childChannel.config().isAutoRead());
-        Http2HeadersFrame headersFrame = inboundHandler.readInbound();
-        assertNotNull(headersFrame);
-
-        childChannel.config().setAutoRead(false);
-
-        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("hello world"), 0, false);
-        Http2DataFrame dataFrame0 = inboundHandler.readInbound();
-        assertNotNull(dataFrame0);
-        release(dataFrame0);
-
-        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("foo"), 0, false);
-        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("bar"), 0, false);
-
-        assertNull(inboundHandler.readInbound());
-
-        childChannel.config().setAutoRead(true);
-        verifyFramesMultiplexedToCorrectChannel(childChannel, inboundHandler, 2);
-    }
-
-    @Test
-    public void readInChannelReadWithoutAutoRead() {
-        useReadWithoutAutoRead(false);
-    }
-
-    @Test
-    public void readInChannelReadCompleteWithoutAutoRead() {
-        useReadWithoutAutoRead(true);
-    }
-
-    private void useReadWithoutAutoRead(final boolean readComplete) {
-        LastInboundHandler inboundHandler = new LastInboundHandler();
-        Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler);
-        assertTrue(childChannel.config().isAutoRead());
-        childChannel.config().setAutoRead(false);
-        assertFalse(childChannel.config().isAutoRead());
-
-        Http2HeadersFrame headersFrame = inboundHandler.readInbound();
-        assertNotNull(headersFrame);
-
-        // Add a handler which will request reads.
-        childChannel.pipeline().addFirst(new ChannelInboundHandlerAdapter() {
-            @Override
-            public void channelRead(ChannelHandlerContext ctx, Object msg) {
-                ctx.fireChannelRead(msg);
-                if (!readComplete) {
-                    ctx.read();
-                }
-            }
-
-            @Override
-            public void channelReadComplete(ChannelHandlerContext ctx) {
-                ctx.fireChannelReadComplete();
-                if (readComplete) {
-                    ctx.read();
-                }
-            }
-        });
-
-        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("hello world"), 0, false);
-        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("foo"), 0, false);
-        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("bar"), 0, false);
-        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("hello world"), 0, false);
-        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("foo"), 0, false);
-        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("bar"), 0, true);
-
-        verifyFramesMultiplexedToCorrectChannel(childChannel, inboundHandler, 6);
-    }
-
-    private Http2StreamChannel newOutboundStream(ChannelHandler handler) {
-        return new Http2StreamChannelBootstrap(parentChannel).handler(handler)
-                .open().syncUninterruptibly().getNow();
-    }
-
-    /**
-     * A child channel for a HTTP/2 stream in IDLE state (that is no headers sent or received),
-     * should not emit a RST_STREAM frame on close, as this is a connection error of type protocol error.
-     */
-    @Test
-    public void idleOutboundStreamShouldNotWriteResetFrameOnClose() {
-        LastInboundHandler handler = new LastInboundHandler();
-
-        Channel childChannel = newOutboundStream(handler);
-        assertTrue(childChannel.isActive());
-
-        childChannel.close();
-        parentChannel.runPendingTasks();
-
-        assertFalse(childChannel.isOpen());
-        assertFalse(childChannel.isActive());
-        assertNull(parentChannel.readOutbound());
-    }
-
-    @Test
-    public void outboundStreamShouldWriteResetFrameOnClose_headersSent() {
-        ChannelHandler handler = new ChannelInboundHandlerAdapter() {
-            @Override
-            public void channelActive(ChannelHandlerContext ctx) {
-                ctx.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers()));
-                ctx.fireChannelActive();
-            }
-        };
-
-        Http2StreamChannel childChannel = newOutboundStream(handler);
-        assertTrue(childChannel.isActive());
-
-        childChannel.close();
-        verify(frameWriter).writeRstStream(eqMultiplexCodecCtx(),
-                eqStreamId(childChannel), eq(Http2Error.CANCEL.code()), anyChannelPromise());
-    }
-
-    @Test
-    public void outboundStreamShouldNotWriteResetFrameOnClose_IfStreamDidntExist() {
-        when(frameWriter.writeHeaders(eqMultiplexCodecCtx(), anyInt(),
-                any(Http2Headers.class), anyInt(), anyShort(), anyBoolean(), anyInt(), anyBoolean(),
-                any(ChannelPromise.class))).thenAnswer(new Answer<ChannelFuture>() {
-
-            private boolean headersWritten;
-            @Override
-            public ChannelFuture answer(InvocationOnMock invocationOnMock) {
-                // We want to fail to write the first headers frame. This is what happens if the connection
-                // refuses to allocate a new stream due to having received a GOAWAY.
-                if (!headersWritten) {
-                    headersWritten = true;
-                    return ((ChannelPromise) invocationOnMock.getArgument(8)).setFailure(new Exception("boom"));
-                }
-                return ((ChannelPromise) invocationOnMock.getArgument(8)).setSuccess();
-            }
-        });
-
-        Http2StreamChannel childChannel = newOutboundStream(new ChannelInboundHandlerAdapter() {
-            @Override
-            public void channelActive(ChannelHandlerContext ctx) {
-                ctx.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers()));
-                ctx.fireChannelActive();
-            }
-        });
-
-        assertFalse(childChannel.isActive());
-
-        childChannel.close();
-        parentChannel.runPendingTasks();
-        // The channel was never active so we should not generate a RST frame.
-        verify(frameWriter, never()).writeRstStream(eqMultiplexCodecCtx(), eqStreamId(childChannel), anyLong(),
-                anyChannelPromise());
-
-        assertTrue(parentChannel.outboundMessages().isEmpty());
-    }
-
-    @Test
-    public void inboundRstStreamFireChannelInactive() {
-        LastInboundHandler inboundHandler = new LastInboundHandler();
-        Http2StreamChannel channel = newInboundStream(3, false, inboundHandler);
-        assertTrue(inboundHandler.isChannelActive());
-        frameInboundWriter.writeInboundRstStream(channel.stream().id(), Http2Error.INTERNAL_ERROR.code());
-
-        assertFalse(inboundHandler.isChannelActive());
-
-        // A RST_STREAM frame should NOT be emitted, as we received a RST_STREAM.
-        verify(frameWriter, Mockito.never()).writeRstStream(eqMultiplexCodecCtx(), eqStreamId(channel),
-                anyLong(), anyChannelPromise());
-    }
-
-    @Test(expected = StreamException.class)
-    public void streamExceptionTriggersChildChannelExceptionAndClose() throws Exception {
-        LastInboundHandler inboundHandler = new LastInboundHandler();
-        Http2StreamChannel channel = newInboundStream(3, false, inboundHandler);
-        assertTrue(channel.isActive());
-        StreamException cause = new StreamException(channel.stream().id(), Http2Error.PROTOCOL_ERROR, "baaam!");
-        parentChannel.pipeline().fireExceptionCaught(cause);
-
-        assertFalse(channel.isActive());
-        inboundHandler.checkException();
-    }
-
-    @Test(expected = ClosedChannelException.class)
-    public void streamClosedErrorTranslatedToClosedChannelExceptionOnWrites() throws Exception {
-        LastInboundHandler inboundHandler = new LastInboundHandler();
-
-        final Http2StreamChannel childChannel = newOutboundStream(inboundHandler);
-        assertTrue(childChannel.isActive());
-
-        Http2Headers headers = new DefaultHttp2Headers();
-        when(frameWriter.writeHeaders(eqMultiplexCodecCtx(), anyInt(),
-                eq(headers), anyInt(), anyShort(), anyBoolean(), anyInt(), anyBoolean(),
-                any(ChannelPromise.class))).thenAnswer(new Answer<ChannelFuture>() {
-            @Override
-            public ChannelFuture answer(InvocationOnMock invocationOnMock) {
-                return ((ChannelPromise) invocationOnMock.getArgument(8)).setFailure(
-                        new StreamException(childChannel.stream().id(), Http2Error.STREAM_CLOSED, "Stream Closed"));
-            }
-        });
-        ChannelFuture future = childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers()));
-
-        parentChannel.flush();
-
-        assertFalse(childChannel.isActive());
-        assertFalse(childChannel.isOpen());
-
-        inboundHandler.checkException();
-
-        future.syncUninterruptibly();
-    }
-
-    @Test
-    public void creatingWritingReadingAndClosingOutboundStreamShouldWork() {
-        LastInboundHandler inboundHandler = new LastInboundHandler();
-        Http2StreamChannel childChannel = newOutboundStream(inboundHandler);
-        assertTrue(childChannel.isActive());
-        assertTrue(inboundHandler.isChannelActive());
-
-        // Write to the child channel
-        Http2Headers headers = new DefaultHttp2Headers().scheme("https").method("GET").path("/foo.txt");
-        childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(headers));
-
-        // Read from the child channel
-        frameInboundWriter.writeInboundHeaders(childChannel.stream().id(), headers, 0, false);
-
-        Http2HeadersFrame headersFrame = inboundHandler.readInbound();
-        assertNotNull(headersFrame);
-        assertEquals(headers, headersFrame.headers());
-
-        // Close the child channel.
-        childChannel.close();
-
-        parentChannel.runPendingTasks();
-        // An active outbound stream should emit a RST_STREAM frame.
-        verify(frameWriter).writeRstStream(eqMultiplexCodecCtx(), eqStreamId(childChannel),
-                anyLong(), anyChannelPromise());
-
-        assertFalse(childChannel.isOpen());
-        assertFalse(childChannel.isActive());
-        assertFalse(inboundHandler.isChannelActive());
-    }
-
-    // Test failing the promise of the first headers frame of an outbound stream. In practice this error case would most
-    // likely happen due to the max concurrent streams limit being hit or the channel running out of stream identifiers.
-    //
-    @Test(expected = Http2NoMoreStreamIdsException.class)
-    public void failedOutboundStreamCreationThrowsAndClosesChannel() throws Exception {
-        LastInboundHandler handler = new LastInboundHandler();
-        Http2StreamChannel childChannel = newOutboundStream(handler);
-        assertTrue(childChannel.isActive());
-
-        Http2Headers headers = new DefaultHttp2Headers();
-        when(frameWriter.writeHeaders(eqMultiplexCodecCtx(), anyInt(),
-               eq(headers), anyInt(), anyShort(), anyBoolean(), anyInt(), anyBoolean(),
-               any(ChannelPromise.class))).thenAnswer(new Answer<ChannelFuture>() {
-           @Override
-           public ChannelFuture answer(InvocationOnMock invocationOnMock) {
-               return ((ChannelPromise) invocationOnMock.getArgument(8)).setFailure(
-                       new Http2NoMoreStreamIdsException());
-            }
-        });
-
-        ChannelFuture future = childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(headers));
-        parentChannel.flush();
-
-        assertFalse(childChannel.isActive());
-        assertFalse(childChannel.isOpen());
-
-        handler.checkException();
-
-        future.syncUninterruptibly();
-    }
-
-    @Test
-    public void channelClosedWhenCloseListenerCompletes() {
-        LastInboundHandler inboundHandler = new LastInboundHandler();
-        Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler);
-
-        assertTrue(childChannel.isOpen());
-        assertTrue(childChannel.isActive());
-
-        final AtomicBoolean channelOpen = new AtomicBoolean(true);
-        final AtomicBoolean channelActive = new AtomicBoolean(true);
-
-        // Create a promise before actually doing the close, because otherwise we would be adding a listener to a future
-        // that is already completed because we are using EmbeddedChannel which executes code in the JUnit thread.
-        ChannelPromise p = childChannel.newPromise();
-        p.addListener(new ChannelFutureListener() {
-            @Override
-            public void operationComplete(ChannelFuture future) {
-                channelOpen.set(future.channel().isOpen());
-                channelActive.set(future.channel().isActive());
-            }
-        });
-        childChannel.close(p).syncUninterruptibly();
-
-        assertFalse(channelOpen.get());
-        assertFalse(channelActive.get());
-        assertFalse(childChannel.isActive());
-    }
-
-    @Test
-    public void channelClosedWhenChannelClosePromiseCompletes() {
-         LastInboundHandler inboundHandler = new LastInboundHandler();
-         Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler);
-
-         assertTrue(childChannel.isOpen());
-         assertTrue(childChannel.isActive());
-
-         final AtomicBoolean channelOpen = new AtomicBoolean(true);
-         final AtomicBoolean channelActive = new AtomicBoolean(true);
-
-         childChannel.closeFuture().addListener(new ChannelFutureListener() {
-             @Override
-             public void operationComplete(ChannelFuture future) {
-                 channelOpen.set(future.channel().isOpen());
-                 channelActive.set(future.channel().isActive());
-             }
-         });
-         childChannel.close().syncUninterruptibly();
-
-         assertFalse(channelOpen.get());
-         assertFalse(channelActive.get());
-         assertFalse(childChannel.isActive());
-    }
-
-    @Test
-    public void channelClosedWhenWriteFutureFails() {
-        final Queue<ChannelPromise> writePromises = new ArrayDeque<ChannelPromise>();
-
-        LastInboundHandler inboundHandler = new LastInboundHandler();
-        Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler);
-
-        assertTrue(childChannel.isOpen());
-        assertTrue(childChannel.isActive());
-
-        final AtomicBoolean channelOpen = new AtomicBoolean(true);
-        final AtomicBoolean channelActive = new AtomicBoolean(true);
-
-        Http2Headers headers = new DefaultHttp2Headers();
-        when(frameWriter.writeHeaders(eqMultiplexCodecCtx(), anyInt(),
-                eq(headers), anyInt(), anyShort(), anyBoolean(), anyInt(), anyBoolean(),
-                any(ChannelPromise.class))).thenAnswer(new Answer<ChannelFuture>() {
-            @Override
-            public ChannelFuture answer(InvocationOnMock invocationOnMock) {
-                ChannelPromise promise = invocationOnMock.getArgument(8);
-                writePromises.offer(promise);
-                return promise;
-            }
-        });
-
-        ChannelFuture f = childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(headers));
-        assertFalse(f.isDone());
-        f.addListener(new ChannelFutureListener() {
-            @Override
-            public void operationComplete(ChannelFuture future) throws Exception {
-                channelOpen.set(future.channel().isOpen());
-                channelActive.set(future.channel().isActive());
-            }
-        });
-
-        ChannelPromise first = writePromises.poll();
-        first.setFailure(new ClosedChannelException());
-        f.awaitUninterruptibly();
-
-        assertFalse(channelOpen.get());
-        assertFalse(channelActive.get());
-        assertFalse(childChannel.isActive());
-    }
-
-    @Test
-    public void channelClosedTwiceMarksPromiseAsSuccessful() {
-        LastInboundHandler inboundHandler = new LastInboundHandler();
-        Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler);
-
-        assertTrue(childChannel.isOpen());
-        assertTrue(childChannel.isActive());
-        childChannel.close().syncUninterruptibly();
-        childChannel.close().syncUninterruptibly();
-
-        assertFalse(childChannel.isOpen());
-        assertFalse(childChannel.isActive());
-    }
-
-    @Test
-    public void settingChannelOptsAndAttrs() {
-        AttributeKey<String> key = AttributeKey.newInstance("foo");
-
-        Channel childChannel = newOutboundStream(new ChannelInboundHandlerAdapter());
-        childChannel.config().setAutoRead(false).setWriteSpinCount(1000);
-        childChannel.attr(key).set("bar");
-        assertFalse(childChannel.config().isAutoRead());
-        assertEquals(1000, childChannel.config().getWriteSpinCount());
-        assertEquals("bar", childChannel.attr(key).get());
-    }
-
-    @Test
-    public void outboundFlowControlWritability() {
-        Http2StreamChannel childChannel = newOutboundStream(new ChannelInboundHandlerAdapter());
-        assertTrue(childChannel.isActive());
-
-        assertTrue(childChannel.isWritable());
-        childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers()));
-        parentChannel.flush();
-
-        // Test for initial window size
-        assertEquals(initialRemoteStreamWindow, childChannel.config().getWriteBufferHighWaterMark());
-
-        assertTrue(childChannel.isWritable());
-        childChannel.write(new DefaultHttp2DataFrame(Unpooled.buffer().writeZero(16 * 1024 * 1024)));
-        assertFalse(childChannel.isWritable());
-    }
-
-    @Test
-    public void writabilityAndFlowControl() {
-        LastInboundHandler inboundHandler = new LastInboundHandler();
-        Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler);
-        assertEquals("", inboundHandler.writabilityStates());
-
-        assertTrue(childChannel.isWritable());
-        // HEADERS frames are not flow controlled, so they should not affect the flow control window.
-        childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers()));
-        codec.onHttp2StreamWritabilityChanged(codec.ctx, childChannel.stream(), true);
-
-        assertTrue(childChannel.isWritable());
-        assertEquals("", inboundHandler.writabilityStates());
-
-        codec.onHttp2StreamWritabilityChanged(codec.ctx, childChannel.stream(), true);
-        assertTrue(childChannel.isWritable());
-        assertEquals("", inboundHandler.writabilityStates());
-
-        codec.onHttp2StreamWritabilityChanged(codec.ctx, childChannel.stream(), false);
-        assertFalse(childChannel.isWritable());
-        assertEquals("false", inboundHandler.writabilityStates());
-
-        codec.onHttp2StreamWritabilityChanged(codec.ctx, childChannel.stream(), false);
-        assertFalse(childChannel.isWritable());
-        assertEquals("false", inboundHandler.writabilityStates());
-    }
-
-    @Test
-    public void channelClosedWhenInactiveFired() {
-        LastInboundHandler inboundHandler = new LastInboundHandler();
-        Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler);
-
-        final AtomicBoolean channelOpen = new AtomicBoolean(false);
-        final AtomicBoolean channelActive = new AtomicBoolean(false);
-        assertTrue(childChannel.isOpen());
-        assertTrue(childChannel.isActive());
-
-        childChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
-            @Override
-            public void channelInactive(ChannelHandlerContext ctx) throws Exception {
-                channelOpen.set(ctx.channel().isOpen());
-                channelActive.set(ctx.channel().isActive());
-
-                super.channelInactive(ctx);
-            }
-        });
-
-        childChannel.close().syncUninterruptibly();
-        assertFalse(channelOpen.get());
-        assertFalse(channelActive.get());
-    }
-
-    @Test
-    public void channelInactiveHappensAfterExceptionCaughtEvents() throws Exception {
-        final AtomicInteger count = new AtomicInteger(0);
-        final AtomicInteger exceptionCaught = new AtomicInteger(-1);
-        final AtomicInteger channelInactive = new AtomicInteger(-1);
-        final AtomicInteger channelUnregistered = new AtomicInteger(-1);
-        Http2StreamChannel childChannel = newOutboundStream(new ChannelInboundHandlerAdapter() {
-
-            @Override
-            public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
-                ctx.close();
-                throw new Exception("exception");
-            }
-        });
-
-        childChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
-
-            @Override
-            public void channelInactive(ChannelHandlerContext ctx) throws Exception {
-                channelInactive.set(count.getAndIncrement());
-                super.channelInactive(ctx);
-            }
-
-            @Override
-            public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
-                exceptionCaught.set(count.getAndIncrement());
-                super.exceptionCaught(ctx, cause);
-            }
-
-            @Override
-            public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
-                channelUnregistered.set(count.getAndIncrement());
-                super.channelUnregistered(ctx);
-            }
-        });
-
-        childChannel.pipeline().fireUserEventTriggered(new Object());
-        parentChannel.runPendingTasks();
-
-        // The events should have happened in this order because the inactive and deregistration events
-        // get deferred as they do in the AbstractChannel.
-        assertEquals(0, exceptionCaught.get());
-        assertEquals(1, channelInactive.get());
-        assertEquals(2, channelUnregistered.get());
-    }
-
-    @Test
-    public void callUnsafeCloseMultipleTimes() {
-        LastInboundHandler inboundHandler = new LastInboundHandler();
-        Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler);
-        childChannel.unsafe().close(childChannel.voidPromise());
-
-        ChannelPromise promise = childChannel.newPromise();
-        childChannel.unsafe().close(promise);
-        promise.syncUninterruptibly();
-        childChannel.closeFuture().syncUninterruptibly();
-    }
-
-    @Test
-    public void endOfStreamDoesNotDiscardData() {
-        AtomicInteger numReads = new AtomicInteger(1);
-        final AtomicBoolean shouldDisableAutoRead = new AtomicBoolean();
-        Consumer<ChannelHandlerContext> ctxConsumer = new Consumer<ChannelHandlerContext>() {
-            @Override
-            public void accept(ChannelHandlerContext obj) {
-                if (shouldDisableAutoRead.get()) {
-                    obj.channel().config().setAutoRead(false);
-                }
-            }
-        };
-        LastInboundHandler inboundHandler = new LastInboundHandler(ctxConsumer);
-        Http2StreamChannel childChannel = newInboundStream(3, false, numReads, inboundHandler);
-        childChannel.config().setAutoRead(false);
-
-        Http2DataFrame dataFrame1 = new DefaultHttp2DataFrame(bb("1")).stream(childChannel.stream());
-        Http2DataFrame dataFrame2 = new DefaultHttp2DataFrame(bb("2")).stream(childChannel.stream());
-        Http2DataFrame dataFrame3 = new DefaultHttp2DataFrame(bb("3")).stream(childChannel.stream());
-        Http2DataFrame dataFrame4 = new DefaultHttp2DataFrame(bb("4")).stream(childChannel.stream());
-
-        assertEquals(new DefaultHttp2HeadersFrame(request).stream(childChannel.stream()), inboundHandler.readInbound());
-
-        ChannelHandler readCompleteSupressHandler = new ChannelInboundHandlerAdapter() {
-            @Override
-            public void channelReadComplete(ChannelHandlerContext ctx) {
-                // We want to simulate the parent channel calling channelRead and delay calling channelReadComplete.
-            }
-        };
-
-        parentChannel.pipeline().addFirst(readCompleteSupressHandler);
-        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("1"), 0, false);
-
-        assertEqualsAndRelease(dataFrame1, inboundHandler.<Http2DataFrame>readInbound());
-
-        // Deliver frames, and then a stream closed while read is inactive.
-        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("2"), 0, false);
-        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("3"), 0, false);
-        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("4"), 0, false);
-
-        shouldDisableAutoRead.set(true);
-        childChannel.config().setAutoRead(true);
-        numReads.set(1);
-
-        frameInboundWriter.writeInboundRstStream(childChannel.stream().id(), Http2Error.NO_ERROR.code());
-
-        // Detecting EOS should flush all pending data regardless of read calls.
-        assertEqualsAndRelease(dataFrame2, inboundHandler.<Http2DataFrame>readInbound());
-        assertEqualsAndRelease(dataFrame3, inboundHandler.<Http2DataFrame>readInbound());
-        assertEqualsAndRelease(dataFrame4, inboundHandler.<Http2DataFrame>readInbound());
-
-        Http2ResetFrame resetFrame = inboundHandler.readInbound();
-        assertEquals(childChannel.stream(), resetFrame.stream());
-        assertEquals(Http2Error.NO_ERROR.code(), resetFrame.errorCode());
-
-        assertNull(inboundHandler.readInbound());
-
-        // Now we want to call channelReadComplete and simulate the end of the read loop.
-        parentChannel.pipeline().remove(readCompleteSupressHandler);
-        parentChannel.flushInbound();
-
-        childChannel.closeFuture().syncUninterruptibly();
-    }
-
-    @Test
-    public void childQueueIsDrainedAndNewDataIsDispatchedInParentReadLoopAutoRead() {
-        AtomicInteger numReads = new AtomicInteger(1);
-        final AtomicInteger channelReadCompleteCount = new AtomicInteger(0);
-        final AtomicBoolean shouldDisableAutoRead = new AtomicBoolean();
-        Consumer<ChannelHandlerContext> ctxConsumer = new Consumer<ChannelHandlerContext>() {
-            @Override
-            public void accept(ChannelHandlerContext obj) {
-                channelReadCompleteCount.incrementAndGet();
-                if (shouldDisableAutoRead.get()) {
-                    obj.channel().config().setAutoRead(false);
-                }
-            }
-        };
-        LastInboundHandler inboundHandler = new LastInboundHandler(ctxConsumer);
-        Http2StreamChannel childChannel = newInboundStream(3, false, numReads, inboundHandler);
-        childChannel.config().setAutoRead(false);
-
-        Http2DataFrame dataFrame1 = new DefaultHttp2DataFrame(bb("1")).stream(childChannel.stream());
-        Http2DataFrame dataFrame2 = new DefaultHttp2DataFrame(bb("2")).stream(childChannel.stream());
-        Http2DataFrame dataFrame3 = new DefaultHttp2DataFrame(bb("3")).stream(childChannel.stream());
-        Http2DataFrame dataFrame4 = new DefaultHttp2DataFrame(bb("4")).stream(childChannel.stream());
-
-        assertEquals(new DefaultHttp2HeadersFrame(request).stream(childChannel.stream()), inboundHandler.readInbound());
-
-        ChannelHandler readCompleteSupressHandler = new ChannelInboundHandlerAdapter() {
-            @Override
-            public void channelReadComplete(ChannelHandlerContext ctx) {
-                // We want to simulate the parent channel calling channelRead and delay calling channelReadComplete.
-            }
-        };
-        parentChannel.pipeline().addFirst(readCompleteSupressHandler);
-
-        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("1"), 0, false);
-
-        assertEqualsAndRelease(dataFrame1, inboundHandler.<Http2DataFrame>readInbound());
-
-        // We want one item to be in the queue, and allow the numReads to be larger than 1. This will ensure that
-        // when beginRead() is called the child channel is added to the readPending queue of the parent channel.
-        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("2"), 0, false);
-
-        numReads.set(10);
-        shouldDisableAutoRead.set(true);
-        childChannel.config().setAutoRead(true);
-
-        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("3"), 0, false);
-        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("4"), 0, false);
-
-        // Detecting EOS should flush all pending data regardless of read calls.
-        assertEqualsAndRelease(dataFrame2, inboundHandler.<Http2DataFrame>readInbound());
-        assertEqualsAndRelease(dataFrame3, inboundHandler.<Http2DataFrame>readInbound());
-        assertEqualsAndRelease(dataFrame4, inboundHandler.<Http2DataFrame>readInbound());
-
-        assertNull(inboundHandler.readInbound());
-
-        // Now we want to call channelReadComplete and simulate the end of the read loop.
-        parentChannel.pipeline().remove(readCompleteSupressHandler);
-        parentChannel.flushInbound();
-
-        // 3 = 1 for initialization + 1 for read when auto read was off + 1 for when auto read was back on
-        assertEquals(3, channelReadCompleteCount.get());
+    @Override
+    protected Http2FrameCodec newCodec(TestChannelInitializer childChannelInitializer, Http2FrameWriter frameWriter) {
+        return new Http2MultiplexCodecBuilder(true, childChannelInitializer).frameWriter(frameWriter).build();
     }
 
-    @Test
-    public void childQueueIsDrainedAndNewDataIsDispatchedInParentReadLoopNoAutoRead() {
-        final AtomicInteger numReads = new AtomicInteger(1);
-        final AtomicInteger channelReadCompleteCount = new AtomicInteger(0);
-        final AtomicBoolean shouldDisableAutoRead = new AtomicBoolean();
-        Consumer<ChannelHandlerContext> ctxConsumer = new Consumer<ChannelHandlerContext>() {
-            @Override
-            public void accept(ChannelHandlerContext obj) {
-                channelReadCompleteCount.incrementAndGet();
-                if (shouldDisableAutoRead.get()) {
-                    obj.channel().config().setAutoRead(false);
-                }
-            }
-        };
-        final LastInboundHandler inboundHandler = new LastInboundHandler(ctxConsumer);
-        Http2StreamChannel childChannel = newInboundStream(3, false, numReads, inboundHandler);
-        childChannel.config().setAutoRead(false);
-
-        Http2DataFrame dataFrame1 = new DefaultHttp2DataFrame(bb("1")).stream(childChannel.stream());
-        Http2DataFrame dataFrame2 = new DefaultHttp2DataFrame(bb("2")).stream(childChannel.stream());
-        Http2DataFrame dataFrame3 = new DefaultHttp2DataFrame(bb("3")).stream(childChannel.stream());
-        Http2DataFrame dataFrame4 = new DefaultHttp2DataFrame(bb("4")).stream(childChannel.stream());
-
-        assertEquals(new DefaultHttp2HeadersFrame(request).stream(childChannel.stream()), inboundHandler.readInbound());
-
-        ChannelHandler readCompleteSupressHandler = new ChannelInboundHandlerAdapter() {
-            @Override
-            public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
-                // We want to simulate the parent channel calling channelRead and delay calling channelReadComplete.
-            }
-        };
-        parentChannel.pipeline().addFirst(readCompleteSupressHandler);
-
-        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("1"), 0, false);
-
-        assertEqualsAndRelease(dataFrame1, inboundHandler.<Http2Frame>readInbound());
-
-        // We want one item to be in the queue, and allow the numReads to be larger than 1. This will ensure that
-        // when beginRead() is called the child channel is added to the readPending queue of the parent channel.
-        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("2"), 0, false);
-
-        numReads.set(2);
-        childChannel.read();
-
-        assertEqualsAndRelease(dataFrame2, inboundHandler.<Http2Frame>readInbound());
-
-        assertNull(inboundHandler.readInbound());
-
-        // This is the second item that was read, this should be the last until we call read() again. This should also
-        // notify of readComplete().
-        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("3"), 0, false);
-
-        assertEqualsAndRelease(dataFrame3, inboundHandler.<Http2Frame>readInbound());
-
-        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("4"), 0, false);
-        assertNull(inboundHandler.readInbound());
-
-        childChannel.read();
-
-        assertEqualsAndRelease(dataFrame4, inboundHandler.<Http2Frame>readInbound());
-
-        assertNull(inboundHandler.readInbound());
-
-        // Now we want to call channelReadComplete and simulate the end of the read loop.
-        parentChannel.pipeline().remove(readCompleteSupressHandler);
-        parentChannel.flushInbound();
-
-        // 3 = 1 for initialization + 1 for first read of 2 items + 1 for second read of 2 items +
-        // 1 for parent channel readComplete
-        assertEquals(4, channelReadCompleteCount.get());
+    @Override
+    protected ChannelHandler newMultiplexer(TestChannelInitializer childChannelInitializer) {
+        return null;
     }
 
-    private static void verifyFramesMultiplexedToCorrectChannel(Http2StreamChannel streamChannel,
-                                                                LastInboundHandler inboundHandler,
-                                                                int numFrames) {
-        for (int i = 0; i < numFrames; i++) {
-            Http2StreamFrame frame = inboundHandler.readInbound();
-            assertNotNull(frame);
-            assertEquals(streamChannel.stream(), frame.stream());
-            release(frame);
-        }
-        assertNull(inboundHandler.readInbound());
+    @Override
+    protected boolean useUserEventForResetFrame() {
+        return false;
     }
 
-    private static int eqStreamId(Http2StreamChannel channel) {
-        return eq(channel.stream().id());
+    @Override
+    protected boolean ignoreWindowUpdateFrames() {
+        return false;
     }
 }
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexHandlerClientUpgradeTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexHandlerClientUpgradeTest.java
new file mode 100644
index 0000000..281c8da
--- /dev/null
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexHandlerClientUpgradeTest.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2019 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.channel.ChannelHandler;
+
+public class Http2MultiplexHandlerClientUpgradeTest extends Http2MultiplexClientUpgradeTest<Http2FrameCodec> {
+
+    @Override
+    protected Http2FrameCodec newCodec(ChannelHandler upgradeHandler) {
+        return Http2FrameCodecBuilder.forClient().build();
+    }
+
+    @Override
+    protected ChannelHandler newMultiplexer(ChannelHandler upgradeHandler) {
+        return new Http2MultiplexHandler(new NoopHandler(), upgradeHandler);
+    }
+}
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexHandlerTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexHandlerTest.java
new file mode 100644
index 0000000..a37f74b
--- /dev/null
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexHandlerTest.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2019 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.channel.ChannelHandler;
+
+/**
+ * Unit tests for {@link Http2MultiplexHandler}.
+ */
+public class Http2MultiplexHandlerTest extends Http2MultiplexTest<Http2FrameCodec> {
+
+    @Override
+    protected Http2FrameCodec newCodec(TestChannelInitializer childChannelInitializer, Http2FrameWriter frameWriter) {
+        return new Http2FrameCodecBuilder(true).frameWriter(frameWriter).build();
+    }
+
+    @Override
+    protected ChannelHandler newMultiplexer(TestChannelInitializer childChannelInitializer) {
+        return new Http2MultiplexHandler(childChannelInitializer, null);
+    }
+
+    @Override
+    protected boolean useUserEventForResetFrame() {
+        return true;
+    }
+
+    @Override
+    protected boolean ignoreWindowUpdateFrames() {
+        return true;
+    }
+}
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexTest.java
new file mode 100644
index 0000000..c1207f6
--- /dev/null
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexTest.java
@@ -0,0 +1,1230 @@
+/*
+ * Copyright 2016 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.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.channel.ChannelOutboundHandlerAdapter;
+import io.netty.channel.ChannelPromise;
+import io.netty.channel.WriteBufferWaterMark;
+import io.netty.channel.embedded.EmbeddedChannel;
+import io.netty.handler.codec.http.HttpMethod;
+import io.netty.handler.codec.http.HttpScheme;
+import io.netty.handler.codec.http2.Http2Exception.StreamException;
+import io.netty.handler.codec.http2.LastInboundHandler.Consumer;
+import io.netty.util.AsciiString;
+import io.netty.util.AttributeKey;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentMatcher;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.net.InetSocketAddress;
+import java.nio.channels.ClosedChannelException;
+import java.util.ArrayDeque;
+import java.util.Queue;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static io.netty.handler.codec.http2.Http2TestUtil.anyChannelPromise;
+import static io.netty.handler.codec.http2.Http2TestUtil.anyHttp2Settings;
+import static io.netty.handler.codec.http2.Http2TestUtil.assertEqualsAndRelease;
+import static io.netty.handler.codec.http2.Http2TestUtil.bb;
+import static io.netty.util.ReferenceCountUtil.release;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public abstract class Http2MultiplexTest<C extends Http2FrameCodec> {
+    private final Http2Headers request = new DefaultHttp2Headers()
+            .method(HttpMethod.GET.asciiName()).scheme(HttpScheme.HTTPS.name())
+            .authority(new AsciiString("example.org")).path(new AsciiString("/foo"));
+
+    private EmbeddedChannel parentChannel;
+    private Http2FrameWriter frameWriter;
+    private Http2FrameInboundWriter frameInboundWriter;
+    private TestChannelInitializer childChannelInitializer;
+    private C codec;
+
+    private static final int initialRemoteStreamWindow = 1024;
+
+    protected abstract C newCodec(TestChannelInitializer childChannelInitializer,  Http2FrameWriter frameWriter);
+    protected abstract ChannelHandler newMultiplexer(TestChannelInitializer childChannelInitializer);
+
+    @Before
+    public void setUp() {
+        childChannelInitializer = new TestChannelInitializer();
+        parentChannel = new EmbeddedChannel();
+        frameInboundWriter = new Http2FrameInboundWriter(parentChannel);
+        parentChannel.connect(new InetSocketAddress(0));
+        frameWriter = Http2TestUtil.mockedFrameWriter();
+        codec = newCodec(childChannelInitializer, frameWriter);
+        parentChannel.pipeline().addLast(codec);
+        ChannelHandler multiplexer = newMultiplexer(childChannelInitializer);
+        if (multiplexer != null) {
+            parentChannel.pipeline().addLast(multiplexer);
+        }
+
+        parentChannel.runPendingTasks();
+        parentChannel.pipeline().fireChannelActive();
+
+        parentChannel.writeInbound(Http2CodecUtil.connectionPrefaceBuf());
+
+        Http2Settings settings = new Http2Settings().initialWindowSize(initialRemoteStreamWindow);
+        frameInboundWriter.writeInboundSettings(settings);
+
+        verify(frameWriter).writeSettingsAck(eqCodecCtx(), anyChannelPromise());
+
+        frameInboundWriter.writeInboundSettingsAck();
+
+        Http2SettingsFrame settingsFrame = parentChannel.readInbound();
+        assertNotNull(settingsFrame);
+        Http2SettingsAckFrame settingsAckFrame = parentChannel.readInbound();
+        assertNotNull(settingsAckFrame);
+
+        // Handshake
+        verify(frameWriter).writeSettings(eqCodecCtx(),
+                anyHttp2Settings(), anyChannelPromise());
+    }
+
+    private ChannelHandlerContext eqCodecCtx() {
+        return eq(codec.ctx);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (childChannelInitializer.handler instanceof LastInboundHandler) {
+            ((LastInboundHandler) childChannelInitializer.handler).finishAndReleaseAll();
+        }
+        parentChannel.finishAndReleaseAll();
+        codec = null;
+    }
+
+    // TODO(buchgr): Flush from child channel
+    // TODO(buchgr): ChildChannel.childReadComplete()
+    // TODO(buchgr): GOAWAY Logic
+    // TODO(buchgr): Test ChannelConfig.setMaxMessagesPerRead
+
+    @Test
+    public void writeUnknownFrame() {
+        Http2StreamChannel childChannel = newOutboundStream(new ChannelInboundHandlerAdapter() {
+            @Override
+            public void channelActive(ChannelHandlerContext ctx) {
+                ctx.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers()));
+                ctx.writeAndFlush(new DefaultHttp2UnknownFrame((byte) 99, new Http2Flags()));
+                ctx.fireChannelActive();
+            }
+        });
+        assertTrue(childChannel.isActive());
+
+        parentChannel.runPendingTasks();
+
+        verify(frameWriter).writeFrame(eq(codec.ctx), eq((byte) 99), eqStreamId(childChannel), any(Http2Flags.class),
+                any(ByteBuf.class), any(ChannelPromise.class));
+    }
+
+    private Http2StreamChannel newInboundStream(int streamId, boolean endStream, final ChannelHandler childHandler) {
+        return newInboundStream(streamId, endStream, null, childHandler);
+    }
+
+    private Http2StreamChannel newInboundStream(int streamId, boolean endStream,
+                                                AtomicInteger maxReads, final ChannelHandler childHandler) {
+        final AtomicReference<Http2StreamChannel> streamChannelRef = new AtomicReference<Http2StreamChannel>();
+        childChannelInitializer.maxReads = maxReads;
+        childChannelInitializer.handler = new ChannelInboundHandlerAdapter() {
+            @Override
+            public void channelRegistered(ChannelHandlerContext ctx) {
+                assertNull(streamChannelRef.get());
+                streamChannelRef.set((Http2StreamChannel) ctx.channel());
+                ctx.pipeline().addLast(childHandler);
+                ctx.fireChannelRegistered();
+            }
+        };
+
+        frameInboundWriter.writeInboundHeaders(streamId, request, 0, endStream);
+        parentChannel.runPendingTasks();
+        Http2StreamChannel channel = streamChannelRef.get();
+        assertEquals(streamId, channel.stream().id());
+        return channel;
+    }
+
+    @Test
+    public void readUnkownFrame() {
+        LastInboundHandler handler = new LastInboundHandler();
+
+        Http2StreamChannel channel = newInboundStream(3, true, handler);
+        frameInboundWriter.writeInboundFrame((byte) 99, channel.stream().id(), new Http2Flags(), Unpooled.EMPTY_BUFFER);
+
+        // header frame and unknown frame
+        verifyFramesMultiplexedToCorrectChannel(channel, handler, 2);
+
+        Channel childChannel = newOutboundStream(new ChannelInboundHandlerAdapter());
+        assertTrue(childChannel.isActive());
+    }
+
+    @Test
+    public void headerAndDataFramesShouldBeDelivered() {
+        LastInboundHandler inboundHandler = new LastInboundHandler();
+
+        Http2StreamChannel channel = newInboundStream(3, false, inboundHandler);
+        Http2HeadersFrame headersFrame = new DefaultHttp2HeadersFrame(request).stream(channel.stream());
+        Http2DataFrame dataFrame1 = new DefaultHttp2DataFrame(bb("hello")).stream(channel.stream());
+        Http2DataFrame dataFrame2 = new DefaultHttp2DataFrame(bb("world")).stream(channel.stream());
+
+        assertTrue(inboundHandler.isChannelActive());
+        frameInboundWriter.writeInboundData(channel.stream().id(), bb("hello"), 0, false);
+        frameInboundWriter.writeInboundData(channel.stream().id(), bb("world"), 0, false);
+
+        assertEquals(headersFrame, inboundHandler.readInbound());
+
+        assertEqualsAndRelease(dataFrame1, inboundHandler.<Http2Frame>readInbound());
+        assertEqualsAndRelease(dataFrame2, inboundHandler.<Http2Frame>readInbound());
+
+        assertNull(inboundHandler.readInbound());
+    }
+
+    @Test
+    public void framesShouldBeMultiplexed() {
+        LastInboundHandler handler1 = new LastInboundHandler();
+        Http2StreamChannel channel1 = newInboundStream(3, false, handler1);
+        LastInboundHandler handler2 = new LastInboundHandler();
+        Http2StreamChannel channel2 = newInboundStream(5, false, handler2);
+        LastInboundHandler handler3 = new LastInboundHandler();
+        Http2StreamChannel channel3 = newInboundStream(11, false, handler3);
+
+        verifyFramesMultiplexedToCorrectChannel(channel1, handler1, 1);
+        verifyFramesMultiplexedToCorrectChannel(channel2, handler2, 1);
+        verifyFramesMultiplexedToCorrectChannel(channel3, handler3, 1);
+
+        frameInboundWriter.writeInboundData(channel2.stream().id(), bb("hello"), 0, false);
+        frameInboundWriter.writeInboundData(channel1.stream().id(), bb("foo"), 0, true);
+        frameInboundWriter.writeInboundData(channel2.stream().id(), bb("world"), 0, true);
+        frameInboundWriter.writeInboundData(channel3.stream().id(), bb("bar"), 0, true);
+
+        verifyFramesMultiplexedToCorrectChannel(channel1, handler1, 1);
+        verifyFramesMultiplexedToCorrectChannel(channel2, handler2, 2);
+        verifyFramesMultiplexedToCorrectChannel(channel3, handler3, 1);
+    }
+
+    @Test
+    public void inboundDataFrameShouldUpdateLocalFlowController() throws Http2Exception {
+        Http2LocalFlowController flowController = Mockito.mock(Http2LocalFlowController.class);
+        codec.connection().local().flowController(flowController);
+
+        LastInboundHandler handler = new LastInboundHandler();
+        final Http2StreamChannel channel = newInboundStream(3, false, handler);
+
+        ByteBuf tenBytes = bb("0123456789");
+
+        frameInboundWriter.writeInboundData(channel.stream().id(), tenBytes, 0, true);
+
+        // Verify we marked the bytes as consumed
+        verify(flowController).consumeBytes(argThat(new ArgumentMatcher<Http2Stream>() {
+            @Override
+            public boolean matches(Http2Stream http2Stream) {
+                return http2Stream.id() == channel.stream().id();
+            }
+        }), eq(10));
+
+        // headers and data frame
+        verifyFramesMultiplexedToCorrectChannel(channel, handler, 2);
+    }
+
+    @Test
+    public void unhandledHttp2FramesShouldBePropagated() {
+        Http2PingFrame pingFrame = new DefaultHttp2PingFrame(0);
+        frameInboundWriter.writeInboundPing(false, 0);
+        assertEquals(parentChannel.readInbound(), pingFrame);
+
+        DefaultHttp2GoAwayFrame goAwayFrame = new DefaultHttp2GoAwayFrame(1,
+                parentChannel.alloc().buffer().writeLong(8));
+        frameInboundWriter.writeInboundGoAway(0, goAwayFrame.errorCode(), goAwayFrame.content().retainedDuplicate());
+
+        Http2GoAwayFrame frame = parentChannel.readInbound();
+        assertEqualsAndRelease(frame, goAwayFrame);
+    }
+
+    @Test
+    public void channelReadShouldRespectAutoRead() {
+        LastInboundHandler inboundHandler = new LastInboundHandler();
+        Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler);
+        assertTrue(childChannel.config().isAutoRead());
+        Http2HeadersFrame headersFrame = inboundHandler.readInbound();
+        assertNotNull(headersFrame);
+
+        childChannel.config().setAutoRead(false);
+
+        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("hello world"), 0, false);
+        Http2DataFrame dataFrame0 = inboundHandler.readInbound();
+        assertNotNull(dataFrame0);
+        release(dataFrame0);
+
+        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("foo"), 0, false);
+        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("bar"), 0, false);
+
+        assertNull(inboundHandler.readInbound());
+
+        childChannel.config().setAutoRead(true);
+        verifyFramesMultiplexedToCorrectChannel(childChannel, inboundHandler, 2);
+    }
+
+    @Test
+    public void channelReadShouldRespectAutoReadAndNotProduceNPE() throws Exception {
+        LastInboundHandler inboundHandler = new LastInboundHandler();
+        Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler);
+        assertTrue(childChannel.config().isAutoRead());
+        Http2HeadersFrame headersFrame = inboundHandler.readInbound();
+        assertNotNull(headersFrame);
+
+        childChannel.config().setAutoRead(false);
+        childChannel.pipeline().addFirst(new ChannelInboundHandlerAdapter() {
+            private int count;
+            @Override
+            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+                ctx.fireChannelRead(msg);
+                // Close channel after 2 reads so there is still something in the inboundBuffer when the close happens.
+                if (++count == 2) {
+                    ctx.close();
+                }
+            }
+        });
+        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("hello world"), 0, false);
+        Http2DataFrame dataFrame0 = inboundHandler.readInbound();
+        assertNotNull(dataFrame0);
+        release(dataFrame0);
+
+        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("foo"), 0, false);
+        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("bar"), 0, false);
+        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("bar"), 0, false);
+
+        assertNull(inboundHandler.readInbound());
+
+        childChannel.config().setAutoRead(true);
+        verifyFramesMultiplexedToCorrectChannel(childChannel, inboundHandler, 3);
+        inboundHandler.checkException();
+    }
+
+    @Test
+    public void readInChannelReadWithoutAutoRead() {
+        useReadWithoutAutoRead(false);
+    }
+
+    @Test
+    public void readInChannelReadCompleteWithoutAutoRead() {
+        useReadWithoutAutoRead(true);
+    }
+
+    private void useReadWithoutAutoRead(final boolean readComplete) {
+        LastInboundHandler inboundHandler = new LastInboundHandler();
+        Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler);
+        assertTrue(childChannel.config().isAutoRead());
+        childChannel.config().setAutoRead(false);
+        assertFalse(childChannel.config().isAutoRead());
+
+        Http2HeadersFrame headersFrame = inboundHandler.readInbound();
+        assertNotNull(headersFrame);
+
+        // Add a handler which will request reads.
+        childChannel.pipeline().addFirst(new ChannelInboundHandlerAdapter() {
+            @Override
+            public void channelRead(ChannelHandlerContext ctx, Object msg) {
+                ctx.fireChannelRead(msg);
+                if (!readComplete) {
+                    ctx.read();
+                }
+            }
+
+            @Override
+            public void channelReadComplete(ChannelHandlerContext ctx) {
+                ctx.fireChannelReadComplete();
+                if (readComplete) {
+                    ctx.read();
+                }
+            }
+        });
+
+        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("hello world"), 0, false);
+        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("foo"), 0, false);
+        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("bar"), 0, false);
+        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("hello world"), 0, false);
+        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("foo"), 0, false);
+        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("bar"), 0, true);
+
+        verifyFramesMultiplexedToCorrectChannel(childChannel, inboundHandler, 6);
+    }
+
+    private Http2StreamChannel newOutboundStream(ChannelHandler handler) {
+        return new Http2StreamChannelBootstrap(parentChannel).handler(handler)
+                .open().syncUninterruptibly().getNow();
+    }
+
+    /**
+     * A child channel for an HTTP/2 stream in IDLE state (that is no headers sent or received),
+     * should not emit a RST_STREAM frame on close, as this is a connection error of type protocol error.
+     */
+    @Test
+    public void idleOutboundStreamShouldNotWriteResetFrameOnClose() {
+        LastInboundHandler handler = new LastInboundHandler();
+
+        Channel childChannel = newOutboundStream(handler);
+        assertTrue(childChannel.isActive());
+
+        childChannel.close();
+        parentChannel.runPendingTasks();
+
+        assertFalse(childChannel.isOpen());
+        assertFalse(childChannel.isActive());
+        assertNull(parentChannel.readOutbound());
+    }
+
+    @Test
+    public void outboundStreamShouldWriteResetFrameOnClose_headersSent() {
+        ChannelHandler handler = new ChannelInboundHandlerAdapter() {
+            @Override
+            public void channelActive(ChannelHandlerContext ctx) {
+                ctx.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers()));
+                ctx.fireChannelActive();
+            }
+        };
+
+        Http2StreamChannel childChannel = newOutboundStream(handler);
+        assertTrue(childChannel.isActive());
+
+        childChannel.close();
+        verify(frameWriter).writeRstStream(eqCodecCtx(),
+                eqStreamId(childChannel), eq(Http2Error.CANCEL.code()), anyChannelPromise());
+    }
+
+    @Test
+    public void outboundStreamShouldNotWriteResetFrameOnClose_IfStreamDidntExist() {
+        when(frameWriter.writeHeaders(eqCodecCtx(), anyInt(),
+                any(Http2Headers.class), anyInt(), anyBoolean(),
+                any(ChannelPromise.class))).thenAnswer(new Answer<ChannelFuture>() {
+
+            private boolean headersWritten;
+            @Override
+            public ChannelFuture answer(InvocationOnMock invocationOnMock) {
+                // We want to fail to write the first headers frame. This is what happens if the connection
+                // refuses to allocate a new stream due to having received a GOAWAY.
+                if (!headersWritten) {
+                    headersWritten = true;
+                    return ((ChannelPromise) invocationOnMock.getArgument(5)).setFailure(new Exception("boom"));
+                }
+                return ((ChannelPromise) invocationOnMock.getArgument(5)).setSuccess();
+            }
+        });
+
+        Http2StreamChannel childChannel = newOutboundStream(new ChannelInboundHandlerAdapter() {
+            @Override
+            public void channelActive(ChannelHandlerContext ctx) {
+                ctx.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers()));
+                ctx.fireChannelActive();
+            }
+        });
+
+        assertFalse(childChannel.isActive());
+
+        childChannel.close();
+        parentChannel.runPendingTasks();
+        // The channel was never active so we should not generate a RST frame.
+        verify(frameWriter, never()).writeRstStream(eqCodecCtx(), eqStreamId(childChannel), anyLong(),
+                anyChannelPromise());
+
+        assertTrue(parentChannel.outboundMessages().isEmpty());
+    }
+
+    @Test
+    public void inboundRstStreamFireChannelInactive() {
+        LastInboundHandler inboundHandler = new LastInboundHandler();
+        Http2StreamChannel channel = newInboundStream(3, false, inboundHandler);
+        assertTrue(inboundHandler.isChannelActive());
+        frameInboundWriter.writeInboundRstStream(channel.stream().id(), Http2Error.INTERNAL_ERROR.code());
+
+        assertFalse(inboundHandler.isChannelActive());
+
+        // A RST_STREAM frame should NOT be emitted, as we received a RST_STREAM.
+        verify(frameWriter, Mockito.never()).writeRstStream(eqCodecCtx(), eqStreamId(channel),
+                anyLong(), anyChannelPromise());
+    }
+
+    @Test(expected = StreamException.class)
+    public void streamExceptionTriggersChildChannelExceptionAndClose() throws Exception {
+        LastInboundHandler inboundHandler = new LastInboundHandler();
+        Http2StreamChannel channel = newInboundStream(3, false, inboundHandler);
+        assertTrue(channel.isActive());
+        StreamException cause = new StreamException(channel.stream().id(), Http2Error.PROTOCOL_ERROR, "baaam!");
+        parentChannel.pipeline().fireExceptionCaught(cause);
+
+        assertFalse(channel.isActive());
+        inboundHandler.checkException();
+    }
+
+    @Test(expected = ClosedChannelException.class)
+    public void streamClosedErrorTranslatedToClosedChannelExceptionOnWrites() throws Exception {
+        LastInboundHandler inboundHandler = new LastInboundHandler();
+
+        final Http2StreamChannel childChannel = newOutboundStream(inboundHandler);
+        assertTrue(childChannel.isActive());
+
+        Http2Headers headers = new DefaultHttp2Headers();
+        when(frameWriter.writeHeaders(eqCodecCtx(), anyInt(),
+                eq(headers), anyInt(), anyBoolean(),
+                any(ChannelPromise.class))).thenAnswer(new Answer<ChannelFuture>() {
+            @Override
+            public ChannelFuture answer(InvocationOnMock invocationOnMock) {
+                return ((ChannelPromise) invocationOnMock.getArgument(5)).setFailure(
+                        new StreamException(childChannel.stream().id(), Http2Error.STREAM_CLOSED, "Stream Closed"));
+            }
+        });
+        ChannelFuture future = childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers()));
+
+        parentChannel.flush();
+
+        assertFalse(childChannel.isActive());
+        assertFalse(childChannel.isOpen());
+
+        inboundHandler.checkException();
+
+        future.syncUninterruptibly();
+    }
+
+    @Test
+    public void creatingWritingReadingAndClosingOutboundStreamShouldWork() {
+        LastInboundHandler inboundHandler = new LastInboundHandler();
+        Http2StreamChannel childChannel = newOutboundStream(inboundHandler);
+        assertTrue(childChannel.isActive());
+        assertTrue(inboundHandler.isChannelActive());
+
+        // Write to the child channel
+        Http2Headers headers = new DefaultHttp2Headers().scheme("https").method("GET").path("/foo.txt");
+        childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(headers));
+
+        // Read from the child channel
+        frameInboundWriter.writeInboundHeaders(childChannel.stream().id(), headers, 0, false);
+
+        Http2HeadersFrame headersFrame = inboundHandler.readInbound();
+        assertNotNull(headersFrame);
+        assertEquals(headers, headersFrame.headers());
+
+        // Close the child channel.
+        childChannel.close();
+
+        parentChannel.runPendingTasks();
+        // An active outbound stream should emit a RST_STREAM frame.
+        verify(frameWriter).writeRstStream(eqCodecCtx(), eqStreamId(childChannel),
+                anyLong(), anyChannelPromise());
+
+        assertFalse(childChannel.isOpen());
+        assertFalse(childChannel.isActive());
+        assertFalse(inboundHandler.isChannelActive());
+    }
+
+    // Test failing the promise of the first headers frame of an outbound stream. In practice this error case would most
+    // likely happen due to the max concurrent streams limit being hit or the channel running out of stream identifiers.
+    //
+    @Test(expected = Http2NoMoreStreamIdsException.class)
+    public void failedOutboundStreamCreationThrowsAndClosesChannel() throws Exception {
+        LastInboundHandler handler = new LastInboundHandler();
+        Http2StreamChannel childChannel = newOutboundStream(handler);
+        assertTrue(childChannel.isActive());
+
+        Http2Headers headers = new DefaultHttp2Headers();
+        when(frameWriter.writeHeaders(eqCodecCtx(), anyInt(),
+               eq(headers), anyInt(), anyBoolean(),
+               any(ChannelPromise.class))).thenAnswer(new Answer<ChannelFuture>() {
+           @Override
+           public ChannelFuture answer(InvocationOnMock invocationOnMock) {
+               return ((ChannelPromise) invocationOnMock.getArgument(5)).setFailure(
+                       new Http2NoMoreStreamIdsException());
+            }
+        });
+
+        ChannelFuture future = childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(headers));
+        parentChannel.flush();
+
+        assertFalse(childChannel.isActive());
+        assertFalse(childChannel.isOpen());
+
+        handler.checkException();
+
+        future.syncUninterruptibly();
+    }
+
+    @Test
+    public void channelClosedWhenCloseListenerCompletes() {
+        LastInboundHandler inboundHandler = new LastInboundHandler();
+        Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler);
+
+        assertTrue(childChannel.isOpen());
+        assertTrue(childChannel.isActive());
+
+        final AtomicBoolean channelOpen = new AtomicBoolean(true);
+        final AtomicBoolean channelActive = new AtomicBoolean(true);
+
+        // Create a promise before actually doing the close, because otherwise we would be adding a listener to a future
+        // that is already completed because we are using EmbeddedChannel which executes code in the JUnit thread.
+        ChannelPromise p = childChannel.newPromise();
+        p.addListener(new ChannelFutureListener() {
+            @Override
+            public void operationComplete(ChannelFuture future) {
+                channelOpen.set(future.channel().isOpen());
+                channelActive.set(future.channel().isActive());
+            }
+        });
+        childChannel.close(p).syncUninterruptibly();
+
+        assertFalse(channelOpen.get());
+        assertFalse(channelActive.get());
+        assertFalse(childChannel.isActive());
+    }
+
+    @Test
+    public void channelClosedWhenChannelClosePromiseCompletes() {
+         LastInboundHandler inboundHandler = new LastInboundHandler();
+         Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler);
+
+         assertTrue(childChannel.isOpen());
+         assertTrue(childChannel.isActive());
+
+         final AtomicBoolean channelOpen = new AtomicBoolean(true);
+         final AtomicBoolean channelActive = new AtomicBoolean(true);
+
+         childChannel.closeFuture().addListener(new ChannelFutureListener() {
+             @Override
+             public void operationComplete(ChannelFuture future) {
+                 channelOpen.set(future.channel().isOpen());
+                 channelActive.set(future.channel().isActive());
+             }
+         });
+         childChannel.close().syncUninterruptibly();
+
+         assertFalse(channelOpen.get());
+         assertFalse(channelActive.get());
+         assertFalse(childChannel.isActive());
+    }
+
+    @Test
+    public void channelClosedWhenWriteFutureFails() {
+        final Queue<ChannelPromise> writePromises = new ArrayDeque<ChannelPromise>();
+
+        LastInboundHandler inboundHandler = new LastInboundHandler();
+        Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler);
+
+        assertTrue(childChannel.isOpen());
+        assertTrue(childChannel.isActive());
+
+        final AtomicBoolean channelOpen = new AtomicBoolean(true);
+        final AtomicBoolean channelActive = new AtomicBoolean(true);
+
+        Http2Headers headers = new DefaultHttp2Headers();
+        when(frameWriter.writeHeaders(eqCodecCtx(), anyInt(),
+                eq(headers), anyInt(), anyBoolean(),
+                any(ChannelPromise.class))).thenAnswer(new Answer<ChannelFuture>() {
+            @Override
+            public ChannelFuture answer(InvocationOnMock invocationOnMock) {
+                ChannelPromise promise = invocationOnMock.getArgument(5);
+                writePromises.offer(promise);
+                return promise;
+            }
+        });
+
+        ChannelFuture f = childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(headers));
+        assertFalse(f.isDone());
+        f.addListener(new ChannelFutureListener() {
+            @Override
+            public void operationComplete(ChannelFuture future) throws Exception {
+                channelOpen.set(future.channel().isOpen());
+                channelActive.set(future.channel().isActive());
+            }
+        });
+
+        ChannelPromise first = writePromises.poll();
+        first.setFailure(new ClosedChannelException());
+        f.awaitUninterruptibly();
+
+        assertFalse(channelOpen.get());
+        assertFalse(channelActive.get());
+        assertFalse(childChannel.isActive());
+    }
+
+    @Test
+    public void channelClosedTwiceMarksPromiseAsSuccessful() {
+        LastInboundHandler inboundHandler = new LastInboundHandler();
+        Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler);
+
+        assertTrue(childChannel.isOpen());
+        assertTrue(childChannel.isActive());
+        childChannel.close().syncUninterruptibly();
+        childChannel.close().syncUninterruptibly();
+
+        assertFalse(childChannel.isOpen());
+        assertFalse(childChannel.isActive());
+    }
+
+    @Test
+    public void settingChannelOptsAndAttrs() {
+        AttributeKey<String> key = AttributeKey.newInstance(UUID.randomUUID().toString());
+
+        Channel childChannel = newOutboundStream(new ChannelInboundHandlerAdapter());
+        childChannel.config().setAutoRead(false).setWriteSpinCount(1000);
+        childChannel.attr(key).set("bar");
+        assertFalse(childChannel.config().isAutoRead());
+        assertEquals(1000, childChannel.config().getWriteSpinCount());
+        assertEquals("bar", childChannel.attr(key).get());
+    }
+
+    @Test
+    public void outboundFlowControlWritability() {
+        Http2StreamChannel childChannel = newOutboundStream(new ChannelInboundHandlerAdapter());
+        assertTrue(childChannel.isActive());
+
+        assertTrue(childChannel.isWritable());
+        childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers()));
+        parentChannel.flush();
+
+        // Test for initial window size
+        assertTrue(initialRemoteStreamWindow < childChannel.config().getWriteBufferHighWaterMark());
+
+        assertTrue(childChannel.isWritable());
+        childChannel.write(new DefaultHttp2DataFrame(Unpooled.buffer().writeZero(16 * 1024 * 1024)));
+        assertEquals(0, childChannel.bytesBeforeUnwritable());
+        assertFalse(childChannel.isWritable());
+    }
+
+    @Test
+    public void writabilityOfParentIsRespected() {
+        Http2StreamChannel childChannel = newOutboundStream(new ChannelInboundHandlerAdapter());
+        childChannel.config().setWriteBufferWaterMark(new WriteBufferWaterMark(2048, 4096));
+        parentChannel.config().setWriteBufferWaterMark(new WriteBufferWaterMark(256, 512));
+        assertTrue(childChannel.isWritable());
+        assertTrue(parentChannel.isActive());
+
+        childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers()));
+        parentChannel.flush();
+
+        assertTrue(childChannel.isWritable());
+        childChannel.write(new DefaultHttp2DataFrame(Unpooled.buffer().writeZero(256)));
+        assertTrue(childChannel.isWritable());
+        childChannel.writeAndFlush(new DefaultHttp2DataFrame(Unpooled.buffer().writeZero(512)));
+
+        long bytesBeforeUnwritable = childChannel.bytesBeforeUnwritable();
+        assertNotEquals(0, bytesBeforeUnwritable);
+        // Add something to the ChannelOutboundBuffer of the parent to simulate queuing in the parents channel buffer
+        // and verify that this only affect the writability of the parent channel while the child stays writable
+        // until it used all of its credits.
+        parentChannel.unsafe().outboundBuffer().addMessage(
+                Unpooled.buffer().writeZero(800), 800, parentChannel.voidPromise());
+        assertFalse(parentChannel.isWritable());
+
+        assertTrue(childChannel.isWritable());
+        assertEquals(4096, childChannel.bytesBeforeUnwritable());
+
+        // Flush everything which simulate writing everything to the socket.
+        parentChannel.flush();
+        assertTrue(parentChannel.isWritable());
+        assertTrue(childChannel.isWritable());
+        assertEquals(bytesBeforeUnwritable, childChannel.bytesBeforeUnwritable());
+
+        ChannelFuture future = childChannel.writeAndFlush(new DefaultHttp2DataFrame(
+                Unpooled.buffer().writeZero((int) bytesBeforeUnwritable)));
+        assertFalse(childChannel.isWritable());
+        assertTrue(parentChannel.isWritable());
+
+        parentChannel.flush();
+        assertFalse(future.isDone());
+        assertTrue(parentChannel.isWritable());
+        assertFalse(childChannel.isWritable());
+
+        // Now write an window update frame for the stream which then should ensure we will flush the bytes that were
+        // queued in the RemoteFlowController before for the stream.
+        frameInboundWriter.writeInboundWindowUpdate(childChannel.stream().id(), (int) bytesBeforeUnwritable);
+        assertTrue(childChannel.isWritable());
+        assertTrue(future.isDone());
+    }
+
+    @Test
+    public void channelClosedWhenInactiveFired() {
+        LastInboundHandler inboundHandler = new LastInboundHandler();
+        Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler);
+
+        final AtomicBoolean channelOpen = new AtomicBoolean(false);
+        final AtomicBoolean channelActive = new AtomicBoolean(false);
+        assertTrue(childChannel.isOpen());
+        assertTrue(childChannel.isActive());
+
+        childChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
+            @Override
+            public void channelInactive(ChannelHandlerContext ctx) throws Exception {
+                channelOpen.set(ctx.channel().isOpen());
+                channelActive.set(ctx.channel().isActive());
+
+                super.channelInactive(ctx);
+            }
+        });
+
+        childChannel.close().syncUninterruptibly();
+        assertFalse(channelOpen.get());
+        assertFalse(channelActive.get());
+    }
+
+    @Test
+    public void channelInactiveHappensAfterExceptionCaughtEvents() throws Exception {
+        final AtomicInteger count = new AtomicInteger(0);
+        final AtomicInteger exceptionCaught = new AtomicInteger(-1);
+        final AtomicInteger channelInactive = new AtomicInteger(-1);
+        final AtomicInteger channelUnregistered = new AtomicInteger(-1);
+        Http2StreamChannel childChannel = newOutboundStream(new ChannelInboundHandlerAdapter() {
+
+            @Override
+            public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
+                ctx.close();
+                throw new Exception("exception");
+            }
+        });
+
+        childChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
+
+            @Override
+            public void channelInactive(ChannelHandlerContext ctx) throws Exception {
+                channelInactive.set(count.getAndIncrement());
+                super.channelInactive(ctx);
+            }
+
+            @Override
+            public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+                exceptionCaught.set(count.getAndIncrement());
+                super.exceptionCaught(ctx, cause);
+            }
+
+            @Override
+            public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
+                channelUnregistered.set(count.getAndIncrement());
+                super.channelUnregistered(ctx);
+            }
+        });
+
+        childChannel.pipeline().fireUserEventTriggered(new Object());
+        parentChannel.runPendingTasks();
+
+        // The events should have happened in this order because the inactive and deregistration events
+        // get deferred as they do in the AbstractChannel.
+        assertEquals(0, exceptionCaught.get());
+        assertEquals(1, channelInactive.get());
+        assertEquals(2, channelUnregistered.get());
+    }
+
+    @Test
+    public void callUnsafeCloseMultipleTimes() {
+        LastInboundHandler inboundHandler = new LastInboundHandler();
+        Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler);
+        childChannel.unsafe().close(childChannel.voidPromise());
+
+        ChannelPromise promise = childChannel.newPromise();
+        childChannel.unsafe().close(promise);
+        promise.syncUninterruptibly();
+        childChannel.closeFuture().syncUninterruptibly();
+    }
+
+    @Test
+    public void endOfStreamDoesNotDiscardData() {
+        AtomicInteger numReads = new AtomicInteger(1);
+        final AtomicBoolean shouldDisableAutoRead = new AtomicBoolean();
+        Consumer<ChannelHandlerContext> ctxConsumer = new Consumer<ChannelHandlerContext>() {
+            @Override
+            public void accept(ChannelHandlerContext obj) {
+                if (shouldDisableAutoRead.get()) {
+                    obj.channel().config().setAutoRead(false);
+                }
+            }
+        };
+        LastInboundHandler inboundHandler = new LastInboundHandler(ctxConsumer);
+        Http2StreamChannel childChannel = newInboundStream(3, false, numReads, inboundHandler);
+        childChannel.config().setAutoRead(false);
+
+        Http2DataFrame dataFrame1 = new DefaultHttp2DataFrame(bb("1")).stream(childChannel.stream());
+        Http2DataFrame dataFrame2 = new DefaultHttp2DataFrame(bb("2")).stream(childChannel.stream());
+        Http2DataFrame dataFrame3 = new DefaultHttp2DataFrame(bb("3")).stream(childChannel.stream());
+        Http2DataFrame dataFrame4 = new DefaultHttp2DataFrame(bb("4")).stream(childChannel.stream());
+
+        assertEquals(new DefaultHttp2HeadersFrame(request).stream(childChannel.stream()), inboundHandler.readInbound());
+
+        ChannelHandler readCompleteSupressHandler = new ChannelInboundHandlerAdapter() {
+            @Override
+            public void channelReadComplete(ChannelHandlerContext ctx) {
+                // We want to simulate the parent channel calling channelRead and delay calling channelReadComplete.
+            }
+        };
+
+        parentChannel.pipeline().addFirst(readCompleteSupressHandler);
+        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("1"), 0, false);
+
+        assertEqualsAndRelease(dataFrame1, inboundHandler.<Http2DataFrame>readInbound());
+
+        // Deliver frames, and then a stream closed while read is inactive.
+        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("2"), 0, false);
+        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("3"), 0, false);
+        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("4"), 0, false);
+
+        shouldDisableAutoRead.set(true);
+        childChannel.config().setAutoRead(true);
+        numReads.set(1);
+
+        frameInboundWriter.writeInboundRstStream(childChannel.stream().id(), Http2Error.NO_ERROR.code());
+
+        // Detecting EOS should flush all pending data regardless of read calls.
+        assertEqualsAndRelease(dataFrame2, inboundHandler.<Http2DataFrame>readInbound());
+        assertNull(inboundHandler.readInbound());
+
+        // As we limited the number to 1 we also need to call read() again.
+        childChannel.read();
+
+        assertEqualsAndRelease(dataFrame3, inboundHandler.<Http2DataFrame>readInbound());
+        assertEqualsAndRelease(dataFrame4, inboundHandler.<Http2DataFrame>readInbound());
+
+        Http2ResetFrame resetFrame = useUserEventForResetFrame() ? inboundHandler.<Http2ResetFrame>readUserEvent() :
+                inboundHandler.<Http2ResetFrame>readInbound();
+
+        assertEquals(childChannel.stream(), resetFrame.stream());
+        assertEquals(Http2Error.NO_ERROR.code(), resetFrame.errorCode());
+
+        assertNull(inboundHandler.readInbound());
+
+        // Now we want to call channelReadComplete and simulate the end of the read loop.
+        parentChannel.pipeline().remove(readCompleteSupressHandler);
+        parentChannel.flushInbound();
+
+        childChannel.closeFuture().syncUninterruptibly();
+    }
+
+    protected abstract boolean useUserEventForResetFrame();
+
+    protected abstract boolean ignoreWindowUpdateFrames();
+
+    @Test
+    public void windowUpdateFrames() {
+        AtomicInteger numReads = new AtomicInteger(1);
+        LastInboundHandler inboundHandler = new LastInboundHandler();
+        Http2StreamChannel childChannel = newInboundStream(3, false, numReads, inboundHandler);
+
+        assertEquals(new DefaultHttp2HeadersFrame(request).stream(childChannel.stream()), inboundHandler.readInbound());
+
+        frameInboundWriter.writeInboundWindowUpdate(childChannel.stream().id(), 4);
+
+        Http2WindowUpdateFrame updateFrame = inboundHandler.readInbound();
+        if (ignoreWindowUpdateFrames()) {
+            assertNull(updateFrame);
+        } else {
+            assertEquals(new DefaultHttp2WindowUpdateFrame(4).stream(childChannel.stream()), updateFrame);
+        }
+
+        frameInboundWriter.writeInboundWindowUpdate(Http2CodecUtil.CONNECTION_STREAM_ID, 6);
+
+        assertNull(parentChannel.readInbound());
+        childChannel.close().syncUninterruptibly();
+    }
+
+    @Test
+    public void childQueueIsDrainedAndNewDataIsDispatchedInParentReadLoopAutoRead() {
+        AtomicInteger numReads = new AtomicInteger(1);
+        final AtomicInteger channelReadCompleteCount = new AtomicInteger(0);
+        final AtomicBoolean shouldDisableAutoRead = new AtomicBoolean();
+        Consumer<ChannelHandlerContext> ctxConsumer = new Consumer<ChannelHandlerContext>() {
+            @Override
+            public void accept(ChannelHandlerContext obj) {
+                channelReadCompleteCount.incrementAndGet();
+                if (shouldDisableAutoRead.get()) {
+                    obj.channel().config().setAutoRead(false);
+                }
+            }
+        };
+        LastInboundHandler inboundHandler = new LastInboundHandler(ctxConsumer);
+        Http2StreamChannel childChannel = newInboundStream(3, false, numReads, inboundHandler);
+        childChannel.config().setAutoRead(false);
+
+        Http2DataFrame dataFrame1 = new DefaultHttp2DataFrame(bb("1")).stream(childChannel.stream());
+        Http2DataFrame dataFrame2 = new DefaultHttp2DataFrame(bb("2")).stream(childChannel.stream());
+        Http2DataFrame dataFrame3 = new DefaultHttp2DataFrame(bb("3")).stream(childChannel.stream());
+        Http2DataFrame dataFrame4 = new DefaultHttp2DataFrame(bb("4")).stream(childChannel.stream());
+
+        assertEquals(new DefaultHttp2HeadersFrame(request).stream(childChannel.stream()), inboundHandler.readInbound());
+
+        ChannelHandler readCompleteSupressHandler = new ChannelInboundHandlerAdapter() {
+            @Override
+            public void channelReadComplete(ChannelHandlerContext ctx) {
+                // We want to simulate the parent channel calling channelRead and delay calling channelReadComplete.
+            }
+        };
+        parentChannel.pipeline().addFirst(readCompleteSupressHandler);
+
+        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("1"), 0, false);
+
+        assertEqualsAndRelease(dataFrame1, inboundHandler.<Http2DataFrame>readInbound());
+
+        // We want one item to be in the queue, and allow the numReads to be larger than 1. This will ensure that
+        // when beginRead() is called the child channel is added to the readPending queue of the parent channel.
+        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("2"), 0, false);
+
+        numReads.set(10);
+        shouldDisableAutoRead.set(true);
+        childChannel.config().setAutoRead(true);
+
+        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("3"), 0, false);
+        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("4"), 0, false);
+
+        // Detecting EOS should flush all pending data regardless of read calls.
+        assertEqualsAndRelease(dataFrame2, inboundHandler.<Http2DataFrame>readInbound());
+        assertEqualsAndRelease(dataFrame3, inboundHandler.<Http2DataFrame>readInbound());
+        assertEqualsAndRelease(dataFrame4, inboundHandler.<Http2DataFrame>readInbound());
+
+        assertNull(inboundHandler.readInbound());
+
+        // Now we want to call channelReadComplete and simulate the end of the read loop.
+        parentChannel.pipeline().remove(readCompleteSupressHandler);
+        parentChannel.flushInbound();
+
+        // 3 = 1 for initialization + 1 for read when auto read was off + 1 for when auto read was back on
+        assertEquals(3, channelReadCompleteCount.get());
+    }
+
+    @Test
+    public void childQueueIsDrainedAndNewDataIsDispatchedInParentReadLoopNoAutoRead() {
+        final AtomicInteger numReads = new AtomicInteger(1);
+        final AtomicInteger channelReadCompleteCount = new AtomicInteger(0);
+        final AtomicBoolean shouldDisableAutoRead = new AtomicBoolean();
+        Consumer<ChannelHandlerContext> ctxConsumer = new Consumer<ChannelHandlerContext>() {
+            @Override
+            public void accept(ChannelHandlerContext obj) {
+                channelReadCompleteCount.incrementAndGet();
+                if (shouldDisableAutoRead.get()) {
+                    obj.channel().config().setAutoRead(false);
+                }
+            }
+        };
+        final LastInboundHandler inboundHandler = new LastInboundHandler(ctxConsumer);
+        Http2StreamChannel childChannel = newInboundStream(3, false, numReads, inboundHandler);
+        childChannel.config().setAutoRead(false);
+
+        Http2DataFrame dataFrame1 = new DefaultHttp2DataFrame(bb("1")).stream(childChannel.stream());
+        Http2DataFrame dataFrame2 = new DefaultHttp2DataFrame(bb("2")).stream(childChannel.stream());
+        Http2DataFrame dataFrame3 = new DefaultHttp2DataFrame(bb("3")).stream(childChannel.stream());
+        Http2DataFrame dataFrame4 = new DefaultHttp2DataFrame(bb("4")).stream(childChannel.stream());
+
+        assertEquals(new DefaultHttp2HeadersFrame(request).stream(childChannel.stream()), inboundHandler.readInbound());
+
+        ChannelHandler readCompleteSupressHandler = new ChannelInboundHandlerAdapter() {
+            @Override
+            public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
+                // We want to simulate the parent channel calling channelRead and delay calling channelReadComplete.
+            }
+        };
+        parentChannel.pipeline().addFirst(readCompleteSupressHandler);
+
+        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("1"), 0, false);
+
+        assertEqualsAndRelease(dataFrame1, inboundHandler.<Http2Frame>readInbound());
+
+        // We want one item to be in the queue, and allow the numReads to be larger than 1. This will ensure that
+        // when beginRead() is called the child channel is added to the readPending queue of the parent channel.
+        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("2"), 0, false);
+
+        numReads.set(2);
+        childChannel.read();
+
+        assertEqualsAndRelease(dataFrame2, inboundHandler.<Http2Frame>readInbound());
+
+        assertNull(inboundHandler.readInbound());
+
+        // This is the second item that was read, this should be the last until we call read() again. This should also
+        // notify of readComplete().
+        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("3"), 0, false);
+
+        assertEqualsAndRelease(dataFrame3, inboundHandler.<Http2Frame>readInbound());
+
+        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("4"), 0, false);
+        assertNull(inboundHandler.readInbound());
+
+        childChannel.read();
+
+        assertEqualsAndRelease(dataFrame4, inboundHandler.<Http2Frame>readInbound());
+
+        assertNull(inboundHandler.readInbound());
+
+        // Now we want to call channelReadComplete and simulate the end of the read loop.
+        parentChannel.pipeline().remove(readCompleteSupressHandler);
+        parentChannel.flushInbound();
+
+        // 3 = 1 for initialization + 1 for first read of 2 items + 1 for second read of 2 items +
+        // 1 for parent channel readComplete
+        assertEquals(4, channelReadCompleteCount.get());
+    }
+
+    @Test
+    public void useReadWithoutAutoReadInRead() {
+        useReadWithoutAutoReadBuffered(false);
+    }
+
+    @Test
+    public void useReadWithoutAutoReadInReadComplete() {
+        useReadWithoutAutoReadBuffered(true);
+    }
+
+    private void useReadWithoutAutoReadBuffered(final boolean triggerOnReadComplete) {
+        LastInboundHandler inboundHandler = new LastInboundHandler();
+        Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler);
+        assertTrue(childChannel.config().isAutoRead());
+        childChannel.config().setAutoRead(false);
+        assertFalse(childChannel.config().isAutoRead());
+
+        Http2HeadersFrame headersFrame = inboundHandler.readInbound();
+        assertNotNull(headersFrame);
+
+        // Write some bytes to get the channel into the idle state with buffered data and also verify we
+        // do not dispatch it until we receive a read() call.
+        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("hello world"), 0, false);
+        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("foo"), 0, false);
+        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("bar"), 0, false);
+
+        // Add a handler which will request reads.
+        childChannel.pipeline().addFirst(new ChannelInboundHandlerAdapter() {
+
+            @Override
+            public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
+                super.channelReadComplete(ctx);
+                if (triggerOnReadComplete) {
+                    ctx.read();
+                    ctx.read();
+                }
+            }
+
+            @Override
+            public void channelRead(ChannelHandlerContext ctx, Object msg) {
+                ctx.fireChannelRead(msg);
+                if (!triggerOnReadComplete) {
+                    ctx.read();
+                    ctx.read();
+                }
+            }
+        });
+
+        inboundHandler.channel().read();
+
+        verifyFramesMultiplexedToCorrectChannel(childChannel, inboundHandler, 3);
+
+        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("hello world2"), 0, false);
+        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("foo2"), 0, false);
+        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb("bar2"), 0, true);
+
+        verifyFramesMultiplexedToCorrectChannel(childChannel, inboundHandler, 3);
+    }
+
+    private static final class FlushSniffer extends ChannelOutboundHandlerAdapter {
+
+        private boolean didFlush;
+
+        public boolean checkFlush() {
+            boolean r = didFlush;
+            didFlush = false;
+            return r;
+        }
+
+        @Override
+        public void flush(ChannelHandlerContext ctx) throws Exception {
+            didFlush = true;
+            super.flush(ctx);
+        }
+    }
+
+    @Test
+    public void windowUpdatesAreFlushed() {
+        LastInboundHandler inboundHandler = new LastInboundHandler();
+        FlushSniffer flushSniffer = new FlushSniffer();
+        parentChannel.pipeline().addFirst(flushSniffer);
+
+        Http2StreamChannel childChannel = newInboundStream(3, false, inboundHandler);
+        assertTrue(childChannel.config().isAutoRead());
+        childChannel.config().setAutoRead(false);
+        assertFalse(childChannel.config().isAutoRead());
+
+        Http2HeadersFrame headersFrame = inboundHandler.readInbound();
+        assertNotNull(headersFrame);
+
+        assertTrue(flushSniffer.checkFlush());
+
+        // Write some bytes to get the channel into the idle state with buffered data and also verify we
+        // do not dispatch it until we receive a read() call.
+        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb(16 * 1024), 0, false);
+        frameInboundWriter.writeInboundData(childChannel.stream().id(), bb(16 * 1024), 0, false);
+        assertTrue(flushSniffer.checkFlush());
+
+        verify(frameWriter, never()).writeWindowUpdate(eqCodecCtx(), anyInt(), anyInt(), anyChannelPromise());
+        // only the first one was read because it was legacy auto-read behavior.
+        verifyFramesMultiplexedToCorrectChannel(childChannel, inboundHandler, 1);
+        assertFalse(flushSniffer.checkFlush());
+
+        // Trigger a read of the second frame.
+        childChannel.read();
+        verifyFramesMultiplexedToCorrectChannel(childChannel, inboundHandler, 1);
+        // We expect a flush here because the StreamChannel will flush the smaller increment but the
+        // connection will collect the bytes and decide not to send a wire level frame until more are consumed.
+        assertTrue(flushSniffer.checkFlush());
+        verify(frameWriter, never()).writeWindowUpdate(eqCodecCtx(), anyInt(), anyInt(), anyChannelPromise());
+
+        // Call read one more time which should trigger the writing of the flow control update.
+        childChannel.read();
+        verify(frameWriter).writeWindowUpdate(eqCodecCtx(), eq(0), eq(32 * 1024), anyChannelPromise());
+        verify(frameWriter).writeWindowUpdate(
+            eqCodecCtx(), eq(childChannel.stream().id()), eq(32 * 1024), anyChannelPromise());
+        assertTrue(flushSniffer.checkFlush());
+    }
+
+    private static void verifyFramesMultiplexedToCorrectChannel(Http2StreamChannel streamChannel,
+                                                                LastInboundHandler inboundHandler,
+                                                                int numFrames) {
+        for (int i = 0; i < numFrames; i++) {
+            Http2StreamFrame frame = inboundHandler.readInbound();
+            assertNotNull(i + " out of " + numFrames + " received", frame);
+            assertEquals(streamChannel.stream(), frame.stream());
+            release(frame);
+        }
+        assertNull(inboundHandler.readInbound());
+    }
+
+    private static int eqStreamId(Http2StreamChannel channel) {
+        return eq(channel.stream().id());
+    }
+}
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexTransportTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexTransportTest.java
new file mode 100644
index 0000000..0484678
--- /dev/null
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MultiplexTransportTest.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright 2019 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.bootstrap.Bootstrap;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.channel.socket.nio.NioSocketChannel;
+import io.netty.util.CharsetUtil;
+import io.netty.util.NetUtil;
+import io.netty.util.ReferenceCountUtil;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.net.InetSocketAddress;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static org.junit.Assert.assertFalse;
+
+public class Http2MultiplexTransportTest {
+    private static final ChannelHandler DISCARD_HANDLER = new ChannelInboundHandlerAdapter() {
+
+        @Override
+        public boolean isSharable() {
+            return true;
+        }
+
+        @Override
+        public void channelRead(ChannelHandlerContext ctx, Object msg) {
+            ReferenceCountUtil.release(msg);
+        }
+
+        @Override
+        public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
+            ReferenceCountUtil.release(evt);
+        }
+    };
+
+    private EventLoopGroup eventLoopGroup;
+    private Channel clientChannel;
+    private Channel serverChannel;
+    private Channel serverConnectedChannel;
+
+    @Before
+    public void setup() {
+        eventLoopGroup = new NioEventLoopGroup();
+    }
+
+    @After
+    public void teardown() {
+        if (clientChannel != null) {
+            clientChannel.close();
+        }
+        if (serverChannel != null) {
+            serverChannel.close();
+        }
+        if (serverConnectedChannel != null) {
+            serverConnectedChannel.close();
+        }
+        eventLoopGroup.shutdownGracefully(0, 0, MILLISECONDS);
+    }
+
+    @Test(timeout = 10000)
+    public void asyncSettingsAckWithMultiplexCodec() throws InterruptedException {
+        asyncSettingsAck0(new Http2MultiplexCodecBuilder(true, DISCARD_HANDLER).build(), null);
+    }
+
+    @Test(timeout = 10000)
+    public void asyncSettingsAckWithMultiplexHandler() throws InterruptedException {
+        asyncSettingsAck0(new Http2FrameCodecBuilder(true).build(),
+                new Http2MultiplexHandler(DISCARD_HANDLER));
+    }
+
+    private void asyncSettingsAck0(final Http2FrameCodec codec, final ChannelHandler multiplexer)
+            throws InterruptedException {
+        // The client expects 2 settings frames. One from the connection setup and one from this test.
+        final CountDownLatch serverAckOneLatch = new CountDownLatch(1);
+        final CountDownLatch serverAckAllLatch = new CountDownLatch(2);
+        final CountDownLatch clientSettingsLatch = new CountDownLatch(2);
+        final CountDownLatch serverConnectedChannelLatch = new CountDownLatch(1);
+        final AtomicReference<Channel> serverConnectedChannelRef = new AtomicReference<Channel>();
+        ServerBootstrap sb = new ServerBootstrap();
+        sb.group(eventLoopGroup);
+        sb.channel(NioServerSocketChannel.class);
+        sb.childHandler(new ChannelInitializer<Channel>() {
+            @Override
+            protected void initChannel(Channel ch) {
+                ch.pipeline().addLast(codec);
+                if (multiplexer != null) {
+                    ch.pipeline().addLast(multiplexer);
+                }
+                ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
+                    @Override
+                    public void channelActive(ChannelHandlerContext ctx) {
+                        serverConnectedChannelRef.set(ctx.channel());
+                        serverConnectedChannelLatch.countDown();
+                    }
+
+                    @Override
+                    public void channelRead(ChannelHandlerContext ctx, Object msg) {
+                        if (msg instanceof Http2SettingsAckFrame) {
+                            serverAckOneLatch.countDown();
+                            serverAckAllLatch.countDown();
+                        }
+                        ReferenceCountUtil.release(msg);
+                    }
+                });
+            }
+        });
+        serverChannel = sb.bind(new InetSocketAddress(NetUtil.LOCALHOST, 0)).awaitUninterruptibly().channel();
+
+        Bootstrap bs = new Bootstrap();
+        bs.group(eventLoopGroup);
+        bs.channel(NioSocketChannel.class);
+        bs.handler(new ChannelInitializer<Channel>() {
+            @Override
+            protected void initChannel(Channel ch) {
+                ch.pipeline().addLast(Http2MultiplexCodecBuilder
+                        .forClient(DISCARD_HANDLER).autoAckSettingsFrame(false).build());
+                ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
+                    @Override
+                    public void channelRead(ChannelHandlerContext ctx, Object msg) {
+                        if (msg instanceof Http2SettingsFrame) {
+                            clientSettingsLatch.countDown();
+                        }
+                        ReferenceCountUtil.release(msg);
+                    }
+                });
+            }
+        });
+        clientChannel = bs.connect(serverChannel.localAddress()).awaitUninterruptibly().channel();
+        serverConnectedChannelLatch.await();
+        serverConnectedChannel = serverConnectedChannelRef.get();
+
+        serverConnectedChannel.writeAndFlush(new DefaultHttp2SettingsFrame(new Http2Settings()
+                .maxConcurrentStreams(10))).sync();
+
+        clientSettingsLatch.await();
+
+        // We expect a timeout here because we want to asynchronously generate the SETTINGS ACK below.
+        assertFalse(serverAckOneLatch.await(300, MILLISECONDS));
+
+        // We expect 2 settings frames, the initial settings frame during connection establishment and the setting frame
+        // written in this test. We should ack both of these settings frames.
+        clientChannel.writeAndFlush(Http2SettingsAckFrame.INSTANCE).sync();
+        clientChannel.writeAndFlush(Http2SettingsAckFrame.INSTANCE).sync();
+
+        serverAckAllLatch.await();
+    }
+
+    @Test(timeout = 5000L)
+    public void testFlushNotDiscarded()
+            throws InterruptedException {
+        final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
+
+        try {
+            ServerBootstrap sb = new ServerBootstrap();
+            sb.group(eventLoopGroup);
+            sb.channel(NioServerSocketChannel.class);
+            sb.childHandler(new ChannelInitializer<Channel>() {
+                @Override
+                protected void initChannel(Channel ch) {
+                    ch.pipeline().addLast(new Http2FrameCodecBuilder(true).build());
+                    ch.pipeline().addLast(new Http2MultiplexHandler(new ChannelInboundHandlerAdapter() {
+                        @Override
+                        public void channelRead(final ChannelHandlerContext ctx, Object msg) {
+                            if (msg instanceof Http2HeadersFrame && ((Http2HeadersFrame) msg).isEndStream()) {
+                                executorService.schedule(new Runnable() {
+                                    @Override
+                                    public void run() {
+                                        ctx.writeAndFlush(new DefaultHttp2HeadersFrame(
+                                                new DefaultHttp2Headers(), false)).addListener(
+                                                        new ChannelFutureListener() {
+                                            @Override
+                                            public void operationComplete(ChannelFuture future) {
+                                                ctx.write(new DefaultHttp2DataFrame(
+                                                        Unpooled.copiedBuffer("Hello World", CharsetUtil.US_ASCII),
+                                                        true));
+                                                ctx.channel().eventLoop().execute(new Runnable() {
+                                                    @Override
+                                                    public void run() {
+                                                        ctx.flush();
+                                                    }
+                                                });
+                                            }
+                                        });
+                                    }
+                                }, 500, TimeUnit.MILLISECONDS);
+                            }
+                            ReferenceCountUtil.release(msg);
+                        }
+                    }));
+                }
+            });
+            serverChannel = sb.bind(new InetSocketAddress(NetUtil.LOCALHOST, 0)).syncUninterruptibly().channel();
+
+            final CountDownLatch latch = new CountDownLatch(1);
+            Bootstrap bs = new Bootstrap();
+            bs.group(eventLoopGroup);
+            bs.channel(NioSocketChannel.class);
+            bs.handler(new ChannelInitializer<Channel>() {
+                @Override
+                protected void initChannel(Channel ch) {
+                    ch.pipeline().addLast(new Http2FrameCodecBuilder(false).build());
+                    ch.pipeline().addLast(new Http2MultiplexHandler(DISCARD_HANDLER));
+                }
+            });
+            clientChannel = bs.connect(serverChannel.localAddress()).syncUninterruptibly().channel();
+            Http2StreamChannelBootstrap h2Bootstrap = new Http2StreamChannelBootstrap(clientChannel);
+            h2Bootstrap.handler(new ChannelInboundHandlerAdapter() {
+                @Override
+                public void channelRead(ChannelHandlerContext ctx, Object msg) {
+                    if (msg instanceof Http2DataFrame && ((Http2DataFrame) msg).isEndStream()) {
+                        latch.countDown();
+                    }
+                    ReferenceCountUtil.release(msg);
+                }
+            });
+            Http2StreamChannel streamChannel = h2Bootstrap.open().syncUninterruptibly().getNow();
+            streamChannel.writeAndFlush(new DefaultHttp2HeadersFrame(new DefaultHttp2Headers(), true))
+                    .syncUninterruptibly();
+
+            latch.await();
+        } finally {
+            executorService.shutdown();
+        }
+    }
+}
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ServerUpgradeCodecTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ServerUpgradeCodecTest.java
index fe93b6b..3350754 100644
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ServerUpgradeCodecTest.java
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ServerUpgradeCodecTest.java
@@ -18,6 +18,8 @@ import io.netty.buffer.ByteBuf;
 import io.netty.channel.ChannelHandler;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.channel.DefaultChannelId;
+import io.netty.channel.ServerChannel;
 import io.netty.channel.embedded.EmbeddedChannel;
 import io.netty.handler.codec.http.DefaultFullHttpRequest;
 import io.netty.handler.codec.http.DefaultHttpHeaders;
@@ -32,40 +34,59 @@ import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 
 import org.junit.Test;
+import org.mockito.Mockito;
 
 public class Http2ServerUpgradeCodecTest {
 
     @Test
     public void testUpgradeToHttp2ConnectionHandler() {
-        testUpgrade(new Http2ConnectionHandlerBuilder().frameListener(new Http2FrameAdapter()).build());
+        testUpgrade(new Http2ConnectionHandlerBuilder().frameListener(new Http2FrameAdapter()).build(), null);
     }
 
     @Test
     public void testUpgradeToHttp2FrameCodec() {
-        testUpgrade(new Http2FrameCodecBuilder(true).build());
+        testUpgrade(new Http2FrameCodecBuilder(true).build(), null);
     }
 
     @Test
     public void testUpgradeToHttp2MultiplexCodec() {
-        testUpgrade(new Http2MultiplexCodecBuilder(true, new HttpInboundHandler()).build());
+        testUpgrade(new Http2MultiplexCodecBuilder(true, new HttpInboundHandler()).build(), null);
     }
 
-    private static void testUpgrade(Http2ConnectionHandler handler) {
+    @Test
+    public void testUpgradeToHttp2FrameCodecWithMultiplexer() {
+        testUpgrade(new Http2FrameCodecBuilder(true).build(),
+                new Http2MultiplexHandler(new HttpInboundHandler()));
+    }
+
+    private static void testUpgrade(Http2ConnectionHandler handler, ChannelHandler multiplexer) {
         FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.OPTIONS, "*");
         request.headers().set(HttpHeaderNames.HOST, "netty.io");
         request.headers().set(HttpHeaderNames.CONNECTION, "Upgrade, HTTP2-Settings");
         request.headers().set(HttpHeaderNames.UPGRADE, "h2c");
         request.headers().set("HTTP2-Settings", "AAMAAABkAAQAAP__");
 
-        EmbeddedChannel channel = new EmbeddedChannel(new ChannelInboundHandlerAdapter());
+        ServerChannel parent = Mockito.mock(ServerChannel.class);
+        EmbeddedChannel channel = new EmbeddedChannel(parent, DefaultChannelId.newInstance(), true, false,
+                new ChannelInboundHandlerAdapter());
         ChannelHandlerContext ctx = channel.pipeline().firstContext();
-        Http2ServerUpgradeCodec codec = new Http2ServerUpgradeCodec("connectionHandler", handler);
+        Http2ServerUpgradeCodec codec;
+        if (multiplexer == null) {
+            codec = new Http2ServerUpgradeCodec(handler);
+        } else {
+            codec = new Http2ServerUpgradeCodec((Http2FrameCodec) handler, multiplexer);
+        }
         assertTrue(codec.prepareUpgradeResponse(ctx, request, new DefaultHttpHeaders()));
         codec.upgradeTo(ctx, request);
         // Flush the channel to ensure we write out all buffered data
         channel.flush();
 
-        assertSame(handler, channel.pipeline().remove("connectionHandler"));
+        channel.writeInbound(Http2CodecUtil.connectionPrefaceBuf());
+        Http2FrameInboundWriter writer = new Http2FrameInboundWriter(channel);
+        writer.writeInboundSettings(new Http2Settings());
+        writer.writeInboundRstStream(Http2CodecUtil.HTTP_UPGRADE_STREAM_ID, Http2Error.CANCEL.code());
+
+        assertSame(handler, channel.pipeline().remove(handler.getClass()));
         assertNull(channel.pipeline().get(handler.getClass()));
         assertTrue(channel.finish());
 
@@ -74,6 +95,10 @@ public class Http2ServerUpgradeCodecTest {
         assertNotNull(settingsBuffer);
         settingsBuffer.release();
 
+        ByteBuf buf = channel.readOutbound();
+        assertNotNull(buf);
+        buf.release();
+
         assertNull(channel.readOutbound());
     }
 
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2StreamChannelIdTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2StreamChannelIdTest.java
new file mode 100644
index 0000000..dddeddc
--- /dev/null
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2StreamChannelIdTest.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2020 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 java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import org.junit.Test;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufInputStream;
+import io.netty.buffer.ByteBufOutputStream;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.ChannelId;
+import io.netty.channel.DefaultChannelId;
+
+import static org.junit.Assert.*;
+
+public class Http2StreamChannelIdTest {
+
+    @Test
+    public void testSerialization() throws Exception {
+        ChannelId normalInstance = new Http2StreamChannelId(DefaultChannelId.newInstance(), 0);
+
+        ByteBuf buf = Unpooled.buffer();
+        ObjectOutputStream outStream = new ObjectOutputStream(new ByteBufOutputStream(buf));
+        try {
+            outStream.writeObject(normalInstance);
+        } finally {
+            outStream.close();
+        }
+
+        ObjectInputStream inStream = new ObjectInputStream(new ByteBufInputStream(buf, true));
+        final ChannelId deserializedInstance;
+        try {
+            deserializedInstance = (ChannelId) inStream.readObject();
+        } finally {
+            inStream.close();
+        }
+
+        assertEquals(normalInstance, deserializedInstance);
+        assertEquals(normalInstance.hashCode(), deserializedInstance.hashCode());
+        assertEquals(0, normalInstance.compareTo(deserializedInstance));
+        assertEquals(normalInstance.asLongText(), deserializedInstance.asLongText());
+        assertEquals(normalInstance.asShortText(), deserializedInstance.asShortText());
+    }
+}
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 f660381..ff420bf 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
@@ -119,7 +119,7 @@ public final class Http2TestUtil {
 
     public static HpackEncoder newTestEncoder(boolean ignoreMaxHeaderListSize,
                                               long maxHeaderListSize, long maxHeaderTableSize) throws Http2Exception {
-        HpackEncoder hpackEncoder = new HpackEncoder();
+        HpackEncoder hpackEncoder = new HpackEncoder(false, 16, 0);
         ByteBuf buf = Unpooled.buffer();
         try {
             hpackEncoder.setMaxHeaderTableSize(buf, maxHeaderTableSize);
@@ -139,7 +139,7 @@ public final class Http2TestUtil {
     }
 
     public static HpackDecoder newTestDecoder(long maxHeaderListSize, long maxHeaderTableSize) throws Http2Exception {
-        HpackDecoder hpackDecoder = new HpackDecoder(maxHeaderListSize, 32);
+        HpackDecoder hpackDecoder = new HpackDecoder(maxHeaderListSize);
         hpackDecoder.setMaxHeaderTableSize(maxHeaderTableSize);
         return hpackDecoder;
     }
@@ -687,6 +687,10 @@ public final class Http2TestUtil {
         return ByteBufUtil.writeUtf8(UnpooledByteBufAllocator.DEFAULT, s);
     }
 
+    static ByteBuf bb(int size) {
+        return UnpooledByteBufAllocator.DEFAULT.buffer().writeZero(size);
+    }
+
     static void assertEqualsAndRelease(Http2Frame expected, Http2Frame actual) {
         try {
             assertEquals(expected, actual);
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/HttpConversionUtilTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/HttpConversionUtilTest.java
index 200dd39..21530c9 100644
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/HttpConversionUtilTest.java
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/HttpConversionUtilTest.java
@@ -17,11 +17,18 @@ package io.netty.handler.codec.http2;
 
 import io.netty.handler.codec.http.DefaultHttpHeaders;
 import io.netty.handler.codec.http.HttpHeaders;
+import io.netty.handler.codec.http.HttpVersion;
 import io.netty.util.AsciiString;
 import org.junit.Test;
 
 import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;
+import static io.netty.handler.codec.http.HttpHeaderNames.COOKIE;
+import static io.netty.handler.codec.http.HttpHeaderNames.HOST;
+import static io.netty.handler.codec.http.HttpHeaderNames.KEEP_ALIVE;
+import static io.netty.handler.codec.http.HttpHeaderNames.PROXY_CONNECTION;
 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.GZIP;
 import static io.netty.handler.codec.http.HttpHeaderValues.TRAILERS;
 import static org.junit.Assert.assertEquals;
@@ -143,4 +150,43 @@ public class HttpConversionUtilTest {
         assertEquals(1, out.size());
         assertSame("world", out.get("hello"));
     }
+
+    @Test
+    public void addHttp2ToHttpHeadersCombinesCookies() throws Http2Exception {
+        Http2Headers inHeaders = new DefaultHttp2Headers();
+        inHeaders.add("yes", "no");
+        inHeaders.add(COOKIE, "foo=bar");
+        inHeaders.add(COOKIE, "bax=baz");
+
+        HttpHeaders outHeaders = new DefaultHttpHeaders();
+
+        HttpConversionUtil.addHttp2ToHttpHeaders(5, inHeaders, outHeaders, HttpVersion.HTTP_1_1, false, false);
+        assertEquals("no", outHeaders.get("yes"));
+        assertEquals("foo=bar; bax=baz", outHeaders.get(COOKIE.toString()));
+    }
+
+    @Test
+    public void connectionSpecificHeadersShouldBeRemoved() {
+        HttpHeaders inHeaders = new DefaultHttpHeaders();
+        inHeaders.add(CONNECTION, "keep-alive");
+        inHeaders.add(HOST, "example.com");
+        @SuppressWarnings("deprecation")
+        AsciiString keepAlive = KEEP_ALIVE;
+        inHeaders.add(keepAlive, "timeout=5, max=1000");
+        @SuppressWarnings("deprecation")
+        AsciiString proxyConnection = PROXY_CONNECTION;
+        inHeaders.add(proxyConnection, "timeout=5, max=1000");
+        inHeaders.add(TRANSFER_ENCODING, "chunked");
+        inHeaders.add(UPGRADE, "h2c");
+
+        Http2Headers outHeaders = new DefaultHttp2Headers();
+        HttpConversionUtil.toHttp2Headers(inHeaders, outHeaders);
+
+        assertFalse(outHeaders.contains(CONNECTION));
+        assertFalse(outHeaders.contains(HOST));
+        assertFalse(outHeaders.contains(keepAlive));
+        assertFalse(outHeaders.contains(proxyConnection));
+        assertFalse(outHeaders.contains(TRANSFER_ENCODING));
+        assertFalse(outHeaders.contains(UPGRADE));
+    }
 }
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/LastInboundHandler.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/LastInboundHandler.java
index 38f400a..dcb9fb3 100644
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/LastInboundHandler.java
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/LastInboundHandler.java
@@ -99,7 +99,7 @@ public class LastInboundHandler extends ChannelDuplexHandler {
 
     @Override
     public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
-        if (writabilityStates == "") {
+        if ("".equals(writabilityStates)) {
             writabilityStates = String.valueOf(ctx.channel().isWritable());
         } else {
             writabilityStates += "," + ctx.channel().isWritable();
diff --git a/codec-memcache/pom.xml b/codec-memcache/pom.xml
index 40c2a68..a4a8e28 100644
--- a/codec-memcache/pom.xml
+++ b/codec-memcache/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>io.netty</groupId>
     <artifactId>netty-parent</artifactId>
-    <version>4.1.33.Final</version>
+    <version>4.1.48.Final</version>
   </parent>
 
   <artifactId>netty-codec-memcache</artifactId>
diff --git a/codec-memcache/src/main/java/io/netty/handler/codec/memcache/AbstractMemcacheObject.java b/codec-memcache/src/main/java/io/netty/handler/codec/memcache/AbstractMemcacheObject.java
index 2ac6c29..04313b5 100644
--- a/codec-memcache/src/main/java/io/netty/handler/codec/memcache/AbstractMemcacheObject.java
+++ b/codec-memcache/src/main/java/io/netty/handler/codec/memcache/AbstractMemcacheObject.java
@@ -17,6 +17,7 @@ package io.netty.handler.codec.memcache;
 
 import io.netty.handler.codec.DecoderResult;
 import io.netty.util.AbstractReferenceCounted;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.UnstableApi;
 
 /**
@@ -38,10 +39,6 @@ public abstract class AbstractMemcacheObject extends AbstractReferenceCounted im
 
     @Override
     public void setDecoderResult(DecoderResult result) {
-        if (result == null) {
-            throw new NullPointerException("DecoderResult should not be null.");
-        }
-
-        decoderResult = result;
+        this.decoderResult = ObjectUtil.checkNotNull(result, "DecoderResult should not be null.");
     }
 }
diff --git a/codec-memcache/src/main/java/io/netty/handler/codec/memcache/DefaultMemcacheContent.java b/codec-memcache/src/main/java/io/netty/handler/codec/memcache/DefaultMemcacheContent.java
index 1ff7bcd..dccd957 100644
--- a/codec-memcache/src/main/java/io/netty/handler/codec/memcache/DefaultMemcacheContent.java
+++ b/codec-memcache/src/main/java/io/netty/handler/codec/memcache/DefaultMemcacheContent.java
@@ -16,6 +16,7 @@
 package io.netty.handler.codec.memcache;
 
 import io.netty.buffer.ByteBuf;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.StringUtil;
 import io.netty.util.internal.UnstableApi;
 
@@ -31,10 +32,7 @@ public class DefaultMemcacheContent extends AbstractMemcacheObject implements Me
      * Creates a new instance with the specified content.
      */
     public DefaultMemcacheContent(ByteBuf content) {
-        if (content == null) {
-            throw new NullPointerException("Content cannot be null.");
-        }
-        this.content = content;
+        this.content = ObjectUtil.checkNotNull(content, "content");
     }
 
     @Override
diff --git a/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/AbstractBinaryMemcacheDecoder.java b/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/AbstractBinaryMemcacheDecoder.java
index 2c90382..bec754a 100644
--- a/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/AbstractBinaryMemcacheDecoder.java
+++ b/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/AbstractBinaryMemcacheDecoder.java
@@ -15,6 +15,8 @@
  */
 package io.netty.handler.codec.memcache.binary;
 
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
+
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
 import io.netty.channel.ChannelHandlerContext;
@@ -59,9 +61,7 @@ public abstract class AbstractBinaryMemcacheDecoder<M extends BinaryMemcacheMess
      * @param chunkSize the maximum chunk size of the payload.
      */
     protected AbstractBinaryMemcacheDecoder(int chunkSize) {
-        if (chunkSize < 0) {
-            throw new IllegalArgumentException("chunkSize must be a positive integer: " + chunkSize);
-        }
+        checkPositiveOrZero(chunkSize, "chunkSize");
 
         this.chunkSize = chunkSize;
     }
diff --git a/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/AbstractBinaryMemcacheMessage.java b/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/AbstractBinaryMemcacheMessage.java
index c40ddf4..c9417e9 100644
--- a/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/AbstractBinaryMemcacheMessage.java
+++ b/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/AbstractBinaryMemcacheMessage.java
@@ -78,7 +78,7 @@ public abstract class AbstractBinaryMemcacheMessage
         this.key = key;
         short oldKeyLength = keyLength;
         keyLength = key == null ? 0 : (short) key.readableBytes();
-        totalBodyLength  = totalBodyLength + keyLength - oldKeyLength;
+        totalBodyLength = totalBodyLength + keyLength - oldKeyLength;
         return this;
     }
 
@@ -232,4 +232,20 @@ public abstract class AbstractBinaryMemcacheMessage
         }
         return this;
     }
+
+    /**
+     * Copies special metadata hold by this instance to the provided instance
+     *
+     * @param dst The instance where to copy the metadata of this instance to
+     */
+    void copyMeta(AbstractBinaryMemcacheMessage dst) {
+        dst.magic = magic;
+        dst.opcode = opcode;
+        dst.keyLength = keyLength;
+        dst.extrasLength = extrasLength;
+        dst.dataType = dataType;
+        dst.totalBodyLength = totalBodyLength;
+        dst.opaque = opaque;
+        dst.cas = cas;
+    }
 }
diff --git a/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/DefaultBinaryMemcacheRequest.java b/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/DefaultBinaryMemcacheRequest.java
index 68feb03..bac5845 100644
--- a/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/DefaultBinaryMemcacheRequest.java
+++ b/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/DefaultBinaryMemcacheRequest.java
@@ -92,4 +92,14 @@ public class DefaultBinaryMemcacheRequest extends AbstractBinaryMemcacheMessage
         super.touch(hint);
         return this;
     }
+
+    /**
+     * Copies special metadata hold by this instance to the provided instance
+     *
+     * @param dst The instance where to copy the metadata of this instance to
+     */
+    void copyMeta(DefaultBinaryMemcacheRequest dst) {
+        super.copyMeta(dst);
+        dst.reserved = reserved;
+    }
 }
diff --git a/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/DefaultBinaryMemcacheResponse.java b/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/DefaultBinaryMemcacheResponse.java
index b632dce..639913e 100644
--- a/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/DefaultBinaryMemcacheResponse.java
+++ b/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/DefaultBinaryMemcacheResponse.java
@@ -41,7 +41,7 @@ public class DefaultBinaryMemcacheResponse extends AbstractBinaryMemcacheMessage
     /**
      * Create a new {@link DefaultBinaryMemcacheResponse} with the header and key.
      *
-     * @param key    the key to use
+     * @param key    the key to use.
      */
     public DefaultBinaryMemcacheResponse(ByteBuf key) {
         this(key, null);
@@ -92,4 +92,14 @@ public class DefaultBinaryMemcacheResponse extends AbstractBinaryMemcacheMessage
         super.touch(hint);
         return this;
     }
+
+    /**
+     * Copies special metadata hold by this instance to the provided instance
+     *
+     * @param dst The instance where to copy the metadata of this instance to
+     */
+    void copyMeta(DefaultBinaryMemcacheResponse dst) {
+        super.copyMeta(dst);
+        dst.status = status;
+    }
 }
diff --git a/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/DefaultFullBinaryMemcacheRequest.java b/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/DefaultFullBinaryMemcacheRequest.java
index dbc5bac..d0f485a 100644
--- a/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/DefaultFullBinaryMemcacheRequest.java
+++ b/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/DefaultFullBinaryMemcacheRequest.java
@@ -17,6 +17,7 @@ package io.netty.handler.codec.memcache.binary;
 
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.UnstableApi;
 
 /**
@@ -48,11 +49,7 @@ public class DefaultFullBinaryMemcacheRequest extends DefaultBinaryMemcacheReque
     public DefaultFullBinaryMemcacheRequest(ByteBuf key, ByteBuf extras,
                                             ByteBuf content) {
         super(key, extras);
-        if (content == null) {
-            throw new NullPointerException("Supplied content is null.");
-        }
-
-        this.content = content;
+        this.content = ObjectUtil.checkNotNull(content, "content");
         setTotalBodyLength(keyLength() + extrasLength() + content.readableBytes());
     }
 
@@ -102,7 +99,7 @@ public class DefaultFullBinaryMemcacheRequest extends DefaultBinaryMemcacheReque
         if (extras != null) {
             extras = extras.copy();
         }
-        return new DefaultFullBinaryMemcacheRequest(key, extras, content().copy());
+        return newInstance(key, extras, content().copy());
     }
 
     @Override
@@ -115,7 +112,7 @@ public class DefaultFullBinaryMemcacheRequest extends DefaultBinaryMemcacheReque
         if (extras != null) {
             extras = extras.duplicate();
         }
-        return new DefaultFullBinaryMemcacheRequest(key, extras, content().duplicate());
+        return newInstance(key, extras, content().duplicate());
     }
 
     @Override
@@ -133,6 +130,12 @@ public class DefaultFullBinaryMemcacheRequest extends DefaultBinaryMemcacheReque
         if (extras != null) {
             extras = extras.retainedDuplicate();
         }
-        return new DefaultFullBinaryMemcacheRequest(key, extras, content);
+        return newInstance(key, extras, content);
+    }
+
+    private DefaultFullBinaryMemcacheRequest newInstance(ByteBuf key, ByteBuf extras, ByteBuf content) {
+        DefaultFullBinaryMemcacheRequest newInstance = new DefaultFullBinaryMemcacheRequest(key, extras, content);
+        copyMeta(newInstance);
+        return newInstance;
     }
 }
diff --git a/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/DefaultFullBinaryMemcacheResponse.java b/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/DefaultFullBinaryMemcacheResponse.java
index 734cba8..686a9ad 100644
--- a/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/DefaultFullBinaryMemcacheResponse.java
+++ b/codec-memcache/src/main/java/io/netty/handler/codec/memcache/binary/DefaultFullBinaryMemcacheResponse.java
@@ -17,6 +17,7 @@ package io.netty.handler.codec.memcache.binary;
 
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.UnstableApi;
 
 /**
@@ -48,11 +49,7 @@ public class DefaultFullBinaryMemcacheResponse extends DefaultBinaryMemcacheResp
     public DefaultFullBinaryMemcacheResponse(ByteBuf key, ByteBuf extras,
         ByteBuf content) {
         super(key, extras);
-        if (content == null) {
-            throw new NullPointerException("Supplied content is null.");
-        }
-
-        this.content = content;
+        this.content = ObjectUtil.checkNotNull(content, "content");
         setTotalBodyLength(keyLength() + extrasLength() + content.readableBytes());
     }
 
@@ -102,7 +99,7 @@ public class DefaultFullBinaryMemcacheResponse extends DefaultBinaryMemcacheResp
         if (extras != null) {
             extras = extras.copy();
         }
-        return new DefaultFullBinaryMemcacheResponse(key, extras, content().copy());
+        return newInstance(key, extras, content().copy());
     }
 
     @Override
@@ -115,7 +112,7 @@ public class DefaultFullBinaryMemcacheResponse extends DefaultBinaryMemcacheResp
         if (extras != null) {
             extras = extras.duplicate();
         }
-        return new DefaultFullBinaryMemcacheResponse(key, extras, content().duplicate());
+        return newInstance(key, extras, content().duplicate());
     }
 
     @Override
@@ -133,6 +130,12 @@ public class DefaultFullBinaryMemcacheResponse extends DefaultBinaryMemcacheResp
         if (extras != null) {
             extras = extras.retainedDuplicate();
         }
-        return new DefaultFullBinaryMemcacheResponse(key, extras, content);
+        return newInstance(key, extras, content);
+    }
+
+    private FullBinaryMemcacheResponse newInstance(ByteBuf key, ByteBuf extras, ByteBuf content) {
+        DefaultFullBinaryMemcacheResponse newInstance = new DefaultFullBinaryMemcacheResponse(key, extras, content);
+        copyMeta(newInstance);
+        return newInstance;
     }
 }
diff --git a/codec-memcache/src/test/java/io/netty/handler/codec/memcache/binary/DefaultFullBinaryMemcacheRequestTest.java b/codec-memcache/src/test/java/io/netty/handler/codec/memcache/binary/DefaultFullBinaryMemcacheRequestTest.java
new file mode 100644
index 0000000..6b7e985
--- /dev/null
+++ b/codec-memcache/src/test/java/io/netty/handler/codec/memcache/binary/DefaultFullBinaryMemcacheRequestTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2019 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.memcache.binary;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.util.CharsetUtil;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotSame;
+
+public class DefaultFullBinaryMemcacheRequestTest {
+
+    private DefaultFullBinaryMemcacheRequest request;
+
+    @Before
+    public void setUp() {
+        request = new DefaultFullBinaryMemcacheRequest(
+                Unpooled.copiedBuffer("key", CharsetUtil.UTF_8),
+                Unpooled.wrappedBuffer(new byte[]{1, 3, 4, 9}),
+                Unpooled.copiedBuffer("some value", CharsetUtil.UTF_8));
+        request.setReserved((short) 534);
+        request.setMagic((byte) 0x03);
+        request.setOpcode((byte) 0x02);
+        request.setKeyLength((short) 32);
+        request.setExtrasLength((byte) 34);
+        request.setDataType((byte) 43);
+        request.setTotalBodyLength(345);
+        request.setOpaque(3);
+        request.setCas(345345L);
+    }
+
+    @Test
+    public void fullCopy() {
+        FullBinaryMemcacheRequest newInstance = request.copy();
+        try {
+            assertCopy(request, request.content(), newInstance);
+        } finally {
+            request.release();
+            newInstance.release();
+        }
+    }
+
+    @Test
+    public void fullDuplicate() {
+        FullBinaryMemcacheRequest newInstance = request.duplicate();
+        try {
+            assertCopy(request, request.content(), newInstance);
+        } finally {
+            request.release();
+        }
+    }
+
+    @Test
+    public void fullReplace() {
+        ByteBuf newContent = Unpooled.copiedBuffer("new value", CharsetUtil.UTF_8);
+        FullBinaryMemcacheRequest newInstance = request.replace(newContent);
+        try {
+            assertCopy(request, newContent, newInstance);
+        } finally {
+            request.release();
+            newInstance.release();
+        }
+    }
+
+    private void assertCopy(FullBinaryMemcacheRequest expected, ByteBuf expectedContent,
+                            FullBinaryMemcacheRequest actual) {
+        assertNotSame(expected, actual);
+
+        assertEquals(expected.key(), actual.key());
+        assertEquals(expected.extras(), actual.extras());
+        assertEquals(expectedContent, actual.content());
+
+        assertEquals(expected.reserved(), actual.reserved());
+        assertEquals(expected.magic(), actual.magic());
+        assertEquals(expected.opcode(), actual.opcode());
+        assertEquals(expected.keyLength(), actual.keyLength());
+        assertEquals(expected.extrasLength(), actual.extrasLength());
+        assertEquals(expected.dataType(), actual.dataType());
+        assertEquals(expected.totalBodyLength(), actual.totalBodyLength());
+        assertEquals(expected.opaque(), actual.opaque());
+        assertEquals(expected.cas(), actual.cas());
+    }
+}
diff --git a/codec-memcache/src/test/java/io/netty/handler/codec/memcache/binary/DefaultFullBinaryMemcacheResponseTest.java b/codec-memcache/src/test/java/io/netty/handler/codec/memcache/binary/DefaultFullBinaryMemcacheResponseTest.java
new file mode 100644
index 0000000..f94989b
--- /dev/null
+++ b/codec-memcache/src/test/java/io/netty/handler/codec/memcache/binary/DefaultFullBinaryMemcacheResponseTest.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2019 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.memcache.binary;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.util.CharsetUtil;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotSame;
+
+public class DefaultFullBinaryMemcacheResponseTest {
+
+    private DefaultFullBinaryMemcacheResponse response;
+
+    @Before
+    public void setUp() {
+        response = new DefaultFullBinaryMemcacheResponse(
+                Unpooled.copiedBuffer("key", CharsetUtil.UTF_8),
+                Unpooled.wrappedBuffer(new byte[]{1, 3, 4, 9}),
+                Unpooled.copiedBuffer("some value", CharsetUtil.UTF_8));
+        response.setStatus((short) 1);
+        response.setMagic((byte) 0x03);
+        response.setOpcode((byte) 0x02);
+        response.setKeyLength((short) 32);
+        response.setExtrasLength((byte) 34);
+        response.setDataType((byte) 43);
+        response.setTotalBodyLength(345);
+        response.setOpaque(3);
+        response.setCas(345345L);
+    }
+
+    @Test
+    public void fullCopy() {
+        FullBinaryMemcacheResponse newInstance = response.copy();
+        try {
+            assertResponseEquals(response, response.content(), newInstance);
+        } finally {
+            response.release();
+            newInstance.release();
+        }
+    }
+
+    @Test
+    public void fullDuplicate() {
+        try {
+            assertResponseEquals(response, response.content(), response.duplicate());
+        } finally {
+            response.release();
+        }
+    }
+
+    @Test
+    public void fullReplace() {
+        ByteBuf newContent = Unpooled.copiedBuffer("new value", CharsetUtil.UTF_8);
+        FullBinaryMemcacheResponse newInstance = response.replace(newContent);
+        try {
+            assertResponseEquals(response, newContent, newInstance);
+        } finally {
+            response.release();
+            newInstance.release();
+        }
+    }
+
+    private void assertResponseEquals(FullBinaryMemcacheResponse expected, ByteBuf expectedContent,
+                                      FullBinaryMemcacheResponse actual) {
+        assertNotSame(expected, actual);
+
+        assertEquals(expected.key(), actual.key());
+        assertEquals(expected.extras(), actual.extras());
+        assertEquals(expectedContent, actual.content());
+
+        assertEquals(expected.status(), actual.status());
+        assertEquals(expected.magic(), actual.magic());
+        assertEquals(expected.opcode(), actual.opcode());
+        assertEquals(expected.keyLength(), actual.keyLength());
+        assertEquals(expected.extrasLength(), actual.extrasLength());
+        assertEquals(expected.dataType(), actual.dataType());
+        assertEquals(expected.totalBodyLength(), actual.totalBodyLength());
+        assertEquals(expected.opaque(), actual.opaque());
+        assertEquals(expected.cas(), actual.cas());
+    }
+}
diff --git a/codec-mqtt/pom.xml b/codec-mqtt/pom.xml
index 1de30c7..f611818 100644
--- a/codec-mqtt/pom.xml
+++ b/codec-mqtt/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>io.netty</groupId>
     <artifactId>netty-parent</artifactId>
-    <version>4.1.33.Final</version>
+    <version>4.1.48.Final</version>
   </parent>
 
   <artifactId>netty-codec-mqtt</artifactId>
diff --git a/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttConnectPayload.java b/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttConnectPayload.java
index b7a4d99..93f63e5 100644
--- a/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttConnectPayload.java
+++ b/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttConnectPayload.java
@@ -16,6 +16,8 @@
 
 package io.netty.handler.codec.mqtt;
 
+import java.util.Arrays;
+
 import io.netty.util.CharsetUtil;
 import io.netty.util.internal.StringUtil;
 
@@ -103,9 +105,9 @@ public final class MqttConnectPayload {
             .append('[')
             .append("clientIdentifier=").append(clientIdentifier)
             .append(", willTopic=").append(willTopic)
-            .append(", willMessage=").append(willMessage)
+            .append(", willMessage=").append(Arrays.toString(willMessage))
             .append(", userName=").append(userName)
-            .append(", password=").append(password)
+            .append(", password=").append(Arrays.toString(password))
             .append(']')
             .toString();
     }
diff --git a/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttMessage.java b/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttMessage.java
index 9b1efa0..4a1a620 100644
--- a/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttMessage.java
+++ b/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttMessage.java
@@ -29,6 +29,17 @@ public class MqttMessage {
     private final Object payload;
     private final DecoderResult decoderResult;
 
+    // Constants for fixed-header only message types with all flags set to 0 (see
+    // http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Table_2.2_-)
+    public static final MqttMessage PINGREQ = new MqttMessage(new MqttFixedHeader(MqttMessageType.PINGREQ, false,
+            MqttQoS.AT_MOST_ONCE, false, 0));
+
+    public static final MqttMessage PINGRESP = new MqttMessage(new MqttFixedHeader(MqttMessageType.PINGRESP, false,
+            MqttQoS.AT_MOST_ONCE, false, 0));
+
+    public static final MqttMessage DISCONNECT = new MqttMessage(new MqttFixedHeader(MqttMessageType.DISCONNECT, false,
+            MqttQoS.AT_MOST_ONCE, false, 0));
+
     public MqttMessage(MqttFixedHeader mqttFixedHeader) {
         this(mqttFixedHeader, null, null);
     }
diff --git a/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttSubAckPayload.java b/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttSubAckPayload.java
index 0e8c909..0929b72 100644
--- a/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttSubAckPayload.java
+++ b/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttSubAckPayload.java
@@ -16,6 +16,7 @@
 
 package io.netty.handler.codec.mqtt;
 
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.StringUtil;
 
 import java.util.ArrayList;
@@ -30,9 +31,7 @@ public class MqttSubAckPayload {
     private final List<Integer> grantedQoSLevels;
 
     public MqttSubAckPayload(int... grantedQoSLevels) {
-        if (grantedQoSLevels == null) {
-            throw new NullPointerException("grantedQoSLevels");
-        }
+        ObjectUtil.checkNotNull(grantedQoSLevels, "grantedQoSLevels");
 
         List<Integer> list = new ArrayList<Integer>(grantedQoSLevels.length);
         for (int v: grantedQoSLevels) {
@@ -42,9 +41,7 @@ public class MqttSubAckPayload {
     }
 
     public MqttSubAckPayload(Iterable<Integer> grantedQoSLevels) {
-        if (grantedQoSLevels == null) {
-            throw new NullPointerException("grantedQoSLevels");
-        }
+        ObjectUtil.checkNotNull(grantedQoSLevels, "grantedQoSLevels");
         List<Integer> list = new ArrayList<Integer>();
         for (Integer v: grantedQoSLevels) {
             if (v == null) {
diff --git a/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttSubscribePayload.java b/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttSubscribePayload.java
index eb1b9c9..aa3a324 100644
--- a/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttSubscribePayload.java
+++ b/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttSubscribePayload.java
@@ -39,11 +39,12 @@ public final class MqttSubscribePayload {
     @Override
     public String toString() {
         StringBuilder builder = new StringBuilder(StringUtil.simpleClassName(this)).append('[');
-        for (int i = 0; i < topicSubscriptions.size() - 1; i++) {
+        for (int i = 0; i < topicSubscriptions.size(); i++) {
             builder.append(topicSubscriptions.get(i)).append(", ");
         }
-        builder.append(topicSubscriptions.get(topicSubscriptions.size() - 1));
-        builder.append(']');
-        return builder.toString();
+        if (!topicSubscriptions.isEmpty()) {
+            builder.setLength(builder.length() - 2);
+        }
+        return builder.append(']').toString();
     }
 }
diff --git a/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttUnsubscribePayload.java b/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttUnsubscribePayload.java
index b032d12..9812bd9 100644
--- a/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttUnsubscribePayload.java
+++ b/codec-mqtt/src/main/java/io/netty/handler/codec/mqtt/MqttUnsubscribePayload.java
@@ -39,11 +39,12 @@ public final class MqttUnsubscribePayload {
     @Override
     public String toString() {
         StringBuilder builder = new StringBuilder(StringUtil.simpleClassName(this)).append('[');
-        for (int i = 0; i < topics.size() - 1; i++) {
+        for (int i = 0; i < topics.size(); i++) {
             builder.append("topicName = ").append(topics.get(i)).append(", ");
         }
-        builder.append("topicName = ").append(topics.get(topics.size() - 1))
-               .append(']');
-        return builder.toString();
+        if (!topics.isEmpty()) {
+            builder.setLength(builder.length() - 2);
+        }
+        return builder.append("]").toString();
     }
 }
diff --git a/codec-mqtt/src/test/java/io/netty/handler/codec/mqtt/MqttCodecTest.java b/codec-mqtt/src/test/java/io/netty/handler/codec/mqtt/MqttCodecTest.java
index 927334c..2264dc4 100644
--- a/codec-mqtt/src/test/java/io/netty/handler/codec/mqtt/MqttCodecTest.java
+++ b/codec-mqtt/src/test/java/io/netty/handler/codec/mqtt/MqttCodecTest.java
@@ -266,17 +266,17 @@ public class MqttCodecTest {
 
     @Test
     public void testPingReqMessage() throws Exception {
-        testMessageWithOnlyFixedHeader(MqttMessageType.PINGREQ);
+        testMessageWithOnlyFixedHeader(MqttMessage.PINGREQ);
     }
 
     @Test
     public void testPingRespMessage() throws Exception {
-        testMessageWithOnlyFixedHeader(MqttMessageType.PINGRESP);
+        testMessageWithOnlyFixedHeader(MqttMessage.PINGRESP);
     }
 
     @Test
     public void testDisconnectMessage() throws Exception {
-        testMessageWithOnlyFixedHeader(MqttMessageType.DISCONNECT);
+        testMessageWithOnlyFixedHeader(MqttMessage.DISCONNECT);
     }
 
     @Test
@@ -450,8 +450,7 @@ public class MqttCodecTest {
         }
     }
 
-    private void testMessageWithOnlyFixedHeader(MqttMessageType messageType) throws Exception {
-        MqttMessage message = createMessageWithFixedHeader(messageType);
+    private void testMessageWithOnlyFixedHeader(MqttMessage message) throws Exception {
         ByteBuf byteBuf = MqttEncoder.doEncode(ALLOCATOR, message);
 
         final List<Object> out = new LinkedList<Object>();
diff --git a/codec-mqtt/src/test/java/io/netty/handler/codec/mqtt/MqttConnectPayloadTest.java b/codec-mqtt/src/test/java/io/netty/handler/codec/mqtt/MqttConnectPayloadTest.java
index 5a929dc..f1d9dc0 100644
--- a/codec-mqtt/src/test/java/io/netty/handler/codec/mqtt/MqttConnectPayloadTest.java
+++ b/codec-mqtt/src/test/java/io/netty/handler/codec/mqtt/MqttConnectPayloadTest.java
@@ -21,6 +21,8 @@ import static org.junit.Assert.assertNull;
 import io.netty.util.CharsetUtil;
 import org.junit.Test;
 
+import java.util.Collections;
+
 public class MqttConnectPayloadTest {
 
     @Test
@@ -88,4 +90,11 @@ public class MqttConnectPayloadTest {
         assertNull(mqttConnectPayload.willMessageInBytes());
         assertNull(mqttConnectPayload.willMessage());
     }
+
+    /* See https://github.com/netty/netty/pull/9202 */
+    @Test
+    public void testEmptyTopicsToString() {
+        new MqttSubscribePayload(Collections.<MqttTopicSubscription>emptyList()).toString();
+        new MqttUnsubscribePayload(Collections.<String>emptyList()).toString();
+    }
 }
diff --git a/codec-redis/pom.xml b/codec-redis/pom.xml
index bbad7d9..07d3bfc 100644
--- a/codec-redis/pom.xml
+++ b/codec-redis/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>io.netty</groupId>
     <artifactId>netty-parent</artifactId>
-    <version>4.1.33.Final</version>
+    <version>4.1.48.Final</version>
   </parent>
 
   <artifactId>netty-codec-redis</artifactId>
diff --git a/codec-redis/src/test/java/io/netty/handler/codec/redis/RedisDecoderTest.java b/codec-redis/src/test/java/io/netty/handler/codec/redis/RedisDecoderTest.java
index 6d9bd19..c62ab40 100644
--- a/codec-redis/src/test/java/io/netty/handler/codec/redis/RedisDecoderTest.java
+++ b/codec-redis/src/test/java/io/netty/handler/codec/redis/RedisDecoderTest.java
@@ -306,4 +306,12 @@ public class RedisDecoderTest {
         ReferenceCountUtil.release(msg);
         ReferenceCountUtil.release(childBuf);
     }
+
+    @Test
+    public void testPredefinedMessagesNotEqual() {
+        // both EMPTY_INSTANCE and NULL_INSTANCE have EMPTY_BUFFER as their 'data',
+        // however we need to check that they are not equal between themselves.
+        assertNotEquals(FullBulkStringRedisMessage.EMPTY_INSTANCE, FullBulkStringRedisMessage.NULL_INSTANCE);
+        assertNotEquals(FullBulkStringRedisMessage.NULL_INSTANCE, FullBulkStringRedisMessage.EMPTY_INSTANCE);
+    }
 }
diff --git a/codec-smtp/pom.xml b/codec-smtp/pom.xml
index 1cd05fd..b9ded10 100644
--- a/codec-smtp/pom.xml
+++ b/codec-smtp/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>io.netty</groupId>
     <artifactId>netty-parent</artifactId>
-    <version>4.1.33.Final</version>
+    <version>4.1.48.Final</version>
   </parent>
 
   <artifactId>netty-codec-smtp</artifactId>
diff --git a/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpCommand.java b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpCommand.java
index 63ba3f5..a2c69b8 100644
--- a/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpCommand.java
+++ b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpCommand.java
@@ -31,6 +31,7 @@ import java.util.Map;
 public final class SmtpCommand {
     public static final SmtpCommand EHLO = new SmtpCommand(AsciiString.cached("EHLO"));
     public static final SmtpCommand HELO = new SmtpCommand(AsciiString.cached("HELO"));
+    public static final SmtpCommand AUTH = new SmtpCommand(AsciiString.cached("AUTH"));
     public static final SmtpCommand MAIL = new SmtpCommand(AsciiString.cached("MAIL"));
     public static final SmtpCommand RCPT = new SmtpCommand(AsciiString.cached("RCPT"));
     public static final SmtpCommand DATA = new SmtpCommand(AsciiString.cached("DATA"));
@@ -40,11 +41,13 @@ public final class SmtpCommand {
     public static final SmtpCommand VRFY = new SmtpCommand(AsciiString.cached("VRFY"));
     public static final SmtpCommand HELP = new SmtpCommand(AsciiString.cached("HELP"));
     public static final SmtpCommand QUIT = new SmtpCommand(AsciiString.cached("QUIT"));
+    public static final SmtpCommand EMPTY = new SmtpCommand(AsciiString.cached(""));
 
     private static final Map<String, SmtpCommand> COMMANDS = new HashMap<String, SmtpCommand>();
     static {
         COMMANDS.put(EHLO.name().toString(), EHLO);
         COMMANDS.put(HELO.name().toString(), HELO);
+        COMMANDS.put(AUTH.name().toString(), AUTH);
         COMMANDS.put(MAIL.name().toString(), MAIL);
         COMMANDS.put(RCPT.name().toString(), RCPT);
         COMMANDS.put(DATA.name().toString(), DATA);
@@ -54,6 +57,7 @@ public final class SmtpCommand {
         COMMANDS.put(VRFY.name().toString(), VRFY);
         COMMANDS.put(HELP.name().toString(), HELP);
         COMMANDS.put(QUIT.name().toString(), QUIT);
+        COMMANDS.put(EMPTY.name().toString(), EMPTY);
     }
 
     /**
diff --git a/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpRequestEncoder.java b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpRequestEncoder.java
index 9fb567f..0355a29 100644
--- a/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpRequestEncoder.java
+++ b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpRequestEncoder.java
@@ -58,7 +58,8 @@ public final class SmtpRequestEncoder extends MessageToMessageEncoder<Object> {
             final ByteBuf buffer = ctx.alloc().buffer();
             try {
                 req.command().encode(buffer);
-                writeParameters(req.parameters(), buffer);
+                boolean notEmpty = req.command() != SmtpCommand.EMPTY;
+                writeParameters(req.parameters(), buffer, notEmpty);
                 ByteBufUtil.writeShortBE(buffer, CRLF_SHORT);
                 out.add(buffer);
                 release = false;
@@ -85,11 +86,13 @@ public final class SmtpRequestEncoder extends MessageToMessageEncoder<Object> {
         }
     }
 
-    private static void writeParameters(List<CharSequence> parameters, ByteBuf out) {
+    private static void writeParameters(List<CharSequence> parameters, ByteBuf out, boolean commandNotEmpty) {
         if (parameters.isEmpty()) {
             return;
         }
-        out.writeByte(SP);
+        if (commandNotEmpty) {
+            out.writeByte(SP);
+        }
         if (parameters instanceof RandomAccess) {
             final int sizeMinusOne = parameters.size() - 1;
             for (int i = 0; i < sizeMinusOne; i++) {
diff --git a/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpRequests.java b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpRequests.java
index 2c42c5d..b9da1a3 100644
--- a/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpRequests.java
+++ b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpRequests.java
@@ -20,6 +20,7 @@ import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.UnstableApi;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -49,6 +50,20 @@ public final class SmtpRequests {
         return new DefaultSmtpRequest(SmtpCommand.EHLO, hostname);
     }
 
+    /**
+     * Creates a {@code EMPTY} request.
+     */
+    public static SmtpRequest empty(CharSequence... parameter) {
+        return new DefaultSmtpRequest(SmtpCommand.EMPTY, parameter);
+    }
+
+    /**
+     * Creates a {@code AUTH} request.
+     */
+    public static SmtpRequest auth(CharSequence... parameter) {
+        return new DefaultSmtpRequest(SmtpCommand.AUTH, parameter);
+    }
+
     /**
      * Creates a {@code NOOP} request.
      */
@@ -94,9 +109,7 @@ public final class SmtpRequests {
         } else {
             List<CharSequence> params = new ArrayList<CharSequence>(mailParameters.length + 1);
             params.add(sender != null? "FROM:<" + sender + '>' : FROM_NULL_SENDER);
-            for (CharSequence param : mailParameters) {
-                params.add(param);
-            }
+            Collections.addAll(params, mailParameters);
             return new DefaultSmtpRequest(SmtpCommand.MAIL, params);
         }
     }
@@ -111,9 +124,7 @@ public final class SmtpRequests {
         } else {
             List<CharSequence> params = new ArrayList<CharSequence>(rcptParameters.length + 1);
             params.add("TO:<" + recipient + '>');
-            for (CharSequence param : rcptParameters) {
-                params.add(param);
-            }
+            Collections.addAll(params, rcptParameters);
             return new DefaultSmtpRequest(SmtpCommand.RCPT, params);
         }
     }
diff --git a/codec-smtp/src/test/java/io/netty/handler/codec/smtp/SmtpRequestEncoderTest.java b/codec-smtp/src/test/java/io/netty/handler/codec/smtp/SmtpRequestEncoderTest.java
index 7717b15..320bae4 100644
--- a/codec-smtp/src/test/java/io/netty/handler/codec/smtp/SmtpRequestEncoderTest.java
+++ b/codec-smtp/src/test/java/io/netty/handler/codec/smtp/SmtpRequestEncoderTest.java
@@ -22,7 +22,9 @@ import io.netty.handler.codec.EncoderException;
 import io.netty.util.CharsetUtil;
 import org.junit.Test;
 
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 
 public class SmtpRequestEncoderTest {
 
@@ -36,6 +38,21 @@ public class SmtpRequestEncoderTest {
         testEncode(SmtpRequests.helo("localhost"), "HELO localhost\r\n");
     }
 
+    @Test
+    public void testEncodeAuth() {
+        testEncode(SmtpRequests.auth("LOGIN"), "AUTH LOGIN\r\n");
+    }
+
+    @Test
+    public void testEncodeAuthWithParameter() {
+        testEncode(SmtpRequests.auth("PLAIN", "dGVzdAB0ZXN0ADEyMzQ="), "AUTH PLAIN dGVzdAB0ZXN0ADEyMzQ=\r\n");
+    }
+
+    @Test
+    public void testEncodeEmpty() {
+        testEncode(SmtpRequests.empty("dGVzdAB0ZXN0ADEyMzQ="),  "dGVzdAB0ZXN0ADEyMzQ=\r\n");
+    }
+
     @Test
     public void testEncodeMail() {
         testEncode(SmtpRequests.mail("me@netty.io"), "MAIL FROM:<me@netty.io>\r\n");
@@ -92,8 +109,12 @@ public class SmtpRequestEncoderTest {
     @Test(expected = EncoderException.class)
     public void testThrowsIfContentExpected() {
         EmbeddedChannel channel = new EmbeddedChannel(new SmtpRequestEncoder());
-        assertTrue(channel.writeOutbound(SmtpRequests.data()));
-        channel.writeOutbound(SmtpRequests.noop());
+        try {
+            assertTrue(channel.writeOutbound(SmtpRequests.data()));
+            channel.writeOutbound(SmtpRequests.noop());
+        } finally {
+            channel.finishAndReleaseAll();
+        }
     }
 
     @Test
diff --git a/codec-socks/pom.xml b/codec-socks/pom.xml
index d2c20ef..8007e41 100644
--- a/codec-socks/pom.xml
+++ b/codec-socks/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>io.netty</groupId>
     <artifactId>netty-parent</artifactId>
-    <version>4.1.33.Final</version>
+    <version>4.1.48.Final</version>
   </parent>
 
   <artifactId>netty-codec-socks</artifactId>
diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksAuthRequest.java b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksAuthRequest.java
index 277e0d3..0b932a6 100644
--- a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksAuthRequest.java
+++ b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksAuthRequest.java
@@ -17,6 +17,7 @@ package io.netty.handler.codec.socks;
 
 import io.netty.buffer.ByteBuf;
 import io.netty.util.CharsetUtil;
+import io.netty.util.internal.ObjectUtil;
 
 import java.nio.charset.CharsetEncoder;
 
@@ -27,19 +28,15 @@ import java.nio.charset.CharsetEncoder;
  * @see SocksAuthRequestDecoder
  */
 public final class SocksAuthRequest extends SocksRequest {
-    private static final CharsetEncoder asciiEncoder = CharsetUtil.encoder(CharsetUtil.US_ASCII);
     private static final SocksSubnegotiationVersion SUBNEGOTIATION_VERSION = SocksSubnegotiationVersion.AUTH_PASSWORD;
     private final String username;
     private final String password;
 
     public SocksAuthRequest(String username, String password) {
         super(SocksRequestType.AUTH);
-        if (username == null) {
-            throw new NullPointerException("username");
-        }
-        if (password == null) {
-            throw new NullPointerException("username");
-        }
+        ObjectUtil.checkNotNull(username, "username");
+        ObjectUtil.checkNotNull(password, "password");
+        final CharsetEncoder asciiEncoder = CharsetUtil.encoder(CharsetUtil.US_ASCII);
         if (!asciiEncoder.canEncode(username) || !asciiEncoder.canEncode(password)) {
             throw new IllegalArgumentException(
                     "username: " + username + " or password: **** values should be in pure ascii");
diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksAuthResponse.java b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksAuthResponse.java
index 5cacdde..64dda05 100644
--- a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksAuthResponse.java
+++ b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksAuthResponse.java
@@ -16,6 +16,7 @@
 package io.netty.handler.codec.socks;
 
 import io.netty.buffer.ByteBuf;
+import io.netty.util.internal.ObjectUtil;
 
 /**
  * An socks auth response.
@@ -29,10 +30,7 @@ public final class SocksAuthResponse extends SocksResponse {
 
     public SocksAuthResponse(SocksAuthStatus authStatus) {
         super(SocksResponseType.AUTH);
-        if (authStatus == null) {
-            throw new NullPointerException("authStatus");
-        }
-        this.authStatus = authStatus;
+        this.authStatus = ObjectUtil.checkNotNull(authStatus, "authStatus");
     }
 
     /**
diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksCmdRequest.java b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksCmdRequest.java
index 18bd65f..997c71b 100644
--- a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksCmdRequest.java
+++ b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksCmdRequest.java
@@ -18,6 +18,7 @@ package io.netty.handler.codec.socks;
 import io.netty.buffer.ByteBuf;
 import io.netty.util.CharsetUtil;
 import io.netty.util.NetUtil;
+import io.netty.util.internal.ObjectUtil;
 
 import java.net.IDN;
 
@@ -35,15 +36,10 @@ public final class SocksCmdRequest extends SocksRequest {
 
     public SocksCmdRequest(SocksCmdType cmdType, SocksAddressType addressType, String host, int port) {
         super(SocksRequestType.CMD);
-        if (cmdType == null) {
-            throw new NullPointerException("cmdType");
-        }
-        if (addressType == null) {
-            throw new NullPointerException("addressType");
-        }
-        if (host == null) {
-            throw new NullPointerException("host");
-        }
+        ObjectUtil.checkNotNull(cmdType, "cmdType");
+        ObjectUtil.checkNotNull(addressType, "addressType");
+        ObjectUtil.checkNotNull(host, "host");
+
         switch (addressType) {
             case IPv4:
                 if (!NetUtil.isValidIpV4Address(host)) {
diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksCmdResponse.java b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksCmdResponse.java
index 94ee516..d4a0d1b 100644
--- a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksCmdResponse.java
+++ b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksCmdResponse.java
@@ -18,6 +18,7 @@ package io.netty.handler.codec.socks;
 import io.netty.buffer.ByteBuf;
 import io.netty.util.CharsetUtil;
 import io.netty.util.NetUtil;
+import io.netty.util.internal.ObjectUtil;
 
 import java.net.IDN;
 
@@ -61,12 +62,8 @@ public final class SocksCmdResponse extends SocksResponse {
      */
     public SocksCmdResponse(SocksCmdStatus cmdStatus, SocksAddressType addressType, String host, int port) {
         super(SocksResponseType.CMD);
-        if (cmdStatus == null) {
-            throw new NullPointerException("cmdStatus");
-        }
-        if (addressType == null) {
-            throw new NullPointerException("addressType");
-        }
+        ObjectUtil.checkNotNull(cmdStatus, "cmdStatus");
+        ObjectUtil.checkNotNull(addressType, "addressType");
         if (host != null) {
             switch (addressType) {
                 case IPv4:
diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksInitRequest.java b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksInitRequest.java
index 2299d2b..df6d4d7 100644
--- a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksInitRequest.java
+++ b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksInitRequest.java
@@ -16,6 +16,7 @@
 package io.netty.handler.codec.socks;
 
 import io.netty.buffer.ByteBuf;
+import io.netty.util.internal.ObjectUtil;
 
 import java.util.Collections;
 import java.util.List;
@@ -31,10 +32,7 @@ public final class SocksInitRequest extends SocksRequest {
 
     public SocksInitRequest(List<SocksAuthScheme> authSchemes) {
         super(SocksRequestType.INIT);
-        if (authSchemes == null) {
-            throw new NullPointerException("authSchemes");
-        }
-        this.authSchemes = authSchemes;
+        this.authSchemes = ObjectUtil.checkNotNull(authSchemes, "authSchemes");
     }
 
     /**
diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksInitResponse.java b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksInitResponse.java
index b4d9039..9857e64 100644
--- a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksInitResponse.java
+++ b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksInitResponse.java
@@ -16,6 +16,7 @@
 package io.netty.handler.codec.socks;
 
 import io.netty.buffer.ByteBuf;
+import io.netty.util.internal.ObjectUtil;
 
 /**
  * An socks init response.
@@ -28,10 +29,7 @@ public final class SocksInitResponse extends SocksResponse {
 
     public SocksInitResponse(SocksAuthScheme authScheme) {
         super(SocksResponseType.INIT);
-        if (authScheme == null) {
-            throw new NullPointerException("authScheme");
-        }
-        this.authScheme = authScheme;
+        this.authScheme = ObjectUtil.checkNotNull(authScheme, "authScheme");
     }
 
     /**
diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksMessage.java b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksMessage.java
index 5f5f48b..46b0de0 100644
--- a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksMessage.java
+++ b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksMessage.java
@@ -16,6 +16,7 @@
 package io.netty.handler.codec.socks;
 
 import io.netty.buffer.ByteBuf;
+import io.netty.util.internal.ObjectUtil;
 
 /**
  * An abstract class that defines a SocksMessage, providing common properties for
@@ -30,10 +31,7 @@ public abstract class SocksMessage {
     private final SocksProtocolVersion protocolVersion = SocksProtocolVersion.SOCKS5;
 
     protected SocksMessage(SocksMessageType type) {
-        if (type == null) {
-            throw new NullPointerException("type");
-        }
-        this.type = type;
+        this.type = ObjectUtil.checkNotNull(type, "type");
     }
 
     /**
diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksRequest.java b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksRequest.java
index 148353c..2dd29a0 100644
--- a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksRequest.java
+++ b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksRequest.java
@@ -15,6 +15,8 @@
  */
 package io.netty.handler.codec.socks;
 
+import io.netty.util.internal.ObjectUtil;
+
 /**
  * An abstract class that defines a SocksRequest, providing common properties for
  * {@link SocksInitRequest}, {@link SocksAuthRequest}, {@link SocksCmdRequest} and {@link UnknownSocksRequest}.
@@ -29,10 +31,7 @@ public abstract class SocksRequest extends SocksMessage {
 
     protected SocksRequest(SocksRequestType requestType) {
         super(SocksMessageType.REQUEST);
-        if (requestType == null) {
-            throw new NullPointerException("requestType");
-        }
-        this.requestType = requestType;
+        this.requestType = ObjectUtil.checkNotNull(requestType, "requestType");
     }
 
     /**
diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksResponse.java b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksResponse.java
index 8bdd3cc..e4226d3 100644
--- a/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksResponse.java
+++ b/codec-socks/src/main/java/io/netty/handler/codec/socks/SocksResponse.java
@@ -15,6 +15,8 @@
  */
 package io.netty.handler.codec.socks;
 
+import io.netty.util.internal.ObjectUtil;
+
 /**
  * An abstract class that defines a SocksResponse, providing common properties for
  * {@link SocksInitResponse}, {@link SocksAuthResponse}, {@link SocksCmdResponse} and {@link UnknownSocksResponse}.
@@ -29,10 +31,7 @@ public abstract class SocksResponse extends SocksMessage {
 
     protected SocksResponse(SocksResponseType responseType) {
         super(SocksMessageType.RESPONSE);
-        if (responseType == null) {
-            throw new NullPointerException("responseType");
-        }
-        this.responseType = responseType;
+        this.responseType = ObjectUtil.checkNotNull(responseType, "responseType");
     }
 
     /**
diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/AbstractSocksMessage.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/AbstractSocksMessage.java
index fbb847a..e66c63c 100644
--- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/AbstractSocksMessage.java
+++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/AbstractSocksMessage.java
@@ -17,6 +17,7 @@
 package io.netty.handler.codec.socksx;
 
 import io.netty.handler.codec.DecoderResult;
+import io.netty.util.internal.ObjectUtil;
 
 /**
  * An abstract {@link SocksMessage}.
@@ -32,9 +33,6 @@ public abstract class AbstractSocksMessage implements SocksMessage {
 
     @Override
     public void setDecoderResult(DecoderResult decoderResult) {
-        if (decoderResult == null) {
-            throw new NullPointerException("decoderResult");
-        }
-        this.decoderResult = decoderResult;
+        this.decoderResult = ObjectUtil.checkNotNull(decoderResult, "decoderResult");
     }
 }
diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/SocksPortUnificationServerHandler.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/SocksPortUnificationServerHandler.java
index c4dfe3f..e826767 100644
--- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/SocksPortUnificationServerHandler.java
+++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/SocksPortUnificationServerHandler.java
@@ -25,6 +25,7 @@ import io.netty.handler.codec.socksx.v4.Socks4ServerEncoder;
 import io.netty.handler.codec.socksx.v5.Socks5AddressEncoder;
 import io.netty.handler.codec.socksx.v5.Socks5InitialRequestDecoder;
 import io.netty.handler.codec.socksx.v5.Socks5ServerEncoder;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 
@@ -53,11 +54,7 @@ public class SocksPortUnificationServerHandler extends ByteToMessageDecoder {
      * This constructor is useful when a user wants to use an alternative {@link Socks5AddressEncoder}.
      */
     public SocksPortUnificationServerHandler(Socks5ServerEncoder socks5encoder) {
-        if (socks5encoder == null) {
-            throw new NullPointerException("socks5encoder");
-        }
-
-        this.socks5encoder = socks5encoder;
+        this.socks5encoder = ObjectUtil.checkNotNull(socks5encoder, "socks5encoder");
     }
 
     @Override
diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v4/DefaultSocks4CommandRequest.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v4/DefaultSocks4CommandRequest.java
index 169768e..4fc527c 100755
--- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v4/DefaultSocks4CommandRequest.java
+++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v4/DefaultSocks4CommandRequest.java
@@ -16,6 +16,7 @@
 package io.netty.handler.codec.socksx.v4;
 
 import io.netty.handler.codec.DecoderResult;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.StringUtil;
 
 import java.net.IDN;
@@ -50,22 +51,13 @@ public class DefaultSocks4CommandRequest extends AbstractSocks4Message implement
      * @param userId the {@code USERID} field of the request
      */
     public DefaultSocks4CommandRequest(Socks4CommandType type, String dstAddr, int dstPort, String userId) {
-        if (type == null) {
-            throw new NullPointerException("type");
-        }
-        if (dstAddr == null) {
-            throw new NullPointerException("dstAddr");
-        }
         if (dstPort <= 0 || dstPort >= 65536) {
             throw new IllegalArgumentException("dstPort: " + dstPort + " (expected: 1~65535)");
         }
-        if (userId == null) {
-            throw new NullPointerException("userId");
-        }
-
-        this.userId = userId;
-        this.type = type;
-        this.dstAddr = IDN.toASCII(dstAddr);
+        this.type = ObjectUtil.checkNotNull(type, "type");
+        this.dstAddr = IDN.toASCII(
+                ObjectUtil.checkNotNull(dstAddr, "dstAddr"));
+        this.userId = ObjectUtil.checkNotNull(userId, "userId");
         this.dstPort = dstPort;
     }
 
diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v4/DefaultSocks4CommandResponse.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v4/DefaultSocks4CommandResponse.java
index bd76265..f9ba3f9 100755
--- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v4/DefaultSocks4CommandResponse.java
+++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v4/DefaultSocks4CommandResponse.java
@@ -17,6 +17,7 @@ package io.netty.handler.codec.socksx.v4;
 
 import io.netty.handler.codec.DecoderResult;
 import io.netty.util.NetUtil;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.StringUtil;
 
 /**
@@ -45,9 +46,6 @@ public class DefaultSocks4CommandResponse extends AbstractSocks4Message implemen
      * @param dstPort the {@code DSTPORT} field of the response
      */
     public DefaultSocks4CommandResponse(Socks4CommandStatus status, String dstAddr, int dstPort) {
-        if (status == null) {
-            throw new NullPointerException("cmdStatus");
-        }
         if (dstAddr != null) {
             if (!NetUtil.isValidIpV4Address(dstAddr)) {
                 throw new IllegalArgumentException(
@@ -58,7 +56,7 @@ public class DefaultSocks4CommandResponse extends AbstractSocks4Message implemen
             throw new IllegalArgumentException("dstPort: " + dstPort + " (expected: 0~65535)");
         }
 
-        this.status = status;
+        this.status = ObjectUtil.checkNotNull(status, "cmdStatus");
         this.dstAddr = dstAddr;
         this.dstPort = dstPort;
     }
diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v4/Socks4CommandStatus.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v4/Socks4CommandStatus.java
index fdcfe0f..959168d 100755
--- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v4/Socks4CommandStatus.java
+++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v4/Socks4CommandStatus.java
@@ -15,6 +15,8 @@
  */
 package io.netty.handler.codec.socksx.v4;
 
+import io.netty.util.internal.ObjectUtil;
+
 /**
  * The status of {@link Socks4CommandResponse}.
  */
@@ -49,12 +51,8 @@ public class Socks4CommandStatus implements Comparable<Socks4CommandStatus> {
     }
 
     public Socks4CommandStatus(int byteValue, String name) {
-        if (name == null) {
-            throw new NullPointerException("name");
-        }
-
+        this.name = ObjectUtil.checkNotNull(name, "name");
         this.byteValue = (byte) byteValue;
-        this.name = name;
     }
 
     public byte byteValue() {
diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v4/Socks4CommandType.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v4/Socks4CommandType.java
index 0cd6fe4..8cb0786 100755
--- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v4/Socks4CommandType.java
+++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v4/Socks4CommandType.java
@@ -15,6 +15,8 @@
  */
 package io.netty.handler.codec.socksx.v4;
 
+import io.netty.util.internal.ObjectUtil;
+
 /**
  * The type of {@link Socks4CommandRequest}.
  */
@@ -43,11 +45,8 @@ public class Socks4CommandType implements Comparable<Socks4CommandType> {
     }
 
     public Socks4CommandType(int byteValue, String name) {
-        if (name == null) {
-            throw new NullPointerException("name");
-        }
+        this.name = ObjectUtil.checkNotNull(name, "name");
         this.byteValue = (byte) byteValue;
-        this.name = name;
     }
 
     public byte byteValue() {
diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5CommandRequest.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5CommandRequest.java
index ddd35fd..83c39e2 100755
--- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5CommandRequest.java
+++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5CommandRequest.java
@@ -17,6 +17,7 @@ package io.netty.handler.codec.socksx.v5;
 
 import io.netty.handler.codec.DecoderResult;
 import io.netty.util.NetUtil;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.StringUtil;
 
 import java.net.IDN;
@@ -34,15 +35,9 @@ public final class DefaultSocks5CommandRequest extends AbstractSocks5Message imp
     public DefaultSocks5CommandRequest(
             Socks5CommandType type, Socks5AddressType dstAddrType, String dstAddr, int dstPort) {
 
-        if (type == null) {
-            throw new NullPointerException("type");
-        }
-        if (dstAddrType == null) {
-            throw new NullPointerException("dstAddrType");
-        }
-        if (dstAddr == null) {
-            throw new NullPointerException("dstAddr");
-        }
+        this.type = ObjectUtil.checkNotNull(type, "type");
+        ObjectUtil.checkNotNull(dstAddrType, "dstAddrType");
+        ObjectUtil.checkNotNull(dstAddr, "dstAddr");
 
         if (dstAddrType == Socks5AddressType.IPv4) {
             if (!NetUtil.isValidIpV4Address(dstAddr)) {
@@ -63,7 +58,6 @@ public final class DefaultSocks5CommandRequest extends AbstractSocks5Message imp
             throw new IllegalArgumentException("dstPort: " + dstPort + " (expected: 0~65535)");
         }
 
-        this.type = type;
         this.dstAddrType = dstAddrType;
         this.dstAddr = dstAddr;
         this.dstPort = dstPort;
diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5CommandResponse.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5CommandResponse.java
index 8d9719e..c46a77a 100755
--- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5CommandResponse.java
+++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5CommandResponse.java
@@ -17,6 +17,7 @@ package io.netty.handler.codec.socksx.v5;
 
 import io.netty.handler.codec.DecoderResult;
 import io.netty.util.NetUtil;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.StringUtil;
 
 import java.net.IDN;
@@ -38,12 +39,9 @@ public final class DefaultSocks5CommandResponse extends AbstractSocks5Message im
     public DefaultSocks5CommandResponse(
             Socks5CommandStatus status, Socks5AddressType bndAddrType, String bndAddr, int bndPort) {
 
-        if (status == null) {
-            throw new NullPointerException("status");
-        }
-        if (bndAddrType == null) {
-            throw new NullPointerException("bndAddrType");
-        }
+        ObjectUtil.checkNotNull(status, "status");
+        ObjectUtil.checkNotNull(bndAddrType, "bndAddrType");
+
         if (bndAddr != null) {
             if (bndAddrType == Socks5AddressType.IPv4) {
                 if (!NetUtil.isValidIpV4Address(bndAddr)) {
diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5InitialRequest.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5InitialRequest.java
index 0610e04..5cef490 100755
--- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5InitialRequest.java
+++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5InitialRequest.java
@@ -16,6 +16,7 @@
 package io.netty.handler.codec.socksx.v5;
 
 import io.netty.handler.codec.DecoderResult;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.StringUtil;
 
 import java.util.ArrayList;
@@ -30,9 +31,7 @@ public class DefaultSocks5InitialRequest extends AbstractSocks5Message implement
     private final List<Socks5AuthMethod> authMethods;
 
     public DefaultSocks5InitialRequest(Socks5AuthMethod... authMethods) {
-        if (authMethods == null) {
-            throw new NullPointerException("authMethods");
-        }
+        ObjectUtil.checkNotNull(authMethods, "authMethods");
 
         List<Socks5AuthMethod> list = new ArrayList<Socks5AuthMethod>(authMethods.length);
         for (Socks5AuthMethod m: authMethods) {
@@ -50,9 +49,7 @@ public class DefaultSocks5InitialRequest extends AbstractSocks5Message implement
     }
 
     public DefaultSocks5InitialRequest(Iterable<Socks5AuthMethod> authMethods) {
-        if (authMethods == null) {
-            throw new NullPointerException("authSchemes");
-        }
+        ObjectUtil.checkNotNull(authMethods, "authSchemes");
 
         List<Socks5AuthMethod> list = new ArrayList<Socks5AuthMethod>();
         for (Socks5AuthMethod m: authMethods) {
diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5InitialResponse.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5InitialResponse.java
index 23d1c12..4fe878d 100755
--- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5InitialResponse.java
+++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5InitialResponse.java
@@ -16,6 +16,7 @@
 package io.netty.handler.codec.socksx.v5;
 
 import io.netty.handler.codec.DecoderResult;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.StringUtil;
 
 /**
@@ -26,10 +27,7 @@ public class DefaultSocks5InitialResponse extends AbstractSocks5Message implemen
     private final Socks5AuthMethod authMethod;
 
     public DefaultSocks5InitialResponse(Socks5AuthMethod authMethod) {
-        if (authMethod == null) {
-            throw new NullPointerException("authMethod");
-        }
-        this.authMethod = authMethod;
+        this.authMethod = ObjectUtil.checkNotNull(authMethod, "authMethod");
     }
 
     @Override
diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5PasswordAuthRequest.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5PasswordAuthRequest.java
index 6917789..c404711 100755
--- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5PasswordAuthRequest.java
+++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5PasswordAuthRequest.java
@@ -16,6 +16,7 @@
 package io.netty.handler.codec.socksx.v5;
 
 import io.netty.handler.codec.DecoderResult;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.StringUtil;
 
 /**
@@ -27,12 +28,8 @@ public class DefaultSocks5PasswordAuthRequest extends AbstractSocks5Message impl
     private final String password;
 
     public DefaultSocks5PasswordAuthRequest(String username, String password) {
-        if (username == null) {
-            throw new NullPointerException("username");
-        }
-        if (password == null) {
-            throw new NullPointerException("password");
-        }
+        ObjectUtil.checkNotNull(username, "username");
+        ObjectUtil.checkNotNull(password, "password");
 
         if (username.length() > 255) {
             throw new IllegalArgumentException("username: **** (expected: less than 256 chars)");
diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5PasswordAuthResponse.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5PasswordAuthResponse.java
index 6cd9a30..07002c7 100755
--- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5PasswordAuthResponse.java
+++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/DefaultSocks5PasswordAuthResponse.java
@@ -16,6 +16,7 @@
 package io.netty.handler.codec.socksx.v5;
 
 import io.netty.handler.codec.DecoderResult;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.StringUtil;
 
 /**
@@ -26,11 +27,7 @@ public class DefaultSocks5PasswordAuthResponse extends AbstractSocks5Message imp
     private final Socks5PasswordAuthStatus status;
 
     public DefaultSocks5PasswordAuthResponse(Socks5PasswordAuthStatus status) {
-        if (status == null) {
-            throw new NullPointerException("status");
-        }
-
-        this.status = status;
+        this.status = ObjectUtil.checkNotNull(status, "status");
     }
 
     @Override
diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5AddressType.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5AddressType.java
index 554721a..da85114 100755
--- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5AddressType.java
+++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5AddressType.java
@@ -16,6 +16,8 @@
 
 package io.netty.handler.codec.socksx.v5;
 
+import io.netty.util.internal.ObjectUtil;
+
 /**
  * The type of address in {@link Socks5CommandRequest} and {@link Socks5CommandResponse}.
  */
@@ -47,12 +49,8 @@ public class Socks5AddressType implements Comparable<Socks5AddressType> {
     }
 
     public Socks5AddressType(int byteValue, String name) {
-        if (name == null) {
-            throw new NullPointerException("name");
-        }
-
+        this.name = ObjectUtil.checkNotNull(name, "name");
         this.byteValue = (byte) byteValue;
-        this.name = name;
     }
 
     public byte byteValue() {
diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5AuthMethod.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5AuthMethod.java
index 8efceee..0f5bfbb 100755
--- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5AuthMethod.java
+++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5AuthMethod.java
@@ -16,6 +16,8 @@
 
 package io.netty.handler.codec.socksx.v5;
 
+import io.netty.util.internal.ObjectUtil;
+
 /**
  * The authentication method of SOCKS5.
  */
@@ -54,12 +56,8 @@ public class Socks5AuthMethod implements Comparable<Socks5AuthMethod> {
     }
 
     public Socks5AuthMethod(int byteValue, String name) {
-        if (name == null) {
-            throw new NullPointerException("name");
-        }
-
+        this.name = ObjectUtil.checkNotNull(name, "name");
         this.byteValue = (byte) byteValue;
-        this.name = name;
     }
 
     public byte byteValue() {
diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5ClientEncoder.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5ClientEncoder.java
index ea6092a..f460fca 100644
--- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5ClientEncoder.java
+++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5ClientEncoder.java
@@ -22,6 +22,7 @@ import io.netty.channel.ChannelHandler.Sharable;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.handler.codec.EncoderException;
 import io.netty.handler.codec.MessageToByteEncoder;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.StringUtil;
 
 import java.util.List;
@@ -48,11 +49,7 @@ public class Socks5ClientEncoder extends MessageToByteEncoder<Socks5Message> {
      * Creates a new instance with the specified {@link Socks5AddressEncoder}.
      */
     public Socks5ClientEncoder(Socks5AddressEncoder addressEncoder) {
-        if (addressEncoder == null) {
-            throw new NullPointerException("addressEncoder");
-        }
-
-        this.addressEncoder = addressEncoder;
+        this.addressEncoder = ObjectUtil.checkNotNull(addressEncoder, "addressEncoder");
     }
 
     /**
diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5CommandRequestDecoder.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5CommandRequestDecoder.java
index eab1b15..444d8fc 100644
--- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5CommandRequestDecoder.java
+++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5CommandRequestDecoder.java
@@ -23,6 +23,7 @@ import io.netty.handler.codec.DecoderResult;
 import io.netty.handler.codec.ReplayingDecoder;
 import io.netty.handler.codec.socksx.SocksVersion;
 import io.netty.handler.codec.socksx.v5.Socks5CommandRequestDecoder.State;
+import io.netty.util.internal.ObjectUtil;
 
 import java.util.List;
 
@@ -48,11 +49,7 @@ public class Socks5CommandRequestDecoder extends ReplayingDecoder<State> {
 
     public Socks5CommandRequestDecoder(Socks5AddressDecoder addressDecoder) {
         super(State.INIT);
-        if (addressDecoder == null) {
-            throw new NullPointerException("addressDecoder");
-        }
-
-        this.addressDecoder = addressDecoder;
+        this.addressDecoder = ObjectUtil.checkNotNull(addressDecoder, "addressDecoder");
     }
 
     @Override
diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5CommandResponseDecoder.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5CommandResponseDecoder.java
index e3ac387..ce6c59e 100644
--- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5CommandResponseDecoder.java
+++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5CommandResponseDecoder.java
@@ -23,6 +23,7 @@ import io.netty.handler.codec.DecoderResult;
 import io.netty.handler.codec.ReplayingDecoder;
 import io.netty.handler.codec.socksx.SocksVersion;
 import io.netty.handler.codec.socksx.v5.Socks5CommandResponseDecoder.State;
+import io.netty.util.internal.ObjectUtil;
 
 import java.util.List;
 
@@ -48,11 +49,7 @@ public class Socks5CommandResponseDecoder extends ReplayingDecoder<State> {
 
     public Socks5CommandResponseDecoder(Socks5AddressDecoder addressDecoder) {
         super(State.INIT);
-        if (addressDecoder == null) {
-            throw new NullPointerException("addressDecoder");
-        }
-
-        this.addressDecoder = addressDecoder;
+        this.addressDecoder = ObjectUtil.checkNotNull(addressDecoder, "addressDecoder");
     }
 
     @Override
diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5CommandStatus.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5CommandStatus.java
index 7973c04..ff2ee3d 100755
--- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5CommandStatus.java
+++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5CommandStatus.java
@@ -16,6 +16,8 @@
 
 package io.netty.handler.codec.socksx.v5;
 
+import io.netty.util.internal.ObjectUtil;
+
 /**
  * The status of {@link Socks5CommandResponse}.
  */
@@ -65,12 +67,8 @@ public class Socks5CommandStatus implements Comparable<Socks5CommandStatus> {
     }
 
     public Socks5CommandStatus(int byteValue, String name) {
-        if (name == null) {
-            throw new NullPointerException("name");
-        }
-
+        this.name = ObjectUtil.checkNotNull(name, "name");
         this.byteValue = (byte) byteValue;
-        this.name = name;
     }
 
     public byte byteValue() {
diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5CommandType.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5CommandType.java
index ecc4ca3..d0ac784 100755
--- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5CommandType.java
+++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5CommandType.java
@@ -16,6 +16,8 @@
 
 package io.netty.handler.codec.socksx.v5;
 
+import io.netty.util.internal.ObjectUtil;
+
 /**
  * The type of {@link Socks5CommandRequest}.
  */
@@ -47,12 +49,8 @@ public class Socks5CommandType implements Comparable<Socks5CommandType> {
     }
 
     public Socks5CommandType(int byteValue, String name) {
-        if (name == null) {
-            throw new NullPointerException("name");
-        }
-
+        this.name = ObjectUtil.checkNotNull(name, "name");
         this.byteValue = (byte) byteValue;
-        this.name = name;
     }
 
     public byte byteValue() {
diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5InitialRequestDecoder.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5InitialRequestDecoder.java
index f9a663a..45682fa 100644
--- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5InitialRequestDecoder.java
+++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5InitialRequestDecoder.java
@@ -56,9 +56,6 @@ public class Socks5InitialRequestDecoder extends ReplayingDecoder<State> {
                 }
 
                 final int authMethodCnt = in.readUnsignedByte();
-                if (actualReadableBytes() < authMethodCnt) {
-                    break;
-                }
 
                 final Socks5AuthMethod[] authMethods = new Socks5AuthMethod[authMethodCnt];
                 for (int i = 0; i < authMethodCnt; i++) {
diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5PasswordAuthStatus.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5PasswordAuthStatus.java
index e7ea4a1..e26b26a 100755
--- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5PasswordAuthStatus.java
+++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5PasswordAuthStatus.java
@@ -16,6 +16,8 @@
 
 package io.netty.handler.codec.socksx.v5;
 
+import io.netty.util.internal.ObjectUtil;
+
 /**
  * The status of {@link Socks5PasswordAuthResponse}.
  */
@@ -44,12 +46,8 @@ public class Socks5PasswordAuthStatus implements Comparable<Socks5PasswordAuthSt
     }
 
     public Socks5PasswordAuthStatus(int byteValue, String name) {
-        if (name == null) {
-            throw new NullPointerException("name");
-        }
-
+        this.name = ObjectUtil.checkNotNull(name, "name");
         this.byteValue = (byte) byteValue;
-        this.name = name;
     }
 
     public byte byteValue() {
diff --git a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5ServerEncoder.java b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5ServerEncoder.java
index d24e559..fe7a8ce 100644
--- a/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5ServerEncoder.java
+++ b/codec-socks/src/main/java/io/netty/handler/codec/socksx/v5/Socks5ServerEncoder.java
@@ -21,6 +21,7 @@ import io.netty.channel.ChannelHandler.Sharable;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.handler.codec.EncoderException;
 import io.netty.handler.codec.MessageToByteEncoder;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.StringUtil;
 
 /**
@@ -44,11 +45,7 @@ public class Socks5ServerEncoder extends MessageToByteEncoder<Socks5Message> {
      * Creates a new instance with the specified {@link Socks5AddressEncoder}.
      */
     public Socks5ServerEncoder(Socks5AddressEncoder addressEncoder) {
-        if (addressEncoder == null) {
-            throw new NullPointerException("addressEncoder");
-        }
-
-        this.addressEncoder = addressEncoder;
+        this.addressEncoder = ObjectUtil.checkNotNull(addressEncoder, "addressEncoder");
     }
 
     /**
diff --git a/codec-socks/src/test/java/io/netty/handler/codec/socksx/v5/Socks5InitialRequestDecoderTest.java b/codec-socks/src/test/java/io/netty/handler/codec/socksx/v5/Socks5InitialRequestDecoderTest.java
new file mode 100644
index 0000000..e472c0f
--- /dev/null
+++ b/codec-socks/src/test/java/io/netty/handler/codec/socksx/v5/Socks5InitialRequestDecoderTest.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2019 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.socksx.v5;
+
+import io.netty.buffer.Unpooled;
+import io.netty.channel.embedded.EmbeddedChannel;
+import io.netty.handler.codec.DecoderResult;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class Socks5InitialRequestDecoderTest {
+    @Test
+    public void testUnpackingCausesDecodeFail() {
+        EmbeddedChannel e = new EmbeddedChannel(new Socks5InitialRequestDecoder());
+        assertFalse(e.writeInbound(Unpooled.wrappedBuffer(new byte[]{5, 2, 0})));
+        assertTrue(e.writeInbound(Unpooled.wrappedBuffer(new byte[]{1})));
+        Object o = e.readInbound();
+
+        assertTrue(o instanceof DefaultSocks5InitialRequest);
+        DefaultSocks5InitialRequest req = (DefaultSocks5InitialRequest) o;
+        assertSame(req.decoderResult(), DecoderResult.SUCCESS);
+        assertFalse(e.finish());
+    }
+}
diff --git a/codec-stomp/pom.xml b/codec-stomp/pom.xml
index 7f5e47d..ec0cd09 100644
--- a/codec-stomp/pom.xml
+++ b/codec-stomp/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>io.netty</groupId>
     <artifactId>netty-parent</artifactId>
-    <version>4.1.33.Final</version>
+    <version>4.1.48.Final</version>
   </parent>
 
   <artifactId>netty-codec-stomp</artifactId>
diff --git a/codec-stomp/src/main/java/io/netty/handler/codec/stomp/DefaultStompFrame.java b/codec-stomp/src/main/java/io/netty/handler/codec/stomp/DefaultStompFrame.java
index 8df4c65..0d0a972 100644
--- a/codec-stomp/src/main/java/io/netty/handler/codec/stomp/DefaultStompFrame.java
+++ b/codec-stomp/src/main/java/io/netty/handler/codec/stomp/DefaultStompFrame.java
@@ -18,6 +18,7 @@ package io.netty.handler.codec.stomp;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
 import io.netty.util.CharsetUtil;
+import io.netty.util.internal.ObjectUtil;
 
 /**
  * Default implementation of {@link StompFrame}.
@@ -36,12 +37,7 @@ public class DefaultStompFrame extends DefaultStompHeadersSubframe implements St
 
     DefaultStompFrame(StompCommand command, ByteBuf content, DefaultStompHeaders headers) {
         super(command, headers);
-
-        if (content == null) {
-            throw new NullPointerException("content");
-        }
-
-        this.content = content;
+        this.content = ObjectUtil.checkNotNull(content, "content");
     }
 
     @Override
diff --git a/codec-stomp/src/main/java/io/netty/handler/codec/stomp/DefaultStompHeadersSubframe.java b/codec-stomp/src/main/java/io/netty/handler/codec/stomp/DefaultStompHeadersSubframe.java
index 728a889..9ece744 100644
--- a/codec-stomp/src/main/java/io/netty/handler/codec/stomp/DefaultStompHeadersSubframe.java
+++ b/codec-stomp/src/main/java/io/netty/handler/codec/stomp/DefaultStompHeadersSubframe.java
@@ -16,6 +16,7 @@
 package io.netty.handler.codec.stomp;
 
 import io.netty.handler.codec.DecoderResult;
+import io.netty.util.internal.ObjectUtil;
 
 /**
  * Default implementation of {@link StompHeadersSubframe}.
@@ -31,11 +32,7 @@ public class DefaultStompHeadersSubframe implements StompHeadersSubframe {
     }
 
     DefaultStompHeadersSubframe(StompCommand command, DefaultStompHeaders headers) {
-        if (command == null) {
-            throw new NullPointerException("command");
-        }
-
-        this.command = command;
+        this.command = ObjectUtil.checkNotNull(command, "command");
         this.headers = headers == null ? new DefaultStompHeaders() : headers;
     }
 
diff --git a/codec-stomp/src/main/java/io/netty/handler/codec/stomp/StompSubframeDecoder.java b/codec-stomp/src/main/java/io/netty/handler/codec/stomp/StompSubframeDecoder.java
index b4d6a14..5700bb1 100644
--- a/codec-stomp/src/main/java/io/netty/handler/codec/stomp/StompSubframeDecoder.java
+++ b/codec-stomp/src/main/java/io/netty/handler/codec/stomp/StompSubframeDecoder.java
@@ -15,9 +15,6 @@
  */
 package io.netty.handler.codec.stomp;
 
-import java.util.List;
-import java.util.Locale;
-
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
 import io.netty.channel.ChannelHandlerContext;
@@ -26,34 +23,32 @@ import io.netty.handler.codec.DecoderResult;
 import io.netty.handler.codec.ReplayingDecoder;
 import io.netty.handler.codec.TooLongFrameException;
 import io.netty.handler.codec.stomp.StompSubframeDecoder.State;
+import io.netty.util.ByteProcessor;
 import io.netty.util.internal.AppendableCharSequence;
+import io.netty.util.internal.StringUtil;
 
-import static io.netty.buffer.ByteBufUtil.indexOf;
-import static io.netty.buffer.ByteBufUtil.readBytes;
+import java.util.List;
+
+import static io.netty.buffer.ByteBufUtil.*;
+import static io.netty.util.internal.ObjectUtil.*;
 
 /**
- * Decodes {@link ByteBuf}s into {@link StompHeadersSubframe}s and
- * {@link StompContentSubframe}s.
+ * Decodes {@link ByteBuf}s into {@link StompHeadersSubframe}s and {@link StompContentSubframe}s.
  *
  * <h3>Parameters to control memory consumption: </h3>
- * {@code maxLineLength} the maximum length of line -
- * restricts length of command and header lines
- * If the length of the initial line exceeds this value, a
- * {@link TooLongFrameException} will be raised.
+ * {@code maxLineLength} the maximum length of line - restricts length of command and header lines If the length of the
+ * initial line exceeds this value, a {@link TooLongFrameException} will be raised.
  * <br>
- * {@code maxChunkSize}
- * The maximum length of the content or each chunk.  If the content length
- * (or the length of each chunk) exceeds this value, the content or chunk
- * ill be split into multiple {@link StompContentSubframe}s whose length is
- * {@code maxChunkSize} at maximum.
+ * {@code maxChunkSize} The maximum length of the content or each chunk.  If the content length (or the length of each
+ * chunk) exceeds this value, the content or chunk ill be split into multiple {@link StompContentSubframe}s whose length
+ * is {@code maxChunkSize} at maximum.
  *
  * <h3>Chunked Content</h3>
- *
- * If the content of a stomp message is greater than {@code maxChunkSize}
- * the transfer encoding of the HTTP message is 'chunked', this decoder
- * generates multiple {@link StompContentSubframe} instances to avoid excessive memory
- * consumption. Note, that every message, even with no content decodes with
- * {@link LastStompContentSubframe} at the end to simplify upstream message parsing.
+ * <p>
+ * If the content of a stomp message is greater than {@code maxChunkSize} the transfer encoding of the HTTP message is
+ * 'chunked', this decoder generates multiple {@link StompContentSubframe} instances to avoid excessive memory
+ * consumption. Note, that every message, even with no content decodes with {@link LastStompContentSubframe} at the end
+ * to simplify upstream message parsing.
  */
 public class StompSubframeDecoder extends ReplayingDecoder<State> {
 
@@ -69,9 +64,9 @@ public class StompSubframeDecoder extends ReplayingDecoder<State> {
         INVALID_CHUNK
     }
 
-    private final int maxLineLength;
+    private final Utf8LineParser commandParser;
+    private final HeaderParser headerParser;
     private final int maxChunkSize;
-    private final boolean validateHeaders;
     private int alreadyReadChunkSize;
     private LastStompContentSubframe lastContent;
     private long contentLength = -1;
@@ -90,19 +85,11 @@ public class StompSubframeDecoder extends ReplayingDecoder<State> {
 
     public StompSubframeDecoder(int maxLineLength, int maxChunkSize, boolean validateHeaders) {
         super(State.SKIP_CONTROL_CHARACTERS);
-        if (maxLineLength <= 0) {
-            throw new IllegalArgumentException(
-                    "maxLineLength must be a positive integer: " +
-                            maxLineLength);
-        }
-        if (maxChunkSize <= 0) {
-            throw new IllegalArgumentException(
-                    "maxChunkSize must be a positive integer: " +
-                            maxChunkSize);
-        }
+        checkPositive(maxLineLength, "maxLineLength");
+        checkPositive(maxChunkSize, "maxChunkSize");
         this.maxChunkSize = maxChunkSize;
-        this.maxLineLength = maxLineLength;
-        this.validateHeaders = validateHeaders;
+        commandParser = new Utf8LineParser(new AppendableCharSequence(16), maxLineLength);
+        headerParser = new HeaderParser(new AppendableCharSequence(128), maxLineLength, validateHeaders);
     }
 
     @Override
@@ -196,34 +183,24 @@ public class StompSubframeDecoder extends ReplayingDecoder<State> {
     }
 
     private StompCommand readCommand(ByteBuf in) {
-        String commandStr = readLine(in, 16);
-        StompCommand command = null;
+        CharSequence commandSequence = commandParser.parse(in);
+        if (commandSequence == null) {
+            throw new DecoderException("Failed to read command from channel");
+        }
+        String commandStr = commandSequence.toString();
         try {
-            command = StompCommand.valueOf(commandStr);
+            return StompCommand.valueOf(commandStr);
         } catch (IllegalArgumentException iae) {
-            //do nothing
-        }
-        if (command == null) {
-            commandStr = commandStr.toUpperCase(Locale.US);
-            try {
-                command = StompCommand.valueOf(commandStr);
-            } catch (IllegalArgumentException iae) {
-                //do nothing
-            }
-        }
-        if (command == null) {
-            throw new DecoderException("failed to read command from channel");
+            throw new DecoderException("Cannot to parse command " + commandStr);
         }
-        return command;
     }
 
     private State readHeaders(ByteBuf buffer, StompHeaders headers) {
-        AppendableCharSequence buf = new AppendableCharSequence(128);
         for (;;) {
-            boolean headerRead = readHeader(headers, buf, buffer);
+            boolean headerRead = headerParser.parseHeader(headers, buffer);
             if (!headerRead) {
                 if (headers.contains(StompHeaders.CONTENT_LENGTH)) {
-                    contentLength = getContentLength(headers, 0);
+                    contentLength = getContentLength(headers);
                     if (contentLength == 0) {
                         return State.FINALIZE_FRAME_READ;
                     }
@@ -233,8 +210,8 @@ public class StompSubframeDecoder extends ReplayingDecoder<State> {
         }
     }
 
-    private static long getContentLength(StompHeaders headers, long defaultValue) {
-        long contentLength = headers.getLong(StompHeaders.CONTENT_LENGTH, defaultValue);
+    private static long getContentLength(StompHeaders headers) {
+        long contentLength = headers.getLong(StompHeaders.CONTENT_LENGTH, 0L);
         if (contentLength < 0) {
             throw new DecoderException(StompHeaders.CONTENT_LENGTH + " must be non-negative");
         }
@@ -259,75 +236,147 @@ public class StompSubframeDecoder extends ReplayingDecoder<State> {
         }
     }
 
-    private String readLine(ByteBuf buffer, int initialBufferSize) {
-        AppendableCharSequence buf = new AppendableCharSequence(initialBufferSize);
-        int lineLength = 0;
-        for (;;) {
-            byte nextByte = buffer.readByte();
+    private void resetDecoder() {
+        checkpoint(State.SKIP_CONTROL_CHARACTERS);
+        contentLength = -1;
+        alreadyReadChunkSize = 0;
+        lastContent = null;
+    }
+
+    private static class Utf8LineParser implements ByteProcessor {
+
+        private final AppendableCharSequence charSeq;
+        private final int maxLineLength;
+
+        private int lineLength;
+        private char interim;
+        private boolean nextRead;
+
+        Utf8LineParser(AppendableCharSequence charSeq, int maxLineLength) {
+            this.charSeq = checkNotNull(charSeq, "charSeq");
+            this.maxLineLength = maxLineLength;
+        }
+
+        AppendableCharSequence parse(ByteBuf byteBuf) {
+            reset();
+            int offset = byteBuf.forEachByte(this);
+            if (offset == -1) {
+                return null;
+            }
+
+            byteBuf.readerIndex(offset + 1);
+            return charSeq;
+        }
+
+        AppendableCharSequence charSequence() {
+            return charSeq;
+        }
+
+        @Override
+        public boolean process(byte nextByte) throws Exception {
             if (nextByte == StompConstants.CR) {
-                //do nothing
-            } else if (nextByte == StompConstants.LF) {
-                return buf.toString();
+                ++lineLength;
+                return true;
+            }
+
+            if (nextByte == StompConstants.LF) {
+                return false;
+            }
+
+            if (++lineLength > maxLineLength) {
+                throw new TooLongFrameException("An STOMP line is larger than " + maxLineLength + " bytes.");
+            }
+
+            // 1 byte   -   0xxxxxxx                    -  7 bits
+            // 2 byte   -   110xxxxx 10xxxxxx           -  11 bits
+            // 3 byte   -   1110xxxx 10xxxxxx 10xxxxxx  -  16 bits
+            if (nextRead) {
+                interim |= (nextByte & 0x3F) << 6;
+                nextRead = false;
+            } else if (interim != 0) { // flush 2 or 3 byte
+                charSeq.append((char) (interim | (nextByte & 0x3F)));
+                interim = 0;
+            } else if (nextByte >= 0) { // INITIAL BRANCH
+                // The first 128 characters (US-ASCII) need one byte.
+                charSeq.append((char) nextByte);
+            } else if ((nextByte & 0xE0) == 0xC0) {
+                // The next 1920 characters need two bytes and we can define
+                // a first byte by mask 110xxxxx.
+                interim = (char) ((nextByte & 0x1F) << 6);
             } else {
-                if (lineLength >= maxLineLength) {
-                    invalidLineLength();
-                }
-                lineLength ++;
-                buf.append((char) nextByte);
+                // The rest of characters need three bytes.
+                interim = (char) ((nextByte & 0x0F) << 12);
+                nextRead = true;
             }
+
+            return true;
+        }
+
+        protected void reset() {
+            charSeq.reset();
+            lineLength = 0;
+            interim = 0;
+            nextRead = false;
         }
     }
 
-    private boolean readHeader(StompHeaders headers, AppendableCharSequence buf, ByteBuf buffer) {
-        buf.reset();
-        int lineLength = 0;
-        String key = null;
-        boolean valid = false;
-        for (;;) {
-            byte nextByte = buffer.readByte();
-
-            if (nextByte == StompConstants.COLON && key == null) {
-                key = buf.toString();
-                valid = true;
-                buf.reset();
-            } else if (nextByte == StompConstants.CR) {
-                //do nothing
-            } else if (nextByte == StompConstants.LF) {
-                if (key == null && lineLength == 0) {
-                    return false;
-                } else if (valid) {
-                    headers.add(key, buf.toString());
-                } else if (validateHeaders) {
-                    invalidHeader(key, buf.toString());
-                }
-                return true;
-            } else {
-                if (lineLength >= maxLineLength) {
-                    invalidLineLength();
-                }
-                if (nextByte == StompConstants.COLON && key != null) {
-                    valid = false;
+    private static final class HeaderParser extends Utf8LineParser {
+
+        private final boolean validateHeaders;
+
+        private String name;
+        private boolean valid;
+
+        HeaderParser(AppendableCharSequence charSeq, int maxLineLength, boolean validateHeaders) {
+            super(charSeq, maxLineLength);
+            this.validateHeaders = validateHeaders;
+        }
+
+        boolean parseHeader(StompHeaders headers, ByteBuf buf) {
+            AppendableCharSequence value = super.parse(buf);
+            if (value == null || (name == null && value.length() == 0)) {
+                return false;
+            }
+
+            if (valid) {
+                headers.add(name, value.toString());
+            } else if (validateHeaders) {
+                if (StringUtil.isNullOrEmpty(name)) {
+                    throw new IllegalArgumentException("received an invalid header line '" + value.toString() + '\'');
                 }
-                lineLength ++;
-                buf.append((char) nextByte);
+                String line = name + ':' + value.toString();
+                throw new IllegalArgumentException("a header value or name contains a prohibited character ':'"
+                                                   + ", " + line);
             }
+            return true;
         }
-    }
 
-    private void invalidHeader(String key, String value) {
-        String line = key != null ? key + ":" + value : value;
-        throw new IllegalArgumentException("a header value or name contains a prohibited character ':'"
-            + ", " + line);
-    }
+        @Override
+        public boolean process(byte nextByte) throws Exception {
+            if (nextByte == StompConstants.COLON) {
+                if (name == null) {
+                    AppendableCharSequence charSeq = charSequence();
+                    if (charSeq.length() != 0) {
+                        name = charSeq.substring(0, charSeq.length());
+                        charSeq.reset();
+                        valid = true;
+                        return true;
+                    } else {
+                        name = StringUtil.EMPTY_STRING;
+                    }
+                } else {
+                    valid = false;
+                }
+            }
 
-    private void invalidLineLength() {
-        throw new TooLongFrameException("An STOMP line is larger than " + maxLineLength + " bytes.");
-    }
+            return super.process(nextByte);
+        }
 
-    private void resetDecoder() {
-        checkpoint(State.SKIP_CONTROL_CHARACTERS);
-        contentLength = -1;
-        alreadyReadChunkSize = 0;
-        lastContent = null;
+        @Override
+        protected void reset() {
+            name = null;
+            valid = false;
+            super.reset();
+        }
     }
 }
diff --git a/codec-stomp/src/main/java/io/netty/handler/codec/stomp/StompSubframeEncoder.java b/codec-stomp/src/main/java/io/netty/handler/codec/stomp/StompSubframeEncoder.java
index 7999279..fd99f7b 100644
--- a/codec-stomp/src/main/java/io/netty/handler/codec/stomp/StompSubframeEncoder.java
+++ b/codec-stomp/src/main/java/io/netty/handler/codec/stomp/StompSubframeEncoder.java
@@ -15,17 +15,15 @@
  */
 package io.netty.handler.codec.stomp;
 
-import java.util.List;
-import java.util.Map.Entry;
-
 import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
 import io.netty.channel.ChannelHandlerContext;
-import io.netty.handler.codec.AsciiHeadersEncoder;
-import io.netty.handler.codec.AsciiHeadersEncoder.NewlineType;
-import io.netty.handler.codec.AsciiHeadersEncoder.SeparatorType;
 import io.netty.handler.codec.MessageToMessageEncoder;
 import io.netty.util.CharsetUtil;
 
+import java.util.List;
+import java.util.Map.Entry;
+
 /**
  * Encodes a {@link StompFrame} or a {@link StompSubframe} into a {@link ByteBuf}.
  */
@@ -64,11 +62,13 @@ public class StompSubframeEncoder extends MessageToMessageEncoder<StompSubframe>
     private static ByteBuf encodeFrame(StompHeadersSubframe frame, ChannelHandlerContext ctx) {
         ByteBuf buf = ctx.alloc().buffer();
 
-        buf.writeCharSequence(frame.command().toString(), CharsetUtil.US_ASCII);
+        buf.writeCharSequence(frame.command().toString(), CharsetUtil.UTF_8);
         buf.writeByte(StompConstants.LF);
-        AsciiHeadersEncoder headersEncoder = new AsciiHeadersEncoder(buf, SeparatorType.COLON, NewlineType.LF);
         for (Entry<CharSequence, CharSequence> entry : frame.headers()) {
-            headersEncoder.encode(entry);
+            ByteBufUtil.writeUtf8(buf, entry.getKey());
+            buf.writeByte(StompConstants.COLON);
+            ByteBufUtil.writeUtf8(buf, entry.getValue());
+            buf.writeByte(StompConstants.LF);
         }
         buf.writeByte(StompConstants.LF);
         return buf;
diff --git a/codec-stomp/src/test/java/io/netty/handler/codec/stomp/StompSubframeDecoderTest.java b/codec-stomp/src/test/java/io/netty/handler/codec/stomp/StompSubframeDecoderTest.java
index fe77212..69e2b00 100644
--- a/codec-stomp/src/test/java/io/netty/handler/codec/stomp/StompSubframeDecoderTest.java
+++ b/codec-stomp/src/test/java/io/netty/handler/codec/stomp/StompSubframeDecoderTest.java
@@ -22,15 +22,9 @@ import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
-import static io.netty.handler.codec.stomp.StompTestConstants.FRAME_WITH_INVALID_HEADER;
-import static io.netty.util.CharsetUtil.US_ASCII;
-import static io.netty.util.CharsetUtil.UTF_8;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
+import static io.netty.handler.codec.stomp.StompTestConstants.*;
+import static io.netty.util.CharsetUtil.*;
+import static org.junit.Assert.*;
 
 public class StompSubframeDecoderTest {
 
@@ -165,7 +159,7 @@ public class StompSubframeDecoderTest {
 
     @Test
     public void testValidateHeadersDecodingDisabled() {
-        ByteBuf invalidIncoming = Unpooled.copiedBuffer(FRAME_WITH_INVALID_HEADER.getBytes(US_ASCII));
+        ByteBuf invalidIncoming = Unpooled.copiedBuffer(FRAME_WITH_INVALID_HEADER.getBytes(UTF_8));
         assertTrue(channel.writeInbound(invalidIncoming));
 
         StompHeadersSubframe frame = channel.readInbound();
@@ -185,7 +179,7 @@ public class StompSubframeDecoderTest {
     public void testValidateHeadersDecodingEnabled() {
         channel = new EmbeddedChannel(new StompSubframeDecoder(true));
 
-        ByteBuf invalidIncoming = Unpooled.copiedBuffer(FRAME_WITH_INVALID_HEADER.getBytes(US_ASCII));
+        ByteBuf invalidIncoming = Unpooled.wrappedBuffer(FRAME_WITH_INVALID_HEADER.getBytes(UTF_8));
         assertTrue(channel.writeInbound(invalidIncoming));
 
         StompHeadersSubframe frame = channel.readInbound();
@@ -194,4 +188,37 @@ public class StompSubframeDecoderTest {
         assertEquals("a header value or name contains a prohibited character ':', current-time:2000-01-01T00:00:00",
                 frame.decoderResult().cause().getMessage());
     }
+
+    @Test
+    public void testNotValidFrameWithEmptyHeaderName() {
+        channel = new EmbeddedChannel(new StompSubframeDecoder(true));
+
+        ByteBuf invalidIncoming = Unpooled.wrappedBuffer(FRAME_WITH_EMPTY_HEADER_NAME.getBytes(UTF_8));
+        assertTrue(channel.writeInbound(invalidIncoming));
+
+        StompHeadersSubframe frame = channel.readInbound();
+        assertNotNull(frame);
+        assertTrue(frame.decoderResult().isFailure());
+        assertEquals("received an invalid header line ':header-value'",
+                     frame.decoderResult().cause().getMessage());
+    }
+
+    @Test
+    public void testUtf8FrameDecoding() {
+        channel = new EmbeddedChannel(new StompSubframeDecoder(true));
+
+        ByteBuf incoming = Unpooled.wrappedBuffer(SEND_FRAME_UTF8.getBytes(UTF_8));
+        assertTrue(channel.writeInbound(incoming));
+
+        StompHeadersSubframe headersSubFrame = channel.readInbound();
+        assertNotNull(headersSubFrame);
+        assertFalse(headersSubFrame.decoderResult().isFailure());
+        assertEquals("/queue/№11±♛нетти♕", headersSubFrame.headers().getAsString("destination"));
+        assertTrue(headersSubFrame.headers().contains("content-type"));
+
+        StompContentSubframe contentSubFrame = channel.readInbound();
+        assertNotNull(contentSubFrame);
+        assertEquals("body", contentSubFrame.content().toString(UTF_8));
+        assertTrue(contentSubFrame.release());
+    }
 }
diff --git a/codec-stomp/src/test/java/io/netty/handler/codec/stomp/StompSubframeEncoderTest.java b/codec-stomp/src/test/java/io/netty/handler/codec/stomp/StompSubframeEncoderTest.java
index 939f8b4..efbac17 100644
--- a/codec-stomp/src/test/java/io/netty/handler/codec/stomp/StompSubframeEncoderTest.java
+++ b/codec-stomp/src/test/java/io/netty/handler/codec/stomp/StompSubframeEncoderTest.java
@@ -18,11 +18,13 @@ package io.netty.handler.codec.stomp;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
 import io.netty.channel.embedded.EmbeddedChannel;
+import io.netty.util.AsciiString;
 import io.netty.util.CharsetUtil;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
+import static io.netty.handler.codec.stomp.StompTestConstants.*;
 import static org.junit.Assert.*;
 
 public class StompSubframeEncoderTest {
@@ -63,4 +65,22 @@ public class StompSubframeEncoderTest {
         assertEquals(StompTestConstants.CONNECT_FRAME, content);
         aggregatedBuffer.release();
     }
+
+    @Test
+    public void testUtf8FrameEncoding() {
+        StompFrame frame = new DefaultStompFrame(StompCommand.SEND,
+                                                 Unpooled.wrappedBuffer("body".getBytes(CharsetUtil.UTF_8)));
+        StompHeaders incoming = frame.headers();
+        incoming.set(StompHeaders.DESTINATION, "/queue/№11±♛нетти♕");
+        incoming.set(StompHeaders.CONTENT_TYPE, AsciiString.of("text/plain"));
+
+        channel.writeOutbound(frame);
+
+        ByteBuf headers = channel.readOutbound();
+        ByteBuf content = channel.readOutbound();
+        ByteBuf fullFrame = Unpooled.wrappedBuffer(headers, content);
+        assertEquals(SEND_FRAME_UTF8, fullFrame.toString(CharsetUtil.UTF_8));
+        assertTrue(fullFrame.release());
+    }
+
 }
diff --git a/codec-stomp/src/test/java/io/netty/handler/codec/stomp/StompTestConstants.java b/codec-stomp/src/test/java/io/netty/handler/codec/stomp/StompTestConstants.java
index 0d89a5c..314a933 100644
--- a/codec-stomp/src/test/java/io/netty/handler/codec/stomp/StompTestConstants.java
+++ b/codec-stomp/src/test/java/io/netty/handler/codec/stomp/StompTestConstants.java
@@ -64,5 +64,18 @@ public final class StompTestConstants {
             '\n' +
             "some body\0";
 
+    public static final String FRAME_WITH_EMPTY_HEADER_NAME = "SEND\n" +
+            "destination:/some-destination\n" +
+            "content-type:text/plain\n" +
+            ":header-value\n" +
+            '\n' +
+            "some body\0";
+
+    public static final String SEND_FRAME_UTF8 = "SEND\n" +
+            "destination:/queue/№11±♛нетти♕\n" +
+            "content-type:text/plain\n" +
+            '\n' +
+            "body\0";
+
     private StompTestConstants() { }
 }
diff --git a/codec-xml/pom.xml b/codec-xml/pom.xml
index 54ef3c5..664314d 100644
--- a/codec-xml/pom.xml
+++ b/codec-xml/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>io.netty</groupId>
     <artifactId>netty-parent</artifactId>
-    <version>4.1.33.Final</version>
+    <version>4.1.48.Final</version>
   </parent>
 
   <artifactId>netty-codec-xml</artifactId>
diff --git a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlAttribute.java b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlAttribute.java
index d40ce13..968d306 100644
--- a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlAttribute.java
+++ b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlAttribute.java
@@ -57,16 +57,30 @@ public class XmlAttribute {
 
     @Override
     public boolean equals(Object o) {
-        if (this == o) { return true; }
-        if (o == null || getClass() != o.getClass()) { return false; }
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
 
         XmlAttribute that = (XmlAttribute) o;
 
-        if (!name.equals(that.name)) { return false; }
-        if (namespace != null ? !namespace.equals(that.namespace) : that.namespace != null) { return false; }
-        if (prefix != null ? !prefix.equals(that.prefix) : that.prefix != null) { return false; }
-        if (type != null ? !type.equals(that.type) : that.type != null) { return false; }
-        if (value != null ? !value.equals(that.value) : that.value != null) { return false; }
+        if (!name.equals(that.name)) {
+            return false;
+        }
+        if (namespace != null ? !namespace.equals(that.namespace) : that.namespace != null) {
+            return false;
+        }
+        if (prefix != null ? !prefix.equals(that.prefix) : that.prefix != null) {
+            return false;
+        }
+        if (type != null ? !type.equals(that.type) : that.type != null) {
+            return false;
+        }
+        if (value != null ? !value.equals(that.value) : that.value != null) {
+            return false;
+        }
 
         return true;
     }
diff --git a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlContent.java b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlContent.java
index a47df33..275297c 100644
--- a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlContent.java
+++ b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlContent.java
@@ -32,12 +32,18 @@ public abstract class XmlContent {
 
     @Override
     public boolean equals(Object o) {
-        if (this == o) { return true; }
-        if (o == null || getClass() != o.getClass()) { return false; }
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
 
         XmlContent that = (XmlContent) o;
 
-        if (data != null ? !data.equals(that.data) : that.data != null) { return false; }
+        if (data != null ? !data.equals(that.data) : that.data != null) {
+            return false;
+        }
 
         return true;
     }
diff --git a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlDTD.java b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlDTD.java
index 754539b..e36648f 100644
--- a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlDTD.java
+++ b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlDTD.java
@@ -32,12 +32,18 @@ public class XmlDTD {
 
     @Override
     public boolean equals(Object o) {
-        if (this == o) { return true; }
-        if (o == null || getClass() != o.getClass()) { return false; }
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
 
         XmlDTD xmlDTD = (XmlDTD) o;
 
-        if (text != null ? !text.equals(xmlDTD.text) : xmlDTD.text != null) { return false; }
+        if (text != null ? !text.equals(xmlDTD.text) : xmlDTD.text != null) {
+            return false;
+        }
 
         return true;
     }
diff --git a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlDecoder.java b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlDecoder.java
index 7408848..e84e595 100644
--- a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlDecoder.java
+++ b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlDecoder.java
@@ -39,7 +39,7 @@ public class XmlDecoder extends ByteToMessageDecoder {
     private static final XmlDocumentEnd XML_DOCUMENT_END = XmlDocumentEnd.INSTANCE;
 
     private final AsyncXMLStreamReader<AsyncByteArrayFeeder> streamReader = XML_INPUT_FACTORY.createAsyncForByteArray();
-    private final AsyncByteArrayFeeder streamFeeder = (AsyncByteArrayFeeder) streamReader.getInputFeeder();
+    private final AsyncByteArrayFeeder streamFeeder = streamReader.getInputFeeder();
 
     @Override
     protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
diff --git a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlDocumentStart.java b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlDocumentStart.java
index 311a1f5..98ce875 100644
--- a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlDocumentStart.java
+++ b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlDocumentStart.java
@@ -54,17 +54,27 @@ public class XmlDocumentStart {
 
     @Override
     public boolean equals(Object o) {
-        if (this == o) { return true; }
-        if (o == null || getClass() != o.getClass()) { return false; }
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
 
         XmlDocumentStart that = (XmlDocumentStart) o;
 
-        if (standalone != that.standalone) { return false; }
-        if (encoding != null ? !encoding.equals(that.encoding) : that.encoding != null) { return false; }
+        if (standalone != that.standalone) {
+            return false;
+        }
+        if (encoding != null ? !encoding.equals(that.encoding) : that.encoding != null) {
+            return false;
+        }
         if (encodingScheme != null ? !encodingScheme.equals(that.encodingScheme) : that.encodingScheme != null) {
             return false;
         }
-        if (version != null ? !version.equals(that.version) : that.version != null) { return false; }
+        if (version != null ? !version.equals(that.version) : that.version != null) {
+            return false;
+        }
 
         return true;
     }
diff --git a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlElement.java b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlElement.java
index 885e814..8391bd0 100644
--- a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlElement.java
+++ b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlElement.java
@@ -54,15 +54,27 @@ public abstract class XmlElement {
 
     @Override
     public boolean equals(Object o) {
-        if (this == o) { return true; }
-        if (o == null || getClass() != o.getClass()) { return false; }
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
 
         XmlElement that = (XmlElement) o;
 
-        if (!name.equals(that.name)) { return false; }
-        if (namespace != null ? !namespace.equals(that.namespace) : that.namespace != null) { return false; }
-        if (namespaces != null ? !namespaces.equals(that.namespaces) : that.namespaces != null) { return false; }
-        if (prefix != null ? !prefix.equals(that.prefix) : that.prefix != null) { return false; }
+        if (!name.equals(that.name)) {
+            return false;
+        }
+        if (namespace != null ? !namespace.equals(that.namespace) : that.namespace != null) {
+            return false;
+        }
+        if (namespaces != null ? !namespaces.equals(that.namespaces) : that.namespaces != null) {
+            return false;
+        }
+        if (prefix != null ? !prefix.equals(that.prefix) : that.prefix != null) {
+            return false;
+        }
 
         return true;
     }
diff --git a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlElementStart.java b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlElementStart.java
index 17d603e..1902423 100644
--- a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlElementStart.java
+++ b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlElementStart.java
@@ -35,13 +35,21 @@ public class XmlElementStart extends XmlElement {
 
     @Override
     public boolean equals(Object o) {
-        if (this == o) { return true; }
-        if (o == null || getClass() != o.getClass()) { return false; }
-        if (!super.equals(o)) { return false; }
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        if (!super.equals(o)) {
+            return false;
+        }
 
         XmlElementStart that = (XmlElementStart) o;
 
-        if (attributes != null ? !attributes.equals(that.attributes) : that.attributes != null) { return false; }
+        if (attributes != null ? !attributes.equals(that.attributes) : that.attributes != null) {
+            return false;
+        }
 
         return true;
     }
diff --git a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlEntityReference.java b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlEntityReference.java
index ba3cba9..78ed9e7 100644
--- a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlEntityReference.java
+++ b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlEntityReference.java
@@ -38,13 +38,21 @@ public class XmlEntityReference {
 
     @Override
     public boolean equals(Object o) {
-        if (this == o) { return true; }
-        if (o == null || getClass() != o.getClass()) { return false; }
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
 
         XmlEntityReference that = (XmlEntityReference) o;
 
-        if (name != null ? !name.equals(that.name) : that.name != null) { return false; }
-        if (text != null ? !text.equals(that.text) : that.text != null) { return false; }
+        if (name != null ? !name.equals(that.name) : that.name != null) {
+            return false;
+        }
+        if (text != null ? !text.equals(that.text) : that.text != null) {
+            return false;
+        }
 
         return true;
     }
diff --git a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlNamespace.java b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlNamespace.java
index 9cbb86f..2d0ae56 100644
--- a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlNamespace.java
+++ b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlNamespace.java
@@ -38,13 +38,21 @@ public class XmlNamespace {
 
     @Override
     public boolean equals(Object o) {
-        if (this == o) { return true; }
-        if (o == null || getClass() != o.getClass()) { return false; }
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
 
         XmlNamespace that = (XmlNamespace) o;
 
-        if (prefix != null ? !prefix.equals(that.prefix) : that.prefix != null) { return false; }
-        if (uri != null ? !uri.equals(that.uri) : that.uri != null) { return false; }
+        if (prefix != null ? !prefix.equals(that.prefix) : that.prefix != null) {
+            return false;
+        }
+        if (uri != null ? !uri.equals(that.uri) : that.uri != null) {
+            return false;
+        }
 
         return true;
     }
diff --git a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlProcessingInstruction.java b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlProcessingInstruction.java
index 27dc4de..6f75880 100644
--- a/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlProcessingInstruction.java
+++ b/codec-xml/src/main/java/io/netty/handler/codec/xml/XmlProcessingInstruction.java
@@ -38,13 +38,21 @@ public class XmlProcessingInstruction {
 
     @Override
     public boolean equals(Object o) {
-        if (this == o) { return true; }
-        if (o == null || getClass() != o.getClass()) { return false; }
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
 
         XmlProcessingInstruction that = (XmlProcessingInstruction) o;
 
-        if (data != null ? !data.equals(that.data) : that.data != null) { return false; }
-        if (target != null ? !target.equals(that.target) : that.target != null) { return false; }
+        if (data != null ? !data.equals(that.data) : that.data != null) {
+            return false;
+        }
+        if (target != null ? !target.equals(that.target) : that.target != null) {
+            return false;
+        }
 
         return true;
     }
diff --git a/codec-xml/src/test/java/io/netty/handler/codec/xml/XmlDecoderTest.java b/codec-xml/src/test/java/io/netty/handler/codec/xml/XmlDecoderTest.java
index 1d623d6..500de1d 100644
--- a/codec-xml/src/test/java/io/netty/handler/codec/xml/XmlDecoderTest.java
+++ b/codec-xml/src/test/java/io/netty/handler/codec/xml/XmlDecoderTest.java
@@ -37,12 +37,12 @@ public class XmlDecoderTest {
             "<!DOCTYPE employee SYSTEM \"employee.dtd\">" +
             "<?xml-stylesheet type=\"text/css\" href=\"netty.css\"?>" +
             "<?xml-test ?>" +
-            "<employee xmlns:nettya=\"http://netty.io/netty/a\">" +
+            "<employee xmlns:nettya=\"https://netty.io/netty/a\">" +
             "<nettya:id>&plusmn;1</nettya:id>\n" +
             "<name ";
 
     private static final String XML2 = "type=\"given\">Alba</name><![CDATA[ <some data &gt;/> ]]>" +
-            "   <!-- namespaced --><nettyb:salary xmlns:nettyb=\"http://netty.io/netty/b\" nettyb:period=\"weekly\">" +
+            "   <!-- namespaced --><nettyb:salary xmlns:nettyb=\"https://netty.io/netty/b\" nettyb:period=\"weekly\">" +
             "100</nettyb:salary><last/></employee>";
 
     private static final String XML3 = "<?xml version=\"1.1\" encoding=\"UTf-8\" standalone=\"yes\"?><netty></netty>";
@@ -99,13 +99,13 @@ public class XmlDecoderTest {
         assertThat(((XmlElementStart) temp).attributes().size(), is(0));
         assertThat(((XmlElementStart) temp).namespaces().size(), is(1));
         assertThat(((XmlElementStart) temp).namespaces().get(0).prefix(), is("nettya"));
-        assertThat(((XmlElementStart) temp).namespaces().get(0).uri(), is("http://netty.io/netty/a"));
+        assertThat(((XmlElementStart) temp).namespaces().get(0).uri(), is("https://netty.io/netty/a"));
 
         temp = channel.readInbound();
         assertThat(temp, instanceOf(XmlElementStart.class));
         assertThat(((XmlElementStart) temp).name(), is("id"));
         assertThat(((XmlElementStart) temp).prefix(), is("nettya"));
-        assertThat(((XmlElementStart) temp).namespace(), is("http://netty.io/netty/a"));
+        assertThat(((XmlElementStart) temp).namespace(), is("https://netty.io/netty/a"));
         assertThat(((XmlElementStart) temp).attributes().size(), is(0));
         assertThat(((XmlElementStart) temp).namespaces().size(), is(0));
 
@@ -122,7 +122,7 @@ public class XmlDecoderTest {
         assertThat(temp, instanceOf(XmlElementEnd.class));
         assertThat(((XmlElementEnd) temp).name(), is("id"));
         assertThat(((XmlElementEnd) temp).prefix(), is("nettya"));
-        assertThat(((XmlElementEnd) temp).namespace(), is("http://netty.io/netty/a"));
+        assertThat(((XmlElementEnd) temp).namespace(), is("https://netty.io/netty/a"));
 
         temp = channel.readInbound();
         assertThat(temp, instanceOf(XmlCharacters.class));
@@ -171,15 +171,15 @@ public class XmlDecoderTest {
         assertThat(temp, instanceOf(XmlElementStart.class));
         assertThat(((XmlElementStart) temp).name(), is("salary"));
         assertThat(((XmlElementStart) temp).prefix(), is("nettyb"));
-        assertThat(((XmlElementStart) temp).namespace(), is("http://netty.io/netty/b"));
+        assertThat(((XmlElementStart) temp).namespace(), is("https://netty.io/netty/b"));
         assertThat(((XmlElementStart) temp).attributes().size(), is(1));
         assertThat(((XmlElementStart) temp).attributes().get(0).name(), is("period"));
         assertThat(((XmlElementStart) temp).attributes().get(0).value(), is("weekly"));
         assertThat(((XmlElementStart) temp).attributes().get(0).prefix(), is("nettyb"));
-        assertThat(((XmlElementStart) temp).attributes().get(0).namespace(), is("http://netty.io/netty/b"));
+        assertThat(((XmlElementStart) temp).attributes().get(0).namespace(), is("https://netty.io/netty/b"));
         assertThat(((XmlElementStart) temp).namespaces().size(), is(1));
         assertThat(((XmlElementStart) temp).namespaces().get(0).prefix(), is("nettyb"));
-        assertThat(((XmlElementStart) temp).namespaces().get(0).uri(), is("http://netty.io/netty/b"));
+        assertThat(((XmlElementStart) temp).namespaces().get(0).uri(), is("https://netty.io/netty/b"));
 
         temp = channel.readInbound();
         assertThat(temp, instanceOf(XmlCharacters.class));
@@ -189,10 +189,10 @@ public class XmlDecoderTest {
         assertThat(temp, instanceOf(XmlElementEnd.class));
         assertThat(((XmlElementEnd) temp).name(), is("salary"));
         assertThat(((XmlElementEnd) temp).prefix(), is("nettyb"));
-        assertThat(((XmlElementEnd) temp).namespace(), is("http://netty.io/netty/b"));
+        assertThat(((XmlElementEnd) temp).namespace(), is("https://netty.io/netty/b"));
         assertThat(((XmlElementEnd) temp).namespaces().size(), is(1));
         assertThat(((XmlElementEnd) temp).namespaces().get(0).prefix(), is("nettyb"));
-        assertThat(((XmlElementEnd) temp).namespaces().get(0).uri(), is("http://netty.io/netty/b"));
+        assertThat(((XmlElementEnd) temp).namespaces().get(0).uri(), is("https://netty.io/netty/b"));
 
         temp = channel.readInbound();
         assertThat(temp, instanceOf(XmlElementStart.class));
@@ -216,7 +216,7 @@ public class XmlDecoderTest {
         assertThat(((XmlElementEnd) temp).namespace(), is(""));
         assertThat(((XmlElementEnd) temp).namespaces().size(), is(1));
         assertThat(((XmlElementEnd) temp).namespaces().get(0).prefix(), is("nettya"));
-        assertThat(((XmlElementEnd) temp).namespaces().get(0).uri(), is("http://netty.io/netty/a"));
+        assertThat(((XmlElementEnd) temp).namespaces().get(0).uri(), is("https://netty.io/netty/a"));
 
         temp = channel.readInbound();
         assertThat(temp, nullValue());
diff --git a/codec/pom.xml b/codec/pom.xml
index 01205ef..33f1475 100644
--- a/codec/pom.xml
+++ b/codec/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>io.netty</groupId>
     <artifactId>netty-parent</artifactId>
-    <version>4.1.33.Final</version>
+    <version>4.1.48.Final</version>
   </parent>
 
   <artifactId>netty-codec</artifactId>
diff --git a/codec/src/main/java/io/netty/handler/codec/AsciiHeadersEncoder.java b/codec/src/main/java/io/netty/handler/codec/AsciiHeadersEncoder.java
index b8162e8..446ef38 100644
--- a/codec/src/main/java/io/netty/handler/codec/AsciiHeadersEncoder.java
+++ b/codec/src/main/java/io/netty/handler/codec/AsciiHeadersEncoder.java
@@ -23,6 +23,7 @@ import io.netty.buffer.ByteBuf;
 import io.netty.buffer.ByteBufUtil;
 import io.netty.util.AsciiString;
 import io.netty.util.CharsetUtil;
+import io.netty.util.internal.ObjectUtil;
 
 public final class AsciiHeadersEncoder {
 
@@ -63,19 +64,9 @@ public final class AsciiHeadersEncoder {
     }
 
     public AsciiHeadersEncoder(ByteBuf buf, SeparatorType separatorType, NewlineType newlineType) {
-        if (buf == null) {
-            throw new NullPointerException("buf");
-        }
-        if (separatorType == null) {
-            throw new NullPointerException("separatorType");
-        }
-        if (newlineType == null) {
-            throw new NullPointerException("newlineType");
-        }
-
-        this.buf = buf;
-        this.separatorType = separatorType;
-        this.newlineType = newlineType;
+        this.buf = ObjectUtil.checkNotNull(buf, "buf");
+        this.separatorType = ObjectUtil.checkNotNull(separatorType, "separatorType");
+        this.newlineType = ObjectUtil.checkNotNull(newlineType, "newlineType");
     }
 
     public void encode(Entry<CharSequence, CharSequence> entry) {
diff --git a/codec/src/main/java/io/netty/handler/codec/ByteToMessageDecoder.java b/codec/src/main/java/io/netty/handler/codec/ByteToMessageDecoder.java
index ddf3d9b..46bd380 100644
--- a/codec/src/main/java/io/netty/handler/codec/ByteToMessageDecoder.java
+++ b/codec/src/main/java/io/netty/handler/codec/ByteToMessageDecoder.java
@@ -19,13 +19,18 @@ import io.netty.buffer.ByteBuf;
 import io.netty.buffer.ByteBufAllocator;
 import io.netty.buffer.CompositeByteBuf;
 import io.netty.buffer.Unpooled;
+import io.netty.channel.ChannelConfig;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelInboundHandlerAdapter;
 import io.netty.channel.socket.ChannelInputShutdownEvent;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.StringUtil;
 
 import java.util.List;
 
+import static io.netty.util.internal.ObjectUtil.checkPositive;
+import static java.lang.Integer.MAX_VALUE;
+
 /**
  * {@link ChannelInboundHandlerAdapter} which decodes bytes in a stream-like fashion from one {@link ByteBuf} to an
  * other Message type.
@@ -75,23 +80,25 @@ public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter
     public static final Cumulator MERGE_CUMULATOR = new Cumulator() {
         @Override
         public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
+            if (!cumulation.isReadable() && in.isContiguous()) {
+                // If cumulation is empty and input buffer is contiguous, use it directly
+                cumulation.release();
+                return in;
+            }
             try {
-                final ByteBuf buffer;
-                if (cumulation.writerIndex() > cumulation.maxCapacity() - in.readableBytes()
-                    || cumulation.refCnt() > 1 || cumulation.isReadOnly()) {
-                    // Expand cumulation (by replace it) when either there is not more room in the buffer
-                    // or if the refCnt is greater then 1 which may happen when the user use slice().retain() or
-                    // duplicate().retain() or if its read-only.
-                    //
-                    // See:
-                    // - https://github.com/netty/netty/issues/2327
-                    // - https://github.com/netty/netty/issues/1764
-                    buffer = expandCumulation(alloc, cumulation, in.readableBytes());
-                } else {
-                    buffer = cumulation;
+                final int required = in.readableBytes();
+                if (required > cumulation.maxWritableBytes() ||
+                        (required > cumulation.maxFastWritableBytes() && cumulation.refCnt() > 1) ||
+                        cumulation.isReadOnly()) {
+                    // Expand cumulation (by replacing it) under the following conditions:
+                    // - cumulation cannot be resized to accommodate the additional data
+                    // - cumulation can be expanded with a reallocation operation to accommodate but the buffer is
+                    //   assumed to be shared (e.g. refCnt() > 1) and the reallocation may not be safe.
+                    return expandCumulation(alloc, cumulation, in);
                 }
-                buffer.writeBytes(in);
-                return buffer;
+                cumulation.writeBytes(in, in.readerIndex(), required);
+                in.readerIndex(in.writerIndex());
+                return cumulation;
             } finally {
                 // We must release in in all cases as otherwise it may produce a leak if writeBytes(...) throw
                 // for whatever release (for example because of OutOfMemoryError)
@@ -108,35 +115,33 @@ public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter
     public static final Cumulator COMPOSITE_CUMULATOR = new Cumulator() {
         @Override
         public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
-            ByteBuf buffer;
+            if (!cumulation.isReadable()) {
+                cumulation.release();
+                return in;
+            }
+            CompositeByteBuf composite = null;
             try {
-                if (cumulation.refCnt() > 1) {
-                    // Expand cumulation (by replace it) when the refCnt is greater then 1 which may happen when the
-                    // user use slice().retain() or duplicate().retain().
-                    //
-                    // See:
-                    // - https://github.com/netty/netty/issues/2327
-                    // - https://github.com/netty/netty/issues/1764
-                    buffer = expandCumulation(alloc, cumulation, in.readableBytes());
-                    buffer.writeBytes(in);
-                } else {
-                    CompositeByteBuf composite;
-                    if (cumulation instanceof CompositeByteBuf) {
-                        composite = (CompositeByteBuf) cumulation;
-                    } else {
-                        composite = alloc.compositeBuffer(Integer.MAX_VALUE);
-                        composite.addComponent(true, cumulation);
+                if (cumulation instanceof CompositeByteBuf && cumulation.refCnt() == 1) {
+                    composite = (CompositeByteBuf) cumulation;
+                    // Writer index must equal capacity if we are going to "write"
+                    // new components to the end
+                    if (composite.writerIndex() != composite.capacity()) {
+                        composite.capacity(composite.writerIndex());
                     }
-                    composite.addComponent(true, in);
-                    in = null;
-                    buffer = composite;
+                } else {
+                    composite = alloc.compositeBuffer(Integer.MAX_VALUE).addFlattenedComponents(true, cumulation);
                 }
-                return buffer;
+                composite.addFlattenedComponents(true, in);
+                in = null;
+                return composite;
             } finally {
                 if (in != null) {
-                    // We must release if the ownership was not transferred as otherwise it may produce a leak if
-                    // writeBytes(...) throw for whatever release (for example because of OutOfMemoryError).
+                    // We must release if the ownership was not transferred as otherwise it may produce a leak
                     in.release();
+                    // Also release any new buffer allocated if we're not returning it
+                    if (composite != null && composite != cumulation) {
+                        composite.release();
+                    }
                 }
             }
         }
@@ -149,8 +154,14 @@ public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter
     ByteBuf cumulation;
     private Cumulator cumulator = MERGE_CUMULATOR;
     private boolean singleDecode;
-    private boolean decodeWasNull;
     private boolean first;
+
+    /**
+     * This flag is used to determine if we need to call {@link ChannelHandlerContext#read()} to consume more data
+     * when {@link ChannelConfig#isAutoRead()} is {@code false}.
+     */
+    private boolean firedChannelRead;
+
     /**
      * A bitmask where the bits are defined as
      * <ul>
@@ -191,10 +202,7 @@ public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter
      * Set the {@link Cumulator} to use for cumulate the received {@link ByteBuf}s.
      */
     public void setCumulator(Cumulator cumulator) {
-        if (cumulator == null) {
-            throw new NullPointerException("cumulator");
-        }
-        this.cumulator = cumulator;
+        this.cumulator = ObjectUtil.checkNotNull(cumulator, "cumulator");
     }
 
     /**
@@ -202,9 +210,7 @@ public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter
      * The default is {@code 16}.
      */
     public void setDiscardAfterReads(int discardAfterReads) {
-        if (discardAfterReads <= 0) {
-            throw new IllegalArgumentException("discardAfterReads must be > 0");
-        }
+        checkPositive(discardAfterReads, "discardAfterReads");
         this.discardAfterReads = discardAfterReads;
     }
 
@@ -241,18 +247,14 @@ public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter
         if (buf != null) {
             // Directly set this to null so we are sure we not access it in any other method here anymore.
             cumulation = null;
-
+            numReads = 0;
             int readable = buf.readableBytes();
             if (readable > 0) {
-                ByteBuf bytes = buf.readBytes(readable);
-                buf.release();
-                ctx.fireChannelRead(bytes);
+                ctx.fireChannelRead(buf);
+                ctx.fireChannelReadComplete();
             } else {
                 buf.release();
             }
-
-            numReads = 0;
-            ctx.fireChannelReadComplete();
         }
         handlerRemoved0(ctx);
     }
@@ -268,13 +270,9 @@ public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter
         if (msg instanceof ByteBuf) {
             CodecOutputList out = CodecOutputList.newInstance();
             try {
-                ByteBuf data = (ByteBuf) msg;
                 first = cumulation == null;
-                if (first) {
-                    cumulation = data;
-                } else {
-                    cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data);
-                }
+                cumulation = cumulator.cumulate(ctx.alloc(),
+                        first ? Unpooled.EMPTY_BUFFER : cumulation, (ByteBuf) msg);
                 callDecode(ctx, cumulation, out);
             } catch (DecoderException e) {
                 throw e;
@@ -293,7 +291,7 @@ public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter
                 }
 
                 int size = out.size();
-                decodeWasNull = !out.insertSinceRecycled();
+                firedChannelRead |= out.insertSinceRecycled();
                 fireChannelRead(ctx, out, size);
                 out.recycle();
             }
@@ -328,12 +326,10 @@ public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter
     public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
         numReads = 0;
         discardSomeReadBytes();
-        if (decodeWasNull) {
-            decodeWasNull = false;
-            if (!ctx.channel().config().isAutoRead()) {
-                ctx.read();
-            }
+        if (!firedChannelRead && !ctx.channel().config().isAutoRead()) {
+            ctx.read();
         }
+        firedChannelRead = false;
         ctx.fireChannelReadComplete();
     }
 
@@ -366,7 +362,7 @@ public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter
         super.userEventTriggered(ctx, evt);
     }
 
-    private void channelInputClosed(ChannelHandlerContext ctx, boolean callChannelInactive) throws Exception {
+    private void channelInputClosed(ChannelHandlerContext ctx, boolean callChannelInactive) {
         CodecOutputList out = CodecOutputList.newInstance();
         try {
             channelInputClosed(ctx, out);
@@ -504,6 +500,8 @@ public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter
             boolean removePending = decodeState == STATE_HANDLER_REMOVED_PENDING;
             decodeState = STATE_INIT;
             if (removePending) {
+                fireChannelRead(ctx, out, out.size());
+                out.clear();
                 handlerRemoved(ctx);
             }
         }
@@ -524,12 +522,23 @@ public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter
         }
     }
 
-    static ByteBuf expandCumulation(ByteBufAllocator alloc, ByteBuf cumulation, int readable) {
-        ByteBuf oldCumulation = cumulation;
-        cumulation = alloc.buffer(oldCumulation.readableBytes() + readable);
-        cumulation.writeBytes(oldCumulation);
-        oldCumulation.release();
-        return cumulation;
+    static ByteBuf expandCumulation(ByteBufAllocator alloc, ByteBuf oldCumulation, ByteBuf in) {
+        int oldBytes = oldCumulation.readableBytes();
+        int newBytes = in.readableBytes();
+        int totalBytes = oldBytes + newBytes;
+        ByteBuf newCumulation = alloc.buffer(alloc.calculateNewCapacity(totalBytes, MAX_VALUE));
+        ByteBuf toRelease = newCumulation;
+        try {
+            // This avoids redundant checks and stack depth compared to calling writeBytes(...)
+            newCumulation.setBytes(0, oldCumulation, oldCumulation.readerIndex(), oldBytes)
+                .setBytes(oldBytes, in, in.readerIndex(), newBytes)
+                .writerIndex(totalBytes);
+            in.readerIndex(in.writerIndex());
+            toRelease = oldCumulation;
+            return newCumulation;
+        } finally {
+            toRelease.release();
+        }
     }
 
     /**
diff --git a/codec/src/main/java/io/netty/handler/codec/DatagramPacketEncoder.java b/codec/src/main/java/io/netty/handler/codec/DatagramPacketEncoder.java
index e8ef7ee..57f9a4e 100644
--- a/codec/src/main/java/io/netty/handler/codec/DatagramPacketEncoder.java
+++ b/codec/src/main/java/io/netty/handler/codec/DatagramPacketEncoder.java
@@ -63,7 +63,7 @@ public class DatagramPacketEncoder<M> extends MessageToMessageEncoder<AddressedE
             @SuppressWarnings("rawtypes")
             AddressedEnvelope envelope = (AddressedEnvelope) msg;
             return encoder.acceptOutboundMessage(envelope.content())
-                    && envelope.sender() instanceof InetSocketAddress
+                    && (envelope.sender() instanceof InetSocketAddress || envelope.sender() == null)
                     && envelope.recipient() instanceof InetSocketAddress;
         }
         return false;
diff --git a/codec/src/main/java/io/netty/handler/codec/DateFormatter.java b/codec/src/main/java/io/netty/handler/codec/DateFormatter.java
index 86df148..b07912a 100644
--- a/codec/src/main/java/io/netty/handler/codec/DateFormatter.java
+++ b/codec/src/main/java/io/netty/handler/codec/DateFormatter.java
@@ -261,10 +261,6 @@ public final class DateFormatter {
         return false;
     }
 
-    private static boolean matchMonth(String month, CharSequence txt, int tokenStart) {
-        return AsciiString.regionMatchesAscii(month, true, 0, txt, tokenStart, 3);
-    }
-
     private boolean tryParseMonth(CharSequence txt, int tokenStart, int tokenEnd) {
         int len = tokenEnd - tokenStart;
 
@@ -272,29 +268,33 @@ public final class DateFormatter {
             return false;
         }
 
-        if (matchMonth("Jan", txt, tokenStart)) {
+        char monthChar1 = AsciiString.toLowerCase(txt.charAt(tokenStart));
+        char monthChar2 = AsciiString.toLowerCase(txt.charAt(tokenStart + 1));
+        char monthChar3 = AsciiString.toLowerCase(txt.charAt(tokenStart + 2));
+
+        if (monthChar1 == 'j' && monthChar2 == 'a' && monthChar3 == 'n') {
             month = Calendar.JANUARY;
-        } else if (matchMonth("Feb", txt, tokenStart)) {
+        } else if (monthChar1 == 'f' && monthChar2 == 'e' && monthChar3 == 'b') {
             month = Calendar.FEBRUARY;
-        } else if (matchMonth("Mar", txt, tokenStart)) {
+        } else if (monthChar1 == 'm' && monthChar2 == 'a' && monthChar3 == 'r') {
             month = Calendar.MARCH;
-        } else if (matchMonth("Apr", txt, tokenStart)) {
+        } else if (monthChar1 == 'a' && monthChar2 == 'p' && monthChar3 == 'r') {
             month = Calendar.APRIL;
-        } else if (matchMonth("May", txt, tokenStart)) {
+        } else if (monthChar1 == 'm' && monthChar2 == 'a' && monthChar3 == 'y') {
             month = Calendar.MAY;
-        } else if (matchMonth("Jun", txt, tokenStart)) {
+        } else if (monthChar1 == 'j' && monthChar2 == 'u' && monthChar3 == 'n') {
             month = Calendar.JUNE;
-        } else if (matchMonth("Jul", txt, tokenStart)) {
+        } else if (monthChar1 == 'j' && monthChar2 == 'u' && monthChar3 == 'l') {
             month = Calendar.JULY;
-        } else if (matchMonth("Aug", txt, tokenStart)) {
+        } else if (monthChar1 == 'a' && monthChar2 == 'u' && monthChar3 == 'g') {
             month = Calendar.AUGUST;
-        } else if (matchMonth("Sep", txt, tokenStart)) {
+        } else if (monthChar1 == 's' && monthChar2 == 'e' && monthChar3 == 'p') {
             month = Calendar.SEPTEMBER;
-        } else if (matchMonth("Oct", txt, tokenStart)) {
+        } else if (monthChar1 == 'o' && monthChar2 == 'c' && monthChar3 == 't') {
             month = Calendar.OCTOBER;
-        } else if (matchMonth("Nov", txt, tokenStart)) {
+        } else if (monthChar1 == 'n' && monthChar2 == 'o' && monthChar3 == 'v') {
             month = Calendar.NOVEMBER;
-        } else if (matchMonth("Dec", txt, tokenStart)) {
+        } else if (monthChar1 == 'd' && monthChar2 == 'e' && monthChar3 == 'c') {
             month = Calendar.DECEMBER;
         } else {
             return false;
diff --git a/codec/src/main/java/io/netty/handler/codec/DecoderResult.java b/codec/src/main/java/io/netty/handler/codec/DecoderResult.java
index f666a3b..253d113 100644
--- a/codec/src/main/java/io/netty/handler/codec/DecoderResult.java
+++ b/codec/src/main/java/io/netty/handler/codec/DecoderResult.java
@@ -16,6 +16,7 @@
 package io.netty.handler.codec;
 
 import io.netty.util.Signal;
+import io.netty.util.internal.ObjectUtil;
 
 public class DecoderResult {
 
@@ -26,19 +27,13 @@ public class DecoderResult {
     public static final DecoderResult SUCCESS = new DecoderResult(SIGNAL_SUCCESS);
 
     public static DecoderResult failure(Throwable cause) {
-        if (cause == null) {
-            throw new NullPointerException("cause");
-        }
-        return new DecoderResult(cause);
+        return new DecoderResult(ObjectUtil.checkNotNull(cause, "cause"));
     }
 
     private final Throwable cause;
 
     protected DecoderResult(Throwable cause) {
-        if (cause == null) {
-            throw new NullPointerException("cause");
-        }
-        this.cause = cause;
+        this.cause = ObjectUtil.checkNotNull(cause, "cause");
     }
 
     public boolean isFinished() {
diff --git a/codec/src/main/java/io/netty/handler/codec/DefaultHeaders.java b/codec/src/main/java/io/netty/handler/codec/DefaultHeaders.java
index df7266d..4109037 100644
--- a/codec/src/main/java/io/netty/handler/codec/DefaultHeaders.java
+++ b/codec/src/main/java/io/netty/handler/codec/DefaultHeaders.java
@@ -1012,6 +1012,20 @@ public class DefaultHeaders<K, V, T extends Headers<K, V, T>> implements Headers
         return value;
     }
 
+    private HeaderEntry<K, V> remove0(HeaderEntry<K, V> entry, HeaderEntry<K, V> previous) {
+        int i = index(entry.hash);
+        HeaderEntry<K, V> e = entries[i];
+        if (e == entry) {
+            entries[i] = entry.next;
+            previous = entries[i];
+        } else {
+            previous.next = entry.next;
+        }
+        entry.remove();
+        --size;
+        return previous;
+    }
+
     @SuppressWarnings("unchecked")
     private T thisT() {
         return (T) this;
@@ -1055,6 +1069,8 @@ public class DefaultHeaders<K, V, T extends Headers<K, V, T>> implements Headers
     private final class ValueIterator implements Iterator<V> {
         private final K name;
         private final int hash;
+        private HeaderEntry<K, V> removalPrevious;
+        private HeaderEntry<K, V> previous;
         private HeaderEntry<K, V> next;
 
         ValueIterator(K name) {
@@ -1073,14 +1089,21 @@ public class DefaultHeaders<K, V, T extends Headers<K, V, T>> implements Headers
             if (!hasNext()) {
                 throw new NoSuchElementException();
             }
-            HeaderEntry<K, V> current = next;
+            if (previous != null) {
+                removalPrevious = previous;
+            }
+            previous = next;
             calculateNext(next.next);
-            return current.value;
+            return previous.value;
         }
 
         @Override
         public void remove() {
-            throw new UnsupportedOperationException("read only");
+            if (previous == null) {
+                throw new IllegalStateException();
+            }
+            removalPrevious = remove0(previous, removalPrevious);
+            previous = null;
         }
 
         private void calculateNext(HeaderEntry<K, V> entry) {
diff --git a/codec/src/main/java/io/netty/handler/codec/DelimiterBasedFrameDecoder.java b/codec/src/main/java/io/netty/handler/codec/DelimiterBasedFrameDecoder.java
index 27e8c20..1184f0f 100644
--- a/codec/src/main/java/io/netty/handler/codec/DelimiterBasedFrameDecoder.java
+++ b/codec/src/main/java/io/netty/handler/codec/DelimiterBasedFrameDecoder.java
@@ -15,8 +15,11 @@
  */
 package io.netty.handler.codec;
 
+import static io.netty.util.internal.ObjectUtil.checkPositive;
+
 import io.netty.buffer.ByteBuf;
 import io.netty.channel.ChannelHandlerContext;
+import io.netty.util.internal.ObjectUtil;
 
 import java.util.List;
 
@@ -164,12 +167,7 @@ public class DelimiterBasedFrameDecoder extends ByteToMessageDecoder {
     public DelimiterBasedFrameDecoder(
             int maxFrameLength, boolean stripDelimiter, boolean failFast, ByteBuf... delimiters) {
         validateMaxFrameLength(maxFrameLength);
-        if (delimiters == null) {
-            throw new NullPointerException("delimiters");
-        }
-        if (delimiters.length == 0) {
-            throw new IllegalArgumentException("empty delimiters");
-        }
+        ObjectUtil.checkNonEmpty(delimiters, "delimiters");
 
         if (isLineBased(delimiters) && !isSubclass()) {
             lineBasedDecoder = new LineBasedFrameDecoder(maxFrameLength, stripDelimiter, failFast);
@@ -337,19 +335,13 @@ public class DelimiterBasedFrameDecoder extends ByteToMessageDecoder {
     }
 
     private static void validateDelimiter(ByteBuf delimiter) {
-        if (delimiter == null) {
-            throw new NullPointerException("delimiter");
-        }
+        ObjectUtil.checkNotNull(delimiter, "delimiter");
         if (!delimiter.isReadable()) {
             throw new IllegalArgumentException("empty delimiter");
         }
     }
 
     private static void validateMaxFrameLength(int maxFrameLength) {
-        if (maxFrameLength <= 0) {
-            throw new IllegalArgumentException(
-                    "maxFrameLength must be a positive integer: " +
-                    maxFrameLength);
-        }
+        checkPositive(maxFrameLength, "maxFrameLength");
     }
 }
diff --git a/codec/src/main/java/io/netty/handler/codec/FixedLengthFrameDecoder.java b/codec/src/main/java/io/netty/handler/codec/FixedLengthFrameDecoder.java
index 5b4bb71..9475e5b 100644
--- a/codec/src/main/java/io/netty/handler/codec/FixedLengthFrameDecoder.java
+++ b/codec/src/main/java/io/netty/handler/codec/FixedLengthFrameDecoder.java
@@ -15,6 +15,8 @@
  */
 package io.netty.handler.codec;
 
+import static io.netty.util.internal.ObjectUtil.checkPositive;
+
 import io.netty.buffer.ByteBuf;
 import io.netty.channel.ChannelHandlerContext;
 
@@ -46,10 +48,7 @@ public class FixedLengthFrameDecoder extends ByteToMessageDecoder {
      * @param frameLength the length of the frame
      */
     public FixedLengthFrameDecoder(int frameLength) {
-        if (frameLength <= 0) {
-            throw new IllegalArgumentException(
-                    "frameLength must be a positive integer: " + frameLength);
-        }
+        checkPositive(frameLength, "frameLength");
         this.frameLength = frameLength;
     }
 
diff --git a/codec/src/main/java/io/netty/handler/codec/LengthFieldBasedFrameDecoder.java b/codec/src/main/java/io/netty/handler/codec/LengthFieldBasedFrameDecoder.java
index 4d94bdf..bed0c79 100644
--- a/codec/src/main/java/io/netty/handler/codec/LengthFieldBasedFrameDecoder.java
+++ b/codec/src/main/java/io/netty/handler/codec/LengthFieldBasedFrameDecoder.java
@@ -15,12 +15,15 @@
  */
 package io.netty.handler.codec;
 
+import static io.netty.util.internal.ObjectUtil.checkNotNull;
+import static io.netty.util.internal.ObjectUtil.checkPositive;
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
+
 import java.nio.ByteOrder;
 import java.util.List;
 
 import io.netty.buffer.ByteBuf;
 import io.netty.channel.ChannelHandlerContext;
-import io.netty.handler.codec.serialization.ObjectDecoder;
 
 /**
  * A decoder that splits the received {@link ByteBuf}s dynamically by the
@@ -298,27 +301,14 @@ public class LengthFieldBasedFrameDecoder extends ByteToMessageDecoder {
     public LengthFieldBasedFrameDecoder(
             ByteOrder byteOrder, int maxFrameLength, int lengthFieldOffset, int lengthFieldLength,
             int lengthAdjustment, int initialBytesToStrip, boolean failFast) {
-        if (byteOrder == null) {
-            throw new NullPointerException("byteOrder");
-        }
 
-        if (maxFrameLength <= 0) {
-            throw new IllegalArgumentException(
-                    "maxFrameLength must be a positive integer: " +
-                    maxFrameLength);
-        }
+        this.byteOrder = checkNotNull(byteOrder, "byteOrder");
 
-        if (lengthFieldOffset < 0) {
-            throw new IllegalArgumentException(
-                    "lengthFieldOffset must be a non-negative integer: " +
-                    lengthFieldOffset);
-        }
+        checkPositive(maxFrameLength, "maxFrameLength");
 
-        if (initialBytesToStrip < 0) {
-            throw new IllegalArgumentException(
-                    "initialBytesToStrip must be a non-negative integer: " +
-                    initialBytesToStrip);
-        }
+        checkPositiveOrZero(lengthFieldOffset, "lengthFieldOffset");
+
+        checkPositiveOrZero(initialBytesToStrip, "initialBytesToStrip");
 
         if (lengthFieldOffset > maxFrameLength - lengthFieldLength) {
             throw new IllegalArgumentException(
@@ -328,12 +318,11 @@ public class LengthFieldBasedFrameDecoder extends ByteToMessageDecoder {
                     "lengthFieldLength (" + lengthFieldLength + ").");
         }
 
-        this.byteOrder = byteOrder;
         this.maxFrameLength = maxFrameLength;
         this.lengthFieldOffset = lengthFieldOffset;
         this.lengthFieldLength = lengthFieldLength;
         this.lengthAdjustment = lengthAdjustment;
-        lengthFieldEndOffset = lengthFieldOffset + lengthFieldLength;
+        this.lengthFieldEndOffset = lengthFieldOffset + lengthFieldLength;
         this.initialBytesToStrip = initialBytesToStrip;
         this.failFast = failFast;
     }
@@ -504,14 +493,6 @@ public class LengthFieldBasedFrameDecoder extends ByteToMessageDecoder {
 
     /**
      * Extract the sub-region of the specified buffer.
-     * <p>
-     * If you are sure that the frame and its content are not accessed after
-     * the current {@link #decode(ChannelHandlerContext, ByteBuf)}
-     * call returns, you can even avoid memory copy by returning the sliced
-     * sub-region (i.e. <tt>return buffer.slice(index, length)</tt>).
-     * It's often useful when you convert the extracted frame into an object.
-     * Refer to the source code of {@link ObjectDecoder} to see how this method
-     * is overridden to avoid memory copy.
      */
     protected ByteBuf extractFrame(ChannelHandlerContext ctx, ByteBuf buffer, int index, int length) {
         return buffer.retainedSlice(index, length);
diff --git a/codec/src/main/java/io/netty/handler/codec/LengthFieldPrepender.java b/codec/src/main/java/io/netty/handler/codec/LengthFieldPrepender.java
index 4076e07..ecc9225 100644
--- a/codec/src/main/java/io/netty/handler/codec/LengthFieldPrepender.java
+++ b/codec/src/main/java/io/netty/handler/codec/LengthFieldPrepender.java
@@ -15,6 +15,8 @@
  */
 package io.netty.handler.codec;
 
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
+
 import io.netty.buffer.ByteBuf;
 import io.netty.channel.ChannelHandler.Sharable;
 import io.netty.channel.ChannelHandlerContext;
@@ -148,9 +150,7 @@ public class LengthFieldPrepender extends MessageToMessageEncoder<ByteBuf> {
                     "lengthFieldLength must be either 1, 2, 3, 4, or 8: " +
                     lengthFieldLength);
         }
-        ObjectUtil.checkNotNull(byteOrder, "byteOrder");
-
-        this.byteOrder = byteOrder;
+        this.byteOrder = ObjectUtil.checkNotNull(byteOrder, "byteOrder");
         this.lengthFieldLength = lengthFieldLength;
         this.lengthIncludesLengthFieldLength = lengthIncludesLengthFieldLength;
         this.lengthAdjustment = lengthAdjustment;
@@ -163,10 +163,7 @@ public class LengthFieldPrepender extends MessageToMessageEncoder<ByteBuf> {
             length += lengthFieldLength;
         }
 
-        if (length < 0) {
-            throw new IllegalArgumentException(
-                    "Adjusted frame length (" + length + ") is less than zero");
-        }
+        checkPositiveOrZero(length, "length");
 
         switch (lengthFieldLength) {
         case 1:
diff --git a/codec/src/main/java/io/netty/handler/codec/MessageAggregator.java b/codec/src/main/java/io/netty/handler/codec/MessageAggregator.java
index 2cdb880..1e46e0d 100644
--- a/codec/src/main/java/io/netty/handler/codec/MessageAggregator.java
+++ b/codec/src/main/java/io/netty/handler/codec/MessageAggregator.java
@@ -28,6 +28,7 @@ import io.netty.util.ReferenceCountUtil;
 import java.util.List;
 
 import static io.netty.buffer.Unpooled.EMPTY_BUFFER;
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
 
 /**
  * An abstract {@link ChannelHandler} that aggregates a series of message objects into a single aggregated message.
@@ -61,6 +62,8 @@ public abstract class MessageAggregator<I, S, C extends ByteBufHolder, O extends
     private ChannelHandlerContext ctx;
     private ChannelFutureListener continueResponseWriteListener;
 
+    private boolean aggregating;
+
     /**
      * Creates a new instance.
      *
@@ -81,9 +84,7 @@ public abstract class MessageAggregator<I, S, C extends ByteBufHolder, O extends
     }
 
     private static void validateMaxContentLength(int maxContentLength) {
-        if (maxContentLength < 0) {
-            throw new IllegalArgumentException("maxContentLength: " + maxContentLength + " (expected: >= 0)");
-        }
+        checkPositiveOrZero(maxContentLength, "maxContentLength");
     }
 
     @Override
@@ -96,7 +97,20 @@ public abstract class MessageAggregator<I, S, C extends ByteBufHolder, O extends
         @SuppressWarnings("unchecked")
         I in = (I) msg;
 
-        return (isContentMessage(in) || isStartMessage(in)) && !isAggregated(in);
+        if (isAggregated(in)) {
+            return false;
+        }
+
+        // NOTE: It's tempting to make this check only if aggregating is false. There are however
+        // side conditions in decode(...) in respect to large messages.
+        if (isStartMessage(in)) {
+            aggregating = true;
+            return true;
+        } else if (aggregating && isContentMessage(in)) {
+            return true;
+        }
+
+        return false;
     }
 
     /**
@@ -192,6 +206,8 @@ public abstract class MessageAggregator<I, S, C extends ByteBufHolder, O extends
 
     @Override
     protected void decode(final ChannelHandlerContext ctx, I msg, List<Object> out) throws Exception {
+        assert aggregating;
+
         if (isStartMessage(msg)) {
             handlingOversizedMessage = false;
             if (currentMessage != null) {
@@ -246,7 +262,7 @@ public abstract class MessageAggregator<I, S, C extends ByteBufHolder, O extends
                 } else {
                     aggregated = beginAggregation(m, EMPTY_BUFFER);
                 }
-                finishAggregation(aggregated);
+                finishAggregation0(aggregated);
                 out.add(aggregated);
                 return;
             }
@@ -301,7 +317,7 @@ public abstract class MessageAggregator<I, S, C extends ByteBufHolder, O extends
             }
 
             if (last) {
-                finishAggregation(currentMessage);
+                finishAggregation0(currentMessage);
 
                 // All done
                 out.add(currentMessage);
@@ -371,6 +387,11 @@ public abstract class MessageAggregator<I, S, C extends ByteBufHolder, O extends
      */
     protected void aggregate(O aggregated, C content) throws Exception { }
 
+    private void finishAggregation0(O aggregated) throws Exception {
+        aggregating = false;
+        finishAggregation(aggregated);
+    }
+
     /**
      * Invoked when the specified {@code aggregated} message is about to be passed to the next handler in the pipeline.
      */
@@ -441,6 +462,7 @@ public abstract class MessageAggregator<I, S, C extends ByteBufHolder, O extends
             currentMessage.release();
             currentMessage = null;
             handlingOversizedMessage = false;
+            aggregating = false;
         }
     }
 }
diff --git a/codec/src/main/java/io/netty/handler/codec/MessageToMessageEncoder.java b/codec/src/main/java/io/netty/handler/codec/MessageToMessageEncoder.java
index 6b47b6f..439dc8c 100644
--- a/codec/src/main/java/io/netty/handler/codec/MessageToMessageEncoder.java
+++ b/codec/src/main/java/io/netty/handler/codec/MessageToMessageEncoder.java
@@ -132,7 +132,7 @@ public abstract class MessageToMessageEncoder<I> extends ChannelOutboundHandlerA
     }
 
     private static void writePromiseCombiner(ChannelHandlerContext ctx, CodecOutputList out, ChannelPromise promise) {
-        final PromiseCombiner combiner = new PromiseCombiner();
+        final PromiseCombiner combiner = new PromiseCombiner(ctx.executor());
         for (int i = 0; i < out.size(); i++) {
             combiner.add(ctx.write(out.getUnsafe(i)));
         }
diff --git a/codec/src/main/java/io/netty/handler/codec/ProtocolDetectionResult.java b/codec/src/main/java/io/netty/handler/codec/ProtocolDetectionResult.java
index d4b4359..1578386 100644
--- a/codec/src/main/java/io/netty/handler/codec/ProtocolDetectionResult.java
+++ b/codec/src/main/java/io/netty/handler/codec/ProtocolDetectionResult.java
@@ -25,7 +25,7 @@ import static io.netty.util.internal.ObjectUtil.checkNotNull;
 public final class ProtocolDetectionResult<T> {
 
     @SuppressWarnings({ "rawtypes", "unchecked" })
-    private static final ProtocolDetectionResult NEEDS_MORE_DATE =
+    private static final ProtocolDetectionResult NEEDS_MORE_DATA =
             new ProtocolDetectionResult(ProtocolDetectionState.NEEDS_MORE_DATA, null);
     @SuppressWarnings({ "rawtypes", "unchecked" })
     private static final ProtocolDetectionResult INVALID =
@@ -39,7 +39,7 @@ public final class ProtocolDetectionResult<T> {
      */
     @SuppressWarnings("unchecked")
     public static <T> ProtocolDetectionResult<T> needsMoreData() {
-        return NEEDS_MORE_DATE;
+        return NEEDS_MORE_DATA;
     }
 
     /**
diff --git a/codec/src/main/java/io/netty/handler/codec/ReplayingDecoderByteBuf.java b/codec/src/main/java/io/netty/handler/codec/ReplayingDecoderByteBuf.java
index 5e71b7d..26f4186 100644
--- a/codec/src/main/java/io/netty/handler/codec/ReplayingDecoderByteBuf.java
+++ b/codec/src/main/java/io/netty/handler/codec/ReplayingDecoderByteBuf.java
@@ -15,14 +15,6 @@
  */
 package io.netty.handler.codec;
 
-import io.netty.buffer.ByteBuf;
-import io.netty.buffer.ByteBufAllocator;
-import io.netty.buffer.SwappedByteBuf;
-import io.netty.buffer.Unpooled;
-import io.netty.util.ByteProcessor;
-import io.netty.util.Signal;
-import io.netty.util.internal.StringUtil;
-
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.nio.ByteBuffer;
@@ -32,6 +24,15 @@ import java.nio.channels.GatheringByteChannel;
 import java.nio.channels.ScatteringByteChannel;
 import java.nio.charset.Charset;
 
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufAllocator;
+import io.netty.buffer.SwappedByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.util.ByteProcessor;
+import io.netty.util.Signal;
+import io.netty.util.internal.ObjectUtil;
+import io.netty.util.internal.StringUtil;
+
 /**
  * Special {@link ByteBuf} implementation which is used by the {@link ReplayingDecoder}
  */
@@ -472,10 +473,7 @@ final class ReplayingDecoderByteBuf extends ByteBuf {
 
     @Override
     public ByteBuf order(ByteOrder endianness) {
-        if (endianness == null) {
-            throw new NullPointerException("endianness");
-        }
-        if (endianness == order()) {
+        if (ObjectUtil.checkNotNull(endianness, "endianness") == order()) {
             return this;
         }
 
@@ -488,12 +486,12 @@ final class ReplayingDecoderByteBuf extends ByteBuf {
 
     @Override
     public boolean isReadable() {
-        return terminated? buffer.isReadable() : true;
+        return !terminated || buffer.isReadable();
     }
 
     @Override
     public boolean isReadable(int size) {
-        return terminated? buffer.isReadable(size) : true;
+        return !terminated || buffer.isReadable(size);
     }
 
     @Override
diff --git a/codec/src/main/java/io/netty/handler/codec/base64/Base64.java b/codec/src/main/java/io/netty/handler/codec/base64/Base64.java
index a4efca2..ace2582 100644
--- a/codec/src/main/java/io/netty/handler/codec/base64/Base64.java
+++ b/codec/src/main/java/io/netty/handler/codec/base64/Base64.java
@@ -22,6 +22,7 @@ package io.netty.handler.codec.base64;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.ByteBufAllocator;
 import io.netty.util.ByteProcessor;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.PlatformDependent;
 
 import java.nio.ByteOrder;
@@ -50,24 +51,15 @@ public final class Base64 {
     private static final byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in encoding
 
     private static byte[] alphabet(Base64Dialect dialect) {
-        if (dialect == null) {
-            throw new NullPointerException("dialect");
-        }
-        return dialect.alphabet;
+        return ObjectUtil.checkNotNull(dialect, "dialect").alphabet;
     }
 
     private static byte[] decodabet(Base64Dialect dialect) {
-        if (dialect == null) {
-            throw new NullPointerException("dialect");
-        }
-        return dialect.decodabet;
+        return ObjectUtil.checkNotNull(dialect, "dialect").decodabet;
     }
 
     private static boolean breakLines(Base64Dialect dialect) {
-        if (dialect == null) {
-            throw new NullPointerException("dialect");
-        }
-        return dialect.breakLinesByDefault;
+        return ObjectUtil.checkNotNull(dialect, "dialect").breakLinesByDefault;
     }
 
     public static ByteBuf encode(ByteBuf src) {
@@ -83,10 +75,7 @@ public final class Base64 {
     }
 
     public static ByteBuf encode(ByteBuf src, boolean breakLines, Base64Dialect dialect) {
-
-        if (src == null) {
-            throw new NullPointerException("src");
-        }
+        ObjectUtil.checkNotNull(src, "src");
 
         ByteBuf dest = encode(src, src.readerIndex(), src.readableBytes(), breakLines, dialect);
         src.readerIndex(src.writerIndex());
@@ -113,12 +102,8 @@ public final class Base64 {
 
     public static ByteBuf encode(
             ByteBuf src, int off, int len, boolean breakLines, Base64Dialect dialect, ByteBufAllocator allocator) {
-        if (src == null) {
-            throw new NullPointerException("src");
-        }
-        if (dialect == null) {
-            throw new NullPointerException("dialect");
-        }
+        ObjectUtil.checkNotNull(src, "src");
+        ObjectUtil.checkNotNull(dialect, "dialect");
 
         ByteBuf dest = allocator.buffer(encodedBufferSize(len, breakLines)).order(src.order());
         byte[] alphabet = alphabet(dialect);
@@ -291,9 +276,7 @@ public final class Base64 {
     }
 
     public static ByteBuf decode(ByteBuf src, Base64Dialect dialect) {
-        if (src == null) {
-            throw new NullPointerException("src");
-        }
+        ObjectUtil.checkNotNull(src, "src");
 
         ByteBuf dest = decode(src, src.readerIndex(), src.readableBytes(), dialect);
         src.readerIndex(src.writerIndex());
@@ -312,12 +295,8 @@ public final class Base64 {
 
     public static ByteBuf decode(
             ByteBuf src, int off, int len, Base64Dialect dialect, ByteBufAllocator allocator) {
-        if (src == null) {
-            throw new NullPointerException("src");
-        }
-        if (dialect == null) {
-            throw new NullPointerException("dialect");
-        }
+        ObjectUtil.checkNotNull(src, "src");
+        ObjectUtil.checkNotNull(dialect, "dialect");
 
         // Using a ByteProcessor to reduce bound and reference count checking.
         return new Decoder().decode(src, off, len, allocator, dialect);
@@ -331,8 +310,6 @@ public final class Base64 {
     private static final class Decoder implements ByteProcessor {
         private final byte[] b4 = new byte[4];
         private int b4Posn;
-        private byte sbiCrop;
-        private byte sbiDecode;
         private byte[] decodabet;
         private int outBuffPosn;
         private ByteBuf dest;
@@ -353,26 +330,24 @@ public final class Base64 {
 
         @Override
         public boolean process(byte value) throws Exception {
-            sbiCrop = (byte) (value & 0x7f); // Only the low seven bits
-            sbiDecode = decodabet[sbiCrop];
-
-            if (sbiDecode >= WHITE_SPACE_ENC) { // White space, Equals sign or better
-                if (sbiDecode >= EQUALS_SIGN_ENC) { // Equals sign or better
-                    b4[b4Posn ++] = sbiCrop;
-                    if (b4Posn > 3) { // Quartet built
-                        outBuffPosn += decode4to3(b4, dest, outBuffPosn, decodabet);
-                        b4Posn = 0;
-
-                        // If that was the equals sign, break out of 'for' loop
-                        if (sbiCrop == EQUALS_SIGN) {
-                            return false;
+            if (value > 0) {
+                byte sbiDecode = decodabet[value];
+                if (sbiDecode >= WHITE_SPACE_ENC) { // White space, Equals sign or better
+                    if (sbiDecode >= EQUALS_SIGN_ENC) { // Equals sign or better
+                        b4[b4Posn ++] = value;
+                        if (b4Posn > 3) { // Quartet built
+                            outBuffPosn += decode4to3(b4, dest, outBuffPosn, decodabet);
+                            b4Posn = 0;
+
+                            // If that was the equals sign, break out of 'for' loop
+                            return value != EQUALS_SIGN;
                         }
                     }
+                    return true;
                 }
-                return true;
             }
             throw new IllegalArgumentException(
-                    "invalid bad Base64 input character: " + (short) (value & 0xFF) + " (decimal)");
+                    "invalid Base64 input character: " + (short) (value & 0xFF) + " (decimal)");
         }
 
         private static int decode4to3(byte[] src, ByteBuf dest, int destOffset, byte[] decodabet) {
diff --git a/codec/src/main/java/io/netty/handler/codec/base64/Base64Decoder.java b/codec/src/main/java/io/netty/handler/codec/base64/Base64Decoder.java
index b8d7279..6759483 100644
--- a/codec/src/main/java/io/netty/handler/codec/base64/Base64Decoder.java
+++ b/codec/src/main/java/io/netty/handler/codec/base64/Base64Decoder.java
@@ -23,6 +23,7 @@ import io.netty.handler.codec.ByteToMessageDecoder;
 import io.netty.handler.codec.DelimiterBasedFrameDecoder;
 import io.netty.handler.codec.Delimiters;
 import io.netty.handler.codec.MessageToMessageDecoder;
+import io.netty.util.internal.ObjectUtil;
 
 import java.util.List;
 
@@ -53,10 +54,7 @@ public class Base64Decoder extends MessageToMessageDecoder<ByteBuf> {
     }
 
     public Base64Decoder(Base64Dialect dialect) {
-        if (dialect == null) {
-            throw new NullPointerException("dialect");
-        }
-        this.dialect = dialect;
+        this.dialect = ObjectUtil.checkNotNull(dialect, "dialect");
     }
 
     @Override
diff --git a/codec/src/main/java/io/netty/handler/codec/base64/Base64Dialect.java b/codec/src/main/java/io/netty/handler/codec/base64/Base64Dialect.java
index 27aecf2..716b0e2 100644
--- a/codec/src/main/java/io/netty/handler/codec/base64/Base64Dialect.java
+++ b/codec/src/main/java/io/netty/handler/codec/base64/Base64Dialect.java
@@ -67,17 +67,17 @@ public enum Base64Dialect {
             -9, -9, -9, -9, -9, -9, // Decimal 91 - 96
             26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
             39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
-            -9, -9, -9, -9, // Decimal 123 - 126
-         /* -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 127 - 139
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 140 - 152
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 153 - 165
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 166 - 178
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 179 - 191
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 192 - 204
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 205 - 217
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 218 - 230
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 231 - 243
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9         // Decimal 244 - 255 */
+            -9, -9, -9, -9, -9 // Decimal 123 - 127
+         /* -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 128 - 140
+            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 141 - 153
+            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 154 - 166
+            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 167 - 179
+            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 180 - 192
+            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 193 - 205
+            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 206 - 218
+            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 219 - 231
+            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 232 - 244
+            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9            // Decimal 245 - 255 */
             }, true),
     /**
      * Base64-like encoding that is URL-safe as described in the Section 4 of
@@ -126,17 +126,17 @@ public enum Base64Dialect {
             -9, // Decimal 96
             26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
             39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
-            -9, -9, -9, -9, // Decimal 123 - 126
-          /*-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 127 - 139
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 140 - 152
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 153 - 165
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 166 - 178
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 179 - 191
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 192 - 204
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 205 - 217
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 218 - 230
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 231 - 243
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9         // Decimal 244 - 255 */
+            -9, -9, -9, -9, -9, // Decimal 123 - 127
+         /* -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 128 - 140
+            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 141 - 153
+            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 154 - 166
+            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 167 - 179
+            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 180 - 192
+            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 193 - 205
+            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 206 - 218
+            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 219 - 231
+            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 232 - 244
+            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9            // Decimal 245 - 255 */
             }, false),
     /**
      * Special "ordered" dialect of Base64 described in
@@ -182,17 +182,17 @@ public enum Base64Dialect {
             -9, // Decimal 96
             38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, // Letters 'a' through 'm'
             51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, // Letters 'n' through 'z'
-            -9, -9, -9, -9, // Decimal 123 - 126
-         /* -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 127 - 139
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 140 - 152
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 153 - 165
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 166 - 178
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 179 - 191
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 192 - 204
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 205 - 217
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 218 - 230
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 231 - 243
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9         // Decimal 244 - 255 */
+            -9, -9, -9, -9, -9 // Decimal 123 - 127
+         /* -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 128 - 140
+            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 141 - 153
+            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 154 - 166
+            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 167 - 179
+            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 180 - 192
+            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 193 - 205
+            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 206 - 218
+            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 219 - 231
+            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 232 - 244
+            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9            // Decimal 245 - 255 */
             }, true);
 
     final byte[] alphabet;
diff --git a/codec/src/main/java/io/netty/handler/codec/base64/Base64Encoder.java b/codec/src/main/java/io/netty/handler/codec/base64/Base64Encoder.java
index 92c23a5..54afb1d 100644
--- a/codec/src/main/java/io/netty/handler/codec/base64/Base64Encoder.java
+++ b/codec/src/main/java/io/netty/handler/codec/base64/Base64Encoder.java
@@ -22,6 +22,7 @@ import io.netty.channel.ChannelPipeline;
 import io.netty.handler.codec.DelimiterBasedFrameDecoder;
 import io.netty.handler.codec.Delimiters;
 import io.netty.handler.codec.MessageToMessageEncoder;
+import io.netty.util.internal.ObjectUtil;
 
 import java.util.List;
 
@@ -54,12 +55,8 @@ public class Base64Encoder extends MessageToMessageEncoder<ByteBuf> {
     }
 
     public Base64Encoder(boolean breakLines, Base64Dialect dialect) {
-        if (dialect == null) {
-            throw new NullPointerException("dialect");
-        }
-
+        this.dialect = ObjectUtil.checkNotNull(dialect, "dialect");
         this.breakLines = breakLines;
-        this.dialect = dialect;
     }
 
     @Override
diff --git a/codec/src/main/java/io/netty/handler/codec/compression/ByteBufChecksum.java b/codec/src/main/java/io/netty/handler/codec/compression/ByteBufChecksum.java
index 2aff623..05456b6 100644
--- a/codec/src/main/java/io/netty/handler/codec/compression/ByteBufChecksum.java
+++ b/codec/src/main/java/io/netty/handler/codec/compression/ByteBufChecksum.java
@@ -55,7 +55,7 @@ abstract class ByteBufChecksum implements Checksum {
         if (PlatformDependent.javaVersion() >= 8) {
             try {
                 Method method = checksum.getClass().getDeclaredMethod("update", ByteBuffer.class);
-                method.invoke(method, ByteBuffer.allocate(1));
+                method.invoke(checksum, ByteBuffer.allocate(1));
                 return method;
             } catch (Throwable ignore) {
                 return null;
@@ -66,6 +66,9 @@ abstract class ByteBufChecksum implements Checksum {
 
     static ByteBufChecksum wrapChecksum(Checksum checksum) {
         ObjectUtil.checkNotNull(checksum, "checksum");
+        if (checksum instanceof ByteBufChecksum) {
+            return (ByteBufChecksum) checksum;
+        }
         if (checksum instanceof Adler32 && ADLER32_UPDATE_METHOD != null) {
             return new ReflectiveByteBufChecksum(checksum, ADLER32_UPDATE_METHOD);
         }
@@ -100,7 +103,7 @@ abstract class ByteBufChecksum implements Checksum {
                 update(b.array(), b.arrayOffset() + off, len);
             } else {
                 try {
-                    method.invoke(checksum, CompressionUtil.safeNioBuffer(b));
+                    method.invoke(checksum, CompressionUtil.safeNioBuffer(b, off, len));
                 } catch (Throwable cause) {
                     throw new Error();
                 }
diff --git a/codec/src/main/java/io/netty/handler/codec/compression/Bzip2DivSufSort.java b/codec/src/main/java/io/netty/handler/codec/compression/Bzip2DivSufSort.java
index cdf92a6..8138729 100644
--- a/codec/src/main/java/io/netty/handler/codec/compression/Bzip2DivSufSort.java
+++ b/codec/src/main/java/io/netty/handler/codec/compression/Bzip2DivSufSort.java
@@ -568,7 +568,9 @@ final class Bzip2DivSufSort {
                     SA[i++] = SA[k];
                     SA[k++] = SA[i];
                     if (last <= k) {
-                        while (j < bufend) { SA[i++] = buf[j]; buf[j++] = SA[i]; }
+                        while (j < bufend) {
+                            SA[i++] = buf[j]; buf[j++] = SA[i];
+                        }
                         SA[i] = buf[j]; buf[j] = t;
                         return;
                     }
diff --git a/codec/src/main/java/io/netty/handler/codec/compression/CompressionUtil.java b/codec/src/main/java/io/netty/handler/codec/compression/CompressionUtil.java
index 8b43e7f..f25f1ee 100644
--- a/codec/src/main/java/io/netty/handler/codec/compression/CompressionUtil.java
+++ b/codec/src/main/java/io/netty/handler/codec/compression/CompressionUtil.java
@@ -40,4 +40,9 @@ final class CompressionUtil {
         return buffer.nioBufferCount() == 1 ? buffer.internalNioBuffer(buffer.readerIndex(), buffer.readableBytes())
                 : buffer.nioBuffer();
     }
+
+    static ByteBuffer safeNioBuffer(ByteBuf buffer, int index, int length) {
+        return buffer.nioBufferCount() == 1 ? buffer.internalNioBuffer(index, length)
+                : buffer.nioBuffer(index, length);
+    }
 }
diff --git a/codec/src/main/java/io/netty/handler/codec/compression/JZlibDecoder.java b/codec/src/main/java/io/netty/handler/codec/compression/JZlibDecoder.java
index 5d23bb8..6c65cd5 100644
--- a/codec/src/main/java/io/netty/handler/codec/compression/JZlibDecoder.java
+++ b/codec/src/main/java/io/netty/handler/codec/compression/JZlibDecoder.java
@@ -18,7 +18,9 @@ package io.netty.handler.codec.compression;
 import com.jcraft.jzlib.Inflater;
 import com.jcraft.jzlib.JZlib;
 import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufAllocator;
 import io.netty.channel.ChannelHandlerContext;
+import io.netty.util.internal.ObjectUtil;
 
 import java.util.List;
 
@@ -34,7 +36,21 @@ public class JZlibDecoder extends ZlibDecoder {
      * @throws DecompressionException if failed to initialize zlib
      */
     public JZlibDecoder() {
-        this(ZlibWrapper.ZLIB);
+        this(ZlibWrapper.ZLIB, 0);
+    }
+
+    /**
+     * Creates a new instance with the default wrapper ({@link ZlibWrapper#ZLIB})
+     * and specified maximum buffer allocation.
+     *
+     * @param maxAllocation
+     *          Maximum size of the decompression buffer. Must be &gt;= 0.
+     *          If zero, maximum size is decided by the {@link ByteBufAllocator}.
+     *
+     * @throws DecompressionException if failed to initialize zlib
+     */
+    public JZlibDecoder(int maxAllocation) {
+        this(ZlibWrapper.ZLIB, maxAllocation);
     }
 
     /**
@@ -43,9 +59,22 @@ public class JZlibDecoder extends ZlibDecoder {
      * @throws DecompressionException if failed to initialize zlib
      */
     public JZlibDecoder(ZlibWrapper wrapper) {
-        if (wrapper == null) {
-            throw new NullPointerException("wrapper");
-        }
+        this(wrapper, 0);
+    }
+
+    /**
+     * Creates a new instance with the specified wrapper and maximum buffer allocation.
+     *
+     * @param maxAllocation
+     *          Maximum size of the decompression buffer. Must be &gt;= 0.
+     *          If zero, maximum size is decided by the {@link ByteBufAllocator}.
+     *
+     * @throws DecompressionException if failed to initialize zlib
+     */
+    public JZlibDecoder(ZlibWrapper wrapper, int maxAllocation) {
+        super(maxAllocation);
+
+        ObjectUtil.checkNotNull(wrapper, "wrapper");
 
         int resultCode = z.init(ZlibUtil.convertWrapperType(wrapper));
         if (resultCode != JZlib.Z_OK) {
@@ -61,11 +90,23 @@ public class JZlibDecoder extends ZlibDecoder {
      * @throws DecompressionException if failed to initialize zlib
      */
     public JZlibDecoder(byte[] dictionary) {
-        if (dictionary == null) {
-            throw new NullPointerException("dictionary");
-        }
-        this.dictionary = dictionary;
+        this(dictionary, 0);
+    }
 
+    /**
+     * Creates a new instance with the specified preset dictionary and maximum buffer allocation.
+     * The wrapper is always {@link ZlibWrapper#ZLIB} because it is the only format that
+     * supports the preset dictionary.
+     *
+     * @param maxAllocation
+     *          Maximum size of the decompression buffer. Must be &gt;= 0.
+     *          If zero, maximum size is decided by the {@link ByteBufAllocator}.
+     *
+     * @throws DecompressionException if failed to initialize zlib
+     */
+    public JZlibDecoder(byte[] dictionary, int maxAllocation) {
+        super(maxAllocation);
+        this.dictionary = ObjectUtil.checkNotNull(dictionary, "dictionary");
         int resultCode;
         resultCode = z.inflateInit(JZlib.W_ZLIB);
         if (resultCode != JZlib.Z_OK) {
@@ -110,11 +151,11 @@ public class JZlibDecoder extends ZlibDecoder {
             final int oldNextInIndex = z.next_in_index;
 
             // Configure output.
-            ByteBuf decompressed = ctx.alloc().heapBuffer(inputLength << 1);
+            ByteBuf decompressed = prepareDecompressBuffer(ctx, null, inputLength << 1);
 
             try {
                 loop: for (;;) {
-                    decompressed.ensureWritable(z.avail_in << 1);
+                    decompressed = prepareDecompressBuffer(ctx, decompressed, z.avail_in << 1);
                     z.avail_out = decompressed.writableBytes();
                     z.next_out = decompressed.array();
                     z.next_out_index = decompressed.arrayOffset() + decompressed.writerIndex();
@@ -170,4 +211,9 @@ public class JZlibDecoder extends ZlibDecoder {
             z.next_out = null;
         }
     }
+
+    @Override
+    protected void decompressionBufferExhausted(ByteBuf buffer) {
+        finished = true;
+    }
 }
diff --git a/codec/src/main/java/io/netty/handler/codec/compression/JZlibEncoder.java b/codec/src/main/java/io/netty/handler/codec/compression/JZlibEncoder.java
index 3868bfb..882cac7 100644
--- a/codec/src/main/java/io/netty/handler/codec/compression/JZlibEncoder.java
+++ b/codec/src/main/java/io/netty/handler/codec/compression/JZlibEncoder.java
@@ -26,6 +26,7 @@ import io.netty.channel.ChannelPromise;
 import io.netty.channel.ChannelPromiseNotifier;
 import io.netty.util.concurrent.EventExecutor;
 import io.netty.util.internal.EmptyArrays;
+import io.netty.util.internal.ObjectUtil;
 
 import java.util.concurrent.TimeUnit;
 
@@ -130,9 +131,7 @@ public class JZlibEncoder extends ZlibEncoder {
             throw new IllegalArgumentException(
                     "memLevel: " + memLevel + " (expected: 1-9)");
         }
-        if (wrapper == null) {
-            throw new NullPointerException("wrapper");
-        }
+        ObjectUtil.checkNotNull(wrapper, "wrapper");
         if (wrapper == ZlibWrapper.ZLIB_OR_NONE) {
             throw new IllegalArgumentException(
                     "wrapper '" + ZlibWrapper.ZLIB_OR_NONE + "' is not " +
@@ -220,9 +219,8 @@ public class JZlibEncoder extends ZlibEncoder {
             throw new IllegalArgumentException(
                     "memLevel: " + memLevel + " (expected: 1-9)");
         }
-        if (dictionary == null) {
-            throw new NullPointerException("dictionary");
-        }
+        ObjectUtil.checkNotNull(dictionary, "dictionary");
+
         int resultCode;
         resultCode = z.deflateInit(
                 compressionLevel, windowBits, memLevel,
diff --git a/codec/src/main/java/io/netty/handler/codec/compression/JdkZlibDecoder.java b/codec/src/main/java/io/netty/handler/codec/compression/JdkZlibDecoder.java
index c90cc4b..7e69422 100644
--- a/codec/src/main/java/io/netty/handler/codec/compression/JdkZlibDecoder.java
+++ b/codec/src/main/java/io/netty/handler/codec/compression/JdkZlibDecoder.java
@@ -16,7 +16,9 @@
 package io.netty.handler.codec.compression;
 
 import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufAllocator;
 import io.netty.channel.ChannelHandlerContext;
+import io.netty.util.internal.ObjectUtil;
 
 import java.util.List;
 import java.util.zip.CRC32;
@@ -64,7 +66,19 @@ public class JdkZlibDecoder extends ZlibDecoder {
      * Creates a new instance with the default wrapper ({@link ZlibWrapper#ZLIB}).
      */
     public JdkZlibDecoder() {
-        this(ZlibWrapper.ZLIB, null, false);
+        this(ZlibWrapper.ZLIB, null, false, 0);
+    }
+
+    /**
+     * Creates a new instance with the default wrapper ({@link ZlibWrapper#ZLIB})
+     * and the specified maximum buffer allocation.
+     *
+     * @param maxAllocation
+     *          Maximum size of the decompression buffer. Must be &gt;= 0.
+     *          If zero, maximum size is decided by the {@link ByteBufAllocator}.
+     */
+    public JdkZlibDecoder(int maxAllocation) {
+        this(ZlibWrapper.ZLIB, null, false, maxAllocation);
     }
 
     /**
@@ -73,7 +87,20 @@ public class JdkZlibDecoder extends ZlibDecoder {
      * supports the preset dictionary.
      */
     public JdkZlibDecoder(byte[] dictionary) {
-        this(ZlibWrapper.ZLIB, dictionary, false);
+        this(ZlibWrapper.ZLIB, dictionary, false, 0);
+    }
+
+    /**
+     * Creates a new instance with the specified preset dictionary and maximum buffer allocation.
+     * The wrapper is always {@link ZlibWrapper#ZLIB} because it is the only format that
+     * supports the preset dictionary.
+     *
+     * @param maxAllocation
+     *          Maximum size of the decompression buffer. Must be &gt;= 0.
+     *          If zero, maximum size is decided by the {@link ByteBufAllocator}.
+     */
+    public JdkZlibDecoder(byte[] dictionary, int maxAllocation) {
+        this(ZlibWrapper.ZLIB, dictionary, false, maxAllocation);
     }
 
     /**
@@ -82,21 +109,43 @@ public class JdkZlibDecoder extends ZlibDecoder {
      * supported atm.
      */
     public JdkZlibDecoder(ZlibWrapper wrapper) {
-        this(wrapper, null, false);
+        this(wrapper, null, false, 0);
+    }
+
+    /**
+     * Creates a new instance with the specified wrapper and maximum buffer allocation.
+     * Be aware that only {@link ZlibWrapper#GZIP}, {@link ZlibWrapper#ZLIB} and {@link ZlibWrapper#NONE} are
+     * supported atm.
+     *
+     * @param maxAllocation
+     *          Maximum size of the decompression buffer. Must be &gt;= 0.
+     *          If zero, maximum size is decided by the {@link ByteBufAllocator}.
+     */
+    public JdkZlibDecoder(ZlibWrapper wrapper, int maxAllocation) {
+        this(wrapper, null, false, maxAllocation);
     }
 
     public JdkZlibDecoder(ZlibWrapper wrapper, boolean decompressConcatenated) {
-        this(wrapper, null, decompressConcatenated);
+        this(wrapper, null, decompressConcatenated, 0);
+    }
+
+    public JdkZlibDecoder(ZlibWrapper wrapper, boolean decompressConcatenated, int maxAllocation) {
+        this(wrapper, null, decompressConcatenated, maxAllocation);
     }
 
     public JdkZlibDecoder(boolean decompressConcatenated) {
-        this(ZlibWrapper.GZIP, null, decompressConcatenated);
+        this(ZlibWrapper.GZIP, null, decompressConcatenated, 0);
     }
 
-    private JdkZlibDecoder(ZlibWrapper wrapper, byte[] dictionary, boolean decompressConcatenated) {
-        if (wrapper == null) {
-            throw new NullPointerException("wrapper");
-        }
+    public JdkZlibDecoder(boolean decompressConcatenated, int maxAllocation) {
+        this(ZlibWrapper.GZIP, null, decompressConcatenated, maxAllocation);
+    }
+
+    private JdkZlibDecoder(ZlibWrapper wrapper, byte[] dictionary, boolean decompressConcatenated, int maxAllocation) {
+        super(maxAllocation);
+
+        ObjectUtil.checkNotNull(wrapper, "wrapper");
+
         this.decompressConcatenated = decompressConcatenated;
         switch (wrapper) {
             case GZIP:
@@ -177,7 +226,7 @@ public class JdkZlibDecoder extends ZlibDecoder {
             inflater.setInput(array);
         }
 
-        ByteBuf decompressed = ctx.alloc().heapBuffer(inflater.getRemaining() << 1);
+        ByteBuf decompressed = prepareDecompressBuffer(ctx, null, inflater.getRemaining() << 1);
         try {
             boolean readFooter = false;
             while (!inflater.needsInput()) {
@@ -208,7 +257,7 @@ public class JdkZlibDecoder extends ZlibDecoder {
                     }
                     break;
                 } else {
-                    decompressed.ensureWritable(inflater.getRemaining() << 1);
+                    decompressed = prepareDecompressBuffer(ctx, decompressed, inflater.getRemaining() << 1);
                 }
             }
 
@@ -238,6 +287,11 @@ public class JdkZlibDecoder extends ZlibDecoder {
         }
     }
 
+    @Override
+    protected void decompressionBufferExhausted(ByteBuf buffer) {
+        finished = true;
+    }
+
     @Override
     protected void handlerRemoved0(ChannelHandlerContext ctx) throws Exception {
         super.handlerRemoved0(ctx);
diff --git a/codec/src/main/java/io/netty/handler/codec/compression/JdkZlibEncoder.java b/codec/src/main/java/io/netty/handler/codec/compression/JdkZlibEncoder.java
index 276d7f8..f1b603f 100644
--- a/codec/src/main/java/io/netty/handler/codec/compression/JdkZlibEncoder.java
+++ b/codec/src/main/java/io/netty/handler/codec/compression/JdkZlibEncoder.java
@@ -22,6 +22,9 @@ import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelPromise;
 import io.netty.channel.ChannelPromiseNotifier;
 import io.netty.util.concurrent.EventExecutor;
+import io.netty.util.internal.ObjectUtil;
+import io.netty.util.internal.PlatformDependent;
+import io.netty.util.internal.SuppressJava6Requirement;
 
 import java.util.concurrent.TimeUnit;
 import java.util.zip.CRC32;
@@ -95,9 +98,7 @@ public class JdkZlibEncoder extends ZlibEncoder {
             throw new IllegalArgumentException(
                     "compressionLevel: " + compressionLevel + " (expected: 0-9)");
         }
-        if (wrapper == null) {
-            throw new NullPointerException("wrapper");
-        }
+        ObjectUtil.checkNotNull(wrapper, "wrapper");
         if (wrapper == ZlibWrapper.ZLIB_OR_NONE) {
             throw new IllegalArgumentException(
                     "wrapper '" + ZlibWrapper.ZLIB_OR_NONE + "' is not " +
@@ -141,9 +142,7 @@ public class JdkZlibEncoder extends ZlibEncoder {
             throw new IllegalArgumentException(
                     "compressionLevel: " + compressionLevel + " (expected: 0-9)");
         }
-        if (dictionary == null) {
-            throw new NullPointerException("dictionary");
-        }
+        ObjectUtil.checkNotNull(dictionary, "dictionary");
 
         wrapper = ZlibWrapper.ZLIB;
         deflater = new Deflater(compressionLevel);
@@ -320,7 +319,11 @@ public class JdkZlibEncoder extends ZlibEncoder {
         return ctx.writeAndFlush(footer, promise);
     }
 
+    @SuppressJava6Requirement(reason = "Usage guarded by java version check")
     private void deflate(ByteBuf out) {
+        if (PlatformDependent.javaVersion() < 7) {
+            deflateJdk6(out);
+        }
         int numBytes;
         do {
             int writerIndex = out.writerIndex();
@@ -330,6 +333,16 @@ public class JdkZlibEncoder extends ZlibEncoder {
         } while (numBytes > 0);
     }
 
+    private void deflateJdk6(ByteBuf out) {
+        int numBytes;
+        do {
+            int writerIndex = out.writerIndex();
+            numBytes = deflater.deflate(
+                    out.array(), out.arrayOffset() + writerIndex, out.writableBytes());
+            out.writerIndex(writerIndex + numBytes);
+        } while (numBytes > 0);
+    }
+
     @Override
     public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
         this.ctx = ctx;
diff --git a/codec/src/main/java/io/netty/handler/codec/compression/Lz4FrameDecoder.java b/codec/src/main/java/io/netty/handler/codec/compression/Lz4FrameDecoder.java
index addf105..92d6e74 100644
--- a/codec/src/main/java/io/netty/handler/codec/compression/Lz4FrameDecoder.java
+++ b/codec/src/main/java/io/netty/handler/codec/compression/Lz4FrameDecoder.java
@@ -18,10 +18,10 @@ package io.netty.handler.codec.compression;
 import io.netty.buffer.ByteBuf;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.handler.codec.ByteToMessageDecoder;
+import io.netty.util.internal.ObjectUtil;
 import net.jpountz.lz4.LZ4Exception;
 import net.jpountz.lz4.LZ4Factory;
 import net.jpountz.lz4.LZ4FastDecompressor;
-import net.jpountz.xxhash.XXHashFactory;
 
 import java.util.List;
 import java.util.zip.Checksum;
@@ -124,9 +124,7 @@ public class Lz4FrameDecoder extends ByteToMessageDecoder {
      *                           <a href="https://github.com/Cyan4973/xxHash">Github</a>.
      */
     public Lz4FrameDecoder(LZ4Factory factory, boolean validateChecksums) {
-        this(factory, validateChecksums ?
-                XXHashFactory.fastestInstance().newStreamingHash32(DEFAULT_SEED).asChecksum()
-              : null);
+        this(factory, validateChecksums ? new Lz4XXHash32(DEFAULT_SEED) : null);
     }
 
     /**
@@ -139,10 +137,7 @@ public class Lz4FrameDecoder extends ByteToMessageDecoder {
      *                  You may set {@code null} if you do not want to validate checksum of each block
      */
     public Lz4FrameDecoder(LZ4Factory factory, Checksum checksum) {
-        if (factory == null) {
-            throw new NullPointerException("factory");
-        }
-        decompressor = factory.fastDecompressor();
+        decompressor = ObjectUtil.checkNotNull(factory, "factory").fastDecompressor();
         this.checksum = checksum == null ? null : ByteBufChecksum.wrapChecksum(checksum);
     }
 
diff --git a/codec/src/main/java/io/netty/handler/codec/compression/Lz4FrameEncoder.java b/codec/src/main/java/io/netty/handler/codec/compression/Lz4FrameEncoder.java
index b94c893..d6a29a7 100644
--- a/codec/src/main/java/io/netty/handler/codec/compression/Lz4FrameEncoder.java
+++ b/codec/src/main/java/io/netty/handler/codec/compression/Lz4FrameEncoder.java
@@ -31,7 +31,6 @@ import io.netty.util.internal.ObjectUtil;
 import net.jpountz.lz4.LZ4Compressor;
 import net.jpountz.lz4.LZ4Exception;
 import net.jpountz.lz4.LZ4Factory;
-import net.jpountz.xxhash.XXHashFactory;
 
 import java.nio.ByteBuffer;
 import java.util.concurrent.TimeUnit;
@@ -50,7 +49,6 @@ import static io.netty.handler.codec.compression.Lz4Constants.MAGIC_NUMBER;
 import static io.netty.handler.codec.compression.Lz4Constants.MAX_BLOCK_SIZE;
 import static io.netty.handler.codec.compression.Lz4Constants.MIN_BLOCK_SIZE;
 import static io.netty.handler.codec.compression.Lz4Constants.TOKEN_OFFSET;
-import static io.netty.util.internal.ThrowableUtil.unknownStackTrace;
 
 /**
  * Compresses a {@link ByteBuf} using the LZ4 format.
@@ -69,9 +67,6 @@ import static io.netty.util.internal.ThrowableUtil.unknownStackTrace;
  *  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *     * * * * * * * * * *
  */
 public class Lz4FrameEncoder extends MessageToByteEncoder<ByteBuf> {
-    private static final EncoderException ENCODE_FINSHED_EXCEPTION = unknownStackTrace(new EncoderException(
-                    new IllegalStateException("encode finished and not enough space to write remaining data")),
-                    Lz4FrameEncoder.class, "encode");
     static final int DEFAULT_MAX_ENCODE_SIZE = Integer.MAX_VALUE;
 
     private final int blockSize;
@@ -129,8 +124,7 @@ public class Lz4FrameEncoder extends MessageToByteEncoder<ByteBuf> {
      *                        and is slower but compresses more efficiently
      */
     public Lz4FrameEncoder(boolean highCompressor) {
-        this(LZ4Factory.fastestInstance(), highCompressor, DEFAULT_BLOCK_SIZE,
-                XXHashFactory.fastestInstance().newStreamingHash32(DEFAULT_SEED).asChecksum());
+        this(LZ4Factory.fastestInstance(), highCompressor, DEFAULT_BLOCK_SIZE, new Lz4XXHash32(DEFAULT_SEED));
     }
 
     /**
@@ -164,12 +158,8 @@ public class Lz4FrameEncoder extends MessageToByteEncoder<ByteBuf> {
          */
     public Lz4FrameEncoder(LZ4Factory factory, boolean highCompressor, int blockSize,
                            Checksum checksum, int maxEncodeSize) {
-        if (factory == null) {
-            throw new NullPointerException("factory");
-        }
-        if (checksum == null) {
-            throw new NullPointerException("checksum");
-        }
+        ObjectUtil.checkNotNull(factory, "factory");
+        ObjectUtil.checkNotNull(checksum, "checksum");
 
         compressor = highCompressor ? factory.highCompressor() : factory.fastCompressor();
         this.checksum = ByteBufChecksum.wrapChecksum(checksum);
@@ -246,7 +236,7 @@ public class Lz4FrameEncoder extends MessageToByteEncoder<ByteBuf> {
         if (finished) {
             if (!out.isWritable(in.readableBytes())) {
                 // out should be EMPTY_BUFFER because we should have allocated enough space above in allocateBuffer.
-                throw ENCODE_FINSHED_EXCEPTION;
+                throw new IllegalStateException("encode finished and not enough space to write remaining data");
             }
             out.writeBytes(in);
             return;
diff --git a/codec/src/main/java/io/netty/handler/codec/compression/Lz4XXHash32.java b/codec/src/main/java/io/netty/handler/codec/compression/Lz4XXHash32.java
new file mode 100644
index 0000000..3d79f53
--- /dev/null
+++ b/codec/src/main/java/io/netty/handler/codec/compression/Lz4XXHash32.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2019 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.compression;
+
+import io.netty.buffer.ByteBuf;
+import net.jpountz.xxhash.StreamingXXHash32;
+import net.jpountz.xxhash.XXHash32;
+import net.jpountz.xxhash.XXHashFactory;
+
+import java.nio.ByteBuffer;
+import java.util.zip.Checksum;
+
+/**
+ * A special-purpose {@link ByteBufChecksum} implementation for use with
+ * {@link Lz4FrameEncoder} and {@link Lz4FrameDecoder}.
+ *
+ * {@link StreamingXXHash32#asChecksum()} has a particularly nasty implementation
+ * of {@link Checksum#update(int)} that allocates a single-element byte array for
+ * every invocation.
+ *
+ * In addition to that, it doesn't implement an overload that accepts a {@link ByteBuffer}
+ * as an argument.
+ *
+ * Combined, this means that we can't use {@code ReflectiveByteBufChecksum} at all,
+ * and can't use {@code SlowByteBufChecksum} because of its atrocious performance
+ * with direct byte buffers (allocating an array and making a JNI call for every byte
+ * checksummed might be considered sub-optimal by some).
+ *
+ * Block version of xxHash32 ({@link XXHash32}), however, does provide
+ * {@link XXHash32#hash(ByteBuffer, int)} method that is efficient and does exactly
+ * what we need, with a caveat that we can only invoke it once before having to reset.
+ * This, however, is fine for our purposes, given the way we use it in
+ * {@link Lz4FrameEncoder} and {@link Lz4FrameDecoder}:
+ * {@code reset()}, followed by one {@code update()}, followed by {@code getValue()}.
+ */
+public final class Lz4XXHash32 extends ByteBufChecksum {
+
+    private static final XXHash32 XXHASH32 = XXHashFactory.fastestInstance().hash32();
+
+    private final int seed;
+    private boolean used;
+    private int value;
+
+    @SuppressWarnings("WeakerAccess")
+    public Lz4XXHash32(int seed) {
+        this.seed = seed;
+    }
+
+    @Override
+    public void update(int b) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void update(byte[] b, int off, int len) {
+        if (used) {
+            throw new IllegalStateException();
+        }
+        value = XXHASH32.hash(b, off, len, seed);
+        used = true;
+    }
+
+    @Override
+    public void update(ByteBuf b, int off, int len) {
+        if (used) {
+            throw new IllegalStateException();
+        }
+        if (b.hasArray()) {
+            value = XXHASH32.hash(b.array(), b.arrayOffset() + off, len, seed);
+        } else {
+            value = XXHASH32.hash(CompressionUtil.safeNioBuffer(b, off, len), seed);
+        }
+        used = true;
+    }
+
+    @Override
+    public long getValue() {
+        if (!used) {
+            throw new IllegalStateException();
+        }
+        /*
+         * If you look carefully, you'll notice that the most significant nibble
+         * is being discarded; we believe this to be a bug, but this is what
+         * StreamingXXHash32#asChecksum() implementation of getValue() does,
+         * so we have to retain this behaviour for compatibility reasons.
+         */
+        return value & 0xFFFFFFFL;
+    }
+
+    @Override
+    public void reset() {
+        used = false;
+    }
+}
diff --git a/codec/src/main/java/io/netty/handler/codec/compression/LzfEncoder.java b/codec/src/main/java/io/netty/handler/codec/compression/LzfEncoder.java
index e541218..32ad151 100644
--- a/codec/src/main/java/io/netty/handler/codec/compression/LzfEncoder.java
+++ b/codec/src/main/java/io/netty/handler/codec/compression/LzfEncoder.java
@@ -17,27 +17,38 @@ package io.netty.handler.codec.compression;
 
 import com.ning.compress.BufferRecycler;
 import com.ning.compress.lzf.ChunkEncoder;
+import com.ning.compress.lzf.LZFChunk;
 import com.ning.compress.lzf.LZFEncoder;
 import com.ning.compress.lzf.util.ChunkEncoderFactory;
 import io.netty.buffer.ByteBuf;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.handler.codec.MessageToByteEncoder;
 
-import static com.ning.compress.lzf.LZFChunk.*;
+import static com.ning.compress.lzf.LZFChunk.MAX_CHUNK_LEN;
 
 /**
  * Compresses a {@link ByteBuf} using the LZF format.
- *
+ * <p>
  * See original <a href="http://oldhome.schmorp.de/marc/liblzf.html">LZF package</a>
  * and <a href="https://github.com/ning/compress/wiki/LZFFormat">LZF format</a> for full description.
  */
 public class LzfEncoder extends MessageToByteEncoder<ByteBuf> {
+
     /**
      * Minimum block size ready for compression. Blocks with length
      * less than {@link #MIN_BLOCK_TO_COMPRESS} will write as uncompressed.
      */
     private static final int MIN_BLOCK_TO_COMPRESS = 16;
 
+    /**
+     * Compress threshold for LZF format. When the amount of input data is less than compressThreshold,
+     * we will construct an uncompressed output according to the LZF format.
+     * <p>
+     * When the value is less than {@see ChunkEncoder#MIN_BLOCK_TO_COMPRESS}, since LZF will not compress data
+     * that is less than {@see ChunkEncoder#MIN_BLOCK_TO_COMPRESS}, compressThreshold will not work.
+     */
+    private final int compressThreshold;
+
     /**
      * Underlying decoder in use.
      */
@@ -55,29 +66,44 @@ public class LzfEncoder extends MessageToByteEncoder<ByteBuf> {
      * non-standard platforms it may be necessary to use {@link #LzfEncoder(boolean)} with {@code true} param.
      */
     public LzfEncoder() {
-        this(false, MAX_CHUNK_LEN);
+        this(false);
     }
 
     /**
      * Creates a new LZF encoder with specified encoding instance.
      *
-     * @param safeInstance
-     *        If {@code true} encoder will use {@link ChunkEncoder} that only uses standard JDK access methods,
-     *        and should work on all Java platforms and JVMs.
-     *        Otherwise encoder will try to use highly optimized {@link ChunkEncoder} implementation that uses
-     *        Sun JDK's {@link sun.misc.Unsafe} class (which may be included by other JDK's as well).
+     * @param safeInstance If {@code true} encoder will use {@link ChunkEncoder} that only uses
+     *                     standard JDK access methods, and should work on all Java platforms and JVMs.
+     *                     Otherwise encoder will try to use highly optimized {@link ChunkEncoder}
+     *                     implementation that uses Sun JDK's {@link sun.misc.Unsafe}
+     *                     class (which may be included by other JDK's as well).
      */
     public LzfEncoder(boolean safeInstance) {
         this(safeInstance, MAX_CHUNK_LEN);
     }
 
+    /**
+     * Creates a new LZF encoder with specified encoding instance and compressThreshold.
+     *
+     * @param safeInstance      If {@code true} encoder will use {@link ChunkEncoder} that only uses standard
+     *                          JDK access methods, and should work on all Java platforms and JVMs.
+     *                          Otherwise encoder will try to use highly optimized {@link ChunkEncoder}
+     *                          implementation that uses Sun JDK's {@link sun.misc.Unsafe}
+     *                          class (which may be included by other JDK's as well).
+     * @param totalLength       Expected total length of content to compress; only matters for outgoing messages
+     *                          that is smaller than maximum chunk size (64k), to optimize encoding hash tables.
+     */
+    public LzfEncoder(boolean safeInstance, int totalLength) {
+        this(safeInstance, totalLength, MIN_BLOCK_TO_COMPRESS);
+    }
+
     /**
      * Creates a new LZF encoder with specified total length of encoded chunk. You can configure it to encode
      * your data flow more efficient if you know the average size of messages that you send.
      *
-     * @param totalLength
-     *        Expected total length of content to compress; only matters for outgoing messages that is smaller
-     *        than maximum chunk size (64k), to optimize encoding hash tables.
+     * @param totalLength Expected total length of content to compress;
+     *                    only matters for outgoing messages that is smaller than maximum chunk size (64k),
+     *                    to optimize encoding hash tables.
      */
     public LzfEncoder(int totalLength) {
         this(false, totalLength);
@@ -86,27 +112,36 @@ public class LzfEncoder extends MessageToByteEncoder<ByteBuf> {
     /**
      * Creates a new LZF encoder with specified settings.
      *
-     * @param safeInstance
-     *        If {@code true} encoder will use {@link ChunkEncoder} that only uses standard JDK access methods,
-     *        and should work on all Java platforms and JVMs.
-     *        Otherwise encoder will try to use highly optimized {@link ChunkEncoder} implementation that uses
-     *        Sun JDK's {@link sun.misc.Unsafe} class (which may be included by other JDK's as well).
-     * @param totalLength
-     *        Expected total length of content to compress; only matters for outgoing messages that is smaller
-     *        than maximum chunk size (64k), to optimize encoding hash tables.
+     * @param safeInstance          If {@code true} encoder will use {@link ChunkEncoder} that only uses standard JDK
+     *                              access methods, and should work on all Java platforms and JVMs.
+     *                              Otherwise encoder will try to use highly optimized {@link ChunkEncoder}
+     *                              implementation that uses Sun JDK's {@link sun.misc.Unsafe}
+     *                              class (which may be included by other JDK's as well).
+     * @param totalLength           Expected total length of content to compress; only matters for outgoing messages
+     *                              that is smaller than maximum chunk size (64k), to optimize encoding hash tables.
+     * @param compressThreshold     Compress threshold for LZF format. When the amount of input data is less than
+     *                              compressThreshold, we will construct an uncompressed output according
+     *                              to the LZF format.
      */
-    public LzfEncoder(boolean safeInstance, int totalLength) {
+    public LzfEncoder(boolean safeInstance, int totalLength, int compressThreshold) {
         super(false);
         if (totalLength < MIN_BLOCK_TO_COMPRESS || totalLength > MAX_CHUNK_LEN) {
             throw new IllegalArgumentException("totalLength: " + totalLength +
                     " (expected: " + MIN_BLOCK_TO_COMPRESS + '-' + MAX_CHUNK_LEN + ')');
         }
 
-        encoder = safeInstance ?
+        if (compressThreshold < MIN_BLOCK_TO_COMPRESS) {
+            // not a suitable value.
+            throw new IllegalArgumentException("compressThreshold:" + compressThreshold +
+                    " expected >=" + MIN_BLOCK_TO_COMPRESS);
+        }
+        this.compressThreshold = compressThreshold;
+
+        this.encoder = safeInstance ?
                 ChunkEncoderFactory.safeNonAllocatingInstance(totalLength)
-              : ChunkEncoderFactory.optimalNonAllocatingInstance(totalLength);
+                : ChunkEncoderFactory.optimalNonAllocatingInstance(totalLength);
 
-        recycler = BufferRecycler.instance();
+        this.recycler = BufferRecycler.instance();
     }
 
     @Override
@@ -128,8 +163,16 @@ public class LzfEncoder extends MessageToByteEncoder<ByteBuf> {
         out.ensureWritable(maxOutputLength);
         final byte[] output = out.array();
         final int outputPtr = out.arrayOffset() + out.writerIndex();
-        final int outputLength = LZFEncoder.appendEncoded(encoder,
-                        input, inputPtr, length,  output, outputPtr) - outputPtr;
+
+        final int outputLength;
+        if (length >= compressThreshold) {
+            // compress.
+            outputLength = encodeCompress(input, inputPtr, length, output, outputPtr);
+        } else {
+            // not compress.
+            outputLength = encodeNonCompress(input, inputPtr, length, output, outputPtr);
+        }
+
         out.writerIndex(out.writerIndex() + outputLength);
         in.skipBytes(length);
 
@@ -137,4 +180,40 @@ public class LzfEncoder extends MessageToByteEncoder<ByteBuf> {
             recycler.releaseInputBuffer(input);
         }
     }
+
+    private int encodeCompress(byte[] input, int inputPtr, int length, byte[] output, int outputPtr) {
+        return LZFEncoder.appendEncoded(encoder,
+                input, inputPtr, length, output, outputPtr) - outputPtr;
+    }
+
+    private static int lzfEncodeNonCompress(byte[] input, int inputPtr, int length, byte[] output, int outputPtr) {
+        int left = length;
+        int chunkLen = Math.min(LZFChunk.MAX_CHUNK_LEN, left);
+        outputPtr = LZFChunk.appendNonCompressed(input, inputPtr, chunkLen, output, outputPtr);
+        left -= chunkLen;
+        if (left < 1) {
+            return outputPtr;
+        }
+        inputPtr += chunkLen;
+        do {
+            chunkLen = Math.min(left, LZFChunk.MAX_CHUNK_LEN);
+            outputPtr = LZFChunk.appendNonCompressed(input, inputPtr, chunkLen, output, outputPtr);
+            inputPtr += chunkLen;
+            left -= chunkLen;
+        } while (left > 0);
+        return outputPtr;
+    }
+
+    /**
+     * Use lzf uncompressed format to encode a piece of input.
+     */
+    private static int encodeNonCompress(byte[] input, int inputPtr, int length, byte[] output, int outputPtr) {
+        return lzfEncodeNonCompress(input, inputPtr, length, output, outputPtr) - outputPtr;
+    }
+
+    @Override
+    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
+        encoder.close();
+        super.handlerRemoved(ctx);
+    }
 }
diff --git a/codec/src/main/java/io/netty/handler/codec/compression/Snappy.java b/codec/src/main/java/io/netty/handler/codec/compression/Snappy.java
index 9264244..8e1825d 100644
--- a/codec/src/main/java/io/netty/handler/codec/compression/Snappy.java
+++ b/codec/src/main/java/io/netty/handler/codec/compression/Snappy.java
@@ -607,7 +607,7 @@ public final class Snappy {
         Crc32c crc32 = new Crc32c();
         try {
             crc32.update(data, offset, length);
-            return maskChecksum((int) crc32.getValue());
+            return maskChecksum(crc32.getValue());
         } finally {
             crc32.reset();
         }
@@ -655,7 +655,7 @@ public final class Snappy {
      * @param checksum The actual checksum of the data
      * @return The masked checksum
      */
-    static int maskChecksum(int checksum) {
-        return (checksum >> 15 | checksum << 17) + 0xa282ead8;
+    static int maskChecksum(long checksum) {
+        return (int) ((checksum >> 15 | checksum << 17) + 0xa282ead8);
     }
 }
diff --git a/codec/src/main/java/io/netty/handler/codec/compression/ZlibDecoder.java b/codec/src/main/java/io/netty/handler/codec/compression/ZlibDecoder.java
index d01bc6b..26fd3e7 100644
--- a/codec/src/main/java/io/netty/handler/codec/compression/ZlibDecoder.java
+++ b/codec/src/main/java/io/netty/handler/codec/compression/ZlibDecoder.java
@@ -16,6 +16,8 @@
 package io.netty.handler.codec.compression;
 
 import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufAllocator;
+import io.netty.channel.ChannelHandlerContext;
 import io.netty.handler.codec.ByteToMessageDecoder;
 
 /**
@@ -23,9 +25,72 @@ import io.netty.handler.codec.ByteToMessageDecoder;
  */
 public abstract class ZlibDecoder extends ByteToMessageDecoder {
 
+    /**
+     * Maximum allowed size of the decompression buffer.
+     */
+    protected final int maxAllocation;
+
+    /**
+     * Same as {@link #ZlibDecoder(int)} with maxAllocation = 0.
+     */
+    public ZlibDecoder() {
+        this(0);
+    }
+
+    /**
+     * Construct a new ZlibDecoder.
+     * @param maxAllocation
+     *          Maximum size of the decompression buffer. Must be &gt;= 0.
+     *          If zero, maximum size is decided by the {@link ByteBufAllocator}.
+     */
+    public ZlibDecoder(int maxAllocation) {
+        if (maxAllocation < 0) {
+            throw new IllegalArgumentException("maxAllocation must be >= 0");
+        }
+        this.maxAllocation = maxAllocation;
+    }
+
     /**
      * Returns {@code true} if and only if the end of the compressed stream
      * has been reached.
      */
     public abstract boolean isClosed();
+
+    /**
+     * Allocate or expand the decompression buffer, without exceeding the maximum allocation.
+     * Calls {@link #decompressionBufferExhausted(ByteBuf)} if the buffer is full and cannot be expanded further.
+     */
+    protected ByteBuf prepareDecompressBuffer(ChannelHandlerContext ctx, ByteBuf buffer, int preferredSize) {
+        if (buffer == null) {
+            if (maxAllocation == 0) {
+                return ctx.alloc().heapBuffer(preferredSize);
+            }
+
+            return ctx.alloc().heapBuffer(Math.min(preferredSize, maxAllocation), maxAllocation);
+        }
+
+        // this always expands the buffer if possible, even if the expansion is less than preferredSize
+        // we throw the exception only if the buffer could not be expanded at all
+        // this means that one final attempt to deserialize will always be made with the buffer at maxAllocation
+        if (buffer.ensureWritable(preferredSize, true) == 1) {
+            // buffer must be consumed so subclasses don't add it to output
+            // we therefore duplicate it when calling decompressionBufferExhausted() to guarantee non-interference
+            // but wait until after to consume it so the subclass can tell how much output is really in the buffer
+            decompressionBufferExhausted(buffer.duplicate());
+            buffer.skipBytes(buffer.readableBytes());
+            throw new DecompressionException("Decompression buffer has reached maximum size: " + buffer.maxCapacity());
+        }
+
+        return buffer;
+    }
+
+    /**
+     * Called when the decompression buffer cannot be expanded further.
+     * Default implementation is a no-op, but subclasses can override in case they want to
+     * do something before the {@link DecompressionException} is thrown, such as log the
+     * data that was decompressed so far.
+     */
+    protected void decompressionBufferExhausted(ByteBuf buffer) {
+    }
+
 }
diff --git a/codec/src/main/java/io/netty/handler/codec/protobuf/ProtobufDecoder.java b/codec/src/main/java/io/netty/handler/codec/protobuf/ProtobufDecoder.java
index 9ef56f1..a5dec53 100644
--- a/codec/src/main/java/io/netty/handler/codec/protobuf/ProtobufDecoder.java
+++ b/codec/src/main/java/io/netty/handler/codec/protobuf/ProtobufDecoder.java
@@ -28,6 +28,7 @@ import io.netty.handler.codec.ByteToMessageDecoder;
 import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
 import io.netty.handler.codec.LengthFieldPrepender;
 import io.netty.handler.codec.MessageToMessageDecoder;
+import io.netty.util.internal.ObjectUtil;
 
 import java.util.List;
 
@@ -95,10 +96,7 @@ public class ProtobufDecoder extends MessageToMessageDecoder<ByteBuf> {
     }
 
     public ProtobufDecoder(MessageLite prototype, ExtensionRegistryLite extensionRegistry) {
-        if (prototype == null) {
-            throw new NullPointerException("prototype");
-        }
-        this.prototype = prototype.getDefaultInstanceForType();
+        this.prototype = ObjectUtil.checkNotNull(prototype, "prototype").getDefaultInstanceForType();
         this.extensionRegistry = extensionRegistry;
     }
 
diff --git a/codec/src/main/java/io/netty/handler/codec/serialization/CompatibleObjectEncoder.java b/codec/src/main/java/io/netty/handler/codec/serialization/CompatibleObjectEncoder.java
index db97def..c56b499 100644
--- a/codec/src/main/java/io/netty/handler/codec/serialization/CompatibleObjectEncoder.java
+++ b/codec/src/main/java/io/netty/handler/codec/serialization/CompatibleObjectEncoder.java
@@ -19,8 +19,6 @@ import io.netty.buffer.ByteBuf;
 import io.netty.buffer.ByteBufOutputStream;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.handler.codec.MessageToByteEncoder;
-import io.netty.util.Attribute;
-import io.netty.util.AttributeKey;
 
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
diff --git a/codec/src/main/java/io/netty/handler/codec/serialization/ObjectDecoderInputStream.java b/codec/src/main/java/io/netty/handler/codec/serialization/ObjectDecoderInputStream.java
index ef705ae..8e117ab 100644
--- a/codec/src/main/java/io/netty/handler/codec/serialization/ObjectDecoderInputStream.java
+++ b/codec/src/main/java/io/netty/handler/codec/serialization/ObjectDecoderInputStream.java
@@ -15,6 +15,8 @@
  */
 package io.netty.handler.codec.serialization;
 
+import io.netty.util.internal.ObjectUtil;
+
 import java.io.BufferedReader;
 import java.io.DataInputStream;
 import java.io.IOException;
@@ -88,12 +90,9 @@ public class ObjectDecoderInputStream extends InputStream implements
      *        a {@link StreamCorruptedException} will be raised.
      */
     public ObjectDecoderInputStream(InputStream in, ClassLoader classLoader, int maxObjectSize) {
-        if (in == null) {
-            throw new NullPointerException("in");
-        }
-        if (maxObjectSize <= 0) {
-            throw new IllegalArgumentException("maxObjectSize: " + maxObjectSize);
-        }
+        ObjectUtil.checkNotNull(in, "in");
+        ObjectUtil.checkPositive(maxObjectSize, "maxObjectSize");
+
         if (in instanceof DataInputStream) {
             this.in = (DataInputStream) in;
         } else {
diff --git a/codec/src/main/java/io/netty/handler/codec/serialization/ObjectEncoderOutputStream.java b/codec/src/main/java/io/netty/handler/codec/serialization/ObjectEncoderOutputStream.java
index 769db41..76ec4a5 100644
--- a/codec/src/main/java/io/netty/handler/codec/serialization/ObjectEncoderOutputStream.java
+++ b/codec/src/main/java/io/netty/handler/codec/serialization/ObjectEncoderOutputStream.java
@@ -18,6 +18,7 @@ package io.netty.handler.codec.serialization;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.ByteBufOutputStream;
 import io.netty.buffer.Unpooled;
+import io.netty.util.internal.ObjectUtil;
 
 import java.io.DataOutputStream;
 import java.io.IOException;
@@ -63,12 +64,8 @@ public class ObjectEncoderOutputStream extends OutputStream implements
      *        cost, please specify the properly estimated value.
      */
     public ObjectEncoderOutputStream(OutputStream out, int estimatedLength) {
-        if (out == null) {
-            throw new NullPointerException("out");
-        }
-        if (estimatedLength < 0) {
-            throw new IllegalArgumentException("estimatedLength: " + estimatedLength);
-        }
+        ObjectUtil.checkNotNull(out, "out");
+        ObjectUtil.checkPositiveOrZero(estimatedLength, "estimatedLength");
 
         if (out instanceof DataOutputStream) {
             this.out = (DataOutputStream) out;
diff --git a/codec/src/main/java/io/netty/handler/codec/string/StringDecoder.java b/codec/src/main/java/io/netty/handler/codec/string/StringDecoder.java
index 0f219ae..5b68950 100644
--- a/codec/src/main/java/io/netty/handler/codec/string/StringDecoder.java
+++ b/codec/src/main/java/io/netty/handler/codec/string/StringDecoder.java
@@ -23,6 +23,7 @@ import io.netty.handler.codec.ByteToMessageDecoder;
 import io.netty.handler.codec.DelimiterBasedFrameDecoder;
 import io.netty.handler.codec.LineBasedFrameDecoder;
 import io.netty.handler.codec.MessageToMessageDecoder;
+import io.netty.util.internal.ObjectUtil;
 
 import java.nio.charset.Charset;
 import java.util.List;
@@ -68,10 +69,7 @@ public class StringDecoder extends MessageToMessageDecoder<ByteBuf> {
      * Creates a new instance with the specified character set.
      */
     public StringDecoder(Charset charset) {
-        if (charset == null) {
-            throw new NullPointerException("charset");
-        }
-        this.charset = charset;
+        this.charset = ObjectUtil.checkNotNull(charset, "charset");
     }
 
     @Override
diff --git a/codec/src/main/java/io/netty/handler/codec/string/StringEncoder.java b/codec/src/main/java/io/netty/handler/codec/string/StringEncoder.java
index 9177fae..a04ac85 100644
--- a/codec/src/main/java/io/netty/handler/codec/string/StringEncoder.java
+++ b/codec/src/main/java/io/netty/handler/codec/string/StringEncoder.java
@@ -22,6 +22,7 @@ import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelPipeline;
 import io.netty.handler.codec.LineBasedFrameDecoder;
 import io.netty.handler.codec.MessageToMessageEncoder;
+import io.netty.util.internal.ObjectUtil;
 
 import java.nio.CharBuffer;
 import java.nio.charset.Charset;
@@ -51,7 +52,6 @@ import java.util.List;
 @Sharable
 public class StringEncoder extends MessageToMessageEncoder<CharSequence> {
 
-    // TODO Use CharsetEncoder instead.
     private final Charset charset;
 
     /**
@@ -65,10 +65,7 @@ public class StringEncoder extends MessageToMessageEncoder<CharSequence> {
      * Creates a new instance with the specified character set.
      */
     public StringEncoder(Charset charset) {
-        if (charset == null) {
-            throw new NullPointerException("charset");
-        }
-        this.charset = charset;
+        this.charset = ObjectUtil.checkNotNull(charset, "charset");
     }
 
     @Override
diff --git a/codec/src/test/java/io/netty/handler/codec/ByteToMessageCodecTest.java b/codec/src/test/java/io/netty/handler/codec/ByteToMessageCodecTest.java
index 556bd24..c837758 100644
--- a/codec/src/test/java/io/netty/handler/codec/ByteToMessageCodecTest.java
+++ b/codec/src/test/java/io/netty/handler/codec/ByteToMessageCodecTest.java
@@ -64,7 +64,7 @@ public class ByteToMessageCodecTest {
         assertTrue(ch.finish());
         assertEquals(1, ch.readInbound());
 
-        ByteBuf buf = (ByteBuf) ch.readInbound();
+        ByteBuf buf = ch.readInbound();
         assertEquals(Unpooled.wrappedBuffer(new byte[]{'0'}), buf);
         buf.release();
         assertNull(ch.readInbound());
diff --git a/codec/src/test/java/io/netty/handler/codec/ByteToMessageDecoderTest.java b/codec/src/test/java/io/netty/handler/codec/ByteToMessageDecoderTest.java
index 93dd221..3db8022 100644
--- a/codec/src/test/java/io/netty/handler/codec/ByteToMessageDecoderTest.java
+++ b/codec/src/test/java/io/netty/handler/codec/ByteToMessageDecoderTest.java
@@ -15,13 +15,16 @@
  */
 package io.netty.handler.codec;
 
+import io.netty.buffer.AbstractByteBufAllocator;
 import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufAllocator;
 import io.netty.buffer.CompositeByteBuf;
 import io.netty.buffer.Unpooled;
 import io.netty.buffer.UnpooledByteBufAllocator;
 import io.netty.buffer.UnpooledHeapByteBuf;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.channel.ChannelOutboundHandlerAdapter;
 import io.netty.channel.embedded.EmbeddedChannel;
 import io.netty.util.internal.PlatformDependent;
 import org.junit.Test;
@@ -30,7 +33,13 @@ import java.util.List;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.LinkedBlockingDeque;
 
-import static org.junit.Assert.*;
+import static io.netty.buffer.Unpooled.wrappedBuffer;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 public class ByteToMessageDecoderTest {
 
@@ -245,7 +254,7 @@ public class ByteToMessageDecoderTest {
         byte[] bytes = new byte[1024];
         PlatformDependent.threadLocalRandom().nextBytes(bytes);
 
-        assertTrue(channel.writeInbound(Unpooled.wrappedBuffer(bytes)));
+        assertTrue(channel.writeInbound(Unpooled.copiedBuffer(bytes)));
         assertBuffer(Unpooled.wrappedBuffer(bytes), (ByteBuf) channel.readInbound());
         assertNull(channel.readInbound());
         assertFalse(channel.finish());
@@ -277,7 +286,7 @@ public class ByteToMessageDecoderTest {
         byte[] bytes = new byte[1024];
         PlatformDependent.threadLocalRandom().nextBytes(bytes);
 
-        assertTrue(channel.writeInbound(Unpooled.wrappedBuffer(bytes)));
+        assertTrue(channel.writeInbound(Unpooled.copiedBuffer(bytes)));
         assertBuffer(Unpooled.wrappedBuffer(bytes, 0, bytes.length - 1), (ByteBuf) channel.readInbound());
         assertNull(channel.readInbound());
         assertTrue(channel.finish());
@@ -306,23 +315,95 @@ public class ByteToMessageDecoderTest {
         assertFalse(channel.finish());
     }
 
+    static class WriteFailingByteBuf extends UnpooledHeapByteBuf {
+        private final Error error = new Error();
+        private int untilFailure;
+
+        WriteFailingByteBuf(int untilFailure, int capacity) {
+            super(UnpooledByteBufAllocator.DEFAULT, capacity, capacity);
+            this.untilFailure = untilFailure;
+        }
+
+        @Override
+        public ByteBuf setBytes(int index, ByteBuf src, int srcIndex, int length) {
+            if (--untilFailure <= 0) {
+                throw error;
+            }
+            return super.setBytes(index, src, srcIndex, length);
+        }
+
+        Error writeError() {
+            return error;
+        }
+    }
+
     @Test
     public void releaseWhenMergeCumulateThrows() {
-        final Error error = new Error();
+        WriteFailingByteBuf oldCumulation = new WriteFailingByteBuf(1, 64);
+        oldCumulation.writeZero(1);
+        ByteBuf in = Unpooled.buffer().writeZero(12);
+
+        Throwable thrown = null;
+        try {
+            ByteToMessageDecoder.MERGE_CUMULATOR.cumulate(UnpooledByteBufAllocator.DEFAULT, oldCumulation, in);
+        } catch (Throwable t) {
+            thrown = t;
+        }
+
+        assertSame(oldCumulation.writeError(), thrown);
+        assertEquals(0, in.refCnt());
+        assertEquals(1, oldCumulation.refCnt());
+        oldCumulation.release();
+    }
+
+    @Test
+    public void releaseWhenMergeCumulateThrowsInExpand() {
+        releaseWhenMergeCumulateThrowsInExpand(1, true);
+        releaseWhenMergeCumulateThrowsInExpand(2, true);
+        releaseWhenMergeCumulateThrowsInExpand(3, false); // sentinel test case
+    }
+
+    private void releaseWhenMergeCumulateThrowsInExpand(int untilFailure, boolean shouldFail) {
+        ByteBuf oldCumulation = UnpooledByteBufAllocator.DEFAULT.heapBuffer(8, 8).writeZero(1);
+        final WriteFailingByteBuf newCumulation = new WriteFailingByteBuf(untilFailure, 16);
 
-        ByteBuf cumulation = new UnpooledHeapByteBuf(UnpooledByteBufAllocator.DEFAULT, 0, 64) {
+        ByteBufAllocator allocator = new AbstractByteBufAllocator(false) {
             @Override
-            public ByteBuf writeBytes(ByteBuf src) {
-                throw error;
+            public boolean isDirectBufferPooled() {
+                return false;
+            }
+
+            @Override
+            protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) {
+                return newCumulation;
+            }
+
+            @Override
+            protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
+                throw new UnsupportedOperationException();
             }
         };
+
         ByteBuf in = Unpooled.buffer().writeZero(12);
+        Throwable thrown = null;
         try {
-            ByteToMessageDecoder.MERGE_CUMULATOR.cumulate(UnpooledByteBufAllocator.DEFAULT, cumulation, in);
-            fail();
-        } catch (Error expected) {
-            assertSame(error, expected);
-            assertEquals(0, in.refCnt());
+            ByteToMessageDecoder.MERGE_CUMULATOR.cumulate(allocator, oldCumulation, in);
+        } catch (Throwable t) {
+            thrown = t;
+        }
+
+        assertEquals(0, in.refCnt());
+
+        if (shouldFail) {
+            assertSame(newCumulation.writeError(), thrown);
+            assertEquals(1, oldCumulation.refCnt());
+            oldCumulation.release();
+            assertEquals(0, newCumulation.refCnt());
+        } else {
+            assertNull(thrown);
+            assertEquals(0, oldCumulation.refCnt());
+            assertEquals(1, newCumulation.refCnt());
+            newCumulation.release();
         }
     }
 
@@ -335,7 +416,11 @@ public class ByteToMessageDecoderTest {
             public CompositeByteBuf addComponent(boolean increaseWriterIndex, ByteBuf buffer) {
                 throw error;
             }
-        };
+            @Override
+            public CompositeByteBuf addFlattenedComponents(boolean increaseWriterIndex, ByteBuf buffer) {
+                throw error;
+            }
+        }.writeZero(1);
         ByteBuf in = Unpooled.buffer().writeZero(12);
         try {
             ByteToMessageDecoder.COMPOSITE_CUMULATOR.cumulate(UnpooledByteBufAllocator.DEFAULT, cumulation, in);
@@ -343,6 +428,86 @@ public class ByteToMessageDecoderTest {
         } catch (Error expected) {
             assertSame(error, expected);
             assertEquals(0, in.refCnt());
+            cumulation.release();
         }
     }
+
+    @Test
+    public void testDoesNotOverRead() {
+        class ReadInterceptingHandler extends ChannelOutboundHandlerAdapter {
+            private int readsTriggered;
+
+            @Override
+            public void read(ChannelHandlerContext ctx) throws Exception {
+                readsTriggered++;
+                super.read(ctx);
+            }
+        }
+        ReadInterceptingHandler interceptor = new ReadInterceptingHandler();
+
+        EmbeddedChannel channel = new EmbeddedChannel();
+        channel.config().setAutoRead(false);
+        channel.pipeline().addLast(interceptor, new FixedLengthFrameDecoder(3));
+        assertEquals(0, interceptor.readsTriggered);
+
+        // 0 complete frames, 1 partial frame: SHOULD trigger a read
+        channel.writeInbound(wrappedBuffer(new byte[] { 0, 1 }));
+        assertEquals(1, interceptor.readsTriggered);
+
+        // 2 complete frames, 0 partial frames: should NOT trigger a read
+        channel.writeInbound(wrappedBuffer(new byte[] { 2 }), wrappedBuffer(new byte[] { 3, 4, 5 }));
+        assertEquals(1, interceptor.readsTriggered);
+
+        // 1 complete frame, 1 partial frame: should NOT trigger a read
+        channel.writeInbound(wrappedBuffer(new byte[] { 6, 7, 8 }), wrappedBuffer(new byte[] { 9 }));
+        assertEquals(1, interceptor.readsTriggered);
+
+        // 1 complete frame, 1 partial frame: should NOT trigger a read
+        channel.writeInbound(wrappedBuffer(new byte[] { 10, 11 }), wrappedBuffer(new byte[] { 12 }));
+        assertEquals(1, interceptor.readsTriggered);
+
+        // 0 complete frames, 1 partial frame: SHOULD trigger a read
+        channel.writeInbound(wrappedBuffer(new byte[] { 13 }));
+        assertEquals(2, interceptor.readsTriggered);
+
+        // 1 complete frame, 0 partial frames: should NOT trigger a read
+        channel.writeInbound(wrappedBuffer(new byte[] { 14 }));
+        assertEquals(2, interceptor.readsTriggered);
+
+        for (int i = 0; i < 5; i++) {
+            ByteBuf read = channel.readInbound();
+            assertEquals(i * 3 + 0, read.getByte(0));
+            assertEquals(i * 3 + 1, read.getByte(1));
+            assertEquals(i * 3 + 2, read.getByte(2));
+            read.release();
+        }
+        assertFalse(channel.finish());
+    }
+
+    @Test
+    public void testDisorder() {
+        ByteToMessageDecoder decoder = new ByteToMessageDecoder() {
+            int count;
+
+            //read 4 byte then remove this decoder
+            @Override
+            protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
+                out.add(in.readByte());
+                if (++count >= 4) {
+                    ctx.pipeline().remove(this);
+                }
+            }
+        };
+        EmbeddedChannel channel = new EmbeddedChannel(decoder);
+        assertTrue(channel.writeInbound(Unpooled.wrappedBuffer(new byte[]{1, 2, 3, 4, 5})));
+        assertEquals((byte) 1,  channel.readInbound());
+        assertEquals((byte) 2,  channel.readInbound());
+        assertEquals((byte) 3,  channel.readInbound());
+        assertEquals((byte) 4,  channel.readInbound());
+        ByteBuf buffer5 = channel.readInbound();
+        assertEquals((byte) 5, buffer5.readByte());
+        assertFalse(buffer5.isReadable());
+        assertTrue(buffer5.release());
+        assertFalse(channel.finish());
+    }
 }
diff --git a/codec/src/test/java/io/netty/handler/codec/DatagramPacketEncoderTest.java b/codec/src/test/java/io/netty/handler/codec/DatagramPacketEncoderTest.java
index fdf1cac..be200bf 100644
--- a/codec/src/test/java/io/netty/handler/codec/DatagramPacketEncoderTest.java
+++ b/codec/src/test/java/io/netty/handler/codec/DatagramPacketEncoderTest.java
@@ -51,8 +51,17 @@ public class DatagramPacketEncoderTest {
 
     @Test
     public void testEncode() {
+        testEncode(false);
+    }
+
+    @Test
+    public void testEncodeWithSenderIsNull() {
+        testEncode(true);
+    }
+
+    private void testEncode(boolean senderIsNull) {
         InetSocketAddress recipient = SocketUtils.socketAddress("127.0.0.1", 10000);
-        InetSocketAddress sender = SocketUtils.socketAddress("127.0.0.1", 20000);
+        InetSocketAddress sender = senderIsNull ? null : SocketUtils.socketAddress("127.0.0.1", 20000);
         assertTrue(channel.writeOutbound(
                 new DefaultAddressedEnvelope<String, InetSocketAddress>("netty", recipient, sender)));
         DatagramPacket packet = channel.readOutbound();
diff --git a/codec/src/test/java/io/netty/handler/codec/DateFormatterTest.java b/codec/src/test/java/io/netty/handler/codec/DateFormatterTest.java
index 99d1d38..e833fdf 100644
--- a/codec/src/test/java/io/netty/handler/codec/DateFormatterTest.java
+++ b/codec/src/test/java/io/netty/handler/codec/DateFormatterTest.java
@@ -17,6 +17,7 @@ package io.netty.handler.codec;
 
 import org.junit.Test;
 
+import java.util.Calendar;
 import java.util.Date;
 
 import static org.junit.Assert.*;
@@ -111,4 +112,26 @@ public class DateFormatterTest {
     public void testFormat() {
         assertEquals("Sun, 6 Nov 1994 08:49:37 GMT", format(DATE));
     }
+
+    @Test
+    public void testParseAllMonths() {
+        assertEquals(Calendar.JANUARY, getMonth(parseHttpDate("Sun, 6 Jan 1994 08:49:37 GMT")));
+        assertEquals(Calendar.FEBRUARY, getMonth(parseHttpDate("Sun, 6 Feb 1994 08:49:37 GMT")));
+        assertEquals(Calendar.MARCH, getMonth(parseHttpDate("Sun, 6 Mar 1994 08:49:37 GMT")));
+        assertEquals(Calendar.APRIL, getMonth(parseHttpDate("Sun, 6 Apr 1994 08:49:37 GMT")));
+        assertEquals(Calendar.MAY, getMonth(parseHttpDate("Sun, 6 May 1994 08:49:37 GMT")));
+        assertEquals(Calendar.JUNE, getMonth(parseHttpDate("Sun, 6 Jun 1994 08:49:37 GMT")));
+        assertEquals(Calendar.JULY, getMonth(parseHttpDate("Sun, 6 Jul 1994 08:49:37 GMT")));
+        assertEquals(Calendar.AUGUST, getMonth(parseHttpDate("Sun, 6 Aug 1994 08:49:37 GMT")));
+        assertEquals(Calendar.SEPTEMBER, getMonth(parseHttpDate("Sun, 6 Sep 1994 08:49:37 GMT")));
+        assertEquals(Calendar.OCTOBER, getMonth(parseHttpDate("Sun Oct 6 08:49:37 1994")));
+        assertEquals(Calendar.NOVEMBER, getMonth(parseHttpDate("Sun Nov 6 08:49:37 1994")));
+        assertEquals(Calendar.DECEMBER, getMonth(parseHttpDate("Sun Dec 6 08:49:37 1994")));
+    }
+
+    private static int getMonth(Date referenceDate) {
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(referenceDate);
+        return cal.get(Calendar.MONTH);
+    }
 }
diff --git a/codec/src/test/java/io/netty/handler/codec/DefaultHeadersTest.java b/codec/src/test/java/io/netty/handler/codec/DefaultHeadersTest.java
index 073872b..6139b46 100644
--- a/codec/src/test/java/io/netty/handler/codec/DefaultHeadersTest.java
+++ b/codec/src/test/java/io/netty/handler/codec/DefaultHeadersTest.java
@@ -41,11 +41,11 @@ public class DefaultHeadersTest {
 
     private static final class TestDefaultHeaders extends
             DefaultHeaders<CharSequence, CharSequence, TestDefaultHeaders> {
-        public TestDefaultHeaders() {
+        TestDefaultHeaders() {
             this(CharSequenceValueConverter.INSTANCE);
         }
 
-        public TestDefaultHeaders(ValueConverter<CharSequence> converter) {
+        TestDefaultHeaders(ValueConverter<CharSequence> converter) {
             super(converter);
         }
     }
@@ -109,21 +109,85 @@ public class DefaultHeadersTest {
         assertTrue(values.containsAll(asList(of("value1"), of("value2"), of("value3"))));
     }
 
+    @Test
+    public void multipleValuesPerNameIteratorWithOtherNames() {
+        TestDefaultHeaders headers = newInstance();
+        headers.add(of("name1"), of("value1"));
+        headers.add(of("name1"), of("value2"));
+        headers.add(of("name2"), of("value4"));
+        headers.add(of("name1"), of("value3"));
+        assertEquals(4, headers.size());
+
+        List<CharSequence> values = new ArrayList<CharSequence>();
+        Iterator<CharSequence> itr = headers.valueIterator(of("name1"));
+        while (itr.hasNext()) {
+            values.add(itr.next());
+            itr.remove();
+        }
+        assertEquals(3, values.size());
+        assertEquals(1, headers.size());
+        assertFalse(headers.isEmpty());
+        assertTrue(values.containsAll(asList(of("value1"), of("value2"), of("value3"))));
+        itr = headers.valueIterator(of("name1"));
+        assertFalse(itr.hasNext());
+        itr = headers.valueIterator(of("name2"));
+        assertTrue(itr.hasNext());
+        assertEquals(of("value4"), itr.next());
+        assertFalse(itr.hasNext());
+    }
+
     @Test
     public void multipleValuesPerNameIterator() {
+        TestDefaultHeaders headers = newInstance();
+        headers.add(of("name1"), of("value1"));
+        headers.add(of("name1"), of("value2"));
+        assertEquals(2, headers.size());
+
+        List<CharSequence> values = new ArrayList<CharSequence>();
+        Iterator<CharSequence> itr = headers.valueIterator(of("name1"));
+        while (itr.hasNext()) {
+            values.add(itr.next());
+            itr.remove();
+        }
+        assertEquals(2, values.size());
+        assertEquals(0, headers.size());
+        assertTrue(headers.isEmpty());
+        assertTrue(values.containsAll(asList(of("value1"), of("value2"))));
+        itr = headers.valueIterator(of("name1"));
+        assertFalse(itr.hasNext());
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void valuesItrRemoveThrowsWhenEmpty() {
+        TestDefaultHeaders headers = newInstance();
+        assertEquals(0, headers.size());
+        assertTrue(headers.isEmpty());
+        Iterator<CharSequence> itr = headers.valueIterator(of("name"));
+        itr.remove();
+    }
+
+    @Test
+    public void valuesItrRemoveThrowsAfterLastElement() {
         TestDefaultHeaders headers = newInstance();
         headers.add(of("name"), of("value1"));
-        headers.add(of("name"), of("value2"));
-        headers.add(of("name"), of("value3"));
-        assertEquals(3, headers.size());
+        assertEquals(1, headers.size());
 
         List<CharSequence> values = new ArrayList<CharSequence>();
         Iterator<CharSequence> itr = headers.valueIterator(of("name"));
         while (itr.hasNext()) {
             values.add(itr.next());
+            itr.remove();
+        }
+        assertEquals(1, values.size());
+        assertEquals(0, headers.size());
+        assertTrue(headers.isEmpty());
+        assertTrue(values.contains(of("value1")));
+        try {
+            itr.remove();
+            fail();
+        } catch (IllegalStateException ignored) {
+            // ignored
         }
-        assertEquals(3, values.size());
-        assertTrue(values.containsAll(asList(of("value1"), of("value2"), of("value3"))));
     }
 
     @Test
diff --git a/codec/src/test/java/io/netty/handler/codec/base64/Base64Test.java b/codec/src/test/java/io/netty/handler/codec/base64/Base64Test.java
index b6cb66f..d3e548c 100644
--- a/codec/src/test/java/io/netty/handler/codec/base64/Base64Test.java
+++ b/codec/src/test/java/io/netty/handler/codec/base64/Base64Test.java
@@ -29,7 +29,7 @@ import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
 
 import static io.netty.buffer.Unpooled.copiedBuffer;
-import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.*;
 
 public class Base64Test {
 
@@ -94,7 +94,7 @@ public class Base64Test {
                 "8i96YWK0VxcCMQC7pf6Wk3RhUU2Sg6S9e6CiirFLDyzLkaWxuCnXcOwTvuXTHUQSeUCp2Q6ygS5q\n" +
                 "Kyc=";
 
-        ByteBuf src =  Unpooled.wrappedBuffer(certFromString(cert).getEncoded());
+        ByteBuf src = Unpooled.wrappedBuffer(certFromString(cert).getEncoded());
         ByteBuf expectedEncoded = copiedBuffer(expected, CharsetUtil.US_ASCII);
         testEncode(src, expectedEncoded);
     }
@@ -169,4 +169,20 @@ public class Base64Test {
     public void testOverflowDecodedBufferSize() {
         assertEquals(1610612736, Base64.decodedBufferSize(Integer.MAX_VALUE));
     }
+
+    @Test
+    public void decodingFailsOnInvalidInputByte() {
+        char[] invalidChars = {'\u007F', '\u0080', '\u00BD', '\u00FF'};
+        for (char invalidChar : invalidChars) {
+            ByteBuf buf = copiedBuffer("eHh4" + invalidChar, CharsetUtil.ISO_8859_1);
+            try {
+                Base64.decode(buf);
+                fail("Invalid character in not detected: " + invalidChar);
+            } catch (IllegalArgumentException ignored) {
+                // as expected
+            } finally {
+                assertTrue(buf.release());
+            }
+        }
+    }
 }
diff --git a/codec/src/test/java/io/netty/handler/codec/compression/ByteBufChecksumTest.java b/codec/src/test/java/io/netty/handler/codec/compression/ByteBufChecksumTest.java
new file mode 100644
index 0000000..d2df38b
--- /dev/null
+++ b/codec/src/test/java/io/netty/handler/codec/compression/ByteBufChecksumTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2019 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.compression;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import net.jpountz.xxhash.XXHashFactory;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.util.Random;
+import java.util.zip.Adler32;
+import java.util.zip.CRC32;
+import java.util.zip.Checksum;
+
+import static io.netty.handler.codec.compression.Lz4Constants.DEFAULT_SEED;
+import static org.junit.Assert.*;
+
+public class ByteBufChecksumTest {
+
+    private static final byte[] BYTE_ARRAY = new byte[1024];
+
+    @BeforeClass
+    public static void setUp() {
+        new Random().nextBytes(BYTE_ARRAY);
+    }
+
+    @Test
+    public void testHeapByteBufUpdate() {
+        testUpdate(Unpooled.wrappedBuffer(BYTE_ARRAY));
+    }
+
+    @Test
+    public void testDirectByteBufUpdate() {
+        ByteBuf buf = Unpooled.directBuffer(BYTE_ARRAY.length);
+        buf.writeBytes(BYTE_ARRAY);
+        testUpdate(buf);
+    }
+
+    private static void testUpdate(ByteBuf buf) {
+        try {
+            // all variations of xxHash32: slow and naive, optimised, wrapped optimised;
+            // the last two should be literally identical, but it's best to guard against
+            // an accidental regression in ByteBufChecksum#wrapChecksum(Checksum)
+            testUpdate(xxHash32(DEFAULT_SEED), ByteBufChecksum.wrapChecksum(xxHash32(DEFAULT_SEED)), buf);
+            testUpdate(xxHash32(DEFAULT_SEED), new Lz4XXHash32(DEFAULT_SEED), buf);
+            testUpdate(xxHash32(DEFAULT_SEED), ByteBufChecksum.wrapChecksum(new Lz4XXHash32(DEFAULT_SEED)), buf);
+
+            // CRC32 and Adler32, special-cased to use ReflectiveByteBufChecksum
+            testUpdate(new CRC32(), ByteBufChecksum.wrapChecksum(new CRC32()), buf);
+            testUpdate(new Adler32(), ByteBufChecksum.wrapChecksum(new Adler32()), buf);
+        } finally {
+            buf.release();
+        }
+    }
+
+    private static void testUpdate(Checksum checksum, ByteBufChecksum wrapped, ByteBuf buf) {
+        testUpdate(checksum, wrapped, buf, 0, BYTE_ARRAY.length);
+        testUpdate(checksum, wrapped, buf, 0, BYTE_ARRAY.length - 1);
+        testUpdate(checksum, wrapped, buf, 1, BYTE_ARRAY.length - 1);
+        testUpdate(checksum, wrapped, buf, 1, BYTE_ARRAY.length - 2);
+    }
+
+    private static void testUpdate(Checksum checksum, ByteBufChecksum wrapped, ByteBuf buf, int off, int len) {
+        checksum.reset();
+        wrapped.reset();
+
+        checksum.update(BYTE_ARRAY, off, len);
+        wrapped.update(buf, off, len);
+
+        assertEquals(checksum.getValue(), wrapped.getValue());
+    }
+
+    private static Checksum xxHash32(int seed) {
+        return XXHashFactory.fastestInstance().newStreamingHash32(seed).asChecksum();
+    }
+}
diff --git a/codec/src/test/java/io/netty/handler/codec/compression/JZlibTest.java b/codec/src/test/java/io/netty/handler/codec/compression/JZlibTest.java
index 28f3919..015559e 100644
--- a/codec/src/test/java/io/netty/handler/codec/compression/JZlibTest.java
+++ b/codec/src/test/java/io/netty/handler/codec/compression/JZlibTest.java
@@ -23,7 +23,7 @@ public class JZlibTest extends ZlibTest {
     }
 
     @Override
-    protected ZlibDecoder createDecoder(ZlibWrapper wrapper) {
-        return new JZlibDecoder(wrapper);
+    protected ZlibDecoder createDecoder(ZlibWrapper wrapper, int maxAllocation) {
+        return new JZlibDecoder(wrapper, maxAllocation);
     }
 }
diff --git a/codec/src/test/java/io/netty/handler/codec/compression/JdkZlibTest.java b/codec/src/test/java/io/netty/handler/codec/compression/JdkZlibTest.java
index 54a48a9..5ff19f1 100644
--- a/codec/src/test/java/io/netty/handler/codec/compression/JdkZlibTest.java
+++ b/codec/src/test/java/io/netty/handler/codec/compression/JdkZlibTest.java
@@ -38,8 +38,8 @@ public class JdkZlibTest extends ZlibTest {
     }
 
     @Override
-    protected ZlibDecoder createDecoder(ZlibWrapper wrapper) {
-        return new JdkZlibDecoder(wrapper);
+    protected ZlibDecoder createDecoder(ZlibWrapper wrapper, int maxAllocation) {
+        return new JdkZlibDecoder(wrapper, maxAllocation);
     }
 
     @Test(expected = DecompressionException.class)
diff --git a/codec/src/test/java/io/netty/handler/codec/compression/LengthAwareLzfIntegrationTest.java b/codec/src/test/java/io/netty/handler/codec/compression/LengthAwareLzfIntegrationTest.java
new file mode 100644
index 0000000..4230447
--- /dev/null
+++ b/codec/src/test/java/io/netty/handler/codec/compression/LengthAwareLzfIntegrationTest.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2020 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.compression;
+
+import io.netty.channel.embedded.EmbeddedChannel;
+
+import static com.ning.compress.lzf.LZFChunk.MAX_CHUNK_LEN;
+
+public class LengthAwareLzfIntegrationTest extends LzfIntegrationTest {
+
+    @Override
+    protected EmbeddedChannel createEncoder() {
+        return new EmbeddedChannel(new LzfEncoder(false, MAX_CHUNK_LEN, 2 * 1024 * 1024));
+    }
+}
diff --git a/codec/src/test/java/io/netty/handler/codec/compression/SnappyFrameDecoderTest.java b/codec/src/test/java/io/netty/handler/codec/compression/SnappyFrameDecoderTest.java
index 474a695..6b31076 100644
--- a/codec/src/test/java/io/netty/handler/codec/compression/SnappyFrameDecoderTest.java
+++ b/codec/src/test/java/io/netty/handler/codec/compression/SnappyFrameDecoderTest.java
@@ -18,6 +18,7 @@ package io.netty.handler.codec.compression;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
 import io.netty.channel.embedded.EmbeddedChannel;
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -31,6 +32,11 @@ public class SnappyFrameDecoderTest {
         channel = new EmbeddedChannel(new SnappyFrameDecoder());
     }
 
+    @After
+    public void tearDown() {
+        assertFalse(channel.finishAndReleaseAll());
+    }
+
     @Test(expected = DecompressionException.class)
     public void testReservedUnskippableChunkTypeCausesError() throws Exception {
         ByteBuf in = Unpooled.wrappedBuffer(new byte[] {
@@ -92,7 +98,7 @@ public class SnappyFrameDecoderTest {
            -0x7f, 0x05, 0x00, 0x00, 'n', 'e', 't', 't', 'y'
         });
 
-        channel.writeInbound(in);
+        assertFalse(channel.writeInbound(in));
         assertNull(channel.readInbound());
 
         assertFalse(in.isReadable());
@@ -105,7 +111,7 @@ public class SnappyFrameDecoderTest {
             0x01, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 'n', 'e', 't', 't', 'y'
         });
 
-        channel.writeInbound(in);
+        assertTrue(channel.writeInbound(in));
 
         ByteBuf expected = Unpooled.wrappedBuffer(new byte[] { 'n', 'e', 't', 't', 'y' });
         ByteBuf actual = channel.readInbound();
@@ -125,7 +131,7 @@ public class SnappyFrameDecoderTest {
                   0x6e, 0x65, 0x74, 0x74, 0x79 // "netty"
         });
 
-        channel.writeInbound(in);
+        assertTrue(channel.writeInbound(in));
 
         ByteBuf expected = Unpooled.wrappedBuffer(new byte[] { 'n', 'e', 't', 't', 'y' });
         ByteBuf actual = channel.readInbound();
@@ -142,26 +148,38 @@ public class SnappyFrameDecoderTest {
     @Test(expected = DecompressionException.class)
     public void testInvalidChecksumThrowsException() throws Exception {
         EmbeddedChannel channel = new EmbeddedChannel(new SnappyFrameDecoder(true));
-
-        // checksum here is presented as 0
-        ByteBuf in = Unpooled.wrappedBuffer(new byte[] {
-           (byte) 0xff, 0x06, 0x00, 0x00, 0x73, 0x4e, 0x61, 0x50, 0x70, 0x59,
-            0x01, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 'n', 'e', 't', 't', 'y'
-        });
-
-        channel.writeInbound(in);
+        try {
+            // checksum here is presented as 0
+            ByteBuf in = Unpooled.wrappedBuffer(new byte[]{
+                    (byte) 0xff, 0x06, 0x00, 0x00, 0x73, 0x4e, 0x61, 0x50, 0x70, 0x59,
+                    0x01, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 'n', 'e', 't', 't', 'y'
+            });
+
+            channel.writeInbound(in);
+        } finally {
+            channel.finishAndReleaseAll();
+        }
     }
 
     @Test
     public void testInvalidChecksumDoesNotThrowException() throws Exception {
         EmbeddedChannel channel = new EmbeddedChannel(new SnappyFrameDecoder(true));
-
-        // checksum here is presented as a282986f (little endian)
-        ByteBuf in = Unpooled.wrappedBuffer(new byte[] {
-           (byte) 0xff, 0x06, 0x00, 0x00, 0x73, 0x4e, 0x61, 0x50, 0x70, 0x59,
-            0x01, 0x09, 0x00, 0x00, 0x6f, -0x68, -0x7e, -0x5e, 'n', 'e', 't', 't', 'y'
-        });
-
-        channel.writeInbound(in);
+        try {
+            // checksum here is presented as a282986f (little endian)
+            ByteBuf in = Unpooled.wrappedBuffer(new byte[]{
+                    (byte) 0xff, 0x06, 0x00, 0x00, 0x73, 0x4e, 0x61, 0x50, 0x70, 0x59,
+                    0x01, 0x09, 0x00, 0x00, 0x6f, -0x68, 0x2e, -0x47, 'n', 'e', 't', 't', 'y'
+            });
+
+            assertTrue(channel.writeInbound(in));
+            ByteBuf expected = Unpooled.wrappedBuffer(new byte[] { 'n', 'e', 't', 't', 'y' });
+            ByteBuf actual = channel.readInbound();
+            assertEquals(expected, actual);
+
+            expected.release();
+            actual.release();
+        } finally {
+            channel.finishAndReleaseAll();
+        }
     }
 }
diff --git a/codec/src/test/java/io/netty/handler/codec/compression/SnappyFrameEncoderTest.java b/codec/src/test/java/io/netty/handler/codec/compression/SnappyFrameEncoderTest.java
index 7b61be1..78a046a 100644
--- a/codec/src/test/java/io/netty/handler/codec/compression/SnappyFrameEncoderTest.java
+++ b/codec/src/test/java/io/netty/handler/codec/compression/SnappyFrameEncoderTest.java
@@ -40,10 +40,9 @@ public class SnappyFrameEncoderTest {
 
         channel.writeOutbound(in);
         assertTrue(channel.finish());
-
         ByteBuf expected = Unpooled.wrappedBuffer(new byte[] {
             (byte) 0xff, 0x06, 0x00, 0x00, 0x73, 0x4e, 0x61, 0x50, 0x70, 0x59,
-             0x01, 0x09, 0x00, 0x00, 0x6f, -0x68, -0x7e, -0x5e, 'n', 'e', 't', 't', 'y'
+             0x01, 0x09, 0x00, 0x00, 0x6f, -0x68, 0x2e, -0x47, 'n', 'e', 't', 't', 'y'
         });
         ByteBuf actual = channel.readOutbound();
         assertEquals(expected, actual);
@@ -89,8 +88,8 @@ public class SnappyFrameEncoderTest {
 
         ByteBuf expected = Unpooled.wrappedBuffer(new byte[] {
             (byte) 0xff, 0x06, 0x00, 0x00, 0x73, 0x4e, 0x61, 0x50, 0x70, 0x59,
-             0x01, 0x09, 0x00, 0x00, 0x6f, -0x68, -0x7e, -0x5e, 'n', 'e', 't', 't', 'y',
-             0x01, 0x09, 0x00, 0x00, 0x6f, -0x68, -0x7e, -0x5e, 'n', 'e', 't', 't', 'y',
+             0x01, 0x09, 0x00, 0x00, 0x6f, -0x68, 0x2e, -0x47, 'n', 'e', 't', 't', 'y',
+             0x01, 0x09, 0x00, 0x00, 0x6f, -0x68, 0x2e, -0x47, 'n', 'e', 't', 't', 'y',
         });
 
         CompositeByteBuf actual = Unpooled.compositeBuffer();
diff --git a/codec/src/test/java/io/netty/handler/codec/compression/SnappyTest.java b/codec/src/test/java/io/netty/handler/codec/compression/SnappyTest.java
index 115deef..1f2206a 100644
--- a/codec/src/test/java/io/netty/handler/codec/compression/SnappyTest.java
+++ b/codec/src/test/java/io/netty/handler/codec/compression/SnappyTest.java
@@ -234,7 +234,19 @@ public class SnappyTest {
         ByteBuf input = Unpooled.wrappedBuffer(new byte[] {
                 'n', 'e', 't', 't', 'y'
         });
-        assertEquals(maskChecksum(0xd6cb8b55), calculateChecksum(input));
+
+        assertEquals(maskChecksum(0xd6cb8b55L), calculateChecksum(input));
+        input.release();
+    }
+
+    @Test
+    public void testMaskChecksum() {
+        ByteBuf input = Unpooled.wrappedBuffer(new byte[] {
+                0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00,
+                0x5f, 0x68, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65,
+                0x61, 0x74, 0x5f,
+        });
+        assertEquals(0x44a4301f, calculateChecksum(input));
         input.release();
     }
 
diff --git a/codec/src/test/java/io/netty/handler/codec/compression/ZlibCrossTest1.java b/codec/src/test/java/io/netty/handler/codec/compression/ZlibCrossTest1.java
index 9e16e1a..3c31274 100644
--- a/codec/src/test/java/io/netty/handler/codec/compression/ZlibCrossTest1.java
+++ b/codec/src/test/java/io/netty/handler/codec/compression/ZlibCrossTest1.java
@@ -23,7 +23,7 @@ public class ZlibCrossTest1 extends ZlibTest {
     }
 
     @Override
-    protected ZlibDecoder createDecoder(ZlibWrapper wrapper) {
-        return new JZlibDecoder(wrapper);
+    protected ZlibDecoder createDecoder(ZlibWrapper wrapper, int maxAllocation) {
+        return new JZlibDecoder(wrapper, maxAllocation);
     }
 }
diff --git a/codec/src/test/java/io/netty/handler/codec/compression/ZlibCrossTest2.java b/codec/src/test/java/io/netty/handler/codec/compression/ZlibCrossTest2.java
index 8717019..00c6e18 100644
--- a/codec/src/test/java/io/netty/handler/codec/compression/ZlibCrossTest2.java
+++ b/codec/src/test/java/io/netty/handler/codec/compression/ZlibCrossTest2.java
@@ -25,8 +25,8 @@ public class ZlibCrossTest2 extends ZlibTest {
     }
 
     @Override
-    protected ZlibDecoder createDecoder(ZlibWrapper wrapper) {
-        return new JdkZlibDecoder(wrapper);
+    protected ZlibDecoder createDecoder(ZlibWrapper wrapper, int maxAllocation) {
+        return new JdkZlibDecoder(wrapper, maxAllocation);
     }
 
     @Test(expected = DecompressionException.class)
diff --git a/codec/src/test/java/io/netty/handler/codec/compression/ZlibTest.java b/codec/src/test/java/io/netty/handler/codec/compression/ZlibTest.java
index 7c25ec4..5e9d128 100644
--- a/codec/src/test/java/io/netty/handler/codec/compression/ZlibTest.java
+++ b/codec/src/test/java/io/netty/handler/codec/compression/ZlibTest.java
@@ -15,7 +15,9 @@
  */
 package io.netty.handler.codec.compression;
 
+import io.netty.buffer.AbstractByteBufAllocator;
 import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufAllocator;
 import io.netty.buffer.ByteBufInputStream;
 import io.netty.buffer.Unpooled;
 import io.netty.channel.embedded.EmbeddedChannel;
@@ -88,8 +90,12 @@ public abstract class ZlibTest {
         rand.nextBytes(BYTES_LARGE);
     }
 
+    protected ZlibDecoder createDecoder(ZlibWrapper wrapper) {
+        return createDecoder(wrapper, 0);
+    }
+
     protected abstract ZlibEncoder createEncoder(ZlibWrapper wrapper);
-    protected abstract ZlibDecoder createDecoder(ZlibWrapper wrapper);
+    protected abstract ZlibDecoder createDecoder(ZlibWrapper wrapper, int maxAllocation);
 
     @Test
     public void testGZIP2() throws Exception {
@@ -223,7 +229,7 @@ public abstract class ZlibTest {
     // Test for https://github.com/netty/netty/issues/2572
     private void testDecompressOnly(ZlibWrapper decoderWrapper, byte[] compressed, byte[] data) throws Exception {
         EmbeddedChannel chDecoder = new EmbeddedChannel(createDecoder(decoderWrapper));
-        chDecoder.writeInbound(Unpooled.wrappedBuffer(compressed));
+        chDecoder.writeInbound(Unpooled.copiedBuffer(compressed));
         assertTrue(chDecoder.finish());
 
         ByteBuf decoded = Unpooled.buffer(data.length);
@@ -236,7 +242,7 @@ public abstract class ZlibTest {
             decoded.writeBytes(buf);
             buf.release();
         }
-        assertEquals(Unpooled.wrappedBuffer(data), decoded);
+        assertEquals(Unpooled.copiedBuffer(data), decoded);
         decoded.release();
     }
 
@@ -345,6 +351,25 @@ public abstract class ZlibTest {
         testCompressLarge(ZlibWrapper.GZIP, ZlibWrapper.ZLIB_OR_NONE);
     }
 
+    @Test
+    public void testMaxAllocation() throws Exception {
+        int maxAllocation = 1024;
+        ZlibDecoder decoder = createDecoder(ZlibWrapper.ZLIB, maxAllocation);
+        EmbeddedChannel chDecoder = new EmbeddedChannel(decoder);
+        TestByteBufAllocator alloc = new TestByteBufAllocator(chDecoder.alloc());
+        chDecoder.config().setAllocator(alloc);
+
+        try {
+            chDecoder.writeInbound(Unpooled.wrappedBuffer(deflate(BYTES_LARGE)));
+            fail("decompressed size > maxAllocation, so should have thrown exception");
+        } catch (DecompressionException e) {
+            assertTrue(e.getMessage().startsWith("Decompression buffer has reached maximum size"));
+            assertEquals(maxAllocation, alloc.getMaxAllocation());
+            assertTrue(decoder.isClosed());
+            assertFalse(chDecoder.finish());
+        }
+    }
+
     private static byte[] gzip(byte[] bytes) throws IOException {
         ByteArrayOutputStream out = new ByteArrayOutputStream();
         GZIPOutputStream stream = new GZIPOutputStream(out);
@@ -360,4 +385,34 @@ public abstract class ZlibTest {
         stream.close();
         return out.toByteArray();
     }
+
+    private static final class TestByteBufAllocator extends AbstractByteBufAllocator {
+        private ByteBufAllocator wrapped;
+        private int maxAllocation;
+
+        TestByteBufAllocator(ByteBufAllocator wrapped) {
+            this.wrapped = wrapped;
+        }
+
+        public int getMaxAllocation() {
+            return maxAllocation;
+        }
+
+        @Override
+        public boolean isDirectBufferPooled() {
+            return wrapped.isDirectBufferPooled();
+        }
+
+        @Override
+        protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) {
+            maxAllocation = Math.max(maxAllocation, maxCapacity);
+            return wrapped.heapBuffer(initialCapacity, maxCapacity);
+        }
+
+        @Override
+        protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
+            maxAllocation = Math.max(maxAllocation, maxCapacity);
+            return wrapped.directBuffer(initialCapacity, maxCapacity);
+        }
+    }
 }
diff --git a/codec/src/test/java/io/netty/handler/codec/serialization/CompatibleObjectEncoderTest.java b/codec/src/test/java/io/netty/handler/codec/serialization/CompatibleObjectEncoderTest.java
index 238b91e..60142eb 100644
--- a/codec/src/test/java/io/netty/handler/codec/serialization/CompatibleObjectEncoderTest.java
+++ b/codec/src/test/java/io/netty/handler/codec/serialization/CompatibleObjectEncoderTest.java
@@ -26,7 +26,6 @@ import java.io.Serializable;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
 
 public class CompatibleObjectEncoderTest {
     @Test
diff --git a/codec/src/test/resources/io/netty/handler/codec/xml/sample-04.xml b/codec/src/test/resources/io/netty/handler/codec/xml/sample-04.xml
index 8f50d30..4550d31 100644
--- a/codec/src/test/resources/io/netty/handler/codec/xml/sample-04.xml
+++ b/codec/src/test/resources/io/netty/handler/codec/xml/sample-04.xml
@@ -29,7 +29,7 @@
   <version>4.0.14.Final-SNAPSHOT</version>
 
   <name>Netty</name>
-  <url>http://netty.io/</url>
+  <url>https://netty.io/</url>
   <description>
     Netty is an asynchronous event-driven network application framework for 
     rapid development of maintainable high performance protocol servers and
@@ -38,7 +38,7 @@
 
   <organization>
     <name>The Netty Project</name>
-    <url>http://netty.io/</url>
+    <url>https://netty.io/</url>
   </organization>
 
   <licenses>
@@ -61,9 +61,9 @@
       <id>netty.io</id>
       <name>The Netty Project Contributors</name>
       <email>netty@googlegroups.com</email>
-      <url>http://netty.io/</url>
+      <url>https://netty.io/</url>
       <organization>The Netty Project</organization>
-      <organizationUrl>http://netty.io/</organizationUrl>
+      <organizationUrl>https://netty.io/</organizationUrl>
     </developer>
   </developers>
 
diff --git a/common/pom.xml b/common/pom.xml
index 3d8aa99..618d160 100644
--- a/common/pom.xml
+++ b/common/pom.xml
@@ -21,7 +21,7 @@
   <parent>
     <groupId>io.netty</groupId>
     <artifactId>netty-parent</artifactId>
-    <version>4.1.33.Final</version>
+    <version>4.1.48.Final</version>
   </parent>
 
   <artifactId>netty-common</artifactId>
@@ -38,6 +38,13 @@
   </properties>
 
   <dependencies>
+    <dependency>
+      <groupId>com.oracle.substratevm</groupId>
+      <artifactId>svm</artifactId>
+      <version>${graalvm.version}</version>
+      <!-- Provided scope as it is only needed for compiling the SVM substitution classes -->
+      <scope>provided</scope>
+    </dependency>
     <dependency>
       <groupId>org.jctools</groupId>
       <artifactId>jctools-core</artifactId>
@@ -71,6 +78,11 @@
       <artifactId>log4j-core</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>io.projectreactor.tools</groupId>
+      <artifactId>blockhound</artifactId>
+      <optional>true</optional>
+    </dependency>
     <dependency>
       <groupId>org.mockito</groupId>
       <artifactId>mockito-core</artifactId>
@@ -109,6 +121,8 @@
                 </relocation>
               </relocations>
               <minimizeJar>true</minimizeJar>
+              <createSourcesJar>true</createSourcesJar>
+              <shadeSourcesContent>true</shadeSourcesContent>
             </configuration>
           </execution>
         </executions>
diff --git a/common/src/main/java/io/netty/util/AbstractReferenceCounted.java b/common/src/main/java/io/netty/util/AbstractReferenceCounted.java
index b7480c4..b5ffb4f 100644
--- a/common/src/main/java/io/netty/util/AbstractReferenceCounted.java
+++ b/common/src/main/java/io/netty/util/AbstractReferenceCounted.java
@@ -15,85 +15,55 @@
  */
 package io.netty.util;
 
-import static io.netty.util.internal.ObjectUtil.checkPositive;
-
 import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
 
-import io.netty.util.internal.PlatformDependent;
+import io.netty.util.internal.ReferenceCountUpdater;
 
 /**
  * Abstract base class for classes wants to implement {@link ReferenceCounted}.
  */
 public abstract class AbstractReferenceCounted implements ReferenceCounted {
-    private static final long REFCNT_FIELD_OFFSET;
-    private static final AtomicIntegerFieldUpdater<AbstractReferenceCounted> refCntUpdater =
+    private static final long REFCNT_FIELD_OFFSET =
+            ReferenceCountUpdater.getUnsafeOffset(AbstractReferenceCounted.class, "refCnt");
+    private static final AtomicIntegerFieldUpdater<AbstractReferenceCounted> AIF_UPDATER =
             AtomicIntegerFieldUpdater.newUpdater(AbstractReferenceCounted.class, "refCnt");
 
-    // even => "real" refcount is (refCnt >>> 1); odd => "real" refcount is 0
-    @SuppressWarnings("unused")
-    private volatile int refCnt = 2;
-
-    static {
-        long refCntFieldOffset = -1;
-        try {
-            if (PlatformDependent.hasUnsafe()) {
-                refCntFieldOffset = PlatformDependent.objectFieldOffset(
-                        AbstractReferenceCounted.class.getDeclaredField("refCnt"));
-            }
-        } catch (Throwable ignore) {
-            refCntFieldOffset = -1;
+    private static final ReferenceCountUpdater<AbstractReferenceCounted> updater =
+            new ReferenceCountUpdater<AbstractReferenceCounted>() {
+        @Override
+        protected AtomicIntegerFieldUpdater<AbstractReferenceCounted> updater() {
+            return AIF_UPDATER;
         }
+        @Override
+        protected long unsafeOffset() {
+            return REFCNT_FIELD_OFFSET;
+        }
+    };
 
-        REFCNT_FIELD_OFFSET = refCntFieldOffset;
-    }
-
-    private static int realRefCnt(int rawCnt) {
-        return (rawCnt & 1) != 0 ? 0 : rawCnt >>> 1;
-    }
-
-    private int nonVolatileRawCnt() {
-        // TODO: Once we compile against later versions of Java we can replace the Unsafe usage here by varhandles.
-        return REFCNT_FIELD_OFFSET != -1 ? PlatformDependent.getInt(this, REFCNT_FIELD_OFFSET)
-                : refCntUpdater.get(this);
-    }
+    // Value might not equal "real" reference count, all access should be via the updater
+    @SuppressWarnings("unused")
+    private volatile int refCnt = updater.initialValue();
 
     @Override
     public int refCnt() {
-        return realRefCnt(refCntUpdater.get(this));
+        return updater.refCnt(this);
     }
 
     /**
      * An unsafe operation intended for use by a subclass that sets the reference count of the buffer directly
      */
-    protected final void setRefCnt(int newRefCnt) {
-        refCntUpdater.set(this, newRefCnt << 1); // overflow OK here
+    protected final void setRefCnt(int refCnt) {
+        updater.setRefCnt(this, refCnt);
     }
 
     @Override
     public ReferenceCounted retain() {
-        return retain0(1);
+        return updater.retain(this);
     }
 
     @Override
     public ReferenceCounted retain(int increment) {
-        return retain0(checkPositive(increment, "increment"));
-    }
-
-    private ReferenceCounted retain0(final int increment) {
-        // all changes to the raw count are 2x the "real" change
-        int adjustedIncrement = increment << 1; // overflow OK here
-        int oldRef = refCntUpdater.getAndAdd(this, adjustedIncrement);
-        if ((oldRef & 1) != 0) {
-            throw new IllegalReferenceCountException(0, increment);
-        }
-        // don't pass 0!
-        if ((oldRef <= 0 && oldRef + adjustedIncrement >= 0)
-                || (oldRef >= 0 && oldRef + adjustedIncrement < oldRef)) {
-            // overflow case
-            refCntUpdater.getAndAdd(this, -adjustedIncrement);
-            throw new IllegalReferenceCountException(realRefCnt(oldRef), increment);
-        }
-        return this;
+        return updater.retain(this, increment);
     }
 
     @Override
@@ -103,64 +73,19 @@ public abstract class AbstractReferenceCounted implements ReferenceCounted {
 
     @Override
     public boolean release() {
-        return release0(1);
+        return handleRelease(updater.release(this));
     }
 
     @Override
     public boolean release(int decrement) {
-        return release0(checkPositive(decrement, "decrement"));
-    }
-
-    private boolean release0(int decrement) {
-        int rawCnt = nonVolatileRawCnt(), realCnt = toLiveRealCnt(rawCnt, decrement);
-        if (decrement == realCnt) {
-            if (refCntUpdater.compareAndSet(this, rawCnt, 1)) {
-                deallocate();
-                return true;
-            }
-            return retryRelease0(decrement);
-        }
-        return releaseNonFinal0(decrement, rawCnt, realCnt);
-    }
-
-    private boolean releaseNonFinal0(int decrement, int rawCnt, int realCnt) {
-        if (decrement < realCnt
-                // all changes to the raw count are 2x the "real" change
-                && refCntUpdater.compareAndSet(this, rawCnt, rawCnt - (decrement << 1))) {
-            return false;
-        }
-        return retryRelease0(decrement);
-    }
-
-    private boolean retryRelease0(int decrement) {
-        for (;;) {
-            int rawCnt = refCntUpdater.get(this), realCnt = toLiveRealCnt(rawCnt, decrement);
-            if (decrement == realCnt) {
-                if (refCntUpdater.compareAndSet(this, rawCnt, 1)) {
-                    deallocate();
-                    return true;
-                }
-            } else if (decrement < realCnt) {
-                // all changes to the raw count are 2x the "real" change
-                if (refCntUpdater.compareAndSet(this, rawCnt, rawCnt - (decrement << 1))) {
-                    return false;
-                }
-            } else {
-                throw new IllegalReferenceCountException(realCnt, -decrement);
-            }
-            Thread.yield(); // this benefits throughput under high contention
-        }
+        return handleRelease(updater.release(this, decrement));
     }
 
-    /**
-     * Like {@link #realRefCnt(int)} but throws if refCnt == 0
-     */
-    private static int toLiveRealCnt(int rawCnt, int decrement) {
-        if ((rawCnt & 1) == 0) {
-            return rawCnt >>> 1;
+    private boolean handleRelease(boolean result) {
+        if (result) {
+            deallocate();
         }
-        // odd rawCnt => already deallocated
-        throw new IllegalReferenceCountException(0, -decrement);
+        return result;
     }
 
     /**
diff --git a/common/src/main/java/io/netty/util/AsciiString.java b/common/src/main/java/io/netty/util/AsciiString.java
index f537469..0154137 100644
--- a/common/src/main/java/io/netty/util/AsciiString.java
+++ b/common/src/main/java/io/netty/util/AsciiString.java
@@ -17,6 +17,7 @@ package io.netty.util;
 
 import io.netty.util.internal.EmptyArrays;
 import io.netty.util.internal.InternalThreadLocalMap;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.PlatformDependent;
 
 import java.nio.ByteBuffer;
@@ -477,7 +478,7 @@ public final class AsciiString implements CharSequence, Comparable<CharSequence>
             return this;
         }
 
-        if (string.getClass() == AsciiString.class) {
+        if (string instanceof AsciiString) {
             AsciiString that = (AsciiString) string;
             if (isEmpty()) {
                 return that;
@@ -522,13 +523,17 @@ public final class AsciiString implements CharSequence, Comparable<CharSequence>
      * @return {@code true} if the specified string is equal to this string, {@code false} otherwise.
      */
     public boolean contentEqualsIgnoreCase(CharSequence string) {
+        if (this == string) {
+            return true;
+        }
+
         if (string == null || string.length() != length()) {
             return false;
         }
 
-        if (string.getClass() == AsciiString.class) {
+        if (string instanceof AsciiString) {
             AsciiString rhs = (AsciiString) string;
-            for (int i = arrayOffset(), j = rhs.arrayOffset(); i < length(); ++i, ++j) {
+            for (int i = arrayOffset(), j = rhs.arrayOffset(), end = i + length(); i < end; ++i, ++j) {
                 if (!equalsIgnoreCase(value[i], rhs.value[j])) {
                     return false;
                 }
@@ -536,7 +541,7 @@ public final class AsciiString implements CharSequence, Comparable<CharSequence>
             return true;
         }
 
-        for (int i = arrayOffset(), j = 0; i < length(); ++i, ++j) {
+        for (int i = arrayOffset(), j = 0, end = length(); j < end; ++i, ++j) {
             if (!equalsIgnoreCase(b2c(value[i]), string.charAt(j))) {
                 return false;
             }
@@ -585,9 +590,7 @@ public final class AsciiString implements CharSequence, Comparable<CharSequence>
      * @param length the number of characters to copy.
      */
     public void copy(int srcIdx, char[] dst, int dstIdx, int length) {
-        if (dst == null) {
-            throw new NullPointerException("dst");
-        }
+        ObjectUtil.checkNotNull(dst, "dst");
 
         if (isOutOfBounds(srcIdx, length, length())) {
             throw new IndexOutOfBoundsException("expected: " + "0 <= srcIdx(" + srcIdx + ") <= srcIdx + length("
@@ -742,7 +745,7 @@ public final class AsciiString implements CharSequence, Comparable<CharSequence>
      */
     public int lastIndexOf(CharSequence string) {
         // Use count instead of count - 1 so lastIndexOf("") answers count
-        return lastIndexOf(string, length());
+        return lastIndexOf(string, length);
     }
 
     /**
@@ -757,23 +760,20 @@ public final class AsciiString implements CharSequence, Comparable<CharSequence>
      */
     public int lastIndexOf(CharSequence subString, int start) {
         final int subCount = subString.length();
+        start = Math.min(start, length - subCount);
         if (start < 0) {
-            start = 0;
-        }
-        if (subCount <= 0) {
-            return start < length ? start : length;
-        }
-        if (subCount > length - start) {
             return INDEX_NOT_FOUND;
         }
+        if (subCount == 0) {
+            return start;
+        }
 
         final char firstChar = subString.charAt(0);
         if (firstChar > MAX_CHAR_VALUE) {
             return INDEX_NOT_FOUND;
         }
         final byte firstCharAsByte = c2b0(firstChar);
-        final int end = offset + start;
-        for (int i = offset + length - subCount; i >= end; --i) {
+        for (int i = offset + start; i >= 0; --i) {
             if (value[i] == firstCharAsByte) {
                 int o1 = i, o2 = 0;
                 while (++o2 < subCount && b2c(value[++o1]) == subString.charAt(o2)) {
@@ -799,9 +799,7 @@ public final class AsciiString implements CharSequence, Comparable<CharSequence>
      * @throws NullPointerException if {@code string} is {@code null}.
      */
     public boolean regionMatches(int thisStart, CharSequence string, int start, int length) {
-        if (string == null) {
-            throw new NullPointerException("string");
-        }
+        ObjectUtil.checkNotNull(string, "string");
 
         if (start < 0 || string.length() - start < length) {
             return false;
@@ -842,9 +840,7 @@ public final class AsciiString implements CharSequence, Comparable<CharSequence>
             return regionMatches(thisStart, string, start, length);
         }
 
-        if (string == null) {
-            throw new NullPointerException("string");
-        }
+        ObjectUtil.checkNotNull(string, "string");
 
         final int thisLen = length();
         if (thisStart < 0 || length > thisLen - thisStart) {
@@ -988,7 +984,7 @@ public final class AsciiString implements CharSequence, Comparable<CharSequence>
      * @return a new string with characters {@code <= \\u0020} removed from the beginning and the end.
      */
     public static CharSequence trim(CharSequence c) {
-        if (c.getClass() == AsciiString.class) {
+        if (c instanceof AsciiString) {
             return ((AsciiString) c).trim();
         }
         if (c instanceof String) {
@@ -1036,10 +1032,14 @@ public final class AsciiString implements CharSequence, Comparable<CharSequence>
      * @return {@code true} if equal, otherwise {@code false}
      */
     public boolean contentEquals(CharSequence a) {
+        if (this == a) {
+            return true;
+        }
+
         if (a == null || a.length() != length()) {
             return false;
         }
-        if (a.getClass() == AsciiString.class) {
+        if (a instanceof AsciiString) {
             return equals(a);
         }
 
@@ -1388,7 +1388,7 @@ public final class AsciiString implements CharSequence, Comparable<CharSequence>
      * {@link AsciiString}, just returns the same instance.
      */
     public static AsciiString of(CharSequence string) {
-        return string.getClass() == AsciiString.class ? (AsciiString) string : new AsciiString(string);
+        return string instanceof AsciiString ? (AsciiString) string : new AsciiString(string);
     }
 
     /**
@@ -1412,7 +1412,7 @@ public final class AsciiString implements CharSequence, Comparable<CharSequence>
         if (value == null) {
             return 0;
         }
-        if (value.getClass() == AsciiString.class) {
+        if (value instanceof AsciiString) {
             return value.hashCode();
         }
 
@@ -1442,10 +1442,10 @@ public final class AsciiString implements CharSequence, Comparable<CharSequence>
             return a == b;
         }
 
-        if (a.getClass() == AsciiString.class) {
+        if (a instanceof AsciiString) {
             return ((AsciiString) a).contentEqualsIgnoreCase(b);
         }
-        if (b.getClass() == AsciiString.class) {
+        if (b instanceof AsciiString) {
             return ((AsciiString) b).contentEqualsIgnoreCase(a);
         }
 
@@ -1504,11 +1504,11 @@ public final class AsciiString implements CharSequence, Comparable<CharSequence>
             return a == b;
         }
 
-        if (a.getClass() == AsciiString.class) {
+        if (a instanceof AsciiString) {
             return ((AsciiString) a).contentEquals(b);
         }
 
-        if (b.getClass() == AsciiString.class) {
+        if (b instanceof AsciiString) {
             return ((AsciiString) b).contentEquals(a);
         }
 
@@ -1827,7 +1827,13 @@ public final class AsciiString implements CharSequence, Comparable<CharSequence>
         return isUpperCase(b) ? (byte) (b + 32) : b;
     }
 
-    private static char toLowerCase(char c) {
+    /**
+     * If the character is uppercase - converts the character to lowercase,
+     * otherwise returns the character as it is. Only for ASCII characters.
+     *
+     * @return lowercase ASCII character equivalent
+     */
+    public static char toLowerCase(char c) {
         return isUpperCase(c) ? (char) (c + 32) : c;
     }
 
diff --git a/common/src/main/java/io/netty/util/AttributeMap.java b/common/src/main/java/io/netty/util/AttributeMap.java
index 826e695..06eba74 100644
--- a/common/src/main/java/io/netty/util/AttributeMap.java
+++ b/common/src/main/java/io/netty/util/AttributeMap.java
@@ -28,7 +28,7 @@ public interface AttributeMap {
     <T> Attribute<T> attr(AttributeKey<T> key);
 
     /**
-     * Returns {@code} true if and only if the given {@link Attribute} exists in this {@link AttributeMap}.
+     * Returns {@code true} if and only if the given {@link Attribute} exists in this {@link AttributeMap}.
      */
     <T> boolean hasAttr(AttributeKey<T> key);
 }
diff --git a/common/src/main/java/io/netty/util/CharsetUtil.java b/common/src/main/java/io/netty/util/CharsetUtil.java
index 4d71b0a..a9317e5 100644
--- a/common/src/main/java/io/netty/util/CharsetUtil.java
+++ b/common/src/main/java/io/netty/util/CharsetUtil.java
@@ -65,7 +65,9 @@ public final class CharsetUtil {
     private static final Charset[] CHARSETS = new Charset[]
             { UTF_16, UTF_16BE, UTF_16LE, UTF_8, ISO_8859_1, US_ASCII };
 
-    public static Charset[] values() { return CHARSETS; }
+    public static Charset[] values() {
+        return CHARSETS;
+    }
 
     /**
      * @deprecated Use {@link #encoder(Charset)}.
diff --git a/common/src/main/java/io/netty/util/ConstantPool.java b/common/src/main/java/io/netty/util/ConstantPool.java
index a05ab7e..e0bb5d6 100644
--- a/common/src/main/java/io/netty/util/ConstantPool.java
+++ b/common/src/main/java/io/netty/util/ConstantPool.java
@@ -37,14 +37,10 @@ public abstract class ConstantPool<T extends Constant<T>> {
      * Shortcut of {@link #valueOf(String) valueOf(firstNameComponent.getName() + "#" + secondNameComponent)}.
      */
     public T valueOf(Class<?> firstNameComponent, String secondNameComponent) {
-        if (firstNameComponent == null) {
-            throw new NullPointerException("firstNameComponent");
-        }
-        if (secondNameComponent == null) {
-            throw new NullPointerException("secondNameComponent");
-        }
-
-        return valueOf(firstNameComponent.getName() + '#' + secondNameComponent);
+        return valueOf(
+                ObjectUtil.checkNotNull(firstNameComponent, "firstNameComponent").getName() +
+                '#' +
+                ObjectUtil.checkNotNull(secondNameComponent, "secondNameComponent"));
     }
 
     /**
diff --git a/common/src/main/java/io/netty/util/DefaultAttributeMap.java b/common/src/main/java/io/netty/util/DefaultAttributeMap.java
index c685e4d..8624620 100644
--- a/common/src/main/java/io/netty/util/DefaultAttributeMap.java
+++ b/common/src/main/java/io/netty/util/DefaultAttributeMap.java
@@ -15,6 +15,8 @@
  */
 package io.netty.util;
 
+import io.netty.util.internal.ObjectUtil;
+
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.concurrent.atomic.AtomicReferenceArray;
 import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
@@ -39,9 +41,7 @@ public class DefaultAttributeMap implements AttributeMap {
     @SuppressWarnings("unchecked")
     @Override
     public <T> Attribute<T> attr(AttributeKey<T> key) {
-        if (key == null) {
-            throw new NullPointerException("key");
-        }
+        ObjectUtil.checkNotNull(key, "key");
         AtomicReferenceArray<DefaultAttribute<?>> attributes = this.attributes;
         if (attributes == null) {
             // Not using ConcurrentHashMap due to high memory consumption.
@@ -90,9 +90,7 @@ public class DefaultAttributeMap implements AttributeMap {
 
     @Override
     public <T> boolean hasAttr(AttributeKey<T> key) {
-        if (key == null) {
-            throw new NullPointerException("key");
-        }
+        ObjectUtil.checkNotNull(key, "key");
         AtomicReferenceArray<DefaultAttribute<?>> attributes = this.attributes;
         if (attributes == null) {
             // no attribute exists
diff --git a/common/src/main/java/io/netty/util/HashedWheelTimer.java b/common/src/main/java/io/netty/util/HashedWheelTimer.java
index e8e72b6..c19dfe2 100644
--- a/common/src/main/java/io/netty/util/HashedWheelTimer.java
+++ b/common/src/main/java/io/netty/util/HashedWheelTimer.java
@@ -15,6 +15,7 @@
  */
 package io.netty.util;
 
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.PlatformDependent;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
@@ -242,18 +243,10 @@ public class HashedWheelTimer implements Timer {
             long tickDuration, TimeUnit unit, int ticksPerWheel, boolean leakDetection,
             long maxPendingTimeouts) {
 
-        if (threadFactory == null) {
-            throw new NullPointerException("threadFactory");
-        }
-        if (unit == null) {
-            throw new NullPointerException("unit");
-        }
-        if (tickDuration <= 0) {
-            throw new IllegalArgumentException("tickDuration must be greater than 0: " + tickDuration);
-        }
-        if (ticksPerWheel <= 0) {
-            throw new IllegalArgumentException("ticksPerWheel must be greater than 0: " + ticksPerWheel);
-        }
+        ObjectUtil.checkNotNull(threadFactory, "threadFactory");
+        ObjectUtil.checkNotNull(unit, "unit");
+        ObjectUtil.checkPositive(tickDuration, "tickDuration");
+        ObjectUtil.checkPositive(ticksPerWheel, "ticksPerWheel");
 
         // Normalize ticksPerWheel to power of two and initialize the wheel.
         wheel = createWheel(ticksPerWheel);
@@ -270,10 +263,8 @@ public class HashedWheelTimer implements Timer {
         }
 
         if (duration < MILLISECOND_NANOS) {
-            if (logger.isWarnEnabled()) {
-                logger.warn("Configured tickDuration %d smaller then %d, using 1ms.",
-                            tickDuration, MILLISECOND_NANOS);
-            }
+            logger.warn("Configured tickDuration {} smaller then {}, using 1ms.",
+                        tickDuration, MILLISECOND_NANOS);
             this.tickDuration = MILLISECOND_NANOS;
         } else {
             this.tickDuration = duration;
@@ -410,12 +401,8 @@ public class HashedWheelTimer implements Timer {
 
     @Override
     public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) {
-        if (task == null) {
-            throw new NullPointerException("task");
-        }
-        if (unit == null) {
-            throw new NullPointerException("unit");
-        }
+        ObjectUtil.checkNotNull(task, "task");
+        ObjectUtil.checkNotNull(unit, "unit");
 
         long pendingTimeoutsCount = pendingTimeouts.incrementAndGet();
 
@@ -573,6 +560,9 @@ public class HashedWheelTimer implements Timer {
                 // See https://github.com/netty/netty/issues/356
                 if (PlatformDependent.isWindows()) {
                     sleepTimeMs = sleepTimeMs / 10 * 10;
+                    if (sleepTimeMs == 0) {
+                        sleepTimeMs = 1;
+                    }
                 }
 
                 try {
diff --git a/common/src/main/java/io/netty/util/NetUtil.java b/common/src/main/java/io/netty/util/NetUtil.java
index 088889d..8634351 100644
--- a/common/src/main/java/io/netty/util/NetUtil.java
+++ b/common/src/main/java/io/netty/util/NetUtil.java
@@ -291,7 +291,10 @@ public final class NetUtil {
                         }
                     }
                 } catch (Exception e) {
-                    logger.debug("Failed to get SOMAXCONN from sysctl and file {}. Default: {}", file, somaxconn, e);
+                    if (logger.isDebugEnabled()) {
+                        logger.debug("Failed to get SOMAXCONN from sysctl and file {}. Default: {}",
+                                file, somaxconn, e);
+                    }
                 } finally {
                     if (in != null) {
                         try {
@@ -320,10 +323,10 @@ public final class NetUtil {
             BufferedReader br = new BufferedReader(isr);
             try {
                 String line = br.readLine();
-                if (line.startsWith(sysctlKey)) {
+                if (line != null && line.startsWith(sysctlKey)) {
                     for (int i = line.length() - 1; i > sysctlKey.length(); --i) {
                         if (!Character.isDigit(line.charAt(i))) {
-                            return Integer.valueOf(line.substring(i + 1, line.length()));
+                            return Integer.valueOf(line.substring(i + 1));
                         }
                     }
                 }
diff --git a/common/src/main/java/io/netty/util/Recycler.java b/common/src/main/java/io/netty/util/Recycler.java
index 87b182e..ae2328e 100644
--- a/common/src/main/java/io/netty/util/Recycler.java
+++ b/common/src/main/java/io/netty/util/Recycler.java
@@ -17,6 +17,7 @@
 package io.netty.util;
 
 import io.netty.util.concurrent.FastThreadLocal;
+import io.netty.util.internal.ObjectPool;
 import io.netty.util.internal.SystemPropertyUtil;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
@@ -105,14 +106,14 @@ public abstract class Recycler<T> {
 
     private final int maxCapacityPerThread;
     private final int maxSharedCapacityFactor;
-    private final int ratioMask;
+    private final int interval;
     private final int maxDelayedQueuesPerThread;
 
     private final FastThreadLocal<Stack<T>> threadLocal = new FastThreadLocal<Stack<T>>() {
         @Override
         protected Stack<T> initialValue() {
             return new Stack<T>(Recycler.this, Thread.currentThread(), maxCapacityPerThread, maxSharedCapacityFactor,
-                    ratioMask, maxDelayedQueuesPerThread);
+                    interval, maxDelayedQueuesPerThread);
         }
 
         @Override
@@ -140,7 +141,7 @@ public abstract class Recycler<T> {
 
     protected Recycler(int maxCapacityPerThread, int maxSharedCapacityFactor,
                        int ratio, int maxDelayedQueuesPerThread) {
-        ratioMask = safeFindNextPositivePowerOfTwo(ratio) - 1;
+        interval = safeFindNextPositivePowerOfTwo(ratio);
         if (maxCapacityPerThread <= 0) {
             this.maxCapacityPerThread = 0;
             this.maxSharedCapacityFactor = 1;
@@ -194,18 +195,16 @@ public abstract class Recycler<T> {
 
     protected abstract T newObject(Handle<T> handle);
 
-    public interface Handle<T> {
-        void recycle(T object);
-    }
+    public interface Handle<T> extends ObjectPool.Handle<T>  { }
 
-    static final class DefaultHandle<T> implements Handle<T> {
-        private int lastRecycledId;
-        private int recycleId;
+    private static final class DefaultHandle<T> implements Handle<T> {
+        int lastRecycledId;
+        int recycleId;
 
         boolean hasBeenRecycled;
 
-        private Stack<?> stack;
-        private Object value;
+        Stack<?> stack;
+        Object value;
 
         DefaultHandle(Stack<?> stack) {
             this.stack = stack;
@@ -236,22 +235,21 @@ public abstract class Recycler<T> {
 
     // a queue that makes only moderate guarantees about visibility: items are seen in the correct order,
     // but we aren't absolutely guaranteed to ever see anything at all, thereby keeping the queue cheap to maintain
-    private static final class WeakOrderQueue {
+    private static final class WeakOrderQueue extends WeakReference<Thread> {
 
         static final WeakOrderQueue DUMMY = new WeakOrderQueue();
 
         // Let Link extend AtomicInteger for intrinsics. The Link itself will be used as writerIndex.
         @SuppressWarnings("serial")
         static final class Link extends AtomicInteger {
-            private final DefaultHandle<?>[] elements = new DefaultHandle[LINK_CAPACITY];
+            final DefaultHandle<?>[] elements = new DefaultHandle[LINK_CAPACITY];
 
-            private int readIndex;
+            int readIndex;
             Link next;
         }
 
-        // This act as a place holder for the head Link but also will reclaim space once finalized.
         // Its important this does not hold any reference to either Stack or WeakOrderQueue.
-        static final class Head {
+        private static final class Head {
             private final AtomicInteger availableSharedCapacity;
 
             Link link;
@@ -260,41 +258,49 @@ public abstract class Recycler<T> {
                 this.availableSharedCapacity = availableSharedCapacity;
             }
 
-            /// TODO: In the future when we move to Java9+ we should use java.lang.ref.Cleaner.
-            @Override
-            protected void finalize() throws Throwable {
-                try {
-                    super.finalize();
-                } finally {
-                    Link head = link;
-                    link = null;
-                    while (head != null) {
-                        reclaimSpace(LINK_CAPACITY);
-                        Link next = head.next;
-                        // Unlink to help GC and guard against GC nepotism.
-                        head.next = null;
-                        head = next;
-                    }
+            /**
+             * Reclaim all used space and also unlink the nodes to prevent GC nepotism.
+             */
+            void reclaimAllSpaceAndUnlink() {
+                Link head = link;
+                link = null;
+                int reclaimSpace = 0;
+                while (head != null) {
+                    reclaimSpace += LINK_CAPACITY;
+                    Link next = head.next;
+                    // Unlink to help GC and guard against GC nepotism.
+                    head.next = null;
+                    head = next;
+                }
+                if (reclaimSpace > 0) {
+                    reclaimSpace(reclaimSpace);
                 }
             }
 
-            void reclaimSpace(int space) {
-                assert space >= 0;
+            private void reclaimSpace(int space) {
                 availableSharedCapacity.addAndGet(space);
             }
 
-            boolean reserveSpace(int space) {
-                return reserveSpace(availableSharedCapacity, space);
+            void relink(Link link) {
+                reclaimSpace(LINK_CAPACITY);
+                this.link = link;
+            }
+
+            /**
+             * Creates a new {@link} and returns it if we can reserve enough space for it, otherwise it
+             * returns {@code null}.
+             */
+            Link newLink() {
+                return reserveSpaceForLink(availableSharedCapacity) ? new Link() : null;
             }
 
-            static boolean reserveSpace(AtomicInteger availableSharedCapacity, int space) {
-                assert space >= 0;
+            static boolean reserveSpaceForLink(AtomicInteger availableSharedCapacity) {
                 for (;;) {
                     int available = availableSharedCapacity.get();
-                    if (available < space) {
+                    if (available < LINK_CAPACITY) {
                         return false;
                     }
-                    if (availableSharedCapacity.compareAndSet(available, available - space)) {
+                    if (availableSharedCapacity.compareAndSet(available, available - LINK_CAPACITY)) {
                         return true;
                     }
                 }
@@ -306,15 +312,18 @@ public abstract class Recycler<T> {
         private Link tail;
         // pointer to another queue of delayed items for the same stack
         private WeakOrderQueue next;
-        private final WeakReference<Thread> owner;
         private final int id = ID_GENERATOR.getAndIncrement();
+        private final int interval;
+        private int handleRecycleCount;
 
         private WeakOrderQueue() {
-            owner = null;
+            super(null);
             head = new Head(null);
+            interval = 0;
         }
 
         private WeakOrderQueue(Stack<?> stack, Thread thread) {
+            super(thread);
             tail = new Link();
 
             // Its important that we not store the Stack itself in the WeakOrderQueue as the Stack also is used in
@@ -322,10 +331,15 @@ public abstract class Recycler<T> {
             // Stack itself GCed.
             head = new Head(stack.availableSharedCapacity);
             head.link = tail;
-            owner = new WeakReference<Thread>(thread);
+            interval = stack.interval;
+            handleRecycleCount = interval; // Start at interval so the first one will be recycled.
         }
 
         static WeakOrderQueue newQueue(Stack<?> stack, Thread thread) {
+            // We allocated a Link so reserve the space
+            if (!Head.reserveSpaceForLink(stack.availableSharedCapacity)) {
+                return null;
+            }
             final WeakOrderQueue queue = new WeakOrderQueue(stack, thread);
             // Done outside of the constructor to ensure WeakOrderQueue.this does not escape the constructor and so
             // may be accessed while its still constructed.
@@ -334,32 +348,43 @@ public abstract class Recycler<T> {
             return queue;
         }
 
-        private void setNext(WeakOrderQueue next) {
+        WeakOrderQueue getNext() {
+            return next;
+        }
+
+        void setNext(WeakOrderQueue next) {
             assert next != this;
             this.next = next;
         }
 
-        /**
-         * Allocate a new {@link WeakOrderQueue} or return {@code null} if not possible.
-         */
-        static WeakOrderQueue allocate(Stack<?> stack, Thread thread) {
-            // We allocated a Link so reserve the space
-            return Head.reserveSpace(stack.availableSharedCapacity, LINK_CAPACITY)
-                    ? newQueue(stack, thread) : null;
+        void reclaimAllSpaceAndUnlink() {
+            head.reclaimAllSpaceAndUnlink();
+            this.next = null;
         }
 
         void add(DefaultHandle<?> handle) {
             handle.lastRecycledId = id;
 
+            // While we also enforce the recycling ratio one we transfer objects from the WeakOrderQueue to the Stack
+            // we better should enforce it as well early. Missing to do so may let the WeakOrderQueue grow very fast
+            // without control if the Stack
+            if (handleRecycleCount < interval) {
+                handleRecycleCount++;
+                // Drop the item to prevent recycling to aggressive.
+                return;
+            }
+            handleRecycleCount = 0;
+
             Link tail = this.tail;
             int writeIndex;
             if ((writeIndex = tail.get()) == LINK_CAPACITY) {
-                if (!head.reserveSpace(LINK_CAPACITY)) {
+                Link link = head.newLink();
+                if (link == null) {
                     // Drop it.
                     return;
                 }
                 // We allocate a Link so reserve the space
-                this.tail = tail = tail.next = new Link();
+                this.tail = tail = tail.next = link;
 
                 writeIndex = tail.get();
             }
@@ -386,7 +411,8 @@ public abstract class Recycler<T> {
                 if (head.next == null) {
                     return false;
                 }
-                this.head.link = head = head.next;
+                head = head.next;
+                this.head.relink(head);
             }
 
             final int srcStart = head.readIndex;
@@ -409,7 +435,7 @@ public abstract class Recycler<T> {
                 final DefaultHandle[] dstElems = dst.elements;
                 int newDstSize = dstSize;
                 for (int i = srcStart; i < srcEnd; i++) {
-                    DefaultHandle element = srcElems[i];
+                    DefaultHandle<?> element = srcElems[i];
                     if (element.recycleId == 0) {
                         element.recycleId = element.lastRecycledId;
                     } else if (element.recycleId != element.lastRecycledId) {
@@ -427,8 +453,7 @@ public abstract class Recycler<T> {
 
                 if (srcEnd == LINK_CAPACITY && head.next != null) {
                     // Add capacity back as the Link is GCed.
-                    this.head.reclaimSpace(LINK_CAPACITY);
-                    this.head.link = head.next;
+                    this.head.relink(head.next);
                 }
 
                 head.readIndex = srcEnd;
@@ -444,7 +469,7 @@ public abstract class Recycler<T> {
         }
     }
 
-    static final class Stack<T> {
+    private static final class Stack<T> {
 
         // we keep a queue of per-thread queues, which is appended to once only, each time a new thread other
         // than the stack owner recycles: when we run out of items in our stack we iterate this collection
@@ -460,24 +485,25 @@ public abstract class Recycler<T> {
         // it in a timely manner).
         final WeakReference<Thread> threadRef;
         final AtomicInteger availableSharedCapacity;
-        final int maxDelayedQueues;
+        private final int maxDelayedQueues;
 
         private final int maxCapacity;
-        private final int ratioMask;
-        private DefaultHandle<?>[] elements;
-        private int size;
-        private int handleRecycleCount = -1; // Start with -1 so the first one will be recycled.
+        private final int interval;
+        DefaultHandle<?>[] elements;
+        int size;
+        private int handleRecycleCount;
         private WeakOrderQueue cursor, prev;
         private volatile WeakOrderQueue head;
 
         Stack(Recycler<T> parent, Thread thread, int maxCapacity, int maxSharedCapacityFactor,
-              int ratioMask, int maxDelayedQueues) {
+              int interval, int maxDelayedQueues) {
             this.parent = parent;
             threadRef = new WeakReference<Thread>(thread);
             this.maxCapacity = maxCapacity;
             availableSharedCapacity = new AtomicInteger(max(maxCapacity / maxSharedCapacityFactor, LINK_CAPACITY));
             elements = new DefaultHandle[min(INITIAL_CAPACITY, maxCapacity)];
-            this.ratioMask = ratioMask;
+            this.interval = interval;
+            handleRecycleCount = interval; // Start at interval so the first one will be recycled.
             this.maxDelayedQueues = maxDelayedQueues;
         }
 
@@ -510,20 +536,28 @@ public abstract class Recycler<T> {
                     return null;
                 }
                 size = this.size;
+                if (size <= 0) {
+                    // double check, avoid races
+                    return null;
+                }
             }
             size --;
             DefaultHandle ret = elements[size];
             elements[size] = null;
+            // As we already set the element[size] to null we also need to store the updated size before we do
+            // any validation. Otherwise we may see a null value when later try to pop again without a new element
+            // added before.
+            this.size = size;
+
             if (ret.lastRecycledId != ret.recycleId) {
                 throw new IllegalStateException("recycled multiple times");
             }
             ret.recycleId = 0;
             ret.lastRecycledId = 0;
-            this.size = size;
             return ret;
         }
 
-        boolean scavenge() {
+        private boolean scavenge() {
             // continue an existing scavenge, if any
             if (scavengeSome()) {
                 return true;
@@ -535,7 +569,7 @@ public abstract class Recycler<T> {
             return false;
         }
 
-        boolean scavengeSome() {
+        private boolean scavengeSome() {
             WeakOrderQueue prev;
             WeakOrderQueue cursor = this.cursor;
             if (cursor == null) {
@@ -554,8 +588,8 @@ public abstract class Recycler<T> {
                     success = true;
                     break;
                 }
-                WeakOrderQueue next = cursor.next;
-                if (cursor.owner.get() == null) {
+                WeakOrderQueue next = cursor.getNext();
+                if (cursor.get() == null) {
                     // If the thread associated with the queue is gone, unlink it, after
                     // performing a volatile read to confirm there is no data left to collect.
                     // We never unlink the first queue, as we don't want to synchronize on updating the head.
@@ -570,6 +604,8 @@ public abstract class Recycler<T> {
                     }
 
                     if (prev != null) {
+                        // Ensure we reclaim all space before dropping the WeakOrderQueue to be GC'ed.
+                        cursor.reclaimAllSpaceAndUnlink();
                         prev.setNext(next);
                     }
                 } else {
@@ -618,6 +654,11 @@ public abstract class Recycler<T> {
         }
 
         private void pushLater(DefaultHandle<?> item, Thread thread) {
+            if (maxDelayedQueues == 0) {
+                // We don't support recycling across threads and should just drop the item on the floor.
+                return;
+            }
+
             // we don't want to have a ref to the queue as the value in our weak map
             // so we null it out; to ensure there are no races with restoring it later
             // we impose a memory ordering here (no-op on x86)
@@ -630,7 +671,7 @@ public abstract class Recycler<T> {
                     return;
                 }
                 // Check if we already reached the maximum number of delayed queues and if we can allocate at all.
-                if ((queue = WeakOrderQueue.allocate(this, thread)) == null) {
+                if ((queue = newWeakOrderQueue(thread)) == null) {
                     // drop object
                     return;
                 }
@@ -643,12 +684,21 @@ public abstract class Recycler<T> {
             queue.add(item);
         }
 
+        /**
+         * Allocate a new {@link WeakOrderQueue} or return {@code null} if not possible.
+         */
+        private WeakOrderQueue newWeakOrderQueue(Thread thread) {
+            return WeakOrderQueue.newQueue(this, thread);
+        }
+
         boolean dropHandle(DefaultHandle<?> handle) {
             if (!handle.hasBeenRecycled) {
-                if ((++handleRecycleCount & ratioMask) != 0) {
+                if (handleRecycleCount < interval) {
+                    handleRecycleCount++;
                     // Drop the object.
                     return true;
                 }
+                handleRecycleCount = 0;
                 handle.hasBeenRecycled = true;
             }
             return false;
diff --git a/common/src/main/java/io/netty/util/ResourceLeakDetector.java b/common/src/main/java/io/netty/util/ResourceLeakDetector.java
index 436d793..5b9dd16 100644
--- a/common/src/main/java/io/netty/util/ResourceLeakDetector.java
+++ b/common/src/main/java/io/netty/util/ResourceLeakDetector.java
@@ -17,6 +17,7 @@
 package io.netty.util;
 
 import io.netty.util.internal.EmptyArrays;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.PlatformDependent;
 import io.netty.util.internal.SystemPropertyUtil;
 import io.netty.util.internal.logging.InternalLogger;
@@ -150,10 +151,7 @@ public class ResourceLeakDetector<T> {
      * Sets the resource leak detection level.
      */
     public static void setLevel(Level level) {
-        if (level == null) {
-            throw new NullPointerException("level");
-        }
-        ResourceLeakDetector.level = level;
+        ResourceLeakDetector.level = ObjectUtil.checkNotNull(level, "level");
     }
 
     /**
@@ -168,7 +166,8 @@ public class ResourceLeakDetector<T> {
             Collections.newSetFromMap(new ConcurrentHashMap<DefaultResourceLeak<?>, Boolean>());
 
     private final ReferenceQueue<Object> refQueue = new ReferenceQueue<Object>();
-    private final ConcurrentMap<String, Boolean> reportedLeaks = PlatformDependent.newConcurrentHashMap();
+    private final Set<String> reportedLeaks =
+            Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());
 
     private final String resourceType;
     private final int samplingInterval;
@@ -220,11 +219,7 @@ public class ResourceLeakDetector<T> {
      */
     @Deprecated
     public ResourceLeakDetector(String resourceType, int samplingInterval, long maxActive) {
-        if (resourceType == null) {
-            throw new NullPointerException("resourceType");
-        }
-
-        this.resourceType = resourceType;
+        this.resourceType = ObjectUtil.checkNotNull(resourceType, "resourceType");
         this.samplingInterval = samplingInterval;
     }
 
@@ -271,7 +266,6 @@ public class ResourceLeakDetector<T> {
 
     private void clearRefQueue() {
         for (;;) {
-            @SuppressWarnings("unchecked")
             DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
             if (ref == null) {
                 break;
@@ -280,15 +274,24 @@ public class ResourceLeakDetector<T> {
         }
     }
 
+    /**
+     * When the return value is {@code true}, {@link #reportTracedLeak} and {@link #reportUntracedLeak}
+     * will be called once a leak is detected, otherwise not.
+     *
+     * @return {@code true} to enable leak reporting.
+     */
+    protected boolean needReport() {
+        return logger.isErrorEnabled();
+    }
+
     private void reportLeak() {
-        if (!logger.isErrorEnabled()) {
+        if (!needReport()) {
             clearRefQueue();
             return;
         }
 
         // Detect and report previous leaks.
         for (;;) {
-            @SuppressWarnings("unchecked")
             DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
             if (ref == null) {
                 break;
@@ -299,7 +302,7 @@ public class ResourceLeakDetector<T> {
             }
 
             String records = ref.toString();
-            if (reportedLeaks.putIfAbsent(records, Boolean.TRUE) == null) {
+            if (reportedLeaks.add(records)) {
                 if (records.isEmpty()) {
                     reportUntracedLeak(resourceType);
                 } else {
@@ -316,7 +319,7 @@ public class ResourceLeakDetector<T> {
     protected void reportTracedLeak(String resourceType, String records) {
         logger.error(
                 "LEAK: {}.release() was not called before it's garbage-collected. " +
-                "See http://netty.io/wiki/reference-counted-objects.html for more information.{}",
+                "See https://netty.io/wiki/reference-counted-objects.html for more information.{}",
                 resourceType, records);
     }
 
@@ -329,7 +332,7 @@ public class ResourceLeakDetector<T> {
                 "Enable advanced leak reporting to find out where the leak occurred. " +
                 "To enable advanced leak reporting, " +
                 "specify the JVM option '-D{}={}' or call {}.setLevel() " +
-                "See http://netty.io/wiki/reference-counted-objects.html for more information.",
+                "See https://netty.io/wiki/reference-counted-objects.html for more information.",
                 resourceType, PROP_LEVEL, Level.ADVANCED.name().toLowerCase(), simpleClassName(this));
     }
 
@@ -498,8 +501,9 @@ public class ResourceLeakDetector<T> {
          */
         private static void reachabilityFence0(Object ref) {
             if (ref != null) {
-                // Empty synchronized is ok: https://stackoverflow.com/a/31933260/1151521
-                synchronized (ref) { }
+                synchronized (ref) {
+                    // Empty synchronized is ok: https://stackoverflow.com/a/31933260/1151521
+                }
             }
         }
 
diff --git a/common/src/main/java/io/netty/util/ResourceLeakDetectorFactory.java b/common/src/main/java/io/netty/util/ResourceLeakDetectorFactory.java
index 65f1fc8..60c016d 100644
--- a/common/src/main/java/io/netty/util/ResourceLeakDetectorFactory.java
+++ b/common/src/main/java/io/netty/util/ResourceLeakDetectorFactory.java
@@ -23,8 +23,6 @@ import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 
 import java.lang.reflect.Constructor;
-import java.security.AccessController;
-import java.security.PrivilegedAction;
 
 /**
  * This static factory should be used to load {@link ResourceLeakDetector}s as needed
@@ -103,12 +101,7 @@ public abstract class ResourceLeakDetectorFactory {
         DefaultResourceLeakDetectorFactory() {
             String customLeakDetector;
             try {
-                customLeakDetector = AccessController.doPrivileged(new PrivilegedAction<String>() {
-                    @Override
-                    public String run() {
-                        return SystemPropertyUtil.get("io.netty.customResourceLeakDetector");
-                    }
-                });
+                customLeakDetector = SystemPropertyUtil.get("io.netty.customResourceLeakDetector");
             } catch (Throwable cause) {
                 logger.error("Could not access System property: io.netty.customResourceLeakDetector", cause);
                 customLeakDetector = null;
diff --git a/common/src/main/java/io/netty/util/ThreadDeathWatcher.java b/common/src/main/java/io/netty/util/ThreadDeathWatcher.java
index 8e755ed..7cc44e6 100644
--- a/common/src/main/java/io/netty/util/ThreadDeathWatcher.java
+++ b/common/src/main/java/io/netty/util/ThreadDeathWatcher.java
@@ -17,6 +17,7 @@
 package io.netty.util;
 
 import io.netty.util.concurrent.DefaultThreadFactory;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.StringUtil;
 import io.netty.util.internal.SystemPropertyUtil;
 import io.netty.util.internal.logging.InternalLogger;
@@ -77,12 +78,9 @@ public final class ThreadDeathWatcher {
      * @throws IllegalArgumentException if the specified {@code thread} is not alive
      */
     public static void watch(Thread thread, Runnable task) {
-        if (thread == null) {
-            throw new NullPointerException("thread");
-        }
-        if (task == null) {
-            throw new NullPointerException("task");
-        }
+        ObjectUtil.checkNotNull(thread, "thread");
+        ObjectUtil.checkNotNull(task, "task");
+
         if (!thread.isAlive()) {
             throw new IllegalArgumentException("thread must be alive.");
         }
@@ -94,14 +92,9 @@ public final class ThreadDeathWatcher {
      * Cancels the task scheduled via {@link #watch(Thread, Runnable)}.
      */
     public static void unwatch(Thread thread, Runnable task) {
-        if (thread == null) {
-            throw new NullPointerException("thread");
-        }
-        if (task == null) {
-            throw new NullPointerException("task");
-        }
-
-        schedule(thread, task, false);
+        schedule(ObjectUtil.checkNotNull(thread, "thread"),
+                ObjectUtil.checkNotNull(task, "task"),
+                false);
     }
 
     private static void schedule(Thread thread, Runnable task, boolean isWatch) {
@@ -137,9 +130,7 @@ public final class ThreadDeathWatcher {
      * @return {@code true} if and only if the watcher thread has been terminated
      */
     public static boolean awaitInactivity(long timeout, TimeUnit unit) throws InterruptedException {
-        if (unit == null) {
-            throw new NullPointerException("unit");
-        }
+        ObjectUtil.checkNotNull(unit, "unit");
 
         Thread watcherThread = ThreadDeathWatcher.watcherThread;
         if (watcherThread != null) {
diff --git a/common/src/main/java/io/netty/util/concurrent/AbstractEventExecutor.java b/common/src/main/java/io/netty/util/concurrent/AbstractEventExecutor.java
index 6cfccbd..4799701 100644
--- a/common/src/main/java/io/netty/util/concurrent/AbstractEventExecutor.java
+++ b/common/src/main/java/io/netty/util/concurrent/AbstractEventExecutor.java
@@ -15,6 +15,7 @@
  */
 package io.netty.util.concurrent;
 
+import io.netty.util.internal.UnstableApi;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 
@@ -165,4 +166,25 @@ public abstract class AbstractEventExecutor extends AbstractExecutorService impl
             logger.warn("A task raised an exception. Task: {}", task, t);
         }
     }
+
+    /**
+     * Like {@link #execute(Runnable)} but does not guarantee the task will be run until either
+     * a non-lazy task is executed or the executor is shut down.
+     *
+     * This is equivalent to submitting a {@link EventExecutor.LazyRunnable} to
+     * {@link #execute(Runnable)} but for an arbitrary {@link Runnable}.
+     *
+     * The default implementation just delegates to {@link #execute(Runnable)}.
+     */
+    @UnstableApi
+    public void lazyExecute(Runnable task) {
+        execute(task);
+    }
+
+    /**
+     * Marker interface for {@link Runnable} to indicate that it should be queued for execution
+     * but does not need to run immediately.
+     */
+    @UnstableApi
+    public interface LazyRunnable extends Runnable { }
 }
diff --git a/common/src/main/java/io/netty/util/concurrent/AbstractScheduledEventExecutor.java b/common/src/main/java/io/netty/util/concurrent/AbstractScheduledEventExecutor.java
index 41430c7..16166d3 100644
--- a/common/src/main/java/io/netty/util/concurrent/AbstractScheduledEventExecutor.java
+++ b/common/src/main/java/io/netty/util/concurrent/AbstractScheduledEventExecutor.java
@@ -19,17 +19,17 @@ import io.netty.util.internal.DefaultPriorityQueue;
 import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.PriorityQueue;
 
+import static io.netty.util.concurrent.ScheduledFutureTask.deadlineNanos;
+
 import java.util.Comparator;
 import java.util.Queue;
 import java.util.concurrent.Callable;
-import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
 
 /**
  * Abstract base class for {@link EventExecutor}s that want to support scheduling.
  */
 public abstract class AbstractScheduledEventExecutor extends AbstractEventExecutor {
-
     private static final Comparator<ScheduledFutureTask<?>> SCHEDULED_FUTURE_TASK_COMPARATOR =
             new Comparator<ScheduledFutureTask<?>>() {
                 @Override
@@ -38,8 +38,15 @@ public abstract class AbstractScheduledEventExecutor extends AbstractEventExecut
                 }
             };
 
+   static final Runnable WAKEUP_TASK = new Runnable() {
+       @Override
+       public void run() { } // Do nothing
+    };
+
     PriorityQueue<ScheduledFutureTask<?>> scheduledTaskQueue;
 
+    long nextTaskId;
+
     protected AbstractScheduledEventExecutor() {
     }
 
@@ -51,6 +58,24 @@ public abstract class AbstractScheduledEventExecutor extends AbstractEventExecut
         return ScheduledFutureTask.nanoTime();
     }
 
+    /**
+     * Given an arbitrary deadline {@code deadlineNanos}, calculate the number of nano seconds from now
+     * {@code deadlineNanos} would expire.
+     * @param deadlineNanos An arbitrary deadline in nano seconds.
+     * @return the number of nano seconds from now {@code deadlineNanos} would expire.
+     */
+    protected static long deadlineToDelayNanos(long deadlineNanos) {
+        return ScheduledFutureTask.deadlineToDelayNanos(deadlineNanos);
+    }
+
+    /**
+     * The initial value used for delay and computations based upon a monatomic time source.
+     * @return initial value used for delay and computations based upon a monatomic time source.
+     */
+    protected static long initialNanoTime() {
+        return ScheduledFutureTask.initialNanoTime();
+    }
+
     PriorityQueue<ScheduledFutureTask<?>> scheduledTaskQueue() {
         if (scheduledTaskQueue == null) {
             scheduledTaskQueue = new DefaultPriorityQueue<ScheduledFutureTask<?>>(
@@ -101,45 +126,42 @@ public abstract class AbstractScheduledEventExecutor extends AbstractEventExecut
     protected final Runnable pollScheduledTask(long nanoTime) {
         assert inEventLoop();
 
-        Queue<ScheduledFutureTask<?>> scheduledTaskQueue = this.scheduledTaskQueue;
-        ScheduledFutureTask<?> scheduledTask = scheduledTaskQueue == null ? null : scheduledTaskQueue.peek();
-        if (scheduledTask == null) {
+        ScheduledFutureTask<?> scheduledTask = peekScheduledTask();
+        if (scheduledTask == null || scheduledTask.deadlineNanos() - nanoTime > 0) {
             return null;
         }
-
-        if (scheduledTask.deadlineNanos() <= nanoTime) {
-            scheduledTaskQueue.remove();
-            return scheduledTask;
-        }
-        return null;
+        scheduledTaskQueue.remove();
+        scheduledTask.setConsumed();
+        return scheduledTask;
     }
 
     /**
-     * Return the nanoseconds when the next scheduled task is ready to be run or {@code -1} if no task is scheduled.
+     * Return the nanoseconds until the next scheduled task is ready to be run or {@code -1} if no task is scheduled.
      */
     protected final long nextScheduledTaskNano() {
-        Queue<ScheduledFutureTask<?>> scheduledTaskQueue = this.scheduledTaskQueue;
-        ScheduledFutureTask<?> scheduledTask = scheduledTaskQueue == null ? null : scheduledTaskQueue.peek();
-        if (scheduledTask == null) {
-            return -1;
-        }
-        return Math.max(0, scheduledTask.deadlineNanos() - nanoTime());
+        ScheduledFutureTask<?> scheduledTask = peekScheduledTask();
+        return scheduledTask != null ? scheduledTask.delayNanos() : -1;
+    }
+
+    /**
+     * Return the deadline (in nanoseconds) when the next scheduled task is ready to be run or {@code -1}
+     * if no task is scheduled.
+     */
+    protected final long nextScheduledTaskDeadlineNanos() {
+        ScheduledFutureTask<?> scheduledTask = peekScheduledTask();
+        return scheduledTask != null ? scheduledTask.deadlineNanos() : -1;
     }
 
     final ScheduledFutureTask<?> peekScheduledTask() {
         Queue<ScheduledFutureTask<?>> scheduledTaskQueue = this.scheduledTaskQueue;
-        if (scheduledTaskQueue == null) {
-            return null;
-        }
-        return scheduledTaskQueue.peek();
+        return scheduledTaskQueue != null ? scheduledTaskQueue.peek() : null;
     }
 
     /**
      * Returns {@code true} if a scheduled task is ready for processing.
      */
     protected final boolean hasScheduledTasks() {
-        Queue<ScheduledFutureTask<?>> scheduledTaskQueue = this.scheduledTaskQueue;
-        ScheduledFutureTask<?> scheduledTask = scheduledTaskQueue == null ? null : scheduledTaskQueue.peek();
+        ScheduledFutureTask<?> scheduledTask = peekScheduledTask();
         return scheduledTask != null && scheduledTask.deadlineNanos() <= nanoTime();
     }
 
@@ -153,7 +175,9 @@ public abstract class AbstractScheduledEventExecutor extends AbstractEventExecut
         validateScheduled0(delay, unit);
 
         return schedule(new ScheduledFutureTask<Void>(
-                this, command, null, ScheduledFutureTask.deadlineNanos(unit.toNanos(delay))));
+                this,
+                command,
+                deadlineNanos(unit.toNanos(delay))));
     }
 
     @Override
@@ -165,8 +189,7 @@ public abstract class AbstractScheduledEventExecutor extends AbstractEventExecut
         }
         validateScheduled0(delay, unit);
 
-        return schedule(new ScheduledFutureTask<V>(
-                this, callable, ScheduledFutureTask.deadlineNanos(unit.toNanos(delay))));
+        return schedule(new ScheduledFutureTask<V>(this, callable, deadlineNanos(unit.toNanos(delay))));
     }
 
     @Override
@@ -185,8 +208,7 @@ public abstract class AbstractScheduledEventExecutor extends AbstractEventExecut
         validateScheduled0(period, unit);
 
         return schedule(new ScheduledFutureTask<Void>(
-                this, Executors.<Void>callable(command, null),
-                ScheduledFutureTask.deadlineNanos(unit.toNanos(initialDelay)), unit.toNanos(period)));
+                this, command, deadlineNanos(unit.toNanos(initialDelay)), unit.toNanos(period)));
     }
 
     @Override
@@ -206,8 +228,7 @@ public abstract class AbstractScheduledEventExecutor extends AbstractEventExecut
         validateScheduled0(delay, unit);
 
         return schedule(new ScheduledFutureTask<Void>(
-                this, Executors.<Void>callable(command, null),
-                ScheduledFutureTask.deadlineNanos(unit.toNanos(initialDelay)), -unit.toNanos(delay)));
+                this, command, deadlineNanos(unit.toNanos(initialDelay)), -unit.toNanos(delay)));
     }
 
     @SuppressWarnings("deprecation")
@@ -225,31 +246,65 @@ public abstract class AbstractScheduledEventExecutor extends AbstractEventExecut
         // NOOP
     }
 
-    <V> ScheduledFuture<V> schedule(final ScheduledFutureTask<V> task) {
+    final void scheduleFromEventLoop(final ScheduledFutureTask<?> task) {
+        // nextTaskId a long and so there is no chance it will overflow back to 0
+        scheduledTaskQueue().add(task.setId(++nextTaskId));
+    }
+
+    private <V> ScheduledFuture<V> schedule(final ScheduledFutureTask<V> task) {
         if (inEventLoop()) {
-            scheduledTaskQueue().add(task);
+            scheduleFromEventLoop(task);
         } else {
-            execute(new Runnable() {
-                @Override
-                public void run() {
-                    scheduledTaskQueue().add(task);
+            final long deadlineNanos = task.deadlineNanos();
+            // task will add itself to scheduled task queue when run if not expired
+            if (beforeScheduledTaskSubmitted(deadlineNanos)) {
+                execute(task);
+            } else {
+                lazyExecute(task);
+                // Second hook after scheduling to facilitate race-avoidance
+                if (afterScheduledTaskSubmitted(deadlineNanos)) {
+                    execute(WAKEUP_TASK);
                 }
-            });
+            }
         }
 
         return task;
     }
 
     final void removeScheduled(final ScheduledFutureTask<?> task) {
+        assert task.isCancelled();
         if (inEventLoop()) {
             scheduledTaskQueue().removeTyped(task);
         } else {
-            execute(new Runnable() {
-                @Override
-                public void run() {
-                    removeScheduled(task);
-                }
-            });
+            // task will remove itself from scheduled task queue when it runs
+            lazyExecute(task);
         }
     }
+
+    /**
+     * Called from arbitrary non-{@link EventExecutor} threads prior to scheduled task submission.
+     * Returns {@code true} if the {@link EventExecutor} thread should be woken immediately to
+     * process the scheduled task (if not already awake).
+     * <p>
+     * If {@code false} is returned, {@link #afterScheduledTaskSubmitted(long)} will be called with
+     * the same value <i>after</i> the scheduled task is enqueued, providing another opportunity
+     * to wake the {@link EventExecutor} thread if required.
+     *
+     * @param deadlineNanos deadline of the to-be-scheduled task
+     *     relative to {@link AbstractScheduledEventExecutor#nanoTime()}
+     * @return {@code true} if the {@link EventExecutor} thread should be woken, {@code false} otherwise
+     */
+    protected boolean beforeScheduledTaskSubmitted(long deadlineNanos) {
+        return true;
+    }
+
+    /**
+     * See {@link #beforeScheduledTaskSubmitted(long)}. Called only after that method returns false.
+     *
+     * @param deadlineNanos relative to {@link AbstractScheduledEventExecutor#nanoTime()}
+     * @return  {@code true} if the {@link EventExecutor} thread should be woken, {@code false} otherwise
+     */
+    protected boolean afterScheduledTaskSubmitted(long deadlineNanos) {
+        return true;
+    }
 }
diff --git a/common/src/main/java/io/netty/util/concurrent/CompleteFuture.java b/common/src/main/java/io/netty/util/concurrent/CompleteFuture.java
index ed5755f..545c84e 100644
--- a/common/src/main/java/io/netty/util/concurrent/CompleteFuture.java
+++ b/common/src/main/java/io/netty/util/concurrent/CompleteFuture.java
@@ -16,6 +16,8 @@
 
 package io.netty.util.concurrent;
 
+import io.netty.util.internal.ObjectUtil;
+
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -43,19 +45,15 @@ public abstract class CompleteFuture<V> extends AbstractFuture<V> {
 
     @Override
     public Future<V> addListener(GenericFutureListener<? extends Future<? super V>> listener) {
-        if (listener == null) {
-            throw new NullPointerException("listener");
-        }
-        DefaultPromise.notifyListener(executor(), this, listener);
+        DefaultPromise.notifyListener(executor(), this, ObjectUtil.checkNotNull(listener, "listener"));
         return this;
     }
 
     @Override
     public Future<V> addListeners(GenericFutureListener<? extends Future<? super V>>... listeners) {
-        if (listeners == null) {
-            throw new NullPointerException("listeners");
-        }
-        for (GenericFutureListener<? extends Future<? super V>> l: listeners) {
+        for (GenericFutureListener<? extends Future<? super V>> l:
+                ObjectUtil.checkNotNull(listeners, "listeners")) {
+
             if (l == null) {
                 break;
             }
diff --git a/common/src/main/java/io/netty/util/concurrent/DefaultPromise.java b/common/src/main/java/io/netty/util/concurrent/DefaultPromise.java
index a910e40..bc7a09b 100644
--- a/common/src/main/java/io/netty/util/concurrent/DefaultPromise.java
+++ b/common/src/main/java/io/netty/util/concurrent/DefaultPromise.java
@@ -24,7 +24,9 @@ import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 
 import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
 
 import static io.netty.util.internal.ObjectUtil.checkNotNull;
@@ -43,6 +45,7 @@ public class DefaultPromise<V> extends AbstractFuture<V> implements Promise<V> {
     private static final Object UNCANCELLABLE = new Object();
     private static final CauseHolder CANCELLATION_CAUSE_HOLDER = new CauseHolder(ThrowableUtil.unknownStackTrace(
             new CancellationException(), DefaultPromise.class, "cancel(...)"));
+    private static final StackTraceElement[] CANCELLATION_STACK = CANCELLATION_CAUSE_HOLDER.cause.getStackTrace();
 
     private volatile Object result;
     private final EventExecutor executor;
@@ -91,7 +94,6 @@ public class DefaultPromise<V> extends AbstractFuture<V> implements Promise<V> {
     @Override
     public Promise<V> setSuccess(V result) {
         if (setSuccess0(result)) {
-            notifyListeners();
             return this;
         }
         throw new IllegalStateException("complete already: " + this);
@@ -99,17 +101,12 @@ public class DefaultPromise<V> extends AbstractFuture<V> implements Promise<V> {
 
     @Override
     public boolean trySuccess(V result) {
-        if (setSuccess0(result)) {
-            notifyListeners();
-            return true;
-        }
-        return false;
+        return setSuccess0(result);
     }
 
     @Override
     public Promise<V> setFailure(Throwable cause) {
         if (setFailure0(cause)) {
-            notifyListeners();
             return this;
         }
         throw new IllegalStateException("complete already: " + this, cause);
@@ -117,11 +114,7 @@ public class DefaultPromise<V> extends AbstractFuture<V> implements Promise<V> {
 
     @Override
     public boolean tryFailure(Throwable cause) {
-        if (setFailure0(cause)) {
-            notifyListeners();
-            return true;
-        }
-        return false;
+        return setFailure0(cause);
     }
 
     @Override
@@ -144,10 +137,38 @@ public class DefaultPromise<V> extends AbstractFuture<V> implements Promise<V> {
         return result == null;
     }
 
+    private static final class LeanCancellationException extends CancellationException {
+        private static final long serialVersionUID = 2794674970981187807L;
+
+        @Override
+        public Throwable fillInStackTrace() {
+            setStackTrace(CANCELLATION_STACK);
+            return this;
+        }
+
+        @Override
+        public String toString() {
+            return CancellationException.class.getName();
+        }
+    }
+
     @Override
     public Throwable cause() {
-        Object result = this.result;
-        return (result instanceof CauseHolder) ? ((CauseHolder) result).cause : null;
+        return cause0(result);
+    }
+
+    private Throwable cause0(Object result) {
+        if (!(result instanceof CauseHolder)) {
+            return null;
+        }
+        if (result == CANCELLATION_CAUSE_HOLDER) {
+            CancellationException ce = new LeanCancellationException();
+            if (RESULT_UPDATER.compareAndSet(this, CANCELLATION_CAUSE_HOLDER, new CauseHolder(ce))) {
+                return ce;
+            }
+            result = this.result;
+        }
+        return ((CauseHolder) result).cause;
     }
 
     @Override
@@ -307,6 +328,50 @@ public class DefaultPromise<V> extends AbstractFuture<V> implements Promise<V> {
         return (V) result;
     }
 
+    @SuppressWarnings("unchecked")
+    @Override
+    public V get() throws InterruptedException, ExecutionException {
+        Object result = this.result;
+        if (!isDone0(result)) {
+            await();
+            result = this.result;
+        }
+        if (result == SUCCESS || result == UNCANCELLABLE) {
+            return null;
+        }
+        Throwable cause = cause0(result);
+        if (cause == null) {
+            return (V) result;
+        }
+        if (cause instanceof CancellationException) {
+            throw (CancellationException) cause;
+        }
+        throw new ExecutionException(cause);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
+        Object result = this.result;
+        if (!isDone0(result)) {
+            if (!await(timeout, unit)) {
+                throw new TimeoutException();
+            }
+            result = this.result;
+        }
+        if (result == SUCCESS || result == UNCANCELLABLE) {
+            return null;
+        }
+        Throwable cause = cause0(result);
+        if (cause == null) {
+            return (V) result;
+        }
+        if (cause instanceof CancellationException) {
+            throw (CancellationException) cause;
+        }
+        throw new ExecutionException(cause);
+    }
+
     /**
      * {@inheritDoc}
      *
@@ -315,8 +380,9 @@ public class DefaultPromise<V> extends AbstractFuture<V> implements Promise<V> {
     @Override
     public boolean cancel(boolean mayInterruptIfRunning) {
         if (RESULT_UPDATER.compareAndSet(this, null, CANCELLATION_CAUSE_HOLDER)) {
-            checkNotifyWaiters();
-            notifyListeners();
+            if (checkNotifyWaiters()) {
+                notifyListeners();
+            }
             return true;
         }
         return false;
@@ -407,10 +473,10 @@ public class DefaultPromise<V> extends AbstractFuture<V> implements Promise<V> {
      */
     protected static void notifyListener(
             EventExecutor eventExecutor, final Future<?> future, final GenericFutureListener<?> listener) {
-        checkNotNull(eventExecutor, "eventExecutor");
-        checkNotNull(future, "future");
-        checkNotNull(listener, "listener");
-        notifyListenerWithStackOverFlowProtection(eventExecutor, future, listener);
+        notifyListenerWithStackOverFlowProtection(
+                checkNotNull(eventExecutor, "eventExecutor"),
+                checkNotNull(future, "future"),
+                checkNotNull(listener, "listener"));
     }
 
     private void notifyListeners() {
@@ -545,16 +611,23 @@ public class DefaultPromise<V> extends AbstractFuture<V> implements Promise<V> {
     private boolean setValue0(Object objResult) {
         if (RESULT_UPDATER.compareAndSet(this, null, objResult) ||
             RESULT_UPDATER.compareAndSet(this, UNCANCELLABLE, objResult)) {
-            checkNotifyWaiters();
+            if (checkNotifyWaiters()) {
+                notifyListeners();
+            }
             return true;
         }
         return false;
     }
 
-    private synchronized void checkNotifyWaiters() {
+    /**
+     * Check if there are any waiters and if so notify these.
+     * @return {@code true} if there are any listeners attached to the promise, {@code false} otherwise.
+     */
+    private synchronized boolean checkNotifyWaiters() {
         if (waiters > 0) {
             notifyAll();
         }
+        return listeners != null;
     }
 
     private void incWaiters() {
diff --git a/common/src/main/java/io/netty/util/concurrent/DefaultThreadFactory.java b/common/src/main/java/io/netty/util/concurrent/DefaultThreadFactory.java
index 15c7658..7c63270 100644
--- a/common/src/main/java/io/netty/util/concurrent/DefaultThreadFactory.java
+++ b/common/src/main/java/io/netty/util/concurrent/DefaultThreadFactory.java
@@ -16,6 +16,7 @@
 
 package io.netty.util.concurrent;
 
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.StringUtil;
 
 import java.util.Locale;
@@ -64,9 +65,7 @@ public class DefaultThreadFactory implements ThreadFactory {
     }
 
     public static String toPoolName(Class<?> poolType) {
-        if (poolType == null) {
-            throw new NullPointerException("poolType");
-        }
+        ObjectUtil.checkNotNull(poolType, "poolType");
 
         String poolName = StringUtil.simpleClassName(poolType);
         switch (poolName.length()) {
@@ -84,9 +83,8 @@ public class DefaultThreadFactory implements ThreadFactory {
     }
 
     public DefaultThreadFactory(String poolName, boolean daemon, int priority, ThreadGroup threadGroup) {
-        if (poolName == null) {
-            throw new NullPointerException("poolName");
-        }
+        ObjectUtil.checkNotNull(poolName, "poolName");
+
         if (priority < Thread.MIN_PRIORITY || priority > Thread.MAX_PRIORITY) {
             throw new IllegalArgumentException(
                     "priority: " + priority + " (expected: Thread.MIN_PRIORITY <= priority <= Thread.MAX_PRIORITY)");
diff --git a/common/src/main/java/io/netty/util/concurrent/FailedFuture.java b/common/src/main/java/io/netty/util/concurrent/FailedFuture.java
index 31ce888..b246913 100644
--- a/common/src/main/java/io/netty/util/concurrent/FailedFuture.java
+++ b/common/src/main/java/io/netty/util/concurrent/FailedFuture.java
@@ -15,6 +15,7 @@
  */
 package io.netty.util.concurrent;
 
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.PlatformDependent;
 
 /**
@@ -34,10 +35,7 @@ public final class FailedFuture<V> extends CompleteFuture<V> {
      */
     public FailedFuture(EventExecutor executor, Throwable cause) {
         super(executor);
-        if (cause == null) {
-            throw new NullPointerException("cause");
-        }
-        this.cause = cause;
+        this.cause = ObjectUtil.checkNotNull(cause, "cause");
     }
 
     @Override
diff --git a/common/src/main/java/io/netty/util/concurrent/FastThreadLocal.java b/common/src/main/java/io/netty/util/concurrent/FastThreadLocal.java
index 9d808c7..ed83fb4 100644
--- a/common/src/main/java/io/netty/util/concurrent/FastThreadLocal.java
+++ b/common/src/main/java/io/netty/util/concurrent/FastThreadLocal.java
@@ -139,33 +139,22 @@ public class FastThreadLocal<V> {
             return (V) v;
         }
 
-        V value = initialize(threadLocalMap);
-        registerCleaner(threadLocalMap);
-        return value;
+        return initialize(threadLocalMap);
     }
 
-    private void registerCleaner(final InternalThreadLocalMap threadLocalMap) {
-        Thread current = Thread.currentThread();
-        if (FastThreadLocalThread.willCleanupFastThreadLocals(current) || threadLocalMap.isCleanerFlagSet(index)) {
-            return;
-        }
-
-        threadLocalMap.setCleanerFlag(index);
-
-        // TODO: We need to find a better way to handle this.
-        /*
-        // We will need to ensure we will trigger remove(InternalThreadLocalMap) so everything will be released
-        // and FastThreadLocal.onRemoval(...) will be called.
-        ObjectCleaner.register(current, new Runnable() {
-            @Override
-            public void run() {
-                remove(threadLocalMap);
-
-                // It's fine to not call InternalThreadLocalMap.remove() here as this will only be triggered once
-                // the Thread is collected by GC. In this case the ThreadLocal will be gone away already.
+    /**
+     * Returns the current value for the current thread if it exists, {@code null} otherwise.
+     */
+    @SuppressWarnings("unchecked")
+    public final V getIfExists() {
+        InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.getIfSet();
+        if (threadLocalMap != null) {
+            Object v = threadLocalMap.indexedVariable(index);
+            if (v != InternalThreadLocalMap.UNSET) {
+                return (V) v;
             }
-        });
-        */
+        }
+        return null;
     }
 
     /**
@@ -201,9 +190,7 @@ public class FastThreadLocal<V> {
     public final void set(V value) {
         if (value != InternalThreadLocalMap.UNSET) {
             InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
-            if (setKnownNotUnset(threadLocalMap, value)) {
-                registerCleaner(threadLocalMap);
-            }
+            setKnownNotUnset(threadLocalMap, value);
         } else {
             remove();
         }
@@ -223,12 +210,10 @@ public class FastThreadLocal<V> {
     /**
      * @return see {@link InternalThreadLocalMap#setIndexedVariable(int, Object)}.
      */
-    private boolean setKnownNotUnset(InternalThreadLocalMap threadLocalMap, V value) {
+    private void setKnownNotUnset(InternalThreadLocalMap threadLocalMap, V value) {
         if (threadLocalMap.setIndexedVariable(index, value)) {
             addToVariablesToRemove(threadLocalMap, this);
-            return true;
         }
-        return false;
     }
 
     /**
diff --git a/common/src/main/java/io/netty/util/concurrent/Future.java b/common/src/main/java/io/netty/util/concurrent/Future.java
index 16ffa72..e455904 100644
--- a/common/src/main/java/io/netty/util/concurrent/Future.java
+++ b/common/src/main/java/io/netty/util/concurrent/Future.java
@@ -155,14 +155,14 @@ public interface Future<V> extends java.util.concurrent.Future<V> {
      * Return the result without blocking. If the future is not done yet this will return {@code null}.
      *
      * As it is possible that a {@code null} value is used to mark the future as successful you also need to check
-     * if the future is really done with {@link #isDone()} and not relay on the returned {@code null} value.
+     * if the future is really done with {@link #isDone()} and not rely on the returned {@code null} value.
      */
     V getNow();
 
     /**
      * {@inheritDoc}
      *
-     * If the cancellation was successful it will fail the future with an {@link CancellationException}.
+     * If the cancellation was successful it will fail the future with a {@link CancellationException}.
      */
     @Override
     boolean cancel(boolean mayInterruptIfRunning);
diff --git a/common/src/main/java/io/netty/util/concurrent/GlobalEventExecutor.java b/common/src/main/java/io/netty/util/concurrent/GlobalEventExecutor.java
index 7e2527d..962fdff 100644
--- a/common/src/main/java/io/netty/util/concurrent/GlobalEventExecutor.java
+++ b/common/src/main/java/io/netty/util/concurrent/GlobalEventExecutor.java
@@ -15,6 +15,8 @@
  */
 package io.netty.util.concurrent;
 
+import io.netty.util.internal.ObjectUtil;
+import io.netty.util.internal.ThreadExecutorMap;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 
@@ -34,7 +36,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
  * task pending in the task queue for 1 second.  Please note it is not scalable to schedule large number of tasks to
  * this executor; use a dedicated executor.
  */
-public final class GlobalEventExecutor extends AbstractScheduledEventExecutor {
+public final class GlobalEventExecutor extends AbstractScheduledEventExecutor implements OrderedEventExecutor {
 
     private static final InternalLogger logger = InternalLoggerFactory.getInstance(GlobalEventExecutor.class);
 
@@ -55,8 +57,7 @@ public final class GlobalEventExecutor extends AbstractScheduledEventExecutor {
     // can trigger the creation of a thread from arbitrary thread groups; for this reason, the thread factory must not
     // be sticky about its thread group
     // visible for testing
-    final ThreadFactory threadFactory =
-            new DefaultThreadFactory(DefaultThreadFactory.toPoolName(getClass()), false, Thread.NORM_PRIORITY, null);
+    final ThreadFactory threadFactory;
     private final TaskRunner taskRunner = new TaskRunner();
     private final AtomicBoolean started = new AtomicBoolean();
     volatile Thread thread;
@@ -65,6 +66,8 @@ public final class GlobalEventExecutor extends AbstractScheduledEventExecutor {
 
     private GlobalEventExecutor() {
         scheduledTaskQueue().add(quietPeriodTask);
+        threadFactory = ThreadExecutorMap.apply(new DefaultThreadFactory(
+                DefaultThreadFactory.toPoolName(getClass()), false, Thread.NORM_PRIORITY, null), this);
     }
 
     /**
@@ -86,7 +89,7 @@ public final class GlobalEventExecutor extends AbstractScheduledEventExecutor {
                 return task;
             } else {
                 long delayNanos = scheduledTask.delayNanos();
-                Runnable task;
+                Runnable task = null;
                 if (delayNanos > 0) {
                     try {
                         task = taskQueue.poll(delayNanos, TimeUnit.NANOSECONDS);
@@ -94,11 +97,12 @@ public final class GlobalEventExecutor extends AbstractScheduledEventExecutor {
                         // Waken up.
                         return null;
                     }
-                } else {
-                    task = taskQueue.poll();
                 }
-
                 if (task == null) {
+                    // We need to fetch the scheduled tasks now as otherwise there may be a chance that
+                    // scheduled tasks are never executed if there is always one task in the taskQueue.
+                    // This is for example true for the read task of OIO Transport
+                    // See https://github.com/netty/netty/issues/1614
                     fetchFromScheduledTaskQueue();
                     task = taskQueue.poll();
                 }
@@ -134,10 +138,7 @@ public final class GlobalEventExecutor extends AbstractScheduledEventExecutor {
      * before.
      */
     private void addTask(Runnable task) {
-        if (task == null) {
-            throw new NullPointerException("task");
-        }
-        taskQueue.add(task);
+        taskQueue.add(ObjectUtil.checkNotNull(task, "task"));
     }
 
     @Override
@@ -190,9 +191,7 @@ public final class GlobalEventExecutor extends AbstractScheduledEventExecutor {
      * @return {@code true} if and only if the worker thread has been terminated
      */
     public boolean awaitInactivity(long timeout, TimeUnit unit) throws InterruptedException {
-        if (unit == null) {
-            throw new NullPointerException("unit");
-        }
+        ObjectUtil.checkNotNull(unit, "unit");
 
         final Thread thread = this.thread;
         if (thread == null) {
@@ -204,11 +203,7 @@ public final class GlobalEventExecutor extends AbstractScheduledEventExecutor {
 
     @Override
     public void execute(Runnable task) {
-        if (task == null) {
-            throw new NullPointerException("task");
-        }
-
-        addTask(task);
+        addTask(ObjectUtil.checkNotNull(task, "task"));
         if (!inEventLoop()) {
             startThread();
         }
diff --git a/common/src/main/java/io/netty/util/concurrent/ImmediateEventExecutor.java b/common/src/main/java/io/netty/util/concurrent/ImmediateEventExecutor.java
index f952cf2..9173711 100644
--- a/common/src/main/java/io/netty/util/concurrent/ImmediateEventExecutor.java
+++ b/common/src/main/java/io/netty/util/concurrent/ImmediateEventExecutor.java
@@ -15,6 +15,7 @@
  */
 package io.netty.util.concurrent;
 
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 
@@ -102,9 +103,7 @@ public final class ImmediateEventExecutor extends AbstractEventExecutor {
 
     @Override
     public void execute(Runnable command) {
-        if (command == null) {
-            throw new NullPointerException("command");
-        }
+        ObjectUtil.checkNotNull(command, "command");
         if (!RUNNING.get()) {
             RUNNING.set(true);
             try {
diff --git a/common/src/main/java/io/netty/util/concurrent/ImmediateExecutor.java b/common/src/main/java/io/netty/util/concurrent/ImmediateExecutor.java
index fa68e0b..6a92355 100644
--- a/common/src/main/java/io/netty/util/concurrent/ImmediateExecutor.java
+++ b/common/src/main/java/io/netty/util/concurrent/ImmediateExecutor.java
@@ -15,6 +15,8 @@
  */
 package io.netty.util.concurrent;
 
+import io.netty.util.internal.ObjectUtil;
+
 import java.util.concurrent.Executor;
 
 /**
@@ -23,15 +25,12 @@ import java.util.concurrent.Executor;
 public final class ImmediateExecutor implements Executor {
     public static final ImmediateExecutor INSTANCE = new ImmediateExecutor();
 
-    private  ImmediateExecutor() {
+    private ImmediateExecutor() {
         // use static instance
     }
 
     @Override
     public void execute(Runnable command) {
-        if (command == null) {
-            throw new NullPointerException("command");
-        }
-        command.run();
+        ObjectUtil.checkNotNull(command, "command").run();
     }
 }
diff --git a/common/src/main/java/io/netty/util/concurrent/NonStickyEventExecutorGroup.java b/common/src/main/java/io/netty/util/concurrent/NonStickyEventExecutorGroup.java
index bcc4b82..a2692f6 100644
--- a/common/src/main/java/io/netty/util/concurrent/NonStickyEventExecutorGroup.java
+++ b/common/src/main/java/io/netty/util/concurrent/NonStickyEventExecutorGroup.java
@@ -274,7 +274,7 @@ public final class NonStickyEventExecutorGroup implements EventExecutorGroup {
                         //
                         // The above cases can be distinguished by performing a
                         // compareAndSet(NONE, RUNNING). If it returns "false", it is case 1; otherwise it is case 2.
-                        if (tasks.peek() == null || !state.compareAndSet(NONE, RUNNING)) {
+                        if (tasks.isEmpty() || !state.compareAndSet(NONE, RUNNING)) {
                             return; // done
                         }
                     }
diff --git a/common/src/main/java/io/netty/util/concurrent/PromiseAggregator.java b/common/src/main/java/io/netty/util/concurrent/PromiseAggregator.java
index 7d908c0..e216b6e 100644
--- a/common/src/main/java/io/netty/util/concurrent/PromiseAggregator.java
+++ b/common/src/main/java/io/netty/util/concurrent/PromiseAggregator.java
@@ -16,11 +16,13 @@
 
 package io.netty.util.concurrent;
 
+import io.netty.util.internal.ObjectUtil;
+
 import java.util.LinkedHashSet;
 import java.util.Set;
 
 /**
- * @deprecated Use {@link PromiseCombiner}
+ * @deprecated Use {@link PromiseCombiner#PromiseCombiner(EventExecutor)}.
  *
  * {@link GenericFutureListener} implementation which consolidates multiple {@link Future}s
  * into one, by listening to individual {@link Future}s and producing an aggregated result
@@ -43,10 +45,7 @@ public class PromiseAggregator<V, F extends Future<V>> implements GenericFutureL
      * @param failPending  {@code true} to fail pending promises, false to leave them unaffected
      */
     public PromiseAggregator(Promise<Void> aggregatePromise, boolean failPending) {
-        if (aggregatePromise == null) {
-            throw new NullPointerException("aggregatePromise");
-        }
-        this.aggregatePromise = aggregatePromise;
+        this.aggregatePromise = ObjectUtil.checkNotNull(aggregatePromise, "aggregatePromise");
         this.failPending = failPending;
     }
 
@@ -63,9 +62,7 @@ public class PromiseAggregator<V, F extends Future<V>> implements GenericFutureL
      */
     @SafeVarargs
     public final PromiseAggregator<V, F> add(Promise<V>... promises) {
-        if (promises == null) {
-            throw new NullPointerException("promises");
-        }
+        ObjectUtil.checkNotNull(promises, "promises");
         if (promises.length == 0) {
             return this;
         }
diff --git a/common/src/main/java/io/netty/util/concurrent/PromiseCombiner.java b/common/src/main/java/io/netty/util/concurrent/PromiseCombiner.java
index 6624f05..8895c1a 100644
--- a/common/src/main/java/io/netty/util/concurrent/PromiseCombiner.java
+++ b/common/src/main/java/io/netty/util/concurrent/PromiseCombiner.java
@@ -28,26 +28,62 @@ import io.netty.util.internal.ObjectUtil;
  * {@link PromiseCombiner#add(Future)} and {@link PromiseCombiner#addAll(Future[])} methods. When all futures to be
  * combined have been added, callers must provide an aggregate promise to be notified when all combined promises have
  * finished via the {@link PromiseCombiner#finish(Promise)} method.</p>
+ *
+ * <p>This implementation is <strong>NOT</strong> thread-safe and all methods must be called
+ * from the {@link EventExecutor} thread.</p>
  */
 public final class PromiseCombiner {
     private int expectedCount;
     private int doneCount;
-    private boolean doneAdding;
     private Promise<Void> aggregatePromise;
     private Throwable cause;
     private final GenericFutureListener<Future<?>> listener = new GenericFutureListener<Future<?>>() {
         @Override
-        public void operationComplete(Future<?> future) throws Exception {
+        public void operationComplete(final Future<?> future) {
+            if (executor.inEventLoop()) {
+                operationComplete0(future);
+            } else {
+                executor.execute(new Runnable() {
+                    @Override
+                    public void run() {
+                        operationComplete0(future);
+                    }
+                });
+            }
+        }
+
+        private void operationComplete0(Future<?> future) {
+            assert executor.inEventLoop();
             ++doneCount;
             if (!future.isSuccess() && cause == null) {
                 cause = future.cause();
             }
-            if (doneCount == expectedCount && doneAdding) {
+            if (doneCount == expectedCount && aggregatePromise != null) {
                 tryPromise();
             }
         }
     };
 
+    private final EventExecutor executor;
+
+    /**
+     * Deprecated use {@link PromiseCombiner#PromiseCombiner(EventExecutor)}.
+     */
+    @Deprecated
+    public PromiseCombiner() {
+        this(ImmediateEventExecutor.INSTANCE);
+    }
+
+    /**
+     * The {@link EventExecutor} to use for notifications. You must call {@link #add(Future)}, {@link #addAll(Future[])}
+     * and {@link #finish(Promise)} from within the {@link EventExecutor} thread.
+     *
+     * @param executor the {@link EventExecutor} to use for notifications.
+     */
+    public PromiseCombiner(EventExecutor executor) {
+        this.executor = ObjectUtil.checkNotNull(executor, "executor");
+    }
+
     /**
      * Adds a new promise to be combined. New promises may be added until an aggregate promise is added via the
      * {@link PromiseCombiner#finish(Promise)} method.
@@ -70,6 +106,7 @@ public final class PromiseCombiner {
     @SuppressWarnings({ "unchecked", "rawtypes" })
     public void add(Future future) {
         checkAddAllowed();
+        checkInEventLoop();
         ++expectedCount;
         future.addListener(listener);
     }
@@ -112,22 +149,29 @@ public final class PromiseCombiner {
      * @param aggregatePromise the promise to notify when all combined futures have finished
      */
     public void finish(Promise<Void> aggregatePromise) {
-        if (doneAdding) {
+        ObjectUtil.checkNotNull(aggregatePromise, "aggregatePromise");
+        checkInEventLoop();
+        if (this.aggregatePromise != null) {
             throw new IllegalStateException("Already finished");
         }
-        doneAdding = true;
-        this.aggregatePromise = ObjectUtil.checkNotNull(aggregatePromise, "aggregatePromise");
+        this.aggregatePromise = aggregatePromise;
         if (doneCount == expectedCount) {
             tryPromise();
         }
     }
 
+    private void checkInEventLoop() {
+        if (!executor.inEventLoop()) {
+            throw new IllegalStateException("Must be called from EventExecutor thread");
+        }
+    }
+
     private boolean tryPromise() {
         return (cause == null) ? aggregatePromise.trySuccess(null) : aggregatePromise.tryFailure(cause);
     }
 
     private void checkAddAllowed() {
-        if (doneAdding) {
+        if (aggregatePromise != null) {
             throw new IllegalStateException("Adding promises is not allowed after finished adding");
         }
     }
diff --git a/common/src/main/java/io/netty/util/concurrent/PromiseTask.java b/common/src/main/java/io/netty/util/concurrent/PromiseTask.java
index 8cb23c7..0025815 100644
--- a/common/src/main/java/io/netty/util/concurrent/PromiseTask.java
+++ b/common/src/main/java/io/netty/util/concurrent/PromiseTask.java
@@ -20,10 +20,6 @@ import java.util.concurrent.RunnableFuture;
 
 class PromiseTask<V> extends DefaultPromise<V> implements RunnableFuture<V> {
 
-    static <T> Callable<T> toCallable(Runnable runnable, T result) {
-        return new RunnableAdapter<T>(runnable, result);
-    }
-
     private static final class RunnableAdapter<T> implements Callable<T> {
         final Runnable task;
         final T result;
@@ -45,10 +41,37 @@ class PromiseTask<V> extends DefaultPromise<V> implements RunnableFuture<V> {
         }
     }
 
-    protected final Callable<V> task;
+    private static final Runnable COMPLETED = new SentinelRunnable("COMPLETED");
+    private static final Runnable CANCELLED = new SentinelRunnable("CANCELLED");
+    private static final Runnable FAILED = new SentinelRunnable("FAILED");
+
+    private static class SentinelRunnable implements Runnable {
+        private final String name;
+
+        SentinelRunnable(String name) {
+            this.name = name;
+        }
+
+        @Override
+        public void run() { } // no-op
+
+        @Override
+        public String toString() {
+            return name;
+        }
+    }
+
+    // Strictly of type Callable<V> or Runnable
+    private Object task;
 
     PromiseTask(EventExecutor executor, Runnable runnable, V result) {
-        this(executor, toCallable(runnable, result));
+        super(executor);
+        task = result == null ? runnable : new RunnableAdapter<V>(runnable, result);
+    }
+
+    PromiseTask(EventExecutor executor, Runnable runnable) {
+        super(executor);
+        task = runnable;
     }
 
     PromiseTask(EventExecutor executor, Callable<V> callable) {
@@ -66,11 +89,21 @@ class PromiseTask<V> extends DefaultPromise<V> implements RunnableFuture<V> {
         return this == obj;
     }
 
+    @SuppressWarnings("unchecked")
+    final V runTask() throws Exception {
+        final Object task = this.task;
+        if (task instanceof Callable) {
+            return ((Callable<V>) task).call();
+        }
+        ((Runnable) task).run();
+        return null;
+    }
+
     @Override
     public void run() {
         try {
             if (setUncancellableInternal()) {
-                V result = task.call();
+                V result = runTask();
                 setSuccessInternal(result);
             }
         } catch (Throwable e) {
@@ -78,6 +111,17 @@ class PromiseTask<V> extends DefaultPromise<V> implements RunnableFuture<V> {
         }
     }
 
+    private boolean clearTaskAfterCompletion(boolean done, Runnable result) {
+        if (done) {
+            // The only time where it might be possible for the sentinel task
+            // to be called is in the case of a periodic ScheduledFutureTask,
+            // in which case it's a benign race with cancellation and the (null)
+            // return value is not used.
+            task = result;
+        }
+        return done;
+    }
+
     @Override
     public final Promise<V> setFailure(Throwable cause) {
         throw new IllegalStateException();
@@ -85,6 +129,7 @@ class PromiseTask<V> extends DefaultPromise<V> implements RunnableFuture<V> {
 
     protected final Promise<V> setFailureInternal(Throwable cause) {
         super.setFailure(cause);
+        clearTaskAfterCompletion(true, FAILED);
         return this;
     }
 
@@ -94,7 +139,7 @@ class PromiseTask<V> extends DefaultPromise<V> implements RunnableFuture<V> {
     }
 
     protected final boolean tryFailureInternal(Throwable cause) {
-        return super.tryFailure(cause);
+        return clearTaskAfterCompletion(super.tryFailure(cause), FAILED);
     }
 
     @Override
@@ -104,6 +149,7 @@ class PromiseTask<V> extends DefaultPromise<V> implements RunnableFuture<V> {
 
     protected final Promise<V> setSuccessInternal(V result) {
         super.setSuccess(result);
+        clearTaskAfterCompletion(true, COMPLETED);
         return this;
     }
 
@@ -113,7 +159,7 @@ class PromiseTask<V> extends DefaultPromise<V> implements RunnableFuture<V> {
     }
 
     protected final boolean trySuccessInternal(V result) {
-        return super.trySuccess(result);
+        return clearTaskAfterCompletion(super.trySuccess(result), COMPLETED);
     }
 
     @Override
@@ -125,6 +171,11 @@ class PromiseTask<V> extends DefaultPromise<V> implements RunnableFuture<V> {
         return super.setUncancellable();
     }
 
+    @Override
+    public boolean cancel(boolean mayInterruptIfRunning) {
+        return clearTaskAfterCompletion(super.cancel(mayInterruptIfRunning), CANCELLED);
+    }
+
     @Override
     protected StringBuilder toStringBuilder() {
         StringBuilder buf = super.toStringBuilder();
diff --git a/common/src/main/java/io/netty/util/concurrent/ScheduledFutureTask.java b/common/src/main/java/io/netty/util/concurrent/ScheduledFutureTask.java
index 1eaa7b9..2278940 100644
--- a/common/src/main/java/io/netty/util/concurrent/ScheduledFutureTask.java
+++ b/common/src/main/java/io/netty/util/concurrent/ScheduledFutureTask.java
@@ -19,15 +19,12 @@ package io.netty.util.concurrent;
 import io.netty.util.internal.DefaultPriorityQueue;
 import io.netty.util.internal.PriorityQueueNode;
 
-import java.util.Queue;
 import java.util.concurrent.Callable;
 import java.util.concurrent.Delayed;
 import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicLong;
 
 @SuppressWarnings("ComparableImplementedButEqualsNotOverridden")
 final class ScheduledFutureTask<V> extends PromiseTask<V> implements ScheduledFuture<V>, PriorityQueueNode {
-    private static final AtomicLong nextTaskId = new AtomicLong();
     private static final long START_TIME = System.nanoTime();
 
     static long nanoTime() {
@@ -40,34 +37,44 @@ final class ScheduledFutureTask<V> extends PromiseTask<V> implements ScheduledFu
         return deadlineNanos < 0 ? Long.MAX_VALUE : deadlineNanos;
     }
 
-    private final long id = nextTaskId.getAndIncrement();
+    static long initialNanoTime() {
+        return START_TIME;
+    }
+
+    // set once when added to priority queue
+    private long id;
+
     private long deadlineNanos;
     /* 0 - no repeat, >0 - repeat at fixed rate, <0 - repeat with fixed delay */
     private final long periodNanos;
 
     private int queueIndex = INDEX_NOT_IN_QUEUE;
 
-    ScheduledFutureTask(
-            AbstractScheduledEventExecutor executor,
-            Runnable runnable, V result, long nanoTime) {
+    ScheduledFutureTask(AbstractScheduledEventExecutor executor,
+            Runnable runnable, long nanoTime) {
 
-        this(executor, toCallable(runnable, result), nanoTime);
+        super(executor, runnable);
+        deadlineNanos = nanoTime;
+        periodNanos = 0;
     }
 
-    ScheduledFutureTask(
-            AbstractScheduledEventExecutor executor,
+    ScheduledFutureTask(AbstractScheduledEventExecutor executor,
+            Runnable runnable, long nanoTime, long period) {
+
+        super(executor, runnable);
+        deadlineNanos = nanoTime;
+        periodNanos = validatePeriod(period);
+    }
+
+    ScheduledFutureTask(AbstractScheduledEventExecutor executor,
             Callable<V> callable, long nanoTime, long period) {
 
         super(executor, callable);
-        if (period == 0) {
-            throw new IllegalArgumentException("period: 0 (expected: != 0)");
-        }
         deadlineNanos = nanoTime;
-        periodNanos = period;
+        periodNanos = validatePeriod(period);
     }
 
-    ScheduledFutureTask(
-            AbstractScheduledEventExecutor executor,
+    ScheduledFutureTask(AbstractScheduledEventExecutor executor,
             Callable<V> callable, long nanoTime) {
 
         super(executor, callable);
@@ -75,6 +82,20 @@ final class ScheduledFutureTask<V> extends PromiseTask<V> implements ScheduledFu
         periodNanos = 0;
     }
 
+    private static long validatePeriod(long period) {
+        if (period == 0) {
+            throw new IllegalArgumentException("period: 0 (expected: != 0)");
+        }
+        return period;
+    }
+
+    ScheduledFutureTask<V> setId(long id) {
+        if (this.id == 0L) {
+            this.id = id;
+        }
+        return this;
+    }
+
     @Override
     protected EventExecutor executor() {
         return super.executor();
@@ -84,12 +105,26 @@ final class ScheduledFutureTask<V> extends PromiseTask<V> implements ScheduledFu
         return deadlineNanos;
     }
 
+    void setConsumed() {
+        // Optimization to avoid checking system clock again
+        // after deadline has passed and task has been dequeued
+        if (periodNanos == 0) {
+            assert nanoTime() >= deadlineNanos;
+            deadlineNanos = 0L;
+        }
+    }
+
     public long delayNanos() {
-        return Math.max(0, deadlineNanos() - nanoTime());
+        return deadlineToDelayNanos(deadlineNanos());
+    }
+
+    static long deadlineToDelayNanos(long deadlineNanos) {
+        return deadlineNanos == 0L ? 0L : Math.max(0L, deadlineNanos - nanoTime());
     }
 
     public long delayNanos(long currentTimeNanos) {
-        return Math.max(0, deadlineNanos() - (currentTimeNanos - START_TIME));
+        return deadlineNanos == 0L ? 0L
+                : Math.max(0L, deadlineNanos() - (currentTimeNanos - START_TIME));
     }
 
     @Override
@@ -111,9 +146,8 @@ final class ScheduledFutureTask<V> extends PromiseTask<V> implements ScheduledFu
             return 1;
         } else if (id < that.id) {
             return -1;
-        } else if (id == that.id) {
-            throw new Error();
         } else {
+            assert id != that.id;
             return 1;
         }
     }
@@ -122,28 +156,32 @@ final class ScheduledFutureTask<V> extends PromiseTask<V> implements ScheduledFu
     public void run() {
         assert executor().inEventLoop();
         try {
+            if (delayNanos() > 0L) {
+                // Not yet expired, need to add or remove from queue
+                if (isCancelled()) {
+                    scheduledExecutor().scheduledTaskQueue().removeTyped(this);
+                } else {
+                    scheduledExecutor().scheduleFromEventLoop(this);
+                }
+                return;
+            }
             if (periodNanos == 0) {
                 if (setUncancellableInternal()) {
-                    V result = task.call();
+                    V result = runTask();
                     setSuccessInternal(result);
                 }
             } else {
                 // check if is done as it may was cancelled
                 if (!isCancelled()) {
-                    task.call();
+                    runTask();
                     if (!executor().isShutdown()) {
-                        long p = periodNanos;
-                        if (p > 0) {
-                            deadlineNanos += p;
+                        if (periodNanos > 0) {
+                            deadlineNanos += periodNanos;
                         } else {
-                            deadlineNanos = nanoTime() - p;
+                            deadlineNanos = nanoTime() - periodNanos;
                         }
                         if (!isCancelled()) {
-                            // scheduledTaskQueue can never be null as we lazy init it before submit the task!
-                            Queue<ScheduledFutureTask<?>> scheduledTaskQueue =
-                                    ((AbstractScheduledEventExecutor) executor()).scheduledTaskQueue;
-                            assert scheduledTaskQueue != null;
-                            scheduledTaskQueue.add(this);
+                            scheduledExecutor().scheduledTaskQueue().add(this);
                         }
                     }
                 }
@@ -153,6 +191,10 @@ final class ScheduledFutureTask<V> extends PromiseTask<V> implements ScheduledFu
         }
     }
 
+    private AbstractScheduledEventExecutor scheduledExecutor() {
+        return (AbstractScheduledEventExecutor) executor();
+    }
+
     /**
      * {@inheritDoc}
      *
@@ -162,7 +204,7 @@ final class ScheduledFutureTask<V> extends PromiseTask<V> implements ScheduledFu
     public boolean cancel(boolean mayInterruptIfRunning) {
         boolean canceled = super.cancel(mayInterruptIfRunning);
         if (canceled) {
-            ((AbstractScheduledEventExecutor) executor()).removeScheduled(this);
+            scheduledExecutor().removeScheduled(this);
         }
         return canceled;
     }
@@ -176,9 +218,7 @@ final class ScheduledFutureTask<V> extends PromiseTask<V> implements ScheduledFu
         StringBuilder buf = super.toStringBuilder();
         buf.setCharAt(buf.length() - 1, ',');
 
-        return buf.append(" id: ")
-                  .append(id)
-                  .append(", deadline: ")
+        return buf.append(" deadline: ")
                   .append(deadlineNanos)
                   .append(", period: ")
                   .append(periodNanos)
diff --git a/common/src/main/java/io/netty/util/concurrent/SingleThreadEventExecutor.java b/common/src/main/java/io/netty/util/concurrent/SingleThreadEventExecutor.java
index ab2a914..f7ab9ac 100644
--- a/common/src/main/java/io/netty/util/concurrent/SingleThreadEventExecutor.java
+++ b/common/src/main/java/io/netty/util/concurrent/SingleThreadEventExecutor.java
@@ -18,6 +18,7 @@ package io.netty.util.concurrent;
 import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.PlatformDependent;
 import io.netty.util.internal.SystemPropertyUtil;
+import io.netty.util.internal.ThreadExecutorMap;
 import io.netty.util.internal.UnstableApi;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
@@ -31,11 +32,11 @@ import java.util.Queue;
 import java.util.Set;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.RejectedExecutionException;
-import java.util.concurrent.Semaphore;
 import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
@@ -60,12 +61,6 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx
     private static final int ST_SHUTDOWN = 4;
     private static final int ST_TERMINATED = 5;
 
-    private static final Runnable WAKEUP_TASK = new Runnable() {
-        @Override
-        public void run() {
-            // Do nothing.
-        }
-    };
     private static final Runnable NOOP_TASK = new Runnable() {
         @Override
         public void run() {
@@ -87,7 +82,7 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx
     private final Executor executor;
     private volatile boolean interrupted;
 
-    private final Semaphore threadLock = new Semaphore(0);
+    private final CountDownLatch threadLock = new CountDownLatch(1);
     private final Set<Runnable> shutdownHooks = new LinkedHashSet<Runnable>();
     private final boolean addTaskWakesUp;
     private final int maxPendingTasks;
@@ -161,11 +156,22 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx
         super(parent);
         this.addTaskWakesUp = addTaskWakesUp;
         this.maxPendingTasks = Math.max(16, maxPendingTasks);
-        this.executor = ObjectUtil.checkNotNull(executor, "executor");
+        this.executor = ThreadExecutorMap.apply(executor, this);
         taskQueue = newTaskQueue(this.maxPendingTasks);
         rejectedExecutionHandler = ObjectUtil.checkNotNull(rejectedHandler, "rejectedHandler");
     }
 
+    protected SingleThreadEventExecutor(EventExecutorGroup parent, Executor executor,
+                                        boolean addTaskWakesUp, Queue<Runnable> taskQueue,
+                                        RejectedExecutionHandler rejectedHandler) {
+        super(parent);
+        this.addTaskWakesUp = addTaskWakesUp;
+        this.maxPendingTasks = DEFAULT_MAX_PENDING_EXECUTOR_TASKS;
+        this.executor = ThreadExecutorMap.apply(executor, this);
+        this.taskQueue = ObjectUtil.checkNotNull(taskQueue, "taskQueue");
+        this.rejectedExecutionHandler = ObjectUtil.checkNotNull(rejectedHandler, "rejectedHandler");
+    }
+
     /**
      * @deprecated Please use and override {@link #newTaskQueue(int)}.
      */
@@ -207,10 +213,9 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx
     protected static Runnable pollTaskFrom(Queue<Runnable> taskQueue) {
         for (;;) {
             Runnable task = taskQueue.poll();
-            if (task == WAKEUP_TASK) {
-                continue;
+            if (task != WAKEUP_TASK) {
+                return task;
             }
-            return task;
         }
     }
 
@@ -271,16 +276,38 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx
     }
 
     private boolean fetchFromScheduledTaskQueue() {
+        if (scheduledTaskQueue == null || scheduledTaskQueue.isEmpty()) {
+            return true;
+        }
         long nanoTime = AbstractScheduledEventExecutor.nanoTime();
-        Runnable scheduledTask  = pollScheduledTask(nanoTime);
-        while (scheduledTask != null) {
+        for (;;) {
+            Runnable scheduledTask = pollScheduledTask(nanoTime);
+            if (scheduledTask == null) {
+                return true;
+            }
             if (!taskQueue.offer(scheduledTask)) {
                 // No space left in the task queue add it back to the scheduledTaskQueue so we pick it up again.
-                scheduledTaskQueue().add((ScheduledFutureTask<?>) scheduledTask);
+                scheduledTaskQueue.add((ScheduledFutureTask<?>) scheduledTask);
                 return false;
             }
-            scheduledTask  = pollScheduledTask(nanoTime);
         }
+    }
+
+    /**
+     * @return {@code true} if at least one scheduled task was executed.
+     */
+    private boolean executeExpiredScheduledTasks() {
+        if (scheduledTaskQueue == null || scheduledTaskQueue.isEmpty()) {
+            return false;
+        }
+        long nanoTime = AbstractScheduledEventExecutor.nanoTime();
+        Runnable scheduledTask = pollScheduledTask(nanoTime);
+        if (scheduledTask == null) {
+            return false;
+        }
+        do {
+            safeExecute(scheduledTask);
+        } while ((scheduledTask = pollScheduledTask(nanoTime)) != null);
         return true;
     }
 
@@ -315,9 +342,7 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx
      * before.
      */
     protected void addTask(Runnable task) {
-        if (task == null) {
-            throw new NullPointerException("task");
-        }
+        ObjectUtil.checkNotNull(task, "task");
         if (!offerTask(task)) {
             reject(task);
         }
@@ -334,10 +359,7 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx
      * @see Queue#remove(Object)
      */
     protected boolean removeTask(Runnable task) {
-        if (task == null) {
-            throw new NullPointerException("task");
-        }
-        return taskQueue.remove(task);
+        return taskQueue.remove(ObjectUtil.checkNotNull(task, "task"));
     }
 
     /**
@@ -364,6 +386,32 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx
         return ranAtLeastOne;
     }
 
+    /**
+     * Execute all expired scheduled tasks and all current tasks in the executor queue until both queues are empty,
+     * or {@code maxDrainAttempts} has been exceeded.
+     * @param maxDrainAttempts The maximum amount of times this method attempts to drain from queues. This is to prevent
+     *                         continuous task execution and scheduling from preventing the EventExecutor thread to
+     *                         make progress and return to the selector mechanism to process inbound I/O events.
+     * @return {@code true} if at least one task was run.
+     */
+    protected final boolean runScheduledAndExecutorTasks(final int maxDrainAttempts) {
+        assert inEventLoop();
+        boolean ranAtLeastOneTask;
+        int drainAttempt = 0;
+        do {
+            // We must run the taskQueue tasks first, because the scheduled tasks from outside the EventLoop are queued
+            // here because the taskQueue is thread safe and the scheduledTaskQueue is not thread safe.
+            ranAtLeastOneTask = runExistingTasksFrom(taskQueue) | executeExpiredScheduledTasks();
+        } while (ranAtLeastOneTask && ++drainAttempt < maxDrainAttempts);
+
+        if (drainAttempt > 0) {
+            lastExecutionTime = ScheduledFutureTask.nanoTime();
+        }
+        afterRunningAllTasks();
+
+        return drainAttempt > 0;
+    }
+
     /**
      * Runs all tasks from the passed {@code taskQueue}.
      *
@@ -385,6 +433,26 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx
         }
     }
 
+    /**
+     * What ever tasks are present in {@code taskQueue} when this method is invoked will be {@link Runnable#run()}.
+     * @param taskQueue the task queue to drain.
+     * @return {@code true} if at least {@link Runnable#run()} was called.
+     */
+    private boolean runExistingTasksFrom(Queue<Runnable> taskQueue) {
+        Runnable task = pollTaskFrom(taskQueue);
+        if (task == null) {
+            return false;
+        }
+        int remaining = Math.min(maxPendingTasks, taskQueue.size());
+        safeExecute(task);
+        // Use taskQueue.poll() directly rather than pollTaskFrom() since the latter may
+        // silently consume more than one item from the queue (skips over WAKEUP_TASK instances)
+        while (remaining-- > 0 && (task = taskQueue.poll()) != null) {
+            safeExecute(task);
+        }
+        return true;
+    }
+
     /**
      * Poll all tasks from the task queue and run them via {@link Runnable#run()} method.  This method stops running
      * the tasks in the task queue and returns if it ran longer than {@code timeoutNanos}.
@@ -397,7 +465,7 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx
             return false;
         }
 
-        final long deadline = ScheduledFutureTask.nanoTime() + timeoutNanos;
+        final long deadline = timeoutNanos > 0 ? ScheduledFutureTask.nanoTime() + timeoutNanos : 0;
         long runTasks = 0;
         long lastExecutionTime;
         for (;;) {
@@ -431,6 +499,7 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx
      */
     @UnstableApi
     protected void afterRunningAllTasks() { }
+
     /**
      * Returns the amount of time left until the scheduled task with the closest dead line is executed.
      */
@@ -480,7 +549,7 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx
     }
 
     protected void wakeup(boolean inEventLoop) {
-        if (!inEventLoop || state == ST_SHUTTING_DOWN) {
+        if (!inEventLoop) {
             // Use offer as we actually only need this to unblock the thread and if offer fails we do not care as there
             // is already something in the queue.
             taskQueue.offer(WAKEUP_TASK);
@@ -550,16 +619,12 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx
 
     @Override
     public Future<?> shutdownGracefully(long quietPeriod, long timeout, TimeUnit unit) {
-        if (quietPeriod < 0) {
-            throw new IllegalArgumentException("quietPeriod: " + quietPeriod + " (expected >= 0)");
-        }
+        ObjectUtil.checkPositiveOrZero(quietPeriod, "quietPeriod");
         if (timeout < quietPeriod) {
             throw new IllegalArgumentException(
                     "timeout: " + timeout + " (expected >= quietPeriod (" + quietPeriod + "))");
         }
-        if (unit == null) {
-            throw new NullPointerException("unit");
-        }
+        ObjectUtil.checkNotNull(unit, "unit");
 
         if (isShuttingDown()) {
             return terminationFuture();
@@ -600,7 +665,10 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx
         }
 
         if (wakeup) {
-            wakeup(inEventLoop);
+            taskQueue.offer(WAKEUP_TASK);
+            if (!addTaskWakesUp) {
+                wakeup(inEventLoop);
+            }
         }
 
         return terminationFuture();
@@ -652,7 +720,10 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx
         }
 
         if (wakeup) {
-            wakeup(inEventLoop);
+            taskQueue.offer(WAKEUP_TASK);
+            if (!addTaskWakesUp) {
+                wakeup(inEventLoop);
+            }
         }
     }
 
@@ -701,7 +772,7 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx
             if (gracefulShutdownQuietPeriod == 0) {
                 return true;
             }
-            wakeup(true);
+            taskQueue.offer(WAKEUP_TASK);
             return false;
         }
 
@@ -714,7 +785,7 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx
         if (nanoTime - lastExecutionTime <= gracefulShutdownQuietPeriod) {
             // Check if any tasks were added to the queue every 100ms.
             // TODO: Change the behavior of takeTask() so that it returns on timeout.
-            wakeup(true);
+            taskQueue.offer(WAKEUP_TASK);
             try {
                 Thread.sleep(100);
             } catch (InterruptedException e) {
@@ -731,27 +802,28 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx
 
     @Override
     public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
-        if (unit == null) {
-            throw new NullPointerException("unit");
-        }
-
+        ObjectUtil.checkNotNull(unit, "unit");
         if (inEventLoop()) {
             throw new IllegalStateException("cannot await termination of the current thread");
         }
 
-        if (threadLock.tryAcquire(timeout, unit)) {
-            threadLock.release();
-        }
+        threadLock.await(timeout, unit);
 
         return isTerminated();
     }
 
     @Override
     public void execute(Runnable task) {
-        if (task == null) {
-            throw new NullPointerException("task");
-        }
+        ObjectUtil.checkNotNull(task, "task");
+        execute(task, !(task instanceof LazyRunnable) && wakesUpForTask(task));
+    }
+
+    @Override
+    public void lazyExecute(Runnable task) {
+        execute(ObjectUtil.checkNotNull(task, "task"), false);
+    }
 
+    private void execute(Runnable task, boolean immediate) {
         boolean inEventLoop = inEventLoop();
         addTask(task);
         if (!inEventLoop) {
@@ -773,7 +845,7 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx
             }
         }
 
-        if (!addTaskWakesUp && wakesUpForTask(task)) {
+        if (!addTaskWakesUp && immediate) {
             wakeup(inEventLoop);
         }
     }
@@ -813,7 +885,7 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx
 
     /**
      * Returns the {@link ThreadProperties} of the {@link Thread} that powers the {@link SingleThreadEventExecutor}.
-     * If the {@link SingleThreadEventExecutor} is not started yet, this operation will start it and block until the
+     * If the {@link SingleThreadEventExecutor} is not started yet, this operation will start it and block until
      * it is fully started.
      */
     public final ThreadProperties threadProperties() {
@@ -836,7 +908,16 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx
         return threadProperties;
     }
 
-    @SuppressWarnings("unused")
+    /**
+     * @deprecated use {@link AbstractEventExecutor.LazyRunnable}
+     */
+    @Deprecated
+    protected interface NonWakeupRunnable extends LazyRunnable { }
+
+    /**
+     * Can be overridden to control which tasks require waking the {@link EventExecutor} thread
+     * if it is waiting so that they can be run immediately.
+     */
     protected boolean wakesUpForTask(Runnable task) {
         return true;
     }
@@ -861,11 +942,14 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx
     private void startThread() {
         if (state == ST_NOT_STARTED) {
             if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {
+                boolean success = false;
                 try {
                     doStartThread();
-                } catch (Throwable cause) {
-                    STATE_UPDATER.set(this, ST_NOT_STARTED);
-                    PlatformDependent.throwException(cause);
+                    success = true;
+                } finally {
+                    if (!success) {
+                        STATE_UPDATER.compareAndSet(this, ST_STARTED, ST_NOT_STARTED);
+                    }
                 }
             }
         }
@@ -925,12 +1009,28 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx
                     }
 
                     try {
-                        // Run all remaining tasks and shutdown hooks.
+                        // Run all remaining tasks and shutdown hooks. At this point the event loop
+                        // is in ST_SHUTTING_DOWN state still accepting tasks which is needed for
+                        // graceful shutdown with quietPeriod.
                         for (;;) {
                             if (confirmShutdown()) {
                                 break;
                             }
                         }
+
+                        // Now we want to make sure no more tasks can be added from this point. This is
+                        // achieved by switching the state. Any new tasks beyond this point will be rejected.
+                        for (;;) {
+                            int oldState = state;
+                            if (oldState >= ST_SHUTDOWN || STATE_UPDATER.compareAndSet(
+                                    SingleThreadEventExecutor.this, oldState, ST_SHUTDOWN)) {
+                                break;
+                            }
+                        }
+
+                        // We have the final set of tasks in the queue now, no more can be added, run all remaining.
+                        // No need to loop here, this is the final pass.
+                        confirmShutdown();
                     } finally {
                         try {
                             cleanup();
@@ -942,12 +1042,11 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx
                             FastThreadLocal.removeAll();
 
                             STATE_UPDATER.set(SingleThreadEventExecutor.this, ST_TERMINATED);
-                            threadLock.release();
-                            if (!taskQueue.isEmpty()) {
-                                if (logger.isWarnEnabled()) {
-                                    logger.warn("An event executor terminated with " +
-                                            "non-empty task queue (" + taskQueue.size() + ')');
-                                }
+                            threadLock.countDown();
+                            int numUserTasks = drainTasks();
+                            if (numUserTasks > 0 && logger.isWarnEnabled()) {
+                                logger.warn("An event executor terminated with " +
+                                        "non-empty task queue (" + numUserTasks + ')');
                             }
                             terminationFuture.setSuccess(null);
                         }
@@ -957,6 +1056,22 @@ public abstract class SingleThreadEventExecutor extends AbstractScheduledEventEx
         });
     }
 
+    final int drainTasks() {
+        int numTasks = 0;
+        for (;;) {
+            Runnable runnable = taskQueue.poll();
+            if (runnable == null) {
+                break;
+            }
+            // WAKEUP_TASK should be just discarded as these are added internally.
+            // The important bit is that we not have any user tasks left.
+            if (WAKEUP_TASK != runnable) {
+                numTasks++;
+            }
+        }
+        return numTasks;
+    }
+
     private static final class DefaultThreadProperties implements ThreadProperties {
         private final Thread t;
 
diff --git a/common/src/main/java/io/netty/util/concurrent/ThreadPerTaskExecutor.java b/common/src/main/java/io/netty/util/concurrent/ThreadPerTaskExecutor.java
index 21210ae..f1a2b19 100644
--- a/common/src/main/java/io/netty/util/concurrent/ThreadPerTaskExecutor.java
+++ b/common/src/main/java/io/netty/util/concurrent/ThreadPerTaskExecutor.java
@@ -15,6 +15,8 @@
  */
 package io.netty.util.concurrent;
 
+import io.netty.util.internal.ObjectUtil;
+
 import java.util.concurrent.Executor;
 import java.util.concurrent.ThreadFactory;
 
@@ -22,10 +24,7 @@ public final class ThreadPerTaskExecutor implements Executor {
     private final ThreadFactory threadFactory;
 
     public ThreadPerTaskExecutor(ThreadFactory threadFactory) {
-        if (threadFactory == null) {
-            throw new NullPointerException("threadFactory");
-        }
-        this.threadFactory = threadFactory;
+        this.threadFactory = ObjectUtil.checkNotNull(threadFactory, "threadFactory");
     }
 
     @Override
diff --git a/common/src/main/java/io/netty/util/concurrent/UnorderedThreadPoolEventExecutor.java b/common/src/main/java/io/netty/util/concurrent/UnorderedThreadPoolEventExecutor.java
index 4ed94da..277c903 100644
--- a/common/src/main/java/io/netty/util/concurrent/UnorderedThreadPoolEventExecutor.java
+++ b/common/src/main/java/io/netty/util/concurrent/UnorderedThreadPoolEventExecutor.java
@@ -215,7 +215,7 @@ public final class UnorderedThreadPoolEventExecutor extends ScheduledThreadPoolE
 
         RunnableScheduledFutureTask(EventExecutor executor, Runnable runnable,
                                            RunnableScheduledFuture<V> future) {
-            super(executor, runnable, null);
+            super(executor, runnable);
             this.future = future;
         }
 
@@ -232,7 +232,7 @@ public final class UnorderedThreadPoolEventExecutor extends ScheduledThreadPoolE
             } else if (!isDone()) {
                 try {
                     // Its a periodic task so we need to ignore the return value
-                    task.call();
+                    runTask();
                 } catch (Throwable cause) {
                     if (!tryFailureInternal(cause)) {
                         logger.warn("Failure during execution of task", cause);
diff --git a/common/src/main/java/io/netty/util/internal/AppendableCharSequence.java b/common/src/main/java/io/netty/util/internal/AppendableCharSequence.java
index 408c32f..2e44b33 100644
--- a/common/src/main/java/io/netty/util/internal/AppendableCharSequence.java
+++ b/common/src/main/java/io/netty/util/internal/AppendableCharSequence.java
@@ -37,6 +37,13 @@ public final class AppendableCharSequence implements CharSequence, Appendable {
         pos = chars.length;
     }
 
+    public void setLength(int length) {
+        if (length < 0 || length > pos) {
+            throw new IllegalArgumentException("length: " + length + " (length: >= 0, <= " + pos + ')');
+        }
+        this.pos = length;
+    }
+
     @Override
     public int length() {
         return pos;
@@ -63,6 +70,12 @@ public final class AppendableCharSequence implements CharSequence, Appendable {
 
     @Override
     public AppendableCharSequence subSequence(int start, int end) {
+        if (start == end) {
+            // If start and end index is the same we need to return an empty sequence to conform to the interface.
+            // As our expanding logic depends on the fact that we have a char[] with length > 0 we need to construct
+            // an instance for which this is true.
+            return new AppendableCharSequence(Math.min(16, chars.length));
+        }
         return new AppendableCharSequence(Arrays.copyOfRange(chars, start, end));
     }
 
diff --git a/common/src/main/java/io/netty/util/internal/Hidden.java b/common/src/main/java/io/netty/util/internal/Hidden.java
new file mode 100644
index 0000000..65887d3
--- /dev/null
+++ b/common/src/main/java/io/netty/util/internal/Hidden.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2019 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.util.internal;
+
+import io.netty.util.concurrent.FastThreadLocalThread;
+import reactor.blockhound.BlockHound;
+import reactor.blockhound.integration.BlockHoundIntegration;
+
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+/**
+ * Contains classes that must be have public visibility but are not public API.
+ */
+class Hidden {
+
+    /**
+     * This class integrates Netty with BlockHound.
+     * <p>
+     * It is public but only because of the ServiceLoader's limitations
+     * and SHOULD NOT be considered a public API.
+     */
+    @UnstableApi
+    @SuppressJava6Requirement(reason = "BlockHound is Java 8+, but this class is only loaded by it's SPI")
+    public static final class NettyBlockHoundIntegration implements BlockHoundIntegration {
+
+        @Override
+        public void applyTo(BlockHound.Builder builder) {
+            builder.allowBlockingCallsInside(
+                    "io.netty.channel.nio.NioEventLoop",
+                    "handleLoopException"
+            );
+
+            builder.allowBlockingCallsInside(
+                    "io.netty.channel.kqueue.KQueueEventLoop",
+                    "handleLoopException"
+            );
+
+            builder.allowBlockingCallsInside(
+                    "io.netty.channel.epoll.EpollEventLoop",
+                    "handleLoopException"
+            );
+
+            builder.allowBlockingCallsInside(
+                    "io.netty.util.HashedWheelTimer$Worker",
+                    "waitForNextTick"
+            );
+
+            builder.allowBlockingCallsInside(
+                    "io.netty.util.concurrent.SingleThreadEventExecutor",
+                    "confirmShutdown"
+            );
+
+            builder.allowBlockingCallsInside(
+                    "io.netty.handler.ssl.SslHandler",
+                    "handshake"
+            );
+
+            builder.allowBlockingCallsInside(
+                    "io.netty.handler.ssl.SslHandler",
+                    "runAllDelegatedTasks"
+            );
+
+            builder.allowBlockingCallsInside(
+                    "io.netty.util.concurrent.GlobalEventExecutor",
+                    "takeTask");
+
+            builder.allowBlockingCallsInside(
+                    "io.netty.util.concurrent.SingleThreadEventExecutor",
+                    "takeTask");
+
+            builder.nonBlockingThreadPredicate(new Function<Predicate<Thread>, Predicate<Thread>>() {
+                @Override
+                public Predicate<Thread> apply(final Predicate<Thread> p) {
+                    return new Predicate<Thread>() {
+                        @Override
+                        @SuppressJava6Requirement(reason = "Predicate#test")
+                        public boolean test(Thread thread) {
+                            return p.test(thread) || thread instanceof FastThreadLocalThread;
+                        }
+                    };
+                }
+            });
+        }
+
+        @Override
+        public int compareTo(BlockHoundIntegration o) {
+            return 0;
+        }
+    }
+}
diff --git a/common/src/main/java/io/netty/util/internal/InternalThreadLocalMap.java b/common/src/main/java/io/netty/util/internal/InternalThreadLocalMap.java
index 0a6a6c5..c844b87 100644
--- a/common/src/main/java/io/netty/util/internal/InternalThreadLocalMap.java
+++ b/common/src/main/java/io/netty/util/internal/InternalThreadLocalMap.java
@@ -43,6 +43,8 @@ public final class InternalThreadLocalMap extends UnpaddedInternalThreadLocalMap
     private static final int DEFAULT_ARRAY_LIST_INITIAL_CAPACITY = 8;
     private static final int STRING_BUILDER_INITIAL_SIZE;
     private static final int STRING_BUILDER_MAX_SIZE;
+    private static final int HANDLER_SHARABLE_CACHE_INITIAL_CAPACITY = 4;
+    private static final int INDEXED_VARIABLE_TABLE_INITIAL_SIZE = 32;
 
     public static final Object UNSET = new Object();
 
@@ -127,7 +129,7 @@ public final class InternalThreadLocalMap extends UnpaddedInternalThreadLocalMap
     }
 
     private static Object[] newIndexedVariableTable() {
-        Object[] array = new Object[32];
+        Object[] array = new Object[INDEXED_VARIABLE_TABLE_INITIAL_SIZE];
         Arrays.fill(array, UNSET);
         return array;
     }
@@ -271,7 +273,7 @@ public final class InternalThreadLocalMap extends UnpaddedInternalThreadLocalMap
         Map<Class<?>, Boolean> cache = handlerSharableCache;
         if (cache == null) {
             // Start with small capacity to keep memory overhead as low as possible.
-            handlerSharableCache = cache = new WeakHashMap<Class<?>, Boolean>(4);
+            handlerSharableCache = cache = new WeakHashMap<Class<?>, Boolean>(HANDLER_SHARABLE_CACHE_INITIAL_CAPACITY);
         }
         return cache;
     }
diff --git a/common/src/main/java/io/netty/util/internal/LongAdderCounter.java b/common/src/main/java/io/netty/util/internal/LongAdderCounter.java
index f7eeb81..b5dbdba 100644
--- a/common/src/main/java/io/netty/util/internal/LongAdderCounter.java
+++ b/common/src/main/java/io/netty/util/internal/LongAdderCounter.java
@@ -17,6 +17,7 @@ package io.netty.util.internal;
 
 import java.util.concurrent.atomic.LongAdder;
 
+@SuppressJava6Requirement(reason = "Usage guarded by java version check")
 final class LongAdderCounter extends LongAdder implements LongCounter {
 
     @Override
diff --git a/common/src/main/java/io/netty/util/internal/NativeLibraryLoader.java b/common/src/main/java/io/netty/util/internal/NativeLibraryLoader.java
index 31b4a46..b059446 100644
--- a/common/src/main/java/io/netty/util/internal/NativeLibraryLoader.java
+++ b/common/src/main/java/io/netty/util/internal/NativeLibraryLoader.java
@@ -137,9 +137,11 @@ public final class NativeLibraryLoader {
             return;
         } catch (Throwable ex) {
             suppressed.add(ex);
-            logger.debug(
-                    "{} cannot be loaded from java.libary.path, "
-                    + "now trying export to -Dio.netty.native.workdir: {}", name, WORKDIR, ex);
+            if (logger.isDebugEnabled()) {
+                logger.debug(
+                        "{} cannot be loaded from java.library.path, "
+                                + "now trying export to -Dio.netty.native.workdir: {}", name, WORKDIR, ex);
+            }
         }
 
         String libname = System.mapLibraryName(name);
@@ -178,34 +180,22 @@ public final class NativeLibraryLoader {
 
             int index = libname.lastIndexOf('.');
             String prefix = libname.substring(0, index);
-            String suffix = libname.substring(index, libname.length());
+            String suffix = libname.substring(index);
 
             tmpFile = File.createTempFile(prefix, suffix, WORKDIR);
             in = url.openStream();
             out = new FileOutputStream(tmpFile);
 
-            byte[] buffer = new byte[8192];
-            int length;
-            if (TRY_TO_PATCH_SHADED_ID && PlatformDependent.isOsx() && !packagePrefix.isEmpty()) {
-                // We read the whole native lib into memory to make it easier to monkey-patch the id.
-                ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(in.available());
-
-                while ((length = in.read(buffer)) > 0) {
-                    byteArrayOutputStream.write(buffer, 0, length);
-                }
-                byteArrayOutputStream.flush();
-                byte[] bytes = byteArrayOutputStream.toByteArray();
-                byteArrayOutputStream.close();
-
-                // Try to patch the library id.
-                patchShadedLibraryId(bytes, originalName, name);
-
-                out.write(bytes);
+            if (shouldShadedLibraryIdBePatched(packagePrefix)) {
+                patchShadedLibraryId(in, out, originalName, name);
             } else {
+                byte[] buffer = new byte[8192];
+                int length;
                 while ((length = in.read(buffer)) > 0) {
                     out.write(buffer, 0, length);
                 }
             }
+
             out.flush();
 
             // Close the output stream before loading the unpacked library,
@@ -217,10 +207,13 @@ public final class NativeLibraryLoader {
             try {
                 if (tmpFile != null && tmpFile.isFile() && tmpFile.canRead() &&
                     !NoexecVolumeDetector.canExecuteExecutable(tmpFile)) {
+                    // Pass "io.netty.native.workdir" as an argument to allow shading tools to see
+                    // the string. Since this is printed out to users to tell them what to do next,
+                    // we want the value to be correct even when shading.
                     logger.info("{} exists but cannot be executed even when execute permissions set; " +
-                                "check volume for \"noexec\" flag; use -Dio.netty.native.workdir=[path] " +
+                                "check volume for \"noexec\" flag; use -D{}=[path] " +
                                 "to set native working directory separately.",
-                                tmpFile.getPath());
+                                tmpFile.getPath(), "io.netty.native.workdir");
                 }
             } catch (Throwable t) {
                 suppressed.add(t);
@@ -246,10 +239,50 @@ public final class NativeLibraryLoader {
         }
     }
 
+    // Package-private for testing.
+    static boolean patchShadedLibraryId(InputStream in, OutputStream out, String originalName, String name)
+            throws IOException {
+        byte[] buffer = new byte[8192];
+        int length;
+        // We read the whole native lib into memory to make it easier to monkey-patch the id.
+        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(in.available());
+
+        while ((length = in.read(buffer)) > 0) {
+            byteArrayOutputStream.write(buffer, 0, length);
+        }
+        byteArrayOutputStream.flush();
+        byte[] bytes = byteArrayOutputStream.toByteArray();
+        byteArrayOutputStream.close();
+
+        final boolean patched;
+        // Try to patch the library id.
+        if (!patchShadedLibraryId(bytes, originalName, name)) {
+            // We did not find the Id, check if we used a originalName that has the os and arch as suffix.
+            // If this is the case we should also try to patch with the os and arch suffix removed.
+            String os = PlatformDependent.normalizedOs();
+            String arch = PlatformDependent.normalizedArch();
+            String osArch = "_" + os + "_" + arch;
+            if (originalName.endsWith(osArch)) {
+                patched = patchShadedLibraryId(bytes,
+                        originalName.substring(0, originalName.length() - osArch.length()), name);
+            } else {
+                patched = false;
+            }
+        } else {
+            patched = true;
+        }
+        out.write(bytes, 0, bytes.length);
+        return patched;
+    }
+
+    private static boolean shouldShadedLibraryIdBePatched(String packagePrefix) {
+        return TRY_TO_PATCH_SHADED_ID && PlatformDependent.isOsx() && !packagePrefix.isEmpty();
+    }
+
     /**
      * Try to patch shaded library to ensure it uses a unique ID.
      */
-    private static void patchShadedLibraryId(byte[] bytes, String originalName, String name) {
+    private static boolean patchShadedLibraryId(byte[] bytes, String originalName, String name) {
         // Our native libs always have the name as part of their id so we can search for it and replace it
         // to make the ID unique if shading is used.
         byte[] nameBytes = originalName.getBytes(CharsetUtil.UTF_8);
@@ -275,6 +308,7 @@ public final class NativeLibraryLoader {
 
         if (idIdx == -1) {
             logger.debug("Was not able to find the ID of the shaded native library {}, can't adjust it.", name);
+            return false;
         } else {
             // We found our ID... now monkey-patch it!
             for (int i = 0; i < nameBytes.length; i++) {
@@ -288,6 +322,7 @@ public final class NativeLibraryLoader {
                         "Found the ID of the shaded native library {}. Replacing ID part {} with {}",
                         name, originalName, new String(bytes, idIdx, nameBytes.length, CharsetUtil.UTF_8));
             }
+            return true;
         }
     }
 
@@ -449,6 +484,7 @@ public final class NativeLibraryLoader {
 
     private static final class NoexecVolumeDetector {
 
+        @SuppressJava6Requirement(reason = "Usage guarded by java version check")
         private static boolean canExecuteExecutable(File file) throws IOException {
             if (PlatformDependent.javaVersion() < 7) {
                 // Pre-JDK7, the Java API did not directly support POSIX permissions; instead of implementing a custom
diff --git a/common/src/main/java/io/netty/util/internal/ObjectPool.java b/common/src/main/java/io/netty/util/internal/ObjectPool.java
new file mode 100644
index 0000000..d0f0e03
--- /dev/null
+++ b/common/src/main/java/io/netty/util/internal/ObjectPool.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2019 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.util.internal;
+
+import io.netty.util.Recycler;
+
+/**
+ * Light-weight object pool.
+ *
+ * @param <T> the type of the pooled object
+ */
+public abstract class ObjectPool<T> {
+
+    ObjectPool() { }
+
+    /**
+     * Get a {@link Object} from the {@link ObjectPool}. The returned {@link Object} may be created via
+     * {@link ObjectCreator#newObject(Handle)} if no pooled {@link Object} is ready to be reused.
+     */
+    public abstract T get();
+
+    /**
+     * Handle for an pooled {@link Object} that will be used to notify the {@link ObjectPool} once it can
+     * reuse the pooled {@link Object} again.
+     * @param <T>
+     */
+    public interface Handle<T> {
+        /**
+         * Recycle the {@link Object} if possible and so make it ready to be reused.
+         */
+        void recycle(T self);
+    }
+
+    /**
+     * Creates a new Object which references the given {@link Handle} and calls {@link Handle#recycle(Object)} once
+     * it can be re-used.
+     *
+     * @param <T> the type of the pooled object
+     */
+    public interface ObjectCreator<T> {
+
+        /**
+         * Creates an returns a new {@link Object} that can be used and later recycled via
+         * {@link Handle#recycle(Object)}.
+         */
+        T newObject(Handle<T> handle);
+    }
+
+    /**
+     * Creates a new {@link ObjectPool} which will use the given {@link ObjectCreator} to create the {@link Object}
+     * that should be pooled.
+     */
+    public static <T> ObjectPool<T> newPool(final ObjectCreator<T> creator) {
+        return new RecyclerObjectPool<T>(ObjectUtil.checkNotNull(creator, "creator"));
+    }
+
+    private static final class RecyclerObjectPool<T> extends ObjectPool<T> {
+        private final Recycler<T> recycler;
+
+        RecyclerObjectPool(final ObjectCreator<T> creator) {
+             recycler = new Recycler<T>() {
+                @Override
+                protected T newObject(Handle<T> handle) {
+                    return creator.newObject(handle);
+                }
+            };
+        }
+
+        @Override
+        public T get() {
+            return recycler.get();
+        }
+    }
+}
diff --git a/common/src/main/java/io/netty/util/internal/PendingWrite.java b/common/src/main/java/io/netty/util/internal/PendingWrite.java
index 6f27cf5..f84826c 100644
--- a/common/src/main/java/io/netty/util/internal/PendingWrite.java
+++ b/common/src/main/java/io/netty/util/internal/PendingWrite.java
@@ -15,20 +15,21 @@
  */
 package io.netty.util.internal;
 
-import io.netty.util.Recycler;
 import io.netty.util.ReferenceCountUtil;
 import io.netty.util.concurrent.Promise;
+import io.netty.util.internal.ObjectPool.Handle;
+import io.netty.util.internal.ObjectPool.ObjectCreator;
 
 /**
  * Some pending write which should be picked up later.
  */
 public final class PendingWrite {
-    private static final Recycler<PendingWrite> RECYCLER = new Recycler<PendingWrite>() {
+    private static final ObjectPool<PendingWrite> RECYCLER = ObjectPool.newPool(new ObjectCreator<PendingWrite>() {
         @Override
-        protected PendingWrite newObject(Handle<PendingWrite> handle) {
+        public PendingWrite newObject(Handle<PendingWrite> handle) {
             return new PendingWrite(handle);
         }
-    };
+    });
 
     /**
      * Create a new empty {@link RecyclableArrayList} instance
@@ -40,11 +41,11 @@ public final class PendingWrite {
         return pending;
     }
 
-    private final Recycler.Handle<PendingWrite> handle;
+    private final Handle<PendingWrite> handle;
     private Object msg;
     private Promise<Void> promise;
 
-    private PendingWrite(Recycler.Handle<PendingWrite> handle) {
+    private PendingWrite(Handle<PendingWrite> handle) {
         this.handle = handle;
     }
 
diff --git a/common/src/main/java/io/netty/util/internal/PlatformDependent.java b/common/src/main/java/io/netty/util/internal/PlatformDependent.java
index 1baeecb..80808ed 100644
--- a/common/src/main/java/io/netty/util/internal/PlatformDependent.java
+++ b/common/src/main/java/io/netty/util/internal/PlatformDependent.java
@@ -15,6 +15,7 @@
  */
 package io.netty.util.internal;
 
+import io.netty.util.CharsetUtil;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 import org.jctools.queues.MpscArrayQueue;
@@ -28,19 +29,28 @@ import org.jctools.queues.atomic.SpscLinkedAtomicQueue;
 import org.jctools.util.Pow2;
 import org.jctools.util.UnsafeAccess;
 
+import java.io.BufferedReader;
 import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
 import java.lang.reflect.Field;
 import java.lang.reflect.Method;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.security.AccessController;
 import java.security.PrivilegedAction;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.Deque;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Queue;
 import java.util.Random;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentLinkedDeque;
 import java.util.concurrent.ConcurrentMap;
@@ -74,6 +84,8 @@ public final class PlatformDependent {
 
     private static final boolean IS_WINDOWS = isWindows0();
     private static final boolean IS_OSX = isOsx0();
+    private static final boolean IS_J9_JVM = isJ9Jvm0();
+    private static final boolean IS_IVKVM_DOT_NET = isIkvmDotNet0();
 
     private static final boolean MAYBE_SUPER_USER;
 
@@ -95,6 +107,10 @@ public final class PlatformDependent {
     private static final String NORMALIZED_ARCH = normalizeArch(SystemPropertyUtil.get("os.arch", ""));
     private static final String NORMALIZED_OS = normalizeOs(SystemPropertyUtil.get("os.name", ""));
 
+    // keep in sync with maven's pom.xml via os.detection.classifierWithLikes!
+    private static final String[] ALLOWED_LINUX_OS_CLASSIFIERS = {"fedora", "suse", "arch"};
+    private static final Set<String> LINUX_OS_CLASSIFIERS;
+
     private static final int ADDRESS_SIZE = addressSize0();
     private static final boolean USE_DIRECT_BUFFER_NO_CLEANER;
     private static final AtomicLong DIRECT_MEMORY_COUNTER;
@@ -102,7 +118,10 @@ public final class PlatformDependent {
     private static final ThreadLocalRandomProvider RANDOM_PROVIDER;
     private static final Cleaner CLEANER;
     private static final int UNINITIALIZED_ARRAY_ALLOCATION_THRESHOLD;
-
+    // For specifications, see https://www.freedesktop.org/software/systemd/man/os-release.html
+    private static final String[] OS_RELEASE_FILES = {"/etc/os-release", "/usr/lib/os-release"};
+    private static final String LINUX_ID_PREFIX = "ID=";
+    private static final String LINUX_ID_LIKE_PREFIX = "ID_LIKE=";
     public static final boolean BIG_ENDIAN_NATIVE_ORDER = ByteOrder.nativeOrder() == ByteOrder.BIG_ENDIAN;
 
     private static final Cleaner NOOP = new Cleaner() {
@@ -116,6 +135,7 @@ public final class PlatformDependent {
         if (javaVersion() >= 7) {
             RANDOM_PROVIDER = new ThreadLocalRandomProvider() {
                 @Override
+                @SuppressJava6Requirement(reason = "Usage guarded by java version check")
                 public Random current() {
                     return java.util.concurrent.ThreadLocalRandom.current();
                 }
@@ -194,6 +214,63 @@ public final class PlatformDependent {
                     "Unless explicitly requested, heap buffer will always be preferred to avoid potential system " +
                     "instability.");
         }
+
+        final Set<String> allowedClassifiers = Collections.unmodifiableSet(
+                new HashSet<String>(Arrays.asList(ALLOWED_LINUX_OS_CLASSIFIERS)));
+        final Set<String> availableClassifiers = new LinkedHashSet<String>();
+        for (final String osReleaseFileName : OS_RELEASE_FILES) {
+            final File file = new File(osReleaseFileName);
+            boolean found = AccessController.doPrivileged(new PrivilegedAction<Boolean>() {
+                @Override
+                public Boolean run() {
+                    try {
+                        if (file.exists()) {
+                            BufferedReader reader = null;
+                            try {
+                                reader = new BufferedReader(
+                                        new InputStreamReader(
+                                                new FileInputStream(file), CharsetUtil.UTF_8));
+
+                                String line;
+                                while ((line = reader.readLine()) != null) {
+                                    if (line.startsWith(LINUX_ID_PREFIX)) {
+                                        String id = normalizeOsReleaseVariableValue(
+                                                line.substring(LINUX_ID_PREFIX.length()));
+                                        addClassifier(allowedClassifiers, availableClassifiers, id);
+                                    } else if (line.startsWith(LINUX_ID_LIKE_PREFIX)) {
+                                        line = normalizeOsReleaseVariableValue(
+                                                line.substring(LINUX_ID_LIKE_PREFIX.length()));
+                                        addClassifier(allowedClassifiers, availableClassifiers, line.split("[ ]+"));
+                                    }
+                                }
+                            } catch (SecurityException e) {
+                                logger.debug("Unable to read {}", osReleaseFileName, e);
+                            } catch (IOException e) {
+                                logger.debug("Error while reading content of {}", osReleaseFileName, e);
+                            } finally {
+                                if (reader != null) {
+                                    try {
+                                        reader.close();
+                                    } catch (IOException ignored) {
+                                        // Ignore
+                                    }
+                                }
+                            }
+                            // specification states we should only fall back if /etc/os-release does not exist
+                            return true;
+                        }
+                    } catch (SecurityException e) {
+                        logger.debug("Unable to check if {} exists", osReleaseFileName, e);
+                    }
+                    return false;
+                }
+            });
+
+            if (found) {
+                break;
+            }
+        }
+        LINUX_OS_CLASSIFIERS = Collections.unmodifiableSet(availableClassifiers);
     }
 
     public static boolean hasDirectBufferNoCleanerConstructor() {
@@ -287,6 +364,16 @@ public final class PlatformDependent {
         return DIRECT_MEMORY_LIMIT;
     }
 
+    /**
+     * Returns the current memory reserved for direct buffer allocation.
+     * This method returns -1 in case that a value is not available.
+     *
+     * @see #maxDirectMemory()
+     */
+    public static long usedDirectMemory() {
+        return DIRECT_MEMORY_COUNTER != null ? DIRECT_MEMORY_COUNTER.get() : -1;
+    }
+
     /**
      * Returns the temporary directory.
      */
@@ -753,83 +840,43 @@ public final class PlatformDependent {
      * The resulting hash code will be case insensitive.
      */
     public static int hashCodeAscii(CharSequence bytes) {
+        final int length = bytes.length();
+        final int remainingBytes = length & 7;
         int hash = HASH_CODE_ASCII_SEED;
-        final int remainingBytes = bytes.length() & 7;
         // Benchmarking shows that by just naively looping for inputs 8~31 bytes long we incur a relatively large
         // performance penalty (only achieve about 60% performance of loop which iterates over each char). So because
         // of this we take special provisions to unroll the looping for these conditions.
-        switch (bytes.length()) {
-            case 31:
-            case 30:
-            case 29:
-            case 28:
-            case 27:
-            case 26:
-            case 25:
-            case 24:
-                hash = hashCodeAsciiCompute(bytes, bytes.length() - 24,
-                        hashCodeAsciiCompute(bytes, bytes.length() - 16,
-                          hashCodeAsciiCompute(bytes, bytes.length() - 8, hash)));
-                break;
-            case 23:
-            case 22:
-            case 21:
-            case 20:
-            case 19:
-            case 18:
-            case 17:
-            case 16:
-                hash = hashCodeAsciiCompute(bytes, bytes.length() - 16,
-                         hashCodeAsciiCompute(bytes, bytes.length() - 8, hash));
-                break;
-            case 15:
-            case 14:
-            case 13:
-            case 12:
-            case 11:
-            case 10:
-            case 9:
-            case 8:
-                hash = hashCodeAsciiCompute(bytes, bytes.length() - 8, hash);
-                break;
-            case 7:
-            case 6:
-            case 5:
-            case 4:
-            case 3:
-            case 2:
-            case 1:
-            case 0:
-                break;
-            default:
-                for (int i = bytes.length() - 8; i >= remainingBytes; i -= 8) {
-                    hash = hashCodeAsciiCompute(bytes, i, hash);
+        if (length >= 32) {
+            for (int i = length - 8; i >= remainingBytes; i -= 8) {
+                hash = hashCodeAsciiCompute(bytes, i, hash);
+            }
+        } else if (length >= 8) {
+            hash = hashCodeAsciiCompute(bytes, length - 8, hash);
+            if (length >= 16) {
+                hash = hashCodeAsciiCompute(bytes, length - 16, hash);
+                if (length >= 24) {
+                    hash = hashCodeAsciiCompute(bytes, length - 24, hash);
                 }
-                break;
+            }
         }
-        switch(remainingBytes) {
-            case 7:
-                return ((hash * HASH_CODE_C1 + hashCodeAsciiSanitizeByte(bytes.charAt(0)))
-                              * HASH_CODE_C2 + hashCodeAsciiSanitizeShort(bytes, 1))
-                              * HASH_CODE_C1 + hashCodeAsciiSanitizeInt(bytes, 3);
-            case 6:
-                return (hash * HASH_CODE_C1 + hashCodeAsciiSanitizeShort(bytes, 0))
-                             * HASH_CODE_C2 + hashCodeAsciiSanitizeInt(bytes, 2);
-            case 5:
-                return (hash * HASH_CODE_C1 + hashCodeAsciiSanitizeByte(bytes.charAt(0)))
-                             * HASH_CODE_C2 + hashCodeAsciiSanitizeInt(bytes, 1);
-            case 4:
-                return hash * HASH_CODE_C1 + hashCodeAsciiSanitizeInt(bytes, 0);
-            case 3:
-                return (hash * HASH_CODE_C1 + hashCodeAsciiSanitizeByte(bytes.charAt(0)))
-                             * HASH_CODE_C2 + hashCodeAsciiSanitizeShort(bytes, 1);
-            case 2:
-                return hash * HASH_CODE_C1 + hashCodeAsciiSanitizeShort(bytes, 0);
-            case 1:
-                return hash * HASH_CODE_C1 + hashCodeAsciiSanitizeByte(bytes.charAt(0));
-            default:
-                return hash;
+        if (remainingBytes == 0) {
+            return hash;
+        }
+        int offset = 0;
+        if (remainingBytes != 2 & remainingBytes != 4 & remainingBytes != 6) { // 1, 3, 5, 7
+            hash = hash * HASH_CODE_C1 + hashCodeAsciiSanitizeByte(bytes.charAt(0));
+            offset = 1;
+        }
+        if (remainingBytes != 1 & remainingBytes != 4 & remainingBytes != 5) { // 2, 3, 6, 7
+            hash = hash * (offset == 0 ? HASH_CODE_C1 : HASH_CODE_C2)
+                    + hashCodeAsciiSanitize(hashCodeAsciiSanitizeShort(bytes, offset));
+            offset += 2;
         }
+        if (remainingBytes >= 4) { // 4, 5, 6, 7
+            return hash * ((offset == 0 | offset == 3) ? HASH_CODE_C1 : HASH_CODE_C2)
+                    + hashCodeAsciiSanitizeInt(bytes, offset);
+        }
+        return hash;
     }
 
     private static final class Mpsc {
@@ -934,6 +981,7 @@ public final class PlatformDependent {
     /**
      * Returns a new concurrent {@link Deque}.
      */
+    @SuppressJava6Requirement(reason = "Usage guarded by java version check")
     public static <C> Deque<C> newConcurrentDeque() {
         if (javaVersion() < 7) {
             return new LinkedBlockingDeque<C>();
@@ -982,6 +1030,12 @@ public final class PlatformDependent {
             logger.debug("sun.misc.Unsafe: unavailable (Android)");
             return new UnsupportedOperationException("sun.misc.Unsafe: unavailable (Android)");
         }
+
+        if (isIkvmDotNet()) {
+            logger.debug("sun.misc.Unsafe: unavailable (IKVM.NET)");
+            return new UnsupportedOperationException("sun.misc.Unsafe: unavailable (IKVM.NET)");
+        }
+
         Throwable cause = PlatformDependent0.getUnsafeUnavailabilityCause();
         if (cause != null) {
             return cause;
@@ -998,6 +1052,31 @@ public final class PlatformDependent {
         }
     }
 
+    /**
+     * Returns {@code true} if the running JVM is either <a href="https://developer.ibm.com/javasdk/">IBM J9</a> or
+     * <a href="https://www.eclipse.org/openj9/">Eclipse OpenJ9</a>, {@code false} otherwise.
+     */
+    public static boolean isJ9Jvm() {
+        return IS_J9_JVM;
+    }
+
+    private static boolean isJ9Jvm0() {
+        String vmName = SystemPropertyUtil.get("java.vm.name", "").toLowerCase();
+        return vmName.startsWith("ibm j9") || vmName.startsWith("eclipse openj9");
+    }
+
+    /**
+     * Returns {@code true} if the running JVM is <a href="https://www.ikvm.net">IKVM.NET</a>, {@code false} otherwise.
+     */
+    public static boolean isIkvmDotNet() {
+        return IS_IVKVM_DOT_NET;
+    }
+
+    private static boolean isIkvmDotNet0() {
+        String vmName = SystemPropertyUtil.get("java.vm.name", "").toUpperCase(Locale.US);
+        return vmName.equals("IKVM.NET");
+    }
+
     private static long maxDirectMemory0() {
         long maxDirectMemory = 0;
 
@@ -1185,8 +1264,8 @@ public final class PlatformDependent {
 
         // Last resort: guess from VM name and then fall back to most common 64-bit mode.
         String vm = SystemPropertyUtil.get("java.vm.name", "").toLowerCase(Locale.US);
-        Pattern BIT_PATTERN = Pattern.compile("([1-9][0-9]+)-?bit");
-        Matcher m = BIT_PATTERN.matcher(vm);
+        Pattern bitPattern = Pattern.compile("([1-9][0-9]+)-?bit");
+        Matcher m = bitPattern.matcher(vm);
         if (m.find()) {
             return Integer.parseInt(m.group(1));
         } else {
@@ -1271,6 +1350,30 @@ public final class PlatformDependent {
         return NORMALIZED_OS;
     }
 
+    public static Set<String> normalizedLinuxClassifiers() {
+        return LINUX_OS_CLASSIFIERS;
+    }
+
+    /**
+     * Adds only those classifier strings to <tt>dest</tt> which are present in <tt>allowed</tt>.
+     *
+     * @param allowed          allowed classifiers
+     * @param dest             destination set
+     * @param maybeClassifiers potential classifiers to add
+     */
+    private static void addClassifier(Set<String> allowed, Set<String> dest, String... maybeClassifiers) {
+        for (String id : maybeClassifiers) {
+            if (allowed.contains(id)) {
+                dest.add(id);
+            }
+        }
+    }
+
+    private static String normalizeOsReleaseVariableValue(String value) {
+        // Variable assignment values may be enclosed in double or single quotes.
+        return value.trim().replaceAll("[\"']", "");
+    }
+
     private static String normalize(String value) {
         return value.toLowerCase(Locale.US).replaceAll("[^a-z0-9]+", "");
     }
diff --git a/common/src/main/java/io/netty/util/internal/PlatformDependent0.java b/common/src/main/java/io/netty/util/internal/PlatformDependent0.java
index df45d16..e6c8948 100644
--- a/common/src/main/java/io/netty/util/internal/PlatformDependent0.java
+++ b/common/src/main/java/io/netty/util/internal/PlatformDependent0.java
@@ -33,6 +33,7 @@ import static io.netty.util.internal.ObjectUtil.checkNotNull;
 /**
  * The {@link PlatformDependent} operations which requires access to {@code sun.misc.*}.
  */
+@SuppressJava6Requirement(reason = "Unsafe access is guarded")
 final class PlatformDependent0 {
 
     private static final InternalLogger logger = InternalLoggerFactory.getInstance(PlatformDependent0.class);
@@ -622,72 +623,57 @@ final class PlatformDependent0 {
     }
 
     static boolean equals(byte[] bytes1, int startPos1, byte[] bytes2, int startPos2, int length) {
-        if (length <= 0) {
-            return true;
-        }
-        final long baseOffset1 = BYTE_ARRAY_BASE_OFFSET + startPos1;
-        final long baseOffset2 = BYTE_ARRAY_BASE_OFFSET + startPos2;
         int remainingBytes = length & 7;
-        final long end = baseOffset1 + remainingBytes;
-        for (long i = baseOffset1 - 8 + length, j = baseOffset2 - 8 + length; i >= end; i -= 8, j -= 8) {
-            if (UNSAFE.getLong(bytes1, i) != UNSAFE.getLong(bytes2, j)) {
-                return false;
+        final long baseOffset1 = BYTE_ARRAY_BASE_OFFSET + startPos1;
+        final long diff = startPos2 - startPos1;
+        if (length >= 8) {
+            final long end = baseOffset1 + remainingBytes;
+            for (long i = baseOffset1 - 8 + length; i >= end; i -= 8) {
+                if (UNSAFE.getLong(bytes1, i) != UNSAFE.getLong(bytes2, i + diff)) {
+                    return false;
+                }
             }
         }
-
         if (remainingBytes >= 4) {
             remainingBytes -= 4;
-            if (UNSAFE.getInt(bytes1, baseOffset1 + remainingBytes) !=
-                UNSAFE.getInt(bytes2, baseOffset2 + remainingBytes)) {
+            long pos = baseOffset1 + remainingBytes;
+            if (UNSAFE.getInt(bytes1, pos) != UNSAFE.getInt(bytes2, pos + diff)) {
                 return false;
             }
         }
+        final long baseOffset2 = baseOffset1 + diff;
         if (remainingBytes >= 2) {
             return UNSAFE.getChar(bytes1, baseOffset1) == UNSAFE.getChar(bytes2, baseOffset2) &&
-                   (remainingBytes == 2 || bytes1[startPos1 + 2] == bytes2[startPos2 + 2]);
+                    (remainingBytes == 2 ||
+                    UNSAFE.getByte(bytes1, baseOffset1 + 2) == UNSAFE.getByte(bytes2, baseOffset2 + 2));
         }
-        return bytes1[startPos1] == bytes2[startPos2];
+        return remainingBytes == 0 ||
+                UNSAFE.getByte(bytes1, baseOffset1) == UNSAFE.getByte(bytes2, baseOffset2);
     }
 
     static int equalsConstantTime(byte[] bytes1, int startPos1, byte[] bytes2, int startPos2, int length) {
         long result = 0;
+        long remainingBytes = length & 7;
         final long baseOffset1 = BYTE_ARRAY_BASE_OFFSET + startPos1;
-        final long baseOffset2 = BYTE_ARRAY_BASE_OFFSET + startPos2;
-        final int remainingBytes = length & 7;
         final long end = baseOffset1 + remainingBytes;
-        for (long i = baseOffset1 - 8 + length, j = baseOffset2 - 8 + length; i >= end; i -= 8, j -= 8) {
-            result |= UNSAFE.getLong(bytes1, i) ^ UNSAFE.getLong(bytes2, j);
-        }
-        switch (remainingBytes) {
-            case 7:
-                return ConstantTimeUtils.equalsConstantTime(result |
-                        (UNSAFE.getInt(bytes1, baseOffset1 + 3) ^ UNSAFE.getInt(bytes2, baseOffset2 + 3)) |
-                        (UNSAFE.getChar(bytes1, baseOffset1 + 1) ^ UNSAFE.getChar(bytes2, baseOffset2 + 1)) |
-                        (UNSAFE.getByte(bytes1, baseOffset1) ^ UNSAFE.getByte(bytes2, baseOffset2)), 0);
-            case 6:
-                return ConstantTimeUtils.equalsConstantTime(result |
-                        (UNSAFE.getInt(bytes1, baseOffset1 + 2) ^ UNSAFE.getInt(bytes2, baseOffset2 + 2)) |
-                        (UNSAFE.getChar(bytes1, baseOffset1) ^ UNSAFE.getChar(bytes2, baseOffset2)), 0);
-            case 5:
-                return ConstantTimeUtils.equalsConstantTime(result |
-                        (UNSAFE.getInt(bytes1, baseOffset1 + 1) ^ UNSAFE.getInt(bytes2, baseOffset2 + 1)) |
-                        (UNSAFE.getByte(bytes1, baseOffset1) ^ UNSAFE.getByte(bytes2, baseOffset2)), 0);
-            case 4:
-                return ConstantTimeUtils.equalsConstantTime(result |
-                        (UNSAFE.getInt(bytes1, baseOffset1) ^ UNSAFE.getInt(bytes2, baseOffset2)), 0);
-            case 3:
-                return ConstantTimeUtils.equalsConstantTime(result |
-                        (UNSAFE.getChar(bytes1, baseOffset1 + 1) ^ UNSAFE.getChar(bytes2, baseOffset2 + 1)) |
-                        (UNSAFE.getByte(bytes1, baseOffset1) ^ UNSAFE.getByte(bytes2, baseOffset2)), 0);
-            case 2:
-                return ConstantTimeUtils.equalsConstantTime(result |
-                        (UNSAFE.getChar(bytes1, baseOffset1) ^ UNSAFE.getChar(bytes2, baseOffset2)), 0);
-            case 1:
-                return ConstantTimeUtils.equalsConstantTime(result |
-                        (UNSAFE.getByte(bytes1, baseOffset1) ^ UNSAFE.getByte(bytes2, baseOffset2)), 0);
-            default:
-                return ConstantTimeUtils.equalsConstantTime(result, 0);
+        final long diff = startPos2 - startPos1;
+        for (long i = baseOffset1 - 8 + length; i >= end; i -= 8) {
+            result |= UNSAFE.getLong(bytes1, i) ^ UNSAFE.getLong(bytes2, i + diff);
         }
+        if (remainingBytes >= 4) {
+            result |= UNSAFE.getInt(bytes1, baseOffset1) ^ UNSAFE.getInt(bytes2, baseOffset1 + diff);
+            remainingBytes -= 4;
+        }
+        if (remainingBytes >= 2) {
+            long pos = end - remainingBytes;
+            result |= UNSAFE.getChar(bytes1, pos) ^ UNSAFE.getChar(bytes2, pos + diff);
+            remainingBytes -= 2;
+        }
+        if (remainingBytes == 1) {
+            long pos = end - 1;
+            result |= UNSAFE.getByte(bytes1, pos) ^ UNSAFE.getByte(bytes2, pos + diff);
+        }
+        return ConstantTimeUtils.equalsConstantTime(result, 0);
     }
 
     static boolean isZero(byte[] bytes, int startPos, int length) {
@@ -718,35 +704,30 @@ final class PlatformDependent0 {
 
     static int hashCodeAscii(byte[] bytes, int startPos, int length) {
         int hash = HASH_CODE_ASCII_SEED;
-        final long baseOffset = BYTE_ARRAY_BASE_OFFSET + startPos;
+        long baseOffset = BYTE_ARRAY_BASE_OFFSET + startPos;
         final int remainingBytes = length & 7;
         final long end = baseOffset + remainingBytes;
         for (long i = baseOffset - 8 + length; i >= end; i -= 8) {
             hash = hashCodeAsciiCompute(UNSAFE.getLong(bytes, i), hash);
         }
-        switch(remainingBytes) {
-        case 7:
-            return ((hash * HASH_CODE_C1 + hashCodeAsciiSanitize(UNSAFE.getByte(bytes, baseOffset)))
-                          * HASH_CODE_C2 + hashCodeAsciiSanitize(UNSAFE.getShort(bytes, baseOffset + 1)))
-                          * HASH_CODE_C1 + hashCodeAsciiSanitize(UNSAFE.getInt(bytes, baseOffset + 3));
-        case 6:
-            return (hash * HASH_CODE_C1 + hashCodeAsciiSanitize(UNSAFE.getShort(bytes, baseOffset)))
-                         * HASH_CODE_C2 + hashCodeAsciiSanitize(UNSAFE.getInt(bytes, baseOffset + 2));
-        case 5:
-            return (hash * HASH_CODE_C1 + hashCodeAsciiSanitize(UNSAFE.getByte(bytes, baseOffset)))
-                         * HASH_CODE_C2 + hashCodeAsciiSanitize(UNSAFE.getInt(bytes, baseOffset + 1));
-        case 4:
-            return hash * HASH_CODE_C1 + hashCodeAsciiSanitize(UNSAFE.getInt(bytes, baseOffset));
-        case 3:
-            return (hash * HASH_CODE_C1 + hashCodeAsciiSanitize(UNSAFE.getByte(bytes, baseOffset)))
-                         * HASH_CODE_C2 + hashCodeAsciiSanitize(UNSAFE.getShort(bytes, baseOffset + 1));
-        case 2:
-            return hash * HASH_CODE_C1 + hashCodeAsciiSanitize(UNSAFE.getShort(bytes, baseOffset));
-        case 1:
-            return hash * HASH_CODE_C1 + hashCodeAsciiSanitize(UNSAFE.getByte(bytes, baseOffset));
-        default:
+        if (remainingBytes == 0) {
             return hash;
         }
+        int hcConst = HASH_CODE_C1;
+        if (remainingBytes != 2 & remainingBytes != 4 & remainingBytes != 6) { // 1, 3, 5, 7
+            hash = hash * HASH_CODE_C1 + hashCodeAsciiSanitize(UNSAFE.getByte(bytes, baseOffset));
+            hcConst = HASH_CODE_C2;
+            baseOffset++;
+        }
+        if (remainingBytes != 1 & remainingBytes != 4 & remainingBytes != 5) { // 2, 3, 6, 7
+            hash = hash * hcConst + hashCodeAsciiSanitize(UNSAFE.getShort(bytes, baseOffset));
+            hcConst = hcConst == HASH_CODE_C1 ? HASH_CODE_C2 : HASH_CODE_C1;
+            baseOffset += 2;
+        }
+        if (remainingBytes >= 4) { // 4, 5, 6, 7
+            return hash * hcConst + hashCodeAsciiSanitize(UNSAFE.getInt(bytes, baseOffset));
+        }
+        return hash;
     }
 
     static int hashCodeAsciiCompute(long value, int hash) {
diff --git a/common/src/main/java/io/netty/util/internal/PromiseNotificationUtil.java b/common/src/main/java/io/netty/util/internal/PromiseNotificationUtil.java
index 42872a8..90a7c89 100644
--- a/common/src/main/java/io/netty/util/internal/PromiseNotificationUtil.java
+++ b/common/src/main/java/io/netty/util/internal/PromiseNotificationUtil.java
@@ -65,7 +65,7 @@ public final class PromiseNotificationUtil {
             Throwable err = p.cause();
             if (err == null) {
                 logger.warn("Failed to mark a promise as failure because it has succeeded already: {}", p, cause);
-            } else {
+            } else if (logger.isWarnEnabled()) {
                 logger.warn(
                         "Failed to mark a promise as failure because it has failed already: {}, unnotified cause: {}",
                         p, ThrowableUtil.stackTraceToString(err), cause);
diff --git a/common/src/main/java/io/netty/util/internal/ReadOnlyIterator.java b/common/src/main/java/io/netty/util/internal/ReadOnlyIterator.java
index f4843e5..59880b1 100644
--- a/common/src/main/java/io/netty/util/internal/ReadOnlyIterator.java
+++ b/common/src/main/java/io/netty/util/internal/ReadOnlyIterator.java
@@ -22,10 +22,7 @@ public final class ReadOnlyIterator<T> implements Iterator<T> {
     private final Iterator<? extends T> iterator;
 
     public ReadOnlyIterator(Iterator<? extends T> iterator) {
-        if (iterator == null) {
-            throw new NullPointerException("iterator");
-        }
-        this.iterator = iterator;
+        this.iterator = ObjectUtil.checkNotNull(iterator, "iterator");
     }
 
     @Override
diff --git a/common/src/main/java/io/netty/util/internal/RecyclableArrayList.java b/common/src/main/java/io/netty/util/internal/RecyclableArrayList.java
index bf98f22..6a1d2c1 100644
--- a/common/src/main/java/io/netty/util/internal/RecyclableArrayList.java
+++ b/common/src/main/java/io/netty/util/internal/RecyclableArrayList.java
@@ -16,8 +16,8 @@
 
 package io.netty.util.internal;
 
-import io.netty.util.Recycler;
-import io.netty.util.Recycler.Handle;
+import io.netty.util.internal.ObjectPool.Handle;
+import io.netty.util.internal.ObjectPool.ObjectCreator;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -33,12 +33,13 @@ public final class RecyclableArrayList extends ArrayList<Object> {
 
     private static final int DEFAULT_INITIAL_CAPACITY = 8;
 
-    private static final Recycler<RecyclableArrayList> RECYCLER = new Recycler<RecyclableArrayList>() {
+    private static final ObjectPool<RecyclableArrayList> RECYCLER = ObjectPool.newPool(
+            new ObjectCreator<RecyclableArrayList>() {
         @Override
-        protected RecyclableArrayList newObject(Handle<RecyclableArrayList> handle) {
+        public RecyclableArrayList newObject(Handle<RecyclableArrayList> handle) {
             return new RecyclableArrayList(handle);
         }
-    };
+    });
 
     private boolean insertSinceRecycled;
 
@@ -110,10 +111,7 @@ public final class RecyclableArrayList extends ArrayList<Object> {
 
     @Override
     public boolean add(Object element) {
-        if (element == null) {
-            throw new NullPointerException("element");
-        }
-        if (super.add(element)) {
+        if (super.add(ObjectUtil.checkNotNull(element, "element"))) {
             insertSinceRecycled = true;
             return true;
         }
@@ -122,19 +120,13 @@ public final class RecyclableArrayList extends ArrayList<Object> {
 
     @Override
     public void add(int index, Object element) {
-        if (element == null) {
-            throw new NullPointerException("element");
-        }
-        super.add(index, element);
+        super.add(index, ObjectUtil.checkNotNull(element, "element"));
         insertSinceRecycled = true;
     }
 
     @Override
     public Object set(int index, Object element) {
-        if (element == null) {
-            throw new NullPointerException("element");
-        }
-        Object old = super.set(index, element);
+        Object old = super.set(index, ObjectUtil.checkNotNull(element, "element"));
         insertSinceRecycled = true;
         return old;
     }
diff --git a/common/src/main/java/io/netty/util/internal/ReferenceCountUpdater.java b/common/src/main/java/io/netty/util/internal/ReferenceCountUpdater.java
new file mode 100644
index 0000000..d6d131b
--- /dev/null
+++ b/common/src/main/java/io/netty/util/internal/ReferenceCountUpdater.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2019 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.util.internal;
+
+import static io.netty.util.internal.ObjectUtil.checkPositive;
+
+import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
+
+import io.netty.util.IllegalReferenceCountException;
+import io.netty.util.ReferenceCounted;
+
+/**
+ * Common logic for {@link ReferenceCounted} implementations
+ */
+public abstract class ReferenceCountUpdater<T extends ReferenceCounted> {
+    /*
+     * Implementation notes:
+     *
+     * For the updated int field:
+     *   Even => "real" refcount is (refCnt >>> 1)
+     *   Odd  => "real" refcount is 0
+     *
+     * (x & y) appears to be surprisingly expensive relative to (x == y). Thus this class uses
+     * a fast-path in some places for most common low values when checking for live (even) refcounts,
+     * for example: if (rawCnt == 2 || rawCnt == 4 || (rawCnt & 1) == 0) { ...
+     */
+
+    protected ReferenceCountUpdater() { }
+
+    public static long getUnsafeOffset(Class<? extends ReferenceCounted> clz, String fieldName) {
+        try {
+            if (PlatformDependent.hasUnsafe()) {
+                return PlatformDependent.objectFieldOffset(clz.getDeclaredField(fieldName));
+            }
+        } catch (Throwable ignore) {
+            // fall-back
+        }
+        return -1;
+    }
+
+    protected abstract AtomicIntegerFieldUpdater<T> updater();
+
+    protected abstract long unsafeOffset();
+
+    public final int initialValue() {
+        return 2;
+    }
+
+    private static int realRefCnt(int rawCnt) {
+        return rawCnt != 2 && rawCnt != 4 && (rawCnt & 1) != 0 ? 0 : rawCnt >>> 1;
+    }
+
+    /**
+     * Like {@link #realRefCnt(int)} but throws if refCnt == 0
+     */
+    private static int toLiveRealRefCnt(int rawCnt, int decrement) {
+        if (rawCnt == 2 || rawCnt == 4 || (rawCnt & 1) == 0) {
+            return rawCnt >>> 1;
+        }
+        // odd rawCnt => already deallocated
+        throw new IllegalReferenceCountException(0, -decrement);
+    }
+
+    private int nonVolatileRawCnt(T instance) {
+        // TODO: Once we compile against later versions of Java we can replace the Unsafe usage here by varhandles.
+        final long offset = unsafeOffset();
+        return offset != -1 ? PlatformDependent.getInt(instance, offset) : updater().get(instance);
+    }
+
+    public final int refCnt(T instance) {
+        return realRefCnt(updater().get(instance));
+    }
+
+    public final boolean isLiveNonVolatile(T instance) {
+        final long offset = unsafeOffset();
+        final int rawCnt = offset != -1 ? PlatformDependent.getInt(instance, offset) : updater().get(instance);
+
+        // The "real" ref count is > 0 if the rawCnt is even.
+        return rawCnt == 2 || rawCnt == 4 || rawCnt == 6 || rawCnt == 8 || (rawCnt & 1) == 0;
+    }
+
+    /**
+     * An unsafe operation that sets the reference count directly
+     */
+    public final void setRefCnt(T instance, int refCnt) {
+        updater().set(instance, refCnt > 0 ? refCnt << 1 : 1); // overflow OK here
+    }
+
+    /**
+     * Resets the reference count to 1
+     */
+    public final void resetRefCnt(T instance) {
+        updater().set(instance, initialValue());
+    }
+
+    public final T retain(T instance) {
+        return retain0(instance, 1, 2);
+    }
+
+    public final T retain(T instance, int increment) {
+        // all changes to the raw count are 2x the "real" change - overflow is OK
+        int rawIncrement = checkPositive(increment, "increment") << 1;
+        return retain0(instance, increment, rawIncrement);
+    }
+
+    // rawIncrement == increment << 1
+    private T retain0(T instance, final int increment, final int rawIncrement) {
+        int oldRef = updater().getAndAdd(instance, rawIncrement);
+        if (oldRef != 2 && oldRef != 4 && (oldRef & 1) != 0) {
+            throw new IllegalReferenceCountException(0, increment);
+        }
+        // don't pass 0!
+        if ((oldRef <= 0 && oldRef + rawIncrement >= 0)
+                || (oldRef >= 0 && oldRef + rawIncrement < oldRef)) {
+            // overflow case
+            updater().getAndAdd(instance, -rawIncrement);
+            throw new IllegalReferenceCountException(realRefCnt(oldRef), increment);
+        }
+        return instance;
+    }
+
+    public final boolean release(T instance) {
+        int rawCnt = nonVolatileRawCnt(instance);
+        return rawCnt == 2 ? tryFinalRelease0(instance, 2) || retryRelease0(instance, 1)
+                : nonFinalRelease0(instance, 1, rawCnt, toLiveRealRefCnt(rawCnt, 1));
+    }
+
+    public final boolean release(T instance, int decrement) {
+        int rawCnt = nonVolatileRawCnt(instance);
+        int realCnt = toLiveRealRefCnt(rawCnt, checkPositive(decrement, "decrement"));
+        return decrement == realCnt ? tryFinalRelease0(instance, rawCnt) || retryRelease0(instance, decrement)
+                : nonFinalRelease0(instance, decrement, rawCnt, realCnt);
+    }
+
+    private boolean tryFinalRelease0(T instance, int expectRawCnt) {
+        return updater().compareAndSet(instance, expectRawCnt, 1); // any odd number will work
+    }
+
+    private boolean nonFinalRelease0(T instance, int decrement, int rawCnt, int realCnt) {
+        if (decrement < realCnt
+                // all changes to the raw count are 2x the "real" change - overflow is OK
+                && updater().compareAndSet(instance, rawCnt, rawCnt - (decrement << 1))) {
+            return false;
+        }
+        return retryRelease0(instance, decrement);
+    }
+
+    private boolean retryRelease0(T instance, int decrement) {
+        for (;;) {
+            int rawCnt = updater().get(instance), realCnt = toLiveRealRefCnt(rawCnt, decrement);
+            if (decrement == realCnt) {
+                if (tryFinalRelease0(instance, rawCnt)) {
+                    return true;
+                }
+            } else if (decrement < realCnt) {
+                // all changes to the raw count are 2x the "real" change
+                if (updater().compareAndSet(instance, rawCnt, rawCnt - (decrement << 1))) {
+                    return false;
+                }
+            } else {
+                throw new IllegalReferenceCountException(realCnt, -decrement);
+            }
+            Thread.yield(); // this benefits throughput under high contention
+        }
+    }
+}
diff --git a/common/src/main/java/io/netty/util/internal/SocketUtils.java b/common/src/main/java/io/netty/util/internal/SocketUtils.java
index 63fb742..02d9896 100644
--- a/common/src/main/java/io/netty/util/internal/SocketUtils.java
+++ b/common/src/main/java/io/netty/util/internal/SocketUtils.java
@@ -32,6 +32,7 @@ import java.security.AccessController;
 import java.security.PrivilegedAction;
 import java.security.PrivilegedActionException;
 import java.security.PrivilegedExceptionAction;
+import java.util.Collections;
 import java.util.Enumeration;
 
 /**
@@ -42,9 +43,16 @@ import java.util.Enumeration;
  */
 public final class SocketUtils {
 
+    private static final Enumeration<Object> EMPTY = Collections.enumeration(Collections.emptyList());
+
     private SocketUtils() {
     }
 
+    @SuppressWarnings("unchecked")
+    private static <T> Enumeration<T> empty() {
+        return (Enumeration<T>) EMPTY;
+    }
+
     public static void connect(final Socket socket, final SocketAddress remoteAddress, final int timeout)
             throws IOException {
         try {
@@ -88,6 +96,7 @@ public final class SocketUtils {
         }
     }
 
+    @SuppressJava6Requirement(reason = "Usage guarded by java version check")
     public static void bind(final SocketChannel socketChannel, final SocketAddress address) throws IOException {
         try {
             AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
@@ -115,6 +124,7 @@ public final class SocketUtils {
         }
     }
 
+    @SuppressJava6Requirement(reason = "Usage guarded by java version check")
     public static void bind(final DatagramChannel networkChannel, final SocketAddress address) throws IOException {
         try {
             AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
@@ -174,14 +184,23 @@ public final class SocketUtils {
     }
 
     public static Enumeration<InetAddress> addressesFromNetworkInterface(final NetworkInterface intf) {
-        return AccessController.doPrivileged(new PrivilegedAction<Enumeration<InetAddress>>() {
+        Enumeration<InetAddress> addresses =
+                AccessController.doPrivileged(new PrivilegedAction<Enumeration<InetAddress>>() {
             @Override
             public Enumeration<InetAddress> run() {
                 return intf.getInetAddresses();
             }
         });
+        // Android seems to sometimes return null even if this is not a valid return value by the api docs.
+        // Just return an empty Enumeration in this case.
+        // See https://github.com/netty/netty/issues/10045
+        if (addresses == null) {
+            return empty();
+        }
+        return addresses;
     }
 
+    @SuppressJava6Requirement(reason = "Usage guarded by java version check")
     public static InetAddress loopbackAddress() {
         return AccessController.doPrivileged(new PrivilegedAction<InetAddress>() {
             @Override
diff --git a/common/src/main/java/io/netty/util/internal/StringUtil.java b/common/src/main/java/io/netty/util/internal/StringUtil.java
index e81a024..7651129 100644
--- a/common/src/main/java/io/netty/util/internal/StringUtil.java
+++ b/common/src/main/java/io/netty/util/internal/StringUtil.java
@@ -17,6 +17,8 @@ package io.netty.util.internal;
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
 import java.util.List;
 
 import static io.netty.util.internal.ObjectUtil.*;
@@ -38,6 +40,7 @@ public final class StringUtil {
 
     private static final String[] BYTE2HEX_PAD = new String[256];
     private static final String[] BYTE2HEX_NOPAD = new String[256];
+    private static final byte[] HEX2B;
 
     /**
      * 2 - Quote character at beginning and end.
@@ -53,6 +56,33 @@ public final class StringUtil {
             BYTE2HEX_PAD[i] = i > 0xf ? str : ('0' + str);
             BYTE2HEX_NOPAD[i] = str;
         }
+        // Generate the lookup table that converts an hex char into its decimal value:
+        // the size of the table is such that the JVM is capable of save any bounds-check
+        // if a char type is used as an index.
+        HEX2B = new byte[Character.MAX_VALUE + 1];
+        Arrays.fill(HEX2B, (byte) -1);
+        HEX2B['0'] = (byte) 0;
+        HEX2B['1'] = (byte) 1;
+        HEX2B['2'] = (byte) 2;
+        HEX2B['3'] = (byte) 3;
+        HEX2B['4'] = (byte) 4;
+        HEX2B['5'] = (byte) 5;
+        HEX2B['6'] = (byte) 6;
+        HEX2B['7'] = (byte) 7;
+        HEX2B['8'] = (byte) 8;
+        HEX2B['9'] = (byte) 9;
+        HEX2B['A'] = (byte) 10;
+        HEX2B['B'] = (byte) 11;
+        HEX2B['C'] = (byte) 12;
+        HEX2B['D'] = (byte) 13;
+        HEX2B['E'] = (byte) 14;
+        HEX2B['F'] = (byte) 15;
+        HEX2B['a'] = (byte) 10;
+        HEX2B['b'] = (byte) 11;
+        HEX2B['c'] = (byte) 12;
+        HEX2B['d'] = (byte) 13;
+        HEX2B['e'] = (byte) 14;
+        HEX2B['f'] = (byte) 15;
     }
 
     private StringUtil() {
@@ -210,18 +240,11 @@ public final class StringUtil {
      * given, or {@code -1} if the character is invalid.
      */
     public static int decodeHexNibble(final char c) {
+        assert HEX2B.length == (Character.MAX_VALUE + 1);
         // Character.digit() is not used here, as it addresses a larger
         // set of characters (both ASCII and full-width latin letters).
-        if (c >= '0' && c <= '9') {
-            return c - '0';
-        }
-        if (c >= 'A' && c <= 'F') {
-            return c - ('A' - 0xA);
-        }
-        if (c >= 'a' && c <= 'f') {
-            return c - ('a' - 0xA);
-        }
-        return -1;
+        final int index = c;
+        return HEX2B[index];
     }
 
     /**
@@ -541,7 +564,7 @@ public final class StringUtil {
      *
      * @param seq    The string to search.
      * @param offset The offset to start searching at.
-     * @return the index of the first non-white space character or &lt;{@code 0} if none was found.
+     * @return the index of the first non-white space character or &lt;{@code -1} if none was found.
      */
     public static int indexOfNonWhiteSpace(CharSequence seq, int offset) {
         for (; offset < seq.length(); ++offset) {
@@ -552,6 +575,22 @@ public final class StringUtil {
         return -1;
     }
 
+    /**
+     * Find the index of the first white space character in {@code s} starting at {@code offset}.
+     *
+     * @param seq    The string to search.
+     * @param offset The offset to start searching at.
+     * @return the index of the first white space character or &lt;{@code -1} if none was found.
+     */
+    public static int indexOfWhiteSpace(CharSequence seq, int offset) {
+        for (; offset < seq.length(); ++offset) {
+            if (Character.isWhitespace(seq.charAt(offset))) {
+                return offset;
+            }
+        }
+        return -1;
+    }
+
     /**
      * Determine if {@code c} lies within the range of values defined for
      * <a href="http://unicode.org/glossary/#surrogate_code_point">Surrogate Code Point</a>.
@@ -597,6 +636,36 @@ public final class StringUtil {
         return start == 0 && end == length - 1 ? value : value.subSequence(start, end + 1);
     }
 
+    /**
+     * Returns a char sequence that contains all {@code elements} joined by a given separator.
+     *
+     * @param separator for each element
+     * @param elements to join together
+     *
+     * @return a char sequence joined by a given separator.
+     */
+    public static CharSequence join(CharSequence separator, Iterable<? extends CharSequence> elements) {
+        ObjectUtil.checkNotNull(separator, "separator");
+        ObjectUtil.checkNotNull(elements, "elements");
+
+        Iterator<? extends CharSequence> iterator = elements.iterator();
+        if (!iterator.hasNext()) {
+            return EMPTY_STRING;
+        }
+
+        CharSequence firstElement = iterator.next();
+        if (!iterator.hasNext()) {
+            return firstElement;
+        }
+
+        StringBuilder builder = new StringBuilder(firstElement);
+        do {
+            builder.append(separator).append(iterator.next());
+        } while (iterator.hasNext());
+
+        return builder;
+    }
+
     /**
      * @return {@code length} if no OWS is found.
      */
@@ -622,4 +691,5 @@ public final class StringUtil {
     private static boolean isOws(char c) {
         return c == SPACE || c == TAB;
     }
+
 }
diff --git a/common/src/main/java/io/netty/util/internal/SuppressJava6Requirement.java b/common/src/main/java/io/netty/util/internal/SuppressJava6Requirement.java
index 557d009..3724dc4 100644
--- a/common/src/main/java/io/netty/util/internal/SuppressJava6Requirement.java
+++ b/common/src/main/java/io/netty/util/internal/SuppressJava6Requirement.java
@@ -25,7 +25,7 @@ import java.lang.annotation.Target;
  * Annotation to suppress the Java 6 source code requirement checks for a method.
  */
 @Retention(RetentionPolicy.CLASS)
-@Target({ ElementType.METHOD })
+@Target({ ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE })
 public @interface SuppressJava6Requirement {
 
     String reason();
diff --git a/common/src/main/java/io/netty/util/internal/SystemPropertyUtil.java b/common/src/main/java/io/netty/util/internal/SystemPropertyUtil.java
index d13f387..f8632d9 100644
--- a/common/src/main/java/io/netty/util/internal/SystemPropertyUtil.java
+++ b/common/src/main/java/io/netty/util/internal/SystemPropertyUtil.java
@@ -56,9 +56,7 @@ public final class SystemPropertyUtil {
      *         specified property is not allowed.
      */
     public static String get(final String key, String def) {
-        if (key == null) {
-            throw new NullPointerException("key");
-        }
+        ObjectUtil.checkNotNull(key, "key");
         if (key.isEmpty()) {
             throw new IllegalArgumentException("key must not be empty.");
         }
diff --git a/common/src/main/java/io/netty/util/internal/ThreadExecutorMap.java b/common/src/main/java/io/netty/util/internal/ThreadExecutorMap.java
new file mode 100644
index 0000000..807698a
--- /dev/null
+++ b/common/src/main/java/io/netty/util/internal/ThreadExecutorMap.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2019 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.util.internal;
+
+import io.netty.util.concurrent.EventExecutor;
+import io.netty.util.concurrent.FastThreadLocal;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ThreadFactory;
+
+/**
+ * Allow to retrieve the {@link EventExecutor} for the calling {@link Thread}.
+ */
+public final class ThreadExecutorMap {
+
+    private static final FastThreadLocal<EventExecutor> mappings = new FastThreadLocal<EventExecutor>();
+
+    private ThreadExecutorMap() { }
+
+    /**
+     * Returns the current {@link EventExecutor} that uses the {@link Thread}, or {@code null} if none / unknown.
+     */
+    public static EventExecutor currentExecutor() {
+        return mappings.get();
+    }
+
+    /**
+     * Set the current {@link EventExecutor} that is used by the {@link Thread}.
+     */
+    private static void setCurrentEventExecutor(EventExecutor executor) {
+        mappings.set(executor);
+    }
+
+    /**
+     * Decorate the given {@link Executor} and ensure {@link #currentExecutor()} will return {@code eventExecutor}
+     * when called from within the {@link Runnable} during execution.
+     */
+    public static Executor apply(final Executor executor, final EventExecutor eventExecutor) {
+        ObjectUtil.checkNotNull(executor, "executor");
+        ObjectUtil.checkNotNull(eventExecutor, "eventExecutor");
+        return new Executor() {
+            @Override
+            public void execute(final Runnable command) {
+                executor.execute(apply(command, eventExecutor));
+            }
+        };
+    }
+
+    /**
+     * Decorate the given {@link Runnable} and ensure {@link #currentExecutor()} will return {@code eventExecutor}
+     * when called from within the {@link Runnable} during execution.
+     */
+    public static Runnable apply(final Runnable command, final EventExecutor eventExecutor) {
+        ObjectUtil.checkNotNull(command, "command");
+        ObjectUtil.checkNotNull(eventExecutor, "eventExecutor");
+        return new Runnable() {
+            @Override
+            public void run() {
+                setCurrentEventExecutor(eventExecutor);
+                try {
+                    command.run();
+                } finally {
+                    setCurrentEventExecutor(null);
+                }
+            }
+        };
+    }
+
+    /**
+     * Decorate the given {@link ThreadFactory} and ensure {@link #currentExecutor()} will return {@code eventExecutor}
+     * when called from within the {@link Runnable} during execution.
+     */
+    public static ThreadFactory apply(final ThreadFactory threadFactory, final EventExecutor eventExecutor) {
+        ObjectUtil.checkNotNull(threadFactory, "command");
+        ObjectUtil.checkNotNull(eventExecutor, "eventExecutor");
+        return new ThreadFactory() {
+            @Override
+            public Thread newThread(Runnable r) {
+                return threadFactory.newThread(apply(r, eventExecutor));
+            }
+        };
+    }
+}
diff --git a/common/src/main/java/io/netty/util/internal/ThreadLocalRandom.java b/common/src/main/java/io/netty/util/internal/ThreadLocalRandom.java
index 2282769..8981c10 100644
--- a/common/src/main/java/io/netty/util/internal/ThreadLocalRandom.java
+++ b/common/src/main/java/io/netty/util/internal/ThreadLocalRandom.java
@@ -271,6 +271,7 @@ public final class ThreadLocalRandom extends Random {
      *
      * @throws UnsupportedOperationException always
      */
+    @Override
     public void setSeed(long seed) {
         if (initialized) {
             throw new UnsupportedOperationException();
@@ -278,6 +279,7 @@ public final class ThreadLocalRandom extends Random {
         rnd = (seed ^ multiplier) & mask;
     }
 
+    @Override
     protected int next(int bits) {
         rnd = (rnd * multiplier + addend) & mask;
         return (int) (rnd >>> (48 - bits));
diff --git a/common/src/main/java/io/netty/util/internal/logging/AbstractInternalLogger.java b/common/src/main/java/io/netty/util/internal/logging/AbstractInternalLogger.java
index 6486efa..b29a415 100644
--- a/common/src/main/java/io/netty/util/internal/logging/AbstractInternalLogger.java
+++ b/common/src/main/java/io/netty/util/internal/logging/AbstractInternalLogger.java
@@ -15,6 +15,7 @@
  */
 package io.netty.util.internal.logging;
 
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.StringUtil;
 
 import java.io.ObjectStreamException;
@@ -37,10 +38,7 @@ public abstract class AbstractInternalLogger implements InternalLogger, Serializ
      * Creates a new instance.
      */
     protected AbstractInternalLogger(String name) {
-        if (name == null) {
-            throw new NullPointerException("name");
-        }
-        this.name = name;
+        this.name = ObjectUtil.checkNotNull(name, "name");
     }
 
     @Override
diff --git a/common/src/main/java/io/netty/util/internal/logging/CommonsLogger.java b/common/src/main/java/io/netty/util/internal/logging/CommonsLogger.java
index 110f0f1..c31c830 100644
--- a/common/src/main/java/io/netty/util/internal/logging/CommonsLogger.java
+++ b/common/src/main/java/io/netty/util/internal/logging/CommonsLogger.java
@@ -39,6 +39,7 @@
  */
 package io.netty.util.internal.logging;
 
+import io.netty.util.internal.ObjectUtil;
 import org.apache.commons.logging.Log;
 
 /**
@@ -57,10 +58,7 @@ class CommonsLogger extends AbstractInternalLogger {
 
     CommonsLogger(Log logger, String name) {
         super(name);
-        if (logger == null) {
-            throw new NullPointerException("logger");
-        }
-        this.logger = logger;
+        this.logger = ObjectUtil.checkNotNull(logger, "logger");
     }
 
     /**
diff --git a/common/src/main/java/io/netty/util/internal/logging/InternalLoggerFactory.java b/common/src/main/java/io/netty/util/internal/logging/InternalLoggerFactory.java
index 12c1b5a..508f571 100644
--- a/common/src/main/java/io/netty/util/internal/logging/InternalLoggerFactory.java
+++ b/common/src/main/java/io/netty/util/internal/logging/InternalLoggerFactory.java
@@ -16,6 +16,8 @@
 
 package io.netty.util.internal.logging;
 
+import io.netty.util.internal.ObjectUtil;
+
 /**
  * Creates an {@link InternalLogger} or changes the default factory
  * implementation.  This factory allows you to choose what logging framework
@@ -43,12 +45,12 @@ public abstract class InternalLoggerFactory {
             f.newInstance(name).debug("Using SLF4J as the default logging framework");
         } catch (Throwable ignore1) {
             try {
-                f = Log4JLoggerFactory.INSTANCE;
-                f.newInstance(name).debug("Using Log4J as the default logging framework");
+                f = Log4J2LoggerFactory.INSTANCE;
+                f.newInstance(name).debug("Using Log4J2 as the default logging framework");
             } catch (Throwable ignore2) {
                 try {
-                    f = Log4J2LoggerFactory.INSTANCE;
-                    f.newInstance(name).debug("Using Log4J2 as the default logging framework");
+                    f = Log4JLoggerFactory.INSTANCE;
+                    f.newInstance(name).debug("Using Log4J as the default logging framework");
                 } catch (Throwable ignore3) {
                     f = JdkLoggerFactory.INSTANCE;
                     f.newInstance(name).debug("Using java.util.logging as the default logging framework");
@@ -73,10 +75,7 @@ public abstract class InternalLoggerFactory {
      * Changes the default factory.
      */
     public static void setDefaultFactory(InternalLoggerFactory defaultFactory) {
-        if (defaultFactory == null) {
-            throw new NullPointerException("defaultFactory");
-        }
-        InternalLoggerFactory.defaultFactory = defaultFactory;
+        InternalLoggerFactory.defaultFactory = ObjectUtil.checkNotNull(defaultFactory, "defaultFactory");
     }
 
     /**
diff --git a/common/src/main/java/io/netty/util/internal/logging/LocationAwareSlf4JLogger.java b/common/src/main/java/io/netty/util/internal/logging/LocationAwareSlf4JLogger.java
index 33eb705..7f36283 100644
--- a/common/src/main/java/io/netty/util/internal/logging/LocationAwareSlf4JLogger.java
+++ b/common/src/main/java/io/netty/util/internal/logging/LocationAwareSlf4JLogger.java
@@ -79,7 +79,7 @@ final class LocationAwareSlf4JLogger extends AbstractInternalLogger {
     @Override
     public void trace(String format, Object... argArray) {
         if (isTraceEnabled()) {
-            log(TRACE_INT, org.slf4j.helpers.MessageFormatter.format(format, argArray));
+            log(TRACE_INT, org.slf4j.helpers.MessageFormatter.arrayFormat(format, argArray));
         }
     }
 
@@ -119,7 +119,7 @@ final class LocationAwareSlf4JLogger extends AbstractInternalLogger {
     @Override
     public void debug(String format, Object... argArray) {
         if (isDebugEnabled()) {
-            log(DEBUG_INT, org.slf4j.helpers.MessageFormatter.format(format, argArray));
+            log(DEBUG_INT, org.slf4j.helpers.MessageFormatter.arrayFormat(format, argArray));
         }
     }
 
@@ -159,7 +159,7 @@ final class LocationAwareSlf4JLogger extends AbstractInternalLogger {
     @Override
     public void info(String format, Object... argArray) {
         if (isInfoEnabled()) {
-            log(INFO_INT, org.slf4j.helpers.MessageFormatter.format(format, argArray));
+            log(INFO_INT, org.slf4j.helpers.MessageFormatter.arrayFormat(format, argArray));
         }
     }
 
@@ -192,7 +192,7 @@ final class LocationAwareSlf4JLogger extends AbstractInternalLogger {
     @Override
     public void warn(String format, Object... argArray) {
         if (isWarnEnabled()) {
-            log(WARN_INT, org.slf4j.helpers.MessageFormatter.format(format, argArray));
+            log(WARN_INT, org.slf4j.helpers.MessageFormatter.arrayFormat(format, argArray));
         }
     }
 
@@ -239,7 +239,7 @@ final class LocationAwareSlf4JLogger extends AbstractInternalLogger {
     @Override
     public void error(String format, Object... argArray) {
         if (isErrorEnabled()) {
-            log(ERROR_INT, org.slf4j.helpers.MessageFormatter.format(format, argArray));
+            log(ERROR_INT, org.slf4j.helpers.MessageFormatter.arrayFormat(format, argArray));
         }
     }
 
diff --git a/common/src/main/java/io/netty/util/internal/svm/CleanerJava6Substitution.java b/common/src/main/java/io/netty/util/internal/svm/CleanerJava6Substitution.java
new file mode 100644
index 0000000..cd78c67
--- /dev/null
+++ b/common/src/main/java/io/netty/util/internal/svm/CleanerJava6Substitution.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2019 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.util.internal.svm;
+
+import com.oracle.svm.core.annotate.Alias;
+import com.oracle.svm.core.annotate.RecomputeFieldValue;
+import com.oracle.svm.core.annotate.TargetClass;
+
+@TargetClass(className = "io.netty.util.internal.CleanerJava6")
+final class CleanerJava6Substitution {
+    private CleanerJava6Substitution() {
+    }
+
+    @Alias
+    @RecomputeFieldValue(
+        kind = RecomputeFieldValue.Kind.FieldOffset,
+        declClassName = "java.nio.DirectByteBuffer",
+        name = "cleaner")
+    private static long CLEANER_FIELD_OFFSET;
+}
diff --git a/common/src/main/java/io/netty/util/internal/svm/PlatformDependent0Substitution.java b/common/src/main/java/io/netty/util/internal/svm/PlatformDependent0Substitution.java
new file mode 100644
index 0000000..01b9db2
--- /dev/null
+++ b/common/src/main/java/io/netty/util/internal/svm/PlatformDependent0Substitution.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2019 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.util.internal.svm;
+
+import com.oracle.svm.core.annotate.Alias;
+import com.oracle.svm.core.annotate.RecomputeFieldValue;
+import com.oracle.svm.core.annotate.TargetClass;
+
+@TargetClass(className = "io.netty.util.internal.PlatformDependent0")
+final class PlatformDependent0Substitution {
+    private PlatformDependent0Substitution() {
+    }
+
+    @Alias
+    @RecomputeFieldValue(
+        kind = RecomputeFieldValue.Kind.FieldOffset,
+        declClassName = "java.nio.Buffer",
+        name = "address")
+    private static long ADDRESS_FIELD_OFFSET;
+}
diff --git a/common/src/main/java/io/netty/util/internal/svm/PlatformDependentSubstitution.java b/common/src/main/java/io/netty/util/internal/svm/PlatformDependentSubstitution.java
new file mode 100644
index 0000000..1afe0a0
--- /dev/null
+++ b/common/src/main/java/io/netty/util/internal/svm/PlatformDependentSubstitution.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2019 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.util.internal.svm;
+
+import com.oracle.svm.core.annotate.Alias;
+import com.oracle.svm.core.annotate.RecomputeFieldValue;
+import com.oracle.svm.core.annotate.TargetClass;
+
+@TargetClass(className = "io.netty.util.internal.PlatformDependent")
+final class PlatformDependentSubstitution {
+    private PlatformDependentSubstitution() {
+    }
+
+    /**
+     * The class PlatformDependent caches the byte array base offset by reading the
+     * field from PlatformDependent0. The automatic recomputation of Substrate VM
+     * correctly recomputes the field in PlatformDependent0, but since the caching
+     * in PlatformDependent happens during image building, the non-recomputed value
+     * is cached.
+     */
+    @Alias
+    @RecomputeFieldValue(
+        kind = RecomputeFieldValue.Kind.ArrayBaseOffset,
+        declClass = byte[].class)
+    private static long BYTE_ARRAY_BASE_OFFSET;
+}
diff --git a/common/src/main/java/io/netty/util/internal/svm/UnsafeRefArrayAccessSubstitution.java b/common/src/main/java/io/netty/util/internal/svm/UnsafeRefArrayAccessSubstitution.java
new file mode 100644
index 0000000..732fe28
--- /dev/null
+++ b/common/src/main/java/io/netty/util/internal/svm/UnsafeRefArrayAccessSubstitution.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2019 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.util.internal.svm;
+
+import com.oracle.svm.core.annotate.Alias;
+import com.oracle.svm.core.annotate.RecomputeFieldValue;
+import com.oracle.svm.core.annotate.TargetClass;
+
+@TargetClass(className = "io.netty.util.internal.shaded.org.jctools.util.UnsafeRefArrayAccess")
+final class UnsafeRefArrayAccessSubstitution {
+    private UnsafeRefArrayAccessSubstitution() {
+    }
+
+    @Alias
+    @RecomputeFieldValue(
+        kind = RecomputeFieldValue.Kind.ArrayIndexShift,
+        declClass = Object[].class)
+    public static int REF_ELEMENT_SHIFT;
+}
diff --git a/common/src/main/java/io/netty/util/internal/svm/package-info.java b/common/src/main/java/io/netty/util/internal/svm/package-info.java
new file mode 100644
index 0000000..e9b82ec
--- /dev/null
+++ b/common/src/main/java/io/netty/util/internal/svm/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2019 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.
+ */
+
+/**
+ * SVM substitutions for classes that will cause trouble while compiling
+ * into native image.
+ */
+package io.netty.util.internal.svm;
diff --git a/common/src/main/resources/META-INF/native-image/io.netty/common/native-image.properties b/common/src/main/resources/META-INF/native-image/io.netty/common/native-image.properties
new file mode 100644
index 0000000..5c11bf6
--- /dev/null
+++ b/common/src/main/resources/META-INF/native-image/io.netty/common/native-image.properties
@@ -0,0 +1,15 @@
+# Copyright 2019 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.
+
+Args = --initialize-at-run-time=io.netty.util.AbstractReferenceCounted,io.netty.util.concurrent.GlobalEventExecutor,io.netty.util.concurrent.ImmediateEventExecutor,io.netty.util.concurrent.ScheduledFutureTask,io.netty.util.internal.ThreadLocalRandom
diff --git a/common/src/main/resources/META-INF/services/reactor.blockhound.integration.BlockHoundIntegration b/common/src/main/resources/META-INF/services/reactor.blockhound.integration.BlockHoundIntegration
new file mode 100644
index 0000000..5cf376d
--- /dev/null
+++ b/common/src/main/resources/META-INF/services/reactor.blockhound.integration.BlockHoundIntegration
@@ -0,0 +1,14 @@
+# Copyright 2019 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.
+io.netty.util.internal.Hidden$NettyBlockHoundIntegration
\ No newline at end of file
diff --git a/common/src/main/templates/io/netty/util/collection/KObjectHashMap.template b/common/src/main/templates/io/netty/util/collection/KObjectHashMap.template
index d8aeb1b..7db1ceb 100644
--- a/common/src/main/templates/io/netty/util/collection/KObjectHashMap.template
+++ b/common/src/main/templates/io/netty/util/collection/KObjectHashMap.template
@@ -236,7 +236,7 @@ public class @K@ObjectHashMap<V> implements @K@ObjectMap<V> {
 
                     @Override
                     public void remove() {
-                        throw new UnsupportedOperationException();
+                        iter.remove();
                     }
                 };
             }
diff --git a/common/src/test/java/io/netty/util/AsciiStringCharacterTest.java b/common/src/test/java/io/netty/util/AsciiStringCharacterTest.java
index c2a835d..7534bb4 100644
--- a/common/src/test/java/io/netty/util/AsciiStringCharacterTest.java
+++ b/common/src/test/java/io/netty/util/AsciiStringCharacterTest.java
@@ -37,6 +37,15 @@ import static org.junit.Assert.assertTrue;
 public class AsciiStringCharacterTest {
     private static final Random r = new Random();
 
+    @Test
+    public void testContentEqualsIgnoreCase() {
+        byte[] bytes = { 32, 'a' };
+        AsciiString asciiString = new AsciiString(bytes, 1, 1, false);
+        // https://github.com/netty/netty/issues/9475
+        assertFalse(asciiString.contentEqualsIgnoreCase("b"));
+        assertFalse(asciiString.contentEqualsIgnoreCase(AsciiString.of("b")));
+    }
+
     @Test
     public void testGetBytesStringBuilder() {
         final StringBuilder b = new StringBuilder();
@@ -356,15 +365,15 @@ public class AsciiStringCharacterTest {
     @Test
     public void testLastIndexOfCharSequence() {
         assertEquals(0, new AsciiString("abcd").lastIndexOf("abcd", 0));
-        assertEquals(0, new AsciiString("abcd").lastIndexOf("abc", 0));
-        assertEquals(1, new AsciiString("abcd").lastIndexOf("bcd", 0));
-        assertEquals(1, new AsciiString("abcd").lastIndexOf("bc", 0));
-        assertEquals(5, new AsciiString("abcdabcd").lastIndexOf("bcd", 0));
-        assertEquals(0, new AsciiString("abcd", 1, 2).lastIndexOf("bc", 0));
-        assertEquals(0, new AsciiString("abcd", 1, 3).lastIndexOf("bcd", 0));
-        assertEquals(1, new AsciiString("abcdabcd", 4, 4).lastIndexOf("bcd", 0));
+        assertEquals(0, new AsciiString("abcd").lastIndexOf("abc", 4));
+        assertEquals(1, new AsciiString("abcd").lastIndexOf("bcd", 4));
+        assertEquals(1, new AsciiString("abcd").lastIndexOf("bc", 4));
+        assertEquals(5, new AsciiString("abcdabcd").lastIndexOf("bcd", 10));
+        assertEquals(0, new AsciiString("abcd", 1, 2).lastIndexOf("bc", 2));
+        assertEquals(0, new AsciiString("abcd", 1, 3).lastIndexOf("bcd", 3));
+        assertEquals(1, new AsciiString("abcdabcd", 4, 4).lastIndexOf("bcd", 4));
         assertEquals(3, new AsciiString("012345").lastIndexOf("345", 3));
-        assertEquals(3, new AsciiString("012345").lastIndexOf("345", 0));
+        assertEquals(3, new AsciiString("012345").lastIndexOf("345", 6));
 
         // Test with empty string
         assertEquals(0, new AsciiString("abcd").lastIndexOf("", 0));
@@ -376,7 +385,7 @@ public class AsciiStringCharacterTest {
         assertEquals(-1, new AsciiString("abcdbc").lastIndexOf("bce", 0));
         assertEquals(-1, new AsciiString("abcd", 1, 3).lastIndexOf("abc", 0));
         assertEquals(-1, new AsciiString("abcd", 1, 2).lastIndexOf("bd", 0));
-        assertEquals(-1, new AsciiString("012345").lastIndexOf("345", 4));
+        assertEquals(-1, new AsciiString("012345").lastIndexOf("345", 2));
         assertEquals(-1, new AsciiString("012345").lastIndexOf("abc", 3));
         assertEquals(-1, new AsciiString("012345").lastIndexOf("abc", 0));
         assertEquals(-1, new AsciiString("012345").lastIndexOf("abcdefghi", 0));
diff --git a/common/src/test/java/io/netty/util/RecyclerTest.java b/common/src/test/java/io/netty/util/RecyclerTest.java
index e4bcbf1..8eaef8d 100644
--- a/common/src/test/java/io/netty/util/RecyclerTest.java
+++ b/common/src/test/java/io/netty/util/RecyclerTest.java
@@ -174,6 +174,7 @@ public class RecyclerTest {
 
         final HandledObject o = recycler.get();
         final HandledObject o2 = recycler.get();
+
         final Thread thread = new Thread() {
             @Override
             public void run() {
diff --git a/common/src/test/java/io/netty/util/concurrent/DefaultPromiseTest.java b/common/src/test/java/io/netty/util/concurrent/DefaultPromiseTest.java
index fd9a3e9..84589bd 100644
--- a/common/src/test/java/io/netty/util/concurrent/DefaultPromiseTest.java
+++ b/common/src/test/java/io/netty/util/concurrent/DefaultPromiseTest.java
@@ -21,6 +21,7 @@ import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 import org.junit.BeforeClass;
 import org.junit.Test;
+import org.mockito.Mockito;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -36,6 +37,7 @@ import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import static java.lang.Math.max;
+import static org.hamcrest.Matchers.instanceOf;
 import static org.hamcrest.Matchers.lessThan;
 import static org.junit.Assert.*;
 
@@ -63,10 +65,45 @@ public class DefaultPromiseTest {
         return max(stackOverflowDepth << 1, stackOverflowDepth);
     }
 
+    @Test
+    public void testCancelDoesNotScheduleWhenNoListeners() {
+        EventExecutor executor = Mockito.mock(EventExecutor.class);
+        Mockito.when(executor.inEventLoop()).thenReturn(false);
+
+        Promise<Void> promise = new DefaultPromise<Void>(executor);
+        assertTrue(promise.cancel(false));
+        Mockito.verify(executor, Mockito.never()).execute(Mockito.any(Runnable.class));
+        assertTrue(promise.isCancelled());
+    }
+
+    @Test
+    public void testSuccessDoesNotScheduleWhenNoListeners() {
+        EventExecutor executor = Mockito.mock(EventExecutor.class);
+        Mockito.when(executor.inEventLoop()).thenReturn(false);
+
+        Object value = new Object();
+        Promise<Object> promise = new DefaultPromise<Object>(executor);
+        promise.setSuccess(value);
+        Mockito.verify(executor, Mockito.never()).execute(Mockito.any(Runnable.class));
+        assertSame(value, promise.getNow());
+    }
+
+    @Test
+    public void testFailureDoesNotScheduleWhenNoListeners() {
+        EventExecutor executor = Mockito.mock(EventExecutor.class);
+        Mockito.when(executor.inEventLoop()).thenReturn(false);
+
+        Exception cause = new Exception();
+        Promise<Void> promise = new DefaultPromise<Void>(executor);
+        promise.setFailure(cause);
+        Mockito.verify(executor, Mockito.never()).execute(Mockito.any(Runnable.class));
+        assertSame(cause, promise.cause());
+    }
+
     @Test(expected = CancellationException.class)
     public void testCancellationExceptionIsThrownWhenBlockingGet() throws InterruptedException, ExecutionException {
         final Promise<Void> promise = new DefaultPromise<Void>(ImmediateEventExecutor.INSTANCE);
-        promise.cancel(false);
+        assertTrue(promise.cancel(false));
         promise.get();
     }
 
@@ -74,10 +111,18 @@ public class DefaultPromiseTest {
     public void testCancellationExceptionIsThrownWhenBlockingGetWithTimeout() throws InterruptedException,
             ExecutionException, TimeoutException {
         final Promise<Void> promise = new DefaultPromise<Void>(ImmediateEventExecutor.INSTANCE);
-        promise.cancel(false);
+        assertTrue(promise.cancel(false));
         promise.get(1, TimeUnit.SECONDS);
     }
 
+    @Test
+    public void testCancellationExceptionIsReturnedAsCause() throws InterruptedException,
+    ExecutionException, TimeoutException {
+        final Promise<Void> promise = new DefaultPromise<Void>(ImmediateEventExecutor.INSTANCE);
+        assertTrue(promise.cancel(false));
+        assertThat(promise.cause(), instanceOf(CancellationException.class));
+    }
+
     @Test
     public void testStackOverflowWithImmediateEventExecutorA() throws Exception {
         testStackOverFlowChainedFuturesA(stackOverflowTestDepth(), ImmediateEventExecutor.INSTANCE, true);
diff --git a/common/src/test/java/io/netty/util/concurrent/FastThreadLocalTest.java b/common/src/test/java/io/netty/util/concurrent/FastThreadLocalTest.java
index 6457de2..c097a34 100644
--- a/common/src/test/java/io/netty/util/concurrent/FastThreadLocalTest.java
+++ b/common/src/test/java/io/netty/util/concurrent/FastThreadLocalTest.java
@@ -27,7 +27,9 @@ import java.util.concurrent.atomic.AtomicReference;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.Matchers.nullValue;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
 
 public class FastThreadLocalTest {
     @Before
@@ -36,6 +38,23 @@ public class FastThreadLocalTest {
         assertThat(FastThreadLocal.size(), is(0));
     }
 
+    @Test
+    public void testGetIfExists() {
+        FastThreadLocal<Boolean> threadLocal = new FastThreadLocal<Boolean>() {
+            @Override
+            protected Boolean initialValue() {
+                return Boolean.TRUE;
+            }
+        };
+
+        assertNull(threadLocal.getIfExists());
+        assertTrue(threadLocal.get());
+        assertTrue(threadLocal.getIfExists());
+
+        FastThreadLocal.removeAll();
+        assertNull(threadLocal.getIfExists());
+    }
+
     @Test(timeout = 10000)
     public void testRemoveAll() throws Exception {
         final AtomicBoolean removed = new AtomicBoolean();
diff --git a/common/src/test/java/io/netty/util/concurrent/GlobalEventExecutorTest.java b/common/src/test/java/io/netty/util/concurrent/GlobalEventExecutorTest.java
index 35c2a59..fcb2c42 100644
--- a/common/src/test/java/io/netty/util/concurrent/GlobalEventExecutorTest.java
+++ b/common/src/test/java/io/netty/util/concurrent/GlobalEventExecutorTest.java
@@ -46,7 +46,7 @@ public class GlobalEventExecutorTest {
         }
     }
 
-    @Test
+    @Test(timeout = 5000)
     public void testAutomaticStartStop() throws Exception {
         final TestRunnable task = new TestRunnable(500);
         e.execute(task);
@@ -56,10 +56,7 @@ public class GlobalEventExecutorTest {
         assertThat(thread, is(not(nullValue())));
         assertThat(thread.isAlive(), is(true));
 
-        Thread.sleep(1500);
-
-        // Ensure the thread stopped itself after running the task.
-        assertThat(thread.isAlive(), is(false));
+        thread.join();
         assertThat(task.ran.get(), is(true));
 
         // Ensure another new thread starts again.
@@ -68,14 +65,12 @@ public class GlobalEventExecutorTest {
         assertThat(e.thread, not(sameInstance(thread)));
         thread = e.thread;
 
-        Thread.sleep(1500);
+        thread.join();
 
-        // Ensure the thread stopped itself after running the task.
-        assertThat(thread.isAlive(), is(false));
         assertThat(task.ran.get(), is(true));
     }
 
-    @Test
+    @Test(timeout = 5000)
     public void testScheduledTasks() throws Exception {
         TestRunnable task = new TestRunnable(0);
         ScheduledFuture<?> f = e.schedule(task, 1500, TimeUnit.MILLISECONDS);
@@ -87,10 +82,7 @@ public class GlobalEventExecutorTest {
         assertThat(thread, is(not(nullValue())));
         assertThat(thread.isAlive(), is(true));
 
-        Thread.sleep(1500);
-
-        // Now it should be stopped.
-        assertThat(thread.isAlive(), is(false));
+        thread.join();
     }
 
     // ensure that when a task submission causes a new thread to be created, the thread inherits the thread group of the
@@ -116,6 +108,62 @@ public class GlobalEventExecutorTest {
         assertEquals(group, capturedGroup.get());
     }
 
+    @Test(timeout = 5000)
+    public void testTakeTask() throws Exception {
+        //add task
+        TestRunnable beforeTask = new TestRunnable(0);
+        e.execute(beforeTask);
+
+        //add scheduled task
+        TestRunnable scheduledTask = new TestRunnable(0);
+        ScheduledFuture<?> f = e.schedule(scheduledTask , 1500, TimeUnit.MILLISECONDS);
+
+        //add task
+        TestRunnable afterTask = new TestRunnable(0);
+        e.execute(afterTask);
+
+        f.sync();
+
+        assertThat(beforeTask.ran.get(), is(true));
+        assertThat(scheduledTask.ran.get(), is(true));
+        assertThat(afterTask.ran.get(), is(true));
+    }
+
+    @Test(timeout = 5000)
+    public void testTakeTaskAlwaysHasTask() throws Exception {
+        //for https://github.com/netty/netty/issues/1614
+        //add scheduled task
+        TestRunnable t = new TestRunnable(0);
+        ScheduledFuture<?> f = e.schedule(t, 1500, TimeUnit.MILLISECONDS);
+
+        final Runnable doNothing = new Runnable() {
+            @Override
+            public void run() {
+                //NOOP
+            }
+        };
+        final AtomicBoolean stop = new AtomicBoolean(false);
+
+        //ensure always has at least one task in taskQueue
+        //check if scheduled tasks are triggered
+        try {
+            new Thread(new Runnable() {
+                @Override
+                public void run() {
+                    while (!stop.get()) {
+                        e.execute(doNothing);
+                    }
+                }
+            }).start();
+
+            f.sync();
+
+            assertThat(t.ran.get(), is(true));
+        } finally {
+            stop.set(true);
+        }
+    }
+
     private static final class TestRunnable implements Runnable {
         final AtomicBoolean ran = new AtomicBoolean();
         final long delay;
diff --git a/common/src/test/java/io/netty/util/concurrent/ImmediateExecutorTest.java b/common/src/test/java/io/netty/util/concurrent/ImmediateExecutorTest.java
new file mode 100644
index 0000000..c1bd565
--- /dev/null
+++ b/common/src/test/java/io/netty/util/concurrent/ImmediateExecutorTest.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2020 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.util.concurrent;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.concurrent.FutureTask;
+
+import org.junit.Test;
+
+public class ImmediateExecutorTest {
+
+    @Test(expected = NullPointerException.class)
+    public void testExecuteNullRunnable() {
+        ImmediateExecutor.INSTANCE.execute(null);
+    }
+
+    @Test
+    public void testExecuteNonNullRunnable() throws Exception {
+        FutureTask<Void> task = new FutureTask<Void>(new Runnable() {
+            @Override
+            public void run() {
+                // NOOP
+            }
+        }, null);
+        ImmediateExecutor.INSTANCE.execute(task);
+        assertTrue(task.isDone());
+        assertFalse(task.isCancelled());
+        assertNull(task.get());
+    }
+}
diff --git a/common/src/test/java/io/netty/util/concurrent/PromiseCombinerTest.java b/common/src/test/java/io/netty/util/concurrent/PromiseCombinerTest.java
index b46fa41..6194c5a 100644
--- a/common/src/test/java/io/netty/util/concurrent/PromiseCombinerTest.java
+++ b/common/src/test/java/io/netty/util/concurrent/PromiseCombinerTest.java
@@ -15,6 +15,7 @@
  */
 package io.netty.util.concurrent;
 
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.Mock;
@@ -25,6 +26,7 @@ import org.mockito.stubbing.Answer;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -55,7 +57,19 @@ public class PromiseCombinerTest {
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
-        combiner = new PromiseCombiner();
+        combiner = new PromiseCombiner(ImmediateEventExecutor.INSTANCE);
+    }
+
+    @Test
+    public void testNullArgument() {
+        try {
+            combiner.finish(null);
+            Assert.fail();
+        } catch (NullPointerException expected) {
+            // expected
+        }
+        combiner.finish(p1);
+        verify(p1).trySuccess(null);
     }
 
     @Test
@@ -148,6 +162,38 @@ public class PromiseCombinerTest {
         verifyFail(p3, e1);
     }
 
+    @Test
+    public void testEventExecutor() {
+        EventExecutor executor = mock(EventExecutor.class);
+        when(executor.inEventLoop()).thenReturn(false);
+        combiner = new PromiseCombiner(executor);
+
+        Future<?> future = mock(Future.class);
+
+        try {
+            combiner.add(future);
+            Assert.fail();
+        } catch (IllegalStateException expected) {
+            // expected
+        }
+
+        try {
+            combiner.addAll(future);
+            Assert.fail();
+        } catch (IllegalStateException expected) {
+            // expected
+        }
+
+        @SuppressWarnings("unchecked")
+        Promise<Void> promise = (Promise<Void>) mock(Promise.class);
+        try {
+            combiner.finish(promise);
+            Assert.fail();
+        } catch (IllegalStateException expected) {
+            // expected
+        }
+    }
+
     private static void verifyFail(Promise<Void> p, Throwable cause) {
         verify(p).tryFailure(eq(cause));
     }
diff --git a/common/src/test/java/io/netty/util/concurrent/SingleThreadEventExecutorTest.java b/common/src/test/java/io/netty/util/concurrent/SingleThreadEventExecutorTest.java
index 55981b2..16f4127 100644
--- a/common/src/test/java/io/netty/util/concurrent/SingleThreadEventExecutorTest.java
+++ b/common/src/test/java/io/netty/util/concurrent/SingleThreadEventExecutorTest.java
@@ -18,20 +18,29 @@ package io.netty.util.concurrent;
 import org.junit.Assert;
 import org.junit.Test;
 
+import io.netty.util.concurrent.AbstractEventExecutor.LazyRunnable;
+
 import java.util.Collections;
 import java.util.Set;
 import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+
 public class SingleThreadEventExecutorTest {
 
     @Test
-    public void testWrappedExecutureIsShutdown() {
+    public void testWrappedExecutorIsShutdown() {
         ExecutorService executorService = Executors.newSingleThreadExecutor();
 
         SingleThreadEventExecutor executor = new SingleThreadEventExecutor(null, executorService, false) {
@@ -122,7 +131,7 @@ public class SingleThreadEventExecutorTest {
 
     private static void testInvokeInEventLoop(final boolean any, final boolean timeout) {
         final SingleThreadEventExecutor executor = new SingleThreadEventExecutor(null,
-                Executors.defaultThreadFactory(), false) {
+                Executors.defaultThreadFactory(), true) {
             @Override
             protected void run() {
                 while (!confirmShutdown()) {
@@ -170,4 +179,240 @@ public class SingleThreadEventExecutorTest {
             executor.shutdownGracefully(0, 0, TimeUnit.MILLISECONDS);
         }
     }
+
+    static class LatchTask extends CountDownLatch implements Runnable {
+        LatchTask() {
+            super(1);
+        }
+
+        @Override
+        public void run() {
+            countDown();
+        }
+    }
+
+    static class LazyLatchTask extends LatchTask implements LazyRunnable { }
+
+    @Test
+    public void testLazyExecution() throws Exception {
+        final SingleThreadEventExecutor executor = new SingleThreadEventExecutor(null,
+                Executors.defaultThreadFactory(), false) {
+            @Override
+            protected void run() {
+                while (!confirmShutdown()) {
+                    try {
+                        synchronized (this) {
+                            if (!hasTasks()) {
+                                wait();
+                            }
+                        }
+                        runAllTasks();
+                    } catch (Exception e) {
+                        e.printStackTrace();
+                        Assert.fail(e.toString());
+                    }
+                }
+            }
+
+            @Override
+            protected void wakeup(boolean inEventLoop) {
+                if (!inEventLoop) {
+                    synchronized (this) {
+                        notifyAll();
+                    }
+                }
+            }
+        };
+
+        // Ensure event loop is started
+        LatchTask latch0 = new LatchTask();
+        executor.execute(latch0);
+        assertTrue(latch0.await(100, TimeUnit.MILLISECONDS));
+        // Pause to ensure it enters waiting state
+        Thread.sleep(100L);
+
+        // Submit task via lazyExecute
+        LatchTask latch1 = new LatchTask();
+        executor.lazyExecute(latch1);
+        // Sumbit lazy task via regular execute
+        LatchTask latch2 = new LazyLatchTask();
+        executor.execute(latch2);
+
+        // Neither should run yet
+        assertFalse(latch1.await(100, TimeUnit.MILLISECONDS));
+        assertFalse(latch2.await(100, TimeUnit.MILLISECONDS));
+
+        // Submit regular task via regular execute
+        LatchTask latch3 = new LatchTask();
+        executor.execute(latch3);
+
+        // Should flush latch1 and latch2 and then run latch3 immediately
+        assertTrue(latch3.await(100, TimeUnit.MILLISECONDS));
+        assertEquals(0, latch1.getCount());
+        assertEquals(0, latch2.getCount());
+    }
+
+    @Test
+    public void testTaskAddedAfterShutdownNotAbandoned() throws Exception {
+
+        // A queue that doesn't support remove, so tasks once added cannot be rejected anymore
+        LinkedBlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<Runnable>() {
+            @Override
+            public boolean remove(Object o) {
+                throw new UnsupportedOperationException();
+            }
+        };
+
+        final Runnable dummyTask = new Runnable() {
+            @Override
+            public void run() {
+            }
+        };
+
+        final LinkedBlockingQueue<Future<?>> submittedTasks = new LinkedBlockingQueue<Future<?>>();
+        final AtomicInteger attempts = new AtomicInteger();
+        final AtomicInteger rejects = new AtomicInteger();
+
+        ExecutorService executorService = Executors.newSingleThreadExecutor();
+        final SingleThreadEventExecutor executor = new SingleThreadEventExecutor(null, executorService, false,
+                taskQueue, RejectedExecutionHandlers.reject()) {
+            @Override
+            protected void run() {
+                while (!confirmShutdown()) {
+                    Runnable task = takeTask();
+                    if (task != null) {
+                        task.run();
+                    }
+                }
+            }
+
+            @Override
+            protected boolean confirmShutdown() {
+                boolean result = super.confirmShutdown();
+                // After shutdown is confirmed, scheduled one more task and record it
+                if (result) {
+                    attempts.incrementAndGet();
+                    try {
+                        submittedTasks.add(submit(dummyTask));
+                    } catch (RejectedExecutionException e) {
+                        // ignore, tasks are either accepted or rejected
+                        rejects.incrementAndGet();
+                    }
+                }
+                return result;
+            }
+        };
+
+        // Start the loop
+        executor.submit(dummyTask).sync();
+
+        // Shutdown without any quiet period
+        executor.shutdownGracefully(0, 100, TimeUnit.MILLISECONDS).sync();
+
+        // Ensure there are no user-tasks left.
+        assertEquals(0, executor.drainTasks());
+
+        // Verify that queue is empty and all attempts either succeeded or were rejected
+        assertTrue(taskQueue.isEmpty());
+        assertTrue(attempts.get() > 0);
+        assertEquals(attempts.get(), submittedTasks.size() + rejects.get());
+        for (Future<?> f : submittedTasks) {
+            assertTrue(f.isSuccess());
+        }
+    }
+
+    @Test(timeout = 5000)
+    public void testTakeTask() throws Exception {
+        final SingleThreadEventExecutor executor =
+                new SingleThreadEventExecutor(null, Executors.defaultThreadFactory(), true) {
+            @Override
+            protected void run() {
+                while (!confirmShutdown()) {
+                    Runnable task = takeTask();
+                    if (task != null) {
+                        task.run();
+                    }
+                }
+            }
+        };
+
+        //add task
+        TestRunnable beforeTask = new TestRunnable();
+        executor.execute(beforeTask);
+
+        //add scheduled task
+        TestRunnable scheduledTask = new TestRunnable();
+        ScheduledFuture<?> f = executor.schedule(scheduledTask , 1500, TimeUnit.MILLISECONDS);
+
+        //add task
+        TestRunnable afterTask = new TestRunnable();
+        executor.execute(afterTask);
+
+        f.sync();
+
+        assertThat(beforeTask.ran.get(), is(true));
+        assertThat(scheduledTask.ran.get(), is(true));
+        assertThat(afterTask.ran.get(), is(true));
+    }
+
+    @Test(timeout = 5000)
+    public void testTakeTaskAlwaysHasTask() throws Exception {
+        //for https://github.com/netty/netty/issues/1614
+
+        final SingleThreadEventExecutor executor =
+                new SingleThreadEventExecutor(null, Executors.defaultThreadFactory(), true) {
+            @Override
+            protected void run() {
+                while (!confirmShutdown()) {
+                    Runnable task = takeTask();
+                    if (task != null) {
+                        task.run();
+                    }
+                }
+            }
+        };
+
+        //add scheduled task
+        TestRunnable t = new TestRunnable();
+        ScheduledFuture<?> f = executor.schedule(t, 1500, TimeUnit.MILLISECONDS);
+
+        final Runnable doNothing = new Runnable() {
+            @Override
+            public void run() {
+                //NOOP
+            }
+        };
+        final AtomicBoolean stop = new AtomicBoolean(false);
+
+        //ensure always has at least one task in taskQueue
+        //check if scheduled tasks are triggered
+        try {
+            new Thread(new Runnable() {
+                @Override
+                public void run() {
+                    while (!stop.get()) {
+                        executor.execute(doNothing);
+                    }
+                }
+            }).start();
+
+            f.sync();
+
+            assertThat(t.ran.get(), is(true));
+        } finally {
+            stop.set(true);
+        }
+    }
+
+    private static final class TestRunnable implements Runnable {
+        final AtomicBoolean ran = new AtomicBoolean();
+
+        TestRunnable() {
+        }
+
+        @Override
+        public void run() {
+            ran.set(true);
+        }
+    }
 }
diff --git a/common/src/test/java/io/netty/util/internal/AppendableCharSequenceTest.java b/common/src/test/java/io/netty/util/internal/AppendableCharSequenceTest.java
index 2d7bab4..9d08c3e 100644
--- a/common/src/test/java/io/netty/util/internal/AppendableCharSequenceTest.java
+++ b/common/src/test/java/io/netty/util/internal/AppendableCharSequenceTest.java
@@ -64,6 +64,16 @@ public class AppendableCharSequenceTest {
         assertEquals("abcdefghij", master.subSequence(0, 10).toString());
     }
 
+    @Test
+    public void testEmptySubSequence() {
+        AppendableCharSequence master = new AppendableCharSequence(26);
+        master.append("abcdefghijlkmonpqrstuvwxyz");
+        AppendableCharSequence sub =  master.subSequence(0, 0);
+        assertEquals(0, sub.length());
+        sub.append('b');
+        assertEquals('b', sub.charAt(0));
+    }
+
     private static void testSimpleAppend0(AppendableCharSequence seq) {
         String text = "testdata";
         for (int i = 0; i < text.length(); i++) {
diff --git a/common/src/test/java/io/netty/util/internal/DefaultPriorityQueueTest.java b/common/src/test/java/io/netty/util/internal/DefaultPriorityQueueTest.java
index b4adc8e..a4e6900 100644
--- a/common/src/test/java/io/netty/util/internal/DefaultPriorityQueueTest.java
+++ b/common/src/test/java/io/netty/util/internal/DefaultPriorityQueueTest.java
@@ -15,9 +15,7 @@
  */
 package io.netty.util.internal;
 
-import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 
 import java.io.Serializable;
 import java.util.ArrayList;
diff --git a/common/src/test/java/io/netty/util/internal/MathUtilTest.java b/common/src/test/java/io/netty/util/internal/MathUtilTest.java
new file mode 100644
index 0000000..16616b8
--- /dev/null
+++ b/common/src/test/java/io/netty/util/internal/MathUtilTest.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2020 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.util.internal;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static io.netty.util.internal.MathUtil.*;
+
+import org.junit.Test;
+
+public class MathUtilTest {
+
+    @Test
+    public void testFindNextPositivePowerOfTwo() {
+        assertEquals(1, findNextPositivePowerOfTwo(0));
+        assertEquals(1, findNextPositivePowerOfTwo(1));
+        assertEquals(1024, findNextPositivePowerOfTwo(1000));
+        assertEquals(1024, findNextPositivePowerOfTwo(1023));
+        assertEquals(2048, findNextPositivePowerOfTwo(2048));
+        assertEquals(1 << 30, findNextPositivePowerOfTwo((1 << 30) - 1));
+        assertEquals(1, findNextPositivePowerOfTwo(-1));
+        assertEquals(1, findNextPositivePowerOfTwo(-10000));
+    }
+
+    @Test
+    public void testSafeFindNextPositivePowerOfTwo() {
+        assertEquals(1, safeFindNextPositivePowerOfTwo(0));
+        assertEquals(1, safeFindNextPositivePowerOfTwo(1));
+        assertEquals(1024, safeFindNextPositivePowerOfTwo(1000));
+        assertEquals(1024, safeFindNextPositivePowerOfTwo(1023));
+        assertEquals(2048, safeFindNextPositivePowerOfTwo(2048));
+        assertEquals(1 << 30, safeFindNextPositivePowerOfTwo((1 << 30) - 1));
+        assertEquals(1, safeFindNextPositivePowerOfTwo(-1));
+        assertEquals(1, safeFindNextPositivePowerOfTwo(-10000));
+        assertEquals(1 << 30, safeFindNextPositivePowerOfTwo(Integer.MAX_VALUE));
+        assertEquals(1 << 30, safeFindNextPositivePowerOfTwo((1 << 30) + 1));
+        assertEquals(1, safeFindNextPositivePowerOfTwo(Integer.MIN_VALUE));
+        assertEquals(1, safeFindNextPositivePowerOfTwo(Integer.MIN_VALUE + 1));
+    }
+
+    @Test
+    public void testIsOutOfBounds() {
+        assertFalse(isOutOfBounds(0, 0, 0));
+        assertFalse(isOutOfBounds(0, 0, 1));
+        assertFalse(isOutOfBounds(0, 1, 1));
+        assertTrue(isOutOfBounds(1, 1, 1));
+        assertTrue(isOutOfBounds(Integer.MAX_VALUE, 1, 1));
+        assertTrue(isOutOfBounds(Integer.MAX_VALUE, Integer.MAX_VALUE, 1));
+        assertTrue(isOutOfBounds(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE));
+        assertFalse(isOutOfBounds(0, Integer.MAX_VALUE, Integer.MAX_VALUE));
+        assertFalse(isOutOfBounds(0, Integer.MAX_VALUE - 1, Integer.MAX_VALUE));
+        assertTrue(isOutOfBounds(0, Integer.MAX_VALUE, Integer.MAX_VALUE - 1));
+        assertFalse(isOutOfBounds(Integer.MAX_VALUE - 1, 1, Integer.MAX_VALUE));
+        assertTrue(isOutOfBounds(Integer.MAX_VALUE - 1, 1, Integer.MAX_VALUE - 1));
+        assertTrue(isOutOfBounds(Integer.MAX_VALUE - 1, 2, Integer.MAX_VALUE));
+        assertTrue(isOutOfBounds(1, Integer.MAX_VALUE, Integer.MAX_VALUE));
+    }
+
+    @Test
+    public void testCompare() {
+        assertEquals(-1, compare(0, 1));
+        assertEquals(-1, compare(0L, 1L));
+        assertEquals(-1, compare(0, Integer.MAX_VALUE));
+        assertEquals(-1, compare(0L, Long.MAX_VALUE));
+        assertEquals(0, compare(0, 0));
+        assertEquals(0, compare(0L, 0L));
+        assertEquals(0, compare(Integer.MIN_VALUE, Integer.MIN_VALUE));
+        assertEquals(0, compare(Long.MIN_VALUE, Long.MIN_VALUE));
+        assertEquals(1, compare(Integer.MAX_VALUE, 0));
+        assertEquals(1, compare(Integer.MAX_VALUE, Integer.MAX_VALUE - 1));
+        assertEquals(1, compare(Long.MAX_VALUE, 0L));
+        assertEquals(1, compare(Long.MAX_VALUE, Long.MAX_VALUE - 1));
+    }
+}
diff --git a/common/src/test/java/io/netty/util/internal/NativeLibraryLoaderTest.java b/common/src/test/java/io/netty/util/internal/NativeLibraryLoaderTest.java
index de73b9e..e591b6c 100644
--- a/common/src/test/java/io/netty/util/internal/NativeLibraryLoaderTest.java
+++ b/common/src/test/java/io/netty/util/internal/NativeLibraryLoaderTest.java
@@ -15,11 +15,19 @@
  */
 package io.netty.util.internal;
 
+import io.netty.util.CharsetUtil;
 import org.junit.Test;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
 import java.util.UUID;
 
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
@@ -65,4 +73,70 @@ public class NativeLibraryLoaderTest {
             throw new RuntimeException(e);
         }
     }
+
+    @Test
+    public void testPatchingId() throws IOException {
+        testPatchingId0(true, false);
+    }
+
+    @Test
+    public void testPatchingIdWithOsArch() throws IOException {
+        testPatchingId0(true, true);
+    }
+
+    @Test
+    public void testPatchingIdNotMatch() throws IOException {
+        testPatchingId0(false, false);
+    }
+
+    @Test
+    public void testPatchingIdWithOsArchNotMatch() throws IOException {
+        testPatchingId0(false, true);
+    }
+
+    private static void testPatchingId0(boolean match, boolean withOsArch) throws IOException {
+        byte[] bytes = new byte[1024];
+        PlatformDependent.threadLocalRandom().nextBytes(bytes);
+        byte[] idBytes = ("/workspace/netty-tcnative/boringssl-static/target/" +
+                "native-build/target/lib/libnetty_tcnative-2.0.20.Final.jnilib").getBytes(CharsetUtil.UTF_8);
+
+        String originalName;
+        if (match) {
+            originalName = "netty-tcnative";
+        } else {
+            originalName = "nonexist_tcnative";
+        }
+        String name = "shaded_" + originalName;
+        if (withOsArch) {
+            name += "_osx_x86_64";
+        }
+
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        out.write(bytes, 0, bytes.length);
+        out.write(idBytes, 0, idBytes.length);
+        out.write(bytes, 0 , bytes.length);
+
+        out.flush();
+        byte[] inBytes = out.toByteArray();
+        out.close();
+
+        InputStream inputStream = new ByteArrayInputStream(inBytes);
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        try {
+            assertEquals(match,
+                    NativeLibraryLoader.patchShadedLibraryId(inputStream, outputStream, originalName, name));
+
+            outputStream.flush();
+            byte[] outputBytes = outputStream.toByteArray();
+            assertArrayEquals(bytes, Arrays.copyOfRange(outputBytes, 0, bytes.length));
+            byte[] patchedId = Arrays.copyOfRange(outputBytes, bytes.length, bytes.length + idBytes.length);
+            assertEquals(!match, Arrays.equals(idBytes, patchedId));
+            assertArrayEquals(bytes,
+                    Arrays.copyOfRange(outputBytes, bytes.length + idBytes.length, outputBytes.length));
+            assertEquals(inBytes.length, outputBytes.length);
+        } finally {
+            inputStream.close();
+            outputStream.close();
+        }
+    }
 }
diff --git a/common/src/test/java/io/netty/util/internal/StringUtilTest.java b/common/src/test/java/io/netty/util/internal/StringUtilTest.java
index 13fe499..c9f6b28 100644
--- a/common/src/test/java/io/netty/util/internal/StringUtilTest.java
+++ b/common/src/test/java/io/netty/util/internal/StringUtilTest.java
@@ -18,9 +18,13 @@ package io.netty.util.internal;
 import org.junit.Test;
 
 import java.util.Arrays;
+import java.util.Collections;
 
 import static io.netty.util.internal.StringUtil.NEWLINE;
 import static io.netty.util.internal.StringUtil.commonSuffixOfLength;
+import static io.netty.util.internal.StringUtil.indexOfWhiteSpace;
+import static io.netty.util.internal.StringUtil.indexOfNonWhiteSpace;
+import static io.netty.util.internal.StringUtil.isNullOrEmpty;
 import static io.netty.util.internal.StringUtil.simpleClassName;
 import static io.netty.util.internal.StringUtil.substringAfter;
 import static io.netty.util.internal.StringUtil.toHexString;
@@ -446,15 +450,15 @@ public class StringUtilTest {
 
     @Test
     public void testUnescapeCsvFields() {
-        assertEquals(Arrays.asList(""), unescapeCsvFields(""));
+        assertEquals(Collections.singletonList(""), unescapeCsvFields(""));
         assertEquals(Arrays.asList("", ""), unescapeCsvFields(","));
         assertEquals(Arrays.asList("a", ""), unescapeCsvFields("a,"));
         assertEquals(Arrays.asList("", "a"), unescapeCsvFields(",a"));
-        assertEquals(Arrays.asList("\""), unescapeCsvFields("\"\"\"\""));
+        assertEquals(Collections.singletonList("\""), unescapeCsvFields("\"\"\"\""));
         assertEquals(Arrays.asList("\"", "\""), unescapeCsvFields("\"\"\"\",\"\"\"\""));
-        assertEquals(Arrays.asList("netty"), unescapeCsvFields("netty"));
+        assertEquals(Collections.singletonList("netty"), unescapeCsvFields("netty"));
         assertEquals(Arrays.asList("hello", "netty"), unescapeCsvFields("hello,netty"));
-        assertEquals(Arrays.asList("hello,netty"), unescapeCsvFields("\"hello,netty\""));
+        assertEquals(Collections.singletonList("hello,netty"), unescapeCsvFields("\"hello,netty\""));
         assertEquals(Arrays.asList("hello", "netty"), unescapeCsvFields("\"hello\",\"netty\""));
         assertEquals(Arrays.asList("a\"b", "c\"d"), unescapeCsvFields("\"a\"\"b\",\"c\"\"d\""));
         assertEquals(Arrays.asList("a\rb", "c\nd"), unescapeCsvFields("\"a\rb\",\"c\nd\""));
@@ -533,4 +537,58 @@ public class StringUtilTest {
         assertEquals("", StringUtil.trimOws("\t ").toString());
         assertEquals("a b", StringUtil.trimOws("\ta b \t").toString());
     }
+
+    @Test
+    public void testJoin() {
+        assertEquals("",
+                     StringUtil.join(",", Collections.<CharSequence>emptyList()).toString());
+        assertEquals("a",
+                     StringUtil.join(",", Collections.singletonList("a")).toString());
+        assertEquals("a,b",
+                     StringUtil.join(",", Arrays.asList("a", "b")).toString());
+        assertEquals("a,b,c",
+                     StringUtil.join(",", Arrays.asList("a", "b", "c")).toString());
+        assertEquals("a,b,c,null,d",
+                     StringUtil.join(",", Arrays.asList("a", "b", "c", null, "d")).toString());
+    }
+
+    @Test
+    public void testIsNullOrEmpty() {
+        assertTrue(isNullOrEmpty(null));
+        assertTrue(isNullOrEmpty(""));
+        assertTrue(isNullOrEmpty(StringUtil.EMPTY_STRING));
+        assertFalse(isNullOrEmpty(" "));
+        assertFalse(isNullOrEmpty("\t"));
+        assertFalse(isNullOrEmpty("\n"));
+        assertFalse(isNullOrEmpty("foo"));
+        assertFalse(isNullOrEmpty(NEWLINE));
+    }
+
+    @Test
+    public void testIndexOfWhiteSpace() {
+        assertEquals(-1, indexOfWhiteSpace("", 0));
+        assertEquals(0, indexOfWhiteSpace(" ", 0));
+        assertEquals(-1, indexOfWhiteSpace(" ", 1));
+        assertEquals(0, indexOfWhiteSpace("\n", 0));
+        assertEquals(-1, indexOfWhiteSpace("\n", 1));
+        assertEquals(0, indexOfWhiteSpace("\t", 0));
+        assertEquals(-1, indexOfWhiteSpace("\t", 1));
+        assertEquals(3, indexOfWhiteSpace("foo\r\nbar", 1));
+        assertEquals(-1, indexOfWhiteSpace("foo\r\nbar", 10));
+        assertEquals(7, indexOfWhiteSpace("foo\tbar\r\n", 6));
+        assertEquals(-1, indexOfWhiteSpace("foo\tbar\r\n", Integer.MAX_VALUE));
+    }
+
+    @Test
+    public void testIndexOfNonWhiteSpace() {
+        assertEquals(-1, indexOfNonWhiteSpace("", 0));
+        assertEquals(-1, indexOfNonWhiteSpace(" ", 0));
+        assertEquals(-1, indexOfNonWhiteSpace(" \t", 0));
+        assertEquals(-1, indexOfNonWhiteSpace(" \t\r\n", 0));
+        assertEquals(2, indexOfNonWhiteSpace(" \tfoo\r\n", 0));
+        assertEquals(2, indexOfNonWhiteSpace(" \tfoo\r\n", 1));
+        assertEquals(4, indexOfNonWhiteSpace(" \tfoo\r\n", 4));
+        assertEquals(-1, indexOfNonWhiteSpace(" \tfoo\r\n", 10));
+        assertEquals(-1, indexOfNonWhiteSpace(" \tfoo\r\n", Integer.MAX_VALUE));
+    }
 }
diff --git a/common/src/test/java/io/netty/util/internal/ThreadExecutorMapTest.java b/common/src/test/java/io/netty/util/internal/ThreadExecutorMapTest.java
new file mode 100644
index 0000000..22069e4
--- /dev/null
+++ b/common/src/test/java/io/netty/util/internal/ThreadExecutorMapTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2019 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.util.internal;
+
+import io.netty.util.concurrent.ImmediateEventExecutor;
+import io.netty.util.concurrent.ImmediateExecutor;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+
+public class ThreadExecutorMapTest {
+
+    @Test
+    public void testDecorateExecutor() {
+        Executor executor = ThreadExecutorMap.apply(ImmediateExecutor.INSTANCE, ImmediateEventExecutor.INSTANCE);
+        executor.execute(new Runnable() {
+            @Override
+            public void run() {
+                Assert.assertSame(ImmediateEventExecutor.INSTANCE, ThreadExecutorMap.currentExecutor());
+            }
+        });
+    }
+
+    @Test
+    public void testDecorateRunnable() {
+        ThreadExecutorMap.apply(new Runnable() {
+            @Override
+            public void run() {
+                Assert.assertSame(ImmediateEventExecutor.INSTANCE, ThreadExecutorMap.currentExecutor());
+            }
+        }, ImmediateEventExecutor.INSTANCE).run();
+    }
+
+    @Test
+    public void testDecorateThreadFactory() throws InterruptedException {
+        ThreadFactory threadFactory =
+                ThreadExecutorMap.apply(Executors.defaultThreadFactory(), ImmediateEventExecutor.INSTANCE);
+        Thread thread = threadFactory.newThread(new Runnable() {
+            @Override
+            public void run() {
+                Assert.assertSame(ImmediateEventExecutor.INSTANCE, ThreadExecutorMap.currentExecutor());
+            }
+        });
+        thread.start();
+        thread.join();
+    }
+}
diff --git a/common/src/test/java/io/netty/util/internal/logging/Log4J2LoggerTest.java b/common/src/test/java/io/netty/util/internal/logging/Log4J2LoggerTest.java
index a72eb9a..9f86a85 100644
--- a/common/src/test/java/io/netty/util/internal/logging/Log4J2LoggerTest.java
+++ b/common/src/test/java/io/netty/util/internal/logging/Log4J2LoggerTest.java
@@ -50,7 +50,7 @@ public class Log4J2LoggerTest extends AbstractInternalLoggerTest<Logger> {
                 result.put("level", level.name());
                 result.put("t", t);
                 super.logMessage(fqcn, level, marker, message, t);
-            };
+            }
         };
     }
 
diff --git a/common/src/test/java/io/netty/util/internal/logging/Slf4JLoggerFactoryTest.java b/common/src/test/java/io/netty/util/internal/logging/Slf4JLoggerFactoryTest.java
index 8cb2f84..bf0f30b 100644
--- a/common/src/test/java/io/netty/util/internal/logging/Slf4JLoggerFactoryTest.java
+++ b/common/src/test/java/io/netty/util/internal/logging/Slf4JLoggerFactoryTest.java
@@ -27,7 +27,6 @@ import java.util.Iterator;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.*;
 import static org.mockito.Mockito.*;
 
 public class Slf4JLoggerFactoryTest {
@@ -71,46 +70,56 @@ public class Slf4JLoggerFactoryTest {
         InternalLogger internalLogger = Slf4JLoggerFactory.wrapLogger(logger);
         internalLogger.debug("{}", "debug");
         internalLogger.debug("{} {}", "debug1", "debug2");
+        internalLogger.debug("{} {} {}", "debug1", "debug2", "debug3");
 
         internalLogger.error("{}", "error");
         internalLogger.error("{} {}", "error1", "error2");
+        internalLogger.error("{} {} {}", "error1", "error2", "error3");
 
         internalLogger.info("{}", "info");
         internalLogger.info("{} {}", "info1", "info2");
+        internalLogger.info("{} {} {}", "info1", "info2", "info3");
 
         internalLogger.trace("{}", "trace");
         internalLogger.trace("{} {}", "trace1", "trace2");
+        internalLogger.trace("{} {} {}", "trace1", "trace2", "trace3");
 
         internalLogger.warn("{}", "warn");
         internalLogger.warn("{} {}", "warn1", "warn2");
+        internalLogger.warn("{} {} {}", "warn1", "warn2", "warn3");
 
-        verify(logger, times(2)).log(ArgumentMatchers.<Marker>isNull(), eq(LocationAwareSlf4JLogger.FQCN),
+        verify(logger, times(3)).log(ArgumentMatchers.<Marker>isNull(), eq(LocationAwareSlf4JLogger.FQCN),
                 eq(LocationAwareLogger.DEBUG_INT), captor.capture(), any(Object[].class),
                 ArgumentMatchers.<Throwable>isNull());
-        verify(logger, times(2)).log(ArgumentMatchers.<Marker>isNull(), eq(LocationAwareSlf4JLogger.FQCN),
+        verify(logger, times(3)).log(ArgumentMatchers.<Marker>isNull(), eq(LocationAwareSlf4JLogger.FQCN),
                 eq(LocationAwareLogger.ERROR_INT), captor.capture(), any(Object[].class),
                 ArgumentMatchers.<Throwable>isNull());
-        verify(logger, times(2)).log(ArgumentMatchers.<Marker>isNull(), eq(LocationAwareSlf4JLogger.FQCN),
+        verify(logger, times(3)).log(ArgumentMatchers.<Marker>isNull(), eq(LocationAwareSlf4JLogger.FQCN),
                 eq(LocationAwareLogger.INFO_INT), captor.capture(), any(Object[].class),
                 ArgumentMatchers.<Throwable>isNull());
-        verify(logger, times(2)).log(ArgumentMatchers.<Marker>isNull(), eq(LocationAwareSlf4JLogger.FQCN),
+        verify(logger, times(3)).log(ArgumentMatchers.<Marker>isNull(), eq(LocationAwareSlf4JLogger.FQCN),
                 eq(LocationAwareLogger.TRACE_INT), captor.capture(), any(Object[].class),
                 ArgumentMatchers.<Throwable>isNull());
-        verify(logger, times(2)).log(ArgumentMatchers.<Marker>isNull(), eq(LocationAwareSlf4JLogger.FQCN),
+        verify(logger, times(3)).log(ArgumentMatchers.<Marker>isNull(), eq(LocationAwareSlf4JLogger.FQCN),
                 eq(LocationAwareLogger.WARN_INT), captor.capture(), any(Object[].class),
                 ArgumentMatchers.<Throwable>isNull());
 
         Iterator<String> logMessages = captor.getAllValues().iterator();
         assertEquals("debug", logMessages.next());
         assertEquals("debug1 debug2", logMessages.next());
+        assertEquals("debug1 debug2 debug3", logMessages.next());
         assertEquals("error", logMessages.next());
         assertEquals("error1 error2", logMessages.next());
+        assertEquals("error1 error2 error3", logMessages.next());
         assertEquals("info", logMessages.next());
         assertEquals("info1 info2", logMessages.next());
+        assertEquals("info1 info2 info3", logMessages.next());
         assertEquals("trace", logMessages.next());
         assertEquals("trace1 trace2", logMessages.next());
+        assertEquals("trace1 trace2 trace3", logMessages.next());
         assertEquals("warn", logMessages.next());
         assertEquals("warn1 warn2", logMessages.next());
+        assertEquals("warn1 warn2 warn3", logMessages.next());
         assertFalse(logMessages.hasNext());
     }
 }
diff --git a/common/src/test/templates/io/netty/util/collection/KObjectHashMapTest.template b/common/src/test/templates/io/netty/util/collection/KObjectHashMapTest.template
index 2be12a9..d4214f7 100644
--- a/common/src/test/templates/io/netty/util/collection/KObjectHashMapTest.template
+++ b/common/src/test/templates/io/netty/util/collection/KObjectHashMapTest.template
@@ -613,4 +613,34 @@ public class @K@ObjectHashMapTest {
         }
         assertTrue(map.isEmpty());
     }
+
+    @Test
+    public void valuesIteratorRemove() {
+        Value v1 = new Value("v1");
+        Value v2 = new Value("v2");
+        Value v3 = new Value("v3");
+        map.put((@k@) 1, v1);
+        map.put((@k@) 2, v2);
+        map.put((@k@) 3, v3);
+
+        Iterator<Value> it = map.values().iterator();
+
+        assertSame(v1, it.next());
+        assertSame(v2, it.next());
+        it.remove();
+
+        assertSame(v3, it.next());
+        assertFalse(it.hasNext());
+
+        assertEquals(2, map.size());
+        assertSame(v1, map.get((@k@) 1));
+        assertNull(map.get((@k@) 2));
+        assertSame(v3, map.get((@k@) 3));
+
+        it = map.values().iterator();
+
+        assertSame(v1, it.next());
+        assertSame(v3, it.next());
+        assertFalse(it.hasNext());
+    }
 }
diff --git a/dev-tools/pom.xml b/dev-tools/pom.xml
index 41b5484..94313aa 100644
--- a/dev-tools/pom.xml
+++ b/dev-tools/pom.xml
@@ -25,7 +25,7 @@
 
   <groupId>io.netty</groupId>
   <artifactId>netty-dev-tools</artifactId>
-  <version>4.1.33.Final</version>
+  <version>4.1.48.Final</version>
 
   <name>Netty/Dev-Tools</name>
 
@@ -52,6 +52,6 @@
   </build>
 
   <scm>
-    <tag>netty-4.1.33.Final</tag>
+    <tag>netty-4.1.48.Final</tag>
   </scm>
 </project>
diff --git a/docker/Dockerfile.centos b/docker/Dockerfile.centos
index 4d88820..e6df940 100644
--- a/docker/Dockerfile.centos
+++ b/docker/Dockerfile.centos
@@ -25,3 +25,6 @@ RUN curl -sL https://github.com/shyiko/jabba/raw/master/install.sh | JABBA_COMMA
 
 RUN echo 'export JAVA_HOME="/jdk"' >> ~/.bashrc
 RUN echo 'PATH=/jdk/bin:$PATH' >> ~/.bashrc
+
+# when the JDK is GraalVM install native-image
+RUN if [ -O /jdk/bin/gu ]; then /jdk/bin/gu install native-image; else echo "Not GraalVM, skip installation of native-image" ; fi
diff --git a/docker/README.md b/docker/README.md
index 9970aae..d96242b 100644
--- a/docker/README.md
+++ b/docker/README.md
@@ -10,10 +10,10 @@ cd /path/to/netty/
 docker-compose -f docker/docker-compose.yaml -f docker/docker-compose.centos-6.18.yaml run test
 ```
 
-## centos 7 with java 9
+## centos 7 with java 11
 
 ```
-docker-compose -f docker/docker-compose.yaml -f docker/docker-compose.centos-7.19.yaml run test
+docker-compose -f docker/docker-compose.yaml -f docker/docker-compose.centos-7.111.yaml run test
 ```
 
 etc, etc
diff --git a/docker/docker-compose.centos-6.110.yaml b/docker/docker-compose.centos-6.110.yaml
index 144780d..7a60f46 100644
--- a/docker/docker-compose.centos-6.110.yaml
+++ b/docker/docker-compose.centos-6.110.yaml
@@ -7,7 +7,7 @@ services:
     build:
       args:
         centos_version : "6"
-        java_version : "openjdk@1.10.0-2"
+        java_version : "zulu@1.10.0-2"
 
   test:
     image: netty:centos-6-1.10
diff --git a/docker/docker-compose.centos-6.111.yaml b/docker/docker-compose.centos-6.111.yaml
index 73618c7..d354a68 100644
--- a/docker/docker-compose.centos-6.111.yaml
+++ b/docker/docker-compose.centos-6.111.yaml
@@ -7,7 +7,7 @@ services:
     build:
       args:
         centos_version : "6"
-        java_version : "openjdk@1.11.0-2"
+        java_version : "adopt@1.11.0-6"
 
   test:
     image: netty:centos-6-1.11
diff --git a/docker/docker-compose.centos-6.112.yaml b/docker/docker-compose.centos-6.112.yaml
index 08b5411..aee3e1f 100644
--- a/docker/docker-compose.centos-6.112.yaml
+++ b/docker/docker-compose.centos-6.112.yaml
@@ -7,7 +7,7 @@ services:
     build:
       args:
         centos_version : "6"
-        java_version : "openjdk@1.12.0-27"
+        java_version : "adopt@1.12.0-2"
 
   test:
     image: netty:centos-6-1.12
diff --git a/docker/docker-compose.centos-6.113.yaml b/docker/docker-compose.centos-6.113.yaml
index c826067..82f53eb 100644
--- a/docker/docker-compose.centos-6.113.yaml
+++ b/docker/docker-compose.centos-6.113.yaml
@@ -7,7 +7,7 @@ services:
     build:
       args:
         centos_version : "6"
-        java_version : "openjdk@1.13.0-3"
+        java_version : "adopt@1.13.0-1"
 
   test:
     image: netty:centos-6-1.13
diff --git a/docker/docker-compose.centos-6.18.yaml b/docker/docker-compose.centos-6.18.yaml
index ecde4da..f53b1cb 100644
--- a/docker/docker-compose.centos-6.18.yaml
+++ b/docker/docker-compose.centos-6.18.yaml
@@ -7,7 +7,7 @@ services:
     build:
       args:
         centos_version : "6"
-        java_version : "1.8.202"
+        java_version : "adopt@1.8.0-242"
 
   test:
     image: netty:centos-6-1.8
diff --git a/docker/docker-compose.centos-6.19.yaml b/docker/docker-compose.centos-6.19.yaml
index 93588d6..ee34c53 100644
--- a/docker/docker-compose.centos-6.19.yaml
+++ b/docker/docker-compose.centos-6.19.yaml
@@ -7,7 +7,7 @@ services:
     build:
       args:
         centos_version : "6"
-        java_version : "openjdk@1.9.0-4"
+        java_version : "zulu@1.9.0-7"
 
   test:
     image: netty:centos-6-1.9
diff --git a/docker/docker-compose.centos-6.graalvm1.yaml b/docker/docker-compose.centos-6.graalvm1.yaml
new file mode 100644
index 0000000..8b74216
--- /dev/null
+++ b/docker/docker-compose.centos-6.graalvm1.yaml
@@ -0,0 +1,22 @@
+version: "3"
+
+services:
+
+  runtime-setup:
+    image: netty:centos-6-1.8
+    build:
+      args:
+        centos_version : "6"
+        java_version : "graalvm@19.2.1"
+
+  test:
+    image: netty:centos-6-1.8
+
+  test-leak:
+    image: netty:centos-6-1.8
+
+  test-boringssl-static:
+    image: netty:centos-6-1.8
+
+  shell:
+    image: netty:centos-6-1.8
diff --git a/docker/docker-compose.centos-6.openj9111.yaml b/docker/docker-compose.centos-6.openj9111.yaml
new file mode 100644
index 0000000..6c1f2c5
--- /dev/null
+++ b/docker/docker-compose.centos-6.openj9111.yaml
@@ -0,0 +1,22 @@
+version: "3"
+
+services:
+
+  runtime-setup:
+    image: netty:centos-6-openj9-1.11
+    build:
+      args:
+        centos_version : "6"
+        java_version : "adopt-openj9@1.11.0-6"
+
+  test:
+    image: netty:centos-6-openj9-1.11
+
+  test-leak:
+    image: netty:centos-6-openj9-1.11
+
+  test-boringssl-static:
+    image: netty:centos-6-openj9-1.11
+
+  shell:
+    image: netty:centos-6-openj9-1.11
diff --git a/docker/docker-compose.centos-7.110.yaml b/docker/docker-compose.centos-7.110.yaml
index 7f16520..280ed73 100644
--- a/docker/docker-compose.centos-7.110.yaml
+++ b/docker/docker-compose.centos-7.110.yaml
@@ -7,7 +7,7 @@ services:
     build:
       args:
         centos_version : "7"
-        java_version : "openjdk@1.10.0-2"
+        java_version : "zulu@1.10.0-2"
 
   test:
     image: netty:centos-7-1.10
diff --git a/docker/docker-compose.centos-7.111.yaml b/docker/docker-compose.centos-7.111.yaml
index 75d635d..391f915 100644
--- a/docker/docker-compose.centos-7.111.yaml
+++ b/docker/docker-compose.centos-7.111.yaml
@@ -7,7 +7,7 @@ services:
     build:
       args:
         centos_version : "7"
-        java_version : "openjdk@1.11.0-2"
+        java_version : "adopt@1.11.0-6"
 
   test:
     image: netty:centos-7-1.11
diff --git a/docker/docker-compose.centos-7.112.yaml b/docker/docker-compose.centos-7.112.yaml
index b85a1d6..d628188 100644
--- a/docker/docker-compose.centos-7.112.yaml
+++ b/docker/docker-compose.centos-7.112.yaml
@@ -7,7 +7,7 @@ services:
     build:
       args:
         centos_version : "7"
-        java_version : "openjdk@1.12.0-27"
+        java_version : "adopt@1.12.0-2"
 
   test:
     image: netty:centos-7-1.12
diff --git a/docker/docker-compose.centos-7.113.yaml b/docker/docker-compose.centos-7.113.yaml
index e49a045..2be6042 100644
--- a/docker/docker-compose.centos-7.113.yaml
+++ b/docker/docker-compose.centos-7.113.yaml
@@ -7,7 +7,7 @@ services:
     build:
       args:
         centos_version : "7"
-        java_version : "openjdk@1.13.0-3"
+        java_version : "adopt@1.13.0-1"
 
   test:
     image: netty:centos-7-1.13
diff --git a/docker/docker-compose.centos-7.18.yaml b/docker/docker-compose.centos-7.18.yaml
index 3279201..04d60a4 100644
--- a/docker/docker-compose.centos-7.18.yaml
+++ b/docker/docker-compose.centos-7.18.yaml
@@ -7,7 +7,7 @@ services:
     build:
       args:
         centos_version : "7"
-        java_version : "1.8.202"
+        java_version : "adopt@1.8.0-242"
 
   test:
     image: netty:centos-7-1.8
diff --git a/docker/docker-compose.centos-7.19.yaml b/docker/docker-compose.centos-7.19.yaml
index 5f1af4f..d1b6acf 100644
--- a/docker/docker-compose.centos-7.19.yaml
+++ b/docker/docker-compose.centos-7.19.yaml
@@ -7,7 +7,7 @@ services:
     build:
       args:
         centos_version : "7"
-        java_version : "openjdk@1.9.0-4"
+        java_version : "zulu@1.9.0-7"
 
   test:
     image: netty:centos-7-1.9
diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml
index 3e3bd2c..5d9ec20 100644
--- a/docker/docker-compose.yaml
+++ b/docker/docker-compose.yaml
@@ -12,9 +12,9 @@ services:
     image: netty:default
     depends_on: [runtime-setup]
     volumes:
-      - ~/.ssh:/root/.ssh
-      - ~/.gnupg:/root/.gnupg
-      - ..:/code
+      - ~/.ssh:/root/.ssh:delegated
+      - ~/.gnupg:/root/.gnupg:delegated
+      - ..:/code:delegated
     working_dir: /code
 
   test-leak:
@@ -35,8 +35,8 @@ services:
       - SANOTYPE_USER
       - SANOTYPE_PASSWORD
     volumes:
-      - ~/.ssh:/root/.ssh
-      - ~/.gnupg:/root/.gnupg
-      - ..:/code
-      - ~/.m2:/root/.m2
+      - ~/.ssh:/root/.ssh:delegated
+      - ~/.gnupg:/root/.gnupg:delegated
+      - ..:/code:delegated
+      - ~/.m2:/root/.m2:delegated
     entrypoint: /bin/bash
diff --git a/example/pom.xml b/example/pom.xml
index aac14ac..5adfe2e 100644
--- a/example/pom.xml
+++ b/example/pom.xml
@@ -21,7 +21,7 @@
   <parent>
     <groupId>io.netty</groupId>
     <artifactId>netty-parent</artifactId>
-    <version>4.1.33.Final</version>
+    <version>4.1.48.Final</version>
   </parent>
 
   <artifactId>netty-example</artifactId>
@@ -29,6 +29,9 @@
 
   <name>Netty/Example</name>
 
+  <properties>
+    <skipJapicmp>true</skipJapicmp>
+  </properties>
   <dependencies>
     <dependency>
       <groupId>${project.groupId}</groupId>
@@ -95,6 +98,11 @@
       <artifactId>netty-codec-stomp</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>netty-codec-mqtt</artifactId>
+      <version>${project.version}</version>
+    </dependency>
 
     <dependency>
       <groupId>com.google.protobuf</groupId>
diff --git a/example/src/main/java/io/netty/example/http/cors/OkResponseHandler.java b/example/src/main/java/io/netty/example/http/cors/OkResponseHandler.java
index 4ad71e6..6616965 100644
--- a/example/src/main/java/io/netty/example/http/cors/OkResponseHandler.java
+++ b/example/src/main/java/io/netty/example/http/cors/OkResponseHandler.java
@@ -15,6 +15,7 @@
  */
 package io.netty.example.http.cors;
 
+import io.netty.buffer.Unpooled;
 import io.netty.channel.ChannelFutureListener;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.SimpleChannelInboundHandler;
@@ -30,7 +31,8 @@ import io.netty.handler.codec.http.HttpVersion;
 public class OkResponseHandler extends SimpleChannelInboundHandler<Object> {
     @Override
     public void channelRead0(ChannelHandlerContext ctx, Object msg) {
-        final FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
+        final FullHttpResponse response = new DefaultFullHttpResponse(
+                HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.EMPTY_BUFFER);
         response.headers().set("custom-response-header", "Some value");
         ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
     }
diff --git a/example/src/main/java/io/netty/example/http/file/HttpStaticFileServerHandler.java b/example/src/main/java/io/netty/example/http/file/HttpStaticFileServerHandler.java
index e2a8013..8424a8c 100644
--- a/example/src/main/java/io/netty/example/http/file/HttpStaticFileServerHandler.java
+++ b/example/src/main/java/io/netty/example/http/file/HttpStaticFileServerHandler.java
@@ -110,36 +110,40 @@ public class HttpStaticFileServerHandler extends SimpleChannelInboundHandler<Ful
     public static final String HTTP_DATE_GMT_TIMEZONE = "GMT";
     public static final int HTTP_CACHE_SECONDS = 60;
 
+    private FullHttpRequest request;
+
     @Override
     public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
+        this.request = request;
         if (!request.decoderResult().isSuccess()) {
             sendError(ctx, BAD_REQUEST);
             return;
         }
 
-        if (request.method() != GET) {
-            sendError(ctx, METHOD_NOT_ALLOWED);
+        if (!GET.equals(request.method())) {
+            this.sendError(ctx, METHOD_NOT_ALLOWED);
             return;
         }
 
+        final boolean keepAlive = HttpUtil.isKeepAlive(request);
         final String uri = request.uri();
         final String path = sanitizeUri(uri);
         if (path == null) {
-            sendError(ctx, FORBIDDEN);
+            this.sendError(ctx, FORBIDDEN);
             return;
         }
 
         File file = new File(path);
         if (file.isHidden() || !file.exists()) {
-            sendError(ctx, NOT_FOUND);
+            this.sendError(ctx, NOT_FOUND);
             return;
         }
 
         if (file.isDirectory()) {
             if (uri.endsWith("/")) {
-                sendListing(ctx, file, uri);
+                this.sendListing(ctx, file, uri);
             } else {
-                sendRedirect(ctx, uri + '/');
+                this.sendRedirect(ctx, uri + '/');
             }
             return;
         }
@@ -160,7 +164,7 @@ public class HttpStaticFileServerHandler extends SimpleChannelInboundHandler<Ful
             long ifModifiedSinceDateSeconds = ifModifiedSinceDate.getTime() / 1000;
             long fileLastModifiedSeconds = file.lastModified() / 1000;
             if (ifModifiedSinceDateSeconds == fileLastModifiedSeconds) {
-                sendNotModified(ctx);
+                this.sendNotModified(ctx);
                 return;
             }
         }
@@ -178,7 +182,10 @@ public class HttpStaticFileServerHandler extends SimpleChannelInboundHandler<Ful
         HttpUtil.setContentLength(response, fileLength);
         setContentTypeHeader(response, file);
         setDateAndCacheHeaders(response, file);
-        if (HttpUtil.isKeepAlive(request)) {
+
+        if (!keepAlive) {
+            response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
+        } else if (request.protocolVersion().equals(HTTP_1_0)) {
             response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
         }
 
@@ -218,7 +225,7 @@ public class HttpStaticFileServerHandler extends SimpleChannelInboundHandler<Ful
         });
 
         // Decide whether to close the connection or not.
-        if (!HttpUtil.isKeepAlive(request)) {
+        if (!keepAlive) {
             // Close the connection when the whole content is written out.
             lastContentFuture.addListener(ChannelFutureListener.CLOSE);
         }
@@ -264,10 +271,7 @@ public class HttpStaticFileServerHandler extends SimpleChannelInboundHandler<Ful
 
     private static final Pattern ALLOWED_FILE_NAME = Pattern.compile("[^-\\._]?[^<>&\\\"]*");
 
-    private static void sendListing(ChannelHandlerContext ctx, File dir, String dirPath) {
-        FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK);
-        response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
-
+    private void sendListing(ChannelHandlerContext ctx, File dir, String dirPath) {
         StringBuilder buf = new StringBuilder()
             .append("<!DOCTYPE html>\r\n")
             .append("<html><head><meta charset='utf-8' /><title>")
@@ -282,47 +286,50 @@ public class HttpStaticFileServerHandler extends SimpleChannelInboundHandler<Ful
             .append("<ul>")
             .append("<li><a href=\"../\">..</a></li>\r\n");
 
-        for (File f: dir.listFiles()) {
-            if (f.isHidden() || !f.canRead()) {
-                continue;
-            }
+        File[] files = dir.listFiles();
+        if (files != null) {
+            for (File f: files) {
+                if (f.isHidden() || !f.canRead()) {
+                    continue;
+                }
 
-            String name = f.getName();
-            if (!ALLOWED_FILE_NAME.matcher(name).matches()) {
-                continue;
-            }
+                String name = f.getName();
+                if (!ALLOWED_FILE_NAME.matcher(name).matches()) {
+                    continue;
+                }
 
-            buf.append("<li><a href=\"")
-               .append(name)
-               .append("\">")
-               .append(name)
-               .append("</a></li>\r\n");
+                buf.append("<li><a href=\"")
+                .append(name)
+                .append("\">")
+                .append(name)
+                .append("</a></li>\r\n");
+            }
         }
 
         buf.append("</ul></body></html>\r\n");
-        ByteBuf buffer = Unpooled.copiedBuffer(buf, CharsetUtil.UTF_8);
-        response.content().writeBytes(buffer);
-        buffer.release();
 
-        // Close the connection as soon as the error message is sent.
-        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
+        ByteBuf buffer = ctx.alloc().buffer(buf.length());
+        buffer.writeCharSequence(buf.toString(), CharsetUtil.UTF_8);
+
+        FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK, buffer);
+        response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
+
+        this.sendAndCleanupConnection(ctx, response);
     }
 
-    private static void sendRedirect(ChannelHandlerContext ctx, String newUri) {
-        FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, FOUND);
+    private void sendRedirect(ChannelHandlerContext ctx, String newUri) {
+        FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, FOUND, Unpooled.EMPTY_BUFFER);
         response.headers().set(HttpHeaderNames.LOCATION, newUri);
 
-        // Close the connection as soon as the error message is sent.
-        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
+        this.sendAndCleanupConnection(ctx, response);
     }
 
-    private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
+    private void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
         FullHttpResponse response = new DefaultFullHttpResponse(
                 HTTP_1_1, status, Unpooled.copiedBuffer("Failure: " + status + "\r\n", CharsetUtil.UTF_8));
         response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
 
-        // Close the connection as soon as the error message is sent.
-        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
+        this.sendAndCleanupConnection(ctx, response);
     }
 
     /**
@@ -331,12 +338,35 @@ public class HttpStaticFileServerHandler extends SimpleChannelInboundHandler<Ful
      * @param ctx
      *            Context
      */
-    private static void sendNotModified(ChannelHandlerContext ctx) {
-        FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, NOT_MODIFIED);
+    private void sendNotModified(ChannelHandlerContext ctx) {
+        FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, NOT_MODIFIED, Unpooled.EMPTY_BUFFER);
         setDateHeader(response);
 
-        // Close the connection as soon as the error message is sent.
-        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
+        this.sendAndCleanupConnection(ctx, response);
+    }
+
+    /**
+     * If Keep-Alive is disabled, attaches "Connection: close" header to the response
+     * and closes the connection after the response being sent.
+     */
+    private void sendAndCleanupConnection(ChannelHandlerContext ctx, FullHttpResponse response) {
+        final FullHttpRequest request = this.request;
+        final boolean keepAlive = HttpUtil.isKeepAlive(request);
+        HttpUtil.setContentLength(response, response.content().readableBytes());
+        if (!keepAlive) {
+            // We're going to close the connection as soon as the response is sent,
+            // so we should also make it clear for the client.
+            response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
+        } else if (request.protocolVersion().equals(HTTP_1_0)) {
+            response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
+        }
+
+        ChannelFuture flushPromise = ctx.writeAndFlush(response);
+
+        if (!keepAlive) {
+            // Close the connection as soon as the response is sent.
+            flushPromise.addListener(ChannelFutureListener.CLOSE);
+        }
     }
 
     /**
diff --git a/example/src/main/java/io/netty/example/http/helloworld/HttpHelloWorldServerHandler.java b/example/src/main/java/io/netty/example/http/helloworld/HttpHelloWorldServerHandler.java
index 3faf51a..ebc5be7 100644
--- a/example/src/main/java/io/netty/example/http/helloworld/HttpHelloWorldServerHandler.java
+++ b/example/src/main/java/io/netty/example/http/helloworld/HttpHelloWorldServerHandler.java
@@ -16,27 +16,27 @@
 package io.netty.example.http.helloworld;
 
 import io.netty.buffer.Unpooled;
+import io.netty.channel.ChannelFuture;
 import io.netty.channel.ChannelFutureListener;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.SimpleChannelInboundHandler;
 import io.netty.handler.codec.http.DefaultFullHttpResponse;
 import io.netty.handler.codec.http.FullHttpResponse;
 import io.netty.handler.codec.http.HttpObject;
-import io.netty.handler.codec.http.HttpUtil;
 import io.netty.handler.codec.http.HttpRequest;
-import io.netty.util.AsciiString;
+import io.netty.handler.codec.http.HttpUtil;
 
-import static io.netty.handler.codec.http.HttpResponseStatus.*;
-import static io.netty.handler.codec.http.HttpVersion.*;
+import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;
+import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
+import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;
+import static io.netty.handler.codec.http.HttpHeaderValues.CLOSE;
+import static io.netty.handler.codec.http.HttpHeaderValues.KEEP_ALIVE;
+import static io.netty.handler.codec.http.HttpHeaderValues.TEXT_PLAIN;
+import static io.netty.handler.codec.http.HttpResponseStatus.OK;
 
 public class HttpHelloWorldServerHandler extends SimpleChannelInboundHandler<HttpObject> {
     private static final byte[] CONTENT = { 'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd' };
 
-    private static final AsciiString CONTENT_TYPE = AsciiString.cached("Content-Type");
-    private static final AsciiString CONTENT_LENGTH = AsciiString.cached("Content-Length");
-    private static final AsciiString CONNECTION = AsciiString.cached("Connection");
-    private static final AsciiString KEEP_ALIVE = AsciiString.cached("keep-alive");
-
     @Override
     public void channelReadComplete(ChannelHandlerContext ctx) {
         ctx.flush();
@@ -48,15 +48,25 @@ public class HttpHelloWorldServerHandler extends SimpleChannelInboundHandler<Htt
             HttpRequest req = (HttpRequest) msg;
 
             boolean keepAlive = HttpUtil.isKeepAlive(req);
-            FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK, Unpooled.wrappedBuffer(CONTENT));
-            response.headers().set(CONTENT_TYPE, "text/plain");
-            response.headers().setInt(CONTENT_LENGTH, response.content().readableBytes());
+            FullHttpResponse response = new DefaultFullHttpResponse(req.protocolVersion(), OK,
+                                                                    Unpooled.wrappedBuffer(CONTENT));
+            response.headers()
+                    .set(CONTENT_TYPE, TEXT_PLAIN)
+                    .setInt(CONTENT_LENGTH, response.content().readableBytes());
 
-            if (!keepAlive) {
-                ctx.write(response).addListener(ChannelFutureListener.CLOSE);
+            if (keepAlive) {
+                if (!req.protocolVersion().isKeepAliveDefault()) {
+                    response.headers().set(CONNECTION, KEEP_ALIVE);
+                }
             } else {
-                response.headers().set(CONNECTION, KEEP_ALIVE);
-                ctx.write(response);
+                // Tell the client we're going to close the connection.
+                response.headers().set(CONNECTION, CLOSE);
+            }
+
+            ChannelFuture f = ctx.write(response);
+
+            if (!keepAlive) {
+                f.addListener(ChannelFutureListener.CLOSE);
             }
         }
     }
diff --git a/example/src/main/java/io/netty/example/http/snoop/HttpSnoopClient.java b/example/src/main/java/io/netty/example/http/snoop/HttpSnoopClient.java
index 2805559..7b10812 100644
--- a/example/src/main/java/io/netty/example/http/snoop/HttpSnoopClient.java
+++ b/example/src/main/java/io/netty/example/http/snoop/HttpSnoopClient.java
@@ -16,6 +16,7 @@
 package io.netty.example.http.snoop;
 
 import io.netty.bootstrap.Bootstrap;
+import io.netty.buffer.Unpooled;
 import io.netty.channel.Channel;
 import io.netty.channel.EventLoopGroup;
 import io.netty.channel.nio.NioEventLoopGroup;
@@ -83,7 +84,7 @@ public final class HttpSnoopClient {
 
             // Prepare the HTTP request.
             HttpRequest request = new DefaultFullHttpRequest(
-                    HttpVersion.HTTP_1_1, HttpMethod.GET, uri.getRawPath());
+                    HttpVersion.HTTP_1_1, HttpMethod.GET, uri.getRawPath(), Unpooled.EMPTY_BUFFER);
             request.headers().set(HttpHeaderNames.HOST, host);
             request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
             request.headers().set(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP);
diff --git a/example/src/main/java/io/netty/example/http/snoop/HttpSnoopServerHandler.java b/example/src/main/java/io/netty/example/http/snoop/HttpSnoopServerHandler.java
index fc555d5..df5b64f 100644
--- a/example/src/main/java/io/netty/example/http/snoop/HttpSnoopServerHandler.java
+++ b/example/src/main/java/io/netty/example/http/snoop/HttpSnoopServerHandler.java
@@ -185,7 +185,7 @@ public class HttpSnoopServerHandler extends SimpleChannelInboundHandler<Object>
     }
 
     private static void send100Continue(ChannelHandlerContext ctx) {
-        FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, CONTINUE);
+        FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, CONTINUE, Unpooled.EMPTY_BUFFER);
         ctx.write(response);
     }
 
diff --git a/example/src/main/java/io/netty/example/http/upload/HttpUploadServer.java b/example/src/main/java/io/netty/example/http/upload/HttpUploadServer.java
index a004f75..342468c 100644
--- a/example/src/main/java/io/netty/example/http/upload/HttpUploadServer.java
+++ b/example/src/main/java/io/netty/example/http/upload/HttpUploadServer.java
@@ -27,7 +27,7 @@ import io.netty.handler.ssl.SslContextBuilder;
 import io.netty.handler.ssl.util.SelfSignedCertificate;
 
 /**
- * A HTTP server showing how to use the HTTP multipart package for file uploads and decoding post data.
+ * An HTTP server showing how to use the HTTP multipart package for file uploads and decoding post data.
  */
 public final class HttpUploadServer {
 
diff --git a/example/src/main/java/io/netty/example/http/upload/HttpUploadServerHandler.java b/example/src/main/java/io/netty/example/http/upload/HttpUploadServerHandler.java
index 43f2d13..7c1c378 100644
--- a/example/src/main/java/io/netty/example/http/upload/HttpUploadServerHandler.java
+++ b/example/src/main/java/io/netty/example/http/upload/HttpUploadServerHandler.java
@@ -69,8 +69,6 @@ public class HttpUploadServerHandler extends SimpleChannelInboundHandler<HttpObj
 
     private HttpRequest request;
 
-    private boolean readingChunks;
-
     private HttpData partialContent;
 
     private final StringBuilder responseContent = new StringBuilder();
@@ -144,9 +142,9 @@ public class HttpUploadServerHandler extends SimpleChannelInboundHandler<HttpObj
             }
             responseContent.append("\r\n\r\n");
 
-            // if GET Method: should not try to create a HttpPostRequestDecoder
-            if (request.method().equals(HttpMethod.GET)) {
-                // GET Method: should not try to create a HttpPostRequestDecoder
+            // if GET Method: should not try to create an HttpPostRequestDecoder
+            if (HttpMethod.GET.equals(request.method())) {
+                // GET Method: should not try to create an HttpPostRequestDecoder
                 // So stop here
                 responseContent.append("\r\n\r\nEND OF GET CONTENT\r\n");
                 // Not now: LastHttpContent will be sent writeResponse(ctx.channel());
@@ -157,18 +155,16 @@ public class HttpUploadServerHandler extends SimpleChannelInboundHandler<HttpObj
             } catch (ErrorDataDecoderException e1) {
                 e1.printStackTrace();
                 responseContent.append(e1.getMessage());
-                writeResponse(ctx.channel());
-                ctx.channel().close();
+                writeResponse(ctx.channel(), true);
                 return;
             }
 
-            readingChunks = HttpUtil.isTransferEncodingChunked(request);
+            boolean readingChunks = HttpUtil.isTransferEncodingChunked(request);
             responseContent.append("Is Chunked: " + readingChunks + "\r\n");
             responseContent.append("IsMultipart: " + decoder.isMultipart() + "\r\n");
             if (readingChunks) {
                 // Chunk version
                 responseContent.append("Chunks: ");
-                readingChunks = true;
             }
         }
 
@@ -183,8 +179,7 @@ public class HttpUploadServerHandler extends SimpleChannelInboundHandler<HttpObj
                 } catch (ErrorDataDecoderException e1) {
                     e1.printStackTrace();
                     responseContent.append(e1.getMessage());
-                    writeResponse(ctx.channel());
-                    ctx.channel().close();
+                    writeResponse(ctx.channel(), true);
                     return;
                 }
                 responseContent.append('o');
@@ -194,7 +189,6 @@ public class HttpUploadServerHandler extends SimpleChannelInboundHandler<HttpObj
                 // example of reading only if at the end
                 if (chunk instanceof LastHttpContent) {
                     writeResponse(ctx.channel());
-                    readingChunks = false;
 
                     reset();
                 }
@@ -311,24 +305,27 @@ public class HttpUploadServerHandler extends SimpleChannelInboundHandler<HttpObj
     }
 
     private void writeResponse(Channel channel) {
+        writeResponse(channel, false);
+    }
+
+    private void writeResponse(Channel channel, boolean forceClose) {
         // Convert the response content to a ChannelBuffer.
         ByteBuf buf = copiedBuffer(responseContent.toString(), CharsetUtil.UTF_8);
         responseContent.setLength(0);
 
         // Decide whether to close the connection or not.
-        boolean close = request.headers().contains(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE, true)
-                || request.protocolVersion().equals(HttpVersion.HTTP_1_0)
-                && !request.headers().contains(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE, true);
+        boolean keepAlive = HttpUtil.isKeepAlive(request) && !forceClose;
 
         // Build the response object.
         FullHttpResponse response = new DefaultFullHttpResponse(
                 HttpVersion.HTTP_1_1, HttpResponseStatus.OK, buf);
         response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
+        response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, buf.readableBytes());
 
-        if (!close) {
-            // There's no need to add 'Content-Length' header
-            // if this is the last response.
-            response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, buf.readableBytes());
+        if (!keepAlive) {
+            response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
+        } else if (request.protocolVersion().equals(HttpVersion.HTTP_1_0)) {
+            response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
         }
 
         Set<Cookie> cookies;
@@ -347,7 +344,7 @@ public class HttpUploadServerHandler extends SimpleChannelInboundHandler<HttpObj
         // Write the response.
         ChannelFuture future = channel.writeAndFlush(response);
         // Close the connection after the write operation is done if necessary.
-        if (close) {
+        if (!keepAlive) {
             future.addListener(ChannelFutureListener.CLOSE);
         }
     }
@@ -432,8 +429,20 @@ public class HttpUploadServerHandler extends SimpleChannelInboundHandler<HttpObj
         response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
         response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, buf.readableBytes());
 
+        // Decide whether to close the connection or not.
+        boolean keepAlive = HttpUtil.isKeepAlive(request);
+        if (!keepAlive) {
+            response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
+        } else if (request.protocolVersion().equals(HttpVersion.HTTP_1_0)) {
+            response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
+        }
+
         // Write the response.
-        ctx.channel().writeAndFlush(response);
+        ChannelFuture future = ctx.channel().writeAndFlush(response);
+        // Close the connection after the write operation is done if necessary.
+        if (!keepAlive) {
+            future.addListener(ChannelFutureListener.CLOSE);
+        }
     }
 
     @Override
diff --git a/example/src/main/java/io/netty/example/http/websocketx/benchmarkserver/WebSocketServerHandler.java b/example/src/main/java/io/netty/example/http/websocketx/benchmarkserver/WebSocketServerHandler.java
index 78f61ba..4d6c8f3 100644
--- a/example/src/main/java/io/netty/example/http/websocketx/benchmarkserver/WebSocketServerHandler.java
+++ b/example/src/main/java/io/netty/example/http/websocketx/benchmarkserver/WebSocketServerHandler.java
@@ -16,7 +16,7 @@
 package io.netty.example.http.websocketx.benchmarkserver;
 
 import io.netty.buffer.ByteBuf;
-import io.netty.buffer.Unpooled;
+import io.netty.buffer.ByteBufUtil;
 import io.netty.channel.ChannelFuture;
 import io.netty.channel.ChannelFutureListener;
 import io.netty.channel.ChannelHandlerContext;
@@ -25,6 +25,7 @@ import io.netty.handler.codec.http.DefaultFullHttpResponse;
 import io.netty.handler.codec.http.FullHttpRequest;
 import io.netty.handler.codec.http.FullHttpResponse;
 import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpResponseStatus;
 import io.netty.handler.codec.http.HttpUtil;
 import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
 import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
@@ -34,11 +35,9 @@ import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
 import io.netty.handler.codec.http.websocketx.WebSocketFrame;
 import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
 import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory;
-import io.netty.util.CharsetUtil;
 
 import static io.netty.handler.codec.http.HttpMethod.*;
 import static io.netty.handler.codec.http.HttpResponseStatus.*;
-import static io.netty.handler.codec.http.HttpVersion.*;
 
 /**
  * Handles handshakes and messages
@@ -66,20 +65,22 @@ public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object>
     private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) {
         // Handle a bad request.
         if (!req.decoderResult().isSuccess()) {
-            sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST));
+            sendHttpResponse(ctx, req, new DefaultFullHttpResponse(req.protocolVersion(), BAD_REQUEST,
+                                                                   ctx.alloc().buffer(0)));
             return;
         }
 
         // Allow only GET methods.
-        if (req.method() != GET) {
-            sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN));
+        if (!GET.equals(req.method())) {
+            sendHttpResponse(ctx, req, new DefaultFullHttpResponse(req.protocolVersion(), FORBIDDEN,
+                                                                   ctx.alloc().buffer(0)));
             return;
         }
 
         // Send the demo page and favicon.ico
         if ("/".equals(req.uri())) {
             ByteBuf content = WebSocketServerBenchmarkPage.getContent(getWebSocketLocation(req));
-            FullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, OK, content);
+            FullHttpResponse res = new DefaultFullHttpResponse(req.protocolVersion(), OK, content);
 
             res.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
             HttpUtil.setContentLength(res, content.readableBytes());
@@ -87,8 +88,10 @@ public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object>
             sendHttpResponse(ctx, req, res);
             return;
         }
+
         if ("/favicon.ico".equals(req.uri())) {
-            FullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, NOT_FOUND);
+            FullHttpResponse res = new DefaultFullHttpResponse(req.protocolVersion(), NOT_FOUND,
+                                                               ctx.alloc().buffer(0));
             sendHttpResponse(ctx, req, res);
             return;
         }
@@ -126,20 +129,19 @@ public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object>
         }
     }
 
-    private static void sendHttpResponse(
-            ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) {
+    private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) {
         // Generate an error page if response getStatus code is not OK (200).
-        if (res.status().code() != 200) {
-            ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(), CharsetUtil.UTF_8);
-            res.content().writeBytes(buf);
-            buf.release();
+        HttpResponseStatus responseStatus = res.status();
+        if (responseStatus.code() != 200) {
+            ByteBufUtil.writeUtf8(res.content(), responseStatus.toString());
             HttpUtil.setContentLength(res, res.content().readableBytes());
         }
-
         // Send the response and close the connection if necessary.
-        ChannelFuture f = ctx.channel().writeAndFlush(res);
-        if (!HttpUtil.isKeepAlive(req) || res.status().code() != 200) {
-            f.addListener(ChannelFutureListener.CLOSE);
+        boolean keepAlive = HttpUtil.isKeepAlive(req) && responseStatus.code() == 200;
+        HttpUtil.setKeepAlive(res, keepAlive);
+        ChannelFuture future = ctx.write(res); // Flushed in channelReadComplete()
+        if (!keepAlive) {
+            future.addListener(ChannelFutureListener.CLOSE);
         }
     }
 
diff --git a/example/src/main/java/io/netty/example/http/websocketx/server/WebSocketFrameHandler.java b/example/src/main/java/io/netty/example/http/websocketx/server/WebSocketFrameHandler.java
index a453668..5c0e9e0 100644
--- a/example/src/main/java/io/netty/example/http/websocketx/server/WebSocketFrameHandler.java
+++ b/example/src/main/java/io/netty/example/http/websocketx/server/WebSocketFrameHandler.java
@@ -15,13 +15,13 @@
  */
 package io.netty.example.http.websocketx.server;
 
-import java.util.Locale;
-
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.SimpleChannelInboundHandler;
 import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
 import io.netty.handler.codec.http.websocketx.WebSocketFrame;
 
+import java.util.Locale;
+
 /**
  * Echoes uppercase content of text frames.
  */
diff --git a/example/src/main/java/io/netty/example/http/websocketx/server/WebSocketIndexPageHandler.java b/example/src/main/java/io/netty/example/http/websocketx/server/WebSocketIndexPageHandler.java
index 7d543ce..a020ecf 100644
--- a/example/src/main/java/io/netty/example/http/websocketx/server/WebSocketIndexPageHandler.java
+++ b/example/src/main/java/io/netty/example/http/websocketx/server/WebSocketIndexPageHandler.java
@@ -16,7 +16,7 @@
 package io.netty.example.http.websocketx.server;
 
 import io.netty.buffer.ByteBuf;
-import io.netty.buffer.Unpooled;
+import io.netty.buffer.ByteBufUtil;
 import io.netty.channel.ChannelFuture;
 import io.netty.channel.ChannelFutureListener;
 import io.netty.channel.ChannelHandlerContext;
@@ -27,16 +27,13 @@ import io.netty.handler.codec.http.FullHttpRequest;
 import io.netty.handler.codec.http.FullHttpResponse;
 import io.netty.handler.codec.http.HttpHeaderNames;
 import io.netty.handler.codec.http.HttpRequest;
+import io.netty.handler.codec.http.HttpResponseStatus;
 import io.netty.handler.codec.http.HttpUtil;
 import io.netty.handler.ssl.SslHandler;
-import io.netty.util.CharsetUtil;
 
-import static io.netty.handler.codec.http.HttpMethod.GET;
-import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
-import static io.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN;
-import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
-import static io.netty.handler.codec.http.HttpResponseStatus.OK;
-import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
+import static io.netty.handler.codec.http.HttpHeaderNames.*;
+import static io.netty.handler.codec.http.HttpMethod.*;
+import static io.netty.handler.codec.http.HttpResponseStatus.*;
 
 /**
  * Outputs index page content.
@@ -53,13 +50,15 @@ public class WebSocketIndexPageHandler extends SimpleChannelInboundHandler<FullH
     protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception {
         // Handle a bad request.
         if (!req.decoderResult().isSuccess()) {
-            sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST));
+            sendHttpResponse(ctx, req, new DefaultFullHttpResponse(req.protocolVersion(), BAD_REQUEST,
+                                                                   ctx.alloc().buffer(0)));
             return;
         }
 
         // Allow only GET methods.
-        if (req.method() != GET) {
-            sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN));
+        if (!GET.equals(req.method())) {
+            sendHttpResponse(ctx, req, new DefaultFullHttpResponse(req.protocolVersion(), FORBIDDEN,
+                                                                   ctx.alloc().buffer(0)));
             return;
         }
 
@@ -67,14 +66,15 @@ public class WebSocketIndexPageHandler extends SimpleChannelInboundHandler<FullH
         if ("/".equals(req.uri()) || "/index.html".equals(req.uri())) {
             String webSocketLocation = getWebSocketLocation(ctx.pipeline(), req, websocketPath);
             ByteBuf content = WebSocketServerIndexPage.getContent(webSocketLocation);
-            FullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, OK, content);
+            FullHttpResponse res = new DefaultFullHttpResponse(req.protocolVersion(), OK, content);
 
-            res.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
+            res.headers().set(CONTENT_TYPE, "text/html; charset=UTF-8");
             HttpUtil.setContentLength(res, content.readableBytes());
 
             sendHttpResponse(ctx, req, res);
         } else {
-            sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, NOT_FOUND));
+            sendHttpResponse(ctx, req, new DefaultFullHttpResponse(req.protocolVersion(), NOT_FOUND,
+                                                                   ctx.alloc().buffer(0)));
         }
     }
 
@@ -86,17 +86,17 @@ public class WebSocketIndexPageHandler extends SimpleChannelInboundHandler<FullH
 
     private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) {
         // Generate an error page if response getStatus code is not OK (200).
-        if (res.status().code() != 200) {
-            ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(), CharsetUtil.UTF_8);
-            res.content().writeBytes(buf);
-            buf.release();
+        HttpResponseStatus responseStatus = res.status();
+        if (responseStatus.code() != 200) {
+            ByteBufUtil.writeUtf8(res.content(), responseStatus.toString());
             HttpUtil.setContentLength(res, res.content().readableBytes());
         }
-
         // Send the response and close the connection if necessary.
-        ChannelFuture f = ctx.channel().writeAndFlush(res);
-        if (!HttpUtil.isKeepAlive(req) || res.status().code() != 200) {
-            f.addListener(ChannelFutureListener.CLOSE);
+        boolean keepAlive = HttpUtil.isKeepAlive(req) && responseStatus.code() == 200;
+        HttpUtil.setKeepAlive(res, keepAlive);
+        ChannelFuture future = ctx.writeAndFlush(res);
+        if (!keepAlive) {
+            future.addListener(ChannelFutureListener.CLOSE);
         }
     }
 
diff --git a/example/src/main/java/io/netty/example/http/websocketx/server/WebSocketServer.java b/example/src/main/java/io/netty/example/http/websocketx/server/WebSocketServer.java
index 8e67854..9315aa7 100644
--- a/example/src/main/java/io/netty/example/http/websocketx/server/WebSocketServer.java
+++ b/example/src/main/java/io/netty/example/http/websocketx/server/WebSocketServer.java
@@ -27,7 +27,7 @@ import io.netty.handler.ssl.SslContextBuilder;
 import io.netty.handler.ssl.util.SelfSignedCertificate;
 
 /**
- * A HTTP server which serves Web Socket requests at:
+ * An HTTP server which serves Web Socket requests at:
  *
  * http://localhost:8080/websocket
  *
diff --git a/example/src/main/java/io/netty/example/http2/helloworld/client/Http2Client.java b/example/src/main/java/io/netty/example/http2/helloworld/client/Http2Client.java
index bc477d5..80470d6 100644
--- a/example/src/main/java/io/netty/example/http2/helloworld/client/Http2Client.java
+++ b/example/src/main/java/io/netty/example/http2/helloworld/client/Http2Client.java
@@ -15,6 +15,7 @@
 package io.netty.example.http2.helloworld.client;
 
 import io.netty.bootstrap.Bootstrap;
+import io.netty.buffer.Unpooled;
 import io.netty.channel.Channel;
 import io.netty.channel.ChannelOption;
 import io.netty.channel.EventLoopGroup;
@@ -49,9 +50,13 @@ import static io.netty.handler.codec.http.HttpMethod.POST;
 import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
 
 /**
- * An HTTP2 client that allows you to send HTTP2 frames to a server. Inbound and outbound frames are
- * logged. When run from the command-line, sends a single HEADERS frame to the server and gets back
+ * An HTTP2 client that allows you to send HTTP2 frames to a server using HTTP1-style approaches
+ * (via {@link io.netty.handler.codec.http2.InboundHttp2ToHttpAdapter}). Inbound and outbound
+ * frames are logged.
+ * When run from the command-line, sends a single HEADERS frame to the server and gets back
  * a "Hello World" response.
+ * See the ./http2/helloworld/frame/client/ example for a HTTP2 client example which does not use
+ * HTTP1-style objects and patterns.
  */
 public final class Http2Client {
 
@@ -113,7 +118,7 @@ public final class Http2Client {
             System.err.println("Sending request(s)...");
             if (URL != null) {
                 // Create a simple GET request.
-                FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, GET, URL);
+                FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, GET, URL, Unpooled.EMPTY_BUFFER);
                 request.headers().add(HttpHeaderNames.HOST, hostName);
                 request.headers().add(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), scheme.name());
                 request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP);
diff --git a/example/src/main/java/io/netty/example/http2/helloworld/client/Http2ClientInitializer.java b/example/src/main/java/io/netty/example/http2/helloworld/client/Http2ClientInitializer.java
index 182c156..f4fa9d2 100644
--- a/example/src/main/java/io/netty/example/http2/helloworld/client/Http2ClientInitializer.java
+++ b/example/src/main/java/io/netty/example/http2/helloworld/client/Http2ClientInitializer.java
@@ -14,6 +14,7 @@
  */
 package io.netty.example.http2.helloworld.client;
 
+import io.netty.buffer.Unpooled;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelInboundHandlerAdapter;
 import io.netty.channel.ChannelInitializer;
@@ -22,6 +23,7 @@ import io.netty.channel.socket.SocketChannel;
 import io.netty.handler.codec.http.DefaultFullHttpRequest;
 import io.netty.handler.codec.http.HttpClientCodec;
 import io.netty.handler.codec.http.HttpClientUpgradeHandler;
+import io.netty.handler.codec.http.HttpHeaderNames;
 import io.netty.handler.codec.http.HttpMethod;
 import io.netty.handler.codec.http.HttpVersion;
 import io.netty.handler.codec.http2.DefaultHttp2Connection;
@@ -36,6 +38,8 @@ import io.netty.handler.ssl.ApplicationProtocolNames;
 import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler;
 import io.netty.handler.ssl.SslContext;
 
+import java.net.InetSocketAddress;
+
 import static io.netty.handler.logging.LogLevel.INFO;
 
 /**
@@ -94,7 +98,8 @@ public class Http2ClientInitializer extends ChannelInitializer<SocketChannel> {
      */
     private void configureSsl(SocketChannel ch) {
         ChannelPipeline pipeline = ch.pipeline();
-        pipeline.addLast(sslCtx.newHandler(ch.alloc()));
+        // Specify Host in SSLContext New Handler to add TLS SNI Extension
+        pipeline.addLast(sslCtx.newHandler(ch.alloc(), Http2Client.HOST, Http2Client.PORT));
         // We must wait for the handshake to finish and the protocol to be negotiated before configuring
         // the HTTP/2 components of the pipeline.
         pipeline.addLast(new ApplicationProtocolNegotiationHandler("") {
@@ -130,10 +135,20 @@ public class Http2ClientInitializer extends ChannelInitializer<SocketChannel> {
      * A handler that triggers the cleartext upgrade to HTTP/2 by sending an initial HTTP request.
      */
     private final class UpgradeRequestHandler extends ChannelInboundHandlerAdapter {
+
         @Override
         public void channelActive(ChannelHandlerContext ctx) throws Exception {
             DefaultFullHttpRequest upgradeRequest =
-                    new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/");
+                    new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/", Unpooled.EMPTY_BUFFER);
+
+            // Set HOST header as the remote peer may require it.
+            InetSocketAddress remote = (InetSocketAddress) ctx.channel().remoteAddress();
+            String hostString = remote.getHostString();
+            if (hostString == null) {
+                hostString = remote.getAddress().getHostAddress();
+            }
+            upgradeRequest.headers().set(HttpHeaderNames.HOST, hostString + ':' + remote.getPort());
+
             ctx.writeAndFlush(upgradeRequest);
 
             ctx.fireChannelActive();
diff --git a/example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2ClientFrameInitializer.java b/example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2ClientFrameInitializer.java
new file mode 100644
index 0000000..b08111f
--- /dev/null
+++ b/example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2ClientFrameInitializer.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2020 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.example.http2.helloworld.frame.client;
+
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.handler.codec.http2.Http2FrameCodec;
+import io.netty.handler.codec.http2.Http2FrameCodecBuilder;
+import io.netty.handler.codec.http2.Http2MultiplexHandler;
+import io.netty.handler.codec.http2.Http2Settings;
+import io.netty.handler.ssl.SslContext;
+
+/**
+ * Configures client pipeline to support HTTP/2 frames via {@link Http2FrameCodec} and {@link Http2MultiplexHandler}.
+ */
+public final class Http2ClientFrameInitializer extends ChannelInitializer<Channel> {
+
+    private final SslContext sslCtx;
+
+    public Http2ClientFrameInitializer(SslContext sslCtx) {
+        this.sslCtx = sslCtx;
+    }
+
+    @Override
+    protected void initChannel(Channel ch) throws Exception {
+        // ensure that our 'trust all' SSL handler is the first in the pipeline if SSL is enabled.
+        if (sslCtx != null) {
+            ch.pipeline().addFirst(sslCtx.newHandler(ch.alloc()));
+        }
+
+        final Http2FrameCodec http2FrameCodec = Http2FrameCodecBuilder.forClient()
+            .initialSettings(Http2Settings.defaultSettings()) // this is the default, but shows it can be changed.
+            .build();
+        ch.pipeline().addLast(http2FrameCodec);
+        ch.pipeline().addLast(new Http2MultiplexHandler(new SimpleChannelInboundHandler() {
+            @Override
+            protected void channelRead0(ChannelHandlerContext ctx, Object msg) {
+                // NOOP (this is the handler for 'inbound' streams, which is not relevant in this example)
+            }
+        }));
+    }
+
+}
diff --git a/example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2ClientStreamFrameResponseHandler.java b/example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2ClientStreamFrameResponseHandler.java
new file mode 100644
index 0000000..2475723
--- /dev/null
+++ b/example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2ClientStreamFrameResponseHandler.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2020 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.example.http2.helloworld.frame.client;
+
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.handler.codec.http2.Http2DataFrame;
+import io.netty.handler.codec.http2.Http2HeadersFrame;
+import io.netty.handler.codec.http2.Http2StreamFrame;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Handles HTTP/2 stream frame responses. This is a useful approach if you specifically want to check
+ * the main HTTP/2 response DATA/HEADERs, but in this example it's used purely to see whether
+ * our request (for a specific stream id) has had a final response (for that same stream id).
+ */
+public final class Http2ClientStreamFrameResponseHandler extends SimpleChannelInboundHandler<Http2StreamFrame> {
+
+    private final CountDownLatch latch = new CountDownLatch(1);
+
+    @Override
+    protected void channelRead0(ChannelHandlerContext ctx, Http2StreamFrame msg) throws Exception {
+        System.out.println("Received HTTP/2 'stream' frame: " + msg);
+
+        // isEndStream() is not from a common interface, so we currently must check both
+        if (msg instanceof Http2DataFrame && ((Http2DataFrame) msg).isEndStream()) {
+            latch.countDown();
+        } else if (msg instanceof Http2HeadersFrame && ((Http2HeadersFrame) msg).isEndStream()) {
+            latch.countDown();
+        }
+    }
+
+    /**
+     * Waits for the latch to be decremented (i.e. for an end of stream message to be received), or for
+     * the latch to expire after 5 seconds.
+     * @return true if a successful HTTP/2 end of stream message was received.
+     */
+    public boolean responseSuccessfullyCompleted() {
+        try {
+            return latch.await(5, TimeUnit.SECONDS);
+        } catch (InterruptedException ie) {
+            System.err.println("Latch exception: " + ie.getMessage());
+            return false;
+        }
+    }
+
+}
diff --git a/example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2FrameClient.java b/example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2FrameClient.java
new file mode 100644
index 0000000..014d488
--- /dev/null
+++ b/example/src/main/java/io/netty/example/http2/helloworld/frame/client/Http2FrameClient.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2020 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.example.http2.helloworld.frame.client;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.nio.NioSocketChannel;
+import io.netty.handler.codec.http2.DefaultHttp2Headers;
+import io.netty.handler.codec.http2.DefaultHttp2HeadersFrame;
+import io.netty.handler.codec.http2.Http2HeadersFrame;
+import io.netty.handler.codec.http2.Http2SecurityUtil;
+import io.netty.handler.codec.http2.Http2StreamChannel;
+import io.netty.handler.codec.http2.Http2StreamChannelBootstrap;
+import io.netty.handler.ssl.ApplicationProtocolConfig;
+import io.netty.handler.ssl.ApplicationProtocolConfig.Protocol;
+import io.netty.handler.ssl.ApplicationProtocolConfig.SelectedListenerFailureBehavior;
+import io.netty.handler.ssl.ApplicationProtocolConfig.SelectorFailureBehavior;
+import io.netty.handler.ssl.ApplicationProtocolNames;
+import io.netty.handler.ssl.SslContext;
+import io.netty.handler.ssl.SslContextBuilder;
+import io.netty.handler.ssl.SslProvider;
+import io.netty.handler.ssl.SupportedCipherSuiteFilter;
+import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
+
+/**
+ * An HTTP2 client that allows you to send HTTP2 frames to a server using the newer HTTP2
+ * approach (via {@link io.netty.handler.codec.http2.Http2FrameCodec}).
+ * When run from the command-line, sends a single HEADERS frame (with prior knowledge) to
+ * the server configured at host:port/path.
+ * You should include {@link io.netty.handler.codec.http2.Http2ClientUpgradeCodec} if the
+ * HTTP/2 server you are hitting doesn't support h2c/prior knowledge.
+ */
+public final class Http2FrameClient {
+
+    static final boolean SSL = System.getProperty("ssl") != null;
+    static final String HOST = System.getProperty("host", "127.0.0.1");
+    static final int PORT = Integer.parseInt(System.getProperty("port", SSL? "8443" : "8080"));
+    static final String PATH = System.getProperty("path", "/");
+
+    private Http2FrameClient() {
+    }
+
+    public static void main(String[] args) throws Exception {
+        final EventLoopGroup clientWorkerGroup = new NioEventLoopGroup();
+
+        // Configure SSL.
+        final SslContext sslCtx;
+        if (SSL) {
+            final SslProvider provider =
+                    SslProvider.isAlpnSupported(SslProvider.OPENSSL)? SslProvider.OPENSSL : SslProvider.JDK;
+            sslCtx = SslContextBuilder.forClient()
+                  .sslProvider(provider)
+                  .ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE)
+                  // you probably won't want to use this in production, but it is fine for this example:
+                  .trustManager(InsecureTrustManagerFactory.INSTANCE)
+                  .applicationProtocolConfig(new ApplicationProtocolConfig(
+                          Protocol.ALPN,
+                          SelectorFailureBehavior.NO_ADVERTISE,
+                          SelectedListenerFailureBehavior.ACCEPT,
+                          ApplicationProtocolNames.HTTP_2,
+                          ApplicationProtocolNames.HTTP_1_1))
+                  .build();
+        } else {
+            sslCtx = null;
+        }
+
+        try {
+            final Bootstrap b = new Bootstrap();
+            b.group(clientWorkerGroup);
+            b.channel(NioSocketChannel.class);
+            b.option(ChannelOption.SO_KEEPALIVE, true);
+            b.remoteAddress(HOST, PORT);
+            b.handler(new Http2ClientFrameInitializer(sslCtx));
+
+            // Start the client.
+            final Channel channel = b.connect().syncUninterruptibly().channel();
+            System.out.println("Connected to [" + HOST + ':' + PORT + ']');
+
+            final Http2ClientStreamFrameResponseHandler streamFrameResponseHandler =
+                    new Http2ClientStreamFrameResponseHandler();
+
+            final Http2StreamChannelBootstrap streamChannelBootstrap = new Http2StreamChannelBootstrap(channel);
+            final Http2StreamChannel streamChannel = streamChannelBootstrap.open().syncUninterruptibly().getNow();
+            streamChannel.pipeline().addLast(streamFrameResponseHandler);
+
+            // Send request (a HTTP/2 HEADERS frame - with ':method = GET' in this case)
+            final DefaultHttp2Headers headers = new DefaultHttp2Headers();
+            headers.method("GET");
+            headers.path(PATH);
+            headers.scheme(SSL? "https" : "http");
+            final Http2HeadersFrame headersFrame = new DefaultHttp2HeadersFrame(headers);
+            streamChannel.writeAndFlush(headersFrame);
+            System.out.println("Sent HTTP/2 GET request to " + PATH);
+
+            // Wait for the responses (or for the latch to expire), then clean up the connections
+            if (!streamFrameResponseHandler.responseSuccessfullyCompleted()) {
+                System.err.println("Did not get HTTP/2 response in expected time.");
+            }
+
+            System.out.println("Finished HTTP/2 request, will close the connection.");
+
+            // Wait until the connection is closed.
+            channel.close().syncUninterruptibly();
+        } finally {
+            clientWorkerGroup.shutdownGracefully();
+        }
+    }
+
+}
diff --git a/example/src/main/java/io/netty/example/http2/helloworld/frame/server/Http2OrHttpHandler.java b/example/src/main/java/io/netty/example/http2/helloworld/frame/server/Http2OrHttpHandler.java
index 01ae593..0ec1da9 100644
--- a/example/src/main/java/io/netty/example/http2/helloworld/frame/server/Http2OrHttpHandler.java
+++ b/example/src/main/java/io/netty/example/http2/helloworld/frame/server/Http2OrHttpHandler.java
@@ -18,7 +18,6 @@ import io.netty.channel.ChannelHandlerContext;
 import io.netty.example.http2.helloworld.server.HelloWorldHttp1Handler;
 import io.netty.handler.codec.http.HttpObjectAggregator;
 import io.netty.handler.codec.http.HttpServerCodec;
-import io.netty.handler.codec.http2.Http2FrameCodec;
 import io.netty.handler.codec.http2.Http2FrameCodecBuilder;
 import io.netty.handler.ssl.ApplicationProtocolNames;
 import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler;
diff --git a/example/src/main/java/io/netty/example/http2/helloworld/frame/server/Http2Server.java b/example/src/main/java/io/netty/example/http2/helloworld/frame/server/Http2Server.java
index f363721..25ceb46 100644
--- a/example/src/main/java/io/netty/example/http2/helloworld/frame/server/Http2Server.java
+++ b/example/src/main/java/io/netty/example/http2/helloworld/frame/server/Http2Server.java
@@ -38,7 +38,7 @@ import io.netty.handler.ssl.SupportedCipherSuiteFilter;
 import io.netty.handler.ssl.util.SelfSignedCertificate;
 
 /**
- * A HTTP/2 Server that responds to requests with a Hello World. Once started, you can test the
+ * An HTTP/2 Server that responds to requests with a Hello World. Once started, you can test the
  * server with the example client.
  *
  * <p>This example is making use of the "multiplexing" http2 API, where streams are mapped to child
diff --git a/example/src/main/java/io/netty/example/http2/helloworld/frame/server/Http2ServerInitializer.java b/example/src/main/java/io/netty/example/http2/helloworld/frame/server/Http2ServerInitializer.java
index b2bb856..09b8399 100644
--- a/example/src/main/java/io/netty/example/http2/helloworld/frame/server/Http2ServerInitializer.java
+++ b/example/src/main/java/io/netty/example/http2/helloworld/frame/server/Http2ServerInitializer.java
@@ -100,8 +100,7 @@ public class Http2ServerInitializer extends ChannelInitializer<SocketChannel> {
                 // If this handler is hit then no upgrade has been attempted and the client is just talking HTTP.
                 System.err.println("Directly talking: " + msg.protocolVersion() + " (no upgrade was attempted)");
                 ChannelPipeline pipeline = ctx.pipeline();
-                ChannelHandlerContext thisCtx = pipeline.context(this);
-                pipeline.addAfter(thisCtx.name(), null, new HelloWorldHttp1Handler("Direct. No Upgrade Attempted."));
+                pipeline.addAfter(ctx.name(), null, new HelloWorldHttp1Handler("Direct. No Upgrade Attempted."));
                 pipeline.replace(this, null, new HttpObjectAggregator(maxHttpContentLength));
                 ctx.fireChannelRead(ReferenceCountUtil.retain(msg));
             }
diff --git a/example/src/main/java/io/netty/example/http2/helloworld/multiplex/server/Http2OrHttpHandler.java b/example/src/main/java/io/netty/example/http2/helloworld/multiplex/server/Http2OrHttpHandler.java
index 4637e79..4099b04 100644
--- a/example/src/main/java/io/netty/example/http2/helloworld/multiplex/server/Http2OrHttpHandler.java
+++ b/example/src/main/java/io/netty/example/http2/helloworld/multiplex/server/Http2OrHttpHandler.java
@@ -18,7 +18,8 @@ import io.netty.channel.ChannelHandlerContext;
 import io.netty.example.http2.helloworld.server.HelloWorldHttp1Handler;
 import io.netty.handler.codec.http.HttpObjectAggregator;
 import io.netty.handler.codec.http.HttpServerCodec;
-import io.netty.handler.codec.http2.Http2MultiplexCodecBuilder;
+import io.netty.handler.codec.http2.Http2FrameCodecBuilder;
+import io.netty.handler.codec.http2.Http2MultiplexHandler;
 import io.netty.handler.ssl.ApplicationProtocolNames;
 import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler;
 
@@ -37,7 +38,8 @@ public class Http2OrHttpHandler extends ApplicationProtocolNegotiationHandler {
     @Override
     protected void configurePipeline(ChannelHandlerContext ctx, String protocol) throws Exception {
         if (ApplicationProtocolNames.HTTP_2.equals(protocol)) {
-            ctx.pipeline().addLast(Http2MultiplexCodecBuilder.forServer(new HelloWorldHttp2Handler()).build());
+            ctx.pipeline().addLast(Http2FrameCodecBuilder.forServer().build());
+            ctx.pipeline().addLast(new Http2MultiplexHandler(new HelloWorldHttp2Handler()));
             return;
         }
 
diff --git a/example/src/main/java/io/netty/example/http2/helloworld/multiplex/server/Http2Server.java b/example/src/main/java/io/netty/example/http2/helloworld/multiplex/server/Http2Server.java
index 61b3f08..89a488b 100644
--- a/example/src/main/java/io/netty/example/http2/helloworld/multiplex/server/Http2Server.java
+++ b/example/src/main/java/io/netty/example/http2/helloworld/multiplex/server/Http2Server.java
@@ -38,7 +38,7 @@ import io.netty.handler.ssl.SupportedCipherSuiteFilter;
 import io.netty.handler.ssl.util.SelfSignedCertificate;
 
 /**
- * A HTTP/2 Server that responds to requests with a Hello World. Once started, you can test the
+ * An HTTP/2 Server that responds to requests with a Hello World. Once started, you can test the
  * server with the example client.
  *
  * <p>This example is making use of the "multiplexing" http2 API, where streams are mapped to child
diff --git a/example/src/main/java/io/netty/example/http2/helloworld/multiplex/server/Http2ServerInitializer.java b/example/src/main/java/io/netty/example/http2/helloworld/multiplex/server/Http2ServerInitializer.java
index 891becc..95190f4 100644
--- a/example/src/main/java/io/netty/example/http2/helloworld/multiplex/server/Http2ServerInitializer.java
+++ b/example/src/main/java/io/netty/example/http2/helloworld/multiplex/server/Http2ServerInitializer.java
@@ -29,8 +29,9 @@ import io.netty.handler.codec.http.HttpServerCodec;
 import io.netty.handler.codec.http.HttpServerUpgradeHandler;
 import io.netty.handler.codec.http.HttpServerUpgradeHandler.UpgradeCodec;
 import io.netty.handler.codec.http.HttpServerUpgradeHandler.UpgradeCodecFactory;
-import io.netty.handler.codec.http2.Http2MultiplexCodecBuilder;
+import io.netty.handler.codec.http2.Http2FrameCodecBuilder;
 import io.netty.handler.codec.http2.Http2CodecUtil;
+import io.netty.handler.codec.http2.Http2MultiplexHandler;
 import io.netty.handler.codec.http2.Http2ServerUpgradeCodec;
 import io.netty.handler.ssl.SslContext;
 import io.netty.util.AsciiString;
@@ -47,7 +48,8 @@ public class Http2ServerInitializer extends ChannelInitializer<SocketChannel> {
         public UpgradeCodec newUpgradeCodec(CharSequence protocol) {
             if (AsciiString.contentEquals(Http2CodecUtil.HTTP_UPGRADE_PROTOCOL_NAME, protocol)) {
                 return new Http2ServerUpgradeCodec(
-                        Http2MultiplexCodecBuilder.forServer(new HelloWorldHttp2Handler()).build());
+                        Http2FrameCodecBuilder.forServer().build(),
+                        new Http2MultiplexHandler(new HelloWorldHttp2Handler()));
             } else {
                 return null;
             }
@@ -100,8 +102,7 @@ public class Http2ServerInitializer extends ChannelInitializer<SocketChannel> {
                 // If this handler is hit then no upgrade has been attempted and the client is just talking HTTP.
                 System.err.println("Directly talking: " + msg.protocolVersion() + " (no upgrade was attempted)");
                 ChannelPipeline pipeline = ctx.pipeline();
-                ChannelHandlerContext thisCtx = pipeline.context(this);
-                pipeline.addAfter(thisCtx.name(), null, new HelloWorldHttp1Handler("Direct. No Upgrade Attempted."));
+                pipeline.addAfter(ctx.name(), null, new HelloWorldHttp1Handler("Direct. No Upgrade Attempted."));
                 pipeline.replace(this, null, new HttpObjectAggregator(maxHttpContentLength));
                 ctx.fireChannelRead(ReferenceCountUtil.retain(msg));
             }
diff --git a/example/src/main/java/io/netty/example/http2/helloworld/server/HelloWorldHttp1Handler.java b/example/src/main/java/io/netty/example/http2/helloworld/server/HelloWorldHttp1Handler.java
index f5cfbf1..cafb57d 100644
--- a/example/src/main/java/io/netty/example/http2/helloworld/server/HelloWorldHttp1Handler.java
+++ b/example/src/main/java/io/netty/example/http2/helloworld/server/HelloWorldHttp1Handler.java
@@ -18,13 +18,17 @@ package io.netty.example.http2.helloworld.server;
 import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;
 import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
 import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;
+import static io.netty.handler.codec.http.HttpHeaderValues.CLOSE;
+import static io.netty.handler.codec.http.HttpHeaderValues.KEEP_ALIVE;
 import static io.netty.handler.codec.http.HttpResponseStatus.CONTINUE;
 import static io.netty.handler.codec.http.HttpResponseStatus.OK;
+import static io.netty.handler.codec.http.HttpVersion.HTTP_1_0;
 import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
 import static io.netty.util.internal.ObjectUtil.checkNotNull;
 
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.ByteBufUtil;
+import io.netty.buffer.Unpooled;
 import io.netty.channel.ChannelFutureListener;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.SimpleChannelInboundHandler;
@@ -32,7 +36,6 @@ import io.netty.handler.codec.http.DefaultFullHttpResponse;
 import io.netty.handler.codec.http.FullHttpRequest;
 import io.netty.handler.codec.http.FullHttpResponse;
 import io.netty.handler.codec.http.HttpUtil;
-import io.netty.handler.codec.http.HttpHeaderValues;
 
 /**
  * HTTP handler that responds with a "Hello World"
@@ -47,7 +50,7 @@ public class HelloWorldHttp1Handler extends SimpleChannelInboundHandler<FullHttp
     @Override
     public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception {
         if (HttpUtil.is100ContinueExpected(req)) {
-            ctx.write(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE));
+            ctx.write(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE, Unpooled.EMPTY_BUFFER));
         }
         boolean keepAlive = HttpUtil.isKeepAlive(req);
 
@@ -59,11 +62,15 @@ public class HelloWorldHttp1Handler extends SimpleChannelInboundHandler<FullHttp
         response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8");
         response.headers().setInt(CONTENT_LENGTH, response.content().readableBytes());
 
-        if (!keepAlive) {
-            ctx.write(response).addListener(ChannelFutureListener.CLOSE);
-        } else {
-            response.headers().set(CONNECTION, HttpHeaderValues.KEEP_ALIVE);
+        if (keepAlive) {
+            if (req.protocolVersion().equals(HTTP_1_0)) {
+                response.headers().set(CONNECTION, KEEP_ALIVE);
+            }
             ctx.write(response);
+        } else {
+            // Tell the client we're going to close the connection.
+            response.headers().set(CONNECTION, CLOSE);
+            ctx.write(response).addListener(ChannelFutureListener.CLOSE);
         }
     }
 
diff --git a/example/src/main/java/io/netty/example/http2/helloworld/server/Http2Server.java b/example/src/main/java/io/netty/example/http2/helloworld/server/Http2Server.java
index b99b375..1b84afa 100644
--- a/example/src/main/java/io/netty/example/http2/helloworld/server/Http2Server.java
+++ b/example/src/main/java/io/netty/example/http2/helloworld/server/Http2Server.java
@@ -38,7 +38,7 @@ import io.netty.handler.ssl.SupportedCipherSuiteFilter;
 import io.netty.handler.ssl.util.SelfSignedCertificate;
 
 /**
- * A HTTP/2 Server that responds to requests with a Hello World. Once started, you can test the
+ * An HTTP/2 Server that responds to requests with a Hello World. Once started, you can test the
  * server with the example client.
  */
 public final class Http2Server {
diff --git a/example/src/main/java/io/netty/example/http2/helloworld/server/Http2ServerInitializer.java b/example/src/main/java/io/netty/example/http2/helloworld/server/Http2ServerInitializer.java
index b5dff01..ca429c6 100644
--- a/example/src/main/java/io/netty/example/http2/helloworld/server/Http2ServerInitializer.java
+++ b/example/src/main/java/io/netty/example/http2/helloworld/server/Http2ServerInitializer.java
@@ -101,8 +101,7 @@ public class Http2ServerInitializer extends ChannelInitializer<SocketChannel> {
                 // If this handler is hit then no upgrade has been attempted and the client is just talking HTTP.
                 System.err.println("Directly talking: " + msg.protocolVersion() + " (no upgrade was attempted)");
                 ChannelPipeline pipeline = ctx.pipeline();
-                ChannelHandlerContext thisCtx = pipeline.context(this);
-                pipeline.addAfter(thisCtx.name(), null, new HelloWorldHttp1Handler("Direct. No Upgrade Attempted."));
+                pipeline.addAfter(ctx.name(), null, new HelloWorldHttp1Handler("Direct. No Upgrade Attempted."));
                 pipeline.replace(this, null, new HttpObjectAggregator(maxHttpContentLength));
                 ctx.fireChannelRead(ReferenceCountUtil.retain(msg));
             }
diff --git a/example/src/main/java/io/netty/example/http2/tiles/FallbackRequestHandler.java b/example/src/main/java/io/netty/example/http2/tiles/FallbackRequestHandler.java
index e9836dc..ee3e732 100644
--- a/example/src/main/java/io/netty/example/http2/tiles/FallbackRequestHandler.java
+++ b/example/src/main/java/io/netty/example/http2/tiles/FallbackRequestHandler.java
@@ -16,6 +16,7 @@
 
 package io.netty.example.http2.tiles;
 
+import static io.netty.buffer.Unpooled.EMPTY_BUFFER;
 import static io.netty.buffer.Unpooled.copiedBuffer;
 import static io.netty.buffer.Unpooled.unreleasableBuffer;
 import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
@@ -47,7 +48,7 @@ public final class FallbackRequestHandler extends SimpleChannelInboundHandler<Ht
     @Override
     protected void channelRead0(ChannelHandlerContext ctx, HttpRequest req) throws Exception {
         if (HttpUtil.is100ContinueExpected(req)) {
-            ctx.write(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE));
+            ctx.write(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE, EMPTY_BUFFER));
         }
 
         ByteBuf content = ctx.alloc().buffer();
diff --git a/example/src/main/java/io/netty/example/http2/tiles/Http1RequestHandler.java b/example/src/main/java/io/netty/example/http2/tiles/Http1RequestHandler.java
index 49590f8..ebc6bf8 100644
--- a/example/src/main/java/io/netty/example/http2/tiles/Http1RequestHandler.java
+++ b/example/src/main/java/io/netty/example/http2/tiles/Http1RequestHandler.java
@@ -17,16 +17,20 @@
 package io.netty.example.http2.tiles;
 
 import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;
+import static io.netty.handler.codec.http.HttpHeaderValues.CLOSE;
+import static io.netty.handler.codec.http.HttpHeaderValues.KEEP_ALIVE;
 import static io.netty.handler.codec.http.HttpUtil.isKeepAlive;
 import static io.netty.handler.codec.http.HttpResponseStatus.CONTINUE;
+import static io.netty.handler.codec.http.HttpVersion.HTTP_1_0;
 import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
+
+import io.netty.buffer.Unpooled;
 import io.netty.channel.ChannelFutureListener;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.handler.codec.http.DefaultFullHttpResponse;
 import io.netty.handler.codec.http.FullHttpRequest;
 import io.netty.handler.codec.http.FullHttpResponse;
 import io.netty.handler.codec.http.HttpUtil;
-import io.netty.handler.codec.http.HttpHeaderValues;
 
 import java.util.concurrent.TimeUnit;
 
@@ -38,7 +42,7 @@ public final class Http1RequestHandler extends Http2RequestHandler {
     @Override
     protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
         if (HttpUtil.is100ContinueExpected(request)) {
-            ctx.write(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE));
+            ctx.write(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE, Unpooled.EMPTY_BUFFER));
         }
         super.channelRead0(ctx, request);
     }
@@ -51,9 +55,13 @@ public final class Http1RequestHandler extends Http2RequestHandler {
             @Override
             public void run() {
                 if (isKeepAlive(request)) {
-                    response.headers().set(CONNECTION, HttpHeaderValues.KEEP_ALIVE);
+                    if (request.protocolVersion().equals(HTTP_1_0)) {
+                        response.headers().set(CONNECTION, KEEP_ALIVE);
+                    }
                     ctx.writeAndFlush(response);
                 } else {
+                    // Tell the client we're going to close the connection.
+                    response.headers().set(CONNECTION, CLOSE);
                     ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
                 }
             }
diff --git a/example/src/main/java/io/netty/example/http2/tiles/Http2Server.java b/example/src/main/java/io/netty/example/http2/tiles/Http2Server.java
index 2a50c5a..1a95f36 100644
--- a/example/src/main/java/io/netty/example/http2/tiles/Http2Server.java
+++ b/example/src/main/java/io/netty/example/http2/tiles/Http2Server.java
@@ -40,7 +40,7 @@ import java.security.cert.CertificateException;
 import javax.net.ssl.SSLException;
 
 /**
- * Demonstrates a Http2 server using Netty to display a bunch of images and
+ * Demonstrates an Http2 server using Netty to display a bunch of images and
  * simulate latency. It is a Netty version of the <a href="https://http2.golang.org/gophertiles?latency=0">
  * Go lang HTTP2 tiles demo</a>.
  */
diff --git a/example/src/main/java/io/netty/example/http2/tiles/HttpServer.java b/example/src/main/java/io/netty/example/http2/tiles/HttpServer.java
index f46d01a..125509f 100644
--- a/example/src/main/java/io/netty/example/http2/tiles/HttpServer.java
+++ b/example/src/main/java/io/netty/example/http2/tiles/HttpServer.java
@@ -31,7 +31,7 @@ import io.netty.handler.logging.LogLevel;
 import io.netty.handler.logging.LoggingHandler;
 
 /**
- * Demonstrates a http server using Netty to display a bunch of images, simulate
+ * Demonstrates an http server using Netty to display a bunch of images, simulate
  * latency and compare it against the http2 implementation.
  */
 public final class HttpServer {
diff --git a/example/src/main/java/io/netty/example/mqtt/heartBeat/MqttHeartBeatBroker.java b/example/src/main/java/io/netty/example/mqtt/heartBeat/MqttHeartBeatBroker.java
new file mode 100644
index 0000000..47a16ad
--- /dev/null
+++ b/example/src/main/java/io/netty/example/mqtt/heartBeat/MqttHeartBeatBroker.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2019 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.example.mqtt.heartBeat;
+
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.handler.codec.mqtt.MqttDecoder;
+import io.netty.handler.codec.mqtt.MqttEncoder;
+import io.netty.handler.timeout.IdleStateHandler;
+
+import java.util.concurrent.TimeUnit;
+
+public final class MqttHeartBeatBroker {
+
+    private MqttHeartBeatBroker() {
+    }
+
+    public static void main(String[] args) throws Exception {
+        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
+        EventLoopGroup workerGroup = new NioEventLoopGroup();
+
+        try {
+            ServerBootstrap b = new ServerBootstrap();
+            b.group(bossGroup, workerGroup);
+            b.option(ChannelOption.SO_BACKLOG, 1024);
+            b.channel(NioServerSocketChannel.class);
+            b.childHandler(new ChannelInitializer<SocketChannel>() {
+                protected void initChannel(SocketChannel ch) throws Exception {
+                    ch.pipeline().addLast("encoder", MqttEncoder.INSTANCE);
+                    ch.pipeline().addLast("decoder", new MqttDecoder());
+                    ch.pipeline().addLast("heartBeatHandler", new IdleStateHandler(45, 0, 0, TimeUnit.SECONDS));
+                    ch.pipeline().addLast("handler", MqttHeartBeatBrokerHandler.INSTANCE);
+                }
+            });
+
+            ChannelFuture f = b.bind(1883).sync();
+            System.out.println("Broker initiated...");
+
+            f.channel().closeFuture().sync();
+        } finally {
+            workerGroup.shutdownGracefully();
+            bossGroup.shutdownGracefully();
+        }
+    }
+}
diff --git a/example/src/main/java/io/netty/example/mqtt/heartBeat/MqttHeartBeatBrokerHandler.java b/example/src/main/java/io/netty/example/mqtt/heartBeat/MqttHeartBeatBrokerHandler.java
new file mode 100644
index 0000000..5505fc2
--- /dev/null
+++ b/example/src/main/java/io/netty/example/mqtt/heartBeat/MqttHeartBeatBrokerHandler.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2019 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.example.mqtt.heartBeat;
+
+import io.netty.channel.ChannelHandler.Sharable;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.handler.codec.mqtt.MqttConnAckMessage;
+import io.netty.handler.codec.mqtt.MqttConnAckVariableHeader;
+import io.netty.handler.codec.mqtt.MqttConnectReturnCode;
+import io.netty.handler.codec.mqtt.MqttFixedHeader;
+import io.netty.handler.codec.mqtt.MqttMessage;
+import io.netty.handler.codec.mqtt.MqttMessageType;
+import io.netty.handler.codec.mqtt.MqttQoS;
+import io.netty.handler.timeout.IdleState;
+import io.netty.handler.timeout.IdleStateEvent;
+import io.netty.util.ReferenceCountUtil;
+
+@Sharable
+public final class MqttHeartBeatBrokerHandler extends ChannelInboundHandlerAdapter {
+
+    public static final MqttHeartBeatBrokerHandler INSTANCE = new MqttHeartBeatBrokerHandler();
+
+    private MqttHeartBeatBrokerHandler() {
+    }
+
+    @Override
+    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+        MqttMessage mqttMessage = (MqttMessage) msg;
+        System.out.println("Received MQTT message: " + mqttMessage);
+        switch (mqttMessage.fixedHeader().messageType()) {
+        case CONNECT:
+            MqttFixedHeader connackFixedHeader =
+                    new MqttFixedHeader(MqttMessageType.CONNACK, false, MqttQoS.AT_MOST_ONCE, false, 0);
+            MqttConnAckVariableHeader mqttConnAckVariableHeader =
+                    new MqttConnAckVariableHeader(MqttConnectReturnCode.CONNECTION_ACCEPTED, false);
+            MqttConnAckMessage connack = new MqttConnAckMessage(connackFixedHeader, mqttConnAckVariableHeader);
+            ctx.writeAndFlush(connack);
+            break;
+        case PINGREQ:
+            MqttFixedHeader pingreqFixedHeader = new MqttFixedHeader(MqttMessageType.PINGRESP, false,
+                                                                     MqttQoS.AT_MOST_ONCE, false, 0);
+            MqttMessage pingResp = new MqttMessage(pingreqFixedHeader);
+            ctx.writeAndFlush(pingResp);
+            break;
+        case DISCONNECT:
+            ctx.close();
+            break;
+        default:
+            System.out.println("Unexpected message type: " + mqttMessage.fixedHeader().messageType());
+            ReferenceCountUtil.release(msg);
+            ctx.close();
+        }
+    }
+
+    @Override
+    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
+        System.out.println("Channel heartBeat lost");
+        if (evt instanceof IdleStateEvent && IdleState.READER_IDLE == ((IdleStateEvent) evt).state()) {
+            ctx.close();
+        }
+    }
+
+    @Override
+    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+        cause.printStackTrace();
+        ctx.close();
+    }
+}
diff --git a/example/src/main/java/io/netty/example/mqtt/heartBeat/MqttHeartBeatClient.java b/example/src/main/java/io/netty/example/mqtt/heartBeat/MqttHeartBeatClient.java
new file mode 100644
index 0000000..b9729c7
--- /dev/null
+++ b/example/src/main/java/io/netty/example/mqtt/heartBeat/MqttHeartBeatClient.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2019 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.example.mqtt.heartBeat;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioSocketChannel;
+import io.netty.handler.codec.mqtt.MqttDecoder;
+import io.netty.handler.codec.mqtt.MqttEncoder;
+import io.netty.handler.timeout.IdleStateHandler;
+
+import java.util.concurrent.TimeUnit;
+
+public final class MqttHeartBeatClient {
+    private MqttHeartBeatClient() {
+    }
+
+    private static final String HOST = System.getProperty("host", "127.0.0.1");
+    private static final int PORT = Integer.parseInt(System.getProperty("port", "1883"));
+    private static final String CLIENT_ID = System.getProperty("clientId", "guestClient");
+    private static final String USER_NAME = System.getProperty("userName", "guest");
+    private static final String PASSWORD = System.getProperty("password", "guest");
+
+    public static void main(String[] args) throws Exception {
+        EventLoopGroup workerGroup = new NioEventLoopGroup();
+
+        try {
+            Bootstrap b = new Bootstrap();
+            b.group(workerGroup);
+            b.channel(NioSocketChannel.class);
+            b.handler(new ChannelInitializer<SocketChannel>() {
+                protected void initChannel(SocketChannel ch) throws Exception {
+                    ch.pipeline().addLast("encoder", MqttEncoder.INSTANCE);
+                    ch.pipeline().addLast("decoder", new MqttDecoder());
+                    ch.pipeline().addLast("heartBeatHandler", new IdleStateHandler(0, 20, 0, TimeUnit.SECONDS));
+                    ch.pipeline().addLast("handler", new MqttHeartBeatClientHandler(CLIENT_ID, USER_NAME, PASSWORD));
+                }
+            });
+
+            ChannelFuture f = b.connect(HOST, PORT).sync();
+            System.out.println("Client connected");
+            f.channel().closeFuture().sync();
+        } finally {
+            workerGroup.shutdownGracefully();
+        }
+    }
+}
diff --git a/example/src/main/java/io/netty/example/mqtt/heartBeat/MqttHeartBeatClientHandler.java b/example/src/main/java/io/netty/example/mqtt/heartBeat/MqttHeartBeatClientHandler.java
new file mode 100644
index 0000000..d04ecf1
--- /dev/null
+++ b/example/src/main/java/io/netty/example/mqtt/heartBeat/MqttHeartBeatClientHandler.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2019 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.example.mqtt.heartBeat;
+
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.handler.codec.mqtt.MqttConnectMessage;
+import io.netty.handler.codec.mqtt.MqttConnectPayload;
+import io.netty.handler.codec.mqtt.MqttConnectVariableHeader;
+import io.netty.handler.codec.mqtt.MqttFixedHeader;
+import io.netty.handler.codec.mqtt.MqttMessage;
+import io.netty.handler.codec.mqtt.MqttMessageType;
+import io.netty.handler.codec.mqtt.MqttQoS;
+import io.netty.handler.timeout.IdleStateEvent;
+import io.netty.util.ReferenceCountUtil;
+
+public class MqttHeartBeatClientHandler extends ChannelInboundHandlerAdapter {
+
+    private static final String PROTOCOL_NAME_MQTT_3_1_1 = "MQTT";
+    private static final int PROTOCOL_VERSION_MQTT_3_1_1 = 4;
+
+    private final String clientId;
+    private final String userName;
+    private final byte[] password;
+
+    public MqttHeartBeatClientHandler(String clientId, String userName, String password) {
+        this.clientId = clientId;
+        this.userName = userName;
+        this.password = password.getBytes();
+    }
+
+    @Override
+    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+        // discard all messages
+        ReferenceCountUtil.release(msg);
+    }
+
+    @Override
+    public void channelActive(ChannelHandlerContext ctx) throws Exception {
+        MqttFixedHeader connectFixedHeader =
+                new MqttFixedHeader(MqttMessageType.CONNECT, false, MqttQoS.AT_MOST_ONCE, false, 0);
+        MqttConnectVariableHeader connectVariableHeader =
+                new MqttConnectVariableHeader(PROTOCOL_NAME_MQTT_3_1_1, PROTOCOL_VERSION_MQTT_3_1_1, true, true, false,
+                                              0, false, false, 20);
+        MqttConnectPayload connectPayload = new MqttConnectPayload(clientId, null, null, userName, password);
+        MqttConnectMessage connectMessage =
+                new MqttConnectMessage(connectFixedHeader, connectVariableHeader, connectPayload);
+        ctx.writeAndFlush(connectMessage);
+        System.out.println("Sent CONNECT");
+    }
+
+    @Override
+    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
+        if (evt instanceof IdleStateEvent) {
+            MqttFixedHeader pingreqFixedHeader =
+                    new MqttFixedHeader(MqttMessageType.PINGREQ, false, MqttQoS.AT_MOST_ONCE, false, 0);
+            MqttMessage pingreqMessage = new MqttMessage(pingreqFixedHeader);
+            ctx.writeAndFlush(pingreqMessage);
+            System.out.println("Sent PINGREQ");
+        } else {
+            super.userEventTriggered(ctx, evt);
+        }
+    }
+
+    @Override
+    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+        cause.printStackTrace();
+        ctx.close();
+    }
+}
diff --git a/example/src/main/java/io/netty/example/ocsp/OcspClientExample.java b/example/src/main/java/io/netty/example/ocsp/OcspClientExample.java
index f36001c..3546646 100644
--- a/example/src/main/java/io/netty/example/ocsp/OcspClientExample.java
+++ b/example/src/main/java/io/netty/example/ocsp/OcspClientExample.java
@@ -21,6 +21,7 @@ import java.math.BigInteger;
 import javax.net.ssl.SSLSession;
 import javax.security.cert.X509Certificate;
 
+import io.netty.buffer.Unpooled;
 import org.bouncycastle.asn1.ocsp.OCSPResponseStatus;
 import org.bouncycastle.cert.ocsp.BasicOCSPResp;
 import org.bouncycastle.cert.ocsp.CertificateStatus;
@@ -57,8 +58,8 @@ import io.netty.util.ReferenceCountUtil;
 import io.netty.util.concurrent.Promise;
 
 /**
- * This is a very simple example for a HTTPS client that uses OCSP stapling.
- * The client connects to a HTTPS server that has OCSP stapling enabled and
+ * This is a very simple example for an HTTPS client that uses OCSP stapling.
+ * The client connects to an HTTPS server that has OCSP stapling enabled and
  * then uses BC to parse and validate it.
  */
 public class OcspClientExample {
@@ -157,14 +158,15 @@ public class OcspClientExample {
 
         private final Promise<FullHttpResponse> promise;
 
-        public HttpClientHandler(String host, Promise<FullHttpResponse> promise) {
+        HttpClientHandler(String host, Promise<FullHttpResponse> promise) {
             this.host = host;
             this.promise = promise;
         }
 
         @Override
         public void channelActive(ChannelHandlerContext ctx) throws Exception {
-            FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/");
+            FullHttpRequest request = new DefaultFullHttpRequest(
+                    HttpVersion.HTTP_1_1, HttpMethod.GET, "/", Unpooled.EMPTY_BUFFER);
             request.headers().set(HttpHeaderNames.HOST, host);
             request.headers().set(HttpHeaderNames.USER_AGENT, "netty-ocsp-example/1.0");
 
@@ -203,7 +205,7 @@ public class OcspClientExample {
 
     private static class ExampleOcspClientHandler extends OcspClientHandler {
 
-        public ExampleOcspClientHandler(ReferenceCountedOpenSslEngine engine) {
+        ExampleOcspClientHandler(ReferenceCountedOpenSslEngine engine) {
             super(engine);
         }
 
diff --git a/example/src/main/java/io/netty/example/ocsp/OcspServerExample.java b/example/src/main/java/io/netty/example/ocsp/OcspServerExample.java
index 5ff2210..b6a17e6 100644
--- a/example/src/main/java/io/netty/example/ocsp/OcspServerExample.java
+++ b/example/src/main/java/io/netty/example/ocsp/OcspServerExample.java
@@ -54,7 +54,7 @@ import io.netty.util.CharsetUtil;
 
 /**
  * ATTENTION: This is an incomplete example! In order to provide a fully functional
- * end-to-end example we'd need a X.509 certificate and the matching PrivateKey.
+ * end-to-end example we'd need an X.509 certificate and the matching PrivateKey.
  */
 @SuppressWarnings("unused")
 public class OcspServerExample {
@@ -71,7 +71,7 @@ public class OcspServerExample {
         X509Certificate issuer = keyCertChain[keyCertChain.length - 1];
 
         // Step 2: We need the URL of the CA's OCSP responder server. It's somewhere encoded
-        // into the certificate! Notice that it's a HTTP URL.
+        // into the certificate! Notice that it's an HTTP URL.
         URI uri = OcspUtils.ocspUri(certificate);
         System.out.println("OCSP Responder URI: " + uri);
 
diff --git a/example/src/main/java/io/netty/example/spdy/client/SpdyClient.java b/example/src/main/java/io/netty/example/spdy/client/SpdyClient.java
index 22a2acd..9f4128c 100644
--- a/example/src/main/java/io/netty/example/spdy/client/SpdyClient.java
+++ b/example/src/main/java/io/netty/example/spdy/client/SpdyClient.java
@@ -16,6 +16,7 @@
 package io.netty.example.spdy.client;
 
 import io.netty.bootstrap.Bootstrap;
+import io.netty.buffer.Unpooled;
 import io.netty.channel.Channel;
 import io.netty.channel.ChannelOption;
 import io.netty.channel.EventLoopGroup;
@@ -84,7 +85,8 @@ public final class SpdyClient {
             System.out.println("Connected to " + HOST + ':' + PORT);
 
             // Create a GET request.
-            HttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "");
+            HttpRequest request = new DefaultFullHttpRequest(
+                    HttpVersion.HTTP_1_1, HttpMethod.GET, "", Unpooled.EMPTY_BUFFER);
             request.headers().set(HttpHeaderNames.HOST, HOST);
             request.headers().set(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP);
 
diff --git a/example/src/main/java/io/netty/example/spdy/client/SpdyFrameLogger.java b/example/src/main/java/io/netty/example/spdy/client/SpdyFrameLogger.java
index ab136d7..ccec0bf 100644
--- a/example/src/main/java/io/netty/example/spdy/client/SpdyFrameLogger.java
+++ b/example/src/main/java/io/netty/example/spdy/client/SpdyFrameLogger.java
@@ -19,6 +19,7 @@ import io.netty.channel.ChannelDuplexHandler;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelPromise;
 import io.netty.handler.codec.spdy.SpdyFrame;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.logging.InternalLogLevel;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
@@ -36,12 +37,8 @@ public class SpdyFrameLogger extends ChannelDuplexHandler {
     private final InternalLogLevel level;
 
     public SpdyFrameLogger(InternalLogLevel level) {
-        if (level == null) {
-            throw new NullPointerException("level");
-        }
-
-        logger = InternalLoggerFactory.getInstance(getClass());
-        this.level = level;
+        this.level = ObjectUtil.checkNotNull(level, "level");
+        this.logger = InternalLoggerFactory.getInstance(getClass());
     }
 
     @Override
diff --git a/example/src/main/java/io/netty/example/spdy/server/SpdyServerHandler.java b/example/src/main/java/io/netty/example/spdy/server/SpdyServerHandler.java
index 93db15d..80cfce2 100644
--- a/example/src/main/java/io/netty/example/spdy/server/SpdyServerHandler.java
+++ b/example/src/main/java/io/netty/example/spdy/server/SpdyServerHandler.java
@@ -22,16 +22,22 @@ import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.SimpleChannelInboundHandler;
 import io.netty.handler.codec.http.DefaultFullHttpResponse;
 import io.netty.handler.codec.http.FullHttpResponse;
-import io.netty.handler.codec.http.HttpHeaderNames;
-import io.netty.handler.codec.http.HttpHeaderValues;
 import io.netty.handler.codec.http.HttpRequest;
 import io.netty.util.CharsetUtil;
 
 import java.util.Date;
 
-import static io.netty.handler.codec.http.HttpUtil.*;
-import static io.netty.handler.codec.http.HttpResponseStatus.*;
-import static io.netty.handler.codec.http.HttpVersion.*;
+import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;
+import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
+import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;
+import static io.netty.handler.codec.http.HttpHeaderValues.CLOSE;
+import static io.netty.handler.codec.http.HttpHeaderValues.KEEP_ALIVE;
+import static io.netty.handler.codec.http.HttpResponseStatus.CONTINUE;
+import static io.netty.handler.codec.http.HttpResponseStatus.OK;
+import static io.netty.handler.codec.http.HttpVersion.HTTP_1_0;
+import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
+import static io.netty.handler.codec.http.HttpUtil.is100ContinueExpected;
+import static io.netty.handler.codec.http.HttpUtil.isKeepAlive;
 
 /**
  * HTTP handler that responds with a "Hello World"
@@ -44,21 +50,25 @@ public class SpdyServerHandler extends SimpleChannelInboundHandler<Object> {
             HttpRequest req = (HttpRequest) msg;
 
             if (is100ContinueExpected(req)) {
-                ctx.write(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE));
+                ctx.write(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE, Unpooled.EMPTY_BUFFER));
             }
             boolean keepAlive = isKeepAlive(req);
 
             ByteBuf content = Unpooled.copiedBuffer("Hello World " + new Date(), CharsetUtil.UTF_8);
 
             FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK, content);
-            response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
-            response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
+            response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8");
+            response.headers().setInt(CONTENT_LENGTH, response.content().readableBytes());
 
-            if (!keepAlive) {
-                ctx.write(response).addListener(ChannelFutureListener.CLOSE);
-            } else {
-                response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
+            if (keepAlive) {
+                if (req.protocolVersion().equals(HTTP_1_0)) {
+                    response.headers().set(CONNECTION, KEEP_ALIVE);
+                }
                 ctx.write(response);
+            } else {
+                // Tell the client we're going to close the connection.
+                response.headers().set(CONNECTION, CLOSE);
+                ctx.write(response).addListener(ChannelFutureListener.CLOSE);
             }
         }
     }
diff --git a/example/src/main/java/io/netty/example/worldclock/WorldClockProtocol.java b/example/src/main/java/io/netty/example/worldclock/WorldClockProtocol.java
index fb85e29..6bbd27a 100644
--- a/example/src/main/java/io/netty/example/worldclock/WorldClockProtocol.java
+++ b/example/src/main/java/io/netty/example/worldclock/WorldClockProtocol.java
@@ -122,6 +122,7 @@ public final class WorldClockProtocol {
     public static final int PACIFIC_VALUE = 10;
 
 
+    @Override
     public final int getNumber() { return value; }
 
     public static Continent valueOf(int value) {
@@ -148,15 +149,18 @@ public final class WorldClockProtocol {
     private static com.google.protobuf.Internal.EnumLiteMap<Continent>
         internalValueMap =
           new com.google.protobuf.Internal.EnumLiteMap<Continent>() {
+            @Override
             public Continent findValueByNumber(int number) {
               return Continent.valueOf(number);
             }
           };
 
+    @Override
     public final com.google.protobuf.Descriptors.EnumValueDescriptor
         getValueDescriptor() {
       return getDescriptor().getValues().get(index);
     }
+    @Override
     public final com.google.protobuf.Descriptors.EnumDescriptor
         getDescriptorForType() {
       return getDescriptor();
@@ -253,6 +257,7 @@ public final class WorldClockProtocol {
     public static final int SATURDAY_VALUE = 7;
 
 
+    @Override
     public final int getNumber() { return value; }
 
     public static DayOfWeek valueOf(int value) {
@@ -275,15 +280,18 @@ public final class WorldClockProtocol {
     private static com.google.protobuf.Internal.EnumLiteMap<DayOfWeek>
         internalValueMap =
           new com.google.protobuf.Internal.EnumLiteMap<DayOfWeek>() {
+            @Override
             public DayOfWeek findValueByNumber(int number) {
               return DayOfWeek.valueOf(number);
             }
           };
 
+    @Override
     public final com.google.protobuf.Descriptors.EnumValueDescriptor
         getValueDescriptor() {
       return getDescriptor().getValues().get(index);
     }
+    @Override
     public final com.google.protobuf.Descriptors.EnumDescriptor
         getDescriptorForType() {
       return getDescriptor();
@@ -361,6 +369,7 @@ public final class WorldClockProtocol {
       return defaultInstance;
     }
 
+    @Override
     public Location getDefaultInstanceForType() {
       return defaultInstance;
     }
@@ -427,6 +436,7 @@ public final class WorldClockProtocol {
       return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_Location_descriptor;
     }
 
+    @Override
     protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
         internalGetFieldAccessorTable() {
       return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_Location_fieldAccessorTable
@@ -436,6 +446,7 @@ public final class WorldClockProtocol {
 
     public static com.google.protobuf.Parser<Location> PARSER =
         new com.google.protobuf.AbstractParser<Location>() {
+      @Override
       public Location parsePartialFrom(
           com.google.protobuf.CodedInputStream input,
           com.google.protobuf.ExtensionRegistryLite extensionRegistry)
@@ -456,12 +467,14 @@ public final class WorldClockProtocol {
     /**
      * <code>required .io.netty.example.worldclock.Continent continent = 1;</code>
      */
+    @Override
     public boolean hasContinent() {
       return ((bitField0_ & 0x00000001) == 0x00000001);
     }
     /**
      * <code>required .io.netty.example.worldclock.Continent continent = 1;</code>
      */
+    @Override
     public io.netty.example.worldclock.WorldClockProtocol.Continent getContinent() {
       return continent_;
     }
@@ -472,12 +485,14 @@ public final class WorldClockProtocol {
     /**
      * <code>required string city = 2;</code>
      */
+    @Override
     public boolean hasCity() {
       return ((bitField0_ & 0x00000002) == 0x00000002);
     }
     /**
      * <code>required string city = 2;</code>
      */
+    @Override
     public java.lang.String getCity() {
       java.lang.Object ref = city_;
       if (ref instanceof java.lang.String) {
@@ -495,6 +510,7 @@ public final class WorldClockProtocol {
     /**
      * <code>required string city = 2;</code>
      */
+    @Override
     public com.google.protobuf.ByteString
         getCityBytes() {
       java.lang.Object ref = city_;
@@ -514,6 +530,7 @@ public final class WorldClockProtocol {
       city_ = "";
     }
     private byte memoizedIsInitialized = -1;
+    @Override
     public final boolean isInitialized() {
       byte isInitialized = memoizedIsInitialized;
       if (isInitialized != -1) return isInitialized == 1;
@@ -530,6 +547,7 @@ public final class WorldClockProtocol {
       return true;
     }
 
+    @Override
     public void writeTo(com.google.protobuf.CodedOutputStream output)
                         throws java.io.IOException {
       getSerializedSize();
@@ -543,6 +561,7 @@ public final class WorldClockProtocol {
     }
 
     private int memoizedSerializedSize = -1;
+    @Override
     public int getSerializedSize() {
       int size = memoizedSerializedSize;
       if (size != -1) return size;
@@ -622,10 +641,12 @@ public final class WorldClockProtocol {
     }
 
     public static Builder newBuilder() { return Builder.create(); }
+    @Override
     public Builder newBuilderForType() { return newBuilder(); }
     public static Builder newBuilder(io.netty.example.worldclock.WorldClockProtocol.Location prototype) {
       return newBuilder().mergeFrom(prototype);
     }
+    @Override
     public Builder toBuilder() { return newBuilder(this); }
 
     @java.lang.Override
@@ -645,6 +666,7 @@ public final class WorldClockProtocol {
         return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_Location_descriptor;
       }
 
+      @Override
       protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
           internalGetFieldAccessorTable() {
         return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_Location_fieldAccessorTable
@@ -670,6 +692,7 @@ public final class WorldClockProtocol {
         return new Builder();
       }
 
+      @Override
       public Builder clear() {
         super.clear();
         continent_ = io.netty.example.worldclock.WorldClockProtocol.Continent.AFRICA;
@@ -679,19 +702,23 @@ public final class WorldClockProtocol {
         return this;
       }
 
+      @Override
       public Builder clone() {
         return create().mergeFrom(buildPartial());
       }
 
+      @Override
       public com.google.protobuf.Descriptors.Descriptor
           getDescriptorForType() {
         return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_Location_descriptor;
       }
 
+      @Override
       public io.netty.example.worldclock.WorldClockProtocol.Location getDefaultInstanceForType() {
         return io.netty.example.worldclock.WorldClockProtocol.Location.getDefaultInstance();
       }
 
+      @Override
       public io.netty.example.worldclock.WorldClockProtocol.Location build() {
         io.netty.example.worldclock.WorldClockProtocol.Location result = buildPartial();
         if (!result.isInitialized()) {
@@ -700,6 +727,7 @@ public final class WorldClockProtocol {
         return result;
       }
 
+      @Override
       public io.netty.example.worldclock.WorldClockProtocol.Location buildPartial() {
         io.netty.example.worldclock.WorldClockProtocol.Location result = new io.netty.example.worldclock.WorldClockProtocol.Location(this);
         int from_bitField0_ = bitField0_;
@@ -717,6 +745,7 @@ public final class WorldClockProtocol {
         return result;
       }
 
+      @Override
       public Builder mergeFrom(com.google.protobuf.Message other) {
         if (other instanceof io.netty.example.worldclock.WorldClockProtocol.Location) {
           return mergeFrom((io.netty.example.worldclock.WorldClockProtocol.Location)other);
@@ -740,6 +769,7 @@ public final class WorldClockProtocol {
         return this;
       }
 
+      @Override
       public final boolean isInitialized() {
         if (!hasContinent()) {
           
@@ -752,6 +782,7 @@ public final class WorldClockProtocol {
         return true;
       }
 
+      @Override
       public Builder mergeFrom(
           com.google.protobuf.CodedInputStream input,
           com.google.protobuf.ExtensionRegistryLite extensionRegistry)
@@ -776,12 +807,14 @@ public final class WorldClockProtocol {
       /**
        * <code>required .io.netty.example.worldclock.Continent continent = 1;</code>
        */
+      @Override
       public boolean hasContinent() {
         return ((bitField0_ & 0x00000001) == 0x00000001);
       }
       /**
        * <code>required .io.netty.example.worldclock.Continent continent = 1;</code>
        */
+      @Override
       public io.netty.example.worldclock.WorldClockProtocol.Continent getContinent() {
         return continent_;
       }
@@ -812,12 +845,14 @@ public final class WorldClockProtocol {
       /**
        * <code>required string city = 2;</code>
        */
+      @Override
       public boolean hasCity() {
         return ((bitField0_ & 0x00000002) == 0x00000002);
       }
       /**
        * <code>required string city = 2;</code>
        */
+      @Override
       public java.lang.String getCity() {
         java.lang.Object ref = city_;
         if (!(ref instanceof java.lang.String)) {
@@ -832,6 +867,7 @@ public final class WorldClockProtocol {
       /**
        * <code>required string city = 2;</code>
        */
+      @Override
       public com.google.protobuf.ByteString
           getCityBytes() {
         java.lang.Object ref = city_;
@@ -938,6 +974,7 @@ public final class WorldClockProtocol {
       return defaultInstance;
     }
 
+    @Override
     public Locations getDefaultInstanceForType() {
       return defaultInstance;
     }
@@ -999,6 +1036,7 @@ public final class WorldClockProtocol {
       return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_Locations_descriptor;
     }
 
+    @Override
     protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
         internalGetFieldAccessorTable() {
       return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_Locations_fieldAccessorTable
@@ -1008,6 +1046,7 @@ public final class WorldClockProtocol {
 
     public static com.google.protobuf.Parser<Locations> PARSER =
         new com.google.protobuf.AbstractParser<Locations>() {
+      @Override
       public Locations parsePartialFrom(
           com.google.protobuf.CodedInputStream input,
           com.google.protobuf.ExtensionRegistryLite extensionRegistry)
@@ -1027,31 +1066,36 @@ public final class WorldClockProtocol {
     /**
      * <code>repeated .io.netty.example.worldclock.Location location = 1;</code>
      */
+    @Override
     public java.util.List<io.netty.example.worldclock.WorldClockProtocol.Location> getLocationList() {
       return location_;
     }
     /**
      * <code>repeated .io.netty.example.worldclock.Location location = 1;</code>
      */
-    public java.util.List<? extends io.netty.example.worldclock.WorldClockProtocol.LocationOrBuilder> 
+    @Override
+    public java.util.List<? extends io.netty.example.worldclock.WorldClockProtocol.LocationOrBuilder>
         getLocationOrBuilderList() {
       return location_;
     }
     /**
      * <code>repeated .io.netty.example.worldclock.Location location = 1;</code>
      */
+    @Override
     public int getLocationCount() {
       return location_.size();
     }
     /**
      * <code>repeated .io.netty.example.worldclock.Location location = 1;</code>
      */
+    @Override
     public io.netty.example.worldclock.WorldClockProtocol.Location getLocation(int index) {
       return location_.get(index);
     }
     /**
      * <code>repeated .io.netty.example.worldclock.Location location = 1;</code>
      */
+    @Override
     public io.netty.example.worldclock.WorldClockProtocol.LocationOrBuilder getLocationOrBuilder(
         int index) {
       return location_.get(index);
@@ -1061,6 +1105,7 @@ public final class WorldClockProtocol {
       location_ = java.util.Collections.emptyList();
     }
     private byte memoizedIsInitialized = -1;
+    @Override
     public final boolean isInitialized() {
       byte isInitialized = memoizedIsInitialized;
       if (isInitialized != -1) return isInitialized == 1;
@@ -1075,6 +1120,7 @@ public final class WorldClockProtocol {
       return true;
     }
 
+    @Override
     public void writeTo(com.google.protobuf.CodedOutputStream output)
                         throws java.io.IOException {
       getSerializedSize();
@@ -1085,6 +1131,7 @@ public final class WorldClockProtocol {
     }
 
     private int memoizedSerializedSize = -1;
+    @Override
     public int getSerializedSize() {
       int size = memoizedSerializedSize;
       if (size != -1) return size;
@@ -1160,10 +1207,12 @@ public final class WorldClockProtocol {
     }
 
     public static Builder newBuilder() { return Builder.create(); }
+    @Override
     public Builder newBuilderForType() { return newBuilder(); }
     public static Builder newBuilder(io.netty.example.worldclock.WorldClockProtocol.Locations prototype) {
       return newBuilder().mergeFrom(prototype);
     }
+    @Override
     public Builder toBuilder() { return newBuilder(this); }
 
     @java.lang.Override
@@ -1183,6 +1232,7 @@ public final class WorldClockProtocol {
         return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_Locations_descriptor;
       }
 
+      @Override
       protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
           internalGetFieldAccessorTable() {
         return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_Locations_fieldAccessorTable
@@ -1209,6 +1259,7 @@ public final class WorldClockProtocol {
         return new Builder();
       }
 
+      @Override
       public Builder clear() {
         super.clear();
         if (locationBuilder_ == null) {
@@ -1220,19 +1271,23 @@ public final class WorldClockProtocol {
         return this;
       }
 
+      @Override
       public Builder clone() {
         return create().mergeFrom(buildPartial());
       }
 
+      @Override
       public com.google.protobuf.Descriptors.Descriptor
           getDescriptorForType() {
         return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_Locations_descriptor;
       }
 
+      @Override
       public io.netty.example.worldclock.WorldClockProtocol.Locations getDefaultInstanceForType() {
         return io.netty.example.worldclock.WorldClockProtocol.Locations.getDefaultInstance();
       }
 
+      @Override
       public io.netty.example.worldclock.WorldClockProtocol.Locations build() {
         io.netty.example.worldclock.WorldClockProtocol.Locations result = buildPartial();
         if (!result.isInitialized()) {
@@ -1241,6 +1296,7 @@ public final class WorldClockProtocol {
         return result;
       }
 
+      @Override
       public io.netty.example.worldclock.WorldClockProtocol.Locations buildPartial() {
         io.netty.example.worldclock.WorldClockProtocol.Locations result = new io.netty.example.worldclock.WorldClockProtocol.Locations(this);
         int from_bitField0_ = bitField0_;
@@ -1257,6 +1313,7 @@ public final class WorldClockProtocol {
         return result;
       }
 
+      @Override
       public Builder mergeFrom(com.google.protobuf.Message other) {
         if (other instanceof io.netty.example.worldclock.WorldClockProtocol.Locations) {
           return mergeFrom((io.netty.example.worldclock.WorldClockProtocol.Locations)other);
@@ -1298,6 +1355,7 @@ public final class WorldClockProtocol {
         return this;
       }
 
+      @Override
       public final boolean isInitialized() {
         for (int i = 0; i < getLocationCount(); i++) {
           if (!getLocation(i).isInitialized()) {
@@ -1308,6 +1366,7 @@ public final class WorldClockProtocol {
         return true;
       }
 
+      @Override
       public Builder mergeFrom(
           com.google.protobuf.CodedInputStream input,
           com.google.protobuf.ExtensionRegistryLite extensionRegistry)
@@ -1343,6 +1402,7 @@ public final class WorldClockProtocol {
       /**
        * <code>repeated .io.netty.example.worldclock.Location location = 1;</code>
        */
+      @Override
       public java.util.List<io.netty.example.worldclock.WorldClockProtocol.Location> getLocationList() {
         if (locationBuilder_ == null) {
           return java.util.Collections.unmodifiableList(location_);
@@ -1353,6 +1413,7 @@ public final class WorldClockProtocol {
       /**
        * <code>repeated .io.netty.example.worldclock.Location location = 1;</code>
        */
+      @Override
       public int getLocationCount() {
         if (locationBuilder_ == null) {
           return location_.size();
@@ -1363,6 +1424,7 @@ public final class WorldClockProtocol {
       /**
        * <code>repeated .io.netty.example.worldclock.Location location = 1;</code>
        */
+      @Override
       public io.netty.example.worldclock.WorldClockProtocol.Location getLocation(int index) {
         if (locationBuilder_ == null) {
           return location_.get(index);
@@ -1512,6 +1574,7 @@ public final class WorldClockProtocol {
       /**
        * <code>repeated .io.netty.example.worldclock.Location location = 1;</code>
        */
+      @Override
       public io.netty.example.worldclock.WorldClockProtocol.LocationOrBuilder getLocationOrBuilder(
           int index) {
         if (locationBuilder_ == null) {
@@ -1522,7 +1585,8 @@ public final class WorldClockProtocol {
       /**
        * <code>repeated .io.netty.example.worldclock.Location location = 1;</code>
        */
-      public java.util.List<? extends io.netty.example.worldclock.WorldClockProtocol.LocationOrBuilder> 
+      @Override
+      public java.util.List<? extends io.netty.example.worldclock.WorldClockProtocol.LocationOrBuilder>
            getLocationOrBuilderList() {
         if (locationBuilder_ != null) {
           return locationBuilder_.getMessageOrBuilderList();
@@ -1669,6 +1733,7 @@ public final class WorldClockProtocol {
       return defaultInstance;
     }
 
+    @Override
     public LocalTime getDefaultInstanceForType() {
       return defaultInstance;
     }
@@ -1760,6 +1825,7 @@ public final class WorldClockProtocol {
       return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_LocalTime_descriptor;
     }
 
+    @Override
     protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
         internalGetFieldAccessorTable() {
       return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_LocalTime_fieldAccessorTable
@@ -1769,6 +1835,7 @@ public final class WorldClockProtocol {
 
     public static com.google.protobuf.Parser<LocalTime> PARSER =
         new com.google.protobuf.AbstractParser<LocalTime>() {
+      @Override
       public LocalTime parsePartialFrom(
           com.google.protobuf.CodedInputStream input,
           com.google.protobuf.ExtensionRegistryLite extensionRegistry)
@@ -1789,12 +1856,14 @@ public final class WorldClockProtocol {
     /**
      * <code>required uint32 year = 1;</code>
      */
+    @Override
     public boolean hasYear() {
       return ((bitField0_ & 0x00000001) == 0x00000001);
     }
     /**
      * <code>required uint32 year = 1;</code>
      */
+    @Override
     public int getYear() {
       return year_;
     }
@@ -1805,12 +1874,14 @@ public final class WorldClockProtocol {
     /**
      * <code>required uint32 month = 2;</code>
      */
+    @Override
     public boolean hasMonth() {
       return ((bitField0_ & 0x00000002) == 0x00000002);
     }
     /**
      * <code>required uint32 month = 2;</code>
      */
+    @Override
     public int getMonth() {
       return month_;
     }
@@ -1821,12 +1892,14 @@ public final class WorldClockProtocol {
     /**
      * <code>required uint32 dayOfMonth = 4;</code>
      */
+    @Override
     public boolean hasDayOfMonth() {
       return ((bitField0_ & 0x00000004) == 0x00000004);
     }
     /**
      * <code>required uint32 dayOfMonth = 4;</code>
      */
+    @Override
     public int getDayOfMonth() {
       return dayOfMonth_;
     }
@@ -1837,12 +1910,14 @@ public final class WorldClockProtocol {
     /**
      * <code>required .io.netty.example.worldclock.DayOfWeek dayOfWeek = 5;</code>
      */
+    @Override
     public boolean hasDayOfWeek() {
       return ((bitField0_ & 0x00000008) == 0x00000008);
     }
     /**
      * <code>required .io.netty.example.worldclock.DayOfWeek dayOfWeek = 5;</code>
      */
+    @Override
     public io.netty.example.worldclock.WorldClockProtocol.DayOfWeek getDayOfWeek() {
       return dayOfWeek_;
     }
@@ -1853,12 +1928,14 @@ public final class WorldClockProtocol {
     /**
      * <code>required uint32 hour = 6;</code>
      */
+    @Override
     public boolean hasHour() {
       return ((bitField0_ & 0x00000010) == 0x00000010);
     }
     /**
      * <code>required uint32 hour = 6;</code>
      */
+    @Override
     public int getHour() {
       return hour_;
     }
@@ -1869,12 +1946,14 @@ public final class WorldClockProtocol {
     /**
      * <code>required uint32 minute = 7;</code>
      */
+    @Override
     public boolean hasMinute() {
       return ((bitField0_ & 0x00000020) == 0x00000020);
     }
     /**
      * <code>required uint32 minute = 7;</code>
      */
+    @Override
     public int getMinute() {
       return minute_;
     }
@@ -1885,12 +1964,14 @@ public final class WorldClockProtocol {
     /**
      * <code>required uint32 second = 8;</code>
      */
+    @Override
     public boolean hasSecond() {
       return ((bitField0_ & 0x00000040) == 0x00000040);
     }
     /**
      * <code>required uint32 second = 8;</code>
      */
+    @Override
     public int getSecond() {
       return second_;
     }
@@ -1905,6 +1986,7 @@ public final class WorldClockProtocol {
       second_ = 0;
     }
     private byte memoizedIsInitialized = -1;
+    @Override
     public final boolean isInitialized() {
       byte isInitialized = memoizedIsInitialized;
       if (isInitialized != -1) return isInitialized == 1;
@@ -1941,6 +2023,7 @@ public final class WorldClockProtocol {
       return true;
     }
 
+    @Override
     public void writeTo(com.google.protobuf.CodedOutputStream output)
                         throws java.io.IOException {
       getSerializedSize();
@@ -1969,6 +2052,7 @@ public final class WorldClockProtocol {
     }
 
     private int memoizedSerializedSize = -1;
+    @Override
     public int getSerializedSize() {
       int size = memoizedSerializedSize;
       if (size != -1) return size;
@@ -2068,10 +2152,12 @@ public final class WorldClockProtocol {
     }
 
     public static Builder newBuilder() { return Builder.create(); }
+    @Override
     public Builder newBuilderForType() { return newBuilder(); }
     public static Builder newBuilder(io.netty.example.worldclock.WorldClockProtocol.LocalTime prototype) {
       return newBuilder().mergeFrom(prototype);
     }
+    @Override
     public Builder toBuilder() { return newBuilder(this); }
 
     @java.lang.Override
@@ -2091,6 +2177,7 @@ public final class WorldClockProtocol {
         return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_LocalTime_descriptor;
       }
 
+      @Override
       protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
           internalGetFieldAccessorTable() {
         return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_LocalTime_fieldAccessorTable
@@ -2116,6 +2203,7 @@ public final class WorldClockProtocol {
         return new Builder();
       }
 
+      @Override
       public Builder clear() {
         super.clear();
         year_ = 0;
@@ -2135,19 +2223,23 @@ public final class WorldClockProtocol {
         return this;
       }
 
+      @Override
       public Builder clone() {
         return create().mergeFrom(buildPartial());
       }
 
+      @Override
       public com.google.protobuf.Descriptors.Descriptor
           getDescriptorForType() {
         return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_LocalTime_descriptor;
       }
 
+      @Override
       public io.netty.example.worldclock.WorldClockProtocol.LocalTime getDefaultInstanceForType() {
         return io.netty.example.worldclock.WorldClockProtocol.LocalTime.getDefaultInstance();
       }
 
+      @Override
       public io.netty.example.worldclock.WorldClockProtocol.LocalTime build() {
         io.netty.example.worldclock.WorldClockProtocol.LocalTime result = buildPartial();
         if (!result.isInitialized()) {
@@ -2156,6 +2248,7 @@ public final class WorldClockProtocol {
         return result;
       }
 
+      @Override
       public io.netty.example.worldclock.WorldClockProtocol.LocalTime buildPartial() {
         io.netty.example.worldclock.WorldClockProtocol.LocalTime result = new io.netty.example.worldclock.WorldClockProtocol.LocalTime(this);
         int from_bitField0_ = bitField0_;
@@ -2193,6 +2286,7 @@ public final class WorldClockProtocol {
         return result;
       }
 
+      @Override
       public Builder mergeFrom(com.google.protobuf.Message other) {
         if (other instanceof io.netty.example.worldclock.WorldClockProtocol.LocalTime) {
           return mergeFrom((io.netty.example.worldclock.WorldClockProtocol.LocalTime)other);
@@ -2229,6 +2323,7 @@ public final class WorldClockProtocol {
         return this;
       }
 
+      @Override
       public final boolean isInitialized() {
         if (!hasYear()) {
           
@@ -2261,6 +2356,7 @@ public final class WorldClockProtocol {
         return true;
       }
 
+      @Override
       public Builder mergeFrom(
           com.google.protobuf.CodedInputStream input,
           com.google.protobuf.ExtensionRegistryLite extensionRegistry)
@@ -2285,12 +2381,14 @@ public final class WorldClockProtocol {
       /**
        * <code>required uint32 year = 1;</code>
        */
+      @Override
       public boolean hasYear() {
         return ((bitField0_ & 0x00000001) == 0x00000001);
       }
       /**
        * <code>required uint32 year = 1;</code>
        */
+      @Override
       public int getYear() {
         return year_;
       }
@@ -2318,12 +2416,14 @@ public final class WorldClockProtocol {
       /**
        * <code>required uint32 month = 2;</code>
        */
+      @Override
       public boolean hasMonth() {
         return ((bitField0_ & 0x00000002) == 0x00000002);
       }
       /**
        * <code>required uint32 month = 2;</code>
        */
+      @Override
       public int getMonth() {
         return month_;
       }
@@ -2351,12 +2451,14 @@ public final class WorldClockProtocol {
       /**
        * <code>required uint32 dayOfMonth = 4;</code>
        */
+      @Override
       public boolean hasDayOfMonth() {
         return ((bitField0_ & 0x00000004) == 0x00000004);
       }
       /**
        * <code>required uint32 dayOfMonth = 4;</code>
        */
+      @Override
       public int getDayOfMonth() {
         return dayOfMonth_;
       }
@@ -2384,12 +2486,14 @@ public final class WorldClockProtocol {
       /**
        * <code>required .io.netty.example.worldclock.DayOfWeek dayOfWeek = 5;</code>
        */
+      @Override
       public boolean hasDayOfWeek() {
         return ((bitField0_ & 0x00000008) == 0x00000008);
       }
       /**
        * <code>required .io.netty.example.worldclock.DayOfWeek dayOfWeek = 5;</code>
        */
+      @Override
       public io.netty.example.worldclock.WorldClockProtocol.DayOfWeek getDayOfWeek() {
         return dayOfWeek_;
       }
@@ -2420,12 +2524,14 @@ public final class WorldClockProtocol {
       /**
        * <code>required uint32 hour = 6;</code>
        */
+      @Override
       public boolean hasHour() {
         return ((bitField0_ & 0x00000010) == 0x00000010);
       }
       /**
        * <code>required uint32 hour = 6;</code>
        */
+      @Override
       public int getHour() {
         return hour_;
       }
@@ -2453,12 +2559,14 @@ public final class WorldClockProtocol {
       /**
        * <code>required uint32 minute = 7;</code>
        */
+      @Override
       public boolean hasMinute() {
         return ((bitField0_ & 0x00000020) == 0x00000020);
       }
       /**
        * <code>required uint32 minute = 7;</code>
        */
+      @Override
       public int getMinute() {
         return minute_;
       }
@@ -2486,12 +2594,14 @@ public final class WorldClockProtocol {
       /**
        * <code>required uint32 second = 8;</code>
        */
+      @Override
       public boolean hasSecond() {
         return ((bitField0_ & 0x00000040) == 0x00000040);
       }
       /**
        * <code>required uint32 second = 8;</code>
        */
+      @Override
       public int getSecond() {
         return second_;
       }
@@ -2571,6 +2681,7 @@ public final class WorldClockProtocol {
       return defaultInstance;
     }
 
+    @Override
     public LocalTimes getDefaultInstanceForType() {
       return defaultInstance;
     }
@@ -2632,6 +2743,7 @@ public final class WorldClockProtocol {
       return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_LocalTimes_descriptor;
     }
 
+    @Override
     protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
         internalGetFieldAccessorTable() {
       return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_LocalTimes_fieldAccessorTable
@@ -2641,6 +2753,7 @@ public final class WorldClockProtocol {
 
     public static com.google.protobuf.Parser<LocalTimes> PARSER =
         new com.google.protobuf.AbstractParser<LocalTimes>() {
+      @Override
       public LocalTimes parsePartialFrom(
           com.google.protobuf.CodedInputStream input,
           com.google.protobuf.ExtensionRegistryLite extensionRegistry)
@@ -2660,31 +2773,36 @@ public final class WorldClockProtocol {
     /**
      * <code>repeated .io.netty.example.worldclock.LocalTime localTime = 1;</code>
      */
+    @Override
     public java.util.List<io.netty.example.worldclock.WorldClockProtocol.LocalTime> getLocalTimeList() {
       return localTime_;
     }
     /**
      * <code>repeated .io.netty.example.worldclock.LocalTime localTime = 1;</code>
      */
-    public java.util.List<? extends io.netty.example.worldclock.WorldClockProtocol.LocalTimeOrBuilder> 
+    @Override
+    public java.util.List<? extends io.netty.example.worldclock.WorldClockProtocol.LocalTimeOrBuilder>
         getLocalTimeOrBuilderList() {
       return localTime_;
     }
     /**
      * <code>repeated .io.netty.example.worldclock.LocalTime localTime = 1;</code>
      */
+    @Override
     public int getLocalTimeCount() {
       return localTime_.size();
     }
     /**
      * <code>repeated .io.netty.example.worldclock.LocalTime localTime = 1;</code>
      */
+    @Override
     public io.netty.example.worldclock.WorldClockProtocol.LocalTime getLocalTime(int index) {
       return localTime_.get(index);
     }
     /**
      * <code>repeated .io.netty.example.worldclock.LocalTime localTime = 1;</code>
      */
+    @Override
     public io.netty.example.worldclock.WorldClockProtocol.LocalTimeOrBuilder getLocalTimeOrBuilder(
         int index) {
       return localTime_.get(index);
@@ -2694,6 +2812,7 @@ public final class WorldClockProtocol {
       localTime_ = java.util.Collections.emptyList();
     }
     private byte memoizedIsInitialized = -1;
+    @Override
     public final boolean isInitialized() {
       byte isInitialized = memoizedIsInitialized;
       if (isInitialized != -1) return isInitialized == 1;
@@ -2708,6 +2827,7 @@ public final class WorldClockProtocol {
       return true;
     }
 
+    @Override
     public void writeTo(com.google.protobuf.CodedOutputStream output)
                         throws java.io.IOException {
       getSerializedSize();
@@ -2718,6 +2838,7 @@ public final class WorldClockProtocol {
     }
 
     private int memoizedSerializedSize = -1;
+    @Override
     public int getSerializedSize() {
       int size = memoizedSerializedSize;
       if (size != -1) return size;
@@ -2793,10 +2914,12 @@ public final class WorldClockProtocol {
     }
 
     public static Builder newBuilder() { return Builder.create(); }
+    @Override
     public Builder newBuilderForType() { return newBuilder(); }
     public static Builder newBuilder(io.netty.example.worldclock.WorldClockProtocol.LocalTimes prototype) {
       return newBuilder().mergeFrom(prototype);
     }
+    @Override
     public Builder toBuilder() { return newBuilder(this); }
 
     @java.lang.Override
@@ -2816,6 +2939,7 @@ public final class WorldClockProtocol {
         return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_LocalTimes_descriptor;
       }
 
+      @Override
       protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
           internalGetFieldAccessorTable() {
         return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_LocalTimes_fieldAccessorTable
@@ -2842,6 +2966,7 @@ public final class WorldClockProtocol {
         return new Builder();
       }
 
+      @Override
       public Builder clear() {
         super.clear();
         if (localTimeBuilder_ == null) {
@@ -2853,19 +2978,23 @@ public final class WorldClockProtocol {
         return this;
       }
 
+      @Override
       public Builder clone() {
         return create().mergeFrom(buildPartial());
       }
 
+      @Override
       public com.google.protobuf.Descriptors.Descriptor
           getDescriptorForType() {
         return io.netty.example.worldclock.WorldClockProtocol.internal_static_io_netty_example_worldclock_LocalTimes_descriptor;
       }
 
+      @Override
       public io.netty.example.worldclock.WorldClockProtocol.LocalTimes getDefaultInstanceForType() {
         return io.netty.example.worldclock.WorldClockProtocol.LocalTimes.getDefaultInstance();
       }
 
+      @Override
       public io.netty.example.worldclock.WorldClockProtocol.LocalTimes build() {
         io.netty.example.worldclock.WorldClockProtocol.LocalTimes result = buildPartial();
         if (!result.isInitialized()) {
@@ -2874,6 +3003,7 @@ public final class WorldClockProtocol {
         return result;
       }
 
+      @Override
       public io.netty.example.worldclock.WorldClockProtocol.LocalTimes buildPartial() {
         io.netty.example.worldclock.WorldClockProtocol.LocalTimes result = new io.netty.example.worldclock.WorldClockProtocol.LocalTimes(this);
         int from_bitField0_ = bitField0_;
@@ -2890,6 +3020,7 @@ public final class WorldClockProtocol {
         return result;
       }
 
+      @Override
       public Builder mergeFrom(com.google.protobuf.Message other) {
         if (other instanceof io.netty.example.worldclock.WorldClockProtocol.LocalTimes) {
           return mergeFrom((io.netty.example.worldclock.WorldClockProtocol.LocalTimes)other);
@@ -2931,6 +3062,7 @@ public final class WorldClockProtocol {
         return this;
       }
 
+      @Override
       public final boolean isInitialized() {
         for (int i = 0; i < getLocalTimeCount(); i++) {
           if (!getLocalTime(i).isInitialized()) {
@@ -2941,6 +3073,7 @@ public final class WorldClockProtocol {
         return true;
       }
 
+      @Override
       public Builder mergeFrom(
           com.google.protobuf.CodedInputStream input,
           com.google.protobuf.ExtensionRegistryLite extensionRegistry)
@@ -2976,6 +3109,7 @@ public final class WorldClockProtocol {
       /**
        * <code>repeated .io.netty.example.worldclock.LocalTime localTime = 1;</code>
        */
+      @Override
       public java.util.List<io.netty.example.worldclock.WorldClockProtocol.LocalTime> getLocalTimeList() {
         if (localTimeBuilder_ == null) {
           return java.util.Collections.unmodifiableList(localTime_);
@@ -2986,6 +3120,7 @@ public final class WorldClockProtocol {
       /**
        * <code>repeated .io.netty.example.worldclock.LocalTime localTime = 1;</code>
        */
+      @Override
       public int getLocalTimeCount() {
         if (localTimeBuilder_ == null) {
           return localTime_.size();
@@ -2996,6 +3131,7 @@ public final class WorldClockProtocol {
       /**
        * <code>repeated .io.netty.example.worldclock.LocalTime localTime = 1;</code>
        */
+      @Override
       public io.netty.example.worldclock.WorldClockProtocol.LocalTime getLocalTime(int index) {
         if (localTimeBuilder_ == null) {
           return localTime_.get(index);
@@ -3145,6 +3281,7 @@ public final class WorldClockProtocol {
       /**
        * <code>repeated .io.netty.example.worldclock.LocalTime localTime = 1;</code>
        */
+      @Override
       public io.netty.example.worldclock.WorldClockProtocol.LocalTimeOrBuilder getLocalTimeOrBuilder(
           int index) {
         if (localTimeBuilder_ == null) {
@@ -3155,7 +3292,8 @@ public final class WorldClockProtocol {
       /**
        * <code>repeated .io.netty.example.worldclock.LocalTime localTime = 1;</code>
        */
-      public java.util.List<? extends io.netty.example.worldclock.WorldClockProtocol.LocalTimeOrBuilder> 
+      @Override
+      public java.util.List<? extends io.netty.example.worldclock.WorldClockProtocol.LocalTimeOrBuilder>
            getLocalTimeOrBuilderList() {
         if (localTimeBuilder_ != null) {
           return localTimeBuilder_.getMessageOrBuilderList();
@@ -3262,6 +3400,7 @@ public final class WorldClockProtocol {
     };
     com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
       new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() {
+        @Override
         public com.google.protobuf.ExtensionRegistry assignDescriptors(
             com.google.protobuf.Descriptors.FileDescriptor root) {
           descriptor = root;
diff --git a/handler-proxy/pom.xml b/handler-proxy/pom.xml
index 0dcad25..452ece8 100644
--- a/handler-proxy/pom.xml
+++ b/handler-proxy/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>io.netty</groupId>
     <artifactId>netty-parent</artifactId>
-    <version>4.1.33.Final</version>
+    <version>4.1.48.Final</version>
   </parent>
 
   <artifactId>netty-handler-proxy</artifactId>
diff --git a/handler-proxy/src/main/java/io/netty/handler/proxy/HttpProxyHandler.java b/handler-proxy/src/main/java/io/netty/handler/proxy/HttpProxyHandler.java
index 2c41fab..9751b05 100644
--- a/handler-proxy/src/main/java/io/netty/handler/proxy/HttpProxyHandler.java
+++ b/handler-proxy/src/main/java/io/netty/handler/proxy/HttpProxyHandler.java
@@ -19,7 +19,10 @@ package io.netty.handler.proxy;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
 import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandler;
+import io.netty.channel.ChannelOutboundHandler;
 import io.netty.channel.ChannelPipeline;
+import io.netty.channel.ChannelPromise;
 import io.netty.handler.codec.base64.Base64;
 import io.netty.handler.codec.http.DefaultFullHttpRequest;
 import io.netty.handler.codec.http.FullHttpRequest;
@@ -34,6 +37,7 @@ import io.netty.handler.codec.http.HttpVersion;
 import io.netty.handler.codec.http.LastHttpContent;
 import io.netty.util.AsciiString;
 import io.netty.util.CharsetUtil;
+import io.netty.util.internal.ObjectUtil;
 
 import java.net.InetSocketAddress;
 import java.net.SocketAddress;
@@ -43,13 +47,20 @@ public final class HttpProxyHandler extends ProxyHandler {
     private static final String PROTOCOL = "http";
     private static final String AUTH_BASIC = "basic";
 
-    private final HttpClientCodec codec = new HttpClientCodec();
+    // Wrapper for the HttpClientCodec to prevent it to be removed by other handlers by mistake (for example the
+    // WebSocket*Handshaker.
+    //
+    // See:
+    // - https://github.com/netty/netty/issues/5201
+    // - https://github.com/netty/netty/issues/5070
+    private final HttpClientCodecWrapper codecWrapper = new HttpClientCodecWrapper();
     private final String username;
     private final String password;
     private final CharSequence authorization;
+    private final HttpHeaders outboundHeaders;
     private final boolean ignoreDefaultPortsInConnectHostHeader;
     private HttpResponseStatus status;
-    private HttpHeaders headers;
+    private HttpHeaders inboundHeaders;
 
     public HttpProxyHandler(SocketAddress proxyAddress) {
         this(proxyAddress, null);
@@ -66,7 +77,7 @@ public final class HttpProxyHandler extends ProxyHandler {
         username = null;
         password = null;
         authorization = null;
-        this.headers = headers;
+        this.outboundHeaders = headers;
         this.ignoreDefaultPortsInConnectHostHeader = ignoreDefaultPortsInConnectHostHeader;
     }
 
@@ -85,14 +96,8 @@ public final class HttpProxyHandler extends ProxyHandler {
                             HttpHeaders headers,
                             boolean ignoreDefaultPortsInConnectHostHeader) {
         super(proxyAddress);
-        if (username == null) {
-            throw new NullPointerException("username");
-        }
-        if (password == null) {
-            throw new NullPointerException("password");
-        }
-        this.username = username;
-        this.password = password;
+        this.username = ObjectUtil.checkNotNull(username, "username");
+        this.password = ObjectUtil.checkNotNull(password, "password");
 
         ByteBuf authz = Unpooled.copiedBuffer(username + ':' + password, CharsetUtil.UTF_8);
         ByteBuf authzBase64 = Base64.encode(authz, false);
@@ -102,7 +107,7 @@ public final class HttpProxyHandler extends ProxyHandler {
         authz.release();
         authzBase64.release();
 
-        this.headers = headers;
+        this.outboundHeaders = headers;
         this.ignoreDefaultPortsInConnectHostHeader = ignoreDefaultPortsInConnectHostHeader;
     }
 
@@ -128,17 +133,17 @@ public final class HttpProxyHandler extends ProxyHandler {
     protected void addCodec(ChannelHandlerContext ctx) throws Exception {
         ChannelPipeline p = ctx.pipeline();
         String name = ctx.name();
-        p.addBefore(name, null, codec);
+        p.addBefore(name, null, codecWrapper);
     }
 
     @Override
     protected void removeEncoder(ChannelHandlerContext ctx) throws Exception {
-        codec.removeOutboundHandler();
+        codecWrapper.codec.removeOutboundHandler();
     }
 
     @Override
     protected void removeDecoder(ChannelHandlerContext ctx) throws Exception {
-        codec.removeInboundHandler();
+        codecWrapper.codec.removeInboundHandler();
     }
 
     @Override
@@ -163,8 +168,8 @@ public final class HttpProxyHandler extends ProxyHandler {
             req.headers().set(HttpHeaderNames.PROXY_AUTHORIZATION, authorization);
         }
 
-        if (headers != null) {
-            req.headers().add(headers);
+        if (outboundHeaders != null) {
+            req.headers().add(outboundHeaders);
         }
 
         return req;
@@ -174,21 +179,149 @@ public final class HttpProxyHandler extends ProxyHandler {
     protected boolean handleResponse(ChannelHandlerContext ctx, Object response) throws Exception {
         if (response instanceof HttpResponse) {
             if (status != null) {
-                throw new ProxyConnectException(exceptionMessage("too many responses"));
+                throw new HttpProxyConnectException(exceptionMessage("too many responses"), /*headers=*/ null);
             }
-            status = ((HttpResponse) response).status();
+            HttpResponse res = (HttpResponse) response;
+            status = res.status();
+            inboundHeaders = res.headers();
         }
 
         boolean finished = response instanceof LastHttpContent;
         if (finished) {
             if (status == null) {
-                throw new ProxyConnectException(exceptionMessage("missing response"));
+                throw new HttpProxyConnectException(exceptionMessage("missing response"), inboundHeaders);
             }
             if (status.code() != 200) {
-                throw new ProxyConnectException(exceptionMessage("status: " + status));
+                throw new HttpProxyConnectException(exceptionMessage("status: " + status), inboundHeaders);
             }
         }
 
         return finished;
     }
+
+    /**
+     * Specific case of a connection failure, which may include headers from the proxy.
+     */
+    public static final class HttpProxyConnectException extends ProxyConnectException {
+        private static final long serialVersionUID = -8824334609292146066L;
+
+        private final HttpHeaders headers;
+
+        /**
+         * @param message The failure message.
+         * @param headers Header associated with the connection failure.  May be {@code null}.
+         */
+        public HttpProxyConnectException(String message, HttpHeaders headers) {
+            super(message);
+            this.headers = headers;
+        }
+
+        /**
+         * Returns headers, if any.  May be {@code null}.
+         */
+        public HttpHeaders headers() {
+            return headers;
+        }
+    }
+
+    private static final class HttpClientCodecWrapper implements ChannelInboundHandler, ChannelOutboundHandler {
+        final HttpClientCodec codec = new HttpClientCodec();
+
+        @Override
+        public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
+            codec.handlerAdded(ctx);
+        }
+
+        @Override
+        public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
+            codec.handlerRemoved(ctx);
+        }
+
+        @Override
+        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+            codec.exceptionCaught(ctx, cause);
+        }
+
+        @Override
+        public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
+            codec.channelRegistered(ctx);
+        }
+
+        @Override
+        public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
+            codec.channelUnregistered(ctx);
+        }
+
+        @Override
+        public void channelActive(ChannelHandlerContext ctx) throws Exception {
+            codec.channelActive(ctx);
+        }
+
+        @Override
+        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
+            codec.channelInactive(ctx);
+        }
+
+        @Override
+        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+            codec.channelRead(ctx, msg);
+        }
+
+        @Override
+        public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
+            codec.channelReadComplete(ctx);
+        }
+
+        @Override
+        public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
+            codec.userEventTriggered(ctx, evt);
+        }
+
+        @Override
+        public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
+            codec.channelWritabilityChanged(ctx);
+        }
+
+        @Override
+        public void bind(ChannelHandlerContext ctx, SocketAddress localAddress,
+                         ChannelPromise promise) throws Exception {
+            codec.bind(ctx, localAddress, promise);
+        }
+
+        @Override
+        public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress,
+                            ChannelPromise promise) throws Exception {
+            codec.connect(ctx, remoteAddress, localAddress, promise);
+        }
+
+        @Override
+        public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
+            codec.disconnect(ctx, promise);
+        }
+
+        @Override
+        public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
+            codec.close(ctx, promise);
+        }
+
+        @Override
+        public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
+            codec.deregister(ctx, promise);
+        }
+
+        @Override
+        public void read(ChannelHandlerContext ctx) throws Exception {
+            codec.read(ctx);
+        }
+
+        @Override
+        public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
+            codec.write(ctx, msg, promise);
+        }
+
+        @Override
+        public void flush(ChannelHandlerContext ctx) throws Exception {
+            codec.flush(ctx);
+        }
+    }
 }
diff --git a/handler-proxy/src/main/java/io/netty/handler/proxy/ProxyConnectionEvent.java b/handler-proxy/src/main/java/io/netty/handler/proxy/ProxyConnectionEvent.java
index 2d923fb..7928260 100644
--- a/handler-proxy/src/main/java/io/netty/handler/proxy/ProxyConnectionEvent.java
+++ b/handler-proxy/src/main/java/io/netty/handler/proxy/ProxyConnectionEvent.java
@@ -16,6 +16,7 @@
 
 package io.netty.handler.proxy;
 
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.StringUtil;
 
 import java.net.SocketAddress;
@@ -33,23 +34,10 @@ public final class ProxyConnectionEvent {
      */
     public ProxyConnectionEvent(
             String protocol, String authScheme, SocketAddress proxyAddress, SocketAddress destinationAddress) {
-        if (protocol == null) {
-            throw new NullPointerException("protocol");
-        }
-        if (authScheme == null) {
-            throw new NullPointerException("authScheme");
-        }
-        if (proxyAddress == null) {
-            throw new NullPointerException("proxyAddress");
-        }
-        if (destinationAddress == null) {
-            throw new NullPointerException("destinationAddress");
-        }
-
-        this.protocol = protocol;
-        this.authScheme = authScheme;
-        this.proxyAddress = proxyAddress;
-        this.destinationAddress = destinationAddress;
+        this.protocol = ObjectUtil.checkNotNull(protocol, "protocol");
+        this.authScheme = ObjectUtil.checkNotNull(authScheme, "authScheme");
+        this.proxyAddress = ObjectUtil.checkNotNull(proxyAddress, "proxyAddress");
+        this.destinationAddress = ObjectUtil.checkNotNull(destinationAddress, "destinationAddress");
     }
 
     /**
diff --git a/handler-proxy/src/main/java/io/netty/handler/proxy/ProxyHandler.java b/handler-proxy/src/main/java/io/netty/handler/proxy/ProxyHandler.java
index 8d3271e..e15baa7 100644
--- a/handler-proxy/src/main/java/io/netty/handler/proxy/ProxyHandler.java
+++ b/handler-proxy/src/main/java/io/netty/handler/proxy/ProxyHandler.java
@@ -28,6 +28,7 @@ import io.netty.util.concurrent.DefaultPromise;
 import io.netty.util.concurrent.EventExecutor;
 import io.netty.util.concurrent.Future;
 import io.netty.util.concurrent.ScheduledFuture;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 
@@ -70,10 +71,7 @@ public abstract class ProxyHandler extends ChannelDuplexHandler {
     };
 
     protected ProxyHandler(SocketAddress proxyAddress) {
-        if (proxyAddress == null) {
-            throw new NullPointerException("proxyAddress");
-        }
-        this.proxyAddress = proxyAddress;
+        this.proxyAddress = ObjectUtil.checkNotNull(proxyAddress, "proxyAddress");
     }
 
     /**
diff --git a/handler-proxy/src/test/java/io/netty/handler/proxy/HttpProxyHandlerTest.java b/handler-proxy/src/test/java/io/netty/handler/proxy/HttpProxyHandlerTest.java
index 04747e4..5e5be9e 100644
--- a/handler-proxy/src/test/java/io/netty/handler/proxy/HttpProxyHandlerTest.java
+++ b/handler-proxy/src/test/java/io/netty/handler/proxy/HttpProxyHandlerTest.java
@@ -15,20 +15,39 @@
  */
 package io.netty.handler.proxy;
 
+import io.netty.bootstrap.Bootstrap;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
 import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.channel.ChannelInitializer;
 import io.netty.channel.ChannelPromise;
+import io.netty.channel.DefaultEventLoopGroup;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.embedded.EmbeddedChannel;
+import io.netty.channel.local.LocalAddress;
+import io.netty.channel.local.LocalChannel;
+import io.netty.channel.local.LocalServerChannel;
+import io.netty.handler.codec.http.DefaultFullHttpResponse;
 import io.netty.handler.codec.http.DefaultHttpHeaders;
 import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.HttpClientCodec;
 import io.netty.handler.codec.http.HttpHeaderNames;
 import io.netty.handler.codec.http.HttpHeaders;
+import io.netty.handler.codec.http.HttpResponseEncoder;
+import io.netty.handler.codec.http.HttpResponseStatus;
 import io.netty.handler.codec.http.HttpVersion;
+import io.netty.handler.proxy.HttpProxyHandler.HttpProxyConnectException;
 import io.netty.util.NetUtil;
+
+import java.util.concurrent.atomic.AtomicReference;
 import org.junit.Test;
 
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 
-import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.*;
 import static org.mockito.Mockito.*;
 
 public class HttpProxyHandlerTest {
@@ -153,6 +172,65 @@ public class HttpProxyHandlerTest {
                 true);
     }
 
+    @Test
+    public void testExceptionDuringConnect() throws Exception {
+        EventLoopGroup group = null;
+        Channel serverChannel = null;
+        Channel clientChannel = null;
+        try {
+            group = new DefaultEventLoopGroup(1);
+            final LocalAddress addr = new LocalAddress("a");
+            final AtomicReference<Throwable> exception = new AtomicReference<Throwable>();
+            ChannelFuture sf =
+                new ServerBootstrap().channel(LocalServerChannel.class).group(group).childHandler(
+                    new ChannelInitializer<Channel>() {
+
+                        @Override
+                        protected void initChannel(Channel ch) {
+                            ch.pipeline().addFirst(new HttpResponseEncoder());
+                            DefaultFullHttpResponse response = new DefaultFullHttpResponse(
+                                HttpVersion.HTTP_1_1,
+                                HttpResponseStatus.BAD_GATEWAY);
+                            response.headers().add("name", "value");
+                            response.headers().add(HttpHeaderNames.CONTENT_LENGTH, "0");
+                            ch.writeAndFlush(response);
+                        }
+                    }).bind(addr);
+            serverChannel = sf.sync().channel();
+            ChannelFuture cf = new Bootstrap().channel(LocalChannel.class).group(group).handler(
+                new ChannelInitializer<Channel>() {
+                    @Override
+                    protected void initChannel(Channel ch) {
+                        ch.pipeline().addFirst(new HttpProxyHandler(addr));
+                        ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
+                            @Override
+                            public void exceptionCaught(ChannelHandlerContext ctx,
+                                Throwable cause) {
+                                exception.set(cause);
+                            }
+                        });
+                    }
+                }).connect(new InetSocketAddress("localhost", 1234));
+            clientChannel = cf.sync().channel();
+            clientChannel.close().sync();
+
+            assertTrue(exception.get() instanceof HttpProxyConnectException);
+            HttpProxyConnectException actual = (HttpProxyConnectException) exception.get();
+            assertNotNull(actual.headers());
+            assertEquals("value", actual.headers().get("name"));
+        } finally {
+            if (clientChannel != null) {
+                clientChannel.close();
+            }
+            if (serverChannel != null) {
+                serverChannel.close();
+            }
+            if (group != null) {
+                group.shutdownGracefully();
+            }
+        }
+    }
+
     private static void testInitialMessage(InetSocketAddress socketAddress,
                                            String expectedUrl,
                                            String expectedHostHeader,
@@ -190,4 +268,18 @@ public class HttpProxyHandlerTest {
         }
         verify(ctx).connect(proxyAddress, null, promise);
     }
+
+    @Test
+    public void testHttpClientCodecIsInvisible() {
+        EmbeddedChannel channel = new EmbeddedChannel(new HttpProxyHandler(
+                new InetSocketAddress(NetUtil.LOCALHOST, 8080))) {
+            @Override
+            public boolean isActive() {
+                // We want to simulate that the Channel did not become active yet.
+                return false;
+            }
+        };
+        assertNotNull(channel.pipeline().get(HttpProxyHandler.class));
+        assertNull(channel.pipeline().get(HttpClientCodec.class));
+    }
 }
diff --git a/handler-proxy/src/test/java/io/netty/handler/proxy/ProxyHandlerTest.java b/handler-proxy/src/test/java/io/netty/handler/proxy/ProxyHandlerTest.java
index 6f0f325..77e20fb 100644
--- a/handler-proxy/src/test/java/io/netty/handler/proxy/ProxyHandlerTest.java
+++ b/handler-proxy/src/test/java/io/netty/handler/proxy/ProxyHandlerTest.java
@@ -431,7 +431,9 @@ public class ProxyHandlerTest {
 
     private final TestItem testItem;
 
-    public ProxyHandlerTest(TestItem testItem) { this.testItem = testItem; }
+    public ProxyHandlerTest(TestItem testItem) {
+        this.testItem = testItem;
+    }
 
     @Before
     public void clearServerExceptions() throws Exception {
diff --git a/handler/pom.xml b/handler/pom.xml
index 4bddf4c..5e0d10f 100644
--- a/handler/pom.xml
+++ b/handler/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>io.netty</groupId>
     <artifactId>netty-parent</artifactId>
-    <version>4.1.33.Final</version>
+    <version>4.1.48.Final</version>
   </parent>
 
   <artifactId>netty-handler</artifactId>
@@ -40,6 +40,11 @@
       <artifactId>netty-common</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>netty-resolver</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>${project.groupId}</groupId>
       <artifactId>netty-buffer</artifactId>
@@ -86,6 +91,14 @@
       <groupId>org.mockito</groupId>
       <artifactId>mockito-core</artifactId>
     </dependency>
+
+    <dependency>
+      <groupId>software.amazon.cryptools</groupId>
+      <artifactId>AmazonCorrettoCryptoProvider</artifactId>
+      <version>1.1.0</version>
+      <classifier>linux-x86_64</classifier>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 </project>
 
diff --git a/handler/src/main/java/io/netty/handler/address/DynamicAddressConnectHandler.java b/handler/src/main/java/io/netty/handler/address/DynamicAddressConnectHandler.java
new file mode 100644
index 0000000..6565ffc
--- /dev/null
+++ b/handler/src/main/java/io/netty/handler/address/DynamicAddressConnectHandler.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2019 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.address;
+
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelOutboundHandler;
+import io.netty.channel.ChannelOutboundHandlerAdapter;
+import io.netty.channel.ChannelPromise;
+
+import java.net.NetworkInterface;
+import java.net.SocketAddress;
+
+/**
+ * {@link ChannelOutboundHandler} implementation which allows to dynamically replace the used
+ * {@code remoteAddress} and / or {@code localAddress} when making a connection attempt.
+ * <p>
+ * This can be useful to for example bind to a specific {@link NetworkInterface} based on
+ * the {@code remoteAddress}.
+ */
+public abstract class DynamicAddressConnectHandler extends ChannelOutboundHandlerAdapter {
+
+    @Override
+    public final void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress,
+                              SocketAddress localAddress, ChannelPromise promise) {
+        final SocketAddress remote;
+        final SocketAddress local;
+        try {
+            remote = remoteAddress(remoteAddress, localAddress);
+            local = localAddress(remoteAddress, localAddress);
+        } catch (Exception e) {
+            promise.setFailure(e);
+            return;
+        }
+        ctx.connect(remote, local, promise).addListener(new ChannelFutureListener() {
+            @Override
+            public void operationComplete(ChannelFuture future) {
+                if (future.isSuccess()) {
+                    // We only remove this handler from the pipeline once the connect was successful as otherwise
+                    // the user may try to connect again.
+                    future.channel().pipeline().remove(DynamicAddressConnectHandler.this);
+                }
+            }
+        });
+    }
+
+    /**
+     * Returns the local {@link SocketAddress} to use for
+     * {@link ChannelHandlerContext#connect(SocketAddress, SocketAddress)} based on the original {@code remoteAddress}
+     * and {@code localAddress}.
+     * By default, this method returns the given {@code localAddress}.
+     */
+    protected SocketAddress localAddress(
+            @SuppressWarnings("unused") SocketAddress remoteAddress, SocketAddress localAddress) throws Exception {
+        return localAddress;
+    }
+
+    /**
+     * Returns the remote {@link SocketAddress} to use for
+     * {@link ChannelHandlerContext#connect(SocketAddress, SocketAddress)} based on the original {@code remoteAddress}
+     * and {@code localAddress}.
+     * By default, this method returns the given {@code remoteAddress}.
+     */
+    protected SocketAddress remoteAddress(
+            SocketAddress remoteAddress, @SuppressWarnings("unused") SocketAddress localAddress) throws Exception {
+        return remoteAddress;
+    }
+}
diff --git a/handler/src/main/java/io/netty/handler/address/ResolveAddressHandler.java b/handler/src/main/java/io/netty/handler/address/ResolveAddressHandler.java
new file mode 100644
index 0000000..89f0067
--- /dev/null
+++ b/handler/src/main/java/io/netty/handler/address/ResolveAddressHandler.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2020 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.address;
+
+import io.netty.channel.ChannelHandler.Sharable;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelOutboundHandlerAdapter;
+import io.netty.channel.ChannelPromise;
+import io.netty.resolver.AddressResolver;
+import io.netty.resolver.AddressResolverGroup;
+import io.netty.util.concurrent.Future;
+import io.netty.util.concurrent.FutureListener;
+import io.netty.util.internal.ObjectUtil;
+
+import java.net.SocketAddress;
+
+/**
+ * {@link ChannelOutboundHandlerAdapter} which will resolve the {@link SocketAddress} that is passed to
+ * {@link #connect(ChannelHandlerContext, SocketAddress, SocketAddress, ChannelPromise)} if it is not already resolved
+ * and the {@link AddressResolver} supports the type of {@link SocketAddress}.
+ */
+@Sharable
+public class ResolveAddressHandler extends ChannelOutboundHandlerAdapter {
+
+    private final AddressResolverGroup<? extends SocketAddress> resolverGroup;
+
+    public ResolveAddressHandler(AddressResolverGroup<? extends SocketAddress> resolverGroup) {
+        this.resolverGroup = ObjectUtil.checkNotNull(resolverGroup, "resolverGroup");
+    }
+
+    @Override
+    public void connect(final ChannelHandlerContext ctx, SocketAddress remoteAddress,
+                        final SocketAddress localAddress, final ChannelPromise promise)  {
+        AddressResolver<? extends SocketAddress> resolver = resolverGroup.getResolver(ctx.executor());
+        if (resolver.isSupported(remoteAddress) && !resolver.isResolved(remoteAddress)) {
+            resolver.resolve(remoteAddress).addListener(new FutureListener<SocketAddress>() {
+                @Override
+                public void operationComplete(Future<SocketAddress> future) {
+                    Throwable cause = future.cause();
+                    if (cause != null) {
+                        promise.setFailure(cause);
+                    } else {
+                        ctx.connect(future.getNow(), localAddress, promise);
+                    }
+                    ctx.pipeline().remove(ResolveAddressHandler.this);
+                }
+            });
+        } else {
+            ctx.connect(remoteAddress, localAddress, promise);
+            ctx.pipeline().remove(this);
+        }
+    }
+}
diff --git a/handler/src/main/java/io/netty/handler/address/package-info.java b/handler/src/main/java/io/netty/handler/address/package-info.java
new file mode 100644
index 0000000..965faa8
--- /dev/null
+++ b/handler/src/main/java/io/netty/handler/address/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2019 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 to dynamically replace local / remote {@link java.net.SocketAddress}.
+ */
+package io.netty.handler.address;
diff --git a/handler/src/main/java/io/netty/handler/flow/FlowControlHandler.java b/handler/src/main/java/io/netty/handler/flow/FlowControlHandler.java
index 576227c..c79c36a 100644
--- a/handler/src/main/java/io/netty/handler/flow/FlowControlHandler.java
+++ b/handler/src/main/java/io/netty/handler/flow/FlowControlHandler.java
@@ -24,9 +24,10 @@ import io.netty.channel.ChannelHandler;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.handler.codec.ByteToMessageDecoder;
 import io.netty.handler.codec.MessageToByteEncoder;
-import io.netty.util.Recycler;
-import io.netty.util.Recycler.Handle;
 import io.netty.util.ReferenceCountUtil;
+import io.netty.util.internal.ObjectPool;
+import io.netty.util.internal.ObjectPool.Handle;
+import io.netty.util.internal.ObjectPool.ObjectCreator;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 
@@ -37,7 +38,7 @@ import io.netty.util.internal.logging.InternalLoggerFactory;
  * many events as they like for any given input. A channel's auto reading configuration doesn't usually
  * apply in these scenarios. This is causing problems in downstream {@link ChannelHandler}s that would
  * like to hold subsequent events while they're processing one event. It's a common problem with the
- * {@code HttpObjectDecoder} that will very often fire a {@code HttpRequest} that is immediately followed
+ * {@code HttpObjectDecoder} that will very often fire an {@code HttpRequest} that is immediately followed
  * by a {@code LastHttpContent} event.
  *
  * <pre>{@code
@@ -88,7 +89,7 @@ public class FlowControlHandler extends ChannelDuplexHandler {
      * testing, debugging and inspection purposes and it is not Thread safe!
      */
     boolean isQueueEmpty() {
-        return queue.isEmpty();
+        return queue == null || queue.isEmpty();
     }
 
     /**
@@ -154,9 +155,13 @@ public class FlowControlHandler extends ChannelDuplexHandler {
 
     @Override
     public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
-        // Don't relay completion events from upstream as they
-        // make no sense in this context. See dequeue() where
-        // a new set of completion events is being produced.
+        if (isQueueEmpty()) {
+            ctx.fireChannelReadComplete();
+        } else {
+            // Don't relay completion events from upstream as they
+            // make no sense in this context. See dequeue() where
+            // a new set of completion events is being produced.
+        }
     }
 
     /**
@@ -172,32 +177,33 @@ public class FlowControlHandler extends ChannelDuplexHandler {
      * @see #channelRead(ChannelHandlerContext, Object)
      */
     private int dequeue(ChannelHandlerContext ctx, int minConsume) {
-        if (queue != null) {
-
-            int consumed = 0;
+        int consumed = 0;
+
+        // fireChannelRead(...) may call ctx.read() and so this method may reentrance. Because of this we need to
+        // check if queue was set to null in the meantime and if so break the loop.
+        while (queue != null && (consumed < minConsume || config.isAutoRead())) {
+            Object msg = queue.poll();
+            if (msg == null) {
+                break;
+            }
 
-            Object msg;
-            while ((consumed < minConsume) || config.isAutoRead()) {
-                msg = queue.poll();
-                if (msg == null) {
-                    break;
-                }
+            ++consumed;
+            ctx.fireChannelRead(msg);
+        }
 
-                ++consumed;
-                ctx.fireChannelRead(msg);
-            }
+        // We're firing a completion event every time one (or more)
+        // messages were consumed and the queue ended up being drained
+        // to an empty state.
+        if (queue != null && queue.isEmpty()) {
+            queue.recycle();
+            queue = null;
 
-            // We're firing a completion event every time one (or more)
-            // messages were consumed and the queue ended up being drained
-            // to an empty state.
-            if (queue.isEmpty() && consumed > 0) {
+            if (consumed > 0) {
                 ctx.fireChannelReadComplete();
             }
-
-            return consumed;
         }
 
-        return 0;
+        return consumed;
     }
 
     /**
@@ -212,12 +218,13 @@ public class FlowControlHandler extends ChannelDuplexHandler {
          */
         private static final int DEFAULT_NUM_ELEMENTS = 2;
 
-        private static final Recycler<RecyclableArrayDeque> RECYCLER = new Recycler<RecyclableArrayDeque>() {
+        private static final ObjectPool<RecyclableArrayDeque> RECYCLER = ObjectPool.newPool(
+                new ObjectCreator<RecyclableArrayDeque>() {
             @Override
-            protected RecyclableArrayDeque newObject(Handle<RecyclableArrayDeque> handle) {
+            public RecyclableArrayDeque newObject(Handle<RecyclableArrayDeque> handle) {
                 return new RecyclableArrayDeque(DEFAULT_NUM_ELEMENTS, handle);
             }
-        };
+        });
 
         public static RecyclableArrayDeque newInstance() {
             return RECYCLER.get();
diff --git a/handler/src/main/java/io/netty/handler/flush/FlushConsolidationHandler.java b/handler/src/main/java/io/netty/handler/flush/FlushConsolidationHandler.java
index 472a83b..b532f81 100644
--- a/handler/src/main/java/io/netty/handler/flush/FlushConsolidationHandler.java
+++ b/handler/src/main/java/io/netty/handler/flush/FlushConsolidationHandler.java
@@ -23,6 +23,7 @@ import io.netty.channel.ChannelOutboundHandler;
 import io.netty.channel.ChannelOutboundInvoker;
 import io.netty.channel.ChannelPipeline;
 import io.netty.channel.ChannelPromise;
+import io.netty.util.internal.ObjectUtil;
 
 import java.util.concurrent.Future;
 
@@ -95,20 +96,17 @@ public class FlushConsolidationHandler extends ChannelDuplexHandler {
      *                                        ongoing.
      */
     public FlushConsolidationHandler(int explicitFlushAfterFlushes, boolean consolidateWhenNoReadInProgress) {
-        if (explicitFlushAfterFlushes <= 0) {
-            throw new IllegalArgumentException("explicitFlushAfterFlushes: "
-                    + explicitFlushAfterFlushes + " (expected: > 0)");
-        }
-        this.explicitFlushAfterFlushes = explicitFlushAfterFlushes;
+        this.explicitFlushAfterFlushes =
+                ObjectUtil.checkPositive(explicitFlushAfterFlushes, "explicitFlushAfterFlushes");
         this.consolidateWhenNoReadInProgress = consolidateWhenNoReadInProgress;
-        flushTask = consolidateWhenNoReadInProgress ?
+        this.flushTask = consolidateWhenNoReadInProgress ?
                 new Runnable() {
                     @Override
                     public void run() {
                         if (flushPendingCount > 0 && !readInProgress) {
                             flushPendingCount = 0;
-                            ctx.flush();
                             nextScheduledFlush = null;
+                            ctx.flush();
                         } // else we'll flush when the read completes
                     }
                 }
diff --git a/handler/src/main/java/io/netty/handler/ipfilter/IpSubnetFilterRule.java b/handler/src/main/java/io/netty/handler/ipfilter/IpSubnetFilterRule.java
index b6c2e83..bc6b1ff 100644
--- a/handler/src/main/java/io/netty/handler/ipfilter/IpSubnetFilterRule.java
+++ b/handler/src/main/java/io/netty/handler/ipfilter/IpSubnetFilterRule.java
@@ -15,6 +15,7 @@
  */
 package io.netty.handler.ipfilter;
 
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.SocketUtils;
 
 import java.math.BigInteger;
@@ -45,13 +46,8 @@ public final class IpSubnetFilterRule implements IpFilterRule {
     }
 
     private static IpFilterRule selectFilterRule(InetAddress ipAddress, int cidrPrefix, IpFilterRuleType ruleType) {
-        if (ipAddress == null) {
-            throw new NullPointerException("ipAddress");
-        }
-
-        if (ruleType == null) {
-            throw new NullPointerException("ruleType");
-        }
+        ObjectUtil.checkNotNull(ipAddress, "ipAddress");
+        ObjectUtil.checkNotNull(ruleType, "ruleType");
 
         if (ipAddress instanceof Inet4Address) {
             return new Ip4SubnetFilterRule((Inet4Address) ipAddress, cidrPrefix, ruleType);
diff --git a/handler/src/main/java/io/netty/handler/ipfilter/RuleBasedIpFilter.java b/handler/src/main/java/io/netty/handler/ipfilter/RuleBasedIpFilter.java
index 66ffa45..8d2b2f1 100644
--- a/handler/src/main/java/io/netty/handler/ipfilter/RuleBasedIpFilter.java
+++ b/handler/src/main/java/io/netty/handler/ipfilter/RuleBasedIpFilter.java
@@ -18,6 +18,7 @@ package io.netty.handler.ipfilter;
 import io.netty.channel.Channel;
 import io.netty.channel.ChannelHandler.Sharable;
 import io.netty.channel.ChannelHandlerContext;
+import io.netty.util.internal.ObjectUtil;
 
 import java.net.InetSocketAddress;
 import java.net.SocketAddress;
@@ -36,11 +37,7 @@ public class RuleBasedIpFilter extends AbstractRemoteAddressFilter<InetSocketAdd
     private final IpFilterRule[] rules;
 
     public RuleBasedIpFilter(IpFilterRule... rules) {
-        if (rules == null) {
-            throw new NullPointerException("rules");
-        }
-
-        this.rules = rules;
+        this.rules = ObjectUtil.checkNotNull(rules, "rules");
     }
 
     @Override
diff --git a/handler/src/main/java/io/netty/handler/logging/ByteBufFormat.java b/handler/src/main/java/io/netty/handler/logging/ByteBufFormat.java
new file mode 100644
index 0000000..c643afd
--- /dev/null
+++ b/handler/src/main/java/io/netty/handler/logging/ByteBufFormat.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2020 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.logging;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufHolder;
+import io.netty.buffer.ByteBufUtil;
+
+/**
+ * Used to control the format and verbosity of logging for {@link ByteBuf}s and {@link ByteBufHolder}s.
+ *
+ * @see LoggingHandler
+ */
+public enum ByteBufFormat {
+    /**
+     * {@link ByteBuf}s will be logged in a simple format, with no hex dump included.
+     */
+    SIMPLE,
+    /**
+     * {@link ByteBuf}s will be logged using {@link ByteBufUtil#appendPrettyHexDump(StringBuilder, ByteBuf)}.
+     */
+    HEX_DUMP
+}
diff --git a/handler/src/main/java/io/netty/handler/logging/LoggingHandler.java b/handler/src/main/java/io/netty/handler/logging/LoggingHandler.java
index 3573fd3..5a84e9f 100644
--- a/handler/src/main/java/io/netty/handler/logging/LoggingHandler.java
+++ b/handler/src/main/java/io/netty/handler/logging/LoggingHandler.java
@@ -23,6 +23,7 @@ import io.netty.channel.ChannelHandler.Sharable;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelOutboundHandler;
 import io.netty.channel.ChannelPromise;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.logging.InternalLogLevel;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
@@ -34,7 +35,7 @@ import static io.netty.util.internal.StringUtil.NEWLINE;
 
 /**
  * A {@link ChannelHandler} that logs all events using a logging framework.
- * By default, all events are logged at <tt>DEBUG</tt> level.
+ * By default, all events are logged at <tt>DEBUG</tt> level and full hex dumps are recorded for ByteBufs.
  */
 @Sharable
 @SuppressWarnings({ "StringConcatenationInsideStringBufferAppend", "StringBufferReplaceableByString" })
@@ -46,6 +47,7 @@ public class LoggingHandler extends ChannelDuplexHandler {
     protected final InternalLogLevel internalLevel;
 
     private final LogLevel level;
+    private final ByteBufFormat byteBufFormat;
 
     /**
      * Creates a new instance whose logger name is the fully qualified class
@@ -62,12 +64,20 @@ public class LoggingHandler extends ChannelDuplexHandler {
      * @param level the log level
      */
     public LoggingHandler(LogLevel level) {
-        if (level == null) {
-            throw new NullPointerException("level");
-        }
+        this(level, ByteBufFormat.HEX_DUMP);
+    }
 
+    /**
+     * Creates a new instance whose logger name is the fully qualified class
+     * name of the instance.
+     *
+     * @param level the log level
+     * @param byteBufFormat the ByteBuf format
+     */
+    public LoggingHandler(LogLevel level, ByteBufFormat byteBufFormat) {
+        this.level = ObjectUtil.checkNotNull(level, "level");
+        this.byteBufFormat = ObjectUtil.checkNotNull(byteBufFormat, "byteBufFormat");
         logger = InternalLoggerFactory.getInstance(getClass());
-        this.level = level;
         internalLevel = level.toInternalLevel();
     }
 
@@ -88,15 +98,21 @@ public class LoggingHandler extends ChannelDuplexHandler {
      * @param level the log level
      */
     public LoggingHandler(Class<?> clazz, LogLevel level) {
-        if (clazz == null) {
-            throw new NullPointerException("clazz");
-        }
-        if (level == null) {
-            throw new NullPointerException("level");
-        }
+        this(clazz, level, ByteBufFormat.HEX_DUMP);
+    }
 
+    /**
+     * Creates a new instance with the specified logger name.
+     *
+     * @param clazz the class type to generate the logger for
+     * @param level the log level
+     * @param byteBufFormat the ByteBuf format
+     */
+    public LoggingHandler(Class<?> clazz, LogLevel level, ByteBufFormat byteBufFormat) {
+        ObjectUtil.checkNotNull(clazz, "clazz");
+        this.level = ObjectUtil.checkNotNull(level, "level");
+        this.byteBufFormat = ObjectUtil.checkNotNull(byteBufFormat, "byteBufFormat");
         logger = InternalLoggerFactory.getInstance(clazz);
-        this.level = level;
         internalLevel = level.toInternalLevel();
     }
 
@@ -116,15 +132,22 @@ public class LoggingHandler extends ChannelDuplexHandler {
      * @param level the log level
      */
     public LoggingHandler(String name, LogLevel level) {
-        if (name == null) {
-            throw new NullPointerException("name");
-        }
-        if (level == null) {
-            throw new NullPointerException("level");
-        }
+        this(name, level, ByteBufFormat.HEX_DUMP);
+    }
+
+    /**
+     * Creates a new instance with the specified logger name.
+     *
+     * @param name the name of the class to use for the logger
+     * @param level the log level
+     * @param byteBufFormat the ByteBuf format
+     */
+    public LoggingHandler(String name, LogLevel level, ByteBufFormat byteBufFormat) {
+        ObjectUtil.checkNotNull(name, "name");
 
+        this.level = ObjectUtil.checkNotNull(level, "level");
+        this.byteBufFormat = ObjectUtil.checkNotNull(byteBufFormat, "byteBufFormat");
         logger = InternalLoggerFactory.getInstance(name);
-        this.level = level;
         internalLevel = level.toInternalLevel();
     }
 
@@ -135,6 +158,13 @@ public class LoggingHandler extends ChannelDuplexHandler {
         return level;
     }
 
+    /**
+     * Returns the {@link ByteBufFormat} that this handler uses to log
+     */
+    public ByteBufFormat byteBufFormat() {
+        return byteBufFormat;
+    }
+
     @Override
     public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
         if (logger.isEnabled(internalLevel)) {
@@ -320,7 +350,7 @@ public class LoggingHandler extends ChannelDuplexHandler {
     /**
      * Generates the default log message of the specified event whose argument is a {@link ByteBuf}.
      */
-    private static String formatByteBuf(ChannelHandlerContext ctx, String eventName, ByteBuf msg) {
+    private String formatByteBuf(ChannelHandlerContext ctx, String eventName, ByteBuf msg) {
         String chStr = ctx.channel().toString();
         int length = msg.readableBytes();
         if (length == 0) {
@@ -328,11 +358,18 @@ public class LoggingHandler extends ChannelDuplexHandler {
             buf.append(chStr).append(' ').append(eventName).append(": 0B");
             return buf.toString();
         } else {
-            int rows = length / 16 + (length % 15 == 0? 0 : 1) + 4;
-            StringBuilder buf = new StringBuilder(chStr.length() + 1 + eventName.length() + 2 + 10 + 1 + 2 + rows * 80);
-
-            buf.append(chStr).append(' ').append(eventName).append(": ").append(length).append('B').append(NEWLINE);
-            appendPrettyHexDump(buf, msg);
+            int outputLength = chStr.length() + 1 + eventName.length() + 2 + 10 + 1;
+            if (byteBufFormat == ByteBufFormat.HEX_DUMP) {
+                int rows = length / 16 + (length % 15 == 0? 0 : 1) + 4;
+                int hexDumpLength = 2 + rows * 80;
+                outputLength += hexDumpLength;
+            }
+            StringBuilder buf = new StringBuilder(outputLength);
+            buf.append(chStr).append(' ').append(eventName).append(": ").append(length).append('B');
+            if (byteBufFormat == ByteBufFormat.HEX_DUMP) {
+                buf.append(NEWLINE);
+                appendPrettyHexDump(buf, msg);
+            }
 
             return buf.toString();
         }
@@ -341,7 +378,7 @@ public class LoggingHandler extends ChannelDuplexHandler {
     /**
      * Generates the default log message of the specified event whose argument is a {@link ByteBufHolder}.
      */
-    private static String formatByteBufHolder(ChannelHandlerContext ctx, String eventName, ByteBufHolder msg) {
+    private String formatByteBufHolder(ChannelHandlerContext ctx, String eventName, ByteBufHolder msg) {
         String chStr = ctx.channel().toString();
         String msgStr = msg.toString();
         ByteBuf content = msg.content();
@@ -351,13 +388,19 @@ public class LoggingHandler extends ChannelDuplexHandler {
             buf.append(chStr).append(' ').append(eventName).append(", ").append(msgStr).append(", 0B");
             return buf.toString();
         } else {
-            int rows = length / 16 + (length % 15 == 0? 0 : 1) + 4;
-            StringBuilder buf = new StringBuilder(
-                    chStr.length() + 1 + eventName.length() + 2 + msgStr.length() + 2 + 10 + 1 + 2 + rows * 80);
-
+            int outputLength = chStr.length() + 1 + eventName.length() + 2 + msgStr.length() + 2 + 10 + 1;
+            if (byteBufFormat == ByteBufFormat.HEX_DUMP) {
+                int rows = length / 16 + (length % 15 == 0? 0 : 1) + 4;
+                int hexDumpLength = 2 + rows * 80;
+                outputLength += hexDumpLength;
+            }
+            StringBuilder buf = new StringBuilder(outputLength);
             buf.append(chStr).append(' ').append(eventName).append(": ")
-               .append(msgStr).append(", ").append(length).append('B').append(NEWLINE);
-            appendPrettyHexDump(buf, content);
+               .append(msgStr).append(", ").append(length).append('B');
+            if (byteBufFormat == ByteBufFormat.HEX_DUMP) {
+                buf.append(NEWLINE);
+                appendPrettyHexDump(buf, content);
+            }
 
             return buf.toString();
         }
diff --git a/handler/src/main/java/io/netty/handler/ssl/AbstractSniHandler.java b/handler/src/main/java/io/netty/handler/ssl/AbstractSniHandler.java
index 05fcaff..dcba585 100644
--- a/handler/src/main/java/io/netty/handler/ssl/AbstractSniHandler.java
+++ b/handler/src/main/java/io/netty/handler/ssl/AbstractSniHandler.java
@@ -16,21 +16,10 @@
 package io.netty.handler.ssl;
 
 import io.netty.buffer.ByteBuf;
-import io.netty.buffer.ByteBufUtil;
 import io.netty.channel.ChannelHandlerContext;
-import io.netty.channel.ChannelOutboundHandler;
-import io.netty.channel.ChannelPromise;
-import io.netty.handler.codec.ByteToMessageDecoder;
-import io.netty.handler.codec.DecoderException;
 import io.netty.util.CharsetUtil;
 import io.netty.util.concurrent.Future;
-import io.netty.util.concurrent.FutureListener;
-import io.netty.util.internal.PlatformDependent;
-import io.netty.util.internal.logging.InternalLogger;
-import io.netty.util.internal.logging.InternalLoggerFactory;
 
-import java.net.SocketAddress;
-import java.util.List;
 import java.util.Locale;
 
 /**
@@ -40,228 +29,107 @@ import java.util.Locale;
  * The client will send host name in the handshake data so server could decide
  * which certificate to choose for the host name.</p>
  */
-public abstract class AbstractSniHandler<T> extends ByteToMessageDecoder implements ChannelOutboundHandler {
-
-    // Maximal number of ssl records to inspect before fallback to the default SslContext.
-    private static final int MAX_SSL_RECORDS = 4;
-
-    private static final InternalLogger logger =
-            InternalLoggerFactory.getInstance(AbstractSniHandler.class);
-
-    private boolean handshakeFailed;
-    private boolean suppressRead;
-    private boolean readPending;
-
-    @Override
-    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
-        if (!suppressRead && !handshakeFailed) {
-            final int writerIndex = in.writerIndex();
-            try {
-                loop:
-                for (int i = 0; i < MAX_SSL_RECORDS; i++) {
-                    final int readerIndex = in.readerIndex();
-                    final int readableBytes = writerIndex - readerIndex;
-                    if (readableBytes < SslUtils.SSL_RECORD_HEADER_LENGTH) {
-                        // Not enough data to determine the record type and length.
-                        return;
+public abstract class AbstractSniHandler<T> extends SslClientHelloHandler<T> {
+
+    private static String extractSniHostname(ByteBuf in) {
+        // See https://tools.ietf.org/html/rfc5246#section-7.4.1.2
+        //
+        // Decode the ssl client hello packet.
+        //
+        // struct {
+        //    ProtocolVersion client_version;
+        //    Random random;
+        //    SessionID session_id;
+        //    CipherSuite cipher_suites<2..2^16-2>;
+        //    CompressionMethod compression_methods<1..2^8-1>;
+        //    select (extensions_present) {
+        //        case false:
+        //            struct {};
+        //        case true:
+        //            Extension extensions<0..2^16-1>;
+        //    };
+        // } ClientHello;
+        //
+
+        // We have to skip bytes until SessionID (which sum to 34 bytes in this case).
+        int offset = in.readerIndex();
+        int endOffset = in.writerIndex();
+        offset += 34;
+
+        if (endOffset - offset >= 6) {
+            final int sessionIdLength = in.getUnsignedByte(offset);
+            offset += sessionIdLength + 1;
+
+            final int cipherSuitesLength = in.getUnsignedShort(offset);
+            offset += cipherSuitesLength + 2;
+
+            final int compressionMethodLength = in.getUnsignedByte(offset);
+            offset += compressionMethodLength + 1;
+
+            final int extensionsLength = in.getUnsignedShort(offset);
+            offset += 2;
+            final int extensionsLimit = offset + extensionsLength;
+
+            // Extensions should never exceed the record boundary.
+            if (extensionsLimit <= endOffset) {
+                while (extensionsLimit - offset >= 4) {
+                    final int extensionType = in.getUnsignedShort(offset);
+                    offset += 2;
+
+                    final int extensionLength = in.getUnsignedShort(offset);
+                    offset += 2;
+
+                    if (extensionsLimit - offset < extensionLength) {
+                        break;
                     }
 
-                    final int command = in.getUnsignedByte(readerIndex);
-
-                    // tls, but not handshake command
-                    switch (command) {
-                        case SslUtils.SSL_CONTENT_TYPE_CHANGE_CIPHER_SPEC:
-                        case SslUtils.SSL_CONTENT_TYPE_ALERT:
-                            final int len = SslUtils.getEncryptedPacketLength(in, readerIndex);
-
-                            // Not an SSL/TLS packet
-                            if (len == SslUtils.NOT_ENCRYPTED) {
-                                handshakeFailed = true;
-                                NotSslRecordException e = new NotSslRecordException(
-                                        "not an SSL/TLS record: " + ByteBufUtil.hexDump(in));
-                                in.skipBytes(in.readableBytes());
-                                ctx.fireUserEventTriggered(new SniCompletionEvent(e));
-                                SslUtils.handleHandshakeFailure(ctx, e, true);
-                                throw e;
-                            }
-                            if (len == SslUtils.NOT_ENOUGH_DATA ||
-                                    writerIndex - readerIndex - SslUtils.SSL_RECORD_HEADER_LENGTH < len) {
-                                // Not enough data
-                                return;
-                            }
-                            // increase readerIndex and try again.
-                            in.skipBytes(len);
-                            continue;
-                        case SslUtils.SSL_CONTENT_TYPE_HANDSHAKE:
-                            final int majorVersion = in.getUnsignedByte(readerIndex + 1);
-
-                            // SSLv3 or TLS
-                            if (majorVersion == 3) {
-                                final int packetLength = in.getUnsignedShort(readerIndex + 3) +
-                                        SslUtils.SSL_RECORD_HEADER_LENGTH;
-
-                                if (readableBytes < packetLength) {
-                                    // client hello incomplete; try again to decode once more data is ready.
-                                    return;
-                                }
-
-                                // See https://tools.ietf.org/html/rfc5246#section-7.4.1.2
-                                //
-                                // Decode the ssl client hello packet.
-                                // We have to skip bytes until SessionID (which sum to 43 bytes).
-                                //
-                                // struct {
-                                //    ProtocolVersion client_version;
-                                //    Random random;
-                                //    SessionID session_id;
-                                //    CipherSuite cipher_suites<2..2^16-2>;
-                                //    CompressionMethod compression_methods<1..2^8-1>;
-                                //    select (extensions_present) {
-                                //        case false:
-                                //            struct {};
-                                //        case true:
-                                //            Extension extensions<0..2^16-1>;
-                                //    };
-                                // } ClientHello;
-                                //
-
-                                final int endOffset = readerIndex + packetLength;
-                                int offset = readerIndex + 43;
-
-                                if (endOffset - offset < 6) {
-                                    break loop;
-                                }
-
-                                final int sessionIdLength = in.getUnsignedByte(offset);
-                                offset += sessionIdLength + 1;
-
-                                final int cipherSuitesLength = in.getUnsignedShort(offset);
-                                offset += cipherSuitesLength + 2;
-
-                                final int compressionMethodLength = in.getUnsignedByte(offset);
-                                offset += compressionMethodLength + 1;
-
-                                final int extensionsLength = in.getUnsignedShort(offset);
-                                offset += 2;
-                                final int extensionsLimit = offset + extensionsLength;
-
-                                if (extensionsLimit > endOffset) {
-                                    // Extensions should never exceed the record boundary.
-                                    break loop;
-                                }
-
-                                for (;;) {
-                                    if (extensionsLimit - offset < 4) {
-                                        break loop;
-                                    }
-
-                                    final int extensionType = in.getUnsignedShort(offset);
-                                    offset += 2;
-
-                                    final int extensionLength = in.getUnsignedShort(offset);
-                                    offset += 2;
-
-                                    if (extensionsLimit - offset < extensionLength) {
-                                        break loop;
-                                    }
-
-                                    // SNI
-                                    // See https://tools.ietf.org/html/rfc6066#page-6
-                                    if (extensionType == 0) {
-                                        offset += 2;
-                                        if (extensionsLimit - offset < 3) {
-                                            break loop;
-                                        }
-
-                                        final int serverNameType = in.getUnsignedByte(offset);
-                                        offset++;
-
-                                        if (serverNameType == 0) {
-                                            final int serverNameLength = in.getUnsignedShort(offset);
-                                            offset += 2;
-
-                                            if (extensionsLimit - offset < serverNameLength) {
-                                                break loop;
-                                            }
+                    // SNI
+                    // See https://tools.ietf.org/html/rfc6066#page-6
+                    if (extensionType == 0) {
+                        offset += 2;
+                        if (extensionsLimit - offset < 3) {
+                            break;
+                        }
 
-                                            final String hostname = in.toString(offset, serverNameLength,
-                                                    CharsetUtil.US_ASCII);
+                        final int serverNameType = in.getUnsignedByte(offset);
+                        offset++;
 
-                                            try {
-                                                select(ctx, hostname.toLowerCase(Locale.US));
-                                            } catch (Throwable t) {
-                                                PlatformDependent.throwException(t);
-                                            }
-                                            return;
-                                        } else {
-                                            // invalid enum value
-                                            break loop;
-                                        }
-                                    }
+                        if (serverNameType == 0) {
+                            final int serverNameLength = in.getUnsignedShort(offset);
+                            offset += 2;
 
-                                    offset += extensionLength;
-                                }
+                            if (extensionsLimit - offset < serverNameLength) {
+                                break;
                             }
-                            // Fall-through
-                        default:
-                            //not tls, ssl or application data, do not try sni
-                            break loop;
+
+                            final String hostname = in.toString(offset, serverNameLength, CharsetUtil.US_ASCII);
+                            return hostname.toLowerCase(Locale.US);
+                        } else {
+                            // invalid enum value
+                            break;
+                        }
                     }
-                }
-            } catch (NotSslRecordException e) {
-                // Just rethrow as in this case we also closed the channel and this is consistent with SslHandler.
-                throw e;
-            } catch (Exception e) {
-                // unexpected encoding, ignore sni and use default
-                if (logger.isDebugEnabled()) {
-                    logger.debug("Unexpected client hello packet: " + ByteBufUtil.hexDump(in), e);
+
+                    offset += extensionLength;
                 }
             }
-            // Just select the default SslContext
-            select(ctx, null);
         }
+        return null;
     }
 
-    private void select(final ChannelHandlerContext ctx, final String hostname) throws Exception {
-        Future<T> future = lookup(ctx, hostname);
-        if (future.isDone()) {
-            fireSniCompletionEvent(ctx, hostname, future);
-            onLookupComplete(ctx, hostname, future);
-        } else {
-            suppressRead = true;
-            future.addListener(new FutureListener<T>() {
-                @Override
-                public void operationComplete(Future<T> future) throws Exception {
-                    try {
-                        suppressRead = false;
-                        try {
-                            fireSniCompletionEvent(ctx, hostname, future);
-                            onLookupComplete(ctx, hostname, future);
-                        } catch (DecoderException err) {
-                            ctx.fireExceptionCaught(err);
-                        } catch (Exception cause) {
-                            ctx.fireExceptionCaught(new DecoderException(cause));
-                        } catch (Throwable cause) {
-                            ctx.fireExceptionCaught(cause);
-                        }
-                    } finally {
-                        if (readPending) {
-                            readPending = false;
-                            ctx.read();
-                        }
-                    }
-                }
-            });
-        }
+    private String hostname;
+
+    @Override
+    protected Future<T> lookup(ChannelHandlerContext ctx, ByteBuf clientHello) throws Exception {
+        hostname = clientHello == null ? null : extractSniHostname(clientHello);
+
+        return lookup(ctx, hostname);
     }
 
-    private void fireSniCompletionEvent(ChannelHandlerContext ctx, String hostname, Future<T> future) {
-        Throwable cause = future.cause();
-        if (cause == null) {
-            ctx.fireUserEventTriggered(new SniCompletionEvent(hostname));
-        } else {
-            ctx.fireUserEventTriggered(new SniCompletionEvent(hostname, cause));
-        }
+    @Override
+    protected void onLookupComplete(ChannelHandlerContext ctx, Future<T> future) throws Exception {
+        fireSniCompletionEvent(ctx, hostname, future);
+        onLookupComplete(ctx, hostname, future);
     }
 
     /**
@@ -280,48 +148,12 @@ public abstract class AbstractSniHandler<T> extends ByteToMessageDecoder impleme
     protected abstract void onLookupComplete(ChannelHandlerContext ctx,
                                              String hostname, Future<T> future) throws Exception;
 
-    @Override
-    public void read(ChannelHandlerContext ctx) throws Exception {
-        if (suppressRead) {
-            readPending = true;
+    private void fireSniCompletionEvent(ChannelHandlerContext ctx, String hostname, Future<T> future) {
+        Throwable cause = future.cause();
+        if (cause == null) {
+            ctx.fireUserEventTriggered(new SniCompletionEvent(hostname));
         } else {
-            ctx.read();
+            ctx.fireUserEventTriggered(new SniCompletionEvent(hostname, cause));
         }
     }
-
-    @Override
-    public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) throws Exception {
-        ctx.bind(localAddress, promise);
-    }
-
-    @Override
-    public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress,
-                        ChannelPromise promise) throws Exception {
-        ctx.connect(remoteAddress, localAddress, promise);
-    }
-
-    @Override
-    public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
-        ctx.disconnect(promise);
-    }
-
-    @Override
-    public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
-        ctx.close(promise);
-    }
-
-    @Override
-    public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
-        ctx.deregister(promise);
-    }
-
-    @Override
-    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
-        ctx.write(msg, promise);
-    }
-
-    @Override
-    public void flush(ChannelHandlerContext ctx) throws Exception {
-        ctx.flush();
-    }
 }
diff --git a/handler/src/main/java/io/netty/handler/ssl/ApplicationProtocolNegotiationHandler.java b/handler/src/main/java/io/netty/handler/ssl/ApplicationProtocolNegotiationHandler.java
index 0e3ea0f..012463e 100644
--- a/handler/src/main/java/io/netty/handler/ssl/ApplicationProtocolNegotiationHandler.java
+++ b/handler/src/main/java/io/netty/handler/ssl/ApplicationProtocolNegotiationHandler.java
@@ -79,22 +79,29 @@ public abstract class ApplicationProtocolNegotiationHandler extends ChannelInbou
     @Override
     public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
         if (evt instanceof SslHandshakeCompletionEvent) {
-            ctx.pipeline().remove(this);
 
-            SslHandshakeCompletionEvent handshakeEvent = (SslHandshakeCompletionEvent) evt;
-            if (handshakeEvent.isSuccess()) {
-                SslHandler sslHandler = ctx.pipeline().get(SslHandler.class);
-                if (sslHandler == null) {
-                    throw new IllegalStateException("cannot find a SslHandler in the pipeline (required for " +
-                                                    "application-level protocol negotiation)");
+            try {
+                SslHandshakeCompletionEvent handshakeEvent = (SslHandshakeCompletionEvent) evt;
+                if (handshakeEvent.isSuccess()) {
+                    SslHandler sslHandler = ctx.pipeline().get(SslHandler.class);
+                    if (sslHandler == null) {
+                        throw new IllegalStateException("cannot find an SslHandler in the pipeline (required for "
+                                + "application-level protocol negotiation)");
+                    }
+                    String protocol = sslHandler.applicationProtocol();
+                    configurePipeline(ctx, protocol != null ? protocol : fallbackProtocol);
+                } else {
+                    handshakeFailure(ctx, handshakeEvent.cause());
+                }
+            } catch (Throwable cause) {
+                exceptionCaught(ctx, cause);
+            } finally {
+                ChannelPipeline pipeline = ctx.pipeline();
+                if (pipeline.context(this) != null) {
+                    pipeline.remove(this);
                 }
-                String protocol = sslHandler.applicationProtocol();
-                configurePipeline(ctx, protocol != null? protocol : fallbackProtocol);
-            } else {
-                handshakeFailure(ctx, handshakeEvent.cause());
             }
         }
-
         ctx.fireUserEventTriggered(evt);
     }
 
@@ -119,6 +126,7 @@ public abstract class ApplicationProtocolNegotiationHandler extends ChannelInbou
     @Override
     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
         logger.warn("{} Failed to select the application-level protocol:", ctx.channel(), cause);
+        ctx.fireExceptionCaught(cause);
         ctx.close();
     }
 }
diff --git a/handler/src/main/java/io/netty/handler/ssl/Conscrypt.java b/handler/src/main/java/io/netty/handler/ssl/Conscrypt.java
index 4d7ec05..9c9e2bb 100644
--- a/handler/src/main/java/io/netty/handler/ssl/Conscrypt.java
+++ b/handler/src/main/java/io/netty/handler/ssl/Conscrypt.java
@@ -28,6 +28,7 @@ final class Conscrypt {
     // This class exists to avoid loading other conscrypt related classes using features only available in JDK8+,
     // because we need to maintain JDK6+ runtime compatibility.
     private static final Method IS_CONSCRYPT_SSLENGINE = loadIsConscryptEngine();
+    private static final boolean CAN_INSTANCE_PROVIDER = canInstanceProvider();
 
     private static Method loadIsConscryptEngine() {
         try {
@@ -40,11 +41,22 @@ final class Conscrypt {
         }
     }
 
+    private static boolean canInstanceProvider() {
+        try {
+            Class<?> providerClass = Class.forName("org.conscrypt.OpenSSLProvider", true,
+                    ConscryptAlpnSslEngine.class.getClassLoader());
+            providerClass.newInstance();
+            return true;
+        } catch (Throwable ignore) {
+            return false;
+        }
+    }
+
     /**
      * Indicates whether or not conscrypt is available on the current system.
      */
     static boolean isAvailable() {
-        return IS_CONSCRYPT_SSLENGINE != null && PlatformDependent.javaVersion() >= 8;
+        return CAN_INSTANCE_PROVIDER && IS_CONSCRYPT_SSLENGINE != null && PlatformDependent.javaVersion() >= 8;
     }
 
     static boolean isEngineSupported(SSLEngine engine) {
diff --git a/handler/src/main/java/io/netty/handler/ssl/DelegatingSslContext.java b/handler/src/main/java/io/netty/handler/ssl/DelegatingSslContext.java
index af02798..c79da85 100644
--- a/handler/src/main/java/io/netty/handler/ssl/DelegatingSslContext.java
+++ b/handler/src/main/java/io/netty/handler/ssl/DelegatingSslContext.java
@@ -21,6 +21,7 @@ import io.netty.util.internal.ObjectUtil;
 import javax.net.ssl.SSLEngine;
 import javax.net.ssl.SSLSessionContext;
 import java.util.List;
+import java.util.concurrent.Executor;
 
 /**
  * Adapter class which allows to wrap another {@link SslContext} and init {@link SSLEngine} instances.
@@ -86,6 +87,21 @@ public abstract class DelegatingSslContext extends SslContext {
         return handler;
     }
 
+    @Override
+    protected SslHandler newHandler(ByteBufAllocator alloc, boolean startTls, Executor executor) {
+        SslHandler handler = ctx.newHandler(alloc, startTls, executor);
+        initHandler(handler);
+        return handler;
+    }
+
+    @Override
+    protected SslHandler newHandler(ByteBufAllocator alloc, String peerHost, int peerPort,
+                                    boolean startTls, Executor executor) {
+        SslHandler handler = ctx.newHandler(alloc, peerHost, peerPort, startTls, executor);
+        initHandler(handler);
+        return handler;
+    }
+
     @Override
     public final SSLSessionContext sessionContext() {
         return ctx.sessionContext();
diff --git a/handler/src/main/java/io/netty/handler/ssl/ExtendedOpenSslSession.java b/handler/src/main/java/io/netty/handler/ssl/ExtendedOpenSslSession.java
index 184845a..879914c 100644
--- a/handler/src/main/java/io/netty/handler/ssl/ExtendedOpenSslSession.java
+++ b/handler/src/main/java/io/netty/handler/ssl/ExtendedOpenSslSession.java
@@ -15,6 +15,8 @@
  */
 package io.netty.handler.ssl;
 
+import io.netty.util.internal.SuppressJava6Requirement;
+
 import javax.net.ssl.ExtendedSSLSession;
 import javax.net.ssl.SSLException;
 import javax.net.ssl.SSLPeerUnverifiedException;
@@ -29,6 +31,7 @@ import java.util.List;
  * Delegates all operations to a wrapped {@link OpenSslSession} except the methods defined by {@link ExtendedSSLSession}
  * itself.
  */
+@SuppressJava6Requirement(reason = "Usage guarded by java version check")
 abstract class ExtendedOpenSslSession extends ExtendedSSLSession implements OpenSslSession {
 
     // TODO: use OpenSSL API to actually fetch the real data but for now just do what Conscrypt does:
@@ -46,6 +49,7 @@ abstract class ExtendedOpenSslSession extends ExtendedSSLSession implements Open
     }
 
     // Use rawtypes an unchecked override to be able to also work on java7.
+    @Override
     @SuppressWarnings({ "unchecked", "rawtypes" })
     public abstract List getRequestedServerNames();
 
diff --git a/handler/src/main/java/io/netty/handler/ssl/Java7SslParametersUtils.java b/handler/src/main/java/io/netty/handler/ssl/Java7SslParametersUtils.java
index 2255871..ed99802 100644
--- a/handler/src/main/java/io/netty/handler/ssl/Java7SslParametersUtils.java
+++ b/handler/src/main/java/io/netty/handler/ssl/Java7SslParametersUtils.java
@@ -15,6 +15,8 @@
  */
 package io.netty.handler.ssl;
 
+import io.netty.util.internal.SuppressJava6Requirement;
+
 import javax.net.ssl.SSLParameters;
 import java.security.AlgorithmConstraints;
 
@@ -29,6 +31,7 @@ final class Java7SslParametersUtils {
      * {@link AlgorithmConstraints} in the code. This helps us to not get into trouble when using it in java
      * version < 7 and especially when using on android.
      */
+    @SuppressJava6Requirement(reason = "Usage guarded by java version check")
     static void setAlgorithmConstraints(SSLParameters sslParameters, Object algorithmConstraints) {
         sslParameters.setAlgorithmConstraints((AlgorithmConstraints) algorithmConstraints);
     }
diff --git a/handler/src/main/java/io/netty/handler/ssl/Java8SslUtils.java b/handler/src/main/java/io/netty/handler/ssl/Java8SslUtils.java
index c47d965..0a5fbc7 100644
--- a/handler/src/main/java/io/netty/handler/ssl/Java8SslUtils.java
+++ b/handler/src/main/java/io/netty/handler/ssl/Java8SslUtils.java
@@ -15,6 +15,8 @@
  */
 package io.netty.handler.ssl;
 
+import io.netty.util.internal.SuppressJava6Requirement;
+
 import javax.net.ssl.SNIHostName;
 import javax.net.ssl.SNIMatcher;
 import javax.net.ssl.SNIServerName;
@@ -25,6 +27,7 @@ import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 
+@SuppressJava6Requirement(reason = "Usage guarded by java version check")
 final class Java8SslUtils {
 
     private Java8SslUtils() { }
diff --git a/handler/src/main/java/io/netty/handler/ssl/Java9SslEngine.java b/handler/src/main/java/io/netty/handler/ssl/Java9SslEngine.java
index 5910855..4851522 100644
--- a/handler/src/main/java/io/netty/handler/ssl/Java9SslEngine.java
+++ b/handler/src/main/java/io/netty/handler/ssl/Java9SslEngine.java
@@ -16,6 +16,7 @@
 package io.netty.handler.ssl;
 
 import io.netty.util.internal.StringUtil;
+import io.netty.util.internal.SuppressJava6Requirement;
 
 import javax.net.ssl.SSLEngine;
 import javax.net.ssl.SSLEngineResult;
@@ -30,6 +31,7 @@ import static io.netty.handler.ssl.SslUtils.toSSLHandshakeException;
 import static io.netty.handler.ssl.JdkApplicationProtocolNegotiator.ProtocolSelectionListener;
 import static io.netty.handler.ssl.JdkApplicationProtocolNegotiator.ProtocolSelector;
 
+@SuppressJava6Requirement(reason = "Usage guarded by java version check")
 final class Java9SslEngine extends JdkSslEngine {
     private final ProtocolSelectionListener selectionListener;
     private final AlpnSelector alpnSelector;
diff --git a/handler/src/main/java/io/netty/handler/ssl/Java9SslUtils.java b/handler/src/main/java/io/netty/handler/ssl/Java9SslUtils.java
index 2fd3bbc..b5886c1 100644
--- a/handler/src/main/java/io/netty/handler/ssl/Java9SslUtils.java
+++ b/handler/src/main/java/io/netty/handler/ssl/Java9SslUtils.java
@@ -27,9 +27,11 @@ import java.util.function.BiFunction;
 
 import io.netty.util.internal.EmptyArrays;
 import io.netty.util.internal.PlatformDependent;
+import io.netty.util.internal.SuppressJava6Requirement;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 
+@SuppressJava6Requirement(reason = "Usage guarded by java version check")
 final class Java9SslUtils {
     private static final InternalLogger logger = InternalLoggerFactory.getInstance(Java9SslUtils.class);
     private static final Method SET_APPLICATION_PROTOCOLS;
diff --git a/handler/src/main/java/io/netty/handler/ssl/JdkAlpnApplicationProtocolNegotiator.java b/handler/src/main/java/io/netty/handler/ssl/JdkAlpnApplicationProtocolNegotiator.java
index 25f7ac8..7b9cbfb 100644
--- a/handler/src/main/java/io/netty/handler/ssl/JdkAlpnApplicationProtocolNegotiator.java
+++ b/handler/src/main/java/io/netty/handler/ssl/JdkAlpnApplicationProtocolNegotiator.java
@@ -148,4 +148,8 @@ public final class JdkAlpnApplicationProtocolNegotiator extends JdkBaseApplicati
     static boolean jdkAlpnSupported() {
         return PlatformDependent.javaVersion() >= 9 && Java9SslUtils.supportsAlpn();
     }
+
+    static boolean isAlpnSupported() {
+        return AVAILABLE;
+    }
 }
diff --git a/handler/src/main/java/io/netty/handler/ssl/JdkSslClientContext.java b/handler/src/main/java/io/netty/handler/ssl/JdkSslClientContext.java
index 6ed131e..35b5325 100644
--- a/handler/src/main/java/io/netty/handler/ssl/JdkSslClientContext.java
+++ b/handler/src/main/java/io/netty/handler/ssl/JdkSslClientContext.java
@@ -16,6 +16,7 @@
 
 package io.netty.handler.ssl;
 
+import java.security.KeyStore;
 import java.security.Provider;
 import javax.net.ssl.KeyManager;
 import javax.net.ssl.KeyManagerFactory;
@@ -169,12 +170,12 @@ public final class JdkSslClientContext extends JdkSslContext {
     }
 
     JdkSslClientContext(Provider provider,
-        File trustCertCollectionFile, TrustManagerFactory trustManagerFactory,
-        Iterable<String> ciphers, CipherSuiteFilter cipherFilter, JdkApplicationProtocolNegotiator apn,
-        long sessionCacheSize, long sessionTimeout) throws SSLException {
+                        File trustCertCollectionFile, TrustManagerFactory trustManagerFactory,
+                        Iterable<String> ciphers, CipherSuiteFilter cipherFilter, JdkApplicationProtocolNegotiator apn,
+                        long sessionCacheSize, long sessionTimeout) throws SSLException {
         super(newSSLContext(provider, toX509CertificatesInternal(trustCertCollectionFile),
                 trustManagerFactory, null, null,
-                null, null, sessionCacheSize, sessionTimeout), true,
+                null, null, sessionCacheSize, sessionTimeout, KeyStore.getDefaultType()), true,
                 ciphers, cipherFilter, apn, ClientAuth.NONE, null, false);
     }
 
@@ -257,7 +258,7 @@ public final class JdkSslClientContext extends JdkSslContext {
         super(newSSLContext(null, toX509CertificatesInternal(
                 trustCertCollectionFile), trustManagerFactory,
                 toX509CertificatesInternal(keyCertChainFile), toPrivateKeyInternal(keyFile, keyPassword),
-                keyPassword, keyManagerFactory, sessionCacheSize, sessionTimeout), true,
+                keyPassword, keyManagerFactory, sessionCacheSize, sessionTimeout, KeyStore.getDefaultType()), true,
                 ciphers, cipherFilter, apn, ClientAuth.NONE, null, false);
     }
 
@@ -265,10 +266,12 @@ public final class JdkSslClientContext extends JdkSslContext {
                         X509Certificate[] trustCertCollection, TrustManagerFactory trustManagerFactory,
                         X509Certificate[] keyCertChain, PrivateKey key, String keyPassword,
                         KeyManagerFactory keyManagerFactory, Iterable<String> ciphers, CipherSuiteFilter cipherFilter,
-                        ApplicationProtocolConfig apn, String[] protocols, long sessionCacheSize, long sessionTimeout)
+                        ApplicationProtocolConfig apn, String[] protocols, long sessionCacheSize, long sessionTimeout,
+                        String keyStoreType)
             throws SSLException {
         super(newSSLContext(sslContextProvider, trustCertCollection, trustManagerFactory,
-                            keyCertChain, key, keyPassword, keyManagerFactory, sessionCacheSize, sessionTimeout),
+                            keyCertChain, key, keyPassword, keyManagerFactory, sessionCacheSize,
+                            sessionTimeout, keyStoreType),
                 true, ciphers, cipherFilter, toNegotiator(apn, false), ClientAuth.NONE, protocols, false);
     }
 
@@ -276,13 +279,14 @@ public final class JdkSslClientContext extends JdkSslContext {
                                             X509Certificate[] trustCertCollection,
                                             TrustManagerFactory trustManagerFactory, X509Certificate[] keyCertChain,
                                             PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory,
-                                            long sessionCacheSize, long sessionTimeout) throws SSLException {
+                                            long sessionCacheSize, long sessionTimeout,
+                                            String keyStore) throws SSLException {
         try {
             if (trustCertCollection != null) {
-                trustManagerFactory = buildTrustManagerFactory(trustCertCollection, trustManagerFactory);
+                trustManagerFactory = buildTrustManagerFactory(trustCertCollection, trustManagerFactory, keyStore);
             }
             if (keyCertChain != null) {
-                keyManagerFactory = buildKeyManagerFactory(keyCertChain, key, keyPassword, keyManagerFactory);
+                keyManagerFactory = buildKeyManagerFactory(keyCertChain, key, keyPassword, keyManagerFactory, keyStore);
             }
             SSLContext ctx = sslContextProvider == null ? SSLContext.getInstance(PROTOCOL)
                 : SSLContext.getInstance(PROTOCOL, sslContextProvider);
diff --git a/handler/src/main/java/io/netty/handler/ssl/JdkSslContext.java b/handler/src/main/java/io/netty/handler/ssl/JdkSslContext.java
index d74bbde..0e9a127 100644
--- a/handler/src/main/java/io/netty/handler/ssl/JdkSslContext.java
+++ b/handler/src/main/java/io/netty/handler/ssl/JdkSslContext.java
@@ -25,6 +25,7 @@ import java.io.File;
 import java.io.IOException;
 import java.security.InvalidAlgorithmParameterException;
 import java.security.KeyException;
+import java.security.KeyStore;
 import java.security.KeyStoreException;
 import java.security.NoSuchAlgorithmException;
 import java.security.Provider;
@@ -78,7 +79,7 @@ public class JdkSslContext extends SslContext {
         DEFAULT_PROVIDER = context.getProvider();
 
         SSLEngine engine = context.createSSLEngine();
-        DEFAULT_PROTOCOLS = defaultProtocols(engine);
+        DEFAULT_PROTOCOLS = defaultProtocols(context, engine);
 
         SUPPORTED_CIPHERS = Collections.unmodifiableSet(supportedCiphers(engine));
         DEFAULT_CIPHERS = Collections.unmodifiableList(defaultCiphers(engine, SUPPORTED_CIPHERS));
@@ -97,13 +98,11 @@ public class JdkSslContext extends SslContext {
         }
     }
 
-    private static String[] defaultProtocols(SSLEngine engine) {
-        // Choose the sensible default list of protocols.
-        final String[] supportedProtocols = engine.getSupportedProtocols();
+    private static String[] defaultProtocols(SSLContext context, SSLEngine engine) {
+        // Choose the sensible default list of protocols that respects JDK flags, eg. jdk.tls.client.protocols
+        final String[] supportedProtocols = context.getDefaultSSLParameters().getProtocols();
         Set<String> supportedProtocolsSet = new HashSet<String>(supportedProtocols.length);
-        for (int i = 0; i < supportedProtocols.length; ++i) {
-            supportedProtocolsSet.add(supportedProtocols[i]);
-        }
+        Collections.addAll(supportedProtocolsSet, supportedProtocols);
         List<String> protocols = new ArrayList<String>();
         addIfSupported(
                 supportedProtocolsSet, protocols,
@@ -262,7 +261,7 @@ public class JdkSslContext extends SslContext {
             SSLEngine engine = sslContext.createSSLEngine();
             try {
                 if (protocols == null) {
-                    this.protocols = defaultProtocols(engine);
+                    this.protocols = defaultProtocols(sslContext, engine);
                 } else {
                     this.protocols = protocols;
                 }
@@ -435,17 +434,16 @@ public class JdkSslContext extends SslContext {
 
     /**
      * Build a {@link KeyManagerFactory} based upon a key file, key file password, and a certificate chain.
-     * @param certChainFile a X.509 certificate chain file in PEM format
+     * @param certChainFile an X.509 certificate chain file in PEM format
      * @param keyFile a PKCS#8 private key file in PEM format
      * @param keyPassword the password of the {@code keyFile}.
      *                    {@code null} if it's not password-protected.
      * @param kmf The existing {@link KeyManagerFactory} that will be used if not {@code null}
+     * @param keyStore the {@link KeyStore} that should be used in the {@link KeyManagerFactory}
      * @return A {@link KeyManagerFactory} based upon a key file, key file password, and a certificate chain.
-     * @deprecated will be removed.
      */
-    @Deprecated
-    protected static KeyManagerFactory buildKeyManagerFactory(File certChainFile, File keyFile, String keyPassword,
-            KeyManagerFactory kmf)
+    static KeyManagerFactory buildKeyManagerFactory(File certChainFile, File keyFile, String keyPassword,
+            KeyManagerFactory kmf, String keyStore)
                     throws UnrecoverableKeyException, KeyStoreException, NoSuchAlgorithmException,
                     NoSuchPaddingException, InvalidKeySpecException, InvalidAlgorithmParameterException,
                     CertificateException, KeyException, IOException {
@@ -453,30 +451,74 @@ public class JdkSslContext extends SslContext {
         if (algorithm == null) {
             algorithm = "SunX509";
         }
-        return buildKeyManagerFactory(certChainFile, algorithm, keyFile, keyPassword, kmf);
+        return buildKeyManagerFactory(certChainFile, algorithm, keyFile, keyPassword, kmf, keyStore);
+    }
+
+    /**
+     * Build a {@link KeyManagerFactory} based upon a key file, key file password, and a certificate chain.
+     * @param certChainFile an X.509 certificate chain file in PEM format
+     * @param keyFile a PKCS#8 private key file in PEM format
+     * @param keyPassword the password of the {@code keyFile}.
+     *                    {@code null} if it's not password-protected.
+     * @param kmf The existing {@link KeyManagerFactory} that will be used if not {@code null}
+     * @return A {@link KeyManagerFactory} based upon a key file, key file password, and a certificate chain.
+     * @deprecated will be removed.
+     */
+    @Deprecated
+    protected static KeyManagerFactory buildKeyManagerFactory(File certChainFile, File keyFile, String keyPassword,
+                                                              KeyManagerFactory kmf)
+            throws UnrecoverableKeyException, KeyStoreException, NoSuchAlgorithmException,
+            NoSuchPaddingException, InvalidKeySpecException, InvalidAlgorithmParameterException,
+            CertificateException, KeyException, IOException {
+        return buildKeyManagerFactory(certChainFile, keyFile, keyPassword, kmf, KeyStore.getDefaultType());
     }
 
     /**
      * Build a {@link KeyManagerFactory} based upon a key algorithm, key file, key file password,
      * and a certificate chain.
-     * @param certChainFile a X.509 certificate chain file in PEM format
+     * @param certChainFile an X.509 certificate chain file in PEM format
      * @param keyAlgorithm the standard name of the requested algorithm. See the Java Secure Socket Extension
-     * Reference Guide for information about standard algorithm names.
+     *                    Reference Guide for information about standard algorithm names.
      * @param keyFile a PKCS#8 private key file in PEM format
      * @param keyPassword the password of the {@code keyFile}.
      *                    {@code null} if it's not password-protected.
      * @param kmf The existing {@link KeyManagerFactory} that will be used if not {@code null}
+     * @param keyStore the {@link KeyStore} that should be used in the {@link KeyManagerFactory}
      * @return A {@link KeyManagerFactory} based upon a key algorithm, key file, key file password,
      * and a certificate chain.
-     * @deprecated will be removed.
      */
-    @Deprecated
-    protected static KeyManagerFactory buildKeyManagerFactory(File certChainFile,
-            String keyAlgorithm, File keyFile, String keyPassword, KeyManagerFactory kmf)
+    static KeyManagerFactory buildKeyManagerFactory(File certChainFile,
+            String keyAlgorithm, File keyFile, String keyPassword, KeyManagerFactory kmf,
+            String keyStore)
                     throws KeyStoreException, NoSuchAlgorithmException, NoSuchPaddingException,
                     InvalidKeySpecException, InvalidAlgorithmParameterException, IOException,
                     CertificateException, KeyException, UnrecoverableKeyException {
         return buildKeyManagerFactory(toX509Certificates(certChainFile), keyAlgorithm,
-                                      toPrivateKey(keyFile, keyPassword), keyPassword, kmf);
+                                      toPrivateKey(keyFile, keyPassword), keyPassword, kmf, keyStore);
+    }
+
+    /**
+     * Build a {@link KeyManagerFactory} based upon a key algorithm, key file, key file password,
+     * and a certificate chain.
+     * @param certChainFile an buildKeyManagerFactory X.509 certificate chain file in PEM format
+     * @param keyAlgorithm the standard name of the requested algorithm. See the Java Secure Socket Extension
+     *                    Reference Guide for information about standard algorithm names.
+     * @param keyFile a PKCS#8 private key file in PEM format
+     * @param keyPassword the password of the {@code keyFile}.
+     *                    {@code null} if it's not password-protected.
+     * @param kmf The existing {@link KeyManagerFactory} that will be used if not {@code null}
+     * @return A {@link KeyManagerFactory} based upon a key algorithm, key file, key file password,
+     * and a certificate chain.
+     * @deprecated will be removed.
+     */
+    @Deprecated
+    protected static KeyManagerFactory buildKeyManagerFactory(File certChainFile,
+                                                              String keyAlgorithm, File keyFile,
+                                                              String keyPassword, KeyManagerFactory kmf)
+            throws KeyStoreException, NoSuchAlgorithmException, NoSuchPaddingException,
+            InvalidKeySpecException, InvalidAlgorithmParameterException, IOException,
+            CertificateException, KeyException, UnrecoverableKeyException {
+        return buildKeyManagerFactory(toX509Certificates(certChainFile), keyAlgorithm,
+                toPrivateKey(keyFile, keyPassword), keyPassword, kmf, KeyStore.getDefaultType());
     }
 }
diff --git a/handler/src/main/java/io/netty/handler/ssl/JdkSslEngine.java b/handler/src/main/java/io/netty/handler/ssl/JdkSslEngine.java
index 3304e38..80e8366 100644
--- a/handler/src/main/java/io/netty/handler/ssl/JdkSslEngine.java
+++ b/handler/src/main/java/io/netty/handler/ssl/JdkSslEngine.java
@@ -15,6 +15,8 @@
  */
 package io.netty.handler.ssl;
 
+import io.netty.util.internal.SuppressJava6Requirement;
+
 import java.nio.ByteBuffer;
 
 import javax.net.ssl.SSLEngine;
@@ -145,6 +147,7 @@ class JdkSslEngine extends SSLEngine implements ApplicationProtocolAccessor {
         engine.setEnabledProtocols(strings);
     }
 
+    @SuppressJava6Requirement(reason = "Can only be called when running on JDK7+")
     @Override
     public SSLSession getHandshakeSession() {
         return engine.getHandshakeSession();
diff --git a/handler/src/main/java/io/netty/handler/ssl/JdkSslServerContext.java b/handler/src/main/java/io/netty/handler/ssl/JdkSslServerContext.java
index 2dcbbc0..141126a 100644
--- a/handler/src/main/java/io/netty/handler/ssl/JdkSslServerContext.java
+++ b/handler/src/main/java/io/netty/handler/ssl/JdkSslServerContext.java
@@ -16,6 +16,7 @@
 
 package io.netty.handler.ssl;
 
+import java.security.KeyStore;
 import java.security.Provider;
 import javax.net.ssl.KeyManager;
 
@@ -47,7 +48,8 @@ public final class JdkSslServerContext extends JdkSslContext {
      */
     @Deprecated
     public JdkSslServerContext(File certChainFile, File keyFile) throws SSLException {
-        this(certChainFile, keyFile, null);
+        this(null, certChainFile, keyFile, null, null, IdentityCipherSuiteFilter.INSTANCE,
+                JdkDefaultApplicationProtocolNegotiator.INSTANCE, 0, 0, null);
     }
 
     /**
@@ -87,8 +89,9 @@ public final class JdkSslServerContext extends JdkSslContext {
             File certChainFile, File keyFile, String keyPassword,
             Iterable<String> ciphers, Iterable<String> nextProtocols,
             long sessionCacheSize, long sessionTimeout) throws SSLException {
-        this(certChainFile, keyFile, keyPassword, ciphers, IdentityCipherSuiteFilter.INSTANCE,
-             toNegotiator(toApplicationProtocolConfig(nextProtocols), true), sessionCacheSize, sessionTimeout);
+        this(null, certChainFile, keyFile, keyPassword, ciphers, IdentityCipherSuiteFilter.INSTANCE,
+                toNegotiator(toApplicationProtocolConfig(nextProtocols), true), sessionCacheSize,
+                sessionTimeout, KeyStore.getDefaultType());
     }
 
     /**
@@ -113,8 +116,8 @@ public final class JdkSslServerContext extends JdkSslContext {
             File certChainFile, File keyFile, String keyPassword,
             Iterable<String> ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn,
             long sessionCacheSize, long sessionTimeout) throws SSLException {
-        this(certChainFile, keyFile, keyPassword, ciphers, cipherFilter,
-                toNegotiator(apn, true), sessionCacheSize, sessionTimeout);
+        this(null, certChainFile, keyFile, keyPassword, ciphers, cipherFilter,
+                toNegotiator(apn, true), sessionCacheSize, sessionTimeout, KeyStore.getDefaultType());
     }
 
     /**
@@ -139,17 +142,18 @@ public final class JdkSslServerContext extends JdkSslContext {
             File certChainFile, File keyFile, String keyPassword,
             Iterable<String> ciphers, CipherSuiteFilter cipherFilter, JdkApplicationProtocolNegotiator apn,
             long sessionCacheSize, long sessionTimeout) throws SSLException {
-        this(null, certChainFile, keyFile, keyPassword, ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout);
+        this(null, certChainFile, keyFile, keyPassword, ciphers, cipherFilter, apn,
+                sessionCacheSize, sessionTimeout, KeyStore.getDefaultType());
     }
 
     JdkSslServerContext(Provider provider,
-        File certChainFile, File keyFile, String keyPassword,
-        Iterable<String> ciphers, CipherSuiteFilter cipherFilter, JdkApplicationProtocolNegotiator apn,
-        long sessionCacheSize, long sessionTimeout) throws SSLException {
+                        File certChainFile, File keyFile, String keyPassword,
+                        Iterable<String> ciphers, CipherSuiteFilter cipherFilter, JdkApplicationProtocolNegotiator apn,
+                        long sessionCacheSize, long sessionTimeout, String keyStore) throws SSLException {
         super(newSSLContext(provider, null, null,
-            toX509CertificatesInternal(certChainFile), toPrivateKeyInternal(keyFile, keyPassword),
-            keyPassword, null, sessionCacheSize, sessionTimeout), false,
-            ciphers, cipherFilter, apn, ClientAuth.NONE, null, false);
+                toX509CertificatesInternal(certChainFile), toPrivateKeyInternal(keyFile, keyPassword),
+                keyPassword, null, sessionCacheSize, sessionTimeout, keyStore), false,
+                ciphers, cipherFilter, apn, ClientAuth.NONE, null, false);
     }
 
     /**
@@ -182,11 +186,14 @@ public final class JdkSslServerContext extends JdkSslContext {
      */
     @Deprecated
     public JdkSslServerContext(File trustCertCollectionFile, TrustManagerFactory trustManagerFactory,
-            File keyCertChainFile, File keyFile, String keyPassword, KeyManagerFactory keyManagerFactory,
-            Iterable<String> ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn,
-            long sessionCacheSize, long sessionTimeout) throws SSLException {
-        this(trustCertCollectionFile, trustManagerFactory, keyCertChainFile, keyFile, keyPassword, keyManagerFactory,
-                ciphers, cipherFilter, toNegotiator(apn, true), sessionCacheSize, sessionTimeout);
+                               File keyCertChainFile, File keyFile, String keyPassword,
+                               KeyManagerFactory keyManagerFactory,
+                               Iterable<String> ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn,
+                               long sessionCacheSize, long sessionTimeout) throws SSLException {
+        super(newSSLContext(null, toX509CertificatesInternal(trustCertCollectionFile), trustManagerFactory,
+                toX509CertificatesInternal(keyCertChainFile), toPrivateKeyInternal(keyFile, keyPassword),
+                keyPassword, keyManagerFactory, sessionCacheSize, sessionTimeout, null), false,
+                ciphers, cipherFilter, apn, ClientAuth.NONE, null, false);
     }
 
     /**
@@ -214,17 +221,19 @@ public final class JdkSslServerContext extends JdkSslContext {
      * @param sessionCacheSize the size of the cache used for storing SSL session objects.
      *                         {@code 0} to use the default value.
      * @param sessionTimeout the timeout for the cached SSL session objects, in seconds.
-     *                       {@code 0} to use the default value.
+     *                       {@code 0} to use the default value
      * @deprecated use {@link SslContextBuilder}
      */
     @Deprecated
     public JdkSslServerContext(File trustCertCollectionFile, TrustManagerFactory trustManagerFactory,
-            File keyCertChainFile, File keyFile, String keyPassword, KeyManagerFactory keyManagerFactory,
-            Iterable<String> ciphers, CipherSuiteFilter cipherFilter, JdkApplicationProtocolNegotiator apn,
-            long sessionCacheSize, long sessionTimeout) throws SSLException {
+                               File keyCertChainFile, File keyFile, String keyPassword,
+                               KeyManagerFactory keyManagerFactory,
+                               Iterable<String> ciphers, CipherSuiteFilter cipherFilter,
+                                JdkApplicationProtocolNegotiator apn,
+                               long sessionCacheSize, long sessionTimeout) throws SSLException {
         super(newSSLContext(null, toX509CertificatesInternal(trustCertCollectionFile), trustManagerFactory,
                 toX509CertificatesInternal(keyCertChainFile), toPrivateKeyInternal(keyFile, keyPassword),
-                keyPassword, keyManagerFactory, sessionCacheSize, sessionTimeout), false,
+                keyPassword, keyManagerFactory, sessionCacheSize, sessionTimeout, KeyStore.getDefaultType()), false,
                 ciphers, cipherFilter, apn, ClientAuth.NONE, null, false);
     }
 
@@ -233,16 +242,17 @@ public final class JdkSslServerContext extends JdkSslContext {
                         X509Certificate[] keyCertChain, PrivateKey key, String keyPassword,
                         KeyManagerFactory keyManagerFactory, Iterable<String> ciphers, CipherSuiteFilter cipherFilter,
                         ApplicationProtocolConfig apn, long sessionCacheSize, long sessionTimeout,
-                        ClientAuth clientAuth, String[] protocols, boolean startTls) throws SSLException {
+                        ClientAuth clientAuth, String[] protocols, boolean startTls,
+                        String keyStore) throws SSLException {
         super(newSSLContext(provider, trustCertCollection, trustManagerFactory, keyCertChain, key,
-                keyPassword, keyManagerFactory, sessionCacheSize, sessionTimeout), false,
+                keyPassword, keyManagerFactory, sessionCacheSize, sessionTimeout, keyStore), false,
                 ciphers, cipherFilter, toNegotiator(apn, true), clientAuth, protocols, startTls);
     }
 
     private static SSLContext newSSLContext(Provider sslContextProvider, X509Certificate[] trustCertCollection,
                                      TrustManagerFactory trustManagerFactory, X509Certificate[] keyCertChain,
                                      PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory,
-                                     long sessionCacheSize, long sessionTimeout)
+                                     long sessionCacheSize, long sessionTimeout, String keyStore)
             throws SSLException {
         if (key == null && keyManagerFactory == null) {
             throw new NullPointerException("key, keyManagerFactory");
@@ -250,10 +260,10 @@ public final class JdkSslServerContext extends JdkSslContext {
 
         try {
             if (trustCertCollection != null) {
-                trustManagerFactory = buildTrustManagerFactory(trustCertCollection, trustManagerFactory);
+                trustManagerFactory = buildTrustManagerFactory(trustCertCollection, trustManagerFactory, keyStore);
             }
             if (key != null) {
-                keyManagerFactory = buildKeyManagerFactory(keyCertChain, key, keyPassword, keyManagerFactory);
+                keyManagerFactory = buildKeyManagerFactory(keyCertChain, key, keyPassword, keyManagerFactory, null);
             }
 
             // Initialize the SSLContext to work with our key managers.
diff --git a/handler/src/main/java/io/netty/handler/ssl/KeyManagerFactoryWrapper.java b/handler/src/main/java/io/netty/handler/ssl/KeyManagerFactoryWrapper.java
new file mode 100644
index 0000000..715b780
--- /dev/null
+++ b/handler/src/main/java/io/netty/handler/ssl/KeyManagerFactoryWrapper.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2019 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.ssl;
+
+import io.netty.handler.ssl.util.SimpleKeyManagerFactory;
+import io.netty.util.internal.ObjectUtil;
+
+import java.security.KeyStore;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.ManagerFactoryParameters;
+
+final class KeyManagerFactoryWrapper extends SimpleKeyManagerFactory {
+    private final KeyManager km;
+
+    KeyManagerFactoryWrapper(KeyManager km) {
+        this.km = ObjectUtil.checkNotNull(km, "km");
+    }
+
+    @Override
+    protected void engineInit(KeyStore keyStore, char[] var2) throws Exception { }
+
+    @Override
+    protected void engineInit(ManagerFactoryParameters managerFactoryParameters)
+            throws Exception { }
+
+    @Override
+    protected KeyManager[] engineGetKeyManagers() {
+        return new KeyManager[] {km};
+    }
+}
diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSsl.java b/handler/src/main/java/io/netty/handler/ssl/OpenSsl.java
index 78a528f..de38c6f 100644
--- a/handler/src/main/java/io/netty/handler/ssl/OpenSsl.java
+++ b/handler/src/main/java/io/netty/handler/ssl/OpenSsl.java
@@ -18,10 +18,12 @@ package io.netty.handler.ssl;
 
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.ByteBufAllocator;
+import io.netty.buffer.UnpooledByteBufAllocator;
 import io.netty.internal.tcnative.Buffer;
 import io.netty.internal.tcnative.Library;
 import io.netty.internal.tcnative.SSL;
 import io.netty.internal.tcnative.SSLContext;
+import io.netty.util.CharsetUtil;
 import io.netty.util.ReferenceCountUtil;
 import io.netty.util.ReferenceCounted;
 import io.netty.util.internal.NativeLibraryLoader;
@@ -31,10 +33,7 @@ import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 
 import java.io.ByteArrayInputStream;
-import java.security.AccessController;
-import java.security.PrivilegedAction;
 import java.security.cert.CertificateException;
-import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -45,7 +44,7 @@ import java.util.Set;
 import static io.netty.handler.ssl.SslUtils.*;
 
 /**
- * Tells if <a href="http://netty.io/wiki/forked-tomcat-native.html">{@code netty-tcnative}</a> and its OpenSSL support
+ * Tells if <a href="https://netty.io/wiki/forked-tomcat-native.html">{@code netty-tcnative}</a> and its OpenSSL support
  * are available.
  */
 public final class OpenSsl {
@@ -58,13 +57,54 @@ public final class OpenSsl {
     private static final Set<String> AVAILABLE_OPENSSL_CIPHER_SUITES;
     private static final Set<String> AVAILABLE_JAVA_CIPHER_SUITES;
     private static final boolean SUPPORTS_KEYMANAGER_FACTORY;
-    private static final boolean SUPPORTS_HOSTNAME_VALIDATION;
     private static final boolean USE_KEYMANAGER_FACTORY;
     private static final boolean SUPPORTS_OCSP;
     private static final boolean TLSV13_SUPPORTED;
     private static final boolean IS_BORINGSSL;
     static final Set<String> SUPPORTED_PROTOCOLS_SET;
 
+    // self-signed certificate for netty.io and the matching private-key
+    private static final String CERT = "-----BEGIN CERTIFICATE-----\n" +
+            "MIICrjCCAZagAwIBAgIIdSvQPv1QAZQwDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAxMLZXhhbXBs\n" +
+            "ZS5jb20wIBcNMTgwNDA2MjIwNjU5WhgPOTk5OTEyMzEyMzU5NTlaMBYxFDASBgNVBAMTC2V4YW1w\n" +
+            "bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAggbWsmDQ6zNzRZ5AW8E3eoGl\n" +
+            "qWvOBDb5Fs1oBRrVQHuYmVAoaqwDzXYJ0LOwa293AgWEQ1jpcbZ2hpoYQzqEZBTLnFhMrhRFlH6K\n" +
+            "bJND8Y33kZ/iSVBBDuGbdSbJShlM+4WwQ9IAso4MZ4vW3S1iv5fGGpLgbtXRmBf/RU8omN0Gijlv\n" +
+            "WlLWHWijLN8xQtySFuBQ7ssW8RcKAary3pUm6UUQB+Co6lnfti0Tzag8PgjhAJq2Z3wbsGRnP2YS\n" +
+            "vYoaK6qzmHXRYlp/PxrjBAZAmkLJs4YTm/XFF+fkeYx4i9zqHbyone5yerRibsHaXZWLnUL+rFoe\n" +
+            "MdKvr0VS3sGmhQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQADQi441pKmXf9FvUV5EHU4v8nJT9Iq\n" +
+            "yqwsKwXnr7AsUlDGHBD7jGrjAXnG5rGxuNKBQ35wRxJATKrUtyaquFUL6H8O6aGQehiFTk6zmPbe\n" +
+            "12Gu44vqqTgIUxnv3JQJiox8S2hMxsSddpeCmSdvmalvD6WG4NthH6B9ZaBEiep1+0s0RUaBYn73\n" +
+            "I7CCUaAtbjfR6pcJjrFk5ei7uwdQZFSJtkP2z8r7zfeANJddAKFlkaMWn7u+OIVuB4XPooWicObk\n" +
+            "NAHFtP65bocUYnDpTVdiyvn8DdqyZ/EO8n1bBKBzuSLplk2msW4pdgaFgY7Vw/0wzcFXfUXmL1uy\n" +
+            "G8sQD/wx\n" +
+            "-----END CERTIFICATE-----";
+
+    private static final String KEY = "-----BEGIN PRIVATE KEY-----\n" +
+            "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCCBtayYNDrM3NFnkBbwTd6gaWp\n" +
+            "a84ENvkWzWgFGtVAe5iZUChqrAPNdgnQs7Brb3cCBYRDWOlxtnaGmhhDOoRkFMucWEyuFEWUfops\n" +
+            "k0PxjfeRn+JJUEEO4Zt1JslKGUz7hbBD0gCyjgxni9bdLWK/l8YakuBu1dGYF/9FTyiY3QaKOW9a\n" +
+            "UtYdaKMs3zFC3JIW4FDuyxbxFwoBqvLelSbpRRAH4KjqWd+2LRPNqDw+COEAmrZnfBuwZGc/ZhK9\n" +
+            "ihorqrOYddFiWn8/GuMEBkCaQsmzhhOb9cUX5+R5jHiL3OodvKid7nJ6tGJuwdpdlYudQv6sWh4x\n" +
+            "0q+vRVLewaaFAgMBAAECggEAP8tPJvFtTxhNJAkCloHz0D0vpDHqQBMgntlkgayqmBqLwhyb18pR\n" +
+            "i0qwgh7HHc7wWqOOQuSqlEnrWRrdcI6TSe8R/sErzfTQNoznKWIPYcI/hskk4sdnQ//Yn9/Jvnsv\n" +
+            "U/BBjOTJxtD+sQbhAl80JcA3R+5sArURQkfzzHOL/YMqzAsn5hTzp7HZCxUqBk3KaHRxV7NefeOE\n" +
+            "xlZuWSmxYWfbFIs4kx19/1t7h8CHQWezw+G60G2VBtSBBxDnhBWvqG6R/wpzJ3nEhPLLY9T+XIHe\n" +
+            "ipzdMOOOUZorfIg7M+pyYPji+ZIZxIpY5OjrOzXHciAjRtr5Y7l99K1CG1LguQKBgQDrQfIMxxtZ\n" +
+            "vxU/1cRmUV9l7pt5bjV5R6byXq178LxPKVYNjdZ840Q0/OpZEVqaT1xKVi35ohP1QfNjxPLlHD+K\n" +
+            "iDAR9z6zkwjIrbwPCnb5kuXy4lpwPcmmmkva25fI7qlpHtbcuQdoBdCfr/KkKaUCMPyY89LCXgEw\n" +
+            "5KTDj64UywKBgQCNfbO+eZLGzhiHhtNJurresCsIGWlInv322gL8CSfBMYl6eNfUTZvUDdFhPISL\n" +
+            "UljKWzXDrjw0ujFSPR0XhUGtiq89H+HUTuPPYv25gVXO+HTgBFZEPl4PpA+BUsSVZy0NddneyqLk\n" +
+            "42Wey9omY9Q8WsdNQS5cbUvy0uG6WFoX7wKBgQDZ1jpW8pa0x2bZsQsm4vo+3G5CRnZlUp+XlWt2\n" +
+            "dDcp5dC0xD1zbs1dc0NcLeGDOTDv9FSl7hok42iHXXq8AygjEm/QcuwwQ1nC2HxmQP5holAiUs4D\n" +
+            "WHM8PWs3wFYPzE459EBoKTxeaeP/uWAn+he8q7d5uWvSZlEcANs/6e77eQKBgD21Ar0hfFfj7mK8\n" +
+            "9E0FeRZBsqK3omkfnhcYgZC11Xa2SgT1yvs2Va2n0RcdM5kncr3eBZav2GYOhhAdwyBM55XuE/sO\n" +
+            "eokDVutNeuZ6d5fqV96TRaRBpvgfTvvRwxZ9hvKF4Vz+9wfn/JvCwANaKmegF6ejs7pvmF3whq2k\n" +
+            "drZVAoGAX5YxQ5XMTD0QbMAl7/6qp6S58xNoVdfCkmkj1ZLKaHKIjS/benkKGlySVQVPexPfnkZx\n" +
+            "p/Vv9yyphBoudiTBS9Uog66ueLYZqpgxlM/6OhYg86Gm3U2ycvMxYjBM1NFiyze21AqAhI+HX+Ot\n" +
+            "mraV2/guSgDgZAhukRZzeQ2RucI=\n" +
+            "-----END PRIVATE KEY-----";
+
     static {
         Throwable cause = null;
 
@@ -97,7 +137,7 @@ public final class OpenSsl {
                             "Failed to load netty-tcnative; " +
                                     OpenSslEngine.class.getSimpleName() + " will be unavailable, unless the " +
                                     "application has already loaded the symbols by some other means. " +
-                                    "See http://netty.io/wiki/forked-tomcat-native.html for more information.", t);
+                                    "See https://netty.io/wiki/forked-tomcat-native.html for more information.", t);
                 }
 
                 try {
@@ -120,7 +160,7 @@ public final class OpenSsl {
                     logger.debug(
                             "Failed to initialize netty-tcnative; " +
                                     OpenSslEngine.class.getSimpleName() + " will be unavailable. " +
-                                    "See http://netty.io/wiki/forked-tomcat-native.html for more information.", t);
+                                    "See https://netty.io/wiki/forked-tomcat-native.html for more information.", t);
                 }
             }
         }
@@ -134,7 +174,6 @@ public final class OpenSsl {
             final Set<String> availableOpenSslCipherSuites = new LinkedHashSet<String>(128);
             boolean supportsKeyManagerFactory = false;
             boolean useKeyManagerFactory = false;
-            boolean supportsHostNameValidation = false;
             boolean tlsv13Supported = false;
 
             IS_BORINGSSL = "BoringSSL".equals(versionString());
@@ -142,6 +181,9 @@ public final class OpenSsl {
             try {
                 final long sslCtx = SSLContext.make(SSL.SSL_PROTOCOL_ALL, SSL.SSL_MODE_SERVER);
                 long certBio = 0;
+                long keyBio = 0;
+                long cert = 0;
+                long key = 0;
                 try {
                     try {
                         StringBuilder tlsv13Ciphers = new StringBuilder();
@@ -188,36 +230,65 @@ public final class OpenSsl {
                                                "AEAD-AES256-GCM-SHA384",
                                                "AEAD-CHACHA20-POLY1305-SHA256");
                         }
+
+                        PemEncoded privateKey = PemPrivateKey.valueOf(KEY.getBytes(CharsetUtil.US_ASCII));
                         try {
-                            SSL.setHostNameValidation(ssl, 0, "netty.io");
-                            supportsHostNameValidation = true;
-                        } catch (Throwable ignore) {
-                            logger.debug("Hostname Verification not supported.");
-                        }
-                        try {
+                            // Let's check if we can set a callback, which may not work if the used OpenSSL version
+                            // is to old.
+                            SSLContext.setCertificateCallback(sslCtx, null);
+
                             X509Certificate certificate = selfSignedCertificate();
                             certBio = ReferenceCountedOpenSslContext.toBIO(ByteBufAllocator.DEFAULT, certificate);
-                            SSL.setCertificateChainBio(ssl, certBio, false);
+                            cert = SSL.parseX509Chain(certBio);
+
+                            keyBio = ReferenceCountedOpenSslContext.toBIO(
+                                    UnpooledByteBufAllocator.DEFAULT, privateKey.retain());
+                            key = SSL.parsePrivateKey(keyBio, null);
+
+                            SSL.setKeyMaterial(ssl, cert, key);
                             supportsKeyManagerFactory = true;
                             try {
-                                useKeyManagerFactory = AccessController.doPrivileged(new PrivilegedAction<Boolean>() {
-                                    @Override
-                                    public Boolean run() {
-                                        return SystemPropertyUtil.getBoolean(
-                                                "io.netty.handler.ssl.openssl.useKeyManagerFactory", true);
+                                boolean propertySet = SystemPropertyUtil.contains(
+                                        "io.netty.handler.ssl.openssl.useKeyManagerFactory");
+                                if (!IS_BORINGSSL) {
+                                    useKeyManagerFactory = SystemPropertyUtil.getBoolean(
+                                            "io.netty.handler.ssl.openssl.useKeyManagerFactory", true);
+
+                                    if (propertySet) {
+                                        logger.info("System property " +
+                                                "'io.netty.handler.ssl.openssl.useKeyManagerFactory'" +
+                                                " is deprecated and so will be ignored in the future");
                                     }
-                                });
+                                } else {
+                                    useKeyManagerFactory = true;
+                                    if (propertySet) {
+                                        logger.info("System property " +
+                                                "'io.netty.handler.ssl.openssl.useKeyManagerFactory'" +
+                                                " is deprecated and will be ignored when using BoringSSL");
+                                    }
+                                }
                             } catch (Throwable ignore) {
                                 logger.debug("Failed to get useKeyManagerFactory system property.");
                             }
-                        } catch (Throwable ignore) {
+                        } catch (Error ignore) {
                             logger.debug("KeyManagerFactory not supported.");
+                        } finally {
+                            privateKey.release();
                         }
                     } finally {
                         SSL.freeSSL(ssl);
                         if (certBio != 0) {
                             SSL.freeBIO(certBio);
                         }
+                        if (keyBio != 0) {
+                            SSL.freeBIO(keyBio);
+                        }
+                        if (cert != 0) {
+                            SSL.freeX509Chain(cert);
+                        }
+                        if (key != 0) {
+                            SSL.freePrivateKey(key);
+                        }
                     }
                 } finally {
                     SSLContext.free(sslCtx);
@@ -254,7 +325,6 @@ public final class OpenSsl {
 
             AVAILABLE_CIPHER_SUITES = availableCipherSuites;
             SUPPORTS_KEYMANAGER_FACTORY = supportsKeyManagerFactory;
-            SUPPORTS_HOSTNAME_VALIDATION = supportsHostNameValidation;
             USE_KEYMANAGER_FACTORY = useKeyManagerFactory;
 
             Set<String> protocols = new LinkedHashSet<String>(6);
@@ -297,7 +367,6 @@ public final class OpenSsl {
             AVAILABLE_JAVA_CIPHER_SUITES = Collections.emptySet();
             AVAILABLE_CIPHER_SUITES = Collections.emptySet();
             SUPPORTS_KEYMANAGER_FACTORY = false;
-            SUPPORTS_HOSTNAME_VALIDATION = false;
             USE_KEYMANAGER_FACTORY = false;
             SUPPORTED_PROTOCOLS_SET = Collections.emptySet();
             SUPPORTS_OCSP = false;
@@ -310,39 +379,9 @@ public final class OpenSsl {
      * Returns a self-signed {@link X509Certificate} for {@code netty.io}.
      */
     static X509Certificate selfSignedCertificate() throws CertificateException {
-        // Bytes of self-signed certificate for netty.io
-        byte[] certBytes = {
-                48, -126, 1, -92, 48, -126, 1, 13, -96, 3, 2, 1, 2, 2, 9, 0, -9, 61,
-                44, 121, -118, -4, -45, -120, 48, 13, 6, 9, 42, -122, 72, -122,
-                -9, 13, 1, 1, 5, 5, 0, 48, 19, 49, 17, 48, 15, 6, 3, 85, 4, 3, 19,
-                8, 110, 101, 116, 116, 121, 46, 105, 111, 48, 32, 23, 13, 49, 55,
-                49, 48, 50, 48, 49, 56, 49, 54, 51, 54, 90, 24, 15, 57, 57, 57, 57,
-                49, 50, 51, 49, 50, 51, 53, 57, 53, 57, 90, 48, 19, 49, 17, 48, 15,
-                6, 3, 85, 4, 3, 19, 8, 110, 101, 116, 116, 121, 46, 105, 111, 48, -127,
-                -97, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 1, 5, 0, 3, -127,
-                -115, 0, 48, -127, -119, 2, -127, -127, 0, -116, 37, 122, -53, 28, 46,
-                13, -90, -14, -33, 111, -108, -41, 59, 90, 124, 113, -112, -66, -17,
-                -102, 44, 13, 7, -33, -28, 24, -79, -126, -76, 40, 111, -126, -103,
-                -102, 34, 11, 45, 16, -38, 63, 24, 80, 24, 76, 88, -93, 96, 11, 38,
-                -19, -64, -11, 87, -49, -52, -65, 24, 36, -22, 53, 8, -42, 14, -121,
-                114, 6, 17, -82, 10, 92, -91, -127, 81, -12, -75, 105, -10, -106, 91,
-                -38, 111, 50, 57, -97, -125, 109, 42, -87, -1, -19, 80, 78, 49, -97, -4,
-                23, -2, -103, 122, -107, -43, 4, -31, -21, 90, 39, -9, -106, 34, -101,
-                -116, 31, -94, -84, 80, -6, -78, -33, 87, -90, 31, 103, 100, 56, -103,
-                -5, 11, 2, 3, 1, 0, 1, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1,
-                5, 5, 0, 3, -127, -127, 0, 112, 45, -73, 5, 64, 49, 59, 101, 51, 73,
-                -96, 62, 23, -84, 90, -41, -58, 83, -20, -72, 38, 123, -108, -45, 28,
-                96, -122, -18, 30, 42, 86, 87, -87, -28, 107, 110, 11, -59, 91, 100,
-                101, -18, 26, -103, -78, -80, -3, 38, 113, 83, -48, -108, 109, 41, -15,
-                6, 112, 105, 7, -46, -11, -3, -51, 40, -66, -73, -83, -46, -94, -121,
-                -88, 51, -106, -77, 109, 53, -7, 123, 91, 75, -105, -22, 64, 121, -72,
-                -59, -21, -44, 84, 12, 9, 120, 21, -26, 13, 49, -81, -58, -47, 117,
-                -44, -18, -17, 124, 49, -48, 19, 16, -41, 71, -52, -107, 99, -19, -29,
-                105, -93, -71, -38, -97, -128, -2, 118, 119, 49, -126, 109, 119 };
-
-        CertificateFactory cf = CertificateFactory.getInstance("X.509");
-        return (X509Certificate) cf.generateCertificate(
-                new ByteArrayInputStream(certBytes));
+        return (X509Certificate) SslContext.X509_CERT_FACTORY.generateCertificate(
+                new ByteArrayInputStream(CERT.getBytes(CharsetUtil.US_ASCII))
+        );
     }
 
     private static boolean doesSupportOcsp() {
@@ -383,7 +422,7 @@ public final class OpenSsl {
 
     /**
      * Returns {@code true} if and only if
-     * <a href="http://netty.io/wiki/forked-tomcat-native.html">{@code netty-tcnative}</a> and its OpenSSL support
+     * <a href="https://netty.io/wiki/forked-tomcat-native.html">{@code netty-tcnative}</a> and its OpenSSL support
      * are available.
      */
     public static boolean isAvailable() {
@@ -393,7 +432,10 @@ public final class OpenSsl {
     /**
      * Returns {@code true} if the used version of openssl supports
      * <a href="https://tools.ietf.org/html/rfc7301">ALPN</a>.
+     *
+     * @deprecated use {@link SslProvider#isAlpnSupported(SslProvider)} with {@link SslProvider#OPENSSL}.
      */
+    @Deprecated
     public static boolean isAlpnSupported() {
         return version() >= 0x10002000L;
     }
@@ -422,7 +464,7 @@ public final class OpenSsl {
     }
 
     /**
-     * Ensure that <a href="http://netty.io/wiki/forked-tomcat-native.html">{@code netty-tcnative}</a> and
+     * Ensure that <a href="https://netty.io/wiki/forked-tomcat-native.html">{@code netty-tcnative}</a> and
      * its OpenSSL support are available.
      *
      * @throws UnsatisfiedLinkError if unavailable
@@ -436,7 +478,7 @@ public final class OpenSsl {
 
     /**
      * Returns the cause of unavailability of
-     * <a href="http://netty.io/wiki/forked-tomcat-native.html">{@code netty-tcnative}</a> and its OpenSSL support.
+     * <a href="https://netty.io/wiki/forked-tomcat-native.html">{@code netty-tcnative}</a> and its OpenSSL support.
      *
      * @return the cause if unavailable. {@code null} if available.
      */
@@ -488,11 +530,14 @@ public final class OpenSsl {
     }
 
     /**
-     * Returns {@code true} if <a href="https://wiki.openssl.org/index.php/Hostname_validation">Hostname Validation</a>
-     * is supported when using OpenSSL.
+     * Always returns {@code true} if {@link #isAvailable()} returns {@code true}.
+     *
+     * @deprecated Will be removed because hostname validation is always done by a
+     * {@link javax.net.ssl.TrustManager} implementation.
      */
+    @Deprecated
     public static boolean supportsHostnameValidation() {
-        return SUPPORTS_HOSTNAME_VALIDATION;
+        return isAvailable();
     }
 
     static boolean useKeyManagerFactory() {
@@ -510,15 +555,25 @@ public final class OpenSsl {
         String os = PlatformDependent.normalizedOs();
         String arch = PlatformDependent.normalizedArch();
 
-        Set<String> libNames = new LinkedHashSet<String>(4);
+        Set<String> libNames = new LinkedHashSet<String>(5);
         String staticLibName = "netty_tcnative";
 
         // First, try loading the platform-specific library. Platform-specific
         // libraries will be available if using a tcnative uber jar.
-        libNames.add(staticLibName + "_" + os + '_' + arch);
         if ("linux".equalsIgnoreCase(os)) {
-            // Fedora SSL lib so naming (libssl.so.10 vs libssl.so.1.0.0)..
+            Set<String> classifiers = PlatformDependent.normalizedLinuxClassifiers();
+            for (String classifier : classifiers) {
+                libNames.add(staticLibName + "_" + os + '_' + arch + "_" + classifier);
+            }
+            // generic arch-dependent library
+            libNames.add(staticLibName + "_" + os + '_' + arch);
+
+            // Fedora SSL lib so naming (libssl.so.10 vs libssl.so.1.0.0).
+            // note: should already be included from the classifiers but if not, we use this as an
+            //       additional fallback option here
             libNames.add(staticLibName + "_" + os + '_' + arch + "_fedora");
+        } else {
+            libNames.add(staticLibName + "_" + os + '_' + arch);
         }
         libNames.add(staticLibName + "_" + arch);
         libNames.add(staticLibName);
diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslCachingKeyMaterialProvider.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslCachingKeyMaterialProvider.java
index db8779b..07b67d9 100644
--- a/handler/src/main/java/io/netty/handler/ssl/OpenSslCachingKeyMaterialProvider.java
+++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslCachingKeyMaterialProvider.java
@@ -28,10 +28,13 @@ import java.util.concurrent.ConcurrentMap;
  */
 final class OpenSslCachingKeyMaterialProvider extends OpenSslKeyMaterialProvider {
 
+    private final int maxCachedEntries;
+    private volatile boolean full;
     private final ConcurrentMap<String, OpenSslKeyMaterial> cache = new ConcurrentHashMap<String, OpenSslKeyMaterial>();
 
-    OpenSslCachingKeyMaterialProvider(X509KeyManager keyManager, String password) {
+    OpenSslCachingKeyMaterialProvider(X509KeyManager keyManager, String password, int maxCachedEntries) {
         super(keyManager, password);
+        this.maxCachedEntries = maxCachedEntries;
     }
 
     @Override
@@ -44,6 +47,14 @@ final class OpenSslCachingKeyMaterialProvider extends OpenSslKeyMaterialProvider
                 return null;
             }
 
+            if (full) {
+                return material;
+            }
+            if (cache.size() > maxCachedEntries) {
+                full = true;
+                // Do not cache...
+                return material;
+            }
             OpenSslKeyMaterial old = cache.putIfAbsent(alias, material);
             if (old != null) {
                 material.release();
diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslCachingX509KeyManagerFactory.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslCachingX509KeyManagerFactory.java
index 6581d9b..7f67bc8 100644
--- a/handler/src/main/java/io/netty/handler/ssl/OpenSslCachingX509KeyManagerFactory.java
+++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslCachingX509KeyManagerFactory.java
@@ -15,10 +15,13 @@
  */
 package io.netty.handler.ssl;
 
+import io.netty.util.internal.ObjectUtil;
+
 import javax.net.ssl.KeyManager;
 import javax.net.ssl.KeyManagerFactory;
 import javax.net.ssl.KeyManagerFactorySpi;
 import javax.net.ssl.ManagerFactoryParameters;
+import javax.net.ssl.X509ExtendedKeyManager;
 import javax.net.ssl.X509KeyManager;
 import java.security.InvalidAlgorithmParameterException;
 import java.security.KeyStore;
@@ -37,7 +40,13 @@ import java.security.cert.X509Certificate;
  */
 public final class OpenSslCachingX509KeyManagerFactory extends KeyManagerFactory {
 
+    private final int maxCachedEntries;
+
     public OpenSslCachingX509KeyManagerFactory(final KeyManagerFactory factory) {
+        this(factory, 1024);
+    }
+
+    public OpenSslCachingX509KeyManagerFactory(final KeyManagerFactory factory, int maxCachedEntries) {
         super(new KeyManagerFactorySpi() {
             @Override
             protected void engineInit(KeyStore keyStore, char[] chars)
@@ -56,5 +65,17 @@ public final class OpenSslCachingX509KeyManagerFactory extends KeyManagerFactory
                 return factory.getKeyManagers();
             }
         }, factory.getProvider(), factory.getAlgorithm());
+        this.maxCachedEntries = ObjectUtil.checkPositive(maxCachedEntries, "maxCachedEntries");
+    }
+
+    OpenSslKeyMaterialProvider newProvider(String password) {
+        X509KeyManager keyManager = ReferenceCountedOpenSslContext.chooseX509KeyManager(getKeyManagers());
+        if ("sun.security.ssl.X509KeyManagerImpl".equals(keyManager.getClass().getName())) {
+            // Don't do caching if X509KeyManagerImpl is used as the returned aliases are not stable and will change
+            // between invocations.
+            return new OpenSslKeyMaterialProvider(keyManager, password);
+        }
+        return new OpenSslCachingKeyMaterialProvider(
+                ReferenceCountedOpenSslContext.chooseX509KeyManager(getKeyManagers()), password, maxCachedEntries);
     }
 }
diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslClientContext.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslClientContext.java
index 6856c7f..7f9b39a 100644
--- a/handler/src/main/java/io/netty/handler/ssl/OpenSslClientContext.java
+++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslClientContext.java
@@ -18,6 +18,7 @@ package io.netty.handler.ssl;
 import io.netty.internal.tcnative.SSL;
 
 import java.io.File;
+import java.security.KeyStore;
 import java.security.PrivateKey;
 import java.security.cert.X509Certificate;
 
@@ -42,7 +43,7 @@ public final class OpenSslClientContext extends OpenSslContext {
      */
     @Deprecated
     public OpenSslClientContext() throws SSLException {
-        this((File) null, null, null, null, null, null, null, IdentityCipherSuiteFilter.INSTANCE, null, 0, 0);
+        this(null, null, null, null, null, null, null, IdentityCipherSuiteFilter.INSTANCE, null, 0, 0);
     }
 
     /**
@@ -176,21 +177,22 @@ public final class OpenSslClientContext extends OpenSslContext {
         this(toX509CertificatesInternal(trustCertCollectionFile), trustManagerFactory,
                 toX509CertificatesInternal(keyCertChainFile), toPrivateKeyInternal(keyFile, keyPassword),
                 keyPassword, keyManagerFactory, ciphers, cipherFilter, apn, null, sessionCacheSize,
-                sessionTimeout, false);
+                sessionTimeout, false, KeyStore.getDefaultType());
     }
 
     OpenSslClientContext(X509Certificate[] trustCertCollection, TrustManagerFactory trustManagerFactory,
                          X509Certificate[] keyCertChain, PrivateKey key, String keyPassword,
                                 KeyManagerFactory keyManagerFactory, Iterable<String> ciphers,
                                 CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, String[] protocols,
-                                long sessionCacheSize, long sessionTimeout, boolean enableOcsp)
+                                long sessionCacheSize, long sessionTimeout, boolean enableOcsp, String keyStore)
             throws SSLException {
         super(ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, SSL.SSL_MODE_CLIENT, keyCertChain,
                 ClientAuth.NONE, protocols, false, enableOcsp);
         boolean success = false;
         try {
+            OpenSslKeyMaterialProvider.validateKeyMaterialSupported(keyCertChain, key, keyPassword);
             sessionContext = newSessionContext(this, ctx, engineMap, trustCertCollection, trustManagerFactory,
-                                               keyCertChain, key, keyPassword, keyManagerFactory);
+                                               keyCertChain, key, keyPassword, keyManagerFactory, keyStore);
             success = true;
         } finally {
             if (!success) {
diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslJavaxX509Certificate.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslJavaxX509Certificate.java
index da10ded..af52ddc 100644
--- a/handler/src/main/java/io/netty/handler/ssl/OpenSslJavaxX509Certificate.java
+++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslJavaxX509Certificate.java
@@ -32,7 +32,7 @@ final class OpenSslJavaxX509Certificate extends X509Certificate {
     private final byte[] bytes;
     private X509Certificate wrapped;
 
-    public OpenSslJavaxX509Certificate(byte[] bytes) {
+    OpenSslJavaxX509Certificate(byte[] bytes) {
         this.bytes = bytes;
     }
 
diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslKeyMaterialManager.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslKeyMaterialManager.java
index 9f0e019..7acbf70 100644
--- a/handler/src/main/java/io/netty/handler/ssl/OpenSslKeyMaterialManager.java
+++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslKeyMaterialManager.java
@@ -15,8 +15,6 @@
  */
 package io.netty.handler.ssl;
 
-import io.netty.internal.tcnative.SSL;
-
 import javax.net.ssl.SSLException;
 import javax.net.ssl.X509ExtendedKeyManager;
 import javax.net.ssl.X509KeyManager;
@@ -66,15 +64,19 @@ final class OpenSslKeyMaterialManager {
     }
 
     void setKeyMaterialServerSide(ReferenceCountedOpenSslEngine engine) throws SSLException {
-        long ssl = engine.sslPointer();
-        String[] authMethods = SSL.authenticationMethods(ssl);
+        String[] authMethods = engine.authMethods();
+        if (authMethods.length == 0) {
+            return;
+        }
         Set<String> aliases = new HashSet<String>(authMethods.length);
         for (String authMethod : authMethods) {
             String type = KEY_TYPES.get(authMethod);
             if (type != null) {
                 String alias = chooseServerAlias(engine, type);
                 if (alias != null && aliases.add(alias)) {
-                    setKeyMaterial(engine, alias);
+                    if (!setKeyMaterial(engine, alias)) {
+                        return;
+                    }
                 }
             }
         }
@@ -91,13 +93,11 @@ final class OpenSslKeyMaterialManager {
         }
     }
 
-    private void setKeyMaterial(ReferenceCountedOpenSslEngine engine, String alias) throws SSLException {
+    private boolean setKeyMaterial(ReferenceCountedOpenSslEngine engine, String alias) throws SSLException {
         OpenSslKeyMaterial keyMaterial = null;
         try {
             keyMaterial = provider.chooseKeyMaterial(engine.alloc, alias);
-            if (keyMaterial != null) {
-                engine.setKeyMaterial(keyMaterial);
-            }
+            return keyMaterial == null || engine.setKeyMaterial(keyMaterial);
         } catch (SSLException e) {
             throw e;
         } catch (Exception e) {
diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslKeyMaterialProvider.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslKeyMaterialProvider.java
index 72cd2e0..f931fcf 100644
--- a/handler/src/main/java/io/netty/handler/ssl/OpenSslKeyMaterialProvider.java
+++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslKeyMaterialProvider.java
@@ -16,8 +16,10 @@
 package io.netty.handler.ssl;
 
 import io.netty.buffer.ByteBufAllocator;
+import io.netty.buffer.UnpooledByteBufAllocator;
 import io.netty.internal.tcnative.SSL;
 
+import javax.net.ssl.SSLException;
 import javax.net.ssl.X509KeyManager;
 import java.security.PrivateKey;
 import java.security.cert.X509Certificate;
@@ -37,6 +39,58 @@ class OpenSslKeyMaterialProvider {
         this.password = password;
     }
 
+    static void validateKeyMaterialSupported(X509Certificate[] keyCertChain, PrivateKey key, String keyPassword)
+            throws SSLException {
+        validateSupported(keyCertChain);
+        validateSupported(key, keyPassword);
+    }
+
+    private static void validateSupported(PrivateKey key, String password) throws SSLException {
+        if (key == null) {
+            return;
+        }
+
+        long pkeyBio = 0;
+        long pkey = 0;
+
+        try {
+            pkeyBio = toBIO(UnpooledByteBufAllocator.DEFAULT, key);
+            pkey = SSL.parsePrivateKey(pkeyBio, password);
+        } catch (Exception e) {
+            throw new SSLException("PrivateKey type not supported " + key.getFormat(), e);
+        } finally {
+            SSL.freeBIO(pkeyBio);
+            if (pkey != 0) {
+                SSL.freePrivateKey(pkey);
+            }
+        }
+    }
+
+    private static void validateSupported(X509Certificate[] certificates) throws SSLException {
+        if (certificates == null || certificates.length == 0) {
+            return;
+        }
+
+        long chainBio = 0;
+        long chain = 0;
+        PemEncoded encoded = null;
+        try {
+            encoded = PemX509Certificate.toPEM(UnpooledByteBufAllocator.DEFAULT, true, certificates);
+            chainBio = toBIO(UnpooledByteBufAllocator.DEFAULT, encoded.retain());
+            chain = SSL.parseX509Chain(chainBio);
+        } catch (Exception e) {
+            throw new SSLException("Certificate type not supported", e);
+        } finally {
+            SSL.freeBIO(chainBio);
+            if (chain != 0) {
+                SSL.freeX509Chain(chain);
+            }
+            if (encoded != null) {
+                encoded.release();
+            }
+        }
+    }
+
     /**
      * Returns the underlying {@link X509KeyManager} that is used.
      */
@@ -66,7 +120,7 @@ class OpenSslKeyMaterialProvider {
 
             OpenSslKeyMaterial keyMaterial;
             if (key instanceof OpenSslPrivateKey) {
-                keyMaterial = ((OpenSslPrivateKey) key).toKeyMaterial(chain, certificates);
+                keyMaterial = ((OpenSslPrivateKey) key).newKeyMaterial(chain, certificates);
             } else {
                 pkeyBio = toBIO(allocator, key);
                 pkey = key == null ? 0 : SSL.parsePrivateKey(pkeyBio, password);
diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslPrivateKey.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslPrivateKey.java
index 67639aa..c2e4f10 100644
--- a/handler/src/main/java/io/netty/handler/ssl/OpenSslPrivateKey.java
+++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslPrivateKey.java
@@ -34,7 +34,7 @@ final class OpenSslPrivateKey extends AbstractReferenceCounted implements Privat
 
     @Override
     public String getAlgorithm() {
-        return "unkown";
+        return "unknown";
     }
 
     @Override
@@ -48,10 +48,7 @@ final class OpenSslPrivateKey extends AbstractReferenceCounted implements Privat
         return null;
     }
 
-    /**
-     * Returns the pointer to the {@code EVP_PKEY}.
-     */
-    long privateKeyAddress() {
+    private long privateKeyAddress() {
         if (refCnt() <= 0) {
             throw new IllegalReferenceCountException();
         }
@@ -94,6 +91,7 @@ final class OpenSslPrivateKey extends AbstractReferenceCounted implements Privat
      *
      * @see Destroyable#destroy()
      */
+    @Override
     public void destroy() {
         release(refCnt());
     }
@@ -105,26 +103,33 @@ final class OpenSslPrivateKey extends AbstractReferenceCounted implements Privat
      *
      * @see Destroyable#isDestroyed()
      */
+    @Override
     public boolean isDestroyed() {
         return refCnt() == 0;
     }
 
     /**
-     * Convert to a {@link OpenSslKeyMaterial}. Reference count of both is shared.
+     * Create a new {@link OpenSslKeyMaterial} which uses the private key that is held by {@link OpenSslPrivateKey}.
+     *
+     * When the material is created we increment the reference count of the enclosing {@link OpenSslPrivateKey} and
+     * decrement it again when the reference count of the {@link OpenSslKeyMaterial} reaches {@code 0}.
      */
-    OpenSslKeyMaterial toKeyMaterial(long certificateChain, X509Certificate[] chain) {
+    OpenSslKeyMaterial newKeyMaterial(long certificateChain, X509Certificate[] chain) {
         return new OpenSslPrivateKeyMaterial(certificateChain, chain);
     }
 
-    private final class OpenSslPrivateKeyMaterial implements OpenSslKeyMaterial {
+    // Package-private for unit-test only
+    final class OpenSslPrivateKeyMaterial extends AbstractReferenceCounted implements OpenSslKeyMaterial {
 
-        private long certificateChain;
+        // Package-private for unit-test only
+        long certificateChain;
         private final X509Certificate[] x509CertificateChain;
 
         OpenSslPrivateKeyMaterial(long certificateChain, X509Certificate[] x509CertificateChain) {
             this.certificateChain = certificateChain;
             this.x509CertificateChain = x509CertificateChain == null ?
                     EmptyArrays.EMPTY_X509_CERTIFICATES : x509CertificateChain;
+            OpenSslPrivateKey.this.retain();
         }
 
         @Override
@@ -142,59 +147,45 @@ final class OpenSslPrivateKey extends AbstractReferenceCounted implements Privat
 
         @Override
         public long privateKeyAddress() {
+            if (refCnt() <= 0) {
+                throw new IllegalReferenceCountException();
+            }
             return OpenSslPrivateKey.this.privateKeyAddress();
         }
 
         @Override
-        public OpenSslKeyMaterial retain() {
-            OpenSslPrivateKey.this.retain();
+        public OpenSslKeyMaterial touch(Object hint) {
+            OpenSslPrivateKey.this.touch(hint);
             return this;
         }
 
         @Override
-        public OpenSslKeyMaterial retain(int increment) {
-            OpenSslPrivateKey.this.retain(increment);
+        public OpenSslKeyMaterial retain() {
+            super.retain();
             return this;
         }
 
         @Override
-        public OpenSslKeyMaterial touch() {
-            OpenSslPrivateKey.this.touch();
+        public OpenSslKeyMaterial retain(int increment) {
+            super.retain(increment);
             return this;
         }
 
         @Override
-        public OpenSslKeyMaterial touch(Object hint) {
-            OpenSslPrivateKey.this.touch(hint);
+        public OpenSslKeyMaterial touch() {
+            OpenSslPrivateKey.this.touch();
             return this;
         }
 
         @Override
-        public boolean release() {
-            if (OpenSslPrivateKey.this.release()) {
-                releaseChain();
-                return true;
-            }
-            return false;
-        }
-
-        @Override
-        public boolean release(int decrement) {
-            if (OpenSslPrivateKey.this.release(decrement)) {
-                releaseChain();
-                return true;
-            }
-            return false;
+        protected void deallocate() {
+            releaseChain();
+            OpenSslPrivateKey.this.release();
         }
 
         private void releaseChain() {
             SSL.freeX509Chain(certificateChain);
             certificateChain = 0;
         }
-
-        @Override
-        public int refCnt() {
-            return OpenSslPrivateKey.this.refCnt();
-        }
     }
 }
diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslPrivateKeyMethod.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslPrivateKeyMethod.java
new file mode 100644
index 0000000..d9fc877
--- /dev/null
+++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslPrivateKeyMethod.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2019 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.ssl;
+
+import io.netty.internal.tcnative.SSLPrivateKeyMethod;
+import io.netty.util.internal.UnstableApi;
+
+import javax.net.ssl.SSLEngine;
+
+/**
+ * Allow to customize private key signing / decrypting (when using RSA). Only supported when using BoringSSL atm.
+ */
+@UnstableApi
+public interface OpenSslPrivateKeyMethod {
+    int SSL_SIGN_RSA_PKCS1_SHA1 = SSLPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA1;
+    int SSL_SIGN_RSA_PKCS1_SHA256 = SSLPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA256;
+    int SSL_SIGN_RSA_PKCS1_SHA384 = SSLPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA384;
+    int SSL_SIGN_RSA_PKCS1_SHA512 = SSLPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA512;
+    int SSL_SIGN_ECDSA_SHA1 = SSLPrivateKeyMethod.SSL_SIGN_ECDSA_SHA1;
+    int SSL_SIGN_ECDSA_SECP256R1_SHA256 = SSLPrivateKeyMethod.SSL_SIGN_ECDSA_SECP256R1_SHA256;
+    int SSL_SIGN_ECDSA_SECP384R1_SHA384 = SSLPrivateKeyMethod.SSL_SIGN_ECDSA_SECP384R1_SHA384;
+    int SSL_SIGN_ECDSA_SECP521R1_SHA512 = SSLPrivateKeyMethod.SSL_SIGN_ECDSA_SECP521R1_SHA512;
+    int SSL_SIGN_RSA_PSS_RSAE_SHA256 = SSLPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA256;
+    int SSL_SIGN_RSA_PSS_RSAE_SHA384 = SSLPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA384;
+    int SSL_SIGN_RSA_PSS_RSAE_SHA512 = SSLPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA512;
+    int SSL_SIGN_ED25519 = SSLPrivateKeyMethod.SSL_SIGN_ED25519;
+    int SSL_SIGN_RSA_PKCS1_MD5_SHA1 = SSLPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_MD5_SHA1;
+
+    /**
+     * Signs the input with the given key and returns the signed bytes.
+     *
+     * @param engine                the {@link SSLEngine}
+     * @param signatureAlgorithm    the algorithm to use for signing
+     * @param input                 the digest itself
+     * @return                      the signed data (must not be {@code null})
+     * @throws Exception            thrown if an error is encountered during the signing
+     */
+    byte[] sign(SSLEngine engine, int signatureAlgorithm, byte[] input) throws Exception;
+
+    /**
+     * Decrypts the input with the given key and returns the decrypted bytes.
+     *
+     * @param engine                the {@link SSLEngine}
+     * @param input                 the input which should be decrypted
+     * @return                      the decrypted data (must not be {@code null})
+     * @throws Exception            thrown if an error is encountered during the decrypting
+     */
+    byte[] decrypt(SSLEngine engine, byte[] input) throws Exception;
+}
diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslServerContext.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslServerContext.java
index e27b05a..da342de 100644
--- a/handler/src/main/java/io/netty/handler/ssl/OpenSslServerContext.java
+++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslServerContext.java
@@ -18,6 +18,7 @@ package io.netty.handler.ssl;
 import io.netty.internal.tcnative.SSL;
 
 import java.io.File;
+import java.security.KeyStore;
 import java.security.PrivateKey;
 import java.security.cert.X509Certificate;
 
@@ -321,7 +322,7 @@ public final class OpenSslServerContext extends OpenSslContext {
         this(toX509CertificatesInternal(trustCertCollectionFile), trustManagerFactory,
                 toX509CertificatesInternal(keyCertChainFile), toPrivateKeyInternal(keyFile, keyPassword),
                 keyPassword, keyManagerFactory, ciphers, cipherFilter,
-                apn, sessionCacheSize, sessionTimeout, ClientAuth.NONE, null, false, false);
+                apn, sessionCacheSize, sessionTimeout, ClientAuth.NONE, null, false, false, KeyStore.getDefaultType());
     }
 
     OpenSslServerContext(
@@ -329,10 +330,10 @@ public final class OpenSslServerContext extends OpenSslContext {
             X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory,
             Iterable<String> ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn,
             long sessionCacheSize, long sessionTimeout, ClientAuth clientAuth, String[] protocols, boolean startTls,
-            boolean enableOcsp) throws SSLException {
+            boolean enableOcsp, String keyStore) throws SSLException {
         this(trustCertCollection, trustManagerFactory, keyCertChain, key, keyPassword, keyManagerFactory, ciphers,
                 cipherFilter, toNegotiator(apn), sessionCacheSize, sessionTimeout, clientAuth, protocols, startTls,
-                enableOcsp);
+                enableOcsp, keyStore);
     }
 
     @SuppressWarnings("deprecation")
@@ -341,14 +342,16 @@ public final class OpenSslServerContext extends OpenSslContext {
             X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory,
             Iterable<String> ciphers, CipherSuiteFilter cipherFilter, OpenSslApplicationProtocolNegotiator apn,
             long sessionCacheSize, long sessionTimeout, ClientAuth clientAuth, String[] protocols, boolean startTls,
-            boolean enableOcsp) throws SSLException {
+            boolean enableOcsp, String keyStore) throws SSLException {
         super(ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, SSL.SSL_MODE_SERVER, keyCertChain,
                 clientAuth, protocols, startTls, enableOcsp);
+
         // Create a new SSL_CTX and configure it.
         boolean success = false;
         try {
+            OpenSslKeyMaterialProvider.validateKeyMaterialSupported(keyCertChain, key, keyPassword);
             sessionContext = newSessionContext(this, ctx, engineMap, trustCertCollection, trustManagerFactory,
-                                               keyCertChain, key, keyPassword, keyManagerFactory);
+                                               keyCertChain, key, keyPassword, keyManagerFactory, keyStore);
             success = true;
         } finally {
             if (!success) {
diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslSessionContext.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslSessionContext.java
index 9faefb1..f063352 100644
--- a/handler/src/main/java/io/netty/handler/ssl/OpenSslSessionContext.java
+++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslSessionContext.java
@@ -15,10 +15,10 @@
  */
 package io.netty.handler.ssl;
 
-import io.netty.util.internal.ObjectUtil;
 import io.netty.internal.tcnative.SSL;
 import io.netty.internal.tcnative.SSLContext;
 import io.netty.internal.tcnative.SessionTicketKey;
+import io.netty.util.internal.ObjectUtil;
 
 import javax.net.ssl.SSLSession;
 import javax.net.ssl.SSLSessionContext;
@@ -54,9 +54,7 @@ public abstract class OpenSslSessionContext implements SSLSessionContext {
 
     @Override
     public SSLSession getSession(byte[] bytes) {
-        if (bytes == null) {
-            throw new NullPointerException("bytes");
-        }
+        ObjectUtil.checkNotNull(bytes, "bytes");
         return null;
     }
 
diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslTlsv13X509ExtendedTrustManager.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslTlsv13X509ExtendedTrustManager.java
index 00c6886..a5a84f2 100644
--- a/handler/src/main/java/io/netty/handler/ssl/OpenSslTlsv13X509ExtendedTrustManager.java
+++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslTlsv13X509ExtendedTrustManager.java
@@ -15,19 +15,15 @@
  */
 package io.netty.handler.ssl;
 
-import io.netty.util.internal.EmptyArrays;
 import io.netty.util.internal.PlatformDependent;
+import io.netty.util.internal.SuppressJava6Requirement;
 
 import javax.net.ssl.SSLEngine;
-import javax.net.ssl.SSLEngineResult;
-import javax.net.ssl.SSLEngineResult.HandshakeStatus;
-import javax.net.ssl.SSLException;
 import javax.net.ssl.SSLPeerUnverifiedException;
 import javax.net.ssl.SSLSession;
 import javax.net.ssl.SSLSessionContext;
 import javax.net.ssl.X509ExtendedTrustManager;
 import java.net.Socket;
-import java.nio.ByteBuffer;
 import java.security.Principal;
 import java.security.cert.Certificate;
 import java.security.cert.CertificateException;
@@ -40,6 +36,7 @@ import java.util.List;
  * default {@link X509ExtendedTrustManager} implementations provided by the JDK that can not handle a protocol version
  * of {@code TLSv1.3}.
  */
+@SuppressJava6Requirement(reason = "Usage guarded by java version check")
 final class OpenSslTlsv13X509ExtendedTrustManager extends X509ExtendedTrustManager {
 
     private final X509ExtendedTrustManager tm;
@@ -48,22 +45,9 @@ final class OpenSslTlsv13X509ExtendedTrustManager extends X509ExtendedTrustManag
         this.tm = tm;
     }
 
-    static X509ExtendedTrustManager wrap(X509ExtendedTrustManager tm, boolean client) {
-        if (PlatformDependent.javaVersion() < 11) {
-            try {
-                X509Certificate[] certs = { OpenSsl.selfSignedCertificate() };
-                if (client) {
-                    tm.checkServerTrusted(certs, "RSA", new DummySSLEngine(true));
-                } else {
-                    tm.checkClientTrusted(certs, "RSA", new DummySSLEngine(false));
-                }
-            } catch (IllegalArgumentException e) {
-                // If this happened we failed because our protocol version was not known by the implementation.
-                // See http://mail.openjdk.java.net/pipermail/security-dev/2018-September/018242.html.
-                return new OpenSslTlsv13X509ExtendedTrustManager(tm);
-            } catch (Throwable ignore) {
-                // Just assume we do not need to wrap.
-            }
+    static X509ExtendedTrustManager wrap(X509ExtendedTrustManager tm) {
+        if (PlatformDependent.javaVersion() < 11 && OpenSsl.isTlsv13Supported()) {
+            return new OpenSslTlsv13X509ExtendedTrustManager(tm);
         }
         return tm;
     }
@@ -253,246 +237,4 @@ final class OpenSslTlsv13X509ExtendedTrustManager extends X509ExtendedTrustManag
     public X509Certificate[] getAcceptedIssuers() {
         return tm.getAcceptedIssuers();
     }
-
-    private static final class DummySSLEngine extends SSLEngine {
-
-        private final boolean client;
-
-        DummySSLEngine(boolean client) {
-            this.client = client;
-        }
-
-        @Override
-        public SSLSession getHandshakeSession() {
-            return new SSLSession() {
-                @Override
-                public byte[] getId() {
-                    return EmptyArrays.EMPTY_BYTES;
-                }
-
-                @Override
-                public SSLSessionContext getSessionContext() {
-                    return null;
-                }
-
-                @Override
-                public long getCreationTime() {
-                    return 0;
-                }
-
-                @Override
-                public long getLastAccessedTime() {
-                    return 0;
-                }
-
-                @Override
-                public void invalidate() {
-                    // NOOP
-                }
-
-                @Override
-                public boolean isValid() {
-                    return false;
-                }
-
-                @Override
-                public void putValue(String s, Object o) {
-                    // NOOP
-                }
-
-                @Override
-                public Object getValue(String s) {
-                    return null;
-                }
-
-                @Override
-                public void removeValue(String s) {
-                    // NOOP
-                }
-
-                @Override
-                public String[] getValueNames() {
-                    return EmptyArrays.EMPTY_STRINGS;
-                }
-
-                @Override
-                public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException {
-                    return EmptyArrays.EMPTY_CERTIFICATES;
-                }
-
-                @Override
-                public Certificate[] getLocalCertificates() {
-                    return EmptyArrays.EMPTY_CERTIFICATES;
-                }
-
-                @Override
-                public javax.security.cert.X509Certificate[] getPeerCertificateChain()
-                        throws SSLPeerUnverifiedException {
-                    return EmptyArrays.EMPTY_JAVAX_X509_CERTIFICATES;
-                }
-
-                @Override
-                public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
-                    return null;
-                }
-
-                @Override
-                public Principal getLocalPrincipal() {
-                    return null;
-                }
-
-                @Override
-                public String getCipherSuite() {
-                    return null;
-                }
-
-                @Override
-                public String getProtocol() {
-                    return SslUtils.PROTOCOL_TLS_V1_3;
-                }
-
-                @Override
-                public String getPeerHost() {
-                    return null;
-                }
-
-                @Override
-                public int getPeerPort() {
-                    return 0;
-                }
-
-                @Override
-                public int getPacketBufferSize() {
-                    return 0;
-                }
-
-                @Override
-                public int getApplicationBufferSize() {
-                    return 0;
-                }
-            };
-        }
-
-        @Override
-        public SSLEngineResult wrap(ByteBuffer[] byteBuffers, int i, int i1, ByteBuffer byteBuffer)
-                            throws SSLException {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public SSLEngineResult unwrap(ByteBuffer byteBuffer, ByteBuffer[] byteBuffers, int i, int i1)
-                            throws SSLException {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public Runnable getDelegatedTask() {
-            return null;
-        }
-
-        @Override
-        public void closeInbound() throws SSLException {
-            // NOOP
-        }
-
-        @Override
-        public boolean isInboundDone() {
-            return true;
-        }
-
-        @Override
-        public void closeOutbound() {
-            // NOOP
-        }
-
-        @Override
-        public boolean isOutboundDone() {
-            return true;
-        }
-
-        @Override
-        public String[] getSupportedCipherSuites() {
-            return EmptyArrays.EMPTY_STRINGS;
-        }
-
-        @Override
-        public String[] getEnabledCipherSuites() {
-            return EmptyArrays.EMPTY_STRINGS;
-        }
-
-        @Override
-        public void setEnabledCipherSuites(String[] strings) {
-            // NOOP
-        }
-
-        @Override
-        public String[] getSupportedProtocols() {
-            return new String[] { SslUtils.PROTOCOL_TLS_V1_3 };
-        }
-
-        @Override
-        public String[] getEnabledProtocols() {
-            return new String[] { SslUtils.PROTOCOL_TLS_V1_3 };
-        }
-
-        @Override
-        public void setEnabledProtocols(String[] strings) {
-            // NOOP
-        }
-
-        @Override
-        public SSLSession getSession() {
-            return getHandshakeSession();
-        }
-
-        @Override
-        public void beginHandshake() throws SSLException {
-            // NOOP
-        }
-
-        @Override
-        public HandshakeStatus getHandshakeStatus() {
-            return HandshakeStatus.NEED_TASK;
-        }
-
-        @Override
-        public void setUseClientMode(boolean b) {
-            // NOOP
-        }
-
-        @Override
-        public boolean getUseClientMode() {
-            return client;
-        }
-
-        @Override
-        public void setNeedClientAuth(boolean b) {
-            // NOOP
-        }
-
-        @Override
-        public boolean getNeedClientAuth() {
-            return false;
-        }
-
-        @Override
-        public void setWantClientAuth(boolean b) {
-            // NOOP
-        }
-
-        @Override
-        public boolean getWantClientAuth() {
-            return false;
-        }
-
-        @Override
-        public void setEnableSessionCreation(boolean b) {
-            // NOOP
-        }
-
-        @Override
-        public boolean getEnableSessionCreation() {
-            return false;
-        }
-    }
 }
diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslX509Certificate.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslX509Certificate.java
index b94a195..d3188eb 100644
--- a/handler/src/main/java/io/netty/handler/ssl/OpenSslX509Certificate.java
+++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslX509Certificate.java
@@ -15,6 +15,8 @@
  */
 package io.netty.handler.ssl;
 
+import io.netty.util.internal.SuppressJava6Requirement;
+
 import javax.security.auth.x500.X500Principal;
 import java.io.ByteArrayInputStream;
 import java.math.BigInteger;
@@ -41,7 +43,7 @@ final class OpenSslX509Certificate extends X509Certificate {
     private final byte[] bytes;
     private X509Certificate wrapped;
 
-    public OpenSslX509Certificate(byte[] bytes) {
+    OpenSslX509Certificate(byte[] bytes) {
         this.bytes = bytes;
     }
 
@@ -81,6 +83,7 @@ final class OpenSslX509Certificate extends X509Certificate {
     }
 
     // No @Override annotation as it was only introduced in Java8.
+    @SuppressJava6Requirement(reason = "Can only be called from Java8 as class is package-private")
     public void verify(PublicKey key, Provider sigProvider)
             throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, SignatureException {
         unwrap().verify(key, sigProvider);
diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslX509KeyManagerFactory.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslX509KeyManagerFactory.java
index 90d94cb..2a86332 100644
--- a/handler/src/main/java/io/netty/handler/ssl/OpenSslX509KeyManagerFactory.java
+++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslX509KeyManagerFactory.java
@@ -52,6 +52,8 @@ import java.util.Map;
  * Special {@link KeyManagerFactory} that pre-compute the keymaterial used when {@link SslProvider#OPENSSL} or
  * {@link SslProvider#OPENSSL_REFCNT} is used and so will improve handshake times and its performance.
  *
+ *
+ *
  * Because the keymaterial is pre-computed any modification to the {@link KeyStore} is ignored after
  * {@link #init(KeyStore, char[])} is called.
  *
@@ -252,15 +254,47 @@ public final class OpenSslX509KeyManagerFactory extends KeyManagerFactory {
     public static OpenSslX509KeyManagerFactory newEngineBased(X509Certificate[] certificateChain, String password)
             throws CertificateException, IOException,
                    KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
-        KeyStore store = new OpenSslEngineKeyStore(certificateChain.clone());
+        KeyStore store = new OpenSslKeyStore(certificateChain.clone(), false);
         store.load(null, null);
         OpenSslX509KeyManagerFactory factory = new OpenSslX509KeyManagerFactory();
         factory.init(store, password == null ? null : password.toCharArray());
         return factory;
     }
 
-    private static final class OpenSslEngineKeyStore extends KeyStore {
-        private OpenSslEngineKeyStore(final X509Certificate[] certificateChain) {
+    /**
+     * See {@link OpenSslX509KeyManagerFactory#newEngineBased(X509Certificate[], String)}.
+     */
+    public static OpenSslX509KeyManagerFactory newKeyless(File chain)
+            throws CertificateException, IOException,
+            KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
+        return newKeyless(SslContext.toX509Certificates(chain));
+    }
+
+    /**
+     * See {@link OpenSslX509KeyManagerFactory#newEngineBased(X509Certificate[], String)}.
+     */
+    public static OpenSslX509KeyManagerFactory newKeyless(InputStream chain)
+            throws CertificateException, IOException,
+            KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
+        return newKeyless(SslContext.toX509Certificates(chain));
+    }
+
+    /**
+     * Returns a new initialized {@link OpenSslX509KeyManagerFactory} which will provide its private key by using the
+     * {@link OpenSslPrivateKeyMethod}.
+     */
+    public static OpenSslX509KeyManagerFactory newKeyless(X509Certificate... certificateChain)
+            throws CertificateException, IOException,
+            KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
+        KeyStore store = new OpenSslKeyStore(certificateChain.clone(), true);
+        store.load(null, null);
+        OpenSslX509KeyManagerFactory factory = new OpenSslX509KeyManagerFactory();
+        factory.init(store, null);
+        return factory;
+    }
+
+    private static final class OpenSslKeyStore extends KeyStore {
+        private OpenSslKeyStore(final X509Certificate[] certificateChain, final boolean keyless) {
             super(new KeyStoreSpi() {
 
                 private final Date creationDate = new Date();
@@ -268,15 +302,21 @@ public final class OpenSslX509KeyManagerFactory extends KeyManagerFactory {
                 @Override
                 public Key engineGetKey(String alias, char[] password) throws UnrecoverableKeyException {
                     if (engineContainsAlias(alias)) {
-                        try {
-                            return new OpenSslPrivateKey(SSL.loadPrivateKeyFromEngine(
-                                            alias, password == null ? null : new String(password)));
-                        } catch (Exception e) {
-                            UnrecoverableKeyException keyException =
-                                    new UnrecoverableKeyException("Unable to load key from engine");
-                            keyException.initCause(e);
-                            throw keyException;
+                        final long privateKeyAddress;
+                        if (keyless) {
+                            privateKeyAddress = 0;
+                        } else {
+                            try {
+                                privateKeyAddress = SSL.loadPrivateKeyFromEngine(
+                                        alias, password == null ? null : new String(password));
+                            } catch (Exception e) {
+                                UnrecoverableKeyException keyException =
+                                        new UnrecoverableKeyException("Unable to load key from engine");
+                                keyException.initCause(e);
+                                throw keyException;
+                            }
                         }
+                        return new OpenSslPrivateKey(privateKeyAddress);
                     }
                     return null;
                 }
diff --git a/handler/src/main/java/io/netty/handler/ssl/OpenSslX509TrustManagerWrapper.java b/handler/src/main/java/io/netty/handler/ssl/OpenSslX509TrustManagerWrapper.java
index 0a0db0b..b3ddcc1 100644
--- a/handler/src/main/java/io/netty/handler/ssl/OpenSslX509TrustManagerWrapper.java
+++ b/handler/src/main/java/io/netty/handler/ssl/OpenSslX509TrustManagerWrapper.java
@@ -17,6 +17,7 @@ package io.netty.handler.ssl;
 
 import io.netty.util.internal.EmptyArrays;
 import io.netty.util.internal.PlatformDependent;
+import io.netty.util.internal.SuppressJava6Requirement;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 
@@ -39,6 +40,7 @@ import java.security.cert.X509Certificate;
  * This is really a "hack" until there is an official API as requested on the in
  * <a href="https://bugs.openjdk.java.net/projects/JDK/issues/JDK-8210843">JDK-8210843</a>.
  */
+@SuppressJava6Requirement(reason = "Usage guarded by java version check")
 final class OpenSslX509TrustManagerWrapper {
     private static final InternalLogger LOGGER = InternalLoggerFactory
             .getInstance(OpenSslX509TrustManagerWrapper.class);
@@ -71,11 +73,13 @@ final class OpenSslX509TrustManagerWrapper {
                             @Override
                             public void checkClientTrusted(X509Certificate[] x509Certificates, String s)
                                     throws CertificateException {
+                                throw new CertificateException();
                             }
 
                             @Override
                             public void checkServerTrusted(X509Certificate[] x509Certificates, String s)
                                     throws CertificateException {
+                                throw new CertificateException();
                             }
 
                             @Override
@@ -161,6 +165,7 @@ final class OpenSslX509TrustManagerWrapper {
             this.tmOffset = tmOffset;
         }
 
+        @SuppressJava6Requirement(reason = "Usage guarded by java version check")
         @Override
         public X509TrustManager wrapIfNeeded(X509TrustManager manager) {
             if (!(manager instanceof X509ExtendedTrustManager)) {
diff --git a/handler/src/main/java/io/netty/handler/ssl/PemPrivateKey.java b/handler/src/main/java/io/netty/handler/ssl/PemPrivateKey.java
index 46145a0..cdb6b64 100644
--- a/handler/src/main/java/io/netty/handler/ssl/PemPrivateKey.java
+++ b/handler/src/main/java/io/netty/handler/ssl/PemPrivateKey.java
@@ -65,6 +65,10 @@ public final class PemPrivateKey extends AbstractReferenceCounted implements Pri
             throw new IllegalArgumentException(key.getClass().getName() + " does not support encoding");
         }
 
+        return toPEM(allocator, useDirect, bytes);
+    }
+
+    static PemEncoded toPEM(ByteBufAllocator allocator, boolean useDirect, byte[] bytes) {
         ByteBuf encoded = Unpooled.wrappedBuffer(bytes);
         try {
             ByteBuf base64 = SslUtils.toBase64(allocator, encoded);
@@ -207,6 +211,7 @@ public final class PemPrivateKey extends AbstractReferenceCounted implements Pri
      *
      * @see Destroyable#destroy()
      */
+    @Override
     public void destroy() {
         release(refCnt());
     }
@@ -218,6 +223,7 @@ public final class PemPrivateKey extends AbstractReferenceCounted implements Pri
      *
      * @see Destroyable#isDestroyed()
      */
+    @Override
     public boolean isDestroyed() {
         return refCnt() == 0;
     }
diff --git a/handler/src/main/java/io/netty/handler/ssl/PemReader.java b/handler/src/main/java/io/netty/handler/ssl/PemReader.java
index 4cddad9..a3a351b 100644
--- a/handler/src/main/java/io/netty/handler/ssl/PemReader.java
+++ b/handler/src/main/java/io/netty/handler/ssl/PemReader.java
@@ -126,7 +126,7 @@ final class PemReader {
         Matcher m = KEY_PATTERN.matcher(content);
         if (!m.find()) {
             throw new KeyException("could not find a PKCS #8 private key in input stream" +
-                    " (see http://netty.io/wiki/sslcontextbuilder-and-private-key.html for more information)");
+                    " (see https://netty.io/wiki/sslcontextbuilder-and-private-key.html for more information)");
         }
 
         ByteBuf base64 = Unpooled.copiedBuffer(m.group(1), CharsetUtil.US_ASCII);
diff --git a/handler/src/main/java/io/netty/handler/ssl/PemValue.java b/handler/src/main/java/io/netty/handler/ssl/PemValue.java
index becb5b8..ada5e4d 100644
--- a/handler/src/main/java/io/netty/handler/ssl/PemValue.java
+++ b/handler/src/main/java/io/netty/handler/ssl/PemValue.java
@@ -34,7 +34,7 @@ class PemValue extends AbstractReferenceCounted implements PemEncoded {
 
     private final boolean sensitive;
 
-    public PemValue(ByteBuf content, boolean sensitive) {
+    PemValue(ByteBuf content, boolean sensitive) {
         this.content = ObjectUtil.checkNotNull(content, "content");
         this.sensitive = sensitive;
     }
diff --git a/handler/src/main/java/io/netty/handler/ssl/PseudoRandomFunction.java b/handler/src/main/java/io/netty/handler/ssl/PseudoRandomFunction.java
new file mode 100644
index 0000000..77eea0b
--- /dev/null
+++ b/handler/src/main/java/io/netty/handler/ssl/PseudoRandomFunction.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2019 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.ssl;
+
+import io.netty.util.internal.EmptyArrays;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.security.GeneralSecurityException;
+import java.util.Arrays;
+
+/**
+ * This pseudorandom function (PRF) takes as input a secret, a seed, and
+ * an identifying label and produces an output of arbitrary length.
+ *
+ * This is used by the TLS RFC to construct/deconstruct an array of bytes into
+ * composite secrets.
+ *
+ * {@link <a href="https://tools.ietf.org/html/rfc5246">rfc5246</a>}
+ */
+final class PseudoRandomFunction {
+
+    /**
+     * Constructor never to be called.
+     */
+    private PseudoRandomFunction() {
+    }
+
+    /**
+     * Use a single hash function to expand a secret and seed into an
+     * arbitrary quantity of output.
+     *
+     * P_hash(secret, seed) = HMAC_hash(secret, A(1) + seed) +
+     *                        HMAC_hash(secret, A(2) + seed) +
+     *                        HMAC_hash(secret, A(3) + seed) + ...
+     * where + indicates concatenation.
+     * A() is defined as:
+     *       A(0) = seed
+     *       A(i) = HMAC_hash(secret, A(i-1))
+     * @param secret The starting secret to use for expansion
+     * @param label An ascii string without a length byte or trailing null character.
+     * @param seed The seed of the hash
+     * @param length The number of bytes to return
+     * @param algo the hmac algorithm to use
+     * @return The expanded secrets
+     * @throws IllegalArgumentException if the algo could not be found.
+     */
+    static byte[] hash(byte[] secret, byte[] label, byte[] seed, int length, String algo) {
+        if (length < 0) {
+            throw new IllegalArgumentException("You must provide a length greater than zero.");
+        }
+        try {
+            Mac hmac = Mac.getInstance(algo);
+            hmac.init(new SecretKeySpec(secret, algo));
+            /*
+             * P_hash(secret, seed) = HMAC_hash(secret, A(1) + seed) +
+             * HMAC_hash(secret, A(2) + seed) + HMAC_hash(secret, A(3) + seed) + ...
+             * where + indicates concatenation. A() is defined as: A(0) = seed, A(i)
+             * = HMAC_hash(secret, A(i-1))
+             */
+
+            int iterations = (int) Math.ceil(length / (double) hmac.getMacLength());
+            byte[] expansion = EmptyArrays.EMPTY_BYTES;
+            byte[] data = concat(label, seed);
+            byte[] A = data;
+            for (int i = 0; i < iterations; i++) {
+                A = hmac.doFinal(A);
+                expansion = concat(expansion, hmac.doFinal(concat(A, data)));
+            }
+            return Arrays.copyOf(expansion, length);
+        } catch (GeneralSecurityException e) {
+            throw new IllegalArgumentException("Could not find algo: " + algo, e);
+        }
+    }
+
+    private static byte[] concat(byte[] first, byte[] second) {
+        byte[] result = Arrays.copyOf(first, first.length + second.length);
+        System.arraycopy(second, 0, result, first.length, second.length);
+        return result;
+    }
+}
diff --git a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslClientContext.java b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslClientContext.java
index f508971..56893b3 100644
--- a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslClientContext.java
+++ b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslClientContext.java
@@ -16,6 +16,7 @@
 package io.netty.handler.ssl;
 
 import io.netty.internal.tcnative.CertificateCallback;
+import io.netty.util.internal.SuppressJava6Requirement;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 import io.netty.internal.tcnative.SSL;
@@ -33,7 +34,6 @@ import java.util.Set;
 
 import javax.net.ssl.KeyManagerFactory;
 import javax.net.ssl.SSLException;
-import javax.net.ssl.SSLHandshakeException;
 import javax.net.ssl.TrustManagerFactory;
 import javax.net.ssl.X509ExtendedTrustManager;
 import javax.net.ssl.X509TrustManager;
@@ -63,13 +63,13 @@ public final class ReferenceCountedOpenSslClientContext extends ReferenceCounted
                                          KeyManagerFactory keyManagerFactory, Iterable<String> ciphers,
                                          CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn,
                                          String[] protocols, long sessionCacheSize, long sessionTimeout,
-                                         boolean enableOcsp) throws SSLException {
+                                         boolean enableOcsp, String keyStore) throws SSLException {
         super(ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, SSL.SSL_MODE_CLIENT, keyCertChain,
               ClientAuth.NONE, protocols, false, enableOcsp, true);
         boolean success = false;
         try {
             sessionContext = newSessionContext(this, ctx, engineMap, trustCertCollection, trustManagerFactory,
-                                               keyCertChain, key, keyPassword, keyManagerFactory);
+                                               keyCertChain, key, keyPassword, keyManagerFactory, keyStore);
             success = true;
         } finally {
             if (!success) {
@@ -87,8 +87,9 @@ public final class ReferenceCountedOpenSslClientContext extends ReferenceCounted
                                                    OpenSslEngineMap engineMap,
                                                    X509Certificate[] trustCertCollection,
                                                    TrustManagerFactory trustManagerFactory,
-                                                   X509Certificate[] keyCertChain, PrivateKey key, String keyPassword,
-                                                   KeyManagerFactory keyManagerFactory) throws SSLException {
+                                                   X509Certificate[] keyCertChain, PrivateKey key,
+                                                   String keyPassword, KeyManagerFactory keyManagerFactory,
+                                                   String keyStore) throws SSLException {
         if (key == null && keyCertChain != null || key != null && keyCertChain == null) {
             throw new IllegalArgumentException(
                     "Either both keyCertChain and key needs to be null or none of them");
@@ -108,7 +109,7 @@ public final class ReferenceCountedOpenSslClientContext extends ReferenceCounted
                     // javadocs state that keyManagerFactory has precedent over keyCertChain
                     if (keyManagerFactory == null && keyCertChain != null) {
                         char[] keyPasswordChars = keyStorePassword(keyPassword);
-                        KeyStore ks = buildKeyStore(keyCertChain, key, keyPasswordChars);
+                        KeyStore ks = buildKeyStore(keyCertChain, key, keyPasswordChars, keyStore);
                         if (ks.aliases().hasMoreElements()) {
                             keyManagerFactory = new OpenSslX509KeyManagerFactory();
                         } else {
@@ -131,11 +132,17 @@ public final class ReferenceCountedOpenSslClientContext extends ReferenceCounted
                 throw new SSLException("failed to set certificate and key", e);
             }
 
-            SSLContext.setVerify(ctx, SSL.SSL_CVERIFY_NONE, VERIFY_DEPTH);
+            // On the client side we always need to use SSL_CVERIFY_OPTIONAL (which will translate to SSL_VERIFY_PEER)
+            // to ensure that when the TrustManager throws we will produce the correct alert back to the server.
+            //
+            // See:
+            //   - https://www.openssl.org/docs/man1.0.2/man3/SSL_CTX_set_verify.html
+            //   - https://github.com/netty/netty/issues/8942
+            SSLContext.setVerify(ctx, SSL.SSL_CVERIFY_OPTIONAL, VERIFY_DEPTH);
 
             try {
                 if (trustCertCollection != null) {
-                    trustManagerFactory = buildTrustManagerFactory(trustCertCollection, trustManagerFactory);
+                    trustManagerFactory = buildTrustManagerFactory(trustCertCollection, trustManagerFactory, keyStore);
                 } else if (trustManagerFactory == null) {
                     trustManagerFactory = TrustManagerFactory.getInstance(
                             TrustManagerFactory.getDefaultAlgorithm());
@@ -149,13 +156,7 @@ public final class ReferenceCountedOpenSslClientContext extends ReferenceCounted
                 //
                 //            See https://github.com/netty/netty/issues/5372
 
-                // Use this to prevent an error when running on java < 7
-                if (useExtendedTrustManager(manager)) {
-                    SSLContext.setCertVerifyCallback(ctx,
-                            new ExtendedTrustManagerVerifyCallback(engineMap, (X509ExtendedTrustManager) manager));
-                } else {
-                    SSLContext.setCertVerifyCallback(ctx, new TrustManagerVerifyCallback(engineMap, manager));
-                }
+                setVerifyCallback(ctx, engineMap, manager);
             } catch (Exception e) {
                 if (keyMaterialProvider != null) {
                     keyMaterialProvider.destroy();
@@ -172,6 +173,17 @@ public final class ReferenceCountedOpenSslClientContext extends ReferenceCounted
         }
     }
 
+    @SuppressJava6Requirement(reason = "Guarded by java version check")
+    private static void setVerifyCallback(long ctx, OpenSslEngineMap engineMap, X509TrustManager manager) {
+        // Use this to prevent an error when running on java < 7
+        if (useExtendedTrustManager(manager)) {
+            SSLContext.setCertVerifyCallback(ctx,
+                    new ExtendedTrustManagerVerifyCallback(engineMap, (X509ExtendedTrustManager) manager));
+        } else {
+            SSLContext.setCertVerifyCallback(ctx, new TrustManagerVerifyCallback(engineMap, manager));
+        }
+    }
+
     // No cache is currently supported for client side mode.
     static final class OpenSslClientSessionContext extends OpenSslSessionContext {
         OpenSslClientSessionContext(ReferenceCountedOpenSslContext context, OpenSslKeyMaterialProvider provider) {
@@ -228,12 +240,13 @@ public final class ReferenceCountedOpenSslClientContext extends ReferenceCounted
         }
     }
 
+    @SuppressJava6Requirement(reason = "Usage guarded by java version check")
     private static final class ExtendedTrustManagerVerifyCallback extends AbstractCertificateVerifier {
         private final X509ExtendedTrustManager manager;
 
         ExtendedTrustManagerVerifyCallback(OpenSslEngineMap engineMap, X509ExtendedTrustManager manager) {
             super(engineMap);
-            this.manager = OpenSslTlsv13X509ExtendedTrustManager.wrap(manager, true);
+            this.manager = OpenSslTlsv13X509ExtendedTrustManager.wrap(manager);
         }
 
         @Override
@@ -255,6 +268,10 @@ public final class ReferenceCountedOpenSslClientContext extends ReferenceCounted
         @Override
         public void handle(long ssl, byte[] keyTypeBytes, byte[][] asn1DerEncodedPrincipals) throws Exception {
             final ReferenceCountedOpenSslEngine engine = engineMap.get(ssl);
+            // May be null if it was destroyed in the meantime.
+            if (engine == null) {
+                return;
+            }
             try {
                 final Set<String> keyTypesSet = supportedClientKeyTypes(keyTypeBytes);
                 final String[] keyTypes = keyTypesSet.toArray(new String[0]);
@@ -270,9 +287,7 @@ public final class ReferenceCountedOpenSslClientContext extends ReferenceCounted
                 keyManagerHolder.setKeyMaterialClientSide(engine, keyTypes, issuers);
             } catch (Throwable cause) {
                 logger.debug("request of key failed", cause);
-                SSLHandshakeException e = new SSLHandshakeException("General OpenSslEngine problem");
-                e.initCause(cause);
-                engine.handshakeException = e;
+                engine.initHandshakeException(cause);
             }
         }
 
diff --git a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslContext.java b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslContext.java
index da1fdb1..bc72db4 100644
--- a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslContext.java
+++ b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslContext.java
@@ -20,20 +20,23 @@ import io.netty.buffer.ByteBufAllocator;
 import io.netty.internal.tcnative.CertificateVerifier;
 import io.netty.internal.tcnative.SSL;
 import io.netty.internal.tcnative.SSLContext;
+import io.netty.internal.tcnative.SSLPrivateKeyMethod;
 import io.netty.util.AbstractReferenceCounted;
 import io.netty.util.ReferenceCounted;
 import io.netty.util.ResourceLeakDetector;
 import io.netty.util.ResourceLeakDetectorFactory;
 import io.netty.util.ResourceLeakTracker;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.PlatformDependent;
 import io.netty.util.internal.StringUtil;
+import io.netty.util.internal.SuppressJava6Requirement;
 import io.netty.util.internal.SystemPropertyUtil;
+import io.netty.util.internal.UnstableApi;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 
-import java.security.AccessController;
 import java.security.PrivateKey;
-import java.security.PrivilegedAction;
+import java.security.SignatureException;
 import java.security.cert.CertPathValidatorException;
 import java.security.cert.Certificate;
 import java.security.cert.CertificateExpiredException;
@@ -44,6 +47,7 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.Executor;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReadWriteLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
@@ -76,16 +80,11 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen
     private static final InternalLogger logger =
             InternalLoggerFactory.getInstance(ReferenceCountedOpenSslContext.class);
 
-    private static final int DEFAULT_BIO_NON_APPLICATION_BUFFER_SIZE =
-            AccessController.doPrivileged(new PrivilegedAction<Integer>() {
-                @Override
-                public Integer run() {
-                    return Math.max(1,
-                            SystemPropertyUtil.getInt("io.netty.handler.ssl.openssl.bioNonApplicationBufferSize",
-                                                      2048));
-                }
-            });
-
+    private static final int DEFAULT_BIO_NON_APPLICATION_BUFFER_SIZE = Math.max(1,
+            SystemPropertyUtil.getInt("io.netty.handler.ssl.openssl.bioNonApplicationBufferSize",
+                    2048));
+    static final boolean USE_TASKS =
+            SystemPropertyUtil.getBoolean("io.netty.handler.ssl.openssl.useTasks", false);
     private static final Integer DH_KEY_LENGTH;
     private static final ResourceLeakDetector<ReferenceCountedOpenSslContext> leakDetector =
             ResourceLeakDetectorFactory.instance().newResourceLeakDetector(ReferenceCountedOpenSslContext.class);
@@ -164,12 +163,7 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen
         Integer dhLen = null;
 
         try {
-            String dhKeySize = AccessController.doPrivileged(new PrivilegedAction<String>() {
-                @Override
-                public String run() {
-                    return SystemPropertyUtil.get("jdk.tls.ephemeralDHKeySize");
-                }
-            });
+            String dhKeySize = SystemPropertyUtil.get("jdk.tls.ephemeralDHKeySize");
             if (dhKeySize != null) {
                 try {
                     dhLen = Integer.valueOf(dhKeySize);
@@ -341,6 +335,8 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen
             if (enableOcsp) {
                 SSLContext.enableOcsp(ctx, isClient());
             }
+
+            SSLContext.setUseTasks(ctx, USE_TASKS);
             success = true;
         } finally {
             if (!success) {
@@ -400,6 +396,17 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen
         return new SslHandler(newEngine0(alloc, peerHost, peerPort, false), startTls);
     }
 
+    @Override
+    protected SslHandler newHandler(ByteBufAllocator alloc, boolean startTls, Executor executor) {
+        return new SslHandler(newEngine0(alloc, null, -1, false), startTls, executor);
+    }
+
+    @Override
+    protected SslHandler newHandler(ByteBufAllocator alloc, String peerHost, int peerPort,
+                                    boolean startTls, Executor executor) {
+        return new SslHandler(newEngine0(alloc, peerHost, peerPort, false), executor);
+    }
+
     SSLEngine newEngine0(ByteBufAllocator alloc, String peerHost, int peerPort, boolean jdkCompatibilityMode) {
         return new ReferenceCountedOpenSslEngine(this, alloc, peerHost, peerPort, jdkCompatibilityMode, true);
     }
@@ -502,6 +509,37 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen
         }
     }
 
+    /**
+     * Set the {@link OpenSslPrivateKeyMethod} to use. This allows to offload private-key operations
+     * if needed.
+     *
+     * This method is currently only supported when {@code BoringSSL} is used.
+     *
+     * @param method method to use.
+     */
+    @UnstableApi
+    public final void setPrivateKeyMethod(OpenSslPrivateKeyMethod method) {
+        ObjectUtil.checkNotNull(method, "method");
+        Lock writerLock = ctxLock.writeLock();
+        writerLock.lock();
+        try {
+            SSLContext.setPrivateKeyMethod(ctx, new PrivateKeyMethod(engineMap, method));
+        } finally {
+            writerLock.unlock();
+        }
+    }
+
+    // Exposed for testing only
+    final void setUseTasks(boolean useTasks) {
+        Lock writerLock = ctxLock.writeLock();
+        writerLock.lock();
+        try {
+            SSLContext.setUseTasks(ctx, useTasks);
+        } finally {
+            writerLock.unlock();
+        }
+    }
+
     // IMPORTANT: This method must only be called from either the constructor or the finalizer as a user MUST never
     //            get access to an OpenSslSessionContext after this method was called to prevent the user from
     //            producing a segfault.
@@ -538,7 +576,10 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen
     protected static X509TrustManager chooseTrustManager(TrustManager[] managers) {
         for (TrustManager m : managers) {
             if (m instanceof X509TrustManager) {
-                return OpenSslX509TrustManagerWrapper.wrapIfNeeded((X509TrustManager) m);
+                if (PlatformDependent.javaVersion() >= 7) {
+                    return OpenSslX509TrustManagerWrapper.wrapIfNeeded((X509TrustManager) m);
+                }
+                return (X509TrustManager) m;
             }
         }
         throw new IllegalStateException("no X509TrustManager found");
@@ -597,6 +638,7 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen
         }
     }
 
+    @SuppressJava6Requirement(reason = "Guarded by java version check")
     static boolean useExtendedTrustManager(X509TrustManager trustManager) {
         return PlatformDependent.javaVersion() >= 7 && trustManager instanceof X509ExtendedTrustManager;
     }
@@ -649,16 +691,18 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen
 
         @Override
         public final int verify(long ssl, byte[][] chain, String auth) {
-            X509Certificate[] peerCerts = certificates(chain);
             final ReferenceCountedOpenSslEngine engine = engineMap.get(ssl);
+            if (engine == null) {
+                // May be null if it was destroyed in the meantime.
+                return CertificateVerifier.X509_V_ERR_UNSPECIFIED;
+            }
+            X509Certificate[] peerCerts = certificates(chain);
             try {
                 verify(engine, peerCerts, auth);
                 return CertificateVerifier.X509_V_OK;
             } catch (Throwable cause) {
                 logger.debug("verification of certificate failed", cause);
-                SSLHandshakeException e = new SSLHandshakeException("General OpenSslEngine problem");
-                e.initCause(cause);
-                engine.handshakeException = e;
+                engine.initHandshakeException(cause);
 
                 // Try to extract the correct error code that should be used.
                 if (cause instanceof OpenSslCertificateException) {
@@ -673,30 +717,7 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen
                     return CertificateVerifier.X509_V_ERR_CERT_NOT_YET_VALID;
                 }
                 if (PlatformDependent.javaVersion() >= 7) {
-                    if (cause instanceof CertificateRevokedException) {
-                        return CertificateVerifier.X509_V_ERR_CERT_REVOKED;
-                    }
-
-                    // The X509TrustManagerImpl uses a Validator which wraps a CertPathValidatorException into
-                    // an CertificateException. So we need to handle the wrapped CertPathValidatorException to be
-                    // able to send the correct alert.
-                    Throwable wrapped = cause.getCause();
-                    while (wrapped != null) {
-                        if (wrapped instanceof CertPathValidatorException) {
-                            CertPathValidatorException ex = (CertPathValidatorException) wrapped;
-                            CertPathValidatorException.Reason reason = ex.getReason();
-                            if (reason == CertPathValidatorException.BasicReason.EXPIRED) {
-                                return CertificateVerifier.X509_V_ERR_CERT_HAS_EXPIRED;
-                            }
-                            if (reason == CertPathValidatorException.BasicReason.NOT_YET_VALID) {
-                                return CertificateVerifier.X509_V_ERR_CERT_NOT_YET_VALID;
-                            }
-                            if (reason == CertPathValidatorException.BasicReason.REVOKED) {
-                                return CertificateVerifier.X509_V_ERR_CERT_REVOKED;
-                            }
-                        }
-                        wrapped = wrapped.getCause();
-                    }
+                    return translateToError(cause);
                 }
 
                 // Could not detect a specific error code to use, so fallback to a default code.
@@ -704,6 +725,35 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen
             }
         }
 
+        @SuppressJava6Requirement(reason = "Usage guarded by java version check")
+        private static int translateToError(Throwable cause) {
+            if (cause instanceof CertificateRevokedException) {
+                return CertificateVerifier.X509_V_ERR_CERT_REVOKED;
+            }
+
+            // The X509TrustManagerImpl uses a Validator which wraps a CertPathValidatorException into
+            // an CertificateException. So we need to handle the wrapped CertPathValidatorException to be
+            // able to send the correct alert.
+            Throwable wrapped = cause.getCause();
+            while (wrapped != null) {
+                if (wrapped instanceof CertPathValidatorException) {
+                    CertPathValidatorException ex = (CertPathValidatorException) wrapped;
+                    CertPathValidatorException.Reason reason = ex.getReason();
+                    if (reason == CertPathValidatorException.BasicReason.EXPIRED) {
+                        return CertificateVerifier.X509_V_ERR_CERT_HAS_EXPIRED;
+                    }
+                    if (reason == CertPathValidatorException.BasicReason.NOT_YET_VALID) {
+                        return CertificateVerifier.X509_V_ERR_CERT_NOT_YET_VALID;
+                    }
+                    if (reason == CertPathValidatorException.BasicReason.REVOKED) {
+                        return CertificateVerifier.X509_V_ERR_CERT_REVOKED;
+                    }
+                }
+                wrapped = wrapped.getCause();
+            }
+            return CertificateVerifier.X509_V_ERR_UNSPECIFIED;
+        }
+
         abstract void verify(ReferenceCountedOpenSslEngine engine, X509Certificate[] peerCerts,
                              String auth) throws Exception;
     }
@@ -861,12 +911,59 @@ public abstract class ReferenceCountedOpenSslContext extends SslContext implemen
             return ((OpenSslX509KeyManagerFactory) factory).newProvider();
         }
 
-        X509KeyManager keyManager = chooseX509KeyManager(factory.getKeyManagers());
         if (factory instanceof OpenSslCachingX509KeyManagerFactory) {
             // The user explicit used OpenSslCachingX509KeyManagerFactory which signals us that its fine to cache.
-            return new OpenSslCachingKeyMaterialProvider(keyManager, password);
+            return ((OpenSslCachingX509KeyManagerFactory) factory).newProvider(password);
         }
         // We can not be sure if the material may change at runtime so we will not cache it.
-        return new OpenSslKeyMaterialProvider(keyManager, password);
+        return new OpenSslKeyMaterialProvider(chooseX509KeyManager(factory.getKeyManagers()), password);
+    }
+
+    private static final class PrivateKeyMethod implements SSLPrivateKeyMethod {
+
+        private final OpenSslEngineMap engineMap;
+        private final OpenSslPrivateKeyMethod keyMethod;
+        PrivateKeyMethod(OpenSslEngineMap engineMap, OpenSslPrivateKeyMethod keyMethod) {
+            this.engineMap = engineMap;
+            this.keyMethod = keyMethod;
+        }
+
+        private ReferenceCountedOpenSslEngine retrieveEngine(long ssl) throws SSLException {
+            ReferenceCountedOpenSslEngine engine = engineMap.get(ssl);
+            if (engine == null) {
+                throw new SSLException("Could not find a " +
+                        StringUtil.simpleClassName(ReferenceCountedOpenSslEngine.class) + " for sslPointer " + ssl);
+            }
+            return engine;
+        }
+
+        @Override
+        public byte[] sign(long ssl, int signatureAlgorithm, byte[] digest) throws Exception {
+            ReferenceCountedOpenSslEngine engine = retrieveEngine(ssl);
+            try {
+                return verifyResult(keyMethod.sign(engine, signatureAlgorithm, digest));
+            } catch (Exception e) {
+                engine.initHandshakeException(e);
+                throw e;
+            }
+        }
+
+        @Override
+        public byte[] decrypt(long ssl, byte[] input) throws Exception {
+            ReferenceCountedOpenSslEngine engine = retrieveEngine(ssl);
+            try {
+                return verifyResult(keyMethod.decrypt(engine, input));
+            } catch (Exception e) {
+                engine.initHandshakeException(e);
+                throw e;
+            }
+        }
+
+        private static byte[] verifyResult(byte[] result) throws SignatureException {
+            if (result == null) {
+                throw new SignatureException();
+            }
+            return result;
+        }
     }
 }
diff --git a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngine.java b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngine.java
index f93d283..163ad17 100644
--- a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngine.java
+++ b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngine.java
@@ -26,9 +26,10 @@ import io.netty.util.ResourceLeakDetector;
 import io.netty.util.ResourceLeakDetectorFactory;
 import io.netty.util.ResourceLeakTracker;
 import io.netty.util.internal.EmptyArrays;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.PlatformDependent;
 import io.netty.util.internal.StringUtil;
-import io.netty.util.internal.ThrowableUtil;
+import io.netty.util.internal.SuppressJava6Requirement;
 import io.netty.util.internal.UnstableApi;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
@@ -46,9 +47,9 @@ import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
 import java.util.concurrent.locks.Lock;
 
+import javax.crypto.spec.SecretKeySpec;
 import javax.net.ssl.SSLEngine;
 import javax.net.ssl.SSLEngineResult;
 import javax.net.ssl.SSLException;
@@ -76,6 +77,7 @@ import static io.netty.util.internal.ObjectUtil.checkNotNull;
 import static java.lang.Integer.MAX_VALUE;
 import static java.lang.Math.min;
 import static javax.net.ssl.SSLEngineResult.HandshakeStatus.FINISHED;
+import static javax.net.ssl.SSLEngineResult.HandshakeStatus.NEED_TASK;
 import static javax.net.ssl.SSLEngineResult.HandshakeStatus.NEED_UNWRAP;
 import static javax.net.ssl.SSLEngineResult.HandshakeStatus.NEED_WRAP;
 import static javax.net.ssl.SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING;
@@ -97,12 +99,6 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
 
     private static final InternalLogger logger = InternalLoggerFactory.getInstance(ReferenceCountedOpenSslEngine.class);
 
-    private static final SSLException BEGIN_HANDSHAKE_ENGINE_CLOSED = ThrowableUtil.unknownStackTrace(
-            new SSLException("engine closed"), ReferenceCountedOpenSslEngine.class, "beginHandshake()");
-    private static final SSLException HANDSHAKE_ENGINE_CLOSED = ThrowableUtil.unknownStackTrace(
-            new SSLException("engine closed"), ReferenceCountedOpenSslEngine.class, "handshake()");
-    private static final SSLException RENEGOTIATION_UNSUPPORTED =  ThrowableUtil.unknownStackTrace(
-            new SSLException("renegotiation unsupported"), ReferenceCountedOpenSslEngine.class, "beginHandshake()");
     private static final ResourceLeakDetector<ReferenceCountedOpenSslEngine> leakDetector =
             ResourceLeakDetectorFactory.instance().newResourceLeakDetector(ReferenceCountedOpenSslEngine.class);
     private static final int OPENSSL_OP_NO_PROTOCOL_INDEX_SSLV2 = 0;
@@ -119,10 +115,6 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
             SSL.SSL_OP_NO_TLSv1_2,
             SSL.SSL_OP_NO_TLSv1_3
     };
-    /**
-     * <a href="https://www.openssl.org/docs/man1.0.2/crypto/X509_check_host.html">The flags argument is usually 0</a>.
-     */
-    private static final int DEFAULT_HOSTNAME_VALIDATION_FLAGS = 0;
 
     /**
      * Depends upon tcnative ... only use if tcnative is available!
@@ -133,9 +125,6 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
      */
     private static final int MAX_RECORD_SIZE = SSL.SSL_MAX_RECORD_LENGTH;
 
-    private static final AtomicIntegerFieldUpdater<ReferenceCountedOpenSslEngine> DESTROYED_UPDATER =
-            AtomicIntegerFieldUpdater.newUpdater(ReferenceCountedOpenSslEngine.class, "destroyed");
-
     private static final SSLEngineResult NEED_UNWRAP_OK = new SSLEngineResult(OK, NEED_UNWRAP, 0, 0);
     private static final SSLEngineResult NEED_UNWRAP_CLOSED = new SSLEngineResult(CLOSED, NEED_UNWRAP, 0, 0);
     private static final SSLEngineResult NEED_WRAP_OK = new SSLEngineResult(OK, NEED_WRAP, 0, 0);
@@ -167,8 +156,9 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
 
     private HandshakeState handshakeState = HandshakeState.NOT_STARTED;
     private boolean receivedShutdown;
-    private volatile int destroyed;
+    private volatile boolean destroyed;
     private volatile String applicationProtocol;
+    private volatile boolean needTask;
 
     // Reference Counting
     private final ResourceLeakTracker<ReferenceCountedOpenSslEngine> leak;
@@ -189,6 +179,7 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
                 boolean closed = leak.close(ReferenceCountedOpenSslEngine.this);
                 assert closed;
             }
+            parentContext.release();
         }
     };
 
@@ -216,16 +207,14 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
     final ByteBufAllocator alloc;
     private final OpenSslEngineMap engineMap;
     private final OpenSslApplicationProtocolNegotiator apn;
+    private final ReferenceCountedOpenSslContext parentContext;
     private final OpenSslSession session;
     private final ByteBuffer[] singleSrcBuffer = new ByteBuffer[1];
     private final ByteBuffer[] singleDstBuffer = new ByteBuffer[1];
     private final boolean enableOcsp;
     private int maxWrapOverhead;
     private int maxWrapBufferSize;
-
-    // This is package-private as we set it from OpenSslContext if an exception is thrown during
-    // the verification step.
-    SSLHandshakeException handshakeException;
+    private Throwable handshakeException;
 
     /**
      * Create a new instance.
@@ -369,22 +358,46 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
                 calculateMaxWrapOverhead();
             } catch (Throwable cause) {
                 // Call shutdown so we are sure we correctly release all native memory and also guard against the
-                // case when shutdown() will be called by the finalizer again. If we would call SSL.free(...) directly
-                // the finalizer may end up calling it again as we would miss to update the DESTROYED_UPDATER.
+                // case when shutdown() will be called by the finalizer again.
                 shutdown();
 
                 PlatformDependent.throwException(cause);
             }
         }
 
+        // Now that everything looks good and we're going to successfully return the
+        // object so we need to retain a reference to the parent context.
+        parentContext = context;
+        parentContext.retain();
+
         // Only create the leak after everything else was executed and so ensure we don't produce a false-positive for
         // the ResourceLeakDetector.
         leak = leakDetection ? leakDetector.track(this) : null;
     }
 
-    final void setKeyMaterial(OpenSslKeyMaterial keyMaterial) throws  Exception {
-        SSL.setKeyMaterial(ssl, keyMaterial.certificateChainAddress(), keyMaterial.privateKeyAddress());
+    final synchronized String[] authMethods() {
+        if (isDestroyed()) {
+            return EmptyArrays.EMPTY_STRINGS;
+        }
+        return SSL.authenticationMethods(ssl);
+    }
+
+    final boolean setKeyMaterial(OpenSslKeyMaterial keyMaterial) throws  Exception {
+        synchronized (this) {
+            if (isDestroyed()) {
+                return false;
+            }
+            SSL.setKeyMaterial(ssl, keyMaterial.certificateChainAddress(), keyMaterial.privateKeyAddress());
+        }
         localCertificateChain = keyMaterial.certificateChain();
+        return true;
+    }
+
+    final synchronized SecretKeySpec masterKey() {
+        if (isDestroyed()) {
+            return null;
+        }
+        return new SecretKeySpec(SSL.getMasterKey(ssl), "AES");
     }
 
     /**
@@ -401,7 +414,9 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
         }
 
         synchronized (this) {
-            SSL.setOcspResponse(ssl, response);
+            if (!isDestroyed()) {
+                SSL.setOcspResponse(ssl, response);
+            }
         }
     }
 
@@ -419,6 +434,9 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
         }
 
         synchronized (this) {
+            if (isDestroyed()) {
+                return EmptyArrays.EMPTY_BYTES;
+            }
             return SSL.getOcspResponse(ssl);
         }
     }
@@ -490,7 +508,8 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
      * Destroys this engine.
      */
     public final synchronized void shutdown() {
-        if (DESTROYED_UPDATER.compareAndSet(this, 0, 1)) {
+        if (!destroyed) {
+            destroyed = true;
             engineMap.remove(ssl);
             SSL.freeSSL(ssl);
             ssl = networkBIO = 0;
@@ -735,7 +754,7 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
                     // Flush any data that may have been written implicitly during the handshake by OpenSSL.
                     bytesProduced = SSL.bioFlushByteBuffer(networkBIO);
 
-                    if (bytesProduced > 0 && handshakeException != null) {
+                    if (handshakeException != null) {
                         // TODO(scott): It is possible that when the handshake failed there was not enough room in the
                         // non-application buffers to hold the alert. We should get all the data before progressing on.
                         // However I'm not aware of a way to do this with the OpenSSL APIs.
@@ -744,7 +763,16 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
                         // We produced / consumed some data during the handshake, signal back to the caller.
                         // If there is a handshake exception and we have produced data, we should send the data before
                         // we allow handshake() to throw the handshake exception.
-                        return newResult(NEED_WRAP, 0, bytesProduced);
+                        //
+                        // When the user calls wrap() again we will propagate the handshake error back to the user as
+                        // soon as there is no more data to was produced (as part of an alert etc).
+                        if (bytesProduced > 0) {
+                            return newResult(NEED_WRAP, 0, bytesProduced);
+                        }
+                        // Nothing was produced see if there is a handshakeException that needs to be propagated
+                        // to the caller by calling handshakeException() which will return the right HandshakeStatus
+                        // if it can "recover" from the exception for now.
+                        return newResult(handshakeException(), 0, 0);
                     }
 
                     status = handshake();
@@ -753,6 +781,10 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
                     // we may have freed up space by flushing above.
                     bytesProduced = bioLengthBefore - SSL.bioLengthByteBuffer(networkBIO);
 
+                    if (status == NEED_TASK) {
+                        return newResult(status, 0, bytesProduced);
+                    }
+
                     if (bytesProduced > 0) {
                         // If we have filled up the dst buffer and we have not finished the handshake we should try to
                         // wrap again. Otherwise we should only try to wrap again if there is still data pending in
@@ -833,14 +865,18 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
                         bytesWritten = writePlaintextData(src, min(remaining, availableCapacityForWrap));
                     }
 
+                    // Determine how much encrypted data was generated.
+                    //
+                    // Even if SSL_write doesn't consume any application data it is possible that OpenSSL will
+                    // produce non-application data into the BIO. For example session tickets....
+                    // See https://github.com/netty/netty/issues/10041
+                    final int pendingNow = SSL.bioLengthByteBuffer(networkBIO);
+                    bytesProduced += bioLengthBefore - pendingNow;
+                    bioLengthBefore = pendingNow;
+
                     if (bytesWritten > 0) {
                         bytesConsumed += bytesWritten;
 
-                        // Determine how much encrypted data was generated:
-                        final int pendingNow = SSL.bioLengthByteBuffer(networkBIO);
-                        bytesProduced += bioLengthBefore - pendingNow;
-                        bioLengthBefore = pendingNow;
-
                         if (jdkCompatibilityMode || bytesProduced == dst.remaining()) {
                             return newResultMayFinishHandshake(status, bytesConsumed, bytesProduced);
                         }
@@ -883,6 +919,11 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
                             // to write encrypted data to. This is an OVERFLOW condition.
                             // [1] https://www.openssl.org/docs/manmaster/ssl/SSL_write.html
                             return newResult(BUFFER_OVERFLOW, status, bytesConsumed, bytesProduced);
+                        } else if (sslError == SSL.SSL_ERROR_WANT_X509_LOOKUP ||
+                                sslError == SSL.SSL_ERROR_WANT_CERTIFICATE_VERIFY ||
+                                sslError == SSL.SSL_ERROR_WANT_PRIVATE_KEY_OPERATION) {
+
+                            return newResult(NEED_TASK, bytesConsumed, bytesProduced);
                         } else {
                             // Everything else is considered as error
                             throw shutdownWithError("SSL_write", sslError);
@@ -923,6 +964,10 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
             }
             return new SSLEngineResult(CLOSED, hs, bytesConsumed, bytesProduced);
         }
+        if (hs == NEED_TASK) {
+            // Set needTask to true so getHandshakeStatus() will return the correct value.
+            needTask = true;
+        }
         return new SSLEngineResult(status, hs, bytesConsumed, bytesProduced);
     }
 
@@ -958,7 +1003,14 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
         if (handshakeState == HandshakeState.FINISHED) {
             return new SSLException(errorString);
         }
-        return new SSLHandshakeException(errorString);
+
+        SSLHandshakeException exception = new SSLHandshakeException(errorString);
+        // If we have a handshakeException stored already we should include it as well to help the user debug things.
+        if (handshakeException != null) {
+            exception.initCause(handshakeException);
+            handshakeException = null;
+        }
+        return exception;
     }
 
     public final SSLEngineResult unwrap(
@@ -966,9 +1018,7 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
             final ByteBuffer[] dsts, int dstsOffset, final int dstsLength) throws SSLException {
 
         // Throw required runtime exceptions
-        if (srcs == null) {
-            throw new NullPointerException("srcs");
-        }
+        ObjectUtil.checkNotNull(srcs, "srcs");
         if (srcsOffset >= srcs.length
                 || srcsOffset + srcsLength > srcs.length) {
             throw new IndexOutOfBoundsException(
@@ -1020,6 +1070,11 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
                 }
 
                 status = handshake();
+
+                if (status == NEED_TASK) {
+                    return newResult(status, 0, 0);
+                }
+
                 if (status == NEED_WRAP) {
                     return NEED_WRAP_OK;
                 }
@@ -1160,7 +1215,12 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
                                         closeAll();
                                     }
                                     return newResultMayFinishHandshake(isInboundDone() ? CLOSED : OK, status,
-                                                                       bytesConsumed, bytesProduced);
+                                            bytesConsumed, bytesProduced);
+                                } else if (sslError == SSL.SSL_ERROR_WANT_X509_LOOKUP ||
+                                        sslError == SSL.SSL_ERROR_WANT_CERTIFICATE_VERIFY ||
+                                        sslError == SSL.SSL_ERROR_WANT_PRIVATE_KEY_OPERATION) {
+                                    return newResult(isInboundDone() ? CLOSED : OK,
+                                            NEED_TASK, bytesConsumed, bytesProduced);
                                 } else {
                                     return sslReadErrorResult(sslError, SSL.getLastErrorNumber(), bytesConsumed,
                                                               bytesProduced);
@@ -1293,10 +1353,29 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
     }
 
     @Override
-    public final Runnable getDelegatedTask() {
-        // Currently, we do not delegate SSL computation tasks
-        // TODO: in the future, possibly create tasks to do encrypt / decrypt async
-        return null;
+    public final synchronized Runnable getDelegatedTask() {
+        if (isDestroyed()) {
+            return null;
+        }
+        final Runnable task = SSL.getTask(ssl);
+        if (task == null) {
+            return null;
+        }
+        return new Runnable() {
+            @Override
+            public void run() {
+                if (isDestroyed()) {
+                    // The engine was destroyed in the meantime, just return.
+                    return;
+                }
+                try {
+                    task.run();
+                } finally {
+                    // The task was run, reset needTask to false so getHandshakeStatus() returns the correct value.
+                    needTask = false;
+                }
+            }
+        };
     }
 
     @Override
@@ -1591,7 +1670,7 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
     public final synchronized void beginHandshake() throws SSLException {
         switch (handshakeState) {
             case STARTED_IMPLICITLY:
-                checkEngineClosed(BEGIN_HANDSHAKE_ENGINE_CLOSED);
+                checkEngineClosed();
 
                 // A user did not start handshake by calling this method by him/herself,
                 // but handshake has been started already by wrap() or unwrap() implicitly.
@@ -1607,10 +1686,13 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
                 // Nothing to do as the handshake is not done yet.
                 break;
             case FINISHED:
-                throw RENEGOTIATION_UNSUPPORTED;
+                throw new SSLException("renegotiation unsupported");
             case NOT_STARTED:
                 handshakeState = HandshakeState.STARTED_EXPLICITLY;
-                handshake();
+                if (handshake() == NEED_TASK) {
+                    // Set needTask to true so getHandshakeStatus() will return the correct value.
+                    needTask = true;
+                }
                 calculateMaxWrapOverhead();
                 break;
             default:
@@ -1618,9 +1700,9 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
         }
     }
 
-    private void checkEngineClosed(SSLException cause) throws SSLException {
+    private void checkEngineClosed() throws SSLException {
         if (isDestroyed()) {
-            throw cause;
+            throw new SSLException("engine closed");
         }
     }
 
@@ -1637,27 +1719,51 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
         return cert == null || cert.length == 0;
     }
 
+    private SSLEngineResult.HandshakeStatus handshakeException() throws SSLException {
+        if (SSL.bioLengthNonApplication(networkBIO) > 0) {
+            // There is something pending, we need to consume it first via a WRAP so we don't loose anything.
+            return NEED_WRAP;
+        }
+
+        Throwable exception = handshakeException;
+        assert exception != null;
+        handshakeException = null;
+        shutdown();
+        if (exception instanceof SSLHandshakeException) {
+            throw (SSLHandshakeException) exception;
+        }
+        SSLHandshakeException e = new SSLHandshakeException("General OpenSslEngine problem");
+        e.initCause(exception);
+        throw e;
+    }
+
+    /**
+     * Should be called if the handshake will be failed due a callback that throws an exception.
+     * This cause will then be used to give more details as part of the {@link SSLHandshakeException}.
+     */
+    final void initHandshakeException(Throwable cause) {
+        assert handshakeException == null;
+        handshakeException = cause;
+    }
+
     private SSLEngineResult.HandshakeStatus handshake() throws SSLException {
+        if (needTask) {
+            return NEED_TASK;
+        }
         if (handshakeState == HandshakeState.FINISHED) {
             return FINISHED;
         }
-        checkEngineClosed(HANDSHAKE_ENGINE_CLOSED);
 
-        // Check if we have a pending handshakeException and if so see if we need to consume all pending data from the
-        // BIO first or can just shutdown and throw it now.
-        // This is needed so we ensure close_notify etc is correctly send to the remote peer.
-        // See https://github.com/netty/netty/issues/3900
-        SSLHandshakeException exception = handshakeException;
-        if (exception != null) {
-            if (SSL.bioLengthNonApplication(networkBIO) > 0) {
-                // There is something pending, we need to consume it first via a WRAP so we don't loose anything.
-                return NEED_WRAP;
-            }
-            // No more data left to send to the remote peer, so null out the exception field, shutdown and throw
-            // the exception.
-            handshakeException = null;
-            shutdown();
-            throw exception;
+        checkEngineClosed();
+
+        if (handshakeException != null) {
+            // Let's call SSL.doHandshake(...) again in case there is some async operation pending that would fill the
+            // outbound buffer.
+            if (SSL.doHandshake(ssl) <= 0) {
+                // Clear any error that was put on the stack by the handshake
+                SSL.clearError();
+            }
+            return handshakeException();
         }
 
         // Adding the OpenSslEngine to the OpenSslEngineMap so it can be used in the AbstractCertificateVerifier.
@@ -1668,26 +1774,32 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
 
         int code = SSL.doHandshake(ssl);
         if (code <= 0) {
+            int sslError = SSL.getError(ssl, code);
+            if (sslError == SSL.SSL_ERROR_WANT_READ || sslError == SSL.SSL_ERROR_WANT_WRITE) {
+                return pendingStatus(SSL.bioLengthNonApplication(networkBIO));
+            }
+
+            if (sslError == SSL.SSL_ERROR_WANT_X509_LOOKUP ||
+                    sslError == SSL.SSL_ERROR_WANT_CERTIFICATE_VERIFY ||
+                    sslError == SSL.SSL_ERROR_WANT_PRIVATE_KEY_OPERATION) {
+                return NEED_TASK;
+            }
+
             // Check if we have a pending exception that was created during the handshake and if so throw it after
             // shutdown the connection.
             if (handshakeException != null) {
-                exception = handshakeException;
-                handshakeException = null;
-                shutdown();
-                throw exception;
+                return handshakeException();
             }
 
-            int sslError = SSL.getError(ssl, code);
-            if (sslError == SSL.SSL_ERROR_WANT_READ || sslError == SSL.SSL_ERROR_WANT_WRITE) {
-                return pendingStatus(SSL.bioLengthNonApplication(networkBIO));
-            } else {
-                // Everything else is considered as error
-                throw shutdownWithError("SSL_do_handshake", sslError);
-            }
+            // Everything else is considered as error
+            throw shutdownWithError("SSL_do_handshake", sslError);
+        }
+        // We have produced more data as part of the handshake if this is the case the user should call wrap(...)
+        if (SSL.bioLengthNonApplication(networkBIO) > 0) {
+            return NEED_WRAP;
         }
         // if SSL_do_handshake returns > 0 or sslError == SSL.SSL_ERROR_NAME it means the handshake was finished.
         session.handshakeFinished();
-        engineMap.remove(ssl);
         return FINISHED;
     }
 
@@ -1704,12 +1816,26 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
     @Override
     public final synchronized SSLEngineResult.HandshakeStatus getHandshakeStatus() {
         // Check if we are in the initial handshake phase or shutdown phase
-        return needPendingStatus() ? pendingStatus(SSL.bioLengthNonApplication(networkBIO)) : NOT_HANDSHAKING;
+        if (needPendingStatus()) {
+            if (needTask) {
+                // There is a task outstanding
+                return NEED_TASK;
+            }
+            return pendingStatus(SSL.bioLengthNonApplication(networkBIO));
+        }
+        return NOT_HANDSHAKING;
     }
 
     private SSLEngineResult.HandshakeStatus getHandshakeStatus(int pending) {
         // Check if we are in the initial handshake phase or shutdown phase
-        return needPendingStatus() ? pendingStatus(pending) : NOT_HANDSHAKING;
+        if (needPendingStatus()) {
+            if (needTask) {
+                // There is a task outstanding
+                return NEED_TASK;
+            }
+            return pendingStatus(pending);
+        }
+        return NOT_HANDSHAKING;
     }
 
     private boolean needPendingStatus() {
@@ -1830,6 +1956,7 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
         return false;
     }
 
+    @SuppressJava6Requirement(reason = "Usage guarded by java version check")
     @Override
     public final synchronized SSLParameters getSSLParameters() {
         SSLParameters sslParameters = super.getSSLParameters();
@@ -1853,6 +1980,7 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
         return sslParameters;
     }
 
+    @SuppressJava6Requirement(reason = "Usage guarded by java version check")
     @Override
     public final synchronized void setSSLParameters(SSLParameters sslParameters) {
         int version = PlatformDependent.javaVersion();
@@ -1882,18 +2010,6 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
             final String endPointIdentificationAlgorithm = sslParameters.getEndpointIdentificationAlgorithm();
             final boolean endPointVerificationEnabled = isEndPointVerificationEnabled(endPointIdentificationAlgorithm);
 
-            final boolean wasEndPointVerificationEnabled =
-                    isEndPointVerificationEnabled(this.endPointIdentificationAlgorithm);
-
-            if (wasEndPointVerificationEnabled && !endPointVerificationEnabled) {
-                // Passing in null will disable hostname verification again so only do so if it was enabled before.
-                SSL.setHostNameValidation(ssl, DEFAULT_HOSTNAME_VALIDATION_FLAGS, null);
-            } else {
-                String host = endPointVerificationEnabled ? getPeerHost() : null;
-                if (host != null && !host.isEmpty()) {
-                    SSL.setHostNameValidation(ssl, DEFAULT_HOSTNAME_VALIDATION_FLAGS, host);
-                }
-            }
             // If the user asks for hostname verification we must ensure we verify the peer.
             // If the user disables hostname verification we leave it up to the user to change the mode manually.
             if (clientMode && endPointVerificationEnabled) {
@@ -1911,7 +2027,7 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
     }
 
     private boolean isDestroyed() {
-        return destroyed != 0;
+        return destroyed;
     }
 
     final boolean checkSniHostnameMatch(byte[] hostname) {
@@ -1938,7 +2054,6 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
         // thread.
         private X509Certificate[] x509PeerCerts;
         private Certificate[] peerCerts;
-        private Certificate[] localCerts;
 
         private String protocol;
         private String cipher;
@@ -2010,12 +2125,9 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
 
         @Override
         public void putValue(String name, Object value) {
-            if (name == null) {
-                throw new NullPointerException("name");
-            }
-            if (value == null) {
-                throw new NullPointerException("value");
-            }
+            ObjectUtil.checkNotNull(name, "name");
+            ObjectUtil.checkNotNull(value, "value");
+
             final Object old;
             synchronized (this) {
                 Map<String, Object> values = this.values;
@@ -2035,9 +2147,7 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
 
         @Override
         public Object getValue(String name) {
-            if (name == null) {
-                throw new NullPointerException("name");
-            }
+            ObjectUtil.checkNotNull(name, "name");
             synchronized (this) {
                 if (values == null) {
                     return null;
@@ -2048,9 +2158,7 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
 
         @Override
         public void removeValue(String name) {
-            if (name == null) {
-                throw new NullPointerException("name");
-            }
+            ObjectUtil.checkNotNull(name, "name");
 
             final Object old;
             synchronized (this) {
@@ -2093,7 +2201,6 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
                     id = SSL.getSessionId(ssl);
                     cipher = toJavaCipherSuite(SSL.getCipherForSSL(ssl));
                     protocol = SSL.getVersion(ssl);
-                    localCerts = localCertificateChain;
 
                     initPeerCerts();
                     selectApplicationProtocol();
@@ -2228,6 +2335,7 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
 
         @Override
         public Certificate[] getLocalCertificates() {
+            Certificate[] localCerts = ReferenceCountedOpenSslEngine.this.localCertificateChain;
             if (localCerts == null) {
                 return null;
             }
@@ -2254,7 +2362,7 @@ public class ReferenceCountedOpenSslEngine extends SSLEngine implements Referenc
 
         @Override
         public Principal getLocalPrincipal() {
-            Certificate[] local = localCerts;
+            Certificate[] local = ReferenceCountedOpenSslEngine.this.localCertificateChain;
             if (local == null || local.length == 0) {
                 return null;
             }
diff --git a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslServerContext.java b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslServerContext.java
index e901aeb..6d78e6d 100644
--- a/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslServerContext.java
+++ b/handler/src/main/java/io/netty/handler/ssl/ReferenceCountedOpenSslServerContext.java
@@ -22,6 +22,7 @@ import io.netty.internal.tcnative.SSLContext;
 import io.netty.internal.tcnative.SniHostNameMatcher;
 import io.netty.util.CharsetUtil;
 import io.netty.util.internal.PlatformDependent;
+import io.netty.util.internal.SuppressJava6Requirement;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 
@@ -30,7 +31,6 @@ import java.security.PrivateKey;
 import java.security.cert.X509Certificate;
 import javax.net.ssl.KeyManagerFactory;
 import javax.net.ssl.SSLException;
-import javax.net.ssl.SSLHandshakeException;
 import javax.net.ssl.TrustManagerFactory;
 import javax.net.ssl.X509ExtendedTrustManager;
 import javax.net.ssl.X509TrustManager;
@@ -56,25 +56,25 @@ public final class ReferenceCountedOpenSslServerContext extends ReferenceCounted
             X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory,
             Iterable<String> ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn,
             long sessionCacheSize, long sessionTimeout, ClientAuth clientAuth, String[] protocols, boolean startTls,
-            boolean enableOcsp) throws SSLException {
+            boolean enableOcsp, String keyStore) throws SSLException {
         this(trustCertCollection, trustManagerFactory, keyCertChain, key, keyPassword, keyManagerFactory, ciphers,
                 cipherFilter, toNegotiator(apn), sessionCacheSize, sessionTimeout, clientAuth, protocols, startTls,
-                enableOcsp);
+                enableOcsp, keyStore);
     }
 
-    private ReferenceCountedOpenSslServerContext(
+    ReferenceCountedOpenSslServerContext(
             X509Certificate[] trustCertCollection, TrustManagerFactory trustManagerFactory,
             X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory,
             Iterable<String> ciphers, CipherSuiteFilter cipherFilter, OpenSslApplicationProtocolNegotiator apn,
             long sessionCacheSize, long sessionTimeout, ClientAuth clientAuth, String[] protocols, boolean startTls,
-            boolean enableOcsp) throws SSLException {
+            boolean enableOcsp, String keyStore) throws SSLException {
         super(ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, SSL.SSL_MODE_SERVER, keyCertChain,
               clientAuth, protocols, startTls, enableOcsp, true);
         // Create a new SSL_CTX and configure it.
         boolean success = false;
         try {
             sessionContext = newSessionContext(this, ctx, engineMap, trustCertCollection, trustManagerFactory,
-                                                      keyCertChain, key, keyPassword, keyManagerFactory);
+                                                      keyCertChain, key, keyPassword, keyManagerFactory, keyStore);
             success = true;
         } finally {
             if (!success) {
@@ -93,7 +93,8 @@ public final class ReferenceCountedOpenSslServerContext extends ReferenceCounted
                                                          X509Certificate[] trustCertCollection,
                                                          TrustManagerFactory trustManagerFactory,
                                                          X509Certificate[] keyCertChain, PrivateKey key,
-                                                         String keyPassword, KeyManagerFactory keyManagerFactory)
+                                                         String keyPassword, KeyManagerFactory keyManagerFactory,
+                                                         String keyStore)
             throws SSLException {
         OpenSslKeyMaterialProvider keyMaterialProvider = null;
         try {
@@ -112,7 +113,7 @@ public final class ReferenceCountedOpenSslServerContext extends ReferenceCounted
                     // keyManagerFactory for the server so build one if it is not specified.
                     if (keyManagerFactory == null) {
                         char[] keyPasswordChars = keyStorePassword(keyPassword);
-                        KeyStore ks = buildKeyStore(keyCertChain, key, keyPasswordChars);
+                        KeyStore ks = buildKeyStore(keyCertChain, key, keyPasswordChars, keyStore);
                         if (ks.aliases().hasMoreElements()) {
                             keyManagerFactory = new OpenSslX509KeyManagerFactory();
                         } else {
@@ -131,7 +132,7 @@ public final class ReferenceCountedOpenSslServerContext extends ReferenceCounted
             }
             try {
                 if (trustCertCollection != null) {
-                    trustManagerFactory = buildTrustManagerFactory(trustCertCollection, trustManagerFactory);
+                    trustManagerFactory = buildTrustManagerFactory(trustCertCollection, trustManagerFactory, keyStore);
                 } else if (trustManagerFactory == null) {
                     // Mimic the way SSLContext.getInstance(KeyManager[], null, null) works
                     trustManagerFactory = TrustManagerFactory.getInstance(
@@ -147,13 +148,7 @@ public final class ReferenceCountedOpenSslServerContext extends ReferenceCounted
                 //
                 //            See https://github.com/netty/netty/issues/5372
 
-                // Use this to prevent an error when running on java < 7
-                if (useExtendedTrustManager(manager)) {
-                    SSLContext.setCertVerifyCallback(ctx, new ExtendedTrustManagerVerifyCallback(
-                            engineMap, (X509ExtendedTrustManager) manager));
-                } else {
-                    SSLContext.setCertVerifyCallback(ctx, new TrustManagerVerifyCallback(engineMap, manager));
-                }
+                setVerifyCallback(ctx, engineMap, manager);
 
                 X509Certificate[] issuers = manager.getAcceptedIssuers();
                 if (issuers != null && issuers.length > 0) {
@@ -194,6 +189,17 @@ public final class ReferenceCountedOpenSslServerContext extends ReferenceCounted
         }
     }
 
+    @SuppressJava6Requirement(reason = "Guarded by java version check")
+    private static void setVerifyCallback(long ctx, OpenSslEngineMap engineMap, X509TrustManager manager) {
+        // Use this to prevent an error when running on java < 7
+        if (useExtendedTrustManager(manager)) {
+            SSLContext.setCertVerifyCallback(ctx, new ExtendedTrustManagerVerifyCallback(
+                    engineMap, (X509ExtendedTrustManager) manager));
+        } else {
+            SSLContext.setCertVerifyCallback(ctx, new TrustManagerVerifyCallback(engineMap, manager));
+        }
+    }
+
     private static final class OpenSslServerCertificateCallback implements CertificateCallback {
         private final OpenSslEngineMap engineMap;
         private final OpenSslKeyMaterialManager keyManagerHolder;
@@ -206,15 +212,17 @@ public final class ReferenceCountedOpenSslServerContext extends ReferenceCounted
         @Override
         public void handle(long ssl, byte[] keyTypeBytes, byte[][] asn1DerEncodedPrincipals) throws Exception {
             final ReferenceCountedOpenSslEngine engine = engineMap.get(ssl);
+            if (engine == null) {
+                // Maybe null if destroyed in the meantime.
+                return;
+            }
             try {
                 // For now we just ignore the asn1DerEncodedPrincipals as this is kind of inline with what the
                 // OpenJDK SSLEngineImpl does.
                 keyManagerHolder.setKeyMaterialServerSide(engine);
             } catch (Throwable cause) {
                 logger.debug("Failed to set the server-side key material", cause);
-                SSLHandshakeException e = new SSLHandshakeException("General OpenSslEngine problem");
-                e.initCause(cause);
-                engine.handshakeException = e;
+                engine.initHandshakeException(cause);
             }
         }
     }
@@ -234,12 +242,13 @@ public final class ReferenceCountedOpenSslServerContext extends ReferenceCounted
         }
     }
 
+    @SuppressJava6Requirement(reason = "Usage guarded by java version check")
     private static final class ExtendedTrustManagerVerifyCallback extends AbstractCertificateVerifier {
         private final X509ExtendedTrustManager manager;
 
         ExtendedTrustManagerVerifyCallback(OpenSslEngineMap engineMap, X509ExtendedTrustManager manager) {
             super(engineMap);
-            this.manager = OpenSslTlsv13X509ExtendedTrustManager.wrap(manager, false);
+            this.manager = OpenSslTlsv13X509ExtendedTrustManager.wrap(manager);
         }
 
         @Override
diff --git a/handler/src/main/java/io/netty/handler/ssl/SniHandler.java b/handler/src/main/java/io/netty/handler/ssl/SniHandler.java
index cda2bbd..c6a8227 100644
--- a/handler/src/main/java/io/netty/handler/ssl/SniHandler.java
+++ b/handler/src/main/java/io/netty/handler/ssl/SniHandler.java
@@ -15,6 +15,7 @@
  */
 package io.netty.handler.ssl;
 
+import io.netty.buffer.ByteBufAllocator;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.handler.codec.DecoderException;
 import io.netty.util.AsyncMapping;
@@ -129,7 +130,7 @@ public class SniHandler extends AbstractSniHandler<SslContext> {
     protected void replaceHandler(ChannelHandlerContext ctx, String hostname, SslContext sslContext) throws Exception {
         SslHandler sslHandler = null;
         try {
-            sslHandler = sslContext.newHandler(ctx.alloc());
+            sslHandler = newSslHandler(sslContext, ctx.alloc());
             ctx.pipeline().replace(this, SslHandler.class.getName(), sslHandler);
             sslHandler = null;
         } finally {
@@ -142,6 +143,14 @@ public class SniHandler extends AbstractSniHandler<SslContext> {
         }
     }
 
+    /**
+     * Returns a new {@link SslHandler} using the given {@link SslContext} and {@link ByteBufAllocator}.
+     * Users may override this method to implement custom behavior.
+     */
+    protected SslHandler newSslHandler(SslContext context, ByteBufAllocator allocator) {
+        return context.newHandler(allocator);
+    }
+
     private static final class AsyncMappingAdapter implements AsyncMapping<String, SslContext> {
         private final Mapping<? super String, ? extends SslContext> mapping;
 
diff --git a/handler/src/main/java/io/netty/handler/ssl/SslClientHelloHandler.java b/handler/src/main/java/io/netty/handler/ssl/SslClientHelloHandler.java
new file mode 100644
index 0000000..4bcf349
--- /dev/null
+++ b/handler/src/main/java/io/netty/handler/ssl/SslClientHelloHandler.java
@@ -0,0 +1,312 @@
+/*
+ * Copyright 2017 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.ssl;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelOutboundHandler;
+import io.netty.channel.ChannelPromise;
+import io.netty.handler.codec.ByteToMessageDecoder;
+import io.netty.handler.codec.DecoderException;
+import io.netty.util.concurrent.Future;
+import io.netty.util.concurrent.FutureListener;
+import io.netty.util.internal.PlatformDependent;
+import io.netty.util.internal.logging.InternalLogger;
+import io.netty.util.internal.logging.InternalLoggerFactory;
+
+import java.net.SocketAddress;
+import java.util.List;
+
+/**
+ * {@link ByteToMessageDecoder} which allows to be notified once a full {@code ClientHello} was received.
+ */
+public abstract class SslClientHelloHandler<T> extends ByteToMessageDecoder implements ChannelOutboundHandler {
+
+    private static final InternalLogger logger =
+            InternalLoggerFactory.getInstance(SslClientHelloHandler.class);
+
+    private boolean handshakeFailed;
+    private boolean suppressRead;
+    private boolean readPending;
+    private ByteBuf handshakeBuffer;
+
+    @Override
+    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
+        if (!suppressRead && !handshakeFailed) {
+            try {
+                int readerIndex = in.readerIndex();
+                int readableBytes = in.readableBytes();
+                int handshakeLength = -1;
+
+                // Check if we have enough data to determine the record type and length.
+                while (readableBytes >= SslUtils.SSL_RECORD_HEADER_LENGTH) {
+                    final int contentType = in.getUnsignedByte(readerIndex);
+                    switch (contentType) {
+                        case SslUtils.SSL_CONTENT_TYPE_CHANGE_CIPHER_SPEC:
+                            // fall-through
+                        case SslUtils.SSL_CONTENT_TYPE_ALERT:
+                            final int len = SslUtils.getEncryptedPacketLength(in, readerIndex);
+
+                            // Not an SSL/TLS packet
+                            if (len == SslUtils.NOT_ENCRYPTED) {
+                                handshakeFailed = true;
+                                NotSslRecordException e = new NotSslRecordException(
+                                        "not an SSL/TLS record: " + ByteBufUtil.hexDump(in));
+                                in.skipBytes(in.readableBytes());
+                                ctx.fireUserEventTriggered(new SniCompletionEvent(e));
+                                SslUtils.handleHandshakeFailure(ctx, e, true);
+                                throw e;
+                            }
+                            if (len == SslUtils.NOT_ENOUGH_DATA) {
+                                // Not enough data
+                                return;
+                            }
+                            // No ClientHello
+                            select(ctx, null);
+                            return;
+                        case SslUtils.SSL_CONTENT_TYPE_HANDSHAKE:
+                            final int majorVersion = in.getUnsignedByte(readerIndex + 1);
+                            // SSLv3 or TLS
+                            if (majorVersion == 3) {
+                                int packetLength = in.getUnsignedShort(readerIndex + 3) +
+                                        SslUtils.SSL_RECORD_HEADER_LENGTH;
+
+                                if (readableBytes < packetLength) {
+                                    // client hello incomplete; try again to decode once more data is ready.
+                                    return;
+                                } else if (packetLength == SslUtils.SSL_RECORD_HEADER_LENGTH) {
+                                    select(ctx, null);
+                                    return;
+                                }
+
+                                final int endOffset = readerIndex + packetLength;
+
+                                // Let's check if we already parsed the handshake length or not.
+                                if (handshakeLength == -1) {
+                                    if (readerIndex + 4 > endOffset) {
+                                        // Need more data to read HandshakeType and handshakeLength (4 bytes)
+                                        return;
+                                    }
+
+                                    final int handshakeType = in.getUnsignedByte(readerIndex +
+                                            SslUtils.SSL_RECORD_HEADER_LENGTH);
+
+                                    // Check if this is a clientHello(1)
+                                    // See https://tools.ietf.org/html/rfc5246#section-7.4
+                                    if (handshakeType != 1) {
+                                        select(ctx, null);
+                                        return;
+                                    }
+
+                                    // Read the length of the handshake as it may arrive in fragments
+                                    // See https://tools.ietf.org/html/rfc5246#section-7.4
+                                    handshakeLength = in.getUnsignedMedium(readerIndex +
+                                            SslUtils.SSL_RECORD_HEADER_LENGTH + 1);
+
+                                    // Consume handshakeType and handshakeLength (this sums up as 4 bytes)
+                                    readerIndex += 4;
+                                    packetLength -= 4;
+
+                                    if (handshakeLength + 4 + SslUtils.SSL_RECORD_HEADER_LENGTH <= packetLength) {
+                                        // We have everything we need in one packet.
+                                        // Skip the record header
+                                        readerIndex += SslUtils.SSL_RECORD_HEADER_LENGTH;
+                                        select(ctx, in.retainedSlice(readerIndex, handshakeLength));
+                                        return;
+                                    } else {
+                                        if (handshakeBuffer == null) {
+                                            handshakeBuffer = ctx.alloc().buffer(handshakeLength);
+                                        } else {
+                                            // Clear the buffer so we can aggregate into it again.
+                                            handshakeBuffer.clear();
+                                        }
+                                    }
+                                }
+
+                                // Combine the encapsulated data in one buffer but not include the SSL_RECORD_HEADER
+                                handshakeBuffer.writeBytes(in, readerIndex + SslUtils.SSL_RECORD_HEADER_LENGTH,
+                                        packetLength - SslUtils.SSL_RECORD_HEADER_LENGTH);
+                                readerIndex += packetLength;
+                                readableBytes -= packetLength;
+                                if (handshakeLength <= handshakeBuffer.readableBytes()) {
+                                    ByteBuf clientHello = handshakeBuffer.setIndex(0, handshakeLength);
+                                    handshakeBuffer = null;
+
+                                    select(ctx, clientHello);
+                                    return;
+                                }
+                                break;
+                            }
+                            // fall-through
+                        default:
+                            // not tls, ssl or application data
+                            select(ctx, null);
+                            return;
+                    }
+                }
+            } catch (NotSslRecordException e) {
+                // Just rethrow as in this case we also closed the channel and this is consistent with SslHandler.
+                throw e;
+            } catch (Exception e) {
+                // unexpected encoding, ignore sni and use default
+                if (logger.isDebugEnabled()) {
+                    logger.debug("Unexpected client hello packet: " + ByteBufUtil.hexDump(in), e);
+                }
+                select(ctx, null);
+            }
+        }
+    }
+
+    private void releaseHandshakeBuffer() {
+        releaseIfNotNull(handshakeBuffer);
+        handshakeBuffer = null;
+    }
+
+    private static void releaseIfNotNull(ByteBuf buffer) {
+        if (buffer != null) {
+            buffer.release();
+        }
+    }
+
+    private void select(final ChannelHandlerContext ctx, ByteBuf clientHello) throws Exception {
+        final Future<T> future;
+        try {
+            future = lookup(ctx, clientHello);
+            if (future.isDone()) {
+                onLookupComplete(ctx, future);
+            } else {
+                suppressRead = true;
+                final ByteBuf finalClientHello = clientHello;
+                future.addListener(new FutureListener<T>() {
+                    @Override
+                    public void operationComplete(Future<T> future) {
+                        releaseIfNotNull(finalClientHello);
+                        try {
+                            suppressRead = false;
+                            try {
+                                onLookupComplete(ctx, future);
+                            } catch (DecoderException err) {
+                                ctx.fireExceptionCaught(err);
+                            } catch (Exception cause) {
+                                ctx.fireExceptionCaught(new DecoderException(cause));
+                            } catch (Throwable cause) {
+                                ctx.fireExceptionCaught(cause);
+                            }
+                        } finally {
+                            if (readPending) {
+                                readPending = false;
+                                ctx.read();
+                            }
+                        }
+                    }
+                });
+
+                // Ownership was transferred to the FutureListener.
+                clientHello = null;
+            }
+        } catch (Throwable cause) {
+            PlatformDependent.throwException(cause);
+        } finally {
+            releaseIfNotNull(clientHello);
+        }
+    }
+
+    @Override
+    protected void handlerRemoved0(ChannelHandlerContext ctx) throws Exception {
+        releaseHandshakeBuffer();
+
+        super.handlerRemoved0(ctx);
+    }
+
+    /**
+     * Kicks off a lookup for the given {@code ClientHello} and returns a {@link Future} which in turn will
+     * notify the {@link #onLookupComplete(ChannelHandlerContext, Future)} on completion.
+     *
+     * See https://tools.ietf.org/html/rfc5246#section-7.4.1.2
+     *
+     * <pre>
+     * struct {
+     *    ProtocolVersion client_version;
+     *    Random random;
+     *    SessionID session_id;
+     *    CipherSuite cipher_suites<2..2^16-2>;
+     *    CompressionMethod compression_methods<1..2^8-1>;
+     *    select (extensions_present) {
+     *        case false:
+     *            struct {};
+     *        case true:
+     *            Extension extensions<0..2^16-1>;
+     *    };
+     * } ClientHello;
+     * </pre>
+     *
+     * @see #onLookupComplete(ChannelHandlerContext, Future)
+     */
+    protected abstract Future<T> lookup(ChannelHandlerContext ctx, ByteBuf clientHello) throws Exception;
+
+    /**
+     * Called upon completion of the {@link #lookup(ChannelHandlerContext, ByteBuf)} {@link Future}.
+     *
+     * @see #lookup(ChannelHandlerContext, ByteBuf)
+     */
+    protected abstract void onLookupComplete(ChannelHandlerContext ctx, Future<T> future) throws Exception;
+
+    @Override
+    public void read(ChannelHandlerContext ctx) throws Exception {
+        if (suppressRead) {
+            readPending = true;
+        } else {
+            ctx.read();
+        }
+    }
+
+    @Override
+    public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) throws Exception {
+        ctx.bind(localAddress, promise);
+    }
+
+    @Override
+    public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress,
+                        ChannelPromise promise) throws Exception {
+        ctx.connect(remoteAddress, localAddress, promise);
+    }
+
+    @Override
+    public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
+        ctx.disconnect(promise);
+    }
+
+    @Override
+    public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
+        ctx.close(promise);
+    }
+
+    @Override
+    public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
+        ctx.deregister(promise);
+    }
+
+    @Override
+    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
+        ctx.write(msg, promise);
+    }
+
+    @Override
+    public void flush(ChannelHandlerContext ctx) throws Exception {
+        ctx.flush();
+    }
+}
diff --git a/handler/src/main/java/io/netty/handler/ssl/SslContext.java b/handler/src/main/java/io/netty/handler/ssl/SslContext.java
index 6c5a6c6..fef2702 100644
--- a/handler/src/main/java/io/netty/handler/ssl/SslContext.java
+++ b/handler/src/main/java/io/netty/handler/ssl/SslContext.java
@@ -24,6 +24,8 @@ import io.netty.channel.ChannelPipeline;
 import io.netty.handler.ssl.ApplicationProtocolConfig.Protocol;
 import io.netty.handler.ssl.ApplicationProtocolConfig.SelectedListenerFailureBehavior;
 import io.netty.handler.ssl.ApplicationProtocolConfig.SelectorFailureBehavior;
+import io.netty.util.AttributeMap;
+import io.netty.util.DefaultAttributeMap;
 import io.netty.util.internal.EmptyArrays;
 
 import java.security.Provider;
@@ -60,6 +62,7 @@ import java.security.cert.X509Certificate;
 import java.security.spec.InvalidKeySpecException;
 import java.security.spec.PKCS8EncodedKeySpec;
 import java.util.List;
+import java.util.concurrent.Executor;
 
 /**
  * A secure socket protocol implementation which acts as a factory for {@link SSLEngine} and {@link SslHandler}.
@@ -96,6 +99,7 @@ public abstract class SslContext {
     }
 
     private final boolean startTls;
+    private final AttributeMap attributes = new DefaultAttributeMap();
 
     /**
      * Returns the default server-side implementation provider currently in use.
@@ -338,7 +342,7 @@ public abstract class SslContext {
             Iterable<String> ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn,
             long sessionCacheSize, long sessionTimeout) throws SSLException {
         return newServerContext(provider, null, null, certChainFile, keyFile, keyPassword, null,
-                ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout);
+                ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, KeyStore.getDefaultType());
     }
 
     /**
@@ -381,12 +385,57 @@ public abstract class SslContext {
             File keyCertChainFile, File keyFile, String keyPassword, KeyManagerFactory keyManagerFactory,
             Iterable<String> ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn,
             long sessionCacheSize, long sessionTimeout) throws SSLException {
+        return newServerContext(provider, trustCertCollectionFile, trustManagerFactory, keyCertChainFile,
+                keyFile, keyPassword, keyManagerFactory, ciphers, cipherFilter, apn,
+                sessionCacheSize, sessionTimeout, KeyStore.getDefaultType());
+    }
+
+    /**
+     * Creates a new server-side {@link SslContext}.
+     * @param provider the {@link SslContext} implementation to use.
+     *                 {@code null} to use the current default one.
+     * @param trustCertCollectionFile an X.509 certificate collection file in PEM format.
+     *                      This provides the certificate collection used for mutual authentication.
+     *                      {@code null} to use the system default
+     * @param trustManagerFactory the {@link TrustManagerFactory} that provides the {@link TrustManager}s
+     *                            that verifies the certificates sent from clients.
+     *                            {@code null} to use the default or the results of parsing
+     *                            {@code trustCertCollectionFile}.
+     *                            This parameter is ignored if {@code provider} is not {@link SslProvider#JDK}.
+     * @param keyCertChainFile an X.509 certificate chain file in PEM format
+     * @param keyFile a PKCS#8 private key file in PEM format
+     * @param keyPassword the password of the {@code keyFile}.
+     *                    {@code null} if it's not password-protected.
+     * @param keyManagerFactory the {@link KeyManagerFactory} that provides the {@link KeyManager}s
+     *                          that is used to encrypt data being sent to clients.
+     *                          {@code null} to use the default or the results of parsing
+     *                          {@code keyCertChainFile} and {@code keyFile}.
+     *                          This parameter is ignored if {@code provider} is not {@link SslProvider#JDK}.
+     * @param ciphers the cipher suites to enable, in the order of preference.
+     *                {@code null} to use the default cipher suites.
+     * @param cipherFilter a filter to apply over the supplied list of ciphers
+     *                Only required if {@code provider} is {@link SslProvider#JDK}
+     * @param apn Provides a means to configure parameters related to application protocol negotiation.
+     * @param sessionCacheSize the size of the cache used for storing SSL session objects.
+     *                         {@code 0} to use the default value.
+     * @param sessionTimeout the timeout for the cached SSL session objects, in seconds.
+     *                       {@code 0} to use the default value.
+     * @param keyStore the keystore type that should  be used
+     * @return a new server-side {@link SslContext}
+     */
+    static SslContext newServerContext(
+            SslProvider provider,
+            File trustCertCollectionFile, TrustManagerFactory trustManagerFactory,
+            File keyCertChainFile, File keyFile, String keyPassword, KeyManagerFactory keyManagerFactory,
+            Iterable<String> ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn,
+            long sessionCacheSize, long sessionTimeout, String keyStore) throws SSLException {
         try {
             return newServerContextInternal(provider, null, toX509Certificates(trustCertCollectionFile),
                                             trustManagerFactory, toX509Certificates(keyCertChainFile),
                                             toPrivateKey(keyFile, keyPassword),
                                             keyPassword, keyManagerFactory, ciphers, cipherFilter, apn,
-                                            sessionCacheSize, sessionTimeout, ClientAuth.NONE, null, false, false);
+                                            sessionCacheSize, sessionTimeout, ClientAuth.NONE, null,
+                                            false, false, keyStore);
         } catch (Exception e) {
             if (e instanceof SSLException) {
                 throw (SSLException) e;
@@ -402,7 +451,7 @@ public abstract class SslContext {
             X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory,
             Iterable<String> ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn,
             long sessionCacheSize, long sessionTimeout, ClientAuth clientAuth, String[] protocols, boolean startTls,
-            boolean enableOcsp) throws SSLException {
+            boolean enableOcsp, String keyStoreType) throws SSLException {
 
         if (provider == null) {
             provider = defaultServerProvider();
@@ -416,19 +465,19 @@ public abstract class SslContext {
             return new JdkSslServerContext(sslContextProvider,
                     trustCertCollection, trustManagerFactory, keyCertChain, key, keyPassword,
                     keyManagerFactory, ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout,
-                    clientAuth, protocols, startTls);
+                    clientAuth, protocols, startTls, keyStoreType);
         case OPENSSL:
             verifyNullSslContextProvider(provider, sslContextProvider);
             return new OpenSslServerContext(
                     trustCertCollection, trustManagerFactory, keyCertChain, key, keyPassword,
                     keyManagerFactory, ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout,
-                    clientAuth, protocols, startTls, enableOcsp);
+                    clientAuth, protocols, startTls, enableOcsp, keyStoreType);
         case OPENSSL_REFCNT:
             verifyNullSslContextProvider(provider, sslContextProvider);
             return new ReferenceCountedOpenSslServerContext(
                     trustCertCollection, trustManagerFactory, keyCertChain, key, keyPassword,
                     keyManagerFactory, ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout,
-                    clientAuth, protocols, startTls, enableOcsp);
+                    clientAuth, protocols, startTls, enableOcsp, keyStoreType);
         default:
             throw new Error(provider.toString());
         }
@@ -744,7 +793,8 @@ public abstract class SslContext {
                                             toX509Certificates(trustCertCollectionFile), trustManagerFactory,
                                             toX509Certificates(keyCertChainFile), toPrivateKey(keyFile, keyPassword),
                                             keyPassword, keyManagerFactory, ciphers, cipherFilter,
-                                            apn, null, sessionCacheSize, sessionTimeout, false);
+                                            apn, null, sessionCacheSize, sessionTimeout, false,
+                                            KeyStore.getDefaultType());
         } catch (Exception e) {
             if (e instanceof SSLException) {
                 throw (SSLException) e;
@@ -759,7 +809,7 @@ public abstract class SslContext {
             X509Certificate[] trustCert, TrustManagerFactory trustManagerFactory,
             X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory,
             Iterable<String> ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, String[] protocols,
-            long sessionCacheSize, long sessionTimeout, boolean enableOcsp) throws SSLException {
+            long sessionCacheSize, long sessionTimeout, boolean enableOcsp, String keyStoreType) throws SSLException {
         if (provider == null) {
             provider = defaultClientProvider();
         }
@@ -770,19 +820,20 @@ public abstract class SslContext {
                 }
                 return new JdkSslClientContext(sslContextProvider,
                         trustCert, trustManagerFactory, keyCertChain, key, keyPassword,
-                        keyManagerFactory, ciphers, cipherFilter, apn, protocols, sessionCacheSize, sessionTimeout);
+                        keyManagerFactory, ciphers, cipherFilter, apn, protocols, sessionCacheSize,
+                        sessionTimeout, keyStoreType);
             case OPENSSL:
                 verifyNullSslContextProvider(provider, sslContextProvider);
                 return new OpenSslClientContext(
                         trustCert, trustManagerFactory, keyCertChain, key, keyPassword,
                         keyManagerFactory, ciphers, cipherFilter, apn, protocols, sessionCacheSize, sessionTimeout,
-                        enableOcsp);
+                        enableOcsp, keyStoreType);
             case OPENSSL_REFCNT:
                 verifyNullSslContextProvider(provider, sslContextProvider);
                 return new ReferenceCountedOpenSslClientContext(
                         trustCert, trustManagerFactory, keyCertChain, key, keyPassword,
                         keyManagerFactory, ciphers, cipherFilter, apn, protocols, sessionCacheSize, sessionTimeout,
-                        enableOcsp);
+                        enableOcsp, keyStoreType);
             default:
                 throw new Error(provider.toString());
         }
@@ -814,6 +865,13 @@ public abstract class SslContext {
         this.startTls = startTls;
     }
 
+    /**
+     * Returns the {@link AttributeMap} that belongs to this {@link SslContext} .
+     */
+    public final AttributeMap attributes() {
+        return attributes;
+    }
+
     /**
      * Returns {@code true} if and only if this context is for server-side.
      */
@@ -879,6 +937,22 @@ public abstract class SslContext {
      */
     public abstract SSLSessionContext sessionContext();
 
+    /**
+     * Create a new SslHandler.
+     * @see #newHandler(ByteBufAllocator, Executor)
+     */
+    public final SslHandler newHandler(ByteBufAllocator alloc) {
+        return newHandler(alloc, startTls);
+    }
+
+    /**
+     * Create a new SslHandler.
+     * @see #newHandler(ByteBufAllocator)
+     */
+    protected SslHandler newHandler(ByteBufAllocator alloc, boolean startTls) {
+        return new SslHandler(newEngine(alloc), startTls);
+    }
+
     /**
      * Creates a new {@link SslHandler}.
      * <p>If {@link SslProvider#OPENSSL_REFCNT} is used then the returned {@link SslHandler} will release the engine
@@ -900,18 +974,37 @@ public abstract class SslContext {
      * <a href="https://docs.oracle.com/javase/7/docs/api/javax/net/ssl/SSLEngine.html">SSLEngine javadocs</a> which
      * limits wrap/unwrap to operate on a single SSL/TLS packet.
      * @param alloc If supported by the SSLEngine then the SSLEngine will use this to allocate ByteBuf objects.
+     * @param delegatedTaskExecutor the {@link Executor} that will be used to execute tasks that are returned by
+     *                              {@link SSLEngine#getDelegatedTask()}.
      * @return a new {@link SslHandler}
      */
-    public final SslHandler newHandler(ByteBufAllocator alloc) {
-        return newHandler(alloc, startTls);
+    public SslHandler newHandler(ByteBufAllocator alloc, Executor delegatedTaskExecutor) {
+        return newHandler(alloc, startTls, delegatedTaskExecutor);
     }
 
     /**
      * Create a new SslHandler.
-     * @see #newHandler(ByteBufAllocator)
+     * @see #newHandler(ByteBufAllocator, String, int, boolean, Executor)
      */
-    protected SslHandler newHandler(ByteBufAllocator alloc, boolean startTls) {
-        return new SslHandler(newEngine(alloc), startTls);
+    protected SslHandler newHandler(ByteBufAllocator alloc, boolean startTls, Executor executor) {
+        return new SslHandler(newEngine(alloc), startTls, executor);
+    }
+
+    /**
+     * Creates a new {@link SslHandler}
+     *
+     * @see #newHandler(ByteBufAllocator, String, int, Executor)
+     */
+    public final SslHandler newHandler(ByteBufAllocator alloc, String peerHost, int peerPort) {
+        return newHandler(alloc, peerHost, peerPort, startTls);
+    }
+
+    /**
+     * Create a new SslHandler.
+     * @see #newHandler(ByteBufAllocator, String, int, boolean, Executor)
+     */
+    protected SslHandler newHandler(ByteBufAllocator alloc, String peerHost, int peerPort, boolean startTls) {
+        return new SslHandler(newEngine(alloc, peerHost, peerPort), startTls);
     }
 
     /**
@@ -937,19 +1030,19 @@ public abstract class SslContext {
      * @param alloc If supported by the SSLEngine then the SSLEngine will use this to allocate ByteBuf objects.
      * @param peerHost the non-authoritative name of the host
      * @param peerPort the non-authoritative port
+     * @param delegatedTaskExecutor the {@link Executor} that will be used to execute tasks that are returned by
+     *                              {@link SSLEngine#getDelegatedTask()}.
      *
      * @return a new {@link SslHandler}
      */
-    public final SslHandler newHandler(ByteBufAllocator alloc, String peerHost, int peerPort) {
-        return newHandler(alloc, peerHost, peerPort, startTls);
+    public SslHandler newHandler(ByteBufAllocator alloc, String peerHost, int peerPort,
+                                 Executor delegatedTaskExecutor) {
+        return newHandler(alloc, peerHost, peerPort, startTls, delegatedTaskExecutor);
     }
 
-    /**
-     * Create a new SslHandler.
-     * @see #newHandler(ByteBufAllocator, String, int, boolean)
-     */
-    protected SslHandler newHandler(ByteBufAllocator alloc, String peerHost, int peerPort, boolean startTls) {
-        return new SslHandler(newEngine(alloc, peerHost, peerPort), startTls);
+    protected SslHandler newHandler(ByteBufAllocator alloc, String peerHost, int peerPort, boolean startTls,
+                                    Executor delegatedTaskExecutor) {
+        return new SslHandler(newEngine(alloc, peerHost, peerPort), startTls, delegatedTaskExecutor);
     }
 
     /**
@@ -990,16 +1083,21 @@ public abstract class SslContext {
     /**
      * Generates a new {@link KeyStore}.
      *
-     * @param certChain a X.509 certificate chain
+     * @param certChain an X.509 certificate chain
      * @param key a PKCS#8 private key
      * @param keyPasswordChars the password of the {@code keyFile}.
      *                    {@code null} if it's not password-protected.
+     * @param keyStoreType The KeyStore Type you want to use
      * @return generated {@link KeyStore}.
      */
-    static KeyStore buildKeyStore(X509Certificate[] certChain, PrivateKey key, char[] keyPasswordChars)
+    static KeyStore buildKeyStore(X509Certificate[] certChain, PrivateKey key,
+                                  char[] keyPasswordChars, String keyStoreType)
             throws KeyStoreException, NoSuchAlgorithmException,
                    CertificateException, IOException {
-        KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
+        if (keyStoreType == null) {
+            keyStoreType = KeyStore.getDefaultType();
+        }
+        KeyStore ks = KeyStore.getInstance(keyStoreType);
         ks.load(null, null);
         ks.setKeyEntry(ALIAS, key, keyPasswordChars, certChain);
         return ks;
@@ -1059,9 +1157,22 @@ public abstract class SslContext {
     protected static TrustManagerFactory buildTrustManagerFactory(
             File certChainFile, TrustManagerFactory trustManagerFactory)
             throws NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException {
+        return buildTrustManagerFactory(certChainFile, trustManagerFactory, KeyStore.getDefaultType());
+    }
+
+    /**
+     * Build a {@link TrustManagerFactory} from a certificate chain file.
+     * @param certChainFile The certificate file to build from.
+     * @param trustManagerFactory The existing {@link TrustManagerFactory} that will be used if not {@code null}.
+     * @param keyType The KeyStore Type you want to use
+     * @return A {@link TrustManagerFactory} which contains the certificates in {@code certChainFile}
+     */
+    static TrustManagerFactory buildTrustManagerFactory(
+            File certChainFile, TrustManagerFactory trustManagerFactory, String keyType)
+            throws NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException {
         X509Certificate[] x509Certs = toX509Certificates(certChainFile);
 
-        return buildTrustManagerFactory(x509Certs, trustManagerFactory);
+        return buildTrustManagerFactory(x509Certs, trustManagerFactory, keyType);
     }
 
     static X509Certificate[] toX509Certificates(File file) throws CertificateException {
@@ -1105,9 +1216,12 @@ public abstract class SslContext {
     }
 
     static TrustManagerFactory buildTrustManagerFactory(
-            X509Certificate[] certCollection, TrustManagerFactory trustManagerFactory)
+            X509Certificate[] certCollection, TrustManagerFactory trustManagerFactory, String keyStoreType)
             throws NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException {
-        final KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
+        if (keyStoreType == null) {
+            keyStoreType = KeyStore.getDefaultType();
+        }
+        final KeyStore ks = KeyStore.getInstance(keyStoreType);
         ks.load(null, null);
 
         int i = 1;
@@ -1143,10 +1257,22 @@ public abstract class SslContext {
     }
 
     static KeyManagerFactory buildKeyManagerFactory(X509Certificate[] certChain, PrivateKey key, String keyPassword,
-                                                    KeyManagerFactory kmf)
+                                                    KeyManagerFactory kmf, String keyStoreType)
             throws UnrecoverableKeyException, KeyStoreException, NoSuchAlgorithmException,
             CertificateException, IOException {
-        return buildKeyManagerFactory(certChain, KeyManagerFactory.getDefaultAlgorithm(), key, keyPassword, kmf);
+        return buildKeyManagerFactory(certChain, KeyManagerFactory.getDefaultAlgorithm(), key,
+                keyPassword, kmf, keyStoreType);
+    }
+
+    static KeyManagerFactory buildKeyManagerFactory(X509Certificate[] certChainFile,
+                                                    String keyAlgorithm, PrivateKey key,
+                                                    String keyPassword, KeyManagerFactory kmf,
+                                                    String keyStore)
+            throws KeyStoreException, NoSuchAlgorithmException, IOException,
+            CertificateException, UnrecoverableKeyException {
+        char[] keyPasswordChars = keyStorePassword(keyPassword);
+        KeyStore ks = buildKeyStore(certChainFile, key, keyPasswordChars, keyStore);
+        return buildKeyManagerFactory(ks, keyAlgorithm, keyPasswordChars, kmf);
     }
 
     static KeyManagerFactory buildKeyManagerFactory(X509Certificate[] certChainFile,
@@ -1155,7 +1281,7 @@ public abstract class SslContext {
             throws KeyStoreException, NoSuchAlgorithmException, IOException,
             CertificateException, UnrecoverableKeyException {
         char[] keyPasswordChars = keyStorePassword(keyPassword);
-        KeyStore ks = buildKeyStore(certChainFile, key, keyPasswordChars);
+        KeyStore ks = buildKeyStore(certChainFile, key, keyPasswordChars, KeyStore.getDefaultType());
         return buildKeyManagerFactory(ks, keyAlgorithm, keyPasswordChars, kmf);
     }
 
diff --git a/handler/src/main/java/io/netty/handler/ssl/SslContextBuilder.java b/handler/src/main/java/io/netty/handler/ssl/SslContextBuilder.java
index ae21440..f7794a9 100644
--- a/handler/src/main/java/io/netty/handler/ssl/SslContextBuilder.java
+++ b/handler/src/main/java/io/netty/handler/ssl/SslContextBuilder.java
@@ -16,20 +16,26 @@
 
 package io.netty.handler.ssl;
 
-import static io.netty.util.internal.ObjectUtil.checkNotNull;
-
 import io.netty.util.internal.UnstableApi;
 
-import java.security.Provider;
+import javax.net.ssl.KeyManager;
 import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLEngine;
 import javax.net.ssl.SSLException;
+import javax.net.ssl.TrustManager;
 import javax.net.ssl.TrustManagerFactory;
-
 import java.io.File;
 import java.io.InputStream;
+import java.security.KeyStore;
 import java.security.PrivateKey;
+import java.security.Provider;
 import java.security.cert.X509Certificate;
-import javax.net.ssl.SSLEngine;
+import java.util.ArrayList;
+import java.util.List;
+
+import static io.netty.util.internal.EmptyArrays.EMPTY_STRINGS;
+import static io.netty.util.internal.EmptyArrays.EMPTY_X509_CERTIFICATES;
+import static io.netty.util.internal.ObjectUtil.checkNotNull;
 
 /**
  * Builder for configuring a new SslContext for creation.
@@ -76,6 +82,17 @@ public final class SslContextBuilder {
         return new SslContextBuilder(true).keyManager(key, keyCertChain);
     }
 
+    /**
+     * Creates a builder for new server-side {@link SslContext}.
+     *
+     * @param key a PKCS#8 private key
+     * @param keyCertChain the X.509 certificate chain
+     * @see #keyManager(PrivateKey, X509Certificate[])
+     */
+    public static SslContextBuilder forServer(PrivateKey key, Iterable<? extends X509Certificate> keyCertChain) {
+        return forServer(key, toArray(keyCertChain, EMPTY_X509_CERTIFICATES));
+    }
+
     /**
      * Creates a builder for new server-side {@link SslContext}.
      *
@@ -118,6 +135,20 @@ public final class SslContextBuilder {
         return new SslContextBuilder(true).keyManager(key, keyPassword, keyCertChain);
     }
 
+    /**
+     * Creates a builder for new server-side {@link SslContext}.
+     *
+     * @param key a PKCS#8 private key
+     * @param keyCertChain the X.509 certificate chain
+     * @param keyPassword the password of the {@code keyFile}, or {@code null} if it's not
+     *     password-protected
+     * @see #keyManager(File, File, String)
+     */
+    public static SslContextBuilder forServer(
+            PrivateKey key, String keyPassword, Iterable<? extends X509Certificate> keyCertChain) {
+        return forServer(key, keyPassword, toArray(keyCertChain, EMPTY_X509_CERTIFICATES));
+    }
+
     /**
      * Creates a builder for new server-side {@link SslContext}.
      *
@@ -131,6 +162,15 @@ public final class SslContextBuilder {
         return new SslContextBuilder(true).keyManager(keyManagerFactory);
     }
 
+    /**
+     * Creates a builder for new server-side {@link SslContext} with {@link KeyManager}.
+     *
+     * @param KeyManager non-{@code null} KeyManager for server's private key
+     */
+    public static SslContextBuilder forServer(KeyManager keyManager) {
+        return new SslContextBuilder(true).keyManager(keyManager);
+    }
+
     private final boolean forServer;
     private SslProvider provider;
     private Provider sslContextProvider;
@@ -149,6 +189,7 @@ public final class SslContextBuilder {
     private String[] protocols;
     private boolean startTls;
     private boolean enableOcsp;
+    private String keyStoreType = KeyStore.getDefaultType();
 
     private SslContextBuilder(boolean forServer) {
         this.forServer = forServer;
@@ -162,6 +203,14 @@ public final class SslContextBuilder {
         return this;
     }
 
+    /**
+     * Sets the {@link KeyStore} type that should be used. {@code null} uses the default one.
+     */
+    public SslContextBuilder keyStoreType(String keyStoreType) {
+        this.keyStoreType = keyStoreType;
+        return this;
+    }
+
     /**
      * The SSLContext {@link Provider} to use. {@code null} uses the default one. This is only
      * used with {@link SslProvider#JDK}.
@@ -205,6 +254,13 @@ public final class SslContextBuilder {
         return this;
     }
 
+    /**
+     * Trusted certificates for verifying the remote endpoint's certificate, {@code null} uses the system default.
+     */
+    public SslContextBuilder trustManager(Iterable<? extends X509Certificate> trustCertCollection) {
+        return trustManager(toArray(trustCertCollection, EMPTY_X509_CERTIFICATES));
+    }
+
     /**
      * Trusted manager for verifying the remote endpoint's certificate. {@code null} uses the system default.
      */
@@ -214,6 +270,19 @@ public final class SslContextBuilder {
         return this;
     }
 
+    /**
+     * A single trusted manager for verifying the remote endpoint's certificate.
+     * This is helpful when custom implementation of {@link TrustManager} is needed.
+     * Internally, a simple wrapper of {@link TrustManagerFactory} that only produces this
+     * specified {@link TrustManager} will be created, thus all the requirements specified in
+     * {@link #trustManager(TrustManagerFactory trustManagerFactory)} also apply here.
+     */
+    public SslContextBuilder trustManager(TrustManager trustManager) {
+        this.trustManagerFactory = new TrustManagerFactoryWrapper(trustManager);
+        trustCertCollection = null;
+        return this;
+    }
+
     /**
      * Identifying certificate for this host. {@code keyCertChainFile} and {@code keyFile} may
      * be {@code null} for client contexts, which disables mutual authentication.
@@ -247,6 +316,17 @@ public final class SslContextBuilder {
         return keyManager(key, null, keyCertChain);
     }
 
+    /**
+     * Identifying certificate for this host. {@code keyCertChain} and {@code key} may
+     * be {@code null} for client contexts, which disables mutual authentication.
+     *
+     * @param key a PKCS#8 private key
+     * @param keyCertChain an X.509 certificate chain
+     */
+    public SslContextBuilder keyManager(PrivateKey key, Iterable<? extends X509Certificate> keyCertChain) {
+        return keyManager(key, toArray(keyCertChain, EMPTY_X509_CERTIFICATES));
+    }
+
     /**
      * Identifying certificate for this host. {@code keyCertChainFile} and {@code keyFile} may
      * be {@code null} for client contexts, which disables mutual authentication.
@@ -331,6 +411,20 @@ public final class SslContextBuilder {
         return this;
     }
 
+    /**
+     * Identifying certificate for this host. {@code keyCertChain} and {@code key} may
+     * be {@code null} for client contexts, which disables mutual authentication.
+     *
+     * @param key a PKCS#8 private key file
+     * @param keyPassword the password of the {@code key}, or {@code null} if it's not
+     *     password-protected
+     * @param keyCertChain an X.509 certificate chain
+     */
+    public SslContextBuilder keyManager(PrivateKey key, String keyPassword,
+                                        Iterable<? extends X509Certificate> keyCertChain) {
+        return keyManager(key, keyPassword, toArray(keyCertChain, EMPTY_X509_CERTIFICATES));
+    }
+
     /**
      * Identifying manager for this host. {@code keyManagerFactory} may be {@code null} for
      * client contexts, which disables mutual authentication. Using a {@link KeyManagerFactory}
@@ -353,6 +447,28 @@ public final class SslContextBuilder {
         return this;
     }
 
+    /**
+     * A single key manager managing the identity information of this host.
+     * This is helpful when custom implementation of {@link KeyManager} is needed.
+     * Internally, a wrapper of {@link KeyManagerFactory} that only produces this specified
+     * {@link KeyManager} will be created, thus all the requirements specified in
+     * {@link #keyManager(KeyManagerFactory keyManagerFactory)} also apply here.
+     */
+    public SslContextBuilder keyManager(KeyManager keyManager) {
+        if (forServer) {
+            checkNotNull(keyManager, "keyManager required for servers");
+        }
+        if (keyManager != null) {
+            this.keyManagerFactory = new KeyManagerFactoryWrapper(keyManager);
+        } else {
+            this.keyManagerFactory = null;
+        }
+        keyCertChain = null;
+        key = null;
+        keyPassword = null;
+        return this;
+    }
+
     /**
      * The cipher suites to enable, in the order of preference. {@code null} to use default
      * cipher suites.
@@ -367,9 +483,8 @@ public final class SslContextBuilder {
      * cipher suites will be used.
      */
     public SslContextBuilder ciphers(Iterable<String> ciphers, CipherSuiteFilter cipherFilter) {
-        checkNotNull(cipherFilter, "cipherFilter");
+        this.cipherFilter = checkNotNull(cipherFilter, "cipherFilter");
         this.ciphers = ciphers;
-        this.cipherFilter = cipherFilter;
         return this;
     }
 
@@ -417,6 +532,15 @@ public final class SslContextBuilder {
         return this;
     }
 
+    /**
+     * The TLS protocol versions to enable.
+     * @param protocols The protocols to enable, or {@code null} to enable the default protocols.
+     * @see SSLEngine#setEnabledCipherSuites(String[])
+     */
+    public SslContextBuilder protocols(Iterable<String> protocols) {
+        return protocols(toArray(protocols, EMPTY_STRINGS));
+    }
+
     /**
      * {@code true} if the first write request shouldn't be encrypted.
      */
@@ -447,11 +571,22 @@ public final class SslContextBuilder {
             return SslContext.newServerContextInternal(provider, sslContextProvider, trustCertCollection,
                 trustManagerFactory, keyCertChain, key, keyPassword, keyManagerFactory,
                 ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, clientAuth, protocols, startTls,
-                enableOcsp);
+                enableOcsp, keyStoreType);
         } else {
             return SslContext.newClientContextInternal(provider, sslContextProvider, trustCertCollection,
                 trustManagerFactory, keyCertChain, key, keyPassword, keyManagerFactory,
-                ciphers, cipherFilter, apn, protocols, sessionCacheSize, sessionTimeout, enableOcsp);
+                ciphers, cipherFilter, apn, protocols, sessionCacheSize, sessionTimeout, enableOcsp, keyStoreType);
+        }
+    }
+
+    private static <T> T[] toArray(Iterable<? extends T> iterable, T[] prototype) {
+        if (iterable == null) {
+            return null;
+        }
+        final List<T> list = new ArrayList<T>();
+        for (T element : iterable) {
+            list.add(element);
         }
+        return list.toArray(prototype);
     }
 }
diff --git a/handler/src/main/java/io/netty/handler/ssl/SslHandler.java b/handler/src/main/java/io/netty/handler/ssl/SslHandler.java
index fe53373..b279abf 100644
--- a/handler/src/main/java/io/netty/handler/ssl/SslHandler.java
+++ b/handler/src/main/java/io/netty/handler/ssl/SslHandler.java
@@ -33,6 +33,7 @@ import io.netty.channel.ChannelPipeline;
 import io.netty.channel.ChannelPromise;
 import io.netty.channel.ChannelPromiseNotifier;
 import io.netty.handler.codec.ByteToMessageDecoder;
+import io.netty.handler.codec.DecoderException;
 import io.netty.handler.codec.UnsupportedMessageTypeException;
 import io.netty.util.ReferenceCountUtil;
 import io.netty.util.ReferenceCounted;
@@ -43,8 +44,8 @@ import io.netty.util.concurrent.FutureListener;
 import io.netty.util.concurrent.ImmediateExecutor;
 import io.netty.util.concurrent.Promise;
 import io.netty.util.concurrent.PromiseNotifier;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.PlatformDependent;
-import io.netty.util.internal.ThrowableUtil;
 import io.netty.util.internal.UnstableApi;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
@@ -55,10 +56,9 @@ import java.nio.ByteBuffer;
 import java.nio.channels.ClosedChannelException;
 import java.nio.channels.DatagramChannel;
 import java.nio.channels.SocketChannel;
-import java.util.ArrayList;
 import java.util.List;
-import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
+import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
 import java.util.regex.Pattern;
@@ -173,18 +173,6 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH
     private static final Pattern IGNORABLE_ERROR_MESSAGE = Pattern.compile(
             "^.*(?:connection.*(?:reset|closed|abort|broken)|broken.*pipe).*$", Pattern.CASE_INSENSITIVE);
 
-    /**
-     * Used in {@link #unwrapNonAppData(ChannelHandlerContext)} as input for
-     * {@link #unwrap(ChannelHandlerContext, ByteBuf, int,  int)}.  Using this static instance reduce object
-     * creation as {@link Unpooled#EMPTY_BUFFER#nioBuffer()} creates a new {@link ByteBuffer} everytime.
-     */
-    private static final SSLException SSLENGINE_CLOSED = ThrowableUtil.unknownStackTrace(
-            new SSLException("SSLEngine closed already"), SslHandler.class, "wrap(...)");
-    private static final SSLException HANDSHAKE_TIMED_OUT = ThrowableUtil.unknownStackTrace(
-            new SSLException("handshake timed out"), SslHandler.class, "handshake(...)");
-    private static final ClosedChannelException CHANNEL_CLOSED = ThrowableUtil.unknownStackTrace(
-            new ClosedChannelException(), SslHandler.class, "channelInactive(...)");
-
     /**
      * <a href="https://tools.ietf.org/html/rfc5246#section-6.2">2^14</a> which is the maximum sized plaintext chunk
      * allowed by the TLS RFC.
@@ -222,14 +210,10 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH
             }
 
             @Override
-            int getPacketBufferSize(SslHandler handler) {
-                return ((ReferenceCountedOpenSslEngine) handler.engine).maxEncryptedPacketLength0();
-            }
-
-            @Override
-            int calculateWrapBufferCapacity(SslHandler handler, int pendingBytes, int numComponents) {
-                return ((ReferenceCountedOpenSslEngine) handler.engine).calculateMaxLengthForWrap(pendingBytes,
-                                                                                                  numComponents);
+            ByteBuf allocateWrapBuffer(SslHandler handler, ByteBufAllocator allocator,
+                                       int pendingBytes, int numComponents) {
+                return allocator.directBuffer(((ReferenceCountedOpenSslEngine) handler.engine)
+                        .calculateMaxLengthForWrap(pendingBytes, numComponents));
             }
 
             @Override
@@ -271,8 +255,10 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH
             }
 
             @Override
-            int calculateWrapBufferCapacity(SslHandler handler, int pendingBytes, int numComponents) {
-                return ((ConscryptAlpnSslEngine) handler.engine).calculateOutNetBufSize(pendingBytes, numComponents);
+            ByteBuf allocateWrapBuffer(SslHandler handler, ByteBufAllocator allocator,
+                                       int pendingBytes, int numComponents) {
+                return allocator.directBuffer(
+                        ((ConscryptAlpnSslEngine) handler.engine).calculateOutNetBufSize(pendingBytes, numComponents));
             }
 
             @Override
@@ -314,8 +300,15 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH
             }
 
             @Override
-            int calculateWrapBufferCapacity(SslHandler handler, int pendingBytes, int numComponents) {
-                return handler.engine.getSession().getPacketBufferSize();
+            ByteBuf allocateWrapBuffer(SslHandler handler, ByteBufAllocator allocator,
+                                       int pendingBytes, int numComponents) {
+                // As for the JDK SSLEngine we always need to allocate buffers of the size required by the SSLEngine
+                // (normally ~16KB). This is required even if the amount of data to encrypt is very small. Use heap
+                // buffers to reduce the native memory usage.
+                //
+                // Beside this the JDK SSLEngine also (as of today) will do an extra heap to direct buffer copy
+                // if a direct buffer is used as its internals operate on byte[].
+                return allocator.heapBuffer(handler.engine.getSession().getPacketBufferSize());
             }
 
             @Override
@@ -339,23 +332,21 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH
             this.cumulator = cumulator;
         }
 
-        int getPacketBufferSize(SslHandler handler) {
-            return handler.engine.getSession().getPacketBufferSize();
-        }
-
         abstract SSLEngineResult unwrap(SslHandler handler, ByteBuf in, int readerIndex, int len, ByteBuf out)
                 throws SSLException;
 
-        abstract int calculateWrapBufferCapacity(SslHandler handler, int pendingBytes, int numComponents);
-
         abstract int calculatePendingData(SslHandler handler, int guess);
 
         abstract boolean jdkCompatibilityMode(SSLEngine engine);
 
+        abstract ByteBuf allocateWrapBuffer(SslHandler handler, ByteBufAllocator allocator,
+                                            int pendingBytes, int numComponents);
+
         // BEGIN Platform-dependent flags
 
         /**
-         * {@code true} if and only if {@link SSLEngine} expects a direct buffer.
+         * {@code true} if and only if {@link SSLEngine} expects a direct buffer and so if a heap buffer
+         * is given will make an extra memory copy.
          */
         final boolean wantsDirectBuffer;
 
@@ -390,6 +381,7 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH
     private boolean flushedBeforeHandshake;
     private boolean readDuringHandshake;
     private boolean handshakeStarted;
+
     private SslHandlerCoalescingBufferQueue pendingUnencryptedWrites;
     private Promise<Channel> handshakePromise = new LazyChannelPromise();
     private final LazyChannelPromise sslClosePromise = new LazyChannelPromise();
@@ -402,6 +394,7 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH
 
     private boolean outboundClosed;
     private boolean closeNotify;
+    private boolean processTask;
 
     private int packetLength;
 
@@ -417,7 +410,7 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH
     volatile int wrapDataSize = MAX_PLAINTEXT_LENGTH;
 
     /**
-     * Creates a new instance.
+     * Creates a new instance which runs all delegated tasks directly on the {@link EventExecutor}.
      *
      * @param engine  the {@link SSLEngine} this handler will use
      */
@@ -426,39 +419,40 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH
     }
 
     /**
-     * Creates a new instance.
+     * Creates a new instance which runs all delegated tasks directly on the {@link EventExecutor}.
      *
      * @param engine    the {@link SSLEngine} this handler will use
      * @param startTls  {@code true} if the first write request shouldn't be
      *                  encrypted by the {@link SSLEngine}
      */
-    @SuppressWarnings("deprecation")
     public SslHandler(SSLEngine engine, boolean startTls) {
         this(engine, startTls, ImmediateExecutor.INSTANCE);
     }
 
     /**
-     * @deprecated Use {@link #SslHandler(SSLEngine)} instead.
+     * Creates a new instance.
+     *
+     * @param engine  the {@link SSLEngine} this handler will use
+     * @param delegatedTaskExecutor the {@link Executor} that will be used to execute tasks that are returned by
+     *                              {@link SSLEngine#getDelegatedTask()}.
      */
-    @Deprecated
     public SslHandler(SSLEngine engine, Executor delegatedTaskExecutor) {
         this(engine, false, delegatedTaskExecutor);
     }
 
     /**
-     * @deprecated Use {@link #SslHandler(SSLEngine, boolean)} instead.
+     * Creates a new instance.
+     *
+     * @param engine  the {@link SSLEngine} this handler will use
+     * @param startTls  {@code true} if the first write request shouldn't be
+     *                  encrypted by the {@link SSLEngine}
+     * @param delegatedTaskExecutor the {@link Executor} that will be used to execute tasks that are returned by
+     *                              {@link SSLEngine#getDelegatedTask()}.
      */
-    @Deprecated
     public SslHandler(SSLEngine engine, boolean startTls, Executor delegatedTaskExecutor) {
-        if (engine == null) {
-            throw new NullPointerException("engine");
-        }
-        if (delegatedTaskExecutor == null) {
-            throw new NullPointerException("delegatedTaskExecutor");
-        }
-        this.engine = engine;
+        this.engine = ObjectUtil.checkNotNull(engine, "engine");
+        this.delegatedTaskExecutor = ObjectUtil.checkNotNull(delegatedTaskExecutor, "delegatedTaskExecutor");
         engineType = SslEngineType.forEngine(engine);
-        this.delegatedTaskExecutor = delegatedTaskExecutor;
         this.startTls = startTls;
         this.jdkCompatibilityMode = engineType.jdkCompatibilityMode(engine);
         setCumulator(engineType.cumulator);
@@ -469,10 +463,7 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH
     }
 
     public void setHandshakeTimeout(long handshakeTimeout, TimeUnit unit) {
-        if (unit == null) {
-            throw new NullPointerException("unit");
-        }
-
+        ObjectUtil.checkNotNull(unit, "unit");
         setHandshakeTimeoutMillis(unit.toMillis(handshakeTimeout));
     }
 
@@ -774,6 +765,10 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH
             return;
         }
 
+        if (processTask) {
+            return;
+        }
+
         try {
             wrapAndFlush(ctx);
         } catch (Throwable cause) {
@@ -813,7 +808,7 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH
             final int wrapDataSize = this.wrapDataSize;
             // Only continue to loop if the handler was not removed in the meantime.
             // See https://github.com/netty/netty/issues/5860
-            while (!ctx.isRemoved()) {
+            outer: while (!ctx.isRemoved()) {
                 promise = ctx.newPromise();
                 buf = wrapDataSize > 0 ?
                         pendingUnencryptedWrites.remove(alloc, wrapDataSize, promise) :
@@ -831,11 +826,12 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH
                 if (result.getStatus() == Status.CLOSED) {
                     buf.release();
                     buf = null;
-                    promise.tryFailure(SSLENGINE_CLOSED);
+                    SSLException exception = new SSLException("SSLEngine closed already");
+                    promise.tryFailure(exception);
                     promise = null;
                     // SSLEngine has been closed already.
                     // Any further write attempts should be denied.
-                    pendingUnencryptedWrites.releaseAndFailAll(ctx, SSLENGINE_CLOSED);
+                    pendingUnencryptedWrites.releaseAndFailAll(ctx, exception);
                     return;
                 } else {
                     if (buf.isReadable()) {
@@ -850,7 +846,11 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH
 
                     switch (result.getHandshakeStatus()) {
                         case NEED_TASK:
-                            runDelegatedTasks();
+                            if (!runDelegatedTasks(inUnwrap)) {
+                                // We scheduled a task on the delegatingTaskExecutor, so stop processing as we will
+                                // resume once the task completes.
+                                break outer;
+                            }
                             break;
                         case FINISHED:
                             setHandshakeSuccess();
@@ -858,11 +858,25 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH
                         case NOT_HANDSHAKING:
                             setHandshakeSuccessIfStillHandshaking();
                             // deliberate fall-through
-                        case NEED_WRAP:
-                            finishWrap(ctx, out, promise, inUnwrap, false);
+                        case NEED_WRAP: {
+                            ChannelPromise p = promise;
+
+                            // Null out the promise so it is not reused in the finally block in the cause of
+                            // finishWrap(...) throwing.
                             promise = null;
-                            out = null;
+                            final ByteBuf b;
+
+                            if (out.isReadable()) {
+                                // There is something in the out buffer. Ensure we null it out so it is not re-used.
+                                b = out;
+                                out = null;
+                            } else {
+                                // If out is not readable we can re-use it and so save an extra allocation
+                                b = null;
+                            }
+                            finishWrap(ctx, b, p, inUnwrap, false);
                             break;
+                        }
                         case NEED_UNWRAP:
                             needUnwrap = true;
                             return;
@@ -913,13 +927,13 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH
      * {@link #setHandshakeFailure(ChannelHandlerContext, Throwable)}.
      * @return {@code true} if this method ends on {@link SSLEngineResult.HandshakeStatus#NOT_HANDSHAKING}.
      */
-    private boolean wrapNonAppData(ChannelHandlerContext ctx, boolean inUnwrap) throws SSLException {
+    private boolean wrapNonAppData(final ChannelHandlerContext ctx, boolean inUnwrap) throws SSLException {
         ByteBuf out = null;
         ByteBufAllocator alloc = ctx.alloc();
         try {
             // Only continue to loop if the handler was not removed in the meantime.
             // See https://github.com/netty/netty/issues/5860
-            while (!ctx.isRemoved()) {
+            outer: while (!ctx.isRemoved()) {
                 if (out == null) {
                     // As this is called for the handshake we have no real idea how big the buffer needs to be.
                     // That said 2048 should give us enough room to include everything like ALPN / NPN data.
@@ -929,19 +943,32 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH
                 SSLEngineResult result = wrap(alloc, engine, Unpooled.EMPTY_BUFFER, out);
 
                 if (result.bytesProduced() > 0) {
-                    ctx.write(out);
+                    ctx.write(out).addListener(new ChannelFutureListener() {
+                        @Override
+                        public void operationComplete(ChannelFuture future) {
+                            Throwable cause = future.cause();
+                            if (cause != null) {
+                                setHandshakeFailureTransportFailure(ctx, cause);
+                            }
+                        }
+                    });
                     if (inUnwrap) {
                         needsFlush = true;
                     }
                     out = null;
                 }
 
-                switch (result.getHandshakeStatus()) {
+                HandshakeStatus status = result.getHandshakeStatus();
+                switch (status) {
                     case FINISHED:
                         setHandshakeSuccess();
                         return false;
                     case NEED_TASK:
-                        runDelegatedTasks();
+                        if (!runDelegatedTasks(inUnwrap)) {
+                            // We scheduled a task on the delegatingTaskExecutor, so stop processing as we will
+                            // resume once the task completes.
+                            break outer;
+                        }
                         break;
                     case NEED_UNWRAP:
                         if (inUnwrap) {
@@ -967,7 +994,9 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH
                         throw new IllegalStateException("Unknown handshake status: " + result.getHandshakeStatus());
                 }
 
-                if (result.bytesProduced() == 0) {
+                // Check if did not produce any bytes and if so break out of the loop, but only if we did not process
+                // a task as last action. It's fine to not produce any data as part of executing a task.
+                if (result.bytesProduced() == 0 && status != HandshakeStatus.NEED_TASK) {
                     break;
                 }
 
@@ -1044,12 +1073,13 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH
 
     @Override
     public void channelInactive(ChannelHandlerContext ctx) throws Exception {
+        ClosedChannelException exception = new ClosedChannelException();
         // Make sure to release SSLEngine,
         // and notify the handshake future if the connection has been closed during handshake.
-        setHandshakeFailure(ctx, CHANNEL_CLOSED, !outboundClosed, handshakeStarted, false);
+        setHandshakeFailure(ctx, exception, !outboundClosed, handshakeStarted, false);
 
         // Ensure we always notify the sslClosePromise as well
-        notifyClosePromise(CHANNEL_CLOSED);
+        notifyClosePromise(exception);
 
         super.channelInactive(ctx);
     }
@@ -1133,8 +1163,10 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH
                         return true;
                     }
                 } catch (Throwable cause) {
-                    logger.debug("Unexpected exception while loading class {} classname {}",
-                                 getClass(), classname, cause);
+                    if (logger.isDebugEnabled()) {
+                        logger.debug("Unexpected exception while loading class {} classname {}",
+                                getClass(), classname, cause);
+                    }
                 }
             }
         }
@@ -1243,6 +1275,9 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH
 
     @Override
     protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws SSLException {
+        if (processTask) {
+            return;
+        }
         if (jdkCompatibilityMode) {
             decodeJdkCompatible(ctx, in);
         } else {
@@ -1252,6 +1287,10 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH
 
     @Override
     public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
+        channelReadComplete0(ctx);
+    }
+
+    private void channelReadComplete0(ChannelHandlerContext ctx) {
         // Discard bytes of the cumulation buffer if needed.
         discardSomeReadBytes();
 
@@ -1366,7 +1405,16 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH
                         }
                         break;
                     case NEED_TASK:
-                        runDelegatedTasks();
+                        if (!runDelegatedTasks(true)) {
+                            // We scheduled a task on the delegatingTaskExecutor, so stop processing as we will
+                            // resume once the task completes.
+                            //
+                            // We break out of the loop only and do NOT return here as we still may need to notify
+                            // about the closure of the SSLEngine.
+                            //
+                            wrapLater = false;
+                            break unwrapLoop;
+                        }
                         break;
                     case FINISHED:
                         setHandshakeSuccess();
@@ -1401,7 +1449,9 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH
                         throw new IllegalStateException("unknown handshake status: " + handshakeStatus);
                 }
 
-                if (status == Status.BUFFER_UNDERFLOW || consumed == 0 && produced == 0) {
+                if (status == Status.BUFFER_UNDERFLOW ||
+                        // If we processed NEED_TASK we should try again even we did not consume or produce anything.
+                        handshakeStatus != HandshakeStatus.NEED_TASK && consumed == 0 && produced == 0) {
                     if (handshakeStatus == HandshakeStatus.NEED_UNWRAP) {
                         // The underlying engine is starving so we need to feed it with more data.
                         // See https://github.com/netty/netty/pull/5039
@@ -1447,65 +1497,239 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH
                 out.nioBuffer(index, len);
     }
 
-    /**
-     * Fetches all delegated tasks from the {@link SSLEngine} and runs them via the {@link #delegatedTaskExecutor}.
-     * If the {@link #delegatedTaskExecutor} is {@link ImmediateExecutor}, just call {@link Runnable#run()} directly
-     * instead of using {@link Executor#execute(Runnable)}.  Otherwise, run the tasks via
-     * the {@link #delegatedTaskExecutor} and wait until the tasks are finished.
-     */
-    private void runDelegatedTasks() {
-        if (delegatedTaskExecutor == ImmediateExecutor.INSTANCE) {
-            for (;;) {
-                Runnable task = engine.getDelegatedTask();
-                if (task == null) {
-                    break;
-                }
+    private static boolean inEventLoop(Executor executor) {
+        return executor instanceof EventExecutor && ((EventExecutor) executor).inEventLoop();
+    }
 
-                task.run();
+    private static void runAllDelegatedTasks(SSLEngine engine) {
+        for (;;) {
+            Runnable task = engine.getDelegatedTask();
+            if (task == null) {
+                return;
             }
+            task.run();
+        }
+    }
+
+    /**
+     * Will either run the delegated task directly calling {@link Runnable#run()} and return {@code true} or will
+     * offload the delegated task using {@link Executor#execute(Runnable)} and return {@code false}.
+     *
+     * If the task is offloaded it will take care to resume its work on the {@link EventExecutor} once there are no
+     * more tasks to process.
+     */
+    private boolean runDelegatedTasks(boolean inUnwrap) {
+        if (delegatedTaskExecutor == ImmediateExecutor.INSTANCE || inEventLoop(delegatedTaskExecutor)) {
+            // We should run the task directly in the EventExecutor thread and not offload at all.
+            runAllDelegatedTasks(engine);
+            return true;
         } else {
-            final List<Runnable> tasks = new ArrayList<Runnable>(2);
-            for (;;) {
-                final Runnable task = engine.getDelegatedTask();
-                if (task == null) {
-                    break;
+            executeDelegatedTasks(inUnwrap);
+            return false;
+        }
+    }
+
+    private void executeDelegatedTasks(boolean inUnwrap) {
+        processTask = true;
+        try {
+            delegatedTaskExecutor.execute(new SslTasksRunner(inUnwrap));
+        } catch (RejectedExecutionException e) {
+            processTask = false;
+            throw e;
+        }
+    }
+
+    /**
+     * {@link Runnable} that will be scheduled on the {@code delegatedTaskExecutor} and will take care
+     * of resume work on the {@link EventExecutor} once the task was executed.
+     */
+    private final class SslTasksRunner implements Runnable {
+        private final boolean inUnwrap;
+
+        SslTasksRunner(boolean inUnwrap) {
+            this.inUnwrap = inUnwrap;
+        }
+
+        // Handle errors which happened during task processing.
+        private void taskError(Throwable e) {
+            if (inUnwrap) {
+                // As the error happened while the task was scheduled as part of unwrap(...) we also need to ensure
+                // we fire it through the pipeline as inbound error to be consistent with what we do in decode(...).
+                //
+                // This will also ensure we fail the handshake future and flush all produced data.
+                try {
+                    handleUnwrapThrowable(ctx, e);
+                } catch (Throwable cause) {
+                    safeExceptionCaught(cause);
                 }
+            } else {
+                setHandshakeFailure(ctx, e);
+                forceFlush(ctx);
+            }
+        }
 
-                tasks.add(task);
+        // Try to call exceptionCaught(...)
+        private void safeExceptionCaught(Throwable cause) {
+            try {
+                exceptionCaught(ctx, wrapIfNeeded(cause));
+            } catch (Throwable error) {
+                ctx.fireExceptionCaught(error);
             }
+        }
 
-            if (tasks.isEmpty()) {
-                return;
+        private Throwable wrapIfNeeded(Throwable cause) {
+            if (!inUnwrap) {
+                // If we are not in unwrap(...) we can just rethrow without wrapping at all.
+                return cause;
+            }
+            // As the exception would have been triggered by an inbound operation we will need to wrap it in a
+            // DecoderException to mimic what a decoder would do when decode(...) throws.
+            return cause instanceof DecoderException ? cause : new DecoderException(cause);
+        }
+
+        private void tryDecodeAgain() {
+            try {
+                channelRead(ctx, Unpooled.EMPTY_BUFFER);
+            } catch (Throwable cause) {
+                safeExceptionCaught(cause);
+            } finally {
+                // As we called channelRead(...) we also need to call channelReadComplete(...) which
+                // will ensure we either call ctx.fireChannelReadComplete() or will trigger a ctx.read() if
+                // more data is needed.
+                channelReadComplete0(ctx);
             }
+        }
 
-            final CountDownLatch latch = new CountDownLatch(1);
-            delegatedTaskExecutor.execute(new Runnable() {
-                @Override
-                public void run() {
-                    try {
-                        for (Runnable task: tasks) {
-                            task.run();
+        /**
+         * Executed after the wrapped {@code task} was executed via {@code delegatedTaskExecutor} to resume work
+         * on the {@link EventExecutor}.
+         */
+        private void resumeOnEventExecutor() {
+            assert ctx.executor().inEventLoop();
+
+            processTask = false;
+
+            try {
+                HandshakeStatus status = engine.getHandshakeStatus();
+                switch (status) {
+                    // There is another task that needs to be executed and offloaded to the delegatingTaskExecutor.
+                    case NEED_TASK:
+                        executeDelegatedTasks(inUnwrap);
+
+                        break;
+
+                    // The handshake finished, lets notify about the completion of it and resume processing.
+                    case FINISHED:
+                        setHandshakeSuccess();
+
+                        // deliberate fall-through
+
+                    // Not handshaking anymore, lets notify about the completion if not done yet and resume processing.
+                    case NOT_HANDSHAKING:
+                        setHandshakeSuccessIfStillHandshaking();
+                        try {
+                            // Lets call wrap to ensure we produce the alert if there is any pending and also to
+                            // ensure we flush any queued data..
+                            wrap(ctx, inUnwrap);
+                        } catch (Throwable e) {
+                            taskError(e);
+                            return;
+                        }
+                        if (inUnwrap) {
+                            // If we were in the unwrap call when the task was processed we should also try to unwrap
+                            // non app data first as there may not anything left in the inbound buffer to process.
+                            unwrapNonAppData(ctx);
                         }
-                    } catch (Exception e) {
-                        ctx.fireExceptionCaught(e);
-                    } finally {
-                        latch.countDown();
-                    }
-                }
-            });
 
-            boolean interrupted = false;
-            while (latch.getCount() != 0) {
-                try {
-                    latch.await();
-                } catch (InterruptedException e) {
-                    // Interrupt later.
-                    interrupted = true;
+                        // Flush now as we may have written some data as part of the wrap call.
+                        forceFlush(ctx);
+
+                        tryDecodeAgain();
+                        break;
+
+                    // We need more data so lets try to unwrap first and then call decode again which will feed us
+                    // with buffered data (if there is any).
+                    case NEED_UNWRAP:
+                        try {
+                            unwrapNonAppData(ctx);
+                        } catch (SSLException e) {
+                            handleUnwrapThrowable(ctx, e);
+                            return;
+                        }
+                        tryDecodeAgain();
+                        break;
+
+                    // To make progress we need to call SSLEngine.wrap(...) which may produce more output data
+                    // that will be written to the Channel.
+                    case NEED_WRAP:
+                        try {
+                            if (!wrapNonAppData(ctx, false) && inUnwrap) {
+                                // The handshake finished in wrapNonAppData(...), we need to try call
+                                // unwrapNonAppData(...) as we may have some alert that we should read.
+                                //
+                                // This mimics what we would do when we are calling this method while in unwrap(...).
+                                unwrapNonAppData(ctx);
+                            }
+
+                            // Flush now as we may have written some data as part of the wrap call.
+                            forceFlush(ctx);
+                        } catch (Throwable e) {
+                            taskError(e);
+                            return;
+                        }
+
+                        // Now try to feed in more data that we have buffered.
+                        tryDecodeAgain();
+                        break;
+
+                    default:
+                        // Should never reach here as we handle all cases.
+                        throw new AssertionError();
                 }
+            } catch (Throwable cause) {
+                safeExceptionCaught(cause);
             }
+        }
+
+        @Override
+        public void run() {
+            try {
+                runAllDelegatedTasks(engine);
+
+                // All tasks were processed.
+                assert engine.getHandshakeStatus() != HandshakeStatus.NEED_TASK;
 
-            if (interrupted) {
-                Thread.currentThread().interrupt();
+                // Jump back on the EventExecutor.
+                ctx.executor().execute(new Runnable() {
+                    @Override
+                    public void run() {
+                        resumeOnEventExecutor();
+                    }
+                });
+            } catch (final Throwable cause) {
+                handleException(cause);
+            }
+        }
+
+        private void handleException(final Throwable cause) {
+            if (ctx.executor().inEventLoop()) {
+                processTask = false;
+                safeExceptionCaught(cause);
+            } else {
+                try {
+                    ctx.executor().execute(new Runnable() {
+                        @Override
+                        public void run() {
+                            processTask = false;
+                            safeExceptionCaught(cause);
+                        }
+                    });
+                } catch (RejectedExecutionException ignore) {
+                    processTask = false;
+                    // the context itself will handle the rejected exception when try to schedule the operation so
+                    // ignore the RejectedExecutionException
+                    ctx.fireExceptionCaught(cause);
+                }
             }
         }
     }
@@ -1582,11 +1806,26 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH
             }
         } finally {
             // Ensure we remove and fail all pending writes in all cases and so release memory quickly.
-            releaseAndFailAll(cause);
+            releaseAndFailAll(ctx, cause);
         }
     }
 
-    private void releaseAndFailAll(Throwable cause) {
+    private void setHandshakeFailureTransportFailure(ChannelHandlerContext ctx, Throwable cause) {
+        // If TLS control frames fail to write we are in an unknown state and may become out of
+        // sync with our peer. We give up and close the channel. This will also take care of
+        // cleaning up any outstanding state (e.g. handshake promise, queued unencrypted data).
+        try {
+            SSLException transportFailure = new SSLException("failure when writing TLS control frames", cause);
+            releaseAndFailAll(ctx, transportFailure);
+            if (handshakePromise.tryFailure(transportFailure)) {
+                ctx.fireUserEventTriggered(new SslHandshakeCompletionEvent(transportFailure));
+            }
+        } finally {
+            ctx.close();
+        }
+    }
+
+    private void releaseAndFailAll(ChannelHandlerContext ctx, Throwable cause) {
         if (pendingUnencryptedWrites != null) {
             pendingUnencryptedWrites.releaseAndFailAll(ctx, cause);
         }
@@ -1694,9 +1933,7 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH
      * Performs TLS renegotiation.
      */
     public Future<Channel> renegotiate(final Promise<Channel> promise) {
-        if (promise == null) {
-            throw new NullPointerException("promise");
-        }
+        ObjectUtil.checkNotNull(promise, "promise");
 
         ChannelHandlerContext ctx = this.ctx;
         if (ctx == null) {
@@ -1777,12 +2014,14 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH
                 if (localHandshakePromise.isDone()) {
                     return;
                 }
+                SSLException exception =
+                        new SslHandshakeTimeoutException("handshake timed out after " + handshakeTimeoutMillis + "ms");
                 try {
-                    if (localHandshakePromise.tryFailure(HANDSHAKE_TIMED_OUT)) {
-                        SslUtils.handleHandshakeFailure(ctx, HANDSHAKE_TIMED_OUT, true);
+                    if (localHandshakePromise.tryFailure(exception)) {
+                        SslUtils.handleHandshakeFailure(ctx, exception, true);
                     }
                 } finally {
-                    releaseAndFailAll(HANDSHAKE_TIMED_OUT);
+                    releaseAndFailAll(ctx, exception);
                 }
             }
         }, handshakeTimeoutMillis, TimeUnit.MILLISECONDS);
@@ -1920,7 +2159,7 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH
      * the specified amount of pending bytes.
      */
     private ByteBuf allocateOutNetBuf(ChannelHandlerContext ctx, int pendingBytes, int numComponents) {
-        return allocate(ctx, engineType.calculateWrapBufferCapacity(this, pendingBytes, numComponents));
+        return engineType.allocateWrapBuffer(this, ctx.alloc(), pendingBytes, numComponents);
     }
 
     /**
@@ -1954,7 +2193,11 @@ public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundH
         protected ByteBuf composeFirst(ByteBufAllocator allocator, ByteBuf first) {
             if (first instanceof CompositeByteBuf) {
                 CompositeByteBuf composite = (CompositeByteBuf) first;
-                first = allocator.directBuffer(composite.readableBytes());
+                if (engineType.wantsDirectBuffer) {
+                    first = allocator.directBuffer(composite.readableBytes());
+                } else {
+                    first = allocator.heapBuffer(composite.readableBytes());
+                }
                 try {
                     first.writeBytes(composite);
                 } catch (Throwable cause) {
diff --git a/handler/src/main/java/io/netty/handler/ssl/SslHandshakeTimeoutException.java b/handler/src/main/java/io/netty/handler/ssl/SslHandshakeTimeoutException.java
new file mode 100644
index 0000000..fbddbcc
--- /dev/null
+++ b/handler/src/main/java/io/netty/handler/ssl/SslHandshakeTimeoutException.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2020 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.ssl;
+
+import javax.net.ssl.SSLHandshakeException;
+
+/**
+ * {@link SSLHandshakeException} that is used when a handshake failed due a configured timeout.
+ */
+public final class SslHandshakeTimeoutException extends SSLHandshakeException {
+
+    SslHandshakeTimeoutException(String reason) {
+        super(reason);
+    }
+}
diff --git a/handler/src/main/java/io/netty/handler/ssl/SslMasterKeyHandler.java b/handler/src/main/java/io/netty/handler/ssl/SslMasterKeyHandler.java
new file mode 100644
index 0000000..b1c710c
--- /dev/null
+++ b/handler/src/main/java/io/netty/handler/ssl/SslMasterKeyHandler.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2019 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.ssl;
+
+import io.netty.buffer.ByteBufUtil;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.util.internal.ReflectionUtil;
+import io.netty.util.internal.SystemPropertyUtil;
+import io.netty.util.internal.logging.InternalLogger;
+import io.netty.util.internal.logging.InternalLoggerFactory;
+
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLSession;
+import java.lang.reflect.Field;
+
+/**
+ * The {@link SslMasterKeyHandler} is a channel-handler you can include in your pipeline to consume the master key
+ * & session identifier for a TLS session.
+ * This can be very useful, for instance the {@link WiresharkSslMasterKeyHandler} implementation will
+ * log the secret & identifier in a format that is consumable by Wireshark -- allowing easy decryption of pcap/tcpdumps.
+ */
+public abstract class SslMasterKeyHandler extends ChannelInboundHandlerAdapter {
+
+    private static final InternalLogger logger = InternalLoggerFactory.getInstance(SslMasterKeyHandler.class);
+
+    /**
+     * The JRE SSLSessionImpl cannot be imported
+     */
+    private static final Class<?> SSL_SESSIONIMPL_CLASS;
+
+    /**
+     * The master key field in the SSLSessionImpl
+     */
+    private static final Field SSL_SESSIONIMPL_MASTER_SECRET_FIELD;
+
+    /**
+     * A system property that can be used to turn on/off the {@link SslMasterKeyHandler} dynamically without having
+     * to edit your pipeline.
+     * <code>-Dio.netty.ssl.masterKeyHandler=true</code>
+     */
+    public static final String SYSTEM_PROP_KEY = "io.netty.ssl.masterKeyHandler";
+
+    /**
+     * The unavailability cause of whether the private Sun implementation of SSLSessionImpl is available.
+     */
+    private static final Throwable UNAVAILABILITY_CAUSE;
+
+    static {
+        Throwable cause = null;
+        Class<?> clazz = null;
+        Field field = null;
+        try {
+            clazz = Class.forName("sun.security.ssl.SSLSessionImpl");
+            field = clazz.getDeclaredField("masterSecret");
+            cause = ReflectionUtil.trySetAccessible(field, true);
+        } catch (Throwable e) {
+            cause = e;
+            logger.debug("sun.security.ssl.SSLSessionImpl is unavailable.", e);
+        }
+        UNAVAILABILITY_CAUSE = cause;
+        SSL_SESSIONIMPL_CLASS = clazz;
+        SSL_SESSIONIMPL_MASTER_SECRET_FIELD = field;
+    }
+
+    /**
+     * Constructor.
+    */
+    protected SslMasterKeyHandler() {
+    }
+
+    /**
+     * Ensure that SSLSessionImpl is available.
+     * @throws UnsatisfiedLinkError if unavailable
+     */
+    public static void ensureSunSslEngineAvailability() {
+        if (UNAVAILABILITY_CAUSE != null) {
+            throw new IllegalStateException(
+                    "Failed to find SSLSessionImpl on classpath", UNAVAILABILITY_CAUSE);
+        }
+    }
+
+    /**
+     * Returns the cause of unavailability.
+     *
+     * @return the cause if unavailable. {@code null} if available.
+     */
+    public static Throwable sunSslEngineUnavailabilityCause() {
+        return UNAVAILABILITY_CAUSE;
+    }
+
+    /* Returns {@code true} if and only if sun.security.ssl.SSLSessionImpl exists in the runtime.
+     */
+    public static boolean isSunSslEngineAvailable() {
+        return UNAVAILABILITY_CAUSE == null;
+    }
+
+    /**
+     * Consume the master key for the session and the sessionId
+     * @param masterKey A 48-byte secret shared between the client and server.
+     * @param session The current TLS session
+     */
+    protected abstract void accept(SecretKey masterKey, SSLSession session);
+
+    @Override
+    public final void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
+        //only try to log the session info if the ssl handshake has successfully completed.
+        if (evt == SslHandshakeCompletionEvent.SUCCESS) {
+            boolean shouldHandle = SystemPropertyUtil.getBoolean(SYSTEM_PROP_KEY, false);
+
+            if (shouldHandle) {
+                final SslHandler handler = ctx.pipeline().get(SslHandler.class);
+                final SSLEngine engine = handler.engine();
+                final SSLSession sslSession = engine.getSession();
+
+                //the OpenJDK does not expose a way to get the master secret, so try to use reflection to get it.
+                if (isSunSslEngineAvailable() && sslSession.getClass().equals(SSL_SESSIONIMPL_CLASS)) {
+                    final SecretKey secretKey;
+                    try {
+                        secretKey = (SecretKey) SSL_SESSIONIMPL_MASTER_SECRET_FIELD.get(sslSession);
+                    } catch (IllegalAccessException e) {
+                        throw new IllegalArgumentException("Failed to access the field 'masterSecret' " +
+                                "via reflection.", e);
+                    }
+                    accept(secretKey, sslSession);
+                } else if (OpenSsl.isAvailable() && engine instanceof ReferenceCountedOpenSslEngine) {
+                    SecretKeySpec secretKey = ((ReferenceCountedOpenSslEngine) engine).masterKey();
+                    accept(secretKey, sslSession);
+                }
+            }
+        }
+
+        ctx.fireUserEventTriggered(evt);
+    }
+
+    /**
+     * Create a {@link WiresharkSslMasterKeyHandler} instance.
+     * This TLS master key handler logs the master key and session-id in a format
+     * understood by Wireshark -- this can be especially useful if you need to ever
+     * decrypt a TLS session and are using perfect forward secrecy (i.e. Diffie-Hellman)
+     * The key and session identifier are forwarded to the log named 'io.netty.wireshark'.
+     */
+    public static SslMasterKeyHandler newWireSharkSslMasterKeyHandler() {
+        return new WiresharkSslMasterKeyHandler();
+    }
+
+    /**
+     * Record the session identifier and master key to the {@link InternalLogger} named <code>io.netty.wireshark</code>.
+     * ex. <code>RSA Session-ID:XXX Master-Key:YYY</code>
+     * This format is understood by Wireshark 1.6.0.
+     * https://code.wireshark.org/review/gitweb?p=wireshark.git;a=commit;h=686d4cabb41185591c361f9ec6b709034317144b
+     * The key and session identifier are forwarded to the log named 'io.netty.wireshark'.
+     */
+    private static final class WiresharkSslMasterKeyHandler extends SslMasterKeyHandler {
+
+        private static final InternalLogger wireshark_logger =
+                InternalLoggerFactory.getInstance("io.netty.wireshark");
+
+        private static final char[] hexCode = "0123456789ABCDEF".toCharArray();
+
+        @Override
+        protected void accept(SecretKey masterKey, SSLSession session) {
+            if (masterKey.getEncoded().length != 48) {
+                throw new IllegalArgumentException("An invalid length master key was provided.");
+            }
+            final byte[] sessionId = session.getId();
+            wireshark_logger.warn("RSA Session-ID:{} Master-Key:{}",
+                    ByteBufUtil.hexDump(sessionId).toLowerCase(),
+                    ByteBufUtil.hexDump(masterKey.getEncoded()).toLowerCase());
+        }
+    }
+
+}
diff --git a/handler/src/main/java/io/netty/handler/ssl/SslProvider.java b/handler/src/main/java/io/netty/handler/ssl/SslProvider.java
index 00fc2aa..e72cfed 100644
--- a/handler/src/main/java/io/netty/handler/ssl/SslProvider.java
+++ b/handler/src/main/java/io/netty/handler/ssl/SslProvider.java
@@ -35,5 +35,21 @@ public enum SslProvider {
      * OpenSSL-based implementation which does not have finalizers and instead implements {@link ReferenceCounted}.
      */
     @UnstableApi
-    OPENSSL_REFCNT
+    OPENSSL_REFCNT;
+
+    /**
+     * Returns {@code true} if the specified {@link SslProvider} supports
+     * <a href="https://tools.ietf.org/html/rfc7301#section-6">TLS ALPN Extension</a>, {@code false} otherwise.
+     */
+    public static boolean isAlpnSupported(final SslProvider provider) {
+        switch (provider) {
+            case JDK:
+                return JdkAlpnApplicationProtocolNegotiator.isAlpnSupported();
+            case OPENSSL:
+            case OPENSSL_REFCNT:
+                return OpenSsl.isAlpnSupported();
+            default:
+                throw new Error("Unknown SslProvider: " + provider);
+        }
+    }
 }
diff --git a/handler/src/main/java/io/netty/handler/ssl/SslUtils.java b/handler/src/main/java/io/netty/handler/ssl/SslUtils.java
index e764036..661c846 100644
--- a/handler/src/main/java/io/netty/handler/ssl/SslUtils.java
+++ b/handler/src/main/java/io/netty/handler/ssl/SslUtils.java
@@ -113,6 +113,7 @@ final class SslUtils {
         defaultCiphers.add("TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384");
         defaultCiphers.add("TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256");
         defaultCiphers.add("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256");
+        defaultCiphers.add("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384");
         defaultCiphers.add("TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA");
         // AES256 requires JCE unlimited strength jurisdiction policy files.
         defaultCiphers.add("TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA");
@@ -122,9 +123,7 @@ final class SslUtils {
         // AES256 requires JCE unlimited strength jurisdiction policy files.
         defaultCiphers.add("TLS_RSA_WITH_AES_256_CBC_SHA");
 
-        for (String tlsv13Cipher: DEFAULT_TLSV13_CIPHER_SUITES) {
-            defaultCiphers.add(tlsv13Cipher);
-        }
+        Collections.addAll(defaultCiphers, DEFAULT_TLSV13_CIPHER_SUITES);
 
         DEFAULT_CIPHER_SUITES = defaultCiphers.toArray(new String[0]);
     }
diff --git a/handler/src/main/java/io/netty/handler/ssl/SupportedCipherSuiteFilter.java b/handler/src/main/java/io/netty/handler/ssl/SupportedCipherSuiteFilter.java
index 2656723..d263300 100644
--- a/handler/src/main/java/io/netty/handler/ssl/SupportedCipherSuiteFilter.java
+++ b/handler/src/main/java/io/netty/handler/ssl/SupportedCipherSuiteFilter.java
@@ -15,6 +15,8 @@
  */
 package io.netty.handler.ssl;
 
+import io.netty.util.internal.ObjectUtil;
+
 import javax.net.ssl.SSLEngine;
 import java.util.ArrayList;
 import java.util.List;
@@ -31,12 +33,8 @@ public final class SupportedCipherSuiteFilter implements CipherSuiteFilter {
     @Override
     public String[] filterCipherSuites(Iterable<String> ciphers, List<String> defaultCiphers,
             Set<String> supportedCiphers) {
-        if (defaultCiphers == null) {
-            throw new NullPointerException("defaultCiphers");
-        }
-        if (supportedCiphers == null) {
-            throw new NullPointerException("supportedCiphers");
-        }
+        ObjectUtil.checkNotNull(defaultCiphers, "defaultCiphers");
+        ObjectUtil.checkNotNull(supportedCiphers, "supportedCiphers");
 
         final List<String> newCiphers;
         if (ciphers == null) {
diff --git a/handler/src/main/java/io/netty/handler/ssl/TrustManagerFactoryWrapper.java b/handler/src/main/java/io/netty/handler/ssl/TrustManagerFactoryWrapper.java
new file mode 100644
index 0000000..5abf8aa
--- /dev/null
+++ b/handler/src/main/java/io/netty/handler/ssl/TrustManagerFactoryWrapper.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2019 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.ssl;
+
+import io.netty.handler.ssl.util.SimpleTrustManagerFactory;
+import io.netty.util.internal.ObjectUtil;
+
+import java.security.KeyStore;
+import javax.net.ssl.ManagerFactoryParameters;
+import javax.net.ssl.TrustManager;
+
+final class TrustManagerFactoryWrapper extends SimpleTrustManagerFactory {
+    private final TrustManager tm;
+
+    TrustManagerFactoryWrapper(TrustManager tm) {
+        this.tm = ObjectUtil.checkNotNull(tm, "tm");
+    }
+
+    @Override
+    protected void engineInit(KeyStore keyStore) throws Exception { }
+
+    @Override
+    protected void engineInit(ManagerFactoryParameters managerFactoryParameters)
+            throws Exception { }
+
+    @Override
+    protected TrustManager[] engineGetTrustManagers() {
+        return new TrustManager[] {tm};
+    }
+}
diff --git a/handler/src/main/java/io/netty/handler/ssl/ocsp/OcspClientHandler.java b/handler/src/main/java/io/netty/handler/ssl/ocsp/OcspClientHandler.java
index aff0949..c45c50e 100644
--- a/handler/src/main/java/io/netty/handler/ssl/ocsp/OcspClientHandler.java
+++ b/handler/src/main/java/io/netty/handler/ssl/ocsp/OcspClientHandler.java
@@ -21,7 +21,6 @@ import io.netty.handler.ssl.ReferenceCountedOpenSslContext;
 import io.netty.handler.ssl.ReferenceCountedOpenSslEngine;
 import io.netty.handler.ssl.SslHandshakeCompletionEvent;
 import io.netty.util.internal.ObjectUtil;
-import io.netty.util.internal.ThrowableUtil;
 import io.netty.util.internal.UnstableApi;
 
 import javax.net.ssl.SSLHandshakeException;
@@ -35,9 +34,6 @@ import javax.net.ssl.SSLHandshakeException;
 @UnstableApi
 public abstract class OcspClientHandler extends ChannelInboundHandlerAdapter {
 
-    private static final SSLHandshakeException OCSP_VERIFICATION_EXCEPTION = ThrowableUtil.unknownStackTrace(
-            new SSLHandshakeException("Bad OCSP response"), OcspClientHandler.class, "verify(...)");
-
     private final ReferenceCountedOpenSslEngine engine;
 
     protected OcspClientHandler(ReferenceCountedOpenSslEngine engine) {
@@ -56,7 +52,7 @@ public abstract class OcspClientHandler extends ChannelInboundHandlerAdapter {
 
             SslHandshakeCompletionEvent event = (SslHandshakeCompletionEvent) evt;
             if (event.isSuccess() && !verify(ctx, engine)) {
-                throw OCSP_VERIFICATION_EXCEPTION;
+                throw new SSLHandshakeException("Bad OCSP response");
             }
         }
 
diff --git a/handler/src/main/java/io/netty/handler/ssl/util/FingerprintTrustManagerFactory.java b/handler/src/main/java/io/netty/handler/ssl/util/FingerprintTrustManagerFactory.java
index 4543345..8a023d4 100644
--- a/handler/src/main/java/io/netty/handler/ssl/util/FingerprintTrustManagerFactory.java
+++ b/handler/src/main/java/io/netty/handler/ssl/util/FingerprintTrustManagerFactory.java
@@ -18,8 +18,9 @@ package io.netty.handler.ssl.util;
 
 import io.netty.buffer.ByteBufUtil;
 import io.netty.buffer.Unpooled;
-import io.netty.util.internal.EmptyArrays;
 import io.netty.util.concurrent.FastThreadLocal;
+import io.netty.util.internal.EmptyArrays;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.StringUtil;
 
 import javax.net.ssl.ManagerFactoryParameters;
@@ -156,9 +157,7 @@ public final class FingerprintTrustManagerFactory extends SimpleTrustManagerFact
      * @param fingerprints a list of SHA1 fingerprints
      */
     public FingerprintTrustManagerFactory(byte[]... fingerprints) {
-        if (fingerprints == null) {
-            throw new NullPointerException("fingerprints");
-        }
+        ObjectUtil.checkNotNull(fingerprints, "fingerprints");
 
         List<byte[]> list = new ArrayList<byte[]>(fingerprints.length);
         for (byte[] f: fingerprints) {
@@ -176,9 +175,7 @@ public final class FingerprintTrustManagerFactory extends SimpleTrustManagerFact
     }
 
     private static byte[][] toFingerprintArray(Iterable<String> fingerprints) {
-        if (fingerprints == null) {
-            throw new NullPointerException("fingerprints");
-        }
+        ObjectUtil.checkNotNull(fingerprints, "fingerprints");
 
         List<byte[]> list = new ArrayList<byte[]>();
         for (String f: fingerprints) {
diff --git a/handler/src/main/java/io/netty/handler/ssl/util/OpenJdkSelfSignedCertGenerator.java b/handler/src/main/java/io/netty/handler/ssl/util/OpenJdkSelfSignedCertGenerator.java
index 30d74e2..dbab743 100644
--- a/handler/src/main/java/io/netty/handler/ssl/util/OpenJdkSelfSignedCertGenerator.java
+++ b/handler/src/main/java/io/netty/handler/ssl/util/OpenJdkSelfSignedCertGenerator.java
@@ -16,6 +16,7 @@
 
 package io.netty.handler.ssl.util;
 
+import io.netty.util.internal.SuppressJava6Requirement;
 import sun.security.x509.AlgorithmId;
 import sun.security.x509.CertificateAlgorithmId;
 import sun.security.x509.CertificateIssuerName;
@@ -42,6 +43,7 @@ import static io.netty.handler.ssl.util.SelfSignedCertificate.*;
  */
 final class OpenJdkSelfSignedCertGenerator {
 
+    @SuppressJava6Requirement(reason = "Usage guarded by dependency check")
     static String[] generate(String fqdn, KeyPair keypair, SecureRandom random, Date notBefore, Date notAfter)
             throws Exception {
         PrivateKey key = keypair.getPrivate();
diff --git a/handler/src/main/java/io/netty/handler/ssl/util/SelfSignedCertificate.java b/handler/src/main/java/io/netty/handler/ssl/util/SelfSignedCertificate.java
index 9f010ce..c0b8467 100644
--- a/handler/src/main/java/io/netty/handler/ssl/util/SelfSignedCertificate.java
+++ b/handler/src/main/java/io/netty/handler/ssl/util/SelfSignedCertificate.java
@@ -21,6 +21,7 @@ import io.netty.buffer.Unpooled;
 import io.netty.handler.codec.base64.Base64;
 import io.netty.util.CharsetUtil;
 import io.netty.util.internal.SystemPropertyUtil;
+import io.netty.util.internal.ThrowableUtil;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 
@@ -48,7 +49,7 @@ import java.util.Date;
  * It is purely for testing purposes, and thus it is very insecure.
  * It even uses an insecure pseudo-random generator for faster generation internally.
  * </p><p>
- * A X.509 certificate file and a RSA private key file are generated in a system's temporary directory using
+ * An X.509 certificate file and a RSA private key file are generated in a system's temporary directory using
  * {@link java.io.File#createTempFile(String, String)}, and they are deleted when the JVM exits using
  * {@link java.io.File#deleteOnExit()}.
  * </p><p>
@@ -67,6 +68,14 @@ public final class SelfSignedCertificate {
     private static final Date DEFAULT_NOT_AFTER = new Date(SystemPropertyUtil.getLong(
             "io.netty.selfSignedCertificate.defaultNotAfter", 253402300799000L));
 
+    /**
+     * FIPS 140-2 encryption requires the key length to be 2048 bits or greater.
+     * Let's use that as a sane default but allow the default to be set dynamically
+     * for those that need more stringent security requirements.
+     */
+    private static final int DEFAULT_KEY_LENGTH_BITS =
+            SystemPropertyUtil.getInt("io.netty.handler.ssl.util.selfSignedKeyStrength", 2048);
+
     private final File certificate;
     private final File privateKey;
     private final X509Certificate cert;
@@ -107,7 +116,7 @@ public final class SelfSignedCertificate {
     public SelfSignedCertificate(String fqdn, Date notBefore, Date notAfter) throws CertificateException {
         // Bypass entropy collection by using insecure random generator.
         // We just want to generate it without any delay because it's for testing purposes only.
-        this(fqdn, ThreadLocalInsecureRandom.current(), 1024, notBefore, notAfter);
+        this(fqdn, ThreadLocalInsecureRandom.current(), DEFAULT_KEY_LENGTH_BITS, notBefore, notAfter);
     }
 
     /**
@@ -154,10 +163,11 @@ public final class SelfSignedCertificate {
                 paths = BouncyCastleSelfSignedCertGenerator.generate(fqdn, keypair, random, notBefore, notAfter);
             } catch (Throwable t2) {
                 logger.debug("Failed to generate a self-signed X.509 certificate using Bouncy Castle:", t2);
-                throw new CertificateException(
+                final CertificateException certificateException = new CertificateException(
                         "No provider succeeded to generate a self-signed certificate. " +
                                 "See debug log for the root cause.", t2);
-                // TODO: consider using Java 7 addSuppressed to append t
+                ThrowableUtil.addSuppressed(certificateException, t);
+                throw certificateException;
             }
         }
 
diff --git a/handler/src/main/java/io/netty/handler/ssl/util/SimpleKeyManagerFactory.java b/handler/src/main/java/io/netty/handler/ssl/util/SimpleKeyManagerFactory.java
new file mode 100644
index 0000000..31b28f8
--- /dev/null
+++ b/handler/src/main/java/io/netty/handler/ssl/util/SimpleKeyManagerFactory.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2019 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.ssl.util;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import io.netty.util.internal.ObjectUtil;
+import io.netty.util.internal.PlatformDependent;
+import io.netty.util.internal.StringUtil;
+import io.netty.util.internal.SuppressJava6Requirement;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.Provider;
+import javax.net.ssl.ManagerFactoryParameters;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.KeyManagerFactorySpi;
+import javax.net.ssl.X509ExtendedKeyManager;
+import javax.net.ssl.X509KeyManager;
+
+/**
+ * Helps to implement a custom {@link KeyManagerFactory}.
+ */
+public abstract class SimpleKeyManagerFactory extends KeyManagerFactory {
+
+    private static final Provider PROVIDER = new Provider("", 0.0, "") {
+        private static final long serialVersionUID = -2680540247105807895L;
+    };
+
+    /**
+     * {@link SimpleKeyManagerFactorySpi} must have a reference to {@link SimpleKeyManagerFactory}
+     * to delegate its callbacks back to {@link SimpleKeyManagerFactory}.  However, it is impossible to do so,
+     * because {@link KeyManagerFactory} requires {@link KeyManagerFactorySpi} at construction time and
+     * does not provide a way to access it later.
+     *
+     * To work around this issue, we use an ugly hack which uses a {@link FastThreadLocal }.
+     */
+    private static final FastThreadLocal<SimpleKeyManagerFactorySpi> CURRENT_SPI =
+            new FastThreadLocal<SimpleKeyManagerFactorySpi>() {
+                @Override
+                protected SimpleKeyManagerFactorySpi initialValue() {
+                    return new SimpleKeyManagerFactorySpi();
+                }
+            };
+
+    /**
+     * Creates a new instance.
+     */
+    protected SimpleKeyManagerFactory() {
+        this(StringUtil.EMPTY_STRING);
+    }
+
+    /**
+     * Creates a new instance.
+     *
+     * @param name the name of this {@link KeyManagerFactory}
+     */
+    protected SimpleKeyManagerFactory(String name) {
+        super(CURRENT_SPI.get(), PROVIDER, ObjectUtil.checkNotNull(name, "name"));
+        CURRENT_SPI.get().init(this);
+        CURRENT_SPI.remove();
+    }
+
+    /**
+     * Initializes this factory with a source of certificate authorities and related key material.
+     *
+     * @see KeyManagerFactorySpi#engineInit(KeyStore, char[])
+     */
+    protected abstract void engineInit(KeyStore keyStore, char[] var2) throws Exception;
+
+    /**
+     * Initializes this factory with a source of provider-specific key material.
+     *
+     * @see KeyManagerFactorySpi#engineInit(ManagerFactoryParameters)
+     */
+    protected abstract void engineInit(ManagerFactoryParameters managerFactoryParameters) throws Exception;
+
+    /**
+     * Returns one key manager for each type of key material.
+     *
+     * @see KeyManagerFactorySpi#engineGetKeyManagers()
+     */
+    protected abstract KeyManager[] engineGetKeyManagers();
+
+    private static final class SimpleKeyManagerFactorySpi extends KeyManagerFactorySpi {
+
+        private SimpleKeyManagerFactory parent;
+        private volatile KeyManager[] keyManagers;
+
+        void init(SimpleKeyManagerFactory parent) {
+            this.parent = parent;
+        }
+
+        @Override
+        protected void engineInit(KeyStore keyStore, char[] pwd) throws KeyStoreException {
+            try {
+                parent.engineInit(keyStore, pwd);
+            } catch (KeyStoreException e) {
+                throw e;
+            } catch (Exception e) {
+                throw new KeyStoreException(e);
+            }
+        }
+
+        @Override
+        protected void engineInit(
+                ManagerFactoryParameters managerFactoryParameters) throws InvalidAlgorithmParameterException {
+            try {
+                parent.engineInit(managerFactoryParameters);
+            } catch (InvalidAlgorithmParameterException e) {
+                throw e;
+            } catch (Exception e) {
+                throw new InvalidAlgorithmParameterException(e);
+            }
+        }
+
+        @Override
+        protected KeyManager[] engineGetKeyManagers() {
+            KeyManager[] keyManagers = this.keyManagers;
+            if (keyManagers == null) {
+                keyManagers = parent.engineGetKeyManagers();
+                if (PlatformDependent.javaVersion() >= 7) {
+                    wrapIfNeeded(keyManagers);
+                }
+                this.keyManagers = keyManagers;
+            }
+            return keyManagers.clone();
+        }
+
+        @SuppressJava6Requirement(reason = "Usage guarded by java version check")
+        private static void wrapIfNeeded(KeyManager[] keyManagers) {
+            for (int i = 0; i < keyManagers.length; i++) {
+                final KeyManager tm = keyManagers[i];
+                if (tm instanceof X509KeyManager && !(tm instanceof X509ExtendedKeyManager)) {
+                    keyManagers[i] = new X509KeyManagerWrapper((X509KeyManager) tm);
+                }
+            }
+        }
+    }
+}
diff --git a/handler/src/main/java/io/netty/handler/ssl/util/SimpleTrustManagerFactory.java b/handler/src/main/java/io/netty/handler/ssl/util/SimpleTrustManagerFactory.java
index a11cede..d2fa903 100644
--- a/handler/src/main/java/io/netty/handler/ssl/util/SimpleTrustManagerFactory.java
+++ b/handler/src/main/java/io/netty/handler/ssl/util/SimpleTrustManagerFactory.java
@@ -17,7 +17,9 @@
 package io.netty.handler.ssl.util;
 
 import io.netty.util.concurrent.FastThreadLocal;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.PlatformDependent;
+import io.netty.util.internal.SuppressJava6Requirement;
 
 import javax.net.ssl.ManagerFactoryParameters;
 import javax.net.ssl.TrustManager;
@@ -72,9 +74,7 @@ public abstract class SimpleTrustManagerFactory extends TrustManagerFactory {
         CURRENT_SPI.get().init(this);
         CURRENT_SPI.remove();
 
-        if (name == null) {
-            throw new NullPointerException("name");
-        }
+        ObjectUtil.checkNotNull(name, "name");
     }
 
     /**
@@ -136,16 +136,21 @@ public abstract class SimpleTrustManagerFactory extends TrustManagerFactory {
             if (trustManagers == null) {
                 trustManagers = parent.engineGetTrustManagers();
                 if (PlatformDependent.javaVersion() >= 7) {
-                    for (int i = 0; i < trustManagers.length; i++) {
-                        final TrustManager tm = trustManagers[i];
-                        if (tm instanceof X509TrustManager && !(tm instanceof X509ExtendedTrustManager)) {
-                            trustManagers[i] = new X509TrustManagerWrapper((X509TrustManager) tm);
-                        }
-                    }
+                    wrapIfNeeded(trustManagers);
                 }
                 this.trustManagers = trustManagers;
             }
             return trustManagers.clone();
         }
+
+        @SuppressJava6Requirement(reason = "Usage guarded by java version check")
+        private static void wrapIfNeeded(TrustManager[] trustManagers) {
+            for (int i = 0; i < trustManagers.length; i++) {
+                final TrustManager tm = trustManagers[i];
+                if (tm instanceof X509TrustManager && !(tm instanceof X509ExtendedTrustManager)) {
+                    trustManagers[i] = new X509TrustManagerWrapper((X509TrustManager) tm);
+                }
+            }
+        }
     }
 }
diff --git a/handler/src/main/java/io/netty/handler/ssl/util/X509KeyManagerWrapper.java b/handler/src/main/java/io/netty/handler/ssl/util/X509KeyManagerWrapper.java
new file mode 100644
index 0000000..20c1f26
--- /dev/null
+++ b/handler/src/main/java/io/netty/handler/ssl/util/X509KeyManagerWrapper.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2019 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.ssl.util;
+
+import static io.netty.util.internal.ObjectUtil.checkNotNull;
+
+import io.netty.util.internal.SuppressJava6Requirement;
+import java.net.Socket;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.X509ExtendedKeyManager;
+import javax.net.ssl.X509KeyManager;
+
+@SuppressJava6Requirement(reason = "Usage guarded by java version check")
+final class X509KeyManagerWrapper extends X509ExtendedKeyManager {
+
+    private final X509KeyManager delegate;
+
+    X509KeyManagerWrapper(X509KeyManager delegate) {
+        this.delegate = checkNotNull(delegate, "delegate");
+    }
+
+    @Override
+    public String[] getClientAliases(String var1, Principal[] var2) {
+        return delegate.getClientAliases(var1, var2);
+    }
+
+    @Override
+    public String chooseClientAlias(String[] var1, Principal[] var2, Socket var3) {
+        return delegate.chooseClientAlias(var1, var2, var3);
+    }
+
+    @Override
+    public String[] getServerAliases(String var1, Principal[] var2) {
+        return delegate.getServerAliases(var1, var2);
+    }
+
+    @Override
+    public String chooseServerAlias(String var1, Principal[] var2, Socket var3) {
+        return delegate.chooseServerAlias(var1, var2, var3);
+    }
+
+    @Override
+    public X509Certificate[] getCertificateChain(String var1) {
+        return delegate.getCertificateChain(var1);
+    }
+
+    @Override
+    public PrivateKey getPrivateKey(String var1) {
+        return delegate.getPrivateKey(var1);
+    }
+
+    @Override
+    public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine) {
+        return delegate.chooseClientAlias(keyType, issuers, null);
+    }
+
+    @Override
+    public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) {
+        return delegate.chooseServerAlias(keyType, issuers, null);
+    }
+}
diff --git a/handler/src/main/java/io/netty/handler/ssl/util/X509TrustManagerWrapper.java b/handler/src/main/java/io/netty/handler/ssl/util/X509TrustManagerWrapper.java
index 1b70b97..4955ef9 100644
--- a/handler/src/main/java/io/netty/handler/ssl/util/X509TrustManagerWrapper.java
+++ b/handler/src/main/java/io/netty/handler/ssl/util/X509TrustManagerWrapper.java
@@ -15,6 +15,8 @@
  */
 package io.netty.handler.ssl.util;
 
+import io.netty.util.internal.SuppressJava6Requirement;
+
 import javax.net.ssl.SSLEngine;
 import javax.net.ssl.X509ExtendedTrustManager;
 import javax.net.ssl.X509TrustManager;
@@ -24,6 +26,7 @@ import java.security.cert.X509Certificate;
 
 import static io.netty.util.internal.ObjectUtil.*;
 
+@SuppressJava6Requirement(reason = "Usage guarded by java version check")
 final class X509TrustManagerWrapper extends X509ExtendedTrustManager {
 
     private final X509TrustManager delegate;
diff --git a/handler/src/main/java/io/netty/handler/stream/ChunkedFile.java b/handler/src/main/java/io/netty/handler/stream/ChunkedFile.java
index 3e12e4a..17d1c42 100644
--- a/handler/src/main/java/io/netty/handler/stream/ChunkedFile.java
+++ b/handler/src/main/java/io/netty/handler/stream/ChunkedFile.java
@@ -19,6 +19,7 @@ import io.netty.buffer.ByteBuf;
 import io.netty.buffer.ByteBufAllocator;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.FileRegion;
+import io.netty.util.internal.ObjectUtil;
 
 import java.io.File;
 import java.io.IOException;
@@ -82,26 +83,14 @@ public class ChunkedFile implements ChunkedInput<ByteBuf> {
      *                  {@link #readChunk(ChannelHandlerContext)} call
      */
     public ChunkedFile(RandomAccessFile file, long offset, long length, int chunkSize) throws IOException {
-        if (file == null) {
-            throw new NullPointerException("file");
-        }
-        if (offset < 0) {
-            throw new IllegalArgumentException(
-                    "offset: " + offset + " (expected: 0 or greater)");
-        }
-        if (length < 0) {
-            throw new IllegalArgumentException(
-                    "length: " + length + " (expected: 0 or greater)");
-        }
-        if (chunkSize <= 0) {
-            throw new IllegalArgumentException(
-                    "chunkSize: " + chunkSize +
-                    " (expected: a positive integer)");
-        }
+        ObjectUtil.checkNotNull(file, "file");
+        ObjectUtil.checkPositiveOrZero(offset, "offset");
+        ObjectUtil.checkPositiveOrZero(length, "length");
+        ObjectUtil.checkPositive(chunkSize, "chunkSize");
 
         this.file = file;
         this.offset = startOffset = offset;
-        endOffset = offset + length;
+        this.endOffset = offset + length;
         this.chunkSize = chunkSize;
 
         file.seek(offset);
diff --git a/handler/src/main/java/io/netty/handler/stream/ChunkedNioFile.java b/handler/src/main/java/io/netty/handler/stream/ChunkedNioFile.java
index 339a3e5..f46db09 100644
--- a/handler/src/main/java/io/netty/handler/stream/ChunkedNioFile.java
+++ b/handler/src/main/java/io/netty/handler/stream/ChunkedNioFile.java
@@ -19,10 +19,12 @@ import io.netty.buffer.ByteBuf;
 import io.netty.buffer.ByteBufAllocator;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.FileRegion;
+import io.netty.util.internal.ObjectUtil;
 
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.channels.ClosedChannelException;
 import java.nio.channels.FileChannel;
 
 /**
@@ -45,7 +47,7 @@ public class ChunkedNioFile implements ChunkedInput<ByteBuf> {
      * Creates a new instance that fetches data from the specified file.
      */
     public ChunkedNioFile(File in) throws IOException {
-        this(new FileInputStream(in).getChannel());
+        this(new RandomAccessFile(in, "r").getChannel());
     }
 
     /**
@@ -55,7 +57,7 @@ public class ChunkedNioFile implements ChunkedInput<ByteBuf> {
      *                  {@link #readChunk(ChannelHandlerContext)} call
      */
     public ChunkedNioFile(File in, int chunkSize) throws IOException {
-        this(new FileInputStream(in).getChannel(), chunkSize);
+        this(new RandomAccessFile(in, "r").getChannel(), chunkSize);
     }
 
     /**
@@ -85,25 +87,12 @@ public class ChunkedNioFile implements ChunkedInput<ByteBuf> {
      */
     public ChunkedNioFile(FileChannel in, long offset, long length, int chunkSize)
             throws IOException {
-        if (in == null) {
-            throw new NullPointerException("in");
-        }
-        if (offset < 0) {
-            throw new IllegalArgumentException(
-                    "offset: " + offset + " (expected: 0 or greater)");
-        }
-        if (length < 0) {
-            throw new IllegalArgumentException(
-                    "length: " + length + " (expected: 0 or greater)");
-        }
-        if (chunkSize <= 0) {
-            throw new IllegalArgumentException(
-                    "chunkSize: " + chunkSize +
-                    " (expected: a positive integer)");
-        }
-
-        if (offset != 0) {
-            in.position(offset);
+        ObjectUtil.checkNotNull(in, "in");
+        ObjectUtil.checkPositiveOrZero(offset, "offset");
+        ObjectUtil.checkPositiveOrZero(length, "length");
+        ObjectUtil.checkPositive(chunkSize, "chunkSize");
+        if (!in.isOpen()) {
+            throw new ClosedChannelException();
         }
         this.in = in;
         this.chunkSize = chunkSize;
@@ -161,7 +150,7 @@ public class ChunkedNioFile implements ChunkedInput<ByteBuf> {
         try {
             int readBytes = 0;
             for (;;) {
-                int localReadBytes = buffer.writeBytes(in, chunkSize - readBytes);
+                int localReadBytes = buffer.writeBytes(in, offset + readBytes, chunkSize - readBytes);
                 if (localReadBytes < 0) {
                     break;
                 }
diff --git a/handler/src/main/java/io/netty/handler/stream/ChunkedNioStream.java b/handler/src/main/java/io/netty/handler/stream/ChunkedNioStream.java
index 22feb63..ebdddce 100644
--- a/handler/src/main/java/io/netty/handler/stream/ChunkedNioStream.java
+++ b/handler/src/main/java/io/netty/handler/stream/ChunkedNioStream.java
@@ -18,6 +18,7 @@ package io.netty.handler.stream;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.ByteBufAllocator;
 import io.netty.channel.ChannelHandlerContext;
+import io.netty.util.internal.ObjectUtil;
 
 import java.nio.ByteBuffer;
 import java.nio.channels.ReadableByteChannel;
@@ -53,9 +54,7 @@ public class ChunkedNioStream implements ChunkedInput<ByteBuf> {
      *                  {@link #readChunk(ChannelHandlerContext)} call
      */
     public ChunkedNioStream(ReadableByteChannel in, int chunkSize) {
-        if (in == null) {
-            throw new NullPointerException("in");
-        }
+        ObjectUtil.checkNotNull(in, "in");
         if (chunkSize <= 0) {
             throw new IllegalArgumentException("chunkSize: " + chunkSize +
                     " (expected: a positive integer)");
diff --git a/handler/src/main/java/io/netty/handler/stream/ChunkedStream.java b/handler/src/main/java/io/netty/handler/stream/ChunkedStream.java
index f476c03..ee2d038 100644
--- a/handler/src/main/java/io/netty/handler/stream/ChunkedStream.java
+++ b/handler/src/main/java/io/netty/handler/stream/ChunkedStream.java
@@ -18,6 +18,7 @@ package io.netty.handler.stream;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.ByteBufAllocator;
 import io.netty.channel.ChannelHandlerContext;
+import io.netty.util.internal.ObjectUtil;
 
 import java.io.InputStream;
 import java.io.PushbackInputStream;
@@ -55,14 +56,8 @@ public class ChunkedStream implements ChunkedInput<ByteBuf> {
      *                  {@link #readChunk(ChannelHandlerContext)} call
      */
     public ChunkedStream(InputStream in, int chunkSize) {
-        if (in == null) {
-            throw new NullPointerException("in");
-        }
-        if (chunkSize <= 0) {
-            throw new IllegalArgumentException(
-                    "chunkSize: " + chunkSize +
-                    " (expected: a positive integer)");
-        }
+        ObjectUtil.checkNotNull(in, "in");
+        ObjectUtil.checkPositive(chunkSize, "chunkSize");
 
         if (in instanceof PushbackInputStream) {
             this.in = (PushbackInputStream) in;
diff --git a/handler/src/main/java/io/netty/handler/stream/ChunkedWriteHandler.java b/handler/src/main/java/io/netty/handler/stream/ChunkedWriteHandler.java
index f39328d..a222595 100644
--- a/handler/src/main/java/io/netty/handler/stream/ChunkedWriteHandler.java
+++ b/handler/src/main/java/io/netty/handler/stream/ChunkedWriteHandler.java
@@ -72,7 +72,6 @@ public class ChunkedWriteHandler extends ChannelDuplexHandler {
 
     private final Queue<PendingWrite> queue = new ArrayDeque<PendingWrite>();
     private volatile ChannelHandlerContext ctx;
-    private PendingWrite currentWrite;
 
     public ChunkedWriteHandler() {
     }
@@ -119,9 +118,7 @@ public class ChunkedWriteHandler extends ChannelDuplexHandler {
         try {
             doFlush(ctx);
         } catch (Exception e) {
-            if (logger.isWarnEnabled()) {
-                logger.warn("Unexpected exception while sending chunks.", e);
-            }
+            logger.warn("Unexpected exception while sending chunks.", e);
         }
     }
 
@@ -152,13 +149,7 @@ public class ChunkedWriteHandler extends ChannelDuplexHandler {
 
     private void discard(Throwable cause) {
         for (;;) {
-            PendingWrite currentWrite = this.currentWrite;
-
-            if (this.currentWrite == null) {
-                currentWrite = queue.poll();
-            } else {
-                this.currentWrite = null;
-            }
+            PendingWrite currentWrite = queue.poll();
 
             if (currentWrite == null) {
                 break;
@@ -166,22 +157,28 @@ public class ChunkedWriteHandler extends ChannelDuplexHandler {
             Object message = currentWrite.msg;
             if (message instanceof ChunkedInput) {
                 ChunkedInput<?> in = (ChunkedInput<?>) message;
+                boolean endOfInput;
+                long inputLength;
                 try {
-                    if (!in.isEndOfInput()) {
-                        if (cause == null) {
-                            cause = new ClosedChannelException();
-                        }
-                        currentWrite.fail(cause);
-                    } else {
-                        currentWrite.success(in.length());
-                    }
+                    endOfInput = in.isEndOfInput();
+                    inputLength = in.length();
                     closeInput(in);
                 } catch (Exception e) {
+                    closeInput(in);
                     currentWrite.fail(e);
                     if (logger.isWarnEnabled()) {
-                        logger.warn(ChunkedInput.class.getSimpleName() + ".isEndOfInput() failed", e);
+                        logger.warn(ChunkedInput.class.getSimpleName() + " failed", e);
                     }
-                    closeInput(in);
+                    continue;
+                }
+
+                if (!endOfInput) {
+                    if (cause == null) {
+                        cause = new ClosedChannelException();
+                    }
+                    currentWrite.fail(cause);
+                } else {
+                    currentWrite.success(inputLength);
                 }
             } else {
                 if (cause == null) {
@@ -202,9 +199,7 @@ public class ChunkedWriteHandler extends ChannelDuplexHandler {
         boolean requiresFlush = true;
         ByteBufAllocator allocator = ctx.alloc();
         while (channel.isWritable()) {
-            if (currentWrite == null) {
-                currentWrite = queue.poll();
-            }
+            final PendingWrite currentWrite = queue.peek();
 
             if (currentWrite == null) {
                 break;
@@ -220,11 +215,10 @@ public class ChunkedWriteHandler extends ChannelDuplexHandler {
                 // as this had to be done already by someone who resolved the
                 // promise (using ChunkedInput.close method).
                 // See https://github.com/netty/netty/issues/8700.
-                this.currentWrite = null;
+                queue.remove();
                 continue;
             }
 
-            final PendingWrite currentWrite = this.currentWrite;
             final Object pendingMessage = currentWrite.msg;
 
             if (pendingMessage instanceof ChunkedInput) {
@@ -243,14 +237,14 @@ public class ChunkedWriteHandler extends ChannelDuplexHandler {
                         suspend = false;
                     }
                 } catch (final Throwable t) {
-                    this.currentWrite = null;
+                    queue.remove();
 
                     if (message != null) {
                         ReferenceCountUtil.release(message);
                     }
 
-                    currentWrite.fail(t);
                     closeInput(chunks);
+                    currentWrite.fail(t);
                     break;
                 }
 
@@ -267,60 +261,42 @@ public class ChunkedWriteHandler extends ChannelDuplexHandler {
                     message = Unpooled.EMPTY_BUFFER;
                 }
 
-                ChannelFuture f = ctx.write(message);
+                // Flush each chunk to conserve memory
+                ChannelFuture f = ctx.writeAndFlush(message);
                 if (endOfInput) {
-                    this.currentWrite = null;
-
-                    // Register a listener which will close the input once the write is complete.
-                    // This is needed because the Chunk may have some resource bound that can not
-                    // be closed before its not written.
-                    //
-                    // See https://github.com/netty/netty/issues/303
-                    f.addListener(new ChannelFutureListener() {
-                        @Override
-                        public void operationComplete(ChannelFuture future) throws Exception {
-                            if (!future.isSuccess()) {
-                                closeInput(chunks);
-                                currentWrite.fail(future.cause());
-                            } else {
-                                currentWrite.progress(chunks.progress(), chunks.length());
-                                currentWrite.success(chunks.length());
-                            }
-                        }
-                    });
-                } else if (channel.isWritable()) {
-                    f.addListener(new ChannelFutureListener() {
-                        @Override
-                        public void operationComplete(ChannelFuture future) throws Exception {
-                            if (!future.isSuccess()) {
-                                closeInput((ChunkedInput<?>) pendingMessage);
-                                currentWrite.fail(future.cause());
-                            } else {
-                                currentWrite.progress(chunks.progress(), chunks.length());
+                    queue.remove();
+
+                    if (f.isDone()) {
+                        handleEndOfInputFuture(f, currentWrite);
+                    } else {
+                        // Register a listener which will close the input once the write is complete.
+                        // This is needed because the Chunk may have some resource bound that can not
+                        // be closed before its not written.
+                        //
+                        // See https://github.com/netty/netty/issues/303
+                        f.addListener(new ChannelFutureListener() {
+                            @Override
+                            public void operationComplete(ChannelFuture future) {
+                                handleEndOfInputFuture(future, currentWrite);
                             }
-                        }
-                    });
+                        });
+                    }
                 } else {
-                    f.addListener(new ChannelFutureListener() {
-                        @Override
-                        public void operationComplete(ChannelFuture future) throws Exception {
-                            if (!future.isSuccess()) {
-                                closeInput((ChunkedInput<?>) pendingMessage);
-                                currentWrite.fail(future.cause());
-                            } else {
-                                currentWrite.progress(chunks.progress(), chunks.length());
-                                if (channel.isWritable()) {
-                                    resumeTransfer();
-                                }
+                    final boolean resume = !channel.isWritable();
+                    if (f.isDone()) {
+                        handleFuture(f, currentWrite, resume);
+                    } else {
+                        f.addListener(new ChannelFutureListener() {
+                            @Override
+                            public void operationComplete(ChannelFuture future) {
+                                handleFuture(future, currentWrite, resume);
                             }
-                        }
-                    });
+                        });
+                    }
                 }
-                // Flush each chunk to conserve memory
-                ctx.flush();
                 requiresFlush = false;
             } else {
-                this.currentWrite = null;
+                queue.remove();
                 ctx.write(pendingMessage, currentWrite.promise);
                 requiresFlush = true;
             }
@@ -336,6 +312,34 @@ public class ChunkedWriteHandler extends ChannelDuplexHandler {
         }
     }
 
+    private static void handleEndOfInputFuture(ChannelFuture future, PendingWrite currentWrite) {
+        ChunkedInput<?> input = (ChunkedInput<?>) currentWrite.msg;
+        if (!future.isSuccess()) {
+            closeInput(input);
+            currentWrite.fail(future.cause());
+        } else {
+            // read state of the input in local variables before closing it
+            long inputProgress = input.progress();
+            long inputLength = input.length();
+            closeInput(input);
+            currentWrite.progress(inputProgress, inputLength);
+            currentWrite.success(inputLength);
+        }
+    }
+
+    private void handleFuture(ChannelFuture future, PendingWrite currentWrite, boolean resume) {
+        ChunkedInput<?> input = (ChunkedInput<?>) currentWrite.msg;
+        if (!future.isSuccess()) {
+            closeInput(input);
+            currentWrite.fail(future.cause());
+        } else {
+            currentWrite.progress(input.progress(), input.length());
+            if (resume && future.channel().isWritable()) {
+                resumeTransfer();
+            }
+        }
+    }
+
     private static void closeInput(ChunkedInput<?> chunks) {
         try {
             chunks.close();
diff --git a/handler/src/main/java/io/netty/handler/timeout/IdleStateEvent.java b/handler/src/main/java/io/netty/handler/timeout/IdleStateEvent.java
index 7ec9e63..0251d06 100644
--- a/handler/src/main/java/io/netty/handler/timeout/IdleStateEvent.java
+++ b/handler/src/main/java/io/netty/handler/timeout/IdleStateEvent.java
@@ -17,17 +17,24 @@ package io.netty.handler.timeout;
 
 import io.netty.channel.Channel;
 import io.netty.util.internal.ObjectUtil;
+import io.netty.util.internal.StringUtil;
 
 /**
  * A user event triggered by {@link IdleStateHandler} when a {@link Channel} is idle.
  */
 public class IdleStateEvent {
-    public static final IdleStateEvent FIRST_READER_IDLE_STATE_EVENT = new IdleStateEvent(IdleState.READER_IDLE, true);
-    public static final IdleStateEvent READER_IDLE_STATE_EVENT = new IdleStateEvent(IdleState.READER_IDLE, false);
-    public static final IdleStateEvent FIRST_WRITER_IDLE_STATE_EVENT = new IdleStateEvent(IdleState.WRITER_IDLE, true);
-    public static final IdleStateEvent WRITER_IDLE_STATE_EVENT = new IdleStateEvent(IdleState.WRITER_IDLE, false);
-    public static final IdleStateEvent FIRST_ALL_IDLE_STATE_EVENT = new IdleStateEvent(IdleState.ALL_IDLE, true);
-    public static final IdleStateEvent ALL_IDLE_STATE_EVENT = new IdleStateEvent(IdleState.ALL_IDLE, false);
+    public static final IdleStateEvent FIRST_READER_IDLE_STATE_EVENT =
+            new DefaultIdleStateEvent(IdleState.READER_IDLE, true);
+    public static final IdleStateEvent READER_IDLE_STATE_EVENT =
+            new DefaultIdleStateEvent(IdleState.READER_IDLE, false);
+    public static final IdleStateEvent FIRST_WRITER_IDLE_STATE_EVENT =
+            new DefaultIdleStateEvent(IdleState.WRITER_IDLE, true);
+    public static final IdleStateEvent WRITER_IDLE_STATE_EVENT =
+            new DefaultIdleStateEvent(IdleState.WRITER_IDLE, false);
+    public static final IdleStateEvent FIRST_ALL_IDLE_STATE_EVENT =
+            new DefaultIdleStateEvent(IdleState.ALL_IDLE, true);
+    public static final IdleStateEvent ALL_IDLE_STATE_EVENT =
+            new DefaultIdleStateEvent(IdleState.ALL_IDLE, false);
 
     private final IdleState state;
     private final boolean first;
@@ -56,4 +63,23 @@ public class IdleStateEvent {
     public boolean isFirst() {
         return first;
     }
+
+    @Override
+    public String toString() {
+        return StringUtil.simpleClassName(this) + '(' + state + (first ? ", first" : "") + ')';
+    }
+
+    private static final class DefaultIdleStateEvent extends IdleStateEvent {
+        private final String representation;
+
+        DefaultIdleStateEvent(IdleState state, boolean first) {
+            super(state, first);
+            this.representation = "IdleStateEvent(" + state + (first ? ", first" : "") + ')';
+        }
+
+        @Override
+        public String toString() {
+            return representation;
+        }
+    }
 }
diff --git a/handler/src/main/java/io/netty/handler/timeout/IdleStateHandler.java b/handler/src/main/java/io/netty/handler/timeout/IdleStateHandler.java
index 299e4c7..c36ef42 100644
--- a/handler/src/main/java/io/netty/handler/timeout/IdleStateHandler.java
+++ b/handler/src/main/java/io/netty/handler/timeout/IdleStateHandler.java
@@ -25,6 +25,7 @@ import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelInitializer;
 import io.netty.channel.ChannelOutboundBuffer;
 import io.netty.channel.ChannelPromise;
+import io.netty.util.internal.ObjectUtil;
 
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
@@ -129,6 +130,7 @@ public class IdleStateHandler extends ChannelDuplexHandler {
     private long lastChangeCheckTimeStamp;
     private int lastMessageHashCode;
     private long lastPendingWriteBytes;
+    private long lastFlushProgress;
 
     /**
      * Creates a new instance firing {@link IdleStateEvent}s.
@@ -189,9 +191,7 @@ public class IdleStateHandler extends ChannelDuplexHandler {
     public IdleStateHandler(boolean observeOutput,
             long readerIdleTime, long writerIdleTime, long allIdleTime,
             TimeUnit unit) {
-        if (unit == null) {
-            throw new NullPointerException("unit");
-        }
+        ObjectUtil.checkNotNull(unit, "unit");
 
         this.observeOutput = observeOutput;
 
@@ -399,6 +399,7 @@ public class IdleStateHandler extends ChannelDuplexHandler {
             if (buf != null) {
                 lastMessageHashCode = System.identityHashCode(buf.current());
                 lastPendingWriteBytes = buf.totalPendingWriteBytes();
+                lastFlushProgress = buf.currentProgress();
             }
         }
     }
@@ -443,6 +444,15 @@ public class IdleStateHandler extends ChannelDuplexHandler {
                         return true;
                     }
                 }
+
+                long flushProgress = buf.currentProgress();
+                if (flushProgress != lastFlushProgress) {
+                    lastFlushProgress = flushProgress;
+
+                    if (!first) {
+                        return true;
+                    }
+                }
             }
         }
 
diff --git a/handler/src/main/java/io/netty/handler/timeout/ReadTimeoutException.java b/handler/src/main/java/io/netty/handler/timeout/ReadTimeoutException.java
index b0aaa95..032ed57 100644
--- a/handler/src/main/java/io/netty/handler/timeout/ReadTimeoutException.java
+++ b/handler/src/main/java/io/netty/handler/timeout/ReadTimeoutException.java
@@ -15,6 +15,8 @@
  */
 package io.netty.handler.timeout;
 
+import io.netty.util.internal.PlatformDependent;
+
 /**
  * A {@link TimeoutException} raised by {@link ReadTimeoutHandler} when no data
  * was read within a certain period of time.
@@ -23,7 +25,12 @@ public final class ReadTimeoutException extends TimeoutException {
 
     private static final long serialVersionUID = 169287984113283421L;
 
-    public static final ReadTimeoutException INSTANCE = new ReadTimeoutException();
+    public static final ReadTimeoutException INSTANCE = PlatformDependent.javaVersion() >= 7 ?
+            new ReadTimeoutException(true) : new ReadTimeoutException();
+
+    ReadTimeoutException() { }
 
-    private ReadTimeoutException() { }
+    private ReadTimeoutException(boolean shared) {
+        super(shared);
+    }
 }
diff --git a/handler/src/main/java/io/netty/handler/timeout/TimeoutException.java b/handler/src/main/java/io/netty/handler/timeout/TimeoutException.java
index 072220b..256da86 100644
--- a/handler/src/main/java/io/netty/handler/timeout/TimeoutException.java
+++ b/handler/src/main/java/io/netty/handler/timeout/TimeoutException.java
@@ -25,7 +25,12 @@ public class TimeoutException extends ChannelException {
 
     private static final long serialVersionUID = 4673641882869672533L;
 
-    TimeoutException() { }
+    TimeoutException() {
+    }
+
+    TimeoutException(boolean shared) {
+        super(null, null, shared);
+    }
 
     @Override
     public Throwable fillInStackTrace() {
diff --git a/handler/src/main/java/io/netty/handler/timeout/WriteTimeoutException.java b/handler/src/main/java/io/netty/handler/timeout/WriteTimeoutException.java
index f728608..6e7cc97 100644
--- a/handler/src/main/java/io/netty/handler/timeout/WriteTimeoutException.java
+++ b/handler/src/main/java/io/netty/handler/timeout/WriteTimeoutException.java
@@ -15,15 +15,22 @@
  */
 package io.netty.handler.timeout;
 
+import io.netty.util.internal.PlatformDependent;
+
 /**
- * A {@link TimeoutException} raised by {@link WriteTimeoutHandler} when no data
- * was written within a certain period of time.
+ * A {@link TimeoutException} raised by {@link WriteTimeoutHandler} when a write operation
+ * cannot finish in a certain period of time.
  */
 public final class WriteTimeoutException extends TimeoutException {
 
     private static final long serialVersionUID = -144786655770296065L;
 
-    public static final WriteTimeoutException INSTANCE = new WriteTimeoutException();
+    public static final WriteTimeoutException INSTANCE = PlatformDependent.javaVersion() >= 7 ?
+            new WriteTimeoutException(true) : new WriteTimeoutException();
 
     private WriteTimeoutException() { }
+
+    private WriteTimeoutException(boolean shared) {
+        super(shared);
+    }
 }
diff --git a/handler/src/main/java/io/netty/handler/timeout/WriteTimeoutHandler.java b/handler/src/main/java/io/netty/handler/timeout/WriteTimeoutHandler.java
index 70c5881..71ed283 100644
--- a/handler/src/main/java/io/netty/handler/timeout/WriteTimeoutHandler.java
+++ b/handler/src/main/java/io/netty/handler/timeout/WriteTimeoutHandler.java
@@ -24,6 +24,7 @@ import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelInitializer;
 import io.netty.channel.ChannelOutboundHandlerAdapter;
 import io.netty.channel.ChannelPromise;
+import io.netty.util.internal.ObjectUtil;
 
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
@@ -93,9 +94,7 @@ public class WriteTimeoutHandler extends ChannelOutboundHandlerAdapter {
      *        the {@link TimeUnit} of {@code timeout}
      */
     public WriteTimeoutHandler(long timeout, TimeUnit unit) {
-        if (unit == null) {
-            throw new NullPointerException("unit");
-        }
+        ObjectUtil.checkNotNull(unit, "unit");
 
         if (timeout <= 0) {
             timeoutNanos = 0;
diff --git a/handler/src/main/java/io/netty/handler/traffic/AbstractTrafficShapingHandler.java b/handler/src/main/java/io/netty/handler/traffic/AbstractTrafficShapingHandler.java
index 09e0f38..ae7f89c 100644
--- a/handler/src/main/java/io/netty/handler/traffic/AbstractTrafficShapingHandler.java
+++ b/handler/src/main/java/io/netty/handler/traffic/AbstractTrafficShapingHandler.java
@@ -67,7 +67,7 @@ public abstract class AbstractTrafficShapingHandler extends ChannelDuplexHandler
     static final long DEFAULT_MAX_SIZE = 4 * 1024 * 1024L;
 
     /**
-     * Default minimal time to wait
+     * Default minimal time to wait: 10ms
      */
     static final long MINIMAL_WAIT = 10;
 
diff --git a/handler/src/main/java/io/netty/handler/traffic/GlobalChannelTrafficCounter.java b/handler/src/main/java/io/netty/handler/traffic/GlobalChannelTrafficCounter.java
index 6af7de8..9741584 100644
--- a/handler/src/main/java/io/netty/handler/traffic/GlobalChannelTrafficCounter.java
+++ b/handler/src/main/java/io/netty/handler/traffic/GlobalChannelTrafficCounter.java
@@ -78,8 +78,6 @@ public class GlobalChannelTrafficCounter extends TrafficCounter {
                 perChannel.channelTrafficCounter.resetAccounting(newLastTime);
             }
             trafficShapingHandler1.doAccounting(counter);
-            counter.scheduledFuture = counter.executor.schedule(this, counter.checkInterval.get(),
-                                                                TimeUnit.MILLISECONDS);
         }
     }
 
@@ -97,7 +95,7 @@ public class GlobalChannelTrafficCounter extends TrafficCounter {
             monitorActive = true;
             monitor = new MixedTrafficMonitoringTask((GlobalChannelTrafficShapingHandler) trafficShapingHandler, this);
             scheduledFuture =
-                executor.schedule(monitor, localCheckInterval, TimeUnit.MILLISECONDS);
+                executor.scheduleAtFixedRate(monitor, 0, localCheckInterval, TimeUnit.MILLISECONDS);
         }
     }
 
diff --git a/handler/src/main/java/io/netty/handler/traffic/GlobalTrafficShapingHandler.java b/handler/src/main/java/io/netty/handler/traffic/GlobalTrafficShapingHandler.java
index ad624f8..d6e59cc 100644
--- a/handler/src/main/java/io/netty/handler/traffic/GlobalTrafficShapingHandler.java
+++ b/handler/src/main/java/io/netty/handler/traffic/GlobalTrafficShapingHandler.java
@@ -21,6 +21,7 @@ import io.netty.channel.Channel;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelPromise;
 import io.netty.util.concurrent.EventExecutor;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.PlatformDependent;
 
 import java.util.ArrayDeque;
@@ -103,10 +104,11 @@ public class GlobalTrafficShapingHandler extends AbstractTrafficShapingHandler {
      * Create the global TrafficCounter.
      */
     void createGlobalTrafficCounter(ScheduledExecutorService executor) {
-        if (executor == null) {
-            throw new NullPointerException("executor");
-        }
-        TrafficCounter tc = new TrafficCounter(this, executor, "GlobalTC", checkInterval);
+        TrafficCounter tc = new TrafficCounter(this,
+                ObjectUtil.checkNotNull(executor, "executor"),
+                "GlobalTC",
+                checkInterval);
+
         setTrafficCounter(tc);
         tc.start();
     }
diff --git a/handler/src/main/java/io/netty/handler/traffic/TrafficCounter.java b/handler/src/main/java/io/netty/handler/traffic/TrafficCounter.java
index 65fc77e..0ca40a5 100644
--- a/handler/src/main/java/io/netty/handler/traffic/TrafficCounter.java
+++ b/handler/src/main/java/io/netty/handler/traffic/TrafficCounter.java
@@ -15,6 +15,7 @@
  */
 package io.netty.handler.traffic;
 
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 
@@ -174,7 +175,6 @@ public class TrafficCounter {
             if (trafficShapingHandler != null) {
                 trafficShapingHandler.doAccounting(TrafficCounter.this);
             }
-            scheduledFuture = executor.schedule(this, checkInterval.get(), TimeUnit.MILLISECONDS);
         }
     }
 
@@ -192,7 +192,7 @@ public class TrafficCounter {
             monitorActive = true;
             monitor = new TrafficMonitoringTask();
             scheduledFuture =
-                executor.schedule(monitor, localCheckInterval, TimeUnit.MILLISECONDS);
+                executor.scheduleAtFixedRate(monitor, 0, localCheckInterval, TimeUnit.MILLISECONDS);
         }
     }
 
@@ -251,13 +251,10 @@ public class TrafficCounter {
      *            the checkInterval in millisecond between two computations.
      */
     public TrafficCounter(ScheduledExecutorService executor, String name, long checkInterval) {
-        if (name == null) {
-            throw new NullPointerException("name");
-        }
 
+        this.name = ObjectUtil.checkNotNull(name, "name");
         trafficShapingHandler = null;
         this.executor = executor;
-        this.name = name;
 
         init(checkInterval);
     }
@@ -283,13 +280,10 @@ public class TrafficCounter {
         if (trafficShapingHandler == null) {
             throw new IllegalArgumentException("trafficShapingHandler");
         }
-        if (name == null) {
-            throw new NullPointerException("name");
-        }
 
+        this.name = ObjectUtil.checkNotNull(name, "name");
         this.trafficShapingHandler = trafficShapingHandler;
         this.executor = executor;
-        this.name = name;
 
         init(checkInterval);
     }
@@ -317,7 +311,8 @@ public class TrafficCounter {
                 // No more active monitoring
                 lastTime.set(milliSecondFromNano());
             } else {
-                // Start if necessary
+                // Restart
+                stop();
                 start();
             }
         }
diff --git a/handler/src/main/resources/META-INF/native-image/io.netty/handler/native-image.properties b/handler/src/main/resources/META-INF/native-image/io.netty/handler/native-image.properties
new file mode 100644
index 0000000..edbc7f1
--- /dev/null
+++ b/handler/src/main/resources/META-INF/native-image/io.netty/handler/native-image.properties
@@ -0,0 +1,15 @@
+# Copyright 2019 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.
+
+Args = --initialize-at-run-time=io.netty.handler.ssl.util.ThreadLocalInsecureRandom
diff --git a/handler/src/test/java/io/netty/handler/address/DynamicAddressConnectHandlerTest.java b/handler/src/test/java/io/netty/handler/address/DynamicAddressConnectHandlerTest.java
new file mode 100644
index 0000000..574a959
--- /dev/null
+++ b/handler/src/test/java/io/netty/handler/address/DynamicAddressConnectHandlerTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2019 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.address;
+
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelOutboundHandlerAdapter;
+import io.netty.channel.ChannelPromise;
+import io.netty.channel.embedded.EmbeddedChannel;
+import org.junit.Test;
+
+import java.net.SocketAddress;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+
+public class DynamicAddressConnectHandlerTest {
+    private static final SocketAddress LOCAL = new SocketAddress() { };
+    private static final SocketAddress LOCAL_NEW = new SocketAddress() { };
+    private static final SocketAddress REMOTE = new SocketAddress() { };
+    private static final SocketAddress REMOTE_NEW = new SocketAddress() { };
+    @Test
+    public void testReplaceAddresses() {
+
+        EmbeddedChannel channel = new EmbeddedChannel(new ChannelOutboundHandlerAdapter() {
+            @Override
+            public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress,
+                                SocketAddress localAddress, ChannelPromise promise) {
+                try {
+                    assertSame(REMOTE_NEW, remoteAddress);
+                    assertSame(LOCAL_NEW, localAddress);
+                    promise.setSuccess();
+                } catch (Throwable cause) {
+                    promise.setFailure(cause);
+                }
+            }
+        }, new DynamicAddressConnectHandler() {
+            @Override
+            protected SocketAddress localAddress(SocketAddress remoteAddress, SocketAddress localAddress) {
+                assertSame(REMOTE, remoteAddress);
+                assertSame(LOCAL, localAddress);
+                return LOCAL_NEW;
+            }
+
+            @Override
+            protected SocketAddress remoteAddress(SocketAddress remoteAddress, SocketAddress localAddress) {
+                assertSame(REMOTE, remoteAddress);
+                assertSame(LOCAL, localAddress);
+                return REMOTE_NEW;
+            }
+        });
+        channel.connect(REMOTE, LOCAL).syncUninterruptibly();
+        assertNull(channel.pipeline().get(DynamicAddressConnectHandler.class));
+        assertFalse(channel.finish());
+    }
+
+    @Test
+    public void testLocalAddressThrows() {
+        testThrows0(true);
+    }
+
+    @Test
+    public void testRemoteAddressThrows() {
+        testThrows0(false);
+    }
+
+    private static void testThrows0(final boolean localThrows) {
+        final IllegalStateException exception = new IllegalStateException();
+
+        EmbeddedChannel channel = new EmbeddedChannel(new DynamicAddressConnectHandler() {
+            @Override
+            protected SocketAddress localAddress(
+                    SocketAddress remoteAddress, SocketAddress localAddress) throws Exception {
+                if (localThrows) {
+                    throw exception;
+                }
+                return super.localAddress(remoteAddress, localAddress);
+            }
+
+            @Override
+            protected SocketAddress remoteAddress(SocketAddress remoteAddress, SocketAddress localAddress)
+                    throws Exception {
+                if (!localThrows) {
+                    throw exception;
+                }
+                return super.remoteAddress(remoteAddress, localAddress);
+            }
+        });
+        assertSame(exception, channel.connect(REMOTE, LOCAL).cause());
+        assertNotNull(channel.pipeline().get(DynamicAddressConnectHandler.class));
+        assertFalse(channel.finish());
+    }
+}
diff --git a/handler/src/test/java/io/netty/handler/address/ResolveAddressHandlerTest.java b/handler/src/test/java/io/netty/handler/address/ResolveAddressHandlerTest.java
new file mode 100644
index 0000000..4685869
--- /dev/null
+++ b/handler/src/test/java/io/netty/handler/address/ResolveAddressHandlerTest.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2020 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.address;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.channel.DefaultEventLoopGroup;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.local.LocalAddress;
+import io.netty.channel.local.LocalChannel;
+import io.netty.channel.local.LocalServerChannel;
+import io.netty.resolver.AbstractAddressResolver;
+import io.netty.resolver.AddressResolver;
+import io.netty.resolver.AddressResolverGroup;
+import io.netty.util.concurrent.EventExecutor;
+import io.netty.util.concurrent.Promise;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.net.SocketAddress;
+import java.net.UnknownHostException;
+import java.util.List;
+import java.util.UUID;
+
+import static org.junit.Assert.*;
+
+public class ResolveAddressHandlerTest {
+
+    private static final LocalAddress UNRESOLVED = new LocalAddress("unresolved-" + UUID.randomUUID().toString());
+    private static final LocalAddress RESOLVED = new LocalAddress("resolved-" + UUID.randomUUID().toString());
+    private static final Exception ERROR = new UnknownHostException();
+
+    private static EventLoopGroup group;
+
+    @BeforeClass
+    public static void createEventLoop() {
+        group = new DefaultEventLoopGroup();
+    }
+
+    @AfterClass
+    public static void destroyEventLoop() {
+        if (group != null) {
+            group.shutdownGracefully();
+        }
+    }
+
+    @Test
+    public void testResolveSuccessful() {
+        testResolve(false);
+    }
+
+    @Test
+    public void testResolveFails() {
+        testResolve(true);
+    }
+
+    private static void testResolve(boolean fail) {
+        AddressResolverGroup<SocketAddress> resolverGroup = new TestResolverGroup(fail);
+        Bootstrap cb = new Bootstrap();
+        cb.group(group).channel(LocalChannel.class).handler(new ResolveAddressHandler(resolverGroup));
+
+        ServerBootstrap sb = new ServerBootstrap();
+        sb.group(group)
+                .channel(LocalServerChannel.class)
+                .childHandler(new ChannelInboundHandlerAdapter() {
+                    @Override
+                    public void channelActive(ChannelHandlerContext ctx) {
+                        ctx.close();
+                    }
+                });
+
+        // Start server
+        Channel sc = sb.bind(RESOLVED).syncUninterruptibly().channel();
+        ChannelFuture future = cb.connect(UNRESOLVED).awaitUninterruptibly();
+        try {
+            if (fail) {
+                assertSame(ERROR, future.cause());
+            } else {
+                assertTrue(future.isSuccess());
+            }
+            future.channel().close().syncUninterruptibly();
+        } finally {
+            future.channel().close().syncUninterruptibly();
+            sc.close().syncUninterruptibly();
+            resolverGroup.close();
+        }
+    }
+
+    private static final class TestResolverGroup extends AddressResolverGroup<SocketAddress> {
+        private final boolean fail;
+
+        TestResolverGroup(boolean fail) {
+            this.fail = fail;
+        }
+
+        @Override
+        protected AddressResolver<SocketAddress> newResolver(EventExecutor executor) {
+            return new AbstractAddressResolver<SocketAddress>(executor) {
+                @Override
+                protected boolean doIsResolved(SocketAddress address) {
+                    return address == RESOLVED;
+                }
+
+                @Override
+                protected void doResolve(SocketAddress unresolvedAddress, Promise<SocketAddress> promise) {
+                    assertSame(UNRESOLVED, unresolvedAddress);
+                    if (fail) {
+                        promise.setFailure(ERROR);
+                    } else {
+                        promise.setSuccess(RESOLVED);
+                    }
+                }
+
+                @Override
+                protected void doResolveAll(SocketAddress unresolvedAddress, Promise<List<SocketAddress>> promise) {
+                    fail();
+                }
+            };
+        }
+    };
+}
diff --git a/handler/src/test/java/io/netty/handler/flow/FlowControlHandlerTest.java b/handler/src/test/java/io/netty/handler/flow/FlowControlHandlerTest.java
index a4effb1..5876137 100644
--- a/handler/src/test/java/io/netty/handler/flow/FlowControlHandlerTest.java
+++ b/handler/src/test/java/io/netty/handler/flow/FlowControlHandlerTest.java
@@ -29,10 +29,13 @@ import io.netty.channel.ChannelInitializer;
 import io.netty.channel.ChannelOption;
 import io.netty.channel.ChannelPipeline;
 import io.netty.channel.EventLoopGroup;
+import io.netty.channel.embedded.EmbeddedChannel;
 import io.netty.channel.nio.NioEventLoopGroup;
 import io.netty.channel.socket.nio.NioServerSocketChannel;
 import io.netty.channel.socket.nio.NioSocketChannel;
 import io.netty.handler.codec.ByteToMessageDecoder;
+import io.netty.handler.timeout.IdleStateEvent;
+import io.netty.handler.timeout.IdleStateHandler;
 import io.netty.util.ReferenceCountUtil;
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
@@ -40,12 +43,14 @@ import org.junit.Test;
 
 import java.net.SocketAddress;
 import java.util.List;
+import java.util.Queue;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Exchanger;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicReference;
 
 import static java.util.concurrent.TimeUnit.*;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
+import static org.junit.Assert.*;
 
 public class FlowControlHandlerTest {
     private static EventLoopGroup GROUP;
@@ -76,7 +81,7 @@ public class FlowControlHandlerTest {
             .childOption(ChannelOption.AUTO_READ, autoRead)
             .childHandler(new ChannelInitializer<Channel>() {
                 @Override
-                protected void initChannel(Channel ch) throws Exception {
+                protected void initChannel(Channel ch) {
                     ChannelPipeline pipeline = ch.pipeline();
                     pipeline.addLast(new OneByteToThreeStringsDecoder());
                     pipeline.addLast(handlers);
@@ -368,13 +373,119 @@ public class FlowControlHandlerTest {
         }
     }
 
+    @Test
+    public void testReentranceNotCausesNPE() throws Throwable {
+        final Exchanger<Channel> peerRef = new Exchanger<Channel>();
+        final CountDownLatch latch = new CountDownLatch(3);
+        final AtomicReference<Throwable> causeRef = new AtomicReference<Throwable>();
+        ChannelInboundHandlerAdapter handler = new ChannelDuplexHandler() {
+            @Override
+            public void channelActive(ChannelHandlerContext ctx) throws Exception {
+                ctx.fireChannelActive();
+                peerRef.exchange(ctx.channel(), 1L, SECONDS);
+            }
+
+            @Override
+            public void channelRead(ChannelHandlerContext ctx, Object msg) {
+                latch.countDown();
+                ctx.read();
+            }
+
+            @Override
+            public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+                causeRef.set(cause);
+            }
+        };
+
+        FlowControlHandler flow = new FlowControlHandler();
+        Channel server = newServer(false, flow, handler);
+        Channel client = newClient(server.localAddress());
+        try {
+            // The client connection on the server side
+            Channel peer = peerRef.exchange(null, 1L, SECONDS);
+
+            // Write the message
+            client.writeAndFlush(newOneMessage())
+                    .syncUninterruptibly();
+
+            // channelRead(1)
+            peer.read();
+            assertTrue(latch.await(1L, SECONDS));
+            assertTrue(flow.isQueueEmpty());
+
+            Throwable cause = causeRef.get();
+            if (cause != null) {
+                throw cause;
+            }
+        } finally {
+            client.close();
+            server.close();
+        }
+    }
+
+    @Test
+    public void testSwallowedReadComplete() throws Exception {
+        final long delayMillis = 100;
+        final Queue<IdleStateEvent> userEvents = new LinkedBlockingQueue<IdleStateEvent>();
+        final EmbeddedChannel channel = new EmbeddedChannel(false, false,
+            new FlowControlHandler(),
+            new IdleStateHandler(delayMillis, 0, 0, MILLISECONDS),
+            new ChannelInboundHandlerAdapter() {
+                @Override
+                public void channelActive(ChannelHandlerContext ctx) {
+                    ctx.fireChannelActive();
+                    ctx.read();
+                }
+
+                @Override
+                public void channelRead(ChannelHandlerContext ctx, Object msg) {
+                    ctx.fireChannelRead(msg);
+                    ctx.read();
+                }
+
+                @Override
+                public void channelReadComplete(ChannelHandlerContext ctx) {
+                    ctx.fireChannelReadComplete();
+                    ctx.read();
+                }
+
+                @Override
+                public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
+                    if (evt instanceof IdleStateEvent) {
+                        userEvents.add((IdleStateEvent) evt);
+                    }
+                    ctx.fireUserEventTriggered(evt);
+                }
+            }
+        );
+
+        channel.config().setAutoRead(false);
+        assertFalse(channel.config().isAutoRead());
+
+        channel.register();
+
+        // Reset read timeout by some message
+        assertTrue(channel.writeInbound(Unpooled.EMPTY_BUFFER));
+        channel.flushInbound();
+        assertEquals(Unpooled.EMPTY_BUFFER, channel.readInbound());
+
+        // Emulate 'no more messages in NIO channel' on the next read attempt.
+        channel.flushInbound();
+        assertNull(channel.readInbound());
+
+        Thread.sleep(delayMillis);
+        channel.runPendingTasks();
+        assertEquals(IdleStateEvent.FIRST_READER_IDLE_STATE_EVENT, userEvents.poll());
+        assertFalse(channel.finish());
+    }
+
     /**
      * This is a fictional message decoder. It decodes each {@code byte}
      * into three strings.
      */
     private static final class OneByteToThreeStringsDecoder extends ByteToMessageDecoder {
         @Override
-        protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
+        protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
             for (int i = 0; i < in.readableBytes(); i++) {
                 out.add("1");
                 out.add("2");
diff --git a/handler/src/test/java/io/netty/handler/flush/FlushConsolidationHandlerTest.java b/handler/src/test/java/io/netty/handler/flush/FlushConsolidationHandlerTest.java
index 05eb80c..1a3af22 100644
--- a/handler/src/test/java/io/netty/handler/flush/FlushConsolidationHandlerTest.java
+++ b/handler/src/test/java/io/netty/handler/flush/FlushConsolidationHandlerTest.java
@@ -19,6 +19,8 @@ import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelInboundHandlerAdapter;
 import io.netty.channel.ChannelOutboundHandlerAdapter;
 import io.netty.channel.embedded.EmbeddedChannel;
+import io.netty.util.concurrent.Future;
+import io.netty.util.concurrent.GenericFutureListener;
 import org.junit.Test;
 
 import java.util.concurrent.atomic.AtomicInteger;
@@ -152,6 +154,26 @@ public class FlushConsolidationHandlerTest {
         assertFalse(channel.finish());
     }
 
+    /**
+     * See https://github.com/netty/netty/issues/9923
+     */
+    @Test
+    public void testResend() throws Exception {
+        final AtomicInteger flushCount = new AtomicInteger();
+        final EmbeddedChannel channel = newChannel(flushCount, true);
+        channel.writeAndFlush(1L).addListener(new GenericFutureListener<Future<? super Void>>() {
+            @Override
+            public void operationComplete(Future<? super Void> future) throws Exception {
+                channel.writeAndFlush(1L);
+            }
+        });
+        channel.flushOutbound();
+        assertEquals(1L, channel.readOutbound());
+        assertEquals(1L, channel.readOutbound());
+        assertNull(channel.readOutbound());
+        assertFalse(channel.finish());
+    }
+
     private static EmbeddedChannel newChannel(final AtomicInteger flushCount, boolean consolidateWhenNoReadInProgress) {
         return new EmbeddedChannel(
                 new ChannelOutboundHandlerAdapter() {
diff --git a/handler/src/test/java/io/netty/handler/logging/LoggingHandlerTest.java b/handler/src/test/java/io/netty/handler/logging/LoggingHandlerTest.java
index 2841688..622c89c 100644
--- a/handler/src/test/java/io/netty/handler/logging/LoggingHandlerTest.java
+++ b/handler/src/test/java/io/netty/handler/logging/LoggingHandlerTest.java
@@ -39,6 +39,7 @@ import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
 
+import static io.netty.util.internal.StringUtil.NEWLINE;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.CoreMatchers.nullValue;
 import static org.hamcrest.CoreMatchers.sameInstance;
@@ -217,7 +218,20 @@ public class LoggingHandlerTest {
         ByteBuf msg = Unpooled.copiedBuffer("hello", CharsetUtil.UTF_8);
         EmbeddedChannel channel = new EmbeddedChannel(new LoggingHandler());
         channel.writeInbound(msg);
-        verify(appender).doAppend(argThat(new RegexLogMatcher(".+READ: " + msg.readableBytes() + "B$")));
+        verify(appender).doAppend(argThat(new RegexLogMatcher(".+READ: " + msg.readableBytes() + "B$", true)));
+
+        ByteBuf handledMsg = channel.readInbound();
+        assertThat(msg, is(sameInstance(handledMsg)));
+        handledMsg.release();
+        assertThat(channel.readInbound(), is(nullValue()));
+    }
+
+    @Test
+    public void shouldLogByteBufDataReadWithSimpleFormat() throws Exception {
+        ByteBuf msg = Unpooled.copiedBuffer("hello", CharsetUtil.UTF_8);
+        EmbeddedChannel channel = new EmbeddedChannel(new LoggingHandler(LogLevel.DEBUG, ByteBufFormat.SIMPLE));
+        channel.writeInbound(msg);
+        verify(appender).doAppend(argThat(new RegexLogMatcher(".+READ: " + msg.readableBytes() + "B$", false)));
 
         ByteBuf handledMsg = channel.readInbound();
         assertThat(msg, is(sameInstance(handledMsg)));
@@ -230,7 +244,7 @@ public class LoggingHandlerTest {
         ByteBuf msg = Unpooled.EMPTY_BUFFER;
         EmbeddedChannel channel = new EmbeddedChannel(new LoggingHandler());
         channel.writeInbound(msg);
-        verify(appender).doAppend(argThat(new RegexLogMatcher(".+READ: 0B$")));
+        verify(appender).doAppend(argThat(new RegexLogMatcher(".+READ: 0B$", false)));
 
         ByteBuf handledMsg = channel.readInbound();
         assertThat(msg, is(sameInstance(handledMsg)));
@@ -248,7 +262,7 @@ public class LoggingHandlerTest {
 
         EmbeddedChannel channel = new EmbeddedChannel(new LoggingHandler());
         channel.writeInbound(msg);
-        verify(appender).doAppend(argThat(new RegexLogMatcher(".+READ: foobar, 5B$")));
+        verify(appender).doAppend(argThat(new RegexLogMatcher(".+READ: foobar, 5B$", true)));
 
         ByteBufHolder handledMsg = channel.readInbound();
         assertThat(msg, is(sameInstance(handledMsg)));
@@ -270,10 +284,16 @@ public class LoggingHandlerTest {
     private static final class RegexLogMatcher implements ArgumentMatcher<ILoggingEvent> {
 
         private final String expected;
+        private final boolean shouldContainNewline;
         private String actualMsg;
 
         RegexLogMatcher(String expected) {
+            this(expected, false);
+        }
+
+        RegexLogMatcher(String expected, boolean shouldContainNewline) {
             this.expected = expected;
+            this.shouldContainNewline = shouldContainNewline;
         }
 
         @Override
@@ -281,7 +301,11 @@ public class LoggingHandlerTest {
         public boolean matches(ILoggingEvent actual) {
             // Match only the first line to skip the validation of hex-dump format.
             actualMsg = actual.getMessage().split("(?s)[\\r\\n]+")[0];
-            return actualMsg.matches(expected);
+            if (actualMsg.matches(expected)) {
+                // The presence of a newline implies a hex-dump was logged
+                return actual.getMessage().contains(NEWLINE) == shouldContainNewline;
+            }
+            return false;
         }
     }
 
diff --git a/handler/src/test/java/io/netty/handler/ssl/AmazonCorrettoSslEngineTest.java b/handler/src/test/java/io/netty/handler/ssl/AmazonCorrettoSslEngineTest.java
new file mode 100644
index 0000000..6c7f4c1
--- /dev/null
+++ b/handler/src/test/java/io/netty/handler/ssl/AmazonCorrettoSslEngineTest.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2019 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.ssl;
+
+import com.amazon.corretto.crypto.provider.AmazonCorrettoCryptoProvider;
+import com.amazon.corretto.crypto.provider.SelfTestStatus;
+import io.netty.util.internal.PlatformDependent;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import javax.crypto.Cipher;
+import java.security.Security;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import static org.junit.Assume.assumeTrue;
+
+@RunWith(Parameterized.class)
+public class AmazonCorrettoSslEngineTest extends SSLEngineTest {
+
+    @Parameterized.Parameters(name = "{index}: bufferType = {0}, combo = {1}, delegate = {2}")
+    public static Collection<Object[]> data() {
+        List<Object[]> params = new ArrayList<Object[]>();
+        for (BufferType type: BufferType.values()) {
+            params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), false });
+            params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), true });
+
+            if (PlatformDependent.javaVersion() >= 11) {
+                params.add(new Object[] { type, ProtocolCipherCombo.tlsv13(), true });
+                params.add(new Object[] { type, ProtocolCipherCombo.tlsv13(), false });
+            }
+        }
+        return params;
+    }
+
+    public AmazonCorrettoSslEngineTest(BufferType type, ProtocolCipherCombo combo, boolean delegate) {
+        super(type, combo, delegate);
+    }
+
+    @BeforeClass
+    public static void checkAccp() {
+        assumeTrue(AmazonCorrettoCryptoProvider.INSTANCE.getLoadingError() == null &&
+                AmazonCorrettoCryptoProvider.INSTANCE.runSelfTests().equals(SelfTestStatus.PASSED));
+    }
+
+    @Override
+    protected SslProvider sslClientProvider() {
+        return SslProvider.JDK;
+    }
+
+    @Override
+    protected SslProvider sslServerProvider() {
+        return SslProvider.JDK;
+    }
+
+    @Before
+    @Override
+    public void setup() {
+        // See https://github.com/corretto/amazon-corretto-crypto-provider/blob/develop/README.md#code
+        Security.insertProviderAt(AmazonCorrettoCryptoProvider.INSTANCE, 1);
+
+        // See https://github.com/corretto/amazon-corretto-crypto-provider/blob/develop/README.md#verification-optional
+        try {
+            AmazonCorrettoCryptoProvider.INSTANCE.assertHealthy();
+            String providerName = Cipher.getInstance("AES/GCM/NoPadding").getProvider().getName();
+            Assert.assertEquals(AmazonCorrettoCryptoProvider.PROVIDER_NAME, providerName);
+        } catch (Throwable e) {
+            Security.removeProvider(AmazonCorrettoCryptoProvider.PROVIDER_NAME);
+            throw new AssertionError(e);
+        }
+        super.setup();
+    }
+
+    @After
+    @Override
+    public void tearDown() throws InterruptedException {
+        super.tearDown();
+
+        // Remove the provider again and verify that it was removed
+        Security.removeProvider(AmazonCorrettoCryptoProvider.PROVIDER_NAME);
+        Assert.assertNull(Security.getProvider(AmazonCorrettoCryptoProvider.PROVIDER_NAME));
+    }
+
+    @Ignore /* Does the JDK support a "max certificate chain length"? */
+    @Override
+    public void testMutualAuthValidClientCertChainTooLongFailOptionalClientAuth() {
+    }
+
+    @Ignore /* Does the JDK support a "max certificate chain length"? */
+    @Override
+    public void testMutualAuthValidClientCertChainTooLongFailRequireClientAuth() {
+    }
+
+    @Override
+    protected boolean mySetupMutualAuthServerIsValidException(Throwable cause) {
+        // TODO(scott): work around for a JDK issue. The exception should be SSLHandshakeException.
+        return super.mySetupMutualAuthServerIsValidException(cause) || causedBySSLException(cause);
+    }
+}
diff --git a/handler/src/test/java/io/netty/handler/ssl/CipherSuiteCanaryTest.java b/handler/src/test/java/io/netty/handler/ssl/CipherSuiteCanaryTest.java
index 9c394cc..855cb84 100644
--- a/handler/src/test/java/io/netty/handler/ssl/CipherSuiteCanaryTest.java
+++ b/handler/src/test/java/io/netty/handler/ssl/CipherSuiteCanaryTest.java
@@ -18,6 +18,7 @@ package io.netty.handler.ssl;
 
 import io.netty.bootstrap.Bootstrap;
 import io.netty.bootstrap.ServerBootstrap;
+import io.netty.buffer.ByteBufAllocator;
 import io.netty.buffer.Unpooled;
 import io.netty.channel.Channel;
 import io.netty.channel.ChannelHandler;
@@ -41,6 +42,9 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
 
 import org.junit.AfterClass;
@@ -66,7 +70,7 @@ public class CipherSuiteCanaryTest {
 
     private static SelfSignedCertificate CERT;
 
-    @Parameters(name = "{index}: serverSslProvider = {0}, clientSslProvider = {1}, rfcCipherName = {2}")
+    @Parameters(name = "{index}: serverSslProvider = {0}, clientSslProvider = {1}, rfcCipherName = {2}, delegate = {3}")
     public static Collection<Object[]> parameters() {
        List<Object[]> dst = new ArrayList<Object[]>();
        dst.addAll(expand("TLS_DHE_RSA_WITH_AES_128_GCM_SHA256")); // DHE-RSA-AES128-GCM-SHA256
@@ -80,7 +84,7 @@ public class CipherSuiteCanaryTest {
     }
 
     @AfterClass
-    public static void destory() {
+    public static void destroy() {
         GROUP.shutdownGracefully();
         CERT.delete();
     }
@@ -90,11 +94,14 @@ public class CipherSuiteCanaryTest {
     private final SslProvider clientSslProvider;
 
     private final String rfcCipherName;
+    private final boolean delegate;
 
-    public CipherSuiteCanaryTest(SslProvider serverSslProvider, SslProvider clientSslProvider, String rfcCipherName) {
+    public CipherSuiteCanaryTest(SslProvider serverSslProvider, SslProvider clientSslProvider,
+                                 String rfcCipherName, boolean delegate) {
         this.serverSslProvider = serverSslProvider;
         this.clientSslProvider = clientSslProvider;
         this.rfcCipherName = rfcCipherName;
+        this.delegate = delegate;
     }
 
     private static void assumeCipherAvailable(SslProvider provider, String cipher) throws NoSuchAlgorithmException {
@@ -113,6 +120,14 @@ public class CipherSuiteCanaryTest {
         Assume.assumeTrue("Unsupported cipher: " + cipher, cipherSupported);
     }
 
+    private static SslHandler newSslHandler(SslContext sslCtx, ByteBufAllocator allocator, Executor executor) {
+        if (executor == null) {
+            return sslCtx.newHandler(allocator);
+        } else {
+            return sslCtx.newHandler(allocator, executor);
+        }
+    }
+
     @Test
     public void testHandshake() throws Exception {
         // Check if the cipher is supported at all which may not be the case for various JDK versions and OpenSSL API
@@ -129,6 +144,8 @@ public class CipherSuiteCanaryTest {
                 .protocols(SslUtils.PROTOCOL_TLS_V1_2)
                 .build();
 
+        final ExecutorService executorService = delegate ? Executors.newCachedThreadPool() : null;
+
         try {
             final SslContext sslClientContext = SslContextBuilder.forClient()
                     .sslProvider(clientSslProvider)
@@ -146,7 +163,7 @@ public class CipherSuiteCanaryTest {
                     @Override
                     protected void initChannel(Channel ch) throws Exception {
                         ChannelPipeline pipeline = ch.pipeline();
-                        pipeline.addLast(sslServerContext.newHandler(ch.alloc()));
+                        pipeline.addLast(newSslHandler(sslServerContext, ch.alloc(), executorService));
 
                         pipeline.addLast(new SimpleChannelInboundHandler<Object>() {
                             @Override
@@ -182,7 +199,7 @@ public class CipherSuiteCanaryTest {
                         @Override
                         protected void initChannel(Channel ch) throws Exception {
                             ChannelPipeline pipeline = ch.pipeline();
-                            pipeline.addLast(sslClientContext.newHandler(ch.alloc()));
+                            pipeline.addLast(newSslHandler(sslClientContext, ch.alloc(), executorService));
 
                             pipeline.addLast(new SimpleChannelInboundHandler<Object>() {
                                 @Override
@@ -229,6 +246,10 @@ public class CipherSuiteCanaryTest {
             }
         } finally {
             ReferenceCountUtil.release(sslServerContext);
+
+            if (executorService != null) {
+                executorService.shutdown();
+            }
         }
     }
 
@@ -267,7 +288,8 @@ public class CipherSuiteCanaryTest {
                     continue;
                 }
 
-                dst.add(new Object[]{serverSslProvider, clientSslProvider, rfcCipherName});
+                dst.add(new Object[]{serverSslProvider, clientSslProvider, rfcCipherName, true});
+                dst.add(new Object[]{serverSslProvider, clientSslProvider, rfcCipherName, false});
             }
         }
 
diff --git a/handler/src/test/java/io/netty/handler/ssl/ConscryptJdkSslEngineInteropTest.java b/handler/src/test/java/io/netty/handler/ssl/ConscryptJdkSslEngineInteropTest.java
index 0976264..da2d767 100644
--- a/handler/src/test/java/io/netty/handler/ssl/ConscryptJdkSslEngineInteropTest.java
+++ b/handler/src/test/java/io/netty/handler/ssl/ConscryptJdkSslEngineInteropTest.java
@@ -31,17 +31,18 @@ import static org.junit.Assume.assumeTrue;
 @RunWith(Parameterized.class)
 public class ConscryptJdkSslEngineInteropTest extends SSLEngineTest {
 
-    @Parameterized.Parameters(name = "{index}: bufferType = {0}, combo = {1}")
+    @Parameterized.Parameters(name = "{index}: bufferType = {0}, combo = {1}, delegate = {2}")
     public static Collection<Object[]> data() {
         List<Object[]> params = new ArrayList<Object[]>();
         for (BufferType type: BufferType.values()) {
-            params.add(new Object[] { type, ProtocolCipherCombo.tlsv12()});
+            params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), false });
+            params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), true });
         }
         return params;
     }
 
-    public ConscryptJdkSslEngineInteropTest(BufferType type, ProtocolCipherCombo combo) {
-        super(type, combo);
+    public ConscryptJdkSslEngineInteropTest(BufferType type, ProtocolCipherCombo combo, boolean delegate) {
+        super(type, combo, delegate);
     }
 
     @BeforeClass
@@ -86,4 +87,11 @@ public class ConscryptJdkSslEngineInteropTest extends SSLEngineTest {
         // Ignore due bug in Conscrypt where the incorrect SSLSession object is used in the SSLSessionBindingEvent.
         // See https://github.com/google/conscrypt/issues/593
     }
+
+    @Ignore("Ignore due bug in Conscrypt")
+    @Override
+    public void testHandshakeSession() throws Exception {
+        // Ignore as Conscrypt does not correctly return the local certificates while the TrustManager is invoked.
+        // See https://github.com/google/conscrypt/issues/634
+    }
 }
diff --git a/handler/src/test/java/io/netty/handler/ssl/ConscryptOpenSslEngineInteropTest.java b/handler/src/test/java/io/netty/handler/ssl/ConscryptOpenSslEngineInteropTest.java
new file mode 100644
index 0000000..2744ac5
--- /dev/null
+++ b/handler/src/test/java/io/netty/handler/ssl/ConscryptOpenSslEngineInteropTest.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2019 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.ssl;
+
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import javax.net.ssl.SSLEngine;
+
+import java.security.Provider;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import static io.netty.handler.ssl.OpenSslTestUtils.checkShouldUseKeyManagerFactory;
+import static org.junit.Assume.assumeTrue;
+
+@RunWith(Parameterized.class)
+public class ConscryptOpenSslEngineInteropTest extends ConscryptSslEngineTest {
+
+    @Parameterized.Parameters(name = "{index}: bufferType = {0}, combo = {1}, delegate = {2}, useTasks = {3}")
+    public static Collection<Object[]> data() {
+        List<Object[]> params = new ArrayList<Object[]>();
+        for (BufferType type: BufferType.values()) {
+            params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), false, false });
+            params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), false, true });
+
+            params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), true, false });
+            params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), true, true });
+        }
+        return params;
+    }
+
+    private final boolean useTasks;
+
+    public ConscryptOpenSslEngineInteropTest(BufferType type, ProtocolCipherCombo combo,
+                                             boolean delegate, boolean useTasks) {
+        super(type, combo, delegate);
+        this.useTasks = useTasks;
+    }
+
+    @BeforeClass
+    public static void checkOpenssl() {
+        assumeTrue(OpenSsl.isAvailable());
+    }
+
+    @Override
+    protected SslProvider sslClientProvider() {
+        return SslProvider.JDK;
+    }
+
+    @Override
+    protected SslProvider sslServerProvider() {
+        return SslProvider.OPENSSL;
+    }
+
+    @Override
+    protected Provider serverSslContextProvider() {
+        return null;
+    }
+
+    @Override
+    @Test
+    @Ignore("TODO: Make this work with Conscrypt")
+    public void testMutualAuthValidClientCertChainTooLongFailOptionalClientAuth() {
+        super.testMutualAuthValidClientCertChainTooLongFailOptionalClientAuth();
+    }
+
+    @Override
+    @Test
+    @Ignore("TODO: Make this work with Conscrypt")
+    public void testMutualAuthValidClientCertChainTooLongFailRequireClientAuth() {
+        super.testMutualAuthValidClientCertChainTooLongFailRequireClientAuth();
+    }
+
+    @Override
+    protected boolean mySetupMutualAuthServerIsValidClientException(Throwable cause) {
+        // TODO(scott): work around for a JDK issue. The exception should be SSLHandshakeException.
+        return super.mySetupMutualAuthServerIsValidClientException(cause) || causedBySSLException(cause);
+    }
+
+    @Override
+    @Test
+    public void testMutualAuthInvalidIntermediateCASucceedWithOptionalClientAuth() throws Exception {
+        checkShouldUseKeyManagerFactory();
+        super.testMutualAuthInvalidIntermediateCASucceedWithOptionalClientAuth();
+    }
+
+    @Override
+    @Test
+    public void testMutualAuthInvalidIntermediateCAFailWithOptionalClientAuth() throws Exception {
+        checkShouldUseKeyManagerFactory();
+        super.testMutualAuthInvalidIntermediateCAFailWithOptionalClientAuth();
+    }
+
+    @Override
+    @Test
+    public void testMutualAuthInvalidIntermediateCAFailWithRequiredClientAuth() throws Exception {
+        checkShouldUseKeyManagerFactory();
+        super.testMutualAuthInvalidIntermediateCAFailWithRequiredClientAuth();
+    }
+
+    @Override
+    @Test
+    public void testSessionAfterHandshakeKeyManagerFactory() throws Exception {
+        checkShouldUseKeyManagerFactory();
+        super.testSessionAfterHandshakeKeyManagerFactory();
+    }
+
+    @Override
+    @Test
+    public void testSessionAfterHandshakeKeyManagerFactoryMutualAuth() throws Exception {
+        checkShouldUseKeyManagerFactory();
+        super.testSessionAfterHandshakeKeyManagerFactoryMutualAuth();
+    }
+
+    @Override
+    @Test
+    public void testSupportedSignatureAlgorithms() throws Exception {
+        checkShouldUseKeyManagerFactory();
+        super.testSupportedSignatureAlgorithms();
+    }
+
+    @Override
+    protected boolean mySetupMutualAuthServerIsValidServerException(Throwable cause) {
+        // TODO(scott): work around for a JDK issue. The exception should be SSLHandshakeException.
+        return super.mySetupMutualAuthServerIsValidServerException(cause) || causedBySSLException(cause);
+    }
+
+    @Override
+    protected SSLEngine wrapEngine(SSLEngine engine) {
+        return Java8SslTestUtils.wrapSSLEngineForTesting(engine);
+    }
+
+    @Override
+    protected SslContext wrapContext(SslContext context) {
+        if (context instanceof OpenSslContext) {
+            ((OpenSslContext) context).setUseTasks(useTasks);
+        }
+        return context;
+    }
+}
diff --git a/handler/src/test/java/io/netty/handler/ssl/ConscryptSslEngineTest.java b/handler/src/test/java/io/netty/handler/ssl/ConscryptSslEngineTest.java
index 7d06840..114552f 100644
--- a/handler/src/test/java/io/netty/handler/ssl/ConscryptSslEngineTest.java
+++ b/handler/src/test/java/io/netty/handler/ssl/ConscryptSslEngineTest.java
@@ -30,17 +30,18 @@ import static org.junit.Assume.assumeTrue;
 @RunWith(Parameterized.class)
 public class ConscryptSslEngineTest extends SSLEngineTest {
 
-    @Parameterized.Parameters(name = "{index}: bufferType = {0}, combo = {1}")
+    @Parameterized.Parameters(name = "{index}: bufferType = {0}, combo = {1}, delegate = {2}")
     public static Collection<Object[]> data() {
         List<Object[]> params = new ArrayList<Object[]>();
         for (BufferType type: BufferType.values()) {
-            params.add(new Object[] { type, ProtocolCipherCombo.tlsv12()});
+            params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), false });
+            params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), true });
         }
         return params;
     }
 
-    public ConscryptSslEngineTest(BufferType type, ProtocolCipherCombo combo) {
-        super(type, combo);
+    public ConscryptSslEngineTest(BufferType type, ProtocolCipherCombo combo, boolean delegate) {
+        super(type, combo, delegate);
     }
 
     @BeforeClass
@@ -84,4 +85,11 @@ public class ConscryptSslEngineTest extends SSLEngineTest {
         // Ignore due bug in Conscrypt where the incorrect SSLSession object is used in the SSLSessionBindingEvent.
         // See https://github.com/google/conscrypt/issues/593
     }
+
+    @Ignore("Ignore due bug in Conscrypt")
+    @Override
+    public void testHandshakeSession() throws Exception {
+        // Ignore as Conscrypt does not correctly return the local certificates while the TrustManager is invoked.
+        // See https://github.com/google/conscrypt/issues/634
+    }
 }
diff --git a/handler/src/test/java/io/netty/handler/ssl/JdkConscryptSslEngineInteropTest.java b/handler/src/test/java/io/netty/handler/ssl/JdkConscryptSslEngineInteropTest.java
index 309490a..d7aa08f 100644
--- a/handler/src/test/java/io/netty/handler/ssl/JdkConscryptSslEngineInteropTest.java
+++ b/handler/src/test/java/io/netty/handler/ssl/JdkConscryptSslEngineInteropTest.java
@@ -32,17 +32,18 @@ import static org.junit.Assume.assumeTrue;
 @RunWith(Parameterized.class)
 public class JdkConscryptSslEngineInteropTest extends SSLEngineTest {
 
-    @Parameterized.Parameters(name = "{index}: bufferType = {0}, combo = {1}")
+    @Parameterized.Parameters(name = "{index}: bufferType = {0}, combo = {1}, delegate = {2}")
     public static Collection<Object[]> data() {
         List<Object[]> params = new ArrayList<Object[]>();
         for (BufferType type: BufferType.values()) {
-            params.add(new Object[] { type, ProtocolCipherCombo.tlsv12()});
+            params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), false });
+            params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), true });
         }
         return params;
     }
 
-    public JdkConscryptSslEngineInteropTest(BufferType type, ProtocolCipherCombo combo) {
-        super(type, combo);
+    public JdkConscryptSslEngineInteropTest(BufferType type, ProtocolCipherCombo combo, boolean delegate) {
+        super(type, combo, delegate);
     }
 
     @BeforeClass
@@ -84,4 +85,11 @@ public class JdkConscryptSslEngineInteropTest extends SSLEngineTest {
         // TODO(scott): work around for a JDK issue. The exception should be SSLHandshakeException.
         return super.mySetupMutualAuthServerIsValidClientException(cause) || causedBySSLException(cause);
     }
+
+    @Ignore("Ignore due bug in Conscrypt")
+    @Override
+    public void testHandshakeSession() throws Exception {
+        // Ignore as Conscrypt does not correctly return the local certificates while the TrustManager is invoked.
+        // See https://github.com/google/conscrypt/issues/634
+    }
 }
diff --git a/handler/src/test/java/io/netty/handler/ssl/JdkOpenSslEngineInteroptTest.java b/handler/src/test/java/io/netty/handler/ssl/JdkOpenSslEngineInteroptTest.java
index 45b3c7e..90e8d98 100644
--- a/handler/src/test/java/io/netty/handler/ssl/JdkOpenSslEngineInteroptTest.java
+++ b/handler/src/test/java/io/netty/handler/ssl/JdkOpenSslEngineInteroptTest.java
@@ -33,21 +33,33 @@ import static org.junit.Assume.assumeTrue;
 @RunWith(Parameterized.class)
 public class JdkOpenSslEngineInteroptTest extends SSLEngineTest {
 
-    @Parameterized.Parameters(name = "{index}: bufferType = {0}, combo = {1}")
+    @Parameterized.Parameters(name = "{index}: bufferType = {0}, combo = {1}, delegate = {2}, useTasks = {3}")
     public static Collection<Object[]> data() {
         List<Object[]> params = new ArrayList<Object[]>();
         for (BufferType type: BufferType.values()) {
-            params.add(new Object[] { type, ProtocolCipherCombo.tlsv12()});
+            params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), false, false });
+            params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), false, true });
+
+            params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), true, false });
+            params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), true, true });
 
             if (PlatformDependent.javaVersion() >= 11 && OpenSsl.isTlsv13Supported()) {
-                params.add(new Object[] { type, ProtocolCipherCombo.tlsv13() });
+                params.add(new Object[] { type, ProtocolCipherCombo.tlsv13(), false, false });
+                params.add(new Object[] { type, ProtocolCipherCombo.tlsv13(), false, true });
+
+                params.add(new Object[] { type, ProtocolCipherCombo.tlsv13(), true, false });
+                params.add(new Object[] { type, ProtocolCipherCombo.tlsv13(), true, true });
             }
         }
         return params;
     }
 
-    public JdkOpenSslEngineInteroptTest(BufferType type, ProtocolCipherCombo protocolCipherCombo) {
-        super(type, protocolCipherCombo);
+    private final boolean useTasks;
+
+    public JdkOpenSslEngineInteroptTest(BufferType type, ProtocolCipherCombo protocolCipherCombo,
+                                        boolean delegate, boolean useTasks) {
+        super(type, protocolCipherCombo, delegate);
+        this.useTasks = useTasks;
     }
 
     @BeforeClass
@@ -126,8 +138,29 @@ public class JdkOpenSslEngineInteroptTest extends SSLEngineTest {
         return super.mySetupMutualAuthServerIsValidClientException(cause) || causedBySSLException(cause);
     }
 
+    @Override
+    public void testHandshakeSession() throws Exception {
+        checkShouldUseKeyManagerFactory();
+        super.testHandshakeSession();
+    }
+
+    @Override
+    @Test
+    public void testSupportedSignatureAlgorithms() throws Exception {
+        checkShouldUseKeyManagerFactory();
+        super.testSupportedSignatureAlgorithms();
+    }
+
     @Override
     protected SSLEngine wrapEngine(SSLEngine engine) {
         return Java8SslTestUtils.wrapSSLEngineForTesting(engine);
     }
+
+    @Override
+    protected SslContext wrapContext(SslContext context) {
+        if (context instanceof OpenSslContext) {
+            ((OpenSslContext) context).setUseTasks(useTasks);
+        }
+        return context;
+    }
 }
diff --git a/handler/src/test/java/io/netty/handler/ssl/JdkSslEngineTest.java b/handler/src/test/java/io/netty/handler/ssl/JdkSslEngineTest.java
index 74f000f..b9fd045 100644
--- a/handler/src/test/java/io/netty/handler/ssl/JdkSslEngineTest.java
+++ b/handler/src/test/java/io/netty/handler/ssl/JdkSslEngineTest.java
@@ -142,14 +142,17 @@ public class JdkSslEngineTest extends SSLEngineTest {
     private static final String FALLBACK_APPLICATION_LEVEL_PROTOCOL = "my-protocol-http1_1";
     private static final String APPLICATION_LEVEL_PROTOCOL_NOT_COMPATIBLE = "my-protocol-FOO";
 
-    @Parameterized.Parameters(name = "{index}: providerType = {0}, bufferType = {1}, combo = {2}")
+    @Parameterized.Parameters(name = "{index}: providerType = {0}, bufferType = {1}, combo = {2}, delegate = {3}")
     public static Collection<Object[]> data() {
         List<Object[]> params = new ArrayList<Object[]>();
         for (ProviderType providerType : ProviderType.values()) {
             for (BufferType bufferType : BufferType.values()) {
-                params.add(new Object[]{ providerType, bufferType, ProtocolCipherCombo.tlsv12()});
+                params.add(new Object[]{ providerType, bufferType, ProtocolCipherCombo.tlsv12(), true });
+                params.add(new Object[]{ providerType, bufferType, ProtocolCipherCombo.tlsv12(), false });
+
                 if (PlatformDependent.javaVersion() >= 11) {
-                    params.add(new Object[] { providerType, bufferType, ProtocolCipherCombo.tlsv13() });
+                    params.add(new Object[] { providerType, bufferType, ProtocolCipherCombo.tlsv13(), true });
+                    params.add(new Object[] { providerType, bufferType, ProtocolCipherCombo.tlsv13(), false });
                 }
             }
         }
@@ -160,8 +163,9 @@ public class JdkSslEngineTest extends SSLEngineTest {
 
     private Provider provider;
 
-    public JdkSslEngineTest(ProviderType providerType, BufferType bufferType, ProtocolCipherCombo protocolCipherCombo) {
-        super(bufferType, protocolCipherCombo);
+    public JdkSslEngineTest(ProviderType providerType, BufferType bufferType,
+                            ProtocolCipherCombo protocolCipherCombo, boolean delegate) {
+        super(bufferType, protocolCipherCombo, delegate);
         this.providerType = providerType;
     }
 
@@ -234,7 +238,7 @@ public class JdkSslEngineTest extends SSLEngineTest {
 
                 SslContext serverSslCtx = new JdkSslServerContext(providerType.provider(),
                     ssc.certificate(), ssc.privateKey(), null, null,
-                    IdentityCipherSuiteFilter.INSTANCE, serverApn, 0, 0);
+                    IdentityCipherSuiteFilter.INSTANCE, serverApn, 0, 0, null);
                 SslContext clientSslCtx = new JdkSslClientContext(providerType.provider(), null,
                     InsecureTrustManagerFactory.INSTANCE, null,
                     IdentityCipherSuiteFilter.INSTANCE, clientApn, 0, 0);
diff --git a/handler/src/test/java/io/netty/handler/ssl/OpenSslCachingKeyMaterialProviderTest.java b/handler/src/test/java/io/netty/handler/ssl/OpenSslCachingKeyMaterialProviderTest.java
index cfb4557..41a89ae 100644
--- a/handler/src/test/java/io/netty/handler/ssl/OpenSslCachingKeyMaterialProviderTest.java
+++ b/handler/src/test/java/io/netty/handler/ssl/OpenSslCachingKeyMaterialProviderTest.java
@@ -16,6 +16,7 @@
 package io.netty.handler.ssl;
 
 import io.netty.buffer.UnpooledByteBufAllocator;
+import org.hamcrest.CoreMatchers;
 import org.junit.Assert;
 import org.junit.Test;
 
@@ -33,7 +34,7 @@ public class OpenSslCachingKeyMaterialProviderTest extends OpenSslKeyMaterialPro
     @Override
     protected OpenSslKeyMaterialProvider newMaterialProvider(KeyManagerFactory factory, String password) {
         return new OpenSslCachingKeyMaterialProvider(ReferenceCountedOpenSslContext.chooseX509KeyManager(
-                factory.getKeyManagers()), password);
+                factory.getKeyManagers()), password, Integer.MAX_VALUE);
     }
 
     @Override
@@ -67,4 +68,22 @@ public class OpenSslCachingKeyMaterialProviderTest extends OpenSslKeyMaterialPro
         assertEquals(0, material.refCnt());
         assertEquals(0, material2.refCnt());
     }
+
+    @Test
+    public void testCacheForSunX509() throws Exception {
+        OpenSslCachingX509KeyManagerFactory factory = new OpenSslCachingX509KeyManagerFactory(
+                super.newKeyManagerFactory("SunX509"));
+        OpenSslKeyMaterialProvider provider = factory.newProvider(PASSWORD);
+        assertThat(provider,
+                CoreMatchers.<OpenSslKeyMaterialProvider>instanceOf(OpenSslCachingKeyMaterialProvider.class));
+    }
+
+    @Test
+    public void testNotCacheForX509() throws Exception {
+        OpenSslCachingX509KeyManagerFactory factory = new OpenSslCachingX509KeyManagerFactory(
+                super.newKeyManagerFactory("PKIX"));
+        OpenSslKeyMaterialProvider provider = factory.newProvider(PASSWORD);
+        assertThat(provider, CoreMatchers.not(
+                CoreMatchers.<OpenSslKeyMaterialProvider>instanceOf(OpenSslCachingKeyMaterialProvider.class)));
+    }
 }
diff --git a/handler/src/test/java/io/netty/handler/ssl/OpenSslConscryptSslEngineInteropTest.java b/handler/src/test/java/io/netty/handler/ssl/OpenSslConscryptSslEngineInteropTest.java
new file mode 100644
index 0000000..16f224c
--- /dev/null
+++ b/handler/src/test/java/io/netty/handler/ssl/OpenSslConscryptSslEngineInteropTest.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2019 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.ssl;
+
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import javax.net.ssl.SSLEngine;
+import java.security.Provider;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import static io.netty.handler.ssl.OpenSslTestUtils.checkShouldUseKeyManagerFactory;
+import static org.junit.Assume.assumeTrue;
+
+@RunWith(Parameterized.class)
+public class OpenSslConscryptSslEngineInteropTest extends ConscryptSslEngineTest {
+
+    @Parameterized.Parameters(name = "{index}: bufferType = {0}, combo = {1}, delegate = {2}, useTasks = {3}")
+    public static Collection<Object[]> data() {
+        List<Object[]> params = new ArrayList<Object[]>();
+        for (BufferType type: BufferType.values()) {
+            params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), false, false });
+            params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), false, true });
+
+            params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), true, false });
+            params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), true, true });
+        }
+        return params;
+    }
+
+    private final boolean useTasks;
+
+    public OpenSslConscryptSslEngineInteropTest(BufferType type, ProtocolCipherCombo combo,
+                                                boolean delegate, boolean useTasks) {
+        super(type, combo, delegate);
+        this.useTasks = useTasks;
+    }
+
+    @BeforeClass
+    public static void checkOpenssl() {
+        assumeTrue(OpenSsl.isAvailable());
+    }
+
+    @Override
+    protected SslProvider sslClientProvider() {
+        return SslProvider.OPENSSL;
+    }
+
+    @Override
+    protected SslProvider sslServerProvider() {
+        return SslProvider.JDK;
+    }
+
+    @Override
+    protected Provider clientSslContextProvider() {
+        return null;
+    }
+
+    @Override
+    @Test
+    @Ignore("TODO: Make this work with Conscrypt")
+    public void testMutualAuthValidClientCertChainTooLongFailOptionalClientAuth() {
+        super.testMutualAuthValidClientCertChainTooLongFailOptionalClientAuth();
+    }
+
+    @Override
+    @Test
+    @Ignore("TODO: Make this work with Conscrypt")
+    public void testMutualAuthValidClientCertChainTooLongFailRequireClientAuth() {
+        super.testMutualAuthValidClientCertChainTooLongFailRequireClientAuth();
+    }
+
+    @Override
+    protected boolean mySetupMutualAuthServerIsValidClientException(Throwable cause) {
+        // TODO(scott): work around for a JDK issue. The exception should be SSLHandshakeException.
+        return super.mySetupMutualAuthServerIsValidClientException(cause) || causedBySSLException(cause);
+    }
+
+    @Override
+    @Test
+    public void testMutualAuthInvalidIntermediateCASucceedWithOptionalClientAuth() throws Exception {
+        checkShouldUseKeyManagerFactory();
+        super.testMutualAuthInvalidIntermediateCASucceedWithOptionalClientAuth();
+    }
+
+    @Override
+    @Test
+    public void testMutualAuthInvalidIntermediateCAFailWithOptionalClientAuth() throws Exception {
+        checkShouldUseKeyManagerFactory();
+        super.testMutualAuthInvalidIntermediateCAFailWithOptionalClientAuth();
+    }
+
+    @Override
+    @Test
+    public void testMutualAuthInvalidIntermediateCAFailWithRequiredClientAuth() throws Exception {
+        checkShouldUseKeyManagerFactory();
+        super.testMutualAuthInvalidIntermediateCAFailWithRequiredClientAuth();
+    }
+
+    @Override
+    @Test
+    public void testSessionAfterHandshakeKeyManagerFactoryMutualAuth() throws Exception {
+        checkShouldUseKeyManagerFactory();
+        super.testSessionAfterHandshakeKeyManagerFactoryMutualAuth();
+    }
+
+    @Override
+    @Test
+    public void testSupportedSignatureAlgorithms() throws Exception {
+        checkShouldUseKeyManagerFactory();
+        super.testSupportedSignatureAlgorithms();
+    }
+
+    @Override
+    protected boolean mySetupMutualAuthServerIsValidServerException(Throwable cause) {
+        // TODO(scott): work around for a JDK issue. The exception should be SSLHandshakeException.
+        return super.mySetupMutualAuthServerIsValidServerException(cause) || causedBySSLException(cause);
+    }
+
+    @Override
+    protected SSLEngine wrapEngine(SSLEngine engine) {
+        return Java8SslTestUtils.wrapSSLEngineForTesting(engine);
+    }
+
+    @Override
+    protected SslContext wrapContext(SslContext context) {
+        if (context instanceof OpenSslContext) {
+            ((OpenSslContext) context).setUseTasks(useTasks);
+        }
+        return context;
+    }
+}
diff --git a/handler/src/test/java/io/netty/handler/ssl/OpenSslEngineTest.java b/handler/src/test/java/io/netty/handler/ssl/OpenSslEngineTest.java
index 32be767..2dc947d 100644
--- a/handler/src/test/java/io/netty/handler/ssl/OpenSslEngineTest.java
+++ b/handler/src/test/java/io/netty/handler/ssl/OpenSslEngineTest.java
@@ -31,19 +31,24 @@ import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLEngineResult;
+import javax.net.ssl.SSLEngineResult.HandshakeStatus;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLParameters;
 import java.nio.ByteBuffer;
 import java.security.AlgorithmConstraints;
 import java.security.AlgorithmParameters;
 import java.security.CryptoPrimitive;
 import java.security.Key;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
 import java.util.Set;
-import javax.net.ssl.SSLEngine;
-import javax.net.ssl.SSLEngineResult;
-import javax.net.ssl.SSLException;
-import javax.net.ssl.SSLParameters;
 
 import static io.netty.handler.ssl.OpenSslTestUtils.checkShouldUseKeyManagerFactory;
 import static io.netty.handler.ssl.ReferenceCountedOpenSslEngine.MAX_PLAINTEXT_LENGTH;
@@ -54,34 +59,46 @@ import static io.netty.handler.ssl.SslUtils.PROTOCOL_TLS_V1_1;
 import static io.netty.handler.ssl.SslUtils.PROTOCOL_TLS_V1_2;
 import static io.netty.internal.tcnative.SSL.SSL_CVERIFY_IGNORED;
 import static java.lang.Integer.MAX_VALUE;
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
-
 @RunWith(Parameterized.class)
 public class OpenSslEngineTest extends SSLEngineTest {
     private static final String PREFERRED_APPLICATION_LEVEL_PROTOCOL = "my-protocol-http2";
     private static final String FALLBACK_APPLICATION_LEVEL_PROTOCOL = "my-protocol-http1_1";
 
-    @Parameterized.Parameters(name = "{index}: bufferType = {0}, combo = {1}")
+    @Parameterized.Parameters(name = "{index}: bufferType = {0}, combo = {1}, delegate = {2}, useTasks = {3}")
     public static Collection<Object[]> data() {
         List<Object[]> params = new ArrayList<Object[]>();
         for (BufferType type: BufferType.values()) {
-            params.add(new Object[] { type, ProtocolCipherCombo.tlsv12()});
+            params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), false, false });
+            params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), false, true });
+
+            params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), true, false });
+            params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), true, true });
 
             if (OpenSsl.isTlsv13Supported()) {
-                params.add(new Object[] { type, ProtocolCipherCombo.tlsv13() });
+                params.add(new Object[] { type, ProtocolCipherCombo.tlsv13(), false, false });
+                params.add(new Object[] { type, ProtocolCipherCombo.tlsv13(), false, true });
+
+                params.add(new Object[] { type, ProtocolCipherCombo.tlsv13(), true, false });
+                params.add(new Object[] { type, ProtocolCipherCombo.tlsv13(), true, true });
             }
         }
         return params;
     }
 
-    public OpenSslEngineTest(BufferType type, ProtocolCipherCombo cipherCombo) {
-        super(type, cipherCombo);
+    protected final boolean useTasks;
+
+    public OpenSslEngineTest(BufferType type, ProtocolCipherCombo cipherCombo, boolean delegate, boolean useTasks) {
+        super(type, cipherCombo, delegate);
+        this.useTasks = useTasks;
     }
 
     @BeforeClass
@@ -145,17 +162,16 @@ public class OpenSslEngineTest extends SSLEngineTest {
     }
 
     @Override
-    @Test
-    public void testClientHostnameValidationSuccess() throws InterruptedException, SSLException {
-        assumeTrue(OpenSsl.supportsHostnameValidation());
-        super.testClientHostnameValidationSuccess();
+    public void testHandshakeSession() throws Exception {
+        checkShouldUseKeyManagerFactory();
+        super.testHandshakeSession();
     }
 
     @Override
     @Test
-    public void testClientHostnameValidationFail() throws InterruptedException, SSLException {
-        assumeTrue(OpenSsl.supportsHostnameValidation());
-        super.testClientHostnameValidationFail();
+    public void testSupportedSignatureAlgorithms() throws Exception {
+        checkShouldUseKeyManagerFactory();
+        super.testSupportedSignatureAlgorithms();
     }
 
     private static boolean isNpnSupported(String versionString) {
@@ -223,18 +239,18 @@ public class OpenSslEngineTest extends SSLEngineTest {
     }
     @Test
     public void testWrapBuffersNoWritePendingError() throws Exception {
-        clientSslCtx = SslContextBuilder.forClient()
+        clientSslCtx = wrapContext(SslContextBuilder.forClient()
                                         .trustManager(InsecureTrustManagerFactory.INSTANCE)
                                         .sslProvider(sslClientProvider())
                                         .protocols(protocols())
                                         .ciphers(ciphers())
-                                        .build();
+                                        .build());
         SelfSignedCertificate ssc = new SelfSignedCertificate();
-        serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
+        serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
                                         .sslProvider(sslServerProvider())
                                         .protocols(protocols())
                                         .ciphers(ciphers())
-                                        .build();
+                                        .build());
         SSLEngine clientEngine = null;
         SSLEngine serverEngine = null;
         try {
@@ -261,18 +277,18 @@ public class OpenSslEngineTest extends SSLEngineTest {
 
     @Test
     public void testOnlySmallBufferNeededForWrap() throws Exception {
-        clientSslCtx = SslContextBuilder.forClient()
+        clientSslCtx = wrapContext(SslContextBuilder.forClient()
                                         .trustManager(InsecureTrustManagerFactory.INSTANCE)
                                         .sslProvider(sslClientProvider())
                                         .protocols(protocols())
                                         .ciphers(ciphers())
-                                        .build();
+                                        .build());
         SelfSignedCertificate ssc = new SelfSignedCertificate();
-        serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
+        serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
                                         .sslProvider(sslServerProvider())
                                         .protocols(protocols())
                                         .ciphers(ciphers())
-                                        .build();
+                                        .build());
         SSLEngine clientEngine = null;
         SSLEngine serverEngine = null;
         try {
@@ -316,18 +332,18 @@ public class OpenSslEngineTest extends SSLEngineTest {
 
     @Test
     public void testNeededDstCapacityIsCorrectlyCalculated() throws Exception {
-        clientSslCtx = SslContextBuilder.forClient()
+        clientSslCtx = wrapContext(SslContextBuilder.forClient()
                                         .trustManager(InsecureTrustManagerFactory.INSTANCE)
                                         .sslProvider(sslClientProvider())
                                         .protocols(protocols())
                                         .ciphers(ciphers())
-                                        .build();
+                                        .build());
         SelfSignedCertificate ssc = new SelfSignedCertificate();
-        serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
+        serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
                                         .sslProvider(sslServerProvider())
                                         .protocols(protocols())
                                         .ciphers(ciphers())
-                                        .build();
+                                        .build());
         SSLEngine clientEngine = null;
         SSLEngine serverEngine = null;
         try {
@@ -356,18 +372,18 @@ public class OpenSslEngineTest extends SSLEngineTest {
 
     @Test
     public void testSrcsLenOverFlowCorrectlyHandled() throws Exception {
-        clientSslCtx = SslContextBuilder.forClient()
+        clientSslCtx = wrapContext(SslContextBuilder.forClient()
                                         .trustManager(InsecureTrustManagerFactory.INSTANCE)
                                         .sslProvider(sslClientProvider())
                                         .protocols(protocols())
                                         .ciphers(ciphers())
-                                        .build();
+                                        .build());
         SelfSignedCertificate ssc = new SelfSignedCertificate();
-        serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
+        serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
                                         .sslProvider(sslServerProvider())
                                         .protocols(protocols())
                                         .ciphers(ciphers())
-                                        .build();
+                                        .build());
         SSLEngine clientEngine = null;
         SSLEngine serverEngine = null;
         try {
@@ -407,12 +423,12 @@ public class OpenSslEngineTest extends SSLEngineTest {
 
     @Test
     public void testCalculateOutNetBufSizeOverflow() throws SSLException {
-        clientSslCtx = SslContextBuilder.forClient()
+        clientSslCtx = wrapContext(SslContextBuilder.forClient()
                                         .trustManager(InsecureTrustManagerFactory.INSTANCE)
                                         .sslProvider(sslClientProvider())
                                         .protocols(protocols())
                                         .ciphers(ciphers())
-                                        .build();
+                                        .build());
         SSLEngine clientEngine = null;
         try {
             clientEngine = clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT);
@@ -425,12 +441,12 @@ public class OpenSslEngineTest extends SSLEngineTest {
 
     @Test
     public void testCalculateOutNetBufSize0() throws SSLException {
-        clientSslCtx = SslContextBuilder.forClient()
+        clientSslCtx = wrapContext(SslContextBuilder.forClient()
                                         .trustManager(InsecureTrustManagerFactory.INSTANCE)
                                         .sslProvider(sslClientProvider())
                                         .protocols(protocols())
                                         .ciphers(ciphers())
-                                        .build();
+                                        .build());
         SSLEngine clientEngine = null;
         try {
             clientEngine = clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT);
@@ -452,18 +468,18 @@ public class OpenSslEngineTest extends SSLEngineTest {
 
     private void testCorrectlyCalculateSpaceForAlert(boolean jdkCompatabilityMode) throws Exception {
         SelfSignedCertificate ssc = new SelfSignedCertificate();
-        serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
+        serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
                                         .sslProvider(sslServerProvider())
                                         .protocols(protocols())
                                         .ciphers(ciphers())
-                                        .build();
+                                        .build());
 
-        clientSslCtx = SslContextBuilder.forClient()
+        clientSslCtx = wrapContext(SslContextBuilder.forClient()
                                         .trustManager(InsecureTrustManagerFactory.INSTANCE)
                                         .sslProvider(sslClientProvider())
                                         .protocols(protocols())
                                         .ciphers(ciphers())
-                                        .build();
+                                        .build());
         SSLEngine clientEngine = null;
         SSLEngine serverEngine = null;
         try {
@@ -481,7 +497,7 @@ public class OpenSslEngineTest extends SSLEngineTest {
 
             ByteBuffer empty = allocateBuffer(0);
             ByteBuffer dst = allocateBuffer(clientEngine.getSession().getPacketBufferSize());
-            // Limit to something that is guaranteed to be too small to hold a SSL Record.
+            // Limit to something that is guaranteed to be too small to hold an SSL Record.
             dst.limit(1);
 
             // As we called closeOutbound() before this should produce a BUFFER_OVERFLOW.
@@ -514,14 +530,14 @@ public class OpenSslEngineTest extends SSLEngineTest {
 
     @Test
     public void testWrapWithDifferentSizesTLSv1() throws Exception {
-        clientSslCtx = SslContextBuilder.forClient()
+        clientSslCtx = wrapContext(SslContextBuilder.forClient()
                                         .trustManager(InsecureTrustManagerFactory.INSTANCE)
                                         .sslProvider(sslClientProvider())
-                                        .build();
+                                        .build());
         SelfSignedCertificate ssc = new SelfSignedCertificate();
-        serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
+        serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
                                         .sslProvider(sslServerProvider())
-                                        .build();
+                                        .build());
 
         testWrapWithDifferentSizes(PROTOCOL_TLS_V1, "AES128-SHA");
         testWrapWithDifferentSizes(PROTOCOL_TLS_V1, "ECDHE-RSA-AES128-SHA");
@@ -545,14 +561,14 @@ public class OpenSslEngineTest extends SSLEngineTest {
 
     @Test
     public void testWrapWithDifferentSizesTLSv1_1() throws Exception {
-        clientSslCtx = SslContextBuilder.forClient()
+        clientSslCtx = wrapContext(SslContextBuilder.forClient()
                                         .trustManager(InsecureTrustManagerFactory.INSTANCE)
                                         .sslProvider(sslClientProvider())
-                                        .build();
+                                        .build());
         SelfSignedCertificate ssc = new SelfSignedCertificate();
-        serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
+        serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
                                         .sslProvider(sslServerProvider())
-                                        .build();
+                                        .build());
 
         testWrapWithDifferentSizes(PROTOCOL_TLS_V1_1, "ECDHE-RSA-AES256-SHA");
         testWrapWithDifferentSizes(PROTOCOL_TLS_V1_1, "AES256-SHA");
@@ -573,14 +589,14 @@ public class OpenSslEngineTest extends SSLEngineTest {
 
     @Test
     public void testWrapWithDifferentSizesTLSv1_2() throws Exception {
-        clientSslCtx = SslContextBuilder.forClient()
+        clientSslCtx = wrapContext(SslContextBuilder.forClient()
                 .trustManager(InsecureTrustManagerFactory.INSTANCE)
                 .sslProvider(sslClientProvider())
-                .build();
+                .build());
         SelfSignedCertificate ssc = new SelfSignedCertificate();
-        serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
+        serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
                 .sslProvider(sslServerProvider())
-                .build();
+                .build());
 
         testWrapWithDifferentSizes(PROTOCOL_TLS_V1_2, "AES128-SHA");
         testWrapWithDifferentSizes(PROTOCOL_TLS_V1_2, "ECDHE-RSA-AES128-SHA");
@@ -611,14 +627,14 @@ public class OpenSslEngineTest extends SSLEngineTest {
 
     @Test
     public void testWrapWithDifferentSizesSSLv3() throws Exception {
-        clientSslCtx = SslContextBuilder.forClient()
+        clientSslCtx = wrapContext(SslContextBuilder.forClient()
                 .trustManager(InsecureTrustManagerFactory.INSTANCE)
                 .sslProvider(sslClientProvider())
-                .build();
+                .build());
         SelfSignedCertificate ssc = new SelfSignedCertificate();
-        serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
+        serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
                 .sslProvider(sslServerProvider())
-                .build();
+                .build());
 
         testWrapWithDifferentSizes(PROTOCOL_SSL_V3, "ADH-AES128-SHA");
         testWrapWithDifferentSizes(PROTOCOL_SSL_V3, "ADH-CAMELLIA128-SHA");
@@ -651,21 +667,21 @@ public class OpenSslEngineTest extends SSLEngineTest {
     public void testMultipleRecordsInOneBufferWithNonZeroPositionJDKCompatabilityModeOff() throws Exception {
         SelfSignedCertificate cert = new SelfSignedCertificate();
 
-        clientSslCtx = SslContextBuilder
+        clientSslCtx = wrapContext(SslContextBuilder
                 .forClient()
                 .trustManager(cert.cert())
                 .sslProvider(sslClientProvider())
                 .protocols(protocols())
                 .ciphers(ciphers())
-                .build();
+                .build());
         SSLEngine client = wrapEngine(clientSslCtx.newHandler(UnpooledByteBufAllocator.DEFAULT).engine());
 
-        serverSslCtx = SslContextBuilder
+        serverSslCtx = wrapContext(SslContextBuilder
                 .forServer(cert.certificate(), cert.privateKey())
                 .sslProvider(sslServerProvider())
                 .protocols(protocols())
                 .ciphers(ciphers())
-                .build();
+                .build());
         SSLEngine server = wrapEngine(serverSslCtx.newHandler(UnpooledByteBufAllocator.DEFAULT).engine());
 
         try {
@@ -732,21 +748,21 @@ public class OpenSslEngineTest extends SSLEngineTest {
     public void testInputTooBigAndFillsUpBuffersJDKCompatabilityModeOff() throws Exception {
         SelfSignedCertificate cert = new SelfSignedCertificate();
 
-        clientSslCtx = SslContextBuilder
+        clientSslCtx = wrapContext(SslContextBuilder
                 .forClient()
                 .trustManager(cert.cert())
                 .sslProvider(sslClientProvider())
                 .protocols(protocols())
                 .ciphers(ciphers())
-                .build();
+                .build());
         SSLEngine client = wrapEngine(clientSslCtx.newHandler(UnpooledByteBufAllocator.DEFAULT).engine());
 
-        serverSslCtx = SslContextBuilder
+        serverSslCtx = wrapContext(SslContextBuilder
                 .forServer(cert.certificate(), cert.privateKey())
                 .sslProvider(sslServerProvider())
                 .protocols(protocols())
                 .ciphers(ciphers())
-                .build();
+                .build());
         SSLEngine server = wrapEngine(serverSslCtx.newHandler(UnpooledByteBufAllocator.DEFAULT).engine());
 
         try {
@@ -820,21 +836,21 @@ public class OpenSslEngineTest extends SSLEngineTest {
     public void testPartialPacketUnwrapJDKCompatabilityModeOff() throws Exception {
         SelfSignedCertificate cert = new SelfSignedCertificate();
 
-        clientSslCtx = SslContextBuilder
+        clientSslCtx = wrapContext(SslContextBuilder
                 .forClient()
                 .trustManager(cert.cert())
                 .sslProvider(sslClientProvider())
                 .protocols(protocols())
                 .ciphers(ciphers())
-                .build();
+                .build());
         SSLEngine client = wrapEngine(clientSslCtx.newHandler(UnpooledByteBufAllocator.DEFAULT).engine());
 
-        serverSslCtx = SslContextBuilder
+        serverSslCtx = wrapContext(SslContextBuilder
                 .forServer(cert.certificate(), cert.privateKey())
                 .sslProvider(sslServerProvider())
                 .protocols(protocols())
                 .ciphers(ciphers())
-                .build();
+                .build());
         SSLEngine server = wrapEngine(serverSslCtx.newHandler(UnpooledByteBufAllocator.DEFAULT).engine());
 
         try {
@@ -899,21 +915,21 @@ public class OpenSslEngineTest extends SSLEngineTest {
     public void testBufferUnderFlowAvoidedIfJDKCompatabilityModeOff() throws Exception {
         SelfSignedCertificate cert = new SelfSignedCertificate();
 
-        clientSslCtx = SslContextBuilder
+        clientSslCtx = wrapContext(SslContextBuilder
                 .forClient()
                 .trustManager(cert.cert())
                 .sslProvider(sslClientProvider())
                 .protocols(protocols())
                 .ciphers(ciphers())
-                .build();
+                .build());
         SSLEngine client = wrapEngine(clientSslCtx.newHandler(UnpooledByteBufAllocator.DEFAULT).engine());
 
-        serverSslCtx = SslContextBuilder
+        serverSslCtx = wrapContext(SslContextBuilder
                 .forServer(cert.certificate(), cert.privateKey())
                 .sslProvider(sslServerProvider())
                 .protocols(protocols())
                 .ciphers(ciphers())
-                .build();
+                .build());
         SSLEngine server = wrapEngine(serverSslCtx.newHandler(UnpooledByteBufAllocator.DEFAULT).engine());
 
         try {
@@ -1039,11 +1055,11 @@ public class OpenSslEngineTest extends SSLEngineTest {
     public void testSNIMatchersDoesNotThrow() throws Exception {
         assumeTrue(PlatformDependent.javaVersion() >= 8);
         SelfSignedCertificate ssc = new SelfSignedCertificate();
-        serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
+        serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
                                         .sslProvider(sslServerProvider())
                                         .protocols(protocols())
                                         .ciphers(ciphers())
-                                        .build();
+                                        .build());
 
         SSLEngine engine = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
         try {
@@ -1061,11 +1077,11 @@ public class OpenSslEngineTest extends SSLEngineTest {
         assumeTrue(PlatformDependent.javaVersion() >= 8);
         byte[] name = "rb8hx3pww30y3tvw0mwy.v1_1".getBytes(CharsetUtil.UTF_8);
         SelfSignedCertificate ssc = new SelfSignedCertificate();
-        serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
+        serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
                                         .sslProvider(sslServerProvider())
                                         .protocols(protocols())
                                         .ciphers(ciphers())
-                                        .build();
+                                        .build());
 
         SSLEngine engine = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
         try {
@@ -1083,11 +1099,11 @@ public class OpenSslEngineTest extends SSLEngineTest {
     @Test(expected = IllegalArgumentException.class)
     public void testAlgorithmConstraintsThrows() throws Exception {
         SelfSignedCertificate ssc = new SelfSignedCertificate();
-        serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
+        serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
                                         .sslProvider(sslServerProvider())
                                         .protocols(protocols())
                                         .ciphers(ciphers())
-                                        .build();
+                                        .build());
 
         SSLEngine engine = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
         try {
@@ -1117,6 +1133,177 @@ public class OpenSslEngineTest extends SSLEngineTest {
         }
     }
 
+    private static void runTasksIfNeeded(SSLEngine engine) {
+        if (engine.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
+            for (;;) {
+                Runnable task = engine.getDelegatedTask();
+                if (task == null) {
+                    assertNotEquals(HandshakeStatus.NEED_TASK, engine.getHandshakeStatus());
+                    break;
+                }
+                task.run();
+            }
+        }
+    }
+
+    @Test
+    public void testExtractMasterkeyWorksCorrectly() throws Exception {
+        SelfSignedCertificate cert = new SelfSignedCertificate();
+        serverSslCtx = wrapContext(SslContextBuilder.forServer(cert.key(), cert.cert())
+                .sslProvider(SslProvider.OPENSSL).build());
+        final SSLEngine serverEngine =
+                wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
+        clientSslCtx = wrapContext(SslContextBuilder.forClient()
+                .trustManager(cert.certificate())
+                .sslProvider(SslProvider.OPENSSL).build());
+        final SSLEngine clientEngine =
+                wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
+
+        try {
+            //lets set the cipher suite to a specific one with DHE
+            assumeTrue("The diffie hellman cipher is not supported on your runtime.",
+                    Arrays.asList(clientEngine.getSupportedCipherSuites())
+                            .contains("TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256"));
+
+            //https://www.ietf.org/rfc/rfc5289.txt
+            //For cipher suites ending with _SHA256, the PRF is the TLS PRF
+            //[RFC5246] with SHA-256 as the hash function.  The MAC is HMAC
+            //[RFC2104] with SHA-256 as the hash function.
+            clientEngine.setEnabledCipherSuites(new String[] { "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256" });
+            serverEngine.setEnabledCipherSuites(new String[] { "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256" });
+
+            int appBufferMax = clientEngine.getSession().getApplicationBufferSize();
+            int netBufferMax = clientEngine.getSession().getPacketBufferSize();
+
+            /*
+             * We'll make the input buffers a bit bigger than the max needed
+             * size, so that unwrap()s following a successful data transfer
+             * won't generate BUFFER_OVERFLOWS.
+             */
+            ByteBuffer clientIn = ByteBuffer.allocate(appBufferMax + 50);
+            ByteBuffer serverIn = ByteBuffer.allocate(appBufferMax + 50);
+
+            ByteBuffer cTOs = ByteBuffer.allocate(netBufferMax);
+            ByteBuffer sTOc = ByteBuffer.allocate(netBufferMax);
+
+            ByteBuffer clientOut = ByteBuffer.wrap("Hi Server, I'm Client".getBytes());
+            ByteBuffer serverOut = ByteBuffer.wrap("Hello Client, I'm Server".getBytes());
+
+            // This implementation is largely imitated from
+            // https://docs.oracle.com/javase/8/docs/technotes/
+            // guides/security/jsse/samples/sslengine/SSLEngineSimpleDemo.java
+            // It has been simplified however without the need for running delegation tasks
+
+            // Do handshake for SSL
+            // A typical handshake will usually contain the following steps:
+            // 1. wrap:     ClientHello
+            // 2. unwrap:   ServerHello/Cert/ServerHelloDone
+            // 3. wrap:     ClientKeyExchange
+            // 4. wrap:     ChangeCipherSpec
+            // 5. wrap:     Finished
+            // 6. unwrap:   ChangeCipherSpec
+            // 7. unwrap:   Finished
+
+            //set a for loop; instead of a while loop to guarantee we quit out eventually
+            boolean asserted = false;
+            for (int i = 0; i < 1000; i++) {
+
+                clientEngine.wrap(clientOut, cTOs);
+                serverEngine.wrap(serverOut, sTOc);
+
+                cTOs.flip();
+                sTOc.flip();
+
+                runTasksIfNeeded(clientEngine);
+                runTasksIfNeeded(serverEngine);
+
+                clientEngine.unwrap(sTOc, clientIn);
+                serverEngine.unwrap(cTOs, serverIn);
+
+                runTasksIfNeeded(clientEngine);
+                runTasksIfNeeded(serverEngine);
+
+                // check when the application data has fully been consumed and sent
+                // for both the client and server
+                if ((clientOut.limit() == serverIn.position()) &&
+                        (serverOut.limit() == clientIn.position())) {
+                    byte[] serverRandom = SSL.getServerRandom(unwrapEngine(serverEngine).sslPointer());
+                    byte[] clientRandom = SSL.getClientRandom(unwrapEngine(clientEngine).sslPointer());
+                    byte[] serverMasterKey = SSL.getMasterKey(unwrapEngine(serverEngine).sslPointer());
+                    byte[] clientMasterKey = SSL.getMasterKey(unwrapEngine(clientEngine).sslPointer());
+
+                    asserted = true;
+                    assertArrayEquals(serverMasterKey, clientMasterKey);
+
+                    // let us re-read the encrypted data and decrypt it ourselves!
+                    cTOs.flip();
+                    sTOc.flip();
+
+                    // See http://tools.ietf.org/html/rfc5246#section-6.3:
+                    // key_block = PRF(SecurityParameters.master_secret, "key expansion",
+                    //                 SecurityParameters.server_random + SecurityParameters.client_random);
+                    //
+                    // partitioned:
+                    //       client_write_MAC_secret[SecurityParameters.hash_size]
+                    //       server_write_MAC_secret[SecurityParameters.hash_size]
+                    //       client_write_key[SecurityParameters.key_material_length]
+                    //       server_write_key[SecurityParameters.key_material_length]
+
+                    int keySize = 16; // AES is 16 bytes or 128 bits
+                    int macSize = 32; // SHA256 is 32 bytes or 256 bits
+                    int keyBlockSize = (2 * keySize) + (2 * macSize);
+
+                    byte[] seed = new byte[serverRandom.length + clientRandom.length];
+                    System.arraycopy(serverRandom, 0, seed, 0, serverRandom.length);
+                    System.arraycopy(clientRandom, 0, seed, serverRandom.length, clientRandom.length);
+                    byte[] keyBlock = PseudoRandomFunction.hash(serverMasterKey,
+                            "key expansion".getBytes(CharsetUtil.US_ASCII), seed, keyBlockSize, "HmacSha256");
+
+                    int offset = 0;
+                    byte[] clientWriteMac = Arrays.copyOfRange(keyBlock, offset, offset + macSize);
+                    offset += macSize;
+
+                    byte[] serverWriteMac = Arrays.copyOfRange(keyBlock, offset, offset + macSize);
+                    offset += macSize;
+
+                    byte[] clientWriteKey = Arrays.copyOfRange(keyBlock, offset, offset + keySize);
+                    offset += keySize;
+
+                    byte[] serverWriteKey = Arrays.copyOfRange(keyBlock, offset, offset + keySize);
+                    offset += keySize;
+
+                    //advance the cipher text by 5
+                    //to take into account the TLS Record Header
+                    cTOs.position(cTOs.position() + 5);
+
+                    byte[] ciphertext = new byte[cTOs.remaining()];
+                    cTOs.get(ciphertext);
+
+                    //the initialization vector is the first 16 bytes (128 bits) of the payload
+                    byte[] clientWriteIV = Arrays.copyOfRange(ciphertext, 0, 16);
+                    ciphertext = Arrays.copyOfRange(ciphertext, 16, ciphertext.length);
+
+                    SecretKeySpec secretKey = new SecretKeySpec(clientWriteKey, "AES");
+                    final IvParameterSpec ivForCBC = new IvParameterSpec(clientWriteIV);
+                    Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
+                    cipher.init(Cipher.DECRYPT_MODE, secretKey, ivForCBC);
+                    byte[] plaintext = cipher.doFinal(ciphertext);
+                    assertTrue(new String(plaintext).startsWith("Hi Server, I'm Client"));
+                    break;
+                } else {
+                    cTOs.compact();
+                    sTOc.compact();
+                }
+            }
+
+            assertTrue("The assertions were never executed.", asserted);
+        } finally {
+            cleanupClientSslEngine(clientEngine);
+            cleanupServerSslEngine(serverEngine);
+            cert.delete();
+        }
+    }
+
     @Override
     protected SslProvider sslClientProvider() {
         return SslProvider.OPENSSL;
@@ -1149,4 +1336,12 @@ public class OpenSslEngineTest extends SSLEngineTest {
         }
         return (ReferenceCountedOpenSslEngine) engine;
     }
+
+    @Override
+    protected SslContext wrapContext(SslContext context) {
+        if (context instanceof OpenSslContext) {
+            ((OpenSslContext) context).setUseTasks(useTasks);
+        }
+        return context;
+    }
 }
diff --git a/handler/src/test/java/io/netty/handler/ssl/OpenSslJdkSslEngineInteroptTest.java b/handler/src/test/java/io/netty/handler/ssl/OpenSslJdkSslEngineInteroptTest.java
index df4e757..3bd4e31 100644
--- a/handler/src/test/java/io/netty/handler/ssl/OpenSslJdkSslEngineInteroptTest.java
+++ b/handler/src/test/java/io/netty/handler/ssl/OpenSslJdkSslEngineInteroptTest.java
@@ -21,7 +21,7 @@ import org.junit.Ignore;
 import org.junit.Test;
 
 import javax.net.ssl.SSLEngine;
-import javax.net.ssl.SSLException;
+
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 
@@ -35,21 +35,33 @@ import static org.junit.Assume.assumeTrue;
 @RunWith(Parameterized.class)
 public class OpenSslJdkSslEngineInteroptTest extends SSLEngineTest {
 
-    @Parameterized.Parameters(name = "{index}: bufferType = {0}, combo = {1}")
+    @Parameterized.Parameters(name = "{index}: bufferType = {0}, combo = {1}, delegate = {2}, useTasks = {3}")
     public static Collection<Object[]> data() {
         List<Object[]> params = new ArrayList<Object[]>();
         for (BufferType type: BufferType.values()) {
-            params.add(new Object[] { type, ProtocolCipherCombo.tlsv12()});
+            params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), false, false });
+            params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), false, true });
+
+            params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), true, false});
+            params.add(new Object[] { type, ProtocolCipherCombo.tlsv12(), true, true });
 
             if (PlatformDependent.javaVersion() >= 11 && OpenSsl.isTlsv13Supported()) {
-                params.add(new Object[] { type, ProtocolCipherCombo.tlsv13() });
+                params.add(new Object[] { type, ProtocolCipherCombo.tlsv13(), false, false });
+                params.add(new Object[] { type, ProtocolCipherCombo.tlsv13(), false, true });
+
+                params.add(new Object[] { type, ProtocolCipherCombo.tlsv13(), true, false });
+                params.add(new Object[] { type, ProtocolCipherCombo.tlsv13(), true, true });
             }
         }
         return params;
     }
 
-    public OpenSslJdkSslEngineInteroptTest(BufferType type, ProtocolCipherCombo combo) {
-        super(type, combo);
+    private final boolean useTasks;
+
+    public OpenSslJdkSslEngineInteroptTest(BufferType type, ProtocolCipherCombo combo,
+                                           boolean delegate, boolean useTasks) {
+        super(type, combo, delegate);
+        this.useTasks = useTasks;
     }
 
     @BeforeClass
@@ -100,33 +112,40 @@ public class OpenSslJdkSslEngineInteroptTest extends SSLEngineTest {
 
     @Override
     @Test
-    public void testClientHostnameValidationSuccess() throws InterruptedException, SSLException {
-        assumeTrue(OpenSsl.supportsHostnameValidation());
-        super.testClientHostnameValidationSuccess();
+    public void testSessionAfterHandshakeKeyManagerFactoryMutualAuth() throws Exception {
+        checkShouldUseKeyManagerFactory();
+        super.testSessionAfterHandshakeKeyManagerFactoryMutualAuth();
     }
 
     @Override
-    @Test
-    public void testClientHostnameValidationFail() throws InterruptedException, SSLException {
-        assumeTrue(OpenSsl.supportsHostnameValidation());
-        super.testClientHostnameValidationFail();
+    protected boolean mySetupMutualAuthServerIsValidServerException(Throwable cause) {
+        // TODO(scott): work around for a JDK issue. The exception should be SSLHandshakeException.
+        return super.mySetupMutualAuthServerIsValidServerException(cause) || causedBySSLException(cause);
     }
 
     @Override
-    @Test
-    public void testSessionAfterHandshakeKeyManagerFactoryMutualAuth() throws Exception {
+    public void testHandshakeSession() throws Exception {
         checkShouldUseKeyManagerFactory();
-        super.testSessionAfterHandshakeKeyManagerFactoryMutualAuth();
+        super.testHandshakeSession();
     }
 
     @Override
-    protected boolean mySetupMutualAuthServerIsValidServerException(Throwable cause) {
-        // TODO(scott): work around for a JDK issue. The exception should be SSLHandshakeException.
-        return super.mySetupMutualAuthServerIsValidServerException(cause) || causedBySSLException(cause);
+    @Test
+    public void testSupportedSignatureAlgorithms() throws Exception {
+        checkShouldUseKeyManagerFactory();
+        super.testSupportedSignatureAlgorithms();
     }
 
     @Override
     protected SSLEngine wrapEngine(SSLEngine engine) {
         return Java8SslTestUtils.wrapSSLEngineForTesting(engine);
     }
+
+    @Override
+    protected SslContext wrapContext(SslContext context) {
+        if (context instanceof OpenSslContext) {
+            ((OpenSslContext) context).setUseTasks(useTasks);
+        }
+        return context;
+    }
 }
diff --git a/handler/src/test/java/io/netty/handler/ssl/OpenSslKeyMaterialProviderTest.java b/handler/src/test/java/io/netty/handler/ssl/OpenSslKeyMaterialProviderTest.java
index 5b793fe..9b41196 100644
--- a/handler/src/test/java/io/netty/handler/ssl/OpenSslKeyMaterialProviderTest.java
+++ b/handler/src/test/java/io/netty/handler/ssl/OpenSslKeyMaterialProviderTest.java
@@ -15,13 +15,21 @@
  */
 package io.netty.handler.ssl;
 
+import io.netty.buffer.ByteBufAllocator;
 import io.netty.buffer.UnpooledByteBufAllocator;
+import io.netty.internal.tcnative.SSL;
+import io.netty.util.ReferenceCountUtil;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
 import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.X509KeyManager;
 
+import java.net.Socket;
 import java.security.KeyStore;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
 
 import static org.junit.Assert.*;
 import static org.junit.Assume.assumeTrue;
@@ -38,12 +46,16 @@ public class OpenSslKeyMaterialProviderTest {
     }
 
     protected KeyManagerFactory newKeyManagerFactory() throws Exception {
+       return newKeyManagerFactory(KeyManagerFactory.getDefaultAlgorithm());
+    }
+
+    protected KeyManagerFactory newKeyManagerFactory(String algorithm) throws Exception {
         char[] password = PASSWORD.toCharArray();
         final KeyStore keystore = KeyStore.getInstance("PKCS12");
         keystore.load(getClass().getResourceAsStream("mutual_auth_server.p12"), password);
 
         KeyManagerFactory kmf =
-                KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+                KeyManagerFactory.getInstance(algorithm);
         kmf.init(keystore, password);
         return kmf;
     }
@@ -72,4 +84,94 @@ public class OpenSslKeyMaterialProviderTest {
 
         provider.destroy();
     }
+
+    /**
+     * Test class used by testChooseOpenSslPrivateKeyMaterial().
+     */
+    private static final class SingleKeyManager implements X509KeyManager {
+        private final String keyAlias;
+        private final PrivateKey pk;
+        private final X509Certificate[] certChain;
+
+        SingleKeyManager(String keyAlias, PrivateKey pk, X509Certificate[] certChain) {
+            this.keyAlias = keyAlias;
+            this.pk = pk;
+            this.certChain = certChain;
+        }
+
+        @Override
+        public String[] getClientAliases(String keyType, Principal[] issuers) {
+            return new String[]{keyAlias};
+        }
+
+        @Override
+        public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
+            return keyAlias;
+        }
+
+        @Override
+        public String[] getServerAliases(String keyType, Principal[] issuers) {
+            return new String[]{keyAlias};
+        }
+
+        @Override
+        public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
+            return keyAlias;
+        }
+
+        @Override
+        public X509Certificate[] getCertificateChain(String alias) {
+            return certChain;
+        }
+
+        @Override
+        public PrivateKey getPrivateKey(String alias) {
+            return pk;
+        }
+    }
+
+    @Test
+    public void testChooseOpenSslPrivateKeyMaterial() throws Exception {
+        PrivateKey privateKey = SslContext.toPrivateKey(
+                getClass().getResourceAsStream("localhost_server.key"),
+                null);
+        assertNotNull(privateKey);
+        assertEquals("PKCS#8", privateKey.getFormat());
+        final X509Certificate[] certChain = SslContext.toX509Certificates(
+                getClass().getResourceAsStream("localhost_server.pem"));
+        assertNotNull(certChain);
+        PemEncoded pemKey = null;
+        long pkeyBio = 0L;
+        OpenSslPrivateKey sslPrivateKey;
+        try {
+            pemKey = PemPrivateKey.toPEM(ByteBufAllocator.DEFAULT, true, privateKey);
+            pkeyBio = ReferenceCountedOpenSslContext.toBIO(ByteBufAllocator.DEFAULT, pemKey.retain());
+            sslPrivateKey = new OpenSslPrivateKey(SSL.parsePrivateKey(pkeyBio, null));
+        } finally {
+            ReferenceCountUtil.safeRelease(pemKey);
+            if (pkeyBio != 0L) {
+                SSL.freeBIO(pkeyBio);
+            }
+        }
+        final String keyAlias = "key";
+
+        OpenSslKeyMaterialProvider provider = new OpenSslKeyMaterialProvider(
+                new SingleKeyManager(keyAlias, sslPrivateKey, certChain),
+                null);
+        OpenSslKeyMaterial material = provider.chooseKeyMaterial(ByteBufAllocator.DEFAULT, keyAlias);
+        assertNotNull(material);
+        assertEquals(2, sslPrivateKey.refCnt());
+        assertEquals(1, material.refCnt());
+        assertTrue(material.release());
+        assertEquals(1, sslPrivateKey.refCnt());
+        // Can get material multiple times from the same key
+        material = provider.chooseKeyMaterial(ByteBufAllocator.DEFAULT, keyAlias);
+        assertNotNull(material);
+        assertEquals(2, sslPrivateKey.refCnt());
+        assertTrue(material.release());
+        assertTrue(sslPrivateKey.release());
+        assertEquals(0, sslPrivateKey.refCnt());
+        assertEquals(0, material.refCnt());
+        assertEquals(0, ((OpenSslPrivateKey.OpenSslPrivateKeyMaterial) material).certificateChain);
+    }
 }
diff --git a/handler/src/test/java/io/netty/handler/ssl/OpenSslPrivateKeyMethodTest.java b/handler/src/test/java/io/netty/handler/ssl/OpenSslPrivateKeyMethodTest.java
new file mode 100644
index 0000000..ae830f6
--- /dev/null
+++ b/handler/src/test/java/io/netty/handler/ssl/OpenSslPrivateKeyMethodTest.java
@@ -0,0 +1,402 @@
+/*
+ * Copyright 2019 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.ssl;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.buffer.ByteBufAllocator;
+import io.netty.buffer.Unpooled;
+import io.netty.buffer.UnpooledByteBufAllocator;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelPipeline;
+import io.netty.channel.DefaultEventLoopGroup;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.channel.local.LocalAddress;
+import io.netty.channel.local.LocalChannel;
+import io.netty.channel.local.LocalServerChannel;
+import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
+import io.netty.handler.ssl.util.SelfSignedCertificate;
+import io.netty.util.ReferenceCountUtil;
+import io.netty.util.concurrent.Promise;
+import org.hamcrest.Matchers;
+import org.junit.AfterClass;
+import org.junit.Assume;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLHandshakeException;
+import java.net.SocketAddress;
+import java.security.NoSuchAlgorithmException;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.spec.MGF1ParameterSpec;
+import java.security.spec.PSSParameterSpec;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static io.netty.handler.ssl.OpenSslTestUtils.checkShouldUseKeyManagerFactory;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(Parameterized.class)
+public class OpenSslPrivateKeyMethodTest {
+    private static final String RFC_CIPHER_NAME = "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256";
+    private static EventLoopGroup GROUP;
+    private static SelfSignedCertificate CERT;
+    private static ExecutorService EXECUTOR;
+
+    @Parameters(name = "{index}: delegate = {0}")
+    public static Collection<Object[]> parameters() {
+        List<Object[]> dst = new ArrayList<Object[]>();
+        dst.add(new Object[] { true });
+        dst.add(new Object[] { false });
+        return dst;
+    }
+
+    @BeforeClass
+    public static void init() throws Exception {
+        checkShouldUseKeyManagerFactory();
+
+        Assume.assumeTrue(OpenSsl.isBoringSSL());
+        // Check if the cipher is supported at all which may not be the case for various JDK versions and OpenSSL API
+        // implementations.
+        assumeCipherAvailable(SslProvider.OPENSSL);
+        assumeCipherAvailable(SslProvider.JDK);
+
+        GROUP = new DefaultEventLoopGroup();
+        CERT = new SelfSignedCertificate();
+        EXECUTOR = Executors.newCachedThreadPool(new ThreadFactory() {
+            @Override
+            public Thread newThread(Runnable r) {
+                return new DelegateThread(r);
+            }
+        });
+    }
+
+    @AfterClass
+    public static void destroy() {
+        if (OpenSsl.isBoringSSL()) {
+            GROUP.shutdownGracefully();
+            CERT.delete();
+            EXECUTOR.shutdown();
+        }
+    }
+
+    private final boolean delegate;
+
+    public OpenSslPrivateKeyMethodTest(boolean delegate) {
+        this.delegate = delegate;
+    }
+
+    private static void assumeCipherAvailable(SslProvider provider) throws NoSuchAlgorithmException {
+        boolean cipherSupported = false;
+        if (provider == SslProvider.JDK) {
+            SSLEngine engine = SSLContext.getDefault().createSSLEngine();
+            for (String c: engine.getSupportedCipherSuites()) {
+               if (RFC_CIPHER_NAME.equals(c)) {
+                   cipherSupported = true;
+                   break;
+               }
+            }
+        } else {
+            cipherSupported = OpenSsl.isCipherSuiteAvailable(RFC_CIPHER_NAME);
+        }
+        Assume.assumeTrue("Unsupported cipher: " + RFC_CIPHER_NAME, cipherSupported);
+    }
+
+    private static SslHandler newSslHandler(SslContext sslCtx, ByteBufAllocator allocator, Executor executor) {
+        if (executor == null) {
+            return sslCtx.newHandler(allocator);
+        } else {
+            return sslCtx.newHandler(allocator, executor);
+        }
+    }
+
+    private SslContext buildServerContext(OpenSslPrivateKeyMethod method) throws Exception {
+        List<String> ciphers = Collections.singletonList(RFC_CIPHER_NAME);
+
+        final KeyManagerFactory kmf = OpenSslX509KeyManagerFactory.newKeyless(CERT.cert());
+
+        final SslContext sslServerContext = SslContextBuilder.forServer(kmf)
+                .sslProvider(SslProvider.OPENSSL)
+                .ciphers(ciphers)
+                // As this is not a TLSv1.3 cipher we should ensure we talk something else.
+                .protocols(SslUtils.PROTOCOL_TLS_V1_2)
+                .build();
+
+        ((OpenSslContext) sslServerContext).setPrivateKeyMethod(method);
+        return sslServerContext;
+    }
+
+    private SslContext buildClientContext()  throws Exception {
+        return SslContextBuilder.forClient()
+                .sslProvider(SslProvider.JDK)
+                .ciphers(Collections.singletonList(RFC_CIPHER_NAME))
+                // As this is not a TLSv1.3 cipher we should ensure we talk something else.
+                .protocols(SslUtils.PROTOCOL_TLS_V1_2)
+                .trustManager(InsecureTrustManagerFactory.INSTANCE)
+                .build();
+    }
+
+    private Executor delegateExecutor() {
+       return delegate ? EXECUTOR : null;
+    }
+
+    private void assertThread() {
+        if (delegate && OpenSslContext.USE_TASKS) {
+            assertEquals(DelegateThread.class, Thread.currentThread().getClass());
+        } else {
+            assertNotEquals(DelegateThread.class, Thread.currentThread().getClass());
+        }
+    }
+
+    @Test
+    public void testPrivateKeyMethod() throws Exception {
+        final AtomicBoolean signCalled = new AtomicBoolean();
+        final SslContext sslServerContext = buildServerContext(new OpenSslPrivateKeyMethod() {
+            @Override
+            public byte[] sign(SSLEngine engine, int signatureAlgorithm, byte[] input) throws Exception {
+                signCalled.set(true);
+                assertThread();
+
+                assertEquals(CERT.cert().getPublicKey(),
+                        engine.getSession().getLocalCertificates()[0].getPublicKey());
+
+                // Delegate signing to Java implementation.
+                final Signature signature;
+                // Depending on the Java version it will pick one or the other.
+                if (signatureAlgorithm == OpenSslPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA256) {
+                    signature = Signature.getInstance("SHA256withRSA");
+                } else if (signatureAlgorithm == OpenSslPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA256) {
+                    signature = Signature.getInstance("RSASSA-PSS");
+                    signature.setParameter(new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256,
+                            32, 1));
+                } else {
+                    throw new AssertionError("Unexpected signature algorithm " + signatureAlgorithm);
+                }
+                signature.initSign(CERT.key());
+                signature.update(input);
+                return signature.sign();
+            }
+
+            @Override
+            public byte[] decrypt(SSLEngine engine, byte[] input) {
+                throw new UnsupportedOperationException();
+            }
+        });
+
+        final SslContext sslClientContext = buildClientContext();
+        try {
+            try {
+                final Promise<Object> serverPromise = GROUP.next().newPromise();
+                final Promise<Object> clientPromise = GROUP.next().newPromise();
+
+                ChannelHandler serverHandler = new ChannelInitializer<Channel>() {
+                    @Override
+                    protected void initChannel(Channel ch) {
+                        ChannelPipeline pipeline = ch.pipeline();
+                        pipeline.addLast(newSslHandler(sslServerContext, ch.alloc(), delegateExecutor()));
+
+                        pipeline.addLast(new SimpleChannelInboundHandler<Object>() {
+                            @Override
+                            public void channelInactive(ChannelHandlerContext ctx) {
+                                serverPromise.cancel(true);
+                                ctx.fireChannelInactive();
+                            }
+
+                            @Override
+                            public void channelRead0(ChannelHandlerContext ctx, Object msg) {
+                                if (serverPromise.trySuccess(null)) {
+                                    ctx.writeAndFlush(Unpooled.wrappedBuffer(new byte[] {'P', 'O', 'N', 'G'}));
+                                }
+                                ctx.close();
+                            }
+
+                            @Override
+                            public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+                                if (!serverPromise.tryFailure(cause)) {
+                                    ctx.fireExceptionCaught(cause);
+                                }
+                            }
+                        });
+                    }
+                };
+
+                LocalAddress address = new LocalAddress("test-" + SslProvider.OPENSSL
+                        + '-' + SslProvider.JDK + '-' + RFC_CIPHER_NAME + '-' + delegate);
+
+                Channel server = server(address, serverHandler);
+                try {
+                    ChannelHandler clientHandler = new ChannelInitializer<Channel>() {
+                        @Override
+                        protected void initChannel(Channel ch) {
+                            ChannelPipeline pipeline = ch.pipeline();
+                            pipeline.addLast(newSslHandler(sslClientContext, ch.alloc(), delegateExecutor()));
+
+                            pipeline.addLast(new SimpleChannelInboundHandler<Object>() {
+                                @Override
+                                public void channelInactive(ChannelHandlerContext ctx) {
+                                    clientPromise.cancel(true);
+                                    ctx.fireChannelInactive();
+                                }
+
+                                @Override
+                                public void channelRead0(ChannelHandlerContext ctx, Object msg) {
+                                    clientPromise.trySuccess(null);
+                                    ctx.close();
+                                }
+
+                                @Override
+                                public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+                                    if (!clientPromise.tryFailure(cause)) {
+                                        ctx.fireExceptionCaught(cause);
+                                    }
+                                }
+                            });
+                        }
+                    };
+
+                    Channel client = client(server, clientHandler);
+                    try {
+                        client.writeAndFlush(Unpooled.wrappedBuffer(new byte[] {'P', 'I', 'N', 'G'}))
+                              .syncUninterruptibly();
+
+                        assertTrue("client timeout", clientPromise.await(5L, TimeUnit.SECONDS));
+                        assertTrue("server timeout", serverPromise.await(5L, TimeUnit.SECONDS));
+
+                        clientPromise.sync();
+                        serverPromise.sync();
+                        assertTrue(signCalled.get());
+                    } finally {
+                        client.close().sync();
+                    }
+                } finally {
+                    server.close().sync();
+                }
+            } finally {
+                ReferenceCountUtil.release(sslClientContext);
+            }
+        } finally {
+            ReferenceCountUtil.release(sslServerContext);
+        }
+    }
+
+    @Test
+    public void testPrivateKeyMethodFailsBecauseOfException() throws Exception {
+        testPrivateKeyMethodFails(false);
+    }
+
+    @Test
+    public void testPrivateKeyMethodFailsBecauseOfNull() throws Exception {
+        testPrivateKeyMethodFails(true);
+    }
+    private void testPrivateKeyMethodFails(final boolean returnNull) throws Exception {
+        final SslContext sslServerContext = buildServerContext(new OpenSslPrivateKeyMethod() {
+            @Override
+            public byte[] sign(SSLEngine engine, int signatureAlgorithm, byte[] input) throws Exception {
+                assertThread();
+                if (returnNull) {
+                    return null;
+                }
+                throw new SignatureException();
+            }
+
+            @Override
+            public byte[] decrypt(SSLEngine engine, byte[] input) {
+                throw new UnsupportedOperationException();
+            }
+        });
+        final SslContext sslClientContext = buildClientContext();
+
+        SslHandler serverSslHandler = newSslHandler(
+                sslServerContext, UnpooledByteBufAllocator.DEFAULT, delegateExecutor());
+        SslHandler clientSslHandler = newSslHandler(
+                sslClientContext, UnpooledByteBufAllocator.DEFAULT, delegateExecutor());
+
+        try {
+            try {
+                LocalAddress address = new LocalAddress("test-" + SslProvider.OPENSSL
+                        + '-' + SslProvider.JDK + '-' + RFC_CIPHER_NAME + '-' + delegate);
+
+                Channel server = server(address, serverSslHandler);
+                try {
+                    Channel client = client(server, clientSslHandler);
+                    try {
+                        Throwable clientCause = clientSslHandler.handshakeFuture().await().cause();
+                        Throwable serverCause = serverSslHandler.handshakeFuture().await().cause();
+                        assertNotNull(clientCause);
+                        assertThat(serverCause, Matchers.instanceOf(SSLHandshakeException.class));
+                    } finally {
+                        client.close().sync();
+                    }
+                } finally {
+                    server.close().sync();
+                }
+            } finally {
+                ReferenceCountUtil.release(sslClientContext);
+            }
+        } finally {
+            ReferenceCountUtil.release(sslServerContext);
+        }
+    }
+
+    private static Channel server(LocalAddress address, ChannelHandler handler) throws Exception {
+        ServerBootstrap bootstrap = new ServerBootstrap()
+                .channel(LocalServerChannel.class)
+                .group(GROUP)
+                .childHandler(handler);
+
+        return bootstrap.bind(address).sync().channel();
+    }
+
+    private static Channel client(Channel server, ChannelHandler handler) throws Exception {
+        SocketAddress remoteAddress = server.localAddress();
+
+        Bootstrap bootstrap = new Bootstrap()
+                .channel(LocalChannel.class)
+                .group(GROUP)
+                .handler(handler);
+
+        return bootstrap.connect(remoteAddress).sync().channel();
+    }
+
+    private static final class DelegateThread extends Thread {
+        DelegateThread(Runnable target) {
+            super(target);
+        }
+    }
+}
diff --git a/handler/src/test/java/io/netty/handler/ssl/PemEncodedTest.java b/handler/src/test/java/io/netty/handler/ssl/PemEncodedTest.java
index b2531eb..e6b36aa 100644
--- a/handler/src/test/java/io/netty/handler/ssl/PemEncodedTest.java
+++ b/handler/src/test/java/io/netty/handler/ssl/PemEncodedTest.java
@@ -26,7 +26,6 @@ import java.io.File;
 import java.io.FileInputStream;
 import java.security.PrivateKey;
 
-import io.netty.buffer.Unpooled;
 import io.netty.buffer.UnpooledByteBufAllocator;
 import org.junit.Test;
 
diff --git a/handler/src/test/java/io/netty/handler/ssl/PseudoRandomFunctionTest.java b/handler/src/test/java/io/netty/handler/ssl/PseudoRandomFunctionTest.java
new file mode 100644
index 0000000..30fc373
--- /dev/null
+++ b/handler/src/test/java/io/netty/handler/ssl/PseudoRandomFunctionTest.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2019 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.ssl;
+
+import io.netty.util.CharsetUtil;
+import org.bouncycastle.util.encoders.Hex;
+import org.junit.Test;
+
+import static org.junit.Assert.assertArrayEquals;
+
+/**
+ * The test vectors here were provided via:
+ * https://www.ietf.org/mail-archive/web/tls/current/msg03416.html
+ */
+public class PseudoRandomFunctionTest {
+
+    @Test
+    public void testPrfSha256() {
+        byte[] secret = Hex.decode("9b be 43 6b a9 40 f0 17 b1 76 52 84 9a 71 db 35");
+        byte[] seed = Hex.decode("a0 ba 9f 93 6c da 31 18 27 a6 f7 96 ff d5 19 8c");
+        byte[] label = "test label".getBytes(CharsetUtil.US_ASCII);
+        byte[] expected = Hex.decode(
+                 "e3 f2 29 ba 72 7b e1 7b" +
+                 "8d 12 26 20 55 7c d4 53" +
+                 "c2 aa b2 1d 07 c3 d4 95" +
+                 "32 9b 52 d4 e6 1e db 5a" +
+                 "6b 30 17 91 e9 0d 35 c9" +
+                 "c9 a4 6b 4e 14 ba f9 af" +
+                 "0f a0 22 f7 07 7d ef 17" +
+                 "ab fd 37 97 c0 56 4b ab" +
+                 "4f bc 91 66 6e 9d ef 9b" +
+                 "97 fc e3 4f 79 67 89 ba" +
+                 "a4 80 82 d1 22 ee 42 c5" +
+                 "a7 2e 5a 51 10 ff f7 01" +
+                 "87 34 7b 66");
+        byte[] actual = PseudoRandomFunction.hash(secret, label, seed, expected.length, "HmacSha256");
+        assertArrayEquals(expected, actual);
+    }
+}
diff --git a/handler/src/test/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngineTest.java b/handler/src/test/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngineTest.java
index 588619d..2e7c260 100644
--- a/handler/src/test/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngineTest.java
+++ b/handler/src/test/java/io/netty/handler/ssl/ReferenceCountedOpenSslEngineTest.java
@@ -15,16 +15,20 @@
  */
 package io.netty.handler.ssl;
 
+import io.netty.buffer.UnpooledByteBufAllocator;
 import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
 import io.netty.util.ReferenceCountUtil;
 import org.junit.Test;
 
 import javax.net.ssl.SSLEngine;
 
+import static junit.framework.TestCase.*;
+
 public class ReferenceCountedOpenSslEngineTest extends OpenSslEngineTest {
 
-    public ReferenceCountedOpenSslEngineTest(BufferType type, ProtocolCipherCombo combo) {
-        super(type, combo);
+    public ReferenceCountedOpenSslEngineTest(BufferType type, ProtocolCipherCombo combo, boolean delegate,
+                                             boolean useTasks) {
+        super(type, combo, delegate, useTasks);
     }
 
     @Override
@@ -59,13 +63,40 @@ public class ReferenceCountedOpenSslEngineTest extends OpenSslEngineTest {
 
     @Test(expected = NullPointerException.class)
     public void testNotLeakOnException() throws Exception {
-        clientSslCtx = SslContextBuilder.forClient()
+        clientSslCtx = wrapContext(SslContextBuilder.forClient()
                                         .trustManager(InsecureTrustManagerFactory.INSTANCE)
                                         .sslProvider(sslClientProvider())
                                         .protocols(protocols())
                                         .ciphers(ciphers())
-                                        .build();
+                                        .build());
 
         clientSslCtx.newEngine(null);
     }
+
+    @Override
+    protected SslContext wrapContext(SslContext context) {
+        if (context instanceof ReferenceCountedOpenSslContext) {
+            ((ReferenceCountedOpenSslContext) context).setUseTasks(useTasks);
+        }
+        return context;
+    }
+
+    @Test
+    public void parentContextIsRetainedByChildEngines() throws Exception {
+        SslContext clientSslCtx = SslContextBuilder.forClient()
+            .trustManager(InsecureTrustManagerFactory.INSTANCE)
+            .sslProvider(sslClientProvider())
+            .protocols(protocols())
+            .ciphers(ciphers())
+            .build();
+
+        SSLEngine engine = clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT);
+        assertEquals(ReferenceCountUtil.refCnt(clientSslCtx), 2);
+
+        cleanupClientSslContext(clientSslCtx);
+        assertEquals(ReferenceCountUtil.refCnt(clientSslCtx), 1);
+
+        cleanupClientSslEngine(engine);
+        assertEquals(ReferenceCountUtil.refCnt(clientSslCtx), 0);
+    }
 }
diff --git a/handler/src/test/java/io/netty/handler/ssl/SSLEngineTest.java b/handler/src/test/java/io/netty/handler/ssl/SSLEngineTest.java
index c1a4e12..a607498 100644
--- a/handler/src/test/java/io/netty/handler/ssl/SSLEngineTest.java
+++ b/handler/src/test/java/io/netty/handler/ssl/SSLEngineTest.java
@@ -36,6 +36,7 @@ import io.netty.channel.socket.nio.NioSocketChannel;
 import io.netty.handler.ssl.ApplicationProtocolConfig.Protocol;
 import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
 import io.netty.handler.ssl.util.SelfSignedCertificate;
+import io.netty.handler.ssl.util.SimpleTrustManagerFactory;
 import io.netty.util.CharsetUtil;
 import io.netty.util.NetUtil;
 import io.netty.util.ReferenceCountUtil;
@@ -45,6 +46,8 @@ import io.netty.util.concurrent.Promise;
 import io.netty.util.internal.EmptyArrays;
 import io.netty.util.internal.PlatformDependent;
 import io.netty.util.internal.StringUtil;
+import io.netty.util.internal.SystemPropertyUtil;
+import org.conscrypt.OpenSSLProvider;
 import org.junit.After;
 import org.junit.Assume;
 import org.junit.Before;
@@ -54,15 +57,23 @@ import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
 import java.io.ByteArrayInputStream;
+import java.io.Closeable;
 import java.io.File;
 import java.io.FileInputStream;
+import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.net.InetSocketAddress;
+import java.net.Socket;
 import java.nio.ByteBuffer;
 import java.nio.channels.ClosedChannelException;
 import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
 import java.security.Principal;
+import java.security.PrivateKey;
 import java.security.Provider;
+import java.security.UnrecoverableKeyException;
 import java.security.cert.Certificate;
 import java.security.cert.CertificateException;
 import java.util.ArrayList;
@@ -73,11 +84,18 @@ import java.util.List;
 import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
 
+import javax.crypto.SecretKey;
+import javax.net.ssl.ExtendedSSLSession;
+import javax.net.ssl.KeyManager;
 import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.KeyManagerFactorySpi;
 import javax.net.ssl.ManagerFactoryParameters;
 import javax.net.ssl.SNIHostName;
+import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLEngine;
 import javax.net.ssl.SSLEngineResult;
 import javax.net.ssl.SSLEngineResult.Status;
@@ -92,6 +110,8 @@ import javax.net.ssl.SSLSocketFactory;
 import javax.net.ssl.TrustManager;
 import javax.net.ssl.TrustManagerFactory;
 import javax.net.ssl.TrustManagerFactorySpi;
+import javax.net.ssl.X509ExtendedKeyManager;
+import javax.net.ssl.X509ExtendedTrustManager;
 import javax.net.ssl.X509TrustManager;
 import javax.security.cert.X509Certificate;
 
@@ -252,10 +272,13 @@ public abstract class SSLEngineTest {
 
     private final BufferType type;
     private final ProtocolCipherCombo protocolCipherCombo;
+    private final boolean delegate;
+    private ExecutorService delegatingExecutor;
 
-    protected SSLEngineTest(BufferType type, ProtocolCipherCombo protocolCipherCombo) {
+    protected SSLEngineTest(BufferType type, ProtocolCipherCombo protocolCipherCombo, boolean delegate) {
         this.type = type;
         this.protocolCipherCombo = protocolCipherCombo;
+        this.delegate = delegate;
     }
 
     protected ByteBuffer allocateBuffer(int len) {
@@ -441,6 +464,9 @@ public abstract class SSLEngineTest {
         MockitoAnnotations.initMocks(this);
         serverLatch = new CountDownLatch(1);
         clientLatch = new CountDownLatch(1);
+        if (delegate) {
+            delegatingExecutor = Executors.newCachedThreadPool();
+        }
     }
 
     @After
@@ -500,6 +526,10 @@ public abstract class SSLEngineTest {
             clientGroupShutdownFuture.sync();
         }
         serverException = null;
+
+        if (delegatingExecutor != null) {
+            delegatingExecutor.shutdown();
+        }
     }
 
     @Test
@@ -668,7 +698,7 @@ public abstract class SSLEngineTest {
                                    final boolean serverInitEngine)
             throws SSLException, InterruptedException {
         serverSslCtx =
-                SslContextBuilder.forServer(serverKMF)
+                wrapContext(SslContextBuilder.forServer(serverKMF)
                                  .protocols(protocols())
                                  .ciphers(ciphers())
                                  .sslProvider(sslServerProvider())
@@ -677,10 +707,10 @@ public abstract class SSLEngineTest {
                                  .clientAuth(clientAuth)
                                  .ciphers(null, IdentityCipherSuiteFilter.INSTANCE)
                                  .sessionCacheSize(0)
-                                 .sessionTimeout(0).build();
+                                 .sessionTimeout(0).build());
 
         clientSslCtx =
-                SslContextBuilder.forClient()
+                wrapContext(SslContextBuilder.forClient()
                                  .protocols(protocols())
                                  .ciphers(ciphers())
                                  .sslProvider(sslClientProvider())
@@ -689,7 +719,7 @@ public abstract class SSLEngineTest {
                                  .keyManager(clientKMF)
                                  .ciphers(null, IdentityCipherSuiteFilter.INSTANCE)
                                  .sessionCacheSize(0)
-                                 .sessionTimeout(0).build();
+                                 .sessionTimeout(0).build());
 
         serverConnectedChannel = null;
         sb = new ServerBootstrap();
@@ -703,7 +733,8 @@ public abstract class SSLEngineTest {
                 ch.config().setAllocator(new TestByteBufAllocator(ch.config().getAllocator(), type));
 
                 ChannelPipeline p = ch.pipeline();
-                SslHandler handler = serverSslCtx.newHandler(ch.alloc());
+                SslHandler handler = delegatingExecutor == null ? serverSslCtx.newHandler(ch.alloc()) :
+                        serverSslCtx.newHandler(ch.alloc(), delegatingExecutor);
                 if (serverInitEngine) {
                     mySetupMutualAuthServerInitSslHandler(handler);
                 }
@@ -746,7 +777,10 @@ public abstract class SSLEngineTest {
             protected void initChannel(Channel ch) throws Exception {
                 ch.config().setAllocator(new TestByteBufAllocator(ch.config().getAllocator(), type));
                 ChannelPipeline p = ch.pipeline();
-                p.addLast(clientSslCtx.newHandler(ch.alloc()));
+
+                SslHandler handler = delegatingExecutor == null ? clientSslCtx.newHandler(ch.alloc()) :
+                        clientSslCtx.newHandler(ch.alloc(), delegatingExecutor);
+                p.addLast(handler);
                 p.addLast(new MessageDelegatorChannelHandler(clientReceiver, clientLatch));
                 p.addLast(new ChannelInboundHandlerAdapter() {
                     @Override
@@ -816,7 +850,7 @@ public abstract class SSLEngineTest {
                                                  final boolean failureExpected)
             throws SSLException, InterruptedException {
         final String expectedHost = "localhost";
-        serverSslCtx = SslContextBuilder.forServer(serverCrtFile, serverKeyFile, null)
+        serverSslCtx = wrapContext(SslContextBuilder.forServer(serverCrtFile, serverKeyFile, null)
                 .sslProvider(sslServerProvider())
                 .protocols(protocols())
                 .ciphers(ciphers())
@@ -825,9 +859,9 @@ public abstract class SSLEngineTest {
                 .ciphers(null, IdentityCipherSuiteFilter.INSTANCE)
                 .sessionCacheSize(0)
                 .sessionTimeout(0)
-                .build();
+                .build());
 
-        clientSslCtx = SslContextBuilder.forClient()
+        clientSslCtx = wrapContext(SslContextBuilder.forClient()
                 .sslProvider(sslClientProvider())
                 .protocols(protocols())
                 .ciphers(ciphers())
@@ -836,7 +870,7 @@ public abstract class SSLEngineTest {
                 .ciphers(null, IdentityCipherSuiteFilter.INSTANCE)
                 .sessionCacheSize(0)
                 .sessionTimeout(0)
-                .build();
+                .build());
 
         serverConnectedChannel = null;
         sb = new ServerBootstrap();
@@ -849,7 +883,10 @@ public abstract class SSLEngineTest {
             protected void initChannel(Channel ch) throws Exception {
                 ch.config().setAllocator(new TestByteBufAllocator(ch.config().getAllocator(), type));
                 ChannelPipeline p = ch.pipeline();
-                p.addLast(serverSslCtx.newHandler(ch.alloc()));
+
+                SslHandler handler = delegatingExecutor == null ? serverSslCtx.newHandler(ch.alloc()) :
+                        serverSslCtx.newHandler(ch.alloc(), delegatingExecutor);
+                p.addLast(handler);
                 p.addLast(new MessageDelegatorChannelHandler(serverReceiver, serverLatch));
                 p.addLast(new ChannelInboundHandlerAdapter() {
                     @Override
@@ -889,7 +926,11 @@ public abstract class SSLEngineTest {
                 ch.config().setAllocator(new TestByteBufAllocator(ch.config().getAllocator(), type));
                 ChannelPipeline p = ch.pipeline();
                 InetSocketAddress remoteAddress = (InetSocketAddress) serverChannel.localAddress();
-                SslHandler sslHandler = clientSslCtx.newHandler(ch.alloc(), expectedHost, 0);
+
+                SslHandler sslHandler = delegatingExecutor == null ?
+                        clientSslCtx.newHandler(ch.alloc(), expectedHost, 0) :
+                        clientSslCtx.newHandler(ch.alloc(), expectedHost, 0,  delegatingExecutor);
+
                 SSLParameters parameters = sslHandler.engine().getSSLParameters();
                 if (SslUtils.isValidHostNameForSNI(expectedHost)) {
                     assertEquals(1, parameters.getServerNames().size());
@@ -979,7 +1020,7 @@ public abstract class SSLEngineTest {
             File clientTrustCrtFile, File clientKeyFile, final File clientCrtFile, String clientKeyPassword)
             throws InterruptedException, SSLException {
         serverSslCtx =
-                SslContextBuilder.forServer(serverCrtFile, serverKeyFile, serverKeyPassword)
+                wrapContext(SslContextBuilder.forServer(serverCrtFile, serverKeyFile, serverKeyPassword)
                                  .sslProvider(sslServerProvider())
                                  .sslContextProvider(serverSslContextProvider())
                                  .protocols(protocols())
@@ -987,9 +1028,9 @@ public abstract class SSLEngineTest {
                                  .trustManager(servertTrustCrtFile)
                                  .ciphers(null, IdentityCipherSuiteFilter.INSTANCE)
                                  .sessionCacheSize(0)
-                                 .sessionTimeout(0).build();
+                                 .sessionTimeout(0).build());
         clientSslCtx =
-                SslContextBuilder.forClient()
+                wrapContext(SslContextBuilder.forClient()
                                  .sslProvider(sslClientProvider())
                                  .sslContextProvider(clientSslContextProvider())
                                  .protocols(protocols())
@@ -998,7 +1039,7 @@ public abstract class SSLEngineTest {
                                  .keyManager(clientCrtFile, clientKeyFile, clientKeyPassword)
                                  .ciphers(null, IdentityCipherSuiteFilter.INSTANCE)
                                  .sessionCacheSize(0)
-                                 .sessionTimeout(0).build();
+                                 .sessionTimeout(0).build());
 
         serverConnectedChannel = null;
         sb = new ServerBootstrap();
@@ -1053,7 +1094,10 @@ public abstract class SSLEngineTest {
             protected void initChannel(Channel ch) throws Exception {
                 ch.config().setAllocator(new TestByteBufAllocator(ch.config().getAllocator(), type));
 
-                final SslHandler handler = clientSslCtx.newHandler(ch.alloc());
+                final SslHandler handler = delegatingExecutor == null ?
+                        clientSslCtx.newHandler(ch.alloc()) :
+                        clientSslCtx.newHandler(ch.alloc(), delegatingExecutor);
+
                 handler.engine().setNeedClientAuth(true);
                 ChannelPipeline p = ch.pipeline();
                 p.addLast(handler);
@@ -1143,9 +1187,9 @@ public abstract class SSLEngineTest {
 
     @Test
     public void testGetCreationTime() throws Exception {
-        clientSslCtx = SslContextBuilder.forClient()
+        clientSslCtx = wrapContext(SslContextBuilder.forClient()
                 .sslProvider(sslClientProvider())
-                .sslContextProvider(clientSslContextProvider()).build();
+                .sslContextProvider(clientSslContextProvider()).build());
         SSLEngine engine = null;
         try {
             engine = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
@@ -1157,20 +1201,20 @@ public abstract class SSLEngineTest {
 
     @Test
     public void testSessionInvalidate() throws Exception {
-        clientSslCtx = SslContextBuilder.forClient()
+        clientSslCtx = wrapContext(SslContextBuilder.forClient()
                 .trustManager(InsecureTrustManagerFactory.INSTANCE)
                 .sslProvider(sslClientProvider())
                 .sslContextProvider(clientSslContextProvider())
                 .protocols(protocols())
                 .ciphers(ciphers())
-                .build();
+                .build());
         SelfSignedCertificate ssc = new SelfSignedCertificate();
-        serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
+        serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
                 .sslProvider(sslServerProvider())
                 .sslContextProvider(serverSslContextProvider())
                 .protocols(protocols())
                 .ciphers(ciphers())
-                .build();
+                .build());
         SSLEngine clientEngine = null;
         SSLEngine serverEngine = null;
         try {
@@ -1191,20 +1235,20 @@ public abstract class SSLEngineTest {
 
     @Test
     public void testSSLSessionId() throws Exception {
-        clientSslCtx = SslContextBuilder.forClient()
+        clientSslCtx = wrapContext(SslContextBuilder.forClient()
                 .trustManager(InsecureTrustManagerFactory.INSTANCE)
                 .sslProvider(sslClientProvider())
                 // This test only works for non TLSv1.3 for now
                 .protocols(PROTOCOL_TLS_V1_2)
                 .sslContextProvider(clientSslContextProvider())
-                .build();
+                .build());
         SelfSignedCertificate ssc = new SelfSignedCertificate();
-        serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
+        serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
                 .sslProvider(sslServerProvider())
                 // This test only works for non TLSv1.3 for now
                 .protocols(PROTOCOL_TLS_V1_2)
                 .sslContextProvider(serverSslContextProvider())
-                .build();
+                .build());
         SSLEngine clientEngine = null;
         SSLEngine serverEngine = null;
         try {
@@ -1233,12 +1277,12 @@ public abstract class SSLEngineTest {
             throws CertificateException, SSLException, InterruptedException, ExecutionException {
         Assume.assumeTrue(PlatformDependent.javaVersion() >= 11);
         final SelfSignedCertificate ssc = new SelfSignedCertificate();
-        serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
+        serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
                                         .sslProvider(sslServerProvider())
                                         .sslContextProvider(serverSslContextProvider())
                                         .protocols(protocols())
                                         .ciphers(ciphers())
-                                        .build();
+                                        .build());
         sb = new ServerBootstrap()
                 .group(new NioEventLoopGroup(1))
                 .channel(NioServerSocketChannel.class)
@@ -1248,7 +1292,12 @@ public abstract class SSLEngineTest {
                         ch.config().setAllocator(new TestByteBufAllocator(ch.config().getAllocator(), type));
 
                         ChannelPipeline p = ch.pipeline();
-                        p.addLast(serverSslCtx.newHandler(ch.alloc()));
+
+                        SslHandler handler = delegatingExecutor == null ?
+                                serverSslCtx.newHandler(ch.alloc()) :
+                                serverSslCtx.newHandler(ch.alloc(), delegatingExecutor);
+
+                        p.addLast(handler);
                         p.addLast(new ChannelInboundHandlerAdapter() {
                             @Override
                             public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
@@ -1287,13 +1336,13 @@ public abstract class SSLEngineTest {
 
         serverChannel = sb.bind(new InetSocketAddress(0)).syncUninterruptibly().channel();
 
-        clientSslCtx = SslContextBuilder.forClient()
+        clientSslCtx = wrapContext(SslContextBuilder.forClient()
                                         // OpenSslEngine doesn't support renegotiation on client side
                                         .sslProvider(SslProvider.JDK)
                                         .trustManager(InsecureTrustManagerFactory.INSTANCE)
                                         .protocols(protocols())
                                         .ciphers(ciphers())
-                                        .build();
+                                        .build());
 
         cb = new Bootstrap();
         cb.group(new NioEventLoopGroup(1))
@@ -1304,7 +1353,11 @@ public abstract class SSLEngineTest {
                         ch.config().setAllocator(new TestByteBufAllocator(ch.config().getAllocator(), type));
 
                         ChannelPipeline p = ch.pipeline();
-                        SslHandler sslHandler = clientSslCtx.newHandler(ch.alloc());
+
+                        SslHandler sslHandler = delegatingExecutor == null ?
+                                clientSslCtx.newHandler(ch.alloc()) :
+                                clientSslCtx.newHandler(ch.alloc(), delegatingExecutor);
+
                         // The renegotiate is not expected to succeed, so we should stop trying in a timely manner so
                         // the unit test can terminate relativley quicly.
                         sslHandler.setHandshakeTimeout(1, TimeUnit.SECONDS);
@@ -1352,12 +1405,12 @@ public abstract class SSLEngineTest {
         try {
             File serverKeyFile = ResourcesUtil.getFile(getClass(), "test_unencrypted.pem");
             File serverCrtFile = ResourcesUtil.getFile(getClass(), "test.crt");
-            serverSslCtx = SslContextBuilder.forServer(serverCrtFile, serverKeyFile)
+            serverSslCtx = wrapContext(SslContextBuilder.forServer(serverCrtFile, serverKeyFile)
                                             .sslProvider(sslServerProvider())
                                             .sslContextProvider(serverSslContextProvider())
                                             .protocols(protocols())
                                             .ciphers(ciphers())
-                                            .build();
+                                            .build());
 
             sslEngine = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
 
@@ -1384,7 +1437,7 @@ public abstract class SSLEngineTest {
         }
     }
 
-    protected void handshake(SSLEngine clientEngine, SSLEngine serverEngine) throws SSLException {
+    protected void handshake(SSLEngine clientEngine, SSLEngine serverEngine) throws Exception {
         ByteBuffer cTOs = allocateBuffer(clientEngine.getSession().getPacketBufferSize());
         ByteBuffer sTOc = allocateBuffer(serverEngine.getSession().getPacketBufferSize());
 
@@ -1477,14 +1530,18 @@ public abstract class SSLEngineTest {
         return result.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.FINISHED;
     }
 
-    private static void runDelegatedTasks(SSLEngineResult result, SSLEngine engine) {
+    private void runDelegatedTasks(SSLEngineResult result, SSLEngine engine) throws Exception {
         if (result.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_TASK) {
             for (;;) {
                 Runnable task = engine.getDelegatedTask();
                 if (task == null) {
                     break;
                 }
-                task.run();
+                if (delegatingExecutor == null) {
+                    task.run();
+                } else {
+                    delegatingExecutor.submit(task).get();
+                }
             }
         }
     }
@@ -1562,7 +1619,7 @@ public abstract class SSLEngineTest {
                 clientCtxBuilder.protocols(PROTOCOL_TLS_V1_2);
             }
 
-            setupHandlers(serverCtxBuilder.build(), clientCtxBuilder.build());
+            setupHandlers(wrapContext(serverCtxBuilder.build()), wrapContext(clientCtxBuilder.build()));
         } finally {
           ssc.delete();
         }
@@ -1586,7 +1643,12 @@ public abstract class SSLEngineTest {
                 ch.config().setAllocator(new TestByteBufAllocator(ch.config().getAllocator(), type));
 
                 ChannelPipeline p = ch.pipeline();
-                p.addLast(serverSslCtx.newHandler(ch.alloc()));
+
+                SslHandler sslHandler = delegatingExecutor == null ?
+                        serverSslCtx.newHandler(ch.alloc()) :
+                        serverSslCtx.newHandler(ch.alloc(), delegatingExecutor);
+
+                p.addLast(sslHandler);
                 p.addLast(new MessageDelegatorChannelHandler(serverReceiver, serverLatch));
                 p.addLast(new ChannelInboundHandlerAdapter() {
                     @Override
@@ -1611,7 +1673,12 @@ public abstract class SSLEngineTest {
                 ch.config().setAllocator(new TestByteBufAllocator(ch.config().getAllocator(), type));
 
                 ChannelPipeline p = ch.pipeline();
-                p.addLast(clientSslCtx.newHandler(ch.alloc()));
+
+                SslHandler sslHandler = delegatingExecutor == null ?
+                        clientSslCtx.newHandler(ch.alloc()) :
+                        clientSslCtx.newHandler(ch.alloc(), delegatingExecutor);
+
+                p.addLast(sslHandler);
                 p.addLast(new MessageDelegatorChannelHandler(clientReceiver, clientLatch));
                 p.addLast(new ChannelInboundHandlerAdapter() {
                     @Override
@@ -1642,14 +1709,14 @@ public abstract class SSLEngineTest {
     @Test(timeout = 30000)
     public void testMutualAuthSameCertChain() throws Exception {
         serverSslCtx =
-                SslContextBuilder.forServer(
+                wrapContext(SslContextBuilder.forServer(
                         new ByteArrayInputStream(X509_CERT_PEM.getBytes(CharsetUtil.UTF_8)),
                         new ByteArrayInputStream(PRIVATE_KEY_PEM.getBytes(CharsetUtil.UTF_8)))
                                  .trustManager(new ByteArrayInputStream(X509_CERT_PEM.getBytes(CharsetUtil.UTF_8)))
                                  .clientAuth(ClientAuth.REQUIRE).sslProvider(sslServerProvider())
                                  .sslContextProvider(serverSslContextProvider())
                                  .protocols(protocols())
-                                 .ciphers(ciphers()).build();
+                                 .ciphers(ciphers()).build());
 
         sb = new ServerBootstrap();
         sb.group(new NioEventLoopGroup(), new NioEventLoopGroup());
@@ -1661,7 +1728,11 @@ public abstract class SSLEngineTest {
             protected void initChannel(Channel ch) throws Exception {
                 ch.config().setAllocator(new TestByteBufAllocator(ch.config().getAllocator(), type));
 
-                ch.pipeline().addFirst(serverSslCtx.newHandler(ch.alloc()));
+                SslHandler sslHandler = delegatingExecutor == null ?
+                        serverSslCtx.newHandler(ch.alloc()) :
+                        serverSslCtx.newHandler(ch.alloc(), delegatingExecutor);
+
+                ch.pipeline().addFirst(sslHandler);
                 ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                     @Override
                     public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
@@ -1701,13 +1772,13 @@ public abstract class SSLEngineTest {
         }).bind(new InetSocketAddress(0)).syncUninterruptibly().channel();
 
         clientSslCtx =
-                SslContextBuilder.forClient().keyManager(
+                wrapContext(SslContextBuilder.forClient().keyManager(
                         new ByteArrayInputStream(CLIENT_X509_CERT_CHAIN_PEM.getBytes(CharsetUtil.UTF_8)),
                         new ByteArrayInputStream(CLIENT_PRIVATE_KEY_PEM.getBytes(CharsetUtil.UTF_8)))
                 .trustManager(new ByteArrayInputStream(X509_CERT_PEM.getBytes(CharsetUtil.UTF_8)))
                 .sslProvider(sslClientProvider())
                 .sslContextProvider(clientSslContextProvider())
-                .protocols(protocols()).ciphers(ciphers()).build();
+                .protocols(protocols()).ciphers(ciphers()).build());
         cb = new Bootstrap();
         cb.group(new NioEventLoopGroup());
         cb.channel(NioSocketChannel.class);
@@ -1727,21 +1798,21 @@ public abstract class SSLEngineTest {
     public void testUnwrapBehavior() throws Exception {
         SelfSignedCertificate cert = new SelfSignedCertificate();
 
-        clientSslCtx = SslContextBuilder
+        clientSslCtx = wrapContext(SslContextBuilder
                 .forClient()
                 .trustManager(cert.cert())
                 .sslProvider(sslClientProvider())
                 .protocols(protocols())
                 .ciphers(ciphers())
-                .build();
+                .build());
         SSLEngine client = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
 
-        serverSslCtx = SslContextBuilder
+        serverSslCtx = wrapContext(SslContextBuilder
                 .forServer(cert.certificate(), cert.privateKey())
                 .sslProvider(sslServerProvider())
                 .protocols(protocols())
                 .ciphers(ciphers())
-                .build();
+                .build());
         SSLEngine server = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
 
         byte[] bytes = "Hello World".getBytes(CharsetUtil.US_ASCII);
@@ -1821,19 +1892,19 @@ public abstract class SSLEngineTest {
     private void testProtocol(String[] clientProtocols, String[] serverProtocols) throws Exception {
         SelfSignedCertificate cert = new SelfSignedCertificate();
 
-        clientSslCtx = SslContextBuilder
+        clientSslCtx = wrapContext(SslContextBuilder
                 .forClient()
                 .trustManager(cert.cert())
                 .sslProvider(sslClientProvider())
                 .protocols(clientProtocols)
-                .build();
+                .build());
         SSLEngine client = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
 
-        serverSslCtx = SslContextBuilder
+        serverSslCtx = wrapContext(SslContextBuilder
                 .forServer(cert.certificate(), cert.privateKey())
                 .sslProvider(sslServerProvider())
                 .protocols(serverProtocols)
-                .build();
+                .build());
         SSLEngine server = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
 
         try {
@@ -1851,18 +1922,18 @@ public abstract class SSLEngineTest {
         // Select a mandatory cipher from the TLSv1.2 RFC https://www.ietf.org/rfc/rfc5246.txt so handshakes won't fail
         // due to no shared/supported cipher.
         final String sharedCipher = "TLS_RSA_WITH_AES_128_CBC_SHA";
-        clientSslCtx = SslContextBuilder.forClient()
+        clientSslCtx = wrapContext(SslContextBuilder.forClient()
                 .trustManager(InsecureTrustManagerFactory.INSTANCE)
-                .ciphers(Arrays.asList(sharedCipher))
+                .ciphers(Collections.singletonList(sharedCipher))
                 .protocols(PROTOCOL_TLS_V1_2, PROTOCOL_TLS_V1)
                 .sslProvider(sslClientProvider())
-                .build();
+                .build());
 
-        serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
-                .ciphers(Arrays.asList(sharedCipher))
+        serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
+                .ciphers(Collections.singletonList(sharedCipher))
                 .protocols(PROTOCOL_TLS_V1_2, PROTOCOL_TLS_V1)
                 .sslProvider(sslServerProvider())
-                .build();
+                .build());
         SSLEngine clientEngine = null;
         SSLEngine serverEngine = null;
         try {
@@ -1882,18 +1953,18 @@ public abstract class SSLEngineTest {
         // Select a mandatory cipher from the TLSv1.2 RFC https://www.ietf.org/rfc/rfc5246.txt so handshakes won't fail
         // due to no shared/supported cipher.
         final String sharedCipher = "TLS_RSA_WITH_AES_128_CBC_SHA";
-        clientSslCtx = SslContextBuilder.forClient()
+        clientSslCtx = wrapContext(SslContextBuilder.forClient()
                 .trustManager(InsecureTrustManagerFactory.INSTANCE)
-                .ciphers(Arrays.asList(sharedCipher), SupportedCipherSuiteFilter.INSTANCE)
+                .ciphers(Collections.singletonList(sharedCipher), SupportedCipherSuiteFilter.INSTANCE)
                 .protocols(PROTOCOL_TLS_V1_2, PROTOCOL_TLS_V1)
                 .sslProvider(sslClientProvider())
-                .build();
+                .build());
 
-        serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
-                .ciphers(Arrays.asList(sharedCipher), SupportedCipherSuiteFilter.INSTANCE)
+        serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
+                .ciphers(Collections.singletonList(sharedCipher), SupportedCipherSuiteFilter.INSTANCE)
                 .protocols(PROTOCOL_TLS_V1_2, PROTOCOL_TLS_V1)
                 .sslProvider(sslServerProvider())
-                .build();
+                .build());
         SSLEngine clientEngine = null;
         SSLEngine serverEngine = null;
         try {
@@ -1911,21 +1982,21 @@ public abstract class SSLEngineTest {
     public void testPacketBufferSizeLimit() throws Exception {
         SelfSignedCertificate cert = new SelfSignedCertificate();
 
-        clientSslCtx = SslContextBuilder
+        clientSslCtx = wrapContext(SslContextBuilder
                 .forClient()
                 .trustManager(cert.cert())
                 .sslProvider(sslClientProvider())
                 .protocols(protocols())
                 .ciphers(ciphers())
-                .build();
+                .build());
         SSLEngine client = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
 
-        serverSslCtx = SslContextBuilder
+        serverSslCtx = wrapContext(SslContextBuilder
                 .forServer(cert.certificate(), cert.privateKey())
                 .sslProvider(sslServerProvider())
                 .protocols(protocols())
                 .ciphers(ciphers())
-                .build();
+                .build());
         SSLEngine server = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
 
         try {
@@ -1955,12 +2026,12 @@ public abstract class SSLEngineTest {
 
     @Test
     public void testSSLEngineUnwrapNoSslRecord() throws Exception {
-        clientSslCtx = SslContextBuilder
+        clientSslCtx = wrapContext(SslContextBuilder
                 .forClient()
                 .sslProvider(sslClientProvider())
                 .protocols(protocols())
                 .ciphers(ciphers())
-                .build();
+                .build());
         SSLEngine client = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
 
         try {
@@ -1985,12 +2056,12 @@ public abstract class SSLEngineTest {
 
     @Test
     public void testBeginHandshakeAfterEngineClosed() throws SSLException {
-        clientSslCtx = SslContextBuilder
+        clientSslCtx = wrapContext(SslContextBuilder
                 .forClient()
                 .sslProvider(sslClientProvider())
                 .protocols(protocols())
                 .ciphers(ciphers())
-                .build();
+                .build());
         SSLEngine client = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
 
         try {
@@ -2011,20 +2082,20 @@ public abstract class SSLEngineTest {
     public void testBeginHandshakeCloseOutbound() throws Exception {
         SelfSignedCertificate cert = new SelfSignedCertificate();
 
-        clientSslCtx = SslContextBuilder
+        clientSslCtx = wrapContext(SslContextBuilder
                 .forClient()
                 .sslProvider(sslClientProvider())
                 .protocols(protocols())
                 .ciphers(ciphers())
-                .build();
+                .build());
         SSLEngine client = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
 
-        serverSslCtx = SslContextBuilder
+        serverSslCtx = wrapContext(SslContextBuilder
                 .forServer(cert.certificate(), cert.privateKey())
                 .sslProvider(sslServerProvider())
                 .protocols(protocols())
                 .ciphers(ciphers())
-                .build();
+                .build());
         SSLEngine server = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
 
         try {
@@ -2062,20 +2133,20 @@ public abstract class SSLEngineTest {
     public void testCloseInboundAfterBeginHandshake() throws Exception {
         SelfSignedCertificate cert = new SelfSignedCertificate();
 
-        clientSslCtx = SslContextBuilder
+        clientSslCtx = wrapContext(SslContextBuilder
                 .forClient()
                 .sslProvider(sslClientProvider())
                 .protocols(protocols())
                 .ciphers(ciphers())
-                .build();
+                .build());
         SSLEngine client = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
 
-        serverSslCtx = SslContextBuilder
+        serverSslCtx = wrapContext(SslContextBuilder
                 .forServer(cert.certificate(), cert.privateKey())
                 .sslProvider(sslServerProvider())
                 .protocols(protocols())
                 .ciphers(ciphers())
-                .build();
+                .build());
         SSLEngine server = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
 
         try {
@@ -2102,21 +2173,21 @@ public abstract class SSLEngineTest {
     public void testCloseNotifySequence() throws Exception {
         SelfSignedCertificate cert = new SelfSignedCertificate();
 
-        clientSslCtx = SslContextBuilder
+        clientSslCtx = wrapContext(SslContextBuilder
                 .forClient()
                 .trustManager(cert.cert())
                 .sslProvider(sslClientProvider())
                 // This test only works for non TLSv1.3 for now
                 .protocols(PROTOCOL_TLS_V1_2)
-                .build();
+                .build());
         SSLEngine client = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
 
-        serverSslCtx = SslContextBuilder
+        serverSslCtx = wrapContext(SslContextBuilder
                 .forServer(cert.certificate(), cert.privateKey())
                 .sslProvider(sslServerProvider())
                 // This test only works for non TLSv1.3 for now
                 .protocols(PROTOCOL_TLS_V1_2)
-                .build();
+                .build());
         SSLEngine server = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
 
         try {
@@ -2142,7 +2213,14 @@ public abstract class SSLEngineTest {
 
             assertEquals(SSLEngineResult.Status.CLOSED, result.getStatus());
             // Need an UNWRAP to read the response of the close_notify
-            assertEquals(SSLEngineResult.HandshakeStatus.NEED_UNWRAP, result.getHandshakeStatus());
+            if (PlatformDependent.javaVersion() >= 12 && sslClientProvider() == SslProvider.JDK) {
+                // This is a workaround for a possible JDK12+ bug.
+                //
+                // See http://mail.openjdk.java.net/pipermail/security-dev/2019-February/019406.html.
+                assertEquals(SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING, result.getHandshakeStatus());
+            } else {
+                assertEquals(SSLEngineResult.HandshakeStatus.NEED_UNWRAP, result.getHandshakeStatus());
+            }
 
             int produced = result.bytesProduced();
             int consumed = result.bytesConsumed();
@@ -2207,7 +2285,7 @@ public abstract class SSLEngineTest {
             assertTrue(client.isOutboundDone());
             assertTrue(client.isInboundDone());
 
-            // Ensure that calling wrap or unwrap again will not produce a SSLException
+            // Ensure that calling wrap or unwrap again will not produce an SSLException
             encryptedServerToClient.clear();
             plainServerOut.clear();
 
@@ -2249,21 +2327,21 @@ public abstract class SSLEngineTest {
     public void testWrapAfterCloseOutbound() throws Exception {
         SelfSignedCertificate cert = new SelfSignedCertificate();
 
-        clientSslCtx = SslContextBuilder
+        clientSslCtx = wrapContext(SslContextBuilder
                 .forClient()
                 .trustManager(cert.cert())
                 .sslProvider(sslClientProvider())
                 .protocols(protocols())
                 .ciphers(ciphers())
-                .build();
+                .build());
         SSLEngine client = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
 
-        serverSslCtx = SslContextBuilder
+        serverSslCtx = wrapContext(SslContextBuilder
                 .forServer(cert.certificate(), cert.privateKey())
                 .sslProvider(sslServerProvider())
                 .protocols(protocols())
                 .ciphers(ciphers())
-                .build();
+                .build());
         SSLEngine server = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
 
         try {
@@ -2292,21 +2370,21 @@ public abstract class SSLEngineTest {
     public void testMultipleRecordsInOneBufferWithNonZeroPosition() throws Exception {
         SelfSignedCertificate cert = new SelfSignedCertificate();
 
-        clientSslCtx = SslContextBuilder
+        clientSslCtx = wrapContext(SslContextBuilder
                 .forClient()
                 .trustManager(cert.cert())
                 .sslProvider(sslClientProvider())
                 .protocols(protocols())
                 .ciphers(ciphers())
-                .build();
+                .build());
         SSLEngine client = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
 
-        serverSslCtx = SslContextBuilder
+        serverSslCtx = wrapContext(SslContextBuilder
                 .forServer(cert.certificate(), cert.privateKey())
                 .sslProvider(sslServerProvider())
                 .protocols(protocols())
                 .ciphers(ciphers())
-                .build();
+                .build());
         SSLEngine server = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
 
         try {
@@ -2371,21 +2449,21 @@ public abstract class SSLEngineTest {
     public void testMultipleRecordsInOneBufferBiggerThenPacketBufferSize() throws Exception {
         SelfSignedCertificate cert = new SelfSignedCertificate();
 
-        clientSslCtx = SslContextBuilder
+        clientSslCtx = wrapContext(SslContextBuilder
                 .forClient()
                 .trustManager(cert.cert())
                 .sslProvider(sslClientProvider())
                 .protocols(protocols())
                 .ciphers(ciphers())
-                .build();
+                .build());
         SSLEngine client = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
 
-        serverSslCtx = SslContextBuilder
+        serverSslCtx = wrapContext(SslContextBuilder
                 .forServer(cert.certificate(), cert.privateKey())
                 .sslProvider(sslServerProvider())
                 .protocols(protocols())
                 .ciphers(ciphers())
-                .build();
+                .build());
         SSLEngine server = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
 
         try {
@@ -2439,21 +2517,21 @@ public abstract class SSLEngineTest {
     public void testBufferUnderFlow() throws Exception {
         SelfSignedCertificate cert = new SelfSignedCertificate();
 
-        clientSslCtx = SslContextBuilder
+        clientSslCtx = wrapContext(SslContextBuilder
                 .forClient()
                 .trustManager(cert.cert())
                 .sslProvider(sslClientProvider())
                 .protocols(protocols())
                 .ciphers(ciphers())
-                .build();
+                .build());
         SSLEngine client = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
 
-        serverSslCtx = SslContextBuilder
+        serverSslCtx = wrapContext(SslContextBuilder
                 .forServer(cert.certificate(), cert.privateKey())
                 .sslProvider(sslServerProvider())
                 .protocols(protocols())
                 .ciphers(ciphers())
-                .build();
+                .build());
         SSLEngine server = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
 
         try {
@@ -2514,21 +2592,21 @@ public abstract class SSLEngineTest {
     public void testWrapDoesNotZeroOutSrc() throws Exception {
         SelfSignedCertificate cert = new SelfSignedCertificate();
 
-        clientSslCtx = SslContextBuilder
+        clientSslCtx = wrapContext(SslContextBuilder
                 .forClient()
                 .trustManager(cert.cert())
                 .sslProvider(sslClientProvider())
                 .protocols(protocols())
                 .ciphers(ciphers())
-                .build();
+                .build());
         SSLEngine client = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
 
-        serverSslCtx = SslContextBuilder
+        serverSslCtx = wrapContext(SslContextBuilder
                 .forServer(cert.certificate(), cert.privateKey())
                 .sslProvider(sslServerProvider())
                 .protocols(protocols())
                 .ciphers(ciphers())
-                .build();
+                .build());
         SSLEngine server = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
 
         try {
@@ -2571,12 +2649,12 @@ public abstract class SSLEngineTest {
     private void testDisableProtocols(String protocol, String... disabledProtocols) throws Exception {
         SelfSignedCertificate cert = new SelfSignedCertificate();
 
-        SslContext ctx = SslContextBuilder
+        SslContext ctx = wrapContext(SslContextBuilder
                 .forServer(cert.certificate(), cert.privateKey())
                 .sslProvider(sslServerProvider())
                 .protocols(protocols())
                 .ciphers(ciphers())
-                .build();
+                .build());
         SSLEngine server = wrapEngine(ctx.newEngine(UnpooledByteBufAllocator.DEFAULT));
 
         try {
@@ -2606,13 +2684,8 @@ public abstract class SSLEngineTest {
     @Test
     public void testUsingX509TrustManagerVerifiesHostname() throws Exception {
         SslProvider clientProvider = sslClientProvider();
-        if (clientProvider == SslProvider.OPENSSL || clientProvider == SslProvider.OPENSSL_REFCNT) {
-            // Need to check if we support hostname validation in the current used OpenSSL version before running
-            // the test.
-            Assume.assumeTrue(OpenSsl.supportsHostnameValidation());
-        }
         SelfSignedCertificate cert = new SelfSignedCertificate();
-        clientSslCtx = SslContextBuilder
+        clientSslCtx = wrapContext(SslContextBuilder
                 .forClient()
                 .trustManager(new TrustManagerFactory(new TrustManagerFactorySpi() {
                     @Override
@@ -2650,17 +2723,17 @@ public abstract class SSLEngineTest {
                 }, null, TrustManagerFactory.getDefaultAlgorithm()) {
                 })
                 .sslProvider(sslClientProvider())
-                .build();
+                .build());
 
         SSLEngine client = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT, "netty.io", 1234));
         SSLParameters sslParameters = client.getSSLParameters();
         sslParameters.setEndpointIdentificationAlgorithm("HTTPS");
         client.setSSLParameters(sslParameters);
 
-        serverSslCtx = SslContextBuilder
+        serverSslCtx = wrapContext(SslContextBuilder
                 .forServer(cert.certificate(), cert.privateKey())
                 .sslProvider(sslServerProvider())
-                .build();
+                .build());
 
         SSLEngine server = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
         try {
@@ -2683,8 +2756,9 @@ public abstract class SSLEngineTest {
         cipherList.add("InvalidCipher");
         SSLEngine server = null;
         try {
-            serverSslCtx = SslContextBuilder.forServer(cert.key(), cert.cert()).sslProvider(sslClientProvider())
-                                            .ciphers(cipherList).build();
+            serverSslCtx = wrapContext(SslContextBuilder.forServer(cert.key(), cert.cert())
+                    .sslProvider(sslClientProvider())
+                    .ciphers(cipherList).build());
             server = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
             fail();
         } catch (IllegalArgumentException expected) {
@@ -2699,20 +2773,20 @@ public abstract class SSLEngineTest {
 
     @Test
     public void testGetCiphersuite() throws Exception {
-        clientSslCtx = SslContextBuilder.forClient()
+        clientSslCtx = wrapContext(SslContextBuilder.forClient()
                                         .trustManager(InsecureTrustManagerFactory.INSTANCE)
                                         .sslProvider(sslClientProvider())
                                         .sslContextProvider(clientSslContextProvider())
                                         .protocols(protocols())
                                         .ciphers(ciphers())
-                                        .build();
+                                        .build());
         SelfSignedCertificate ssc = new SelfSignedCertificate();
-        serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
+        serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
                                         .sslProvider(sslServerProvider())
                                         .sslContextProvider(serverSslContextProvider())
                                         .protocols(protocols())
                                         .ciphers(ciphers())
-                                        .build();
+                                        .build());
         SSLEngine clientEngine = null;
         SSLEngine serverEngine = null;
         try {
@@ -2734,20 +2808,20 @@ public abstract class SSLEngineTest {
 
     @Test
     public void testSessionBindingEvent() throws Exception {
-        clientSslCtx = SslContextBuilder.forClient()
+        clientSslCtx = wrapContext(SslContextBuilder.forClient()
                 .trustManager(InsecureTrustManagerFactory.INSTANCE)
                 .sslProvider(sslClientProvider())
                 .sslContextProvider(clientSslContextProvider())
                 .protocols(protocols())
                 .ciphers(ciphers())
-                .build();
+                .build());
         SelfSignedCertificate ssc = new SelfSignedCertificate();
-        serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
+        serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
                 .sslProvider(sslServerProvider())
                 .sslContextProvider(serverSslContextProvider())
                 .protocols(protocols())
                 .ciphers(ciphers())
-                .build();
+                .build());
         SSLEngine clientEngine = null;
         SSLEngine serverEngine = null;
         try {
@@ -2836,7 +2910,7 @@ public abstract class SSLEngineTest {
         SelfSignedCertificate ssc = new SelfSignedCertificate();
         KeyManagerFactory kmf = useKeyManagerFactory ?
                 SslContext.buildKeyManagerFactory(
-                        new java.security.cert.X509Certificate[] { ssc.cert()}, ssc.key(), null, null) : null;
+                        new java.security.cert.X509Certificate[] { ssc.cert()}, ssc.key(), null, null, null) : null;
 
         SslContextBuilder clientContextBuilder = SslContextBuilder.forClient();
         if (mutualAuth) {
@@ -2846,13 +2920,13 @@ public abstract class SSLEngineTest {
                 clientContextBuilder.keyManager(ssc.key(), ssc.cert());
             }
         }
-        clientSslCtx = clientContextBuilder
+        clientSslCtx = wrapContext(clientContextBuilder
                                         .trustManager(InsecureTrustManagerFactory.INSTANCE)
                                         .sslProvider(sslClientProvider())
                                         .sslContextProvider(clientSslContextProvider())
                                         .protocols(protocols())
                                         .ciphers(ciphers())
-                                        .build();
+                                        .build());
 
         SslContextBuilder serverContextBuilder = kmf != null ?
                 SslContextBuilder.forServer(kmf) :
@@ -2860,12 +2934,12 @@ public abstract class SSLEngineTest {
         if (mutualAuth) {
             serverContextBuilder.clientAuth(ClientAuth.REQUIRE);
         }
-        serverSslCtx = serverContextBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE)
+        serverSslCtx = wrapContext(serverContextBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE)
                                      .sslProvider(sslServerProvider())
                                      .sslContextProvider(serverSslContextProvider())
                                      .protocols(protocols())
                                      .ciphers(ciphers())
-                                     .build();
+                                     .build());
         SSLEngine clientEngine = null;
         SSLEngine serverEngine = null;
         try {
@@ -2989,10 +3063,361 @@ public abstract class SSLEngineTest {
         }
     }
 
+    @Test
+    public void testSupportedSignatureAlgorithms() throws Exception {
+        final SelfSignedCertificate ssc = new SelfSignedCertificate();
+
+        final class TestKeyManagerFactory extends KeyManagerFactory {
+            TestKeyManagerFactory(final KeyManagerFactory factory) {
+                super(new KeyManagerFactorySpi() {
+
+                    private final KeyManager[] managers = factory.getKeyManagers();
+
+                    @Override
+                    protected void engineInit(KeyStore keyStore, char[] chars)  {
+                        throw new UnsupportedOperationException();
+                    }
+
+                    @Override
+                    protected void engineInit(ManagerFactoryParameters managerFactoryParameters) {
+                        throw new UnsupportedOperationException();
+                    }
+
+                    @Override
+                    protected KeyManager[] engineGetKeyManagers() {
+                        KeyManager[] array = new KeyManager[managers.length];
+
+                        for (int i = 0 ; i < array.length; i++) {
+                            final X509ExtendedKeyManager x509ExtendedKeyManager = (X509ExtendedKeyManager) managers[i];
+
+                            array[i] = new X509ExtendedKeyManager() {
+                                @Override
+                                public String[] getClientAliases(String s, Principal[] principals) {
+                                    fail();
+                                    return null;
+                                }
+
+                                @Override
+                                public String chooseClientAlias(
+                                        String[] strings, Principal[] principals, Socket socket) {
+                                    fail();
+                                    return null;
+                                }
+
+                                @Override
+                                public String[] getServerAliases(String s, Principal[] principals) {
+                                    fail();
+                                    return null;
+                                }
+
+                                @Override
+                                public String chooseServerAlias(String s, Principal[] principals, Socket socket) {
+                                    fail();
+                                    return null;
+                                }
+
+                                @Override
+                                public String chooseEngineClientAlias(
+                                        String[] strings, Principal[] principals, SSLEngine sslEngine) {
+                                    assertNotEquals(0, ((ExtendedSSLSession) sslEngine.getHandshakeSession())
+                                            .getPeerSupportedSignatureAlgorithms().length);
+                                    assertNotEquals(0, ((ExtendedSSLSession) sslEngine.getHandshakeSession())
+                                            .getLocalSupportedSignatureAlgorithms().length);
+                                    return x509ExtendedKeyManager.chooseEngineClientAlias(
+                                            strings, principals, sslEngine);
+                                }
+
+                                @Override
+                                public String chooseEngineServerAlias(
+                                        String s, Principal[] principals, SSLEngine sslEngine) {
+                                    assertNotEquals(0, ((ExtendedSSLSession) sslEngine.getHandshakeSession())
+                                            .getPeerSupportedSignatureAlgorithms().length);
+                                    assertNotEquals(0, ((ExtendedSSLSession) sslEngine.getHandshakeSession())
+                                            .getLocalSupportedSignatureAlgorithms().length);
+                                    return x509ExtendedKeyManager.chooseEngineServerAlias(s, principals, sslEngine);
+                                }
+
+                                @Override
+                                public java.security.cert.X509Certificate[] getCertificateChain(String s) {
+                                    return x509ExtendedKeyManager.getCertificateChain(s);
+                                }
+
+                                @Override
+                                public PrivateKey getPrivateKey(String s) {
+                                    return x509ExtendedKeyManager.getPrivateKey(s);
+                                }
+                            };
+                        }
+                        return array;
+                    }
+                }, factory.getProvider(), factory.getAlgorithm());
+            }
+        }
+
+        clientSslCtx = wrapContext(SslContextBuilder.forClient()
+                .keyManager(new TestKeyManagerFactory(newKeyManagerFactory(ssc)))
+                .trustManager(InsecureTrustManagerFactory.INSTANCE)
+                .sslProvider(sslClientProvider())
+                .sslContextProvider(clientSslContextProvider())
+                .protocols(protocols())
+                .ciphers(ciphers())
+                .build());
+
+        serverSslCtx = wrapContext(SslContextBuilder.forServer(new TestKeyManagerFactory(newKeyManagerFactory(ssc)))
+                .trustManager(InsecureTrustManagerFactory.INSTANCE)
+                .sslContextProvider(serverSslContextProvider())
+                .sslProvider(sslServerProvider())
+                .protocols(protocols())
+                .ciphers(ciphers())
+                .clientAuth(ClientAuth.REQUIRE)
+                .build());
+        SSLEngine clientEngine = null;
+        SSLEngine serverEngine = null;
+        try {
+            clientEngine = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
+            serverEngine = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
+            handshake(clientEngine, serverEngine);
+        } finally {
+            cleanupClientSslEngine(clientEngine);
+            cleanupServerSslEngine(serverEngine);
+            ssc.delete();
+        }
+    }
+
+    @Test
+    public void testHandshakeSession() throws Exception {
+        final SelfSignedCertificate ssc = new SelfSignedCertificate();
+
+        final TestTrustManagerFactory clientTmf = new TestTrustManagerFactory(ssc.cert());
+        final TestTrustManagerFactory serverTmf = new TestTrustManagerFactory(ssc.cert());
+
+        clientSslCtx = wrapContext(SslContextBuilder.forClient()
+                .trustManager(new SimpleTrustManagerFactory() {
+                    @Override
+                    protected void engineInit(KeyStore keyStore) {
+                        // NOOP
+                    }
+
+                    @Override
+                    protected void engineInit(ManagerFactoryParameters managerFactoryParameters) {
+                        // NOOP
+                    }
+
+                    @Override
+                    protected TrustManager[] engineGetTrustManagers() {
+                        return new TrustManager[] { clientTmf };
+                    }
+                })
+                .keyManager(newKeyManagerFactory(ssc))
+                .sslProvider(sslClientProvider())
+                .sslContextProvider(clientSslContextProvider())
+                .protocols(protocols())
+                .ciphers(ciphers())
+                .build());
+        serverSslCtx = wrapContext(SslContextBuilder.forServer(newKeyManagerFactory(ssc))
+                .trustManager(new SimpleTrustManagerFactory() {
+                    @Override
+                    protected void engineInit(KeyStore keyStore) {
+                        // NOOP
+                    }
+
+                    @Override
+                    protected void engineInit(ManagerFactoryParameters managerFactoryParameters) {
+                        // NOOP
+                    }
+
+                    @Override
+                    protected TrustManager[] engineGetTrustManagers() {
+                        return new TrustManager[] { serverTmf };
+                    }
+                })
+                .sslProvider(sslServerProvider())
+                .sslContextProvider(serverSslContextProvider())
+                .protocols(protocols())
+                .ciphers(ciphers())
+                .clientAuth(ClientAuth.REQUIRE)
+                .build());
+        SSLEngine clientEngine = null;
+        SSLEngine serverEngine = null;
+        try {
+            clientEngine = wrapEngine(clientSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
+            serverEngine = wrapEngine(serverSslCtx.newEngine(UnpooledByteBufAllocator.DEFAULT));
+            handshake(clientEngine, serverEngine);
+
+            assertTrue(clientTmf.isVerified());
+            assertTrue(serverTmf.isVerified());
+        } finally {
+            cleanupClientSslEngine(clientEngine);
+            cleanupServerSslEngine(serverEngine);
+            ssc.delete();
+        }
+    }
+
+    @Test
+    public void testMasterKeyLogging() throws Exception {
+
+        /*
+         * At the moment master key logging is not supported for conscrypt
+         */
+        Assume.assumeFalse(serverSslContextProvider() instanceof OpenSSLProvider);
+
+        /*
+         * The JDK SSL engine master key retrieval relies on being able to set field access to true.
+         * That is not available in JDK9+
+         */
+        Assume.assumeFalse(sslServerProvider() == SslProvider.JDK && PlatformDependent.javaVersion() > 8);
+
+        String originalSystemPropertyValue = SystemPropertyUtil.get(SslMasterKeyHandler.SYSTEM_PROP_KEY);
+        System.setProperty(SslMasterKeyHandler.SYSTEM_PROP_KEY, Boolean.TRUE.toString());
+
+        SelfSignedCertificate ssc = new SelfSignedCertificate();
+        serverSslCtx = wrapContext(SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
+                .sslProvider(sslServerProvider())
+                .sslContextProvider(serverSslContextProvider())
+                .build());
+        Socket socket = null;
+
+        try {
+            sb = new ServerBootstrap();
+            sb.group(new NioEventLoopGroup(), new NioEventLoopGroup());
+            sb.channel(NioServerSocketChannel.class);
+
+            final Promise<SecretKey> promise = sb.config().group().next().newPromise();
+            serverChannel = sb.childHandler(new ChannelInitializer<Channel>() {
+                @Override
+                protected void initChannel(Channel ch) throws Exception {
+                    ch.config().setAllocator(new TestByteBufAllocator(ch.config().getAllocator(), type));
+
+                    SslHandler sslHandler = delegatingExecutor == null ?
+                            serverSslCtx.newHandler(ch.alloc()) :
+                            serverSslCtx.newHandler(ch.alloc(), delegatingExecutor);
+
+                    ch.pipeline().addLast(sslHandler);
+                    ch.pipeline().addLast(new SslMasterKeyHandler() {
+                        @Override
+                        protected void accept(SecretKey masterKey, SSLSession session) {
+                            promise.setSuccess(masterKey);
+                        }
+                    });
+                    serverConnectedChannel = ch;
+                }
+            }).bind(new InetSocketAddress(0)).sync().channel();
+
+            int port = ((InetSocketAddress) serverChannel.localAddress()).getPort();
+
+            SSLContext sslContext = SSLContext.getInstance("TLS");
+            sslContext.init(null, InsecureTrustManagerFactory.INSTANCE.getTrustManagers(), null);
+            socket = sslContext.getSocketFactory().createSocket(NetUtil.LOCALHOST, port);
+            OutputStream out = socket.getOutputStream();
+            out.write(1);
+            out.flush();
+
+            assertTrue(promise.await(10, TimeUnit.SECONDS));
+            SecretKey key = promise.get();
+            assertEquals("AES secret key must be 48 bytes", 48, key.getEncoded().length);
+        } finally {
+            closeQuietly(socket);
+            if (originalSystemPropertyValue != null) {
+                System.setProperty(SslMasterKeyHandler.SYSTEM_PROP_KEY, originalSystemPropertyValue);
+            } else {
+                System.clearProperty(SslMasterKeyHandler.SYSTEM_PROP_KEY);
+            }
+            ssc.delete();
+        }
+    }
+
+    private static void closeQuietly(Closeable c) {
+        if (c != null) {
+            try {
+                c.close();
+            } catch (IOException ignore) {
+                // ignore
+            }
+        }
+    }
+
+    private KeyManagerFactory newKeyManagerFactory(SelfSignedCertificate ssc)
+            throws UnrecoverableKeyException, KeyStoreException, NoSuchAlgorithmException,
+            CertificateException, IOException {
+        return SslContext.buildKeyManagerFactory(
+                new java.security.cert.X509Certificate[] { ssc.cert() }, ssc.key(), null, null, null);
+    }
+
+    private final class TestTrustManagerFactory extends X509ExtendedTrustManager {
+        private final Certificate localCert;
+        private volatile boolean verified;
+
+        TestTrustManagerFactory(Certificate localCert) {
+            this.localCert = localCert;
+        }
+
+        boolean isVerified() {
+            return verified;
+        }
+
+        @Override
+        public void checkClientTrusted(
+                java.security.cert.X509Certificate[] x509Certificates, String s, Socket socket) {
+            fail();
+        }
+
+        @Override
+        public void checkServerTrusted(
+                java.security.cert.X509Certificate[] x509Certificates, String s, Socket socket) {
+            fail();
+        }
+
+        @Override
+        public void checkClientTrusted(
+                java.security.cert.X509Certificate[] x509Certificates, String s, SSLEngine sslEngine) {
+            verified = true;
+            assertFalse(sslEngine.getUseClientMode());
+            SSLSession session = sslEngine.getHandshakeSession();
+            assertNotNull(session);
+            Certificate[] localCertificates = session.getLocalCertificates();
+            assertNotNull(localCertificates);
+            assertEquals(1, localCertificates.length);
+            assertEquals(localCert, localCertificates[0]);
+            assertNotNull(session.getLocalPrincipal());
+        }
+
+        @Override
+        public void checkServerTrusted(
+                java.security.cert.X509Certificate[] x509Certificates, String s, SSLEngine sslEngine) {
+            verified = true;
+            assertTrue(sslEngine.getUseClientMode());
+            SSLSession session = sslEngine.getHandshakeSession();
+            assertNotNull(session);
+            assertNull(session.getLocalCertificates());
+            assertNull(session.getLocalPrincipal());
+        }
+
+        @Override
+        public void checkClientTrusted(
+                java.security.cert.X509Certificate[] x509Certificates, String s) {
+            fail();
+        }
+
+        @Override
+        public void checkServerTrusted(
+                java.security.cert.X509Certificate[] x509Certificates, String s) {
+            fail();
+        }
+
+        @Override
+        public java.security.cert.X509Certificate[] getAcceptedIssuers() {
+            return EmptyArrays.EMPTY_X509_CERTIFICATES;
+        }
+    }
+
     protected SSLEngine wrapEngine(SSLEngine engine) {
         return engine;
     }
 
+    protected SslContext wrapContext(SslContext context) {
+        return context;
+    }
+
     protected List<String> ciphers() {
         return Collections.singletonList(protocolCipherCombo.cipher);
     }
diff --git a/handler/src/test/java/io/netty/handler/ssl/SniClientJava8TestUtil.java b/handler/src/test/java/io/netty/handler/ssl/SniClientJava8TestUtil.java
index 4db7c7b..d4bfe25 100644
--- a/handler/src/test/java/io/netty/handler/ssl/SniClientJava8TestUtil.java
+++ b/handler/src/test/java/io/netty/handler/ssl/SniClientJava8TestUtil.java
@@ -260,7 +260,7 @@ final class SniClientJava8TestUtil {
                    IOException, CertificateException {
         return new SniX509KeyManagerFactory(
                 new SNIHostName(hostname), SslContext.buildKeyManagerFactory(
-                new X509Certificate[] { cert.cert() }, cert.key(), null, null));
+                new X509Certificate[] { cert.cert() }, cert.key(), null, null, null));
     }
 
     private static final class SniX509KeyManagerFactory extends KeyManagerFactory {
diff --git a/handler/src/test/java/io/netty/handler/ssl/SniClientTest.java b/handler/src/test/java/io/netty/handler/ssl/SniClientTest.java
index 56ea815..d959e87 100644
--- a/handler/src/test/java/io/netty/handler/ssl/SniClientTest.java
+++ b/handler/src/test/java/io/netty/handler/ssl/SniClientTest.java
@@ -29,7 +29,6 @@ import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
 import io.netty.handler.ssl.util.SelfSignedCertificate;
 import io.netty.util.Mapping;
 import io.netty.util.ReferenceCountUtil;
-import io.netty.util.ReferenceCounted;
 import io.netty.util.concurrent.Promise;
 import io.netty.util.internal.PlatformDependent;
 import org.junit.Assert;
@@ -114,7 +113,7 @@ public class SniClientTest {
                 KeyManagerFactory kmf = PlatformDependent.javaVersion() >= 8 ?
                         SniClientJava8TestUtil.newSniX509KeyManagerFactory(cert, sniHostName) :
                         SslContext.buildKeyManagerFactory(
-                                new X509Certificate[] { cert.cert() }, cert.key(), null, null);
+                                new X509Certificate[] { cert.cert() }, cert.key(), null, null, null);
 
                sslServerContext = SslContextBuilder.forServer(kmf)
                                                    .sslProvider(sslServerProvider)
diff --git a/handler/src/test/java/io/netty/handler/ssl/SniHandlerTest.java b/handler/src/test/java/io/netty/handler/ssl/SniHandlerTest.java
index d3bf1f2..02f4ac3 100644
--- a/handler/src/test/java/io/netty/handler/ssl/SniHandlerTest.java
+++ b/handler/src/test/java/io/netty/handler/ssl/SniHandlerTest.java
@@ -16,6 +16,34 @@
 
 package io.netty.handler.ssl;
 
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import java.io.File;
+import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLException;
+
+import io.netty.util.concurrent.Future;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
 import io.netty.bootstrap.Bootstrap;
 import io.netty.bootstrap.ServerBootstrap;
 import io.netty.buffer.ByteBuf;
@@ -44,28 +72,11 @@ import io.netty.util.DomainNameMappingBuilder;
 import io.netty.util.Mapping;
 import io.netty.util.ReferenceCountUtil;
 import io.netty.util.ReferenceCounted;
-import io.netty.util.internal.ResourcesUtil;
 import io.netty.util.concurrent.Promise;
 import io.netty.util.internal.ObjectUtil;
+import io.netty.util.internal.ResourcesUtil;
 import io.netty.util.internal.StringUtil;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-
-import java.io.File;
-import java.net.InetSocketAddress;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
-
-import javax.net.ssl.SSLEngine;
-
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.CoreMatchers.nullValue;
-import static org.junit.Assert.*;
-import static org.junit.Assume.assumeTrue;
+import org.mockito.Mockito;
 
 @RunWith(Parameterized.class)
 public class SniHandlerTest {
@@ -307,7 +318,7 @@ public class SniHandlerTest {
             try {
                 // Push the handshake message.
                 ch.writeInbound(Unpooled.wrappedBuffer(message));
-                // TODO(scott): This should fail becasue the engine should reject zero length records during handshake.
+                // TODO(scott): This should fail because the engine should reject zero length records during handshake.
                 // See https://github.com/netty/netty/issues/6348.
                 // fail();
             } catch (Exception e) {
@@ -333,12 +344,55 @@ public class SniHandlerTest {
         }
     }
 
+    @Test(timeout = 10000)
+    public void testMajorVersionNot3() throws Exception {
+        SslContext nettyContext = makeSslContext(provider, false);
+
+        try {
+            DomainNameMapping<SslContext> mapping = new DomainNameMappingBuilder<SslContext>(nettyContext).build();
+
+            SniHandler handler = new SniHandler(mapping);
+            EmbeddedChannel ch = new EmbeddedChannel(handler);
+
+            // invalid
+            byte[] message = {22, 2, 0, 0, 0};
+            try {
+                // Push the handshake message.
+                ch.writeInbound(Unpooled.wrappedBuffer(message));
+                // TODO(scott): This should fail because the engine should reject zero length records during handshake.
+                // See https://github.com/netty/netty/issues/6348.
+                // fail();
+            } catch (Exception e) {
+                // expected
+            }
+
+            ch.close();
+
+            // Consume all the outbound data that may be produced by the SSLEngine.
+            for (;;) {
+                ByteBuf buf = ch.readOutbound();
+                if (buf == null) {
+                    break;
+                }
+                buf.release();
+            }
+
+            assertThat(ch.finish(), is(false));
+            assertThat(handler.hostname(), nullValue());
+            assertThat(handler.sslContext(), is(nettyContext));
+        } finally {
+            releaseAll(nettyContext);
+        }
+    }
+
     @Test
     public void testSniWithApnHandler() throws Exception {
         SslContext nettyContext = makeSslContext(provider, true);
         SslContext sniContext = makeSslContext(provider, true);
         final SslContext clientContext = makeSslClientContext(provider, true);
         try {
+            final AtomicBoolean serverApnCtx = new AtomicBoolean(false);
+            final AtomicBoolean clientApnCtx = new AtomicBoolean(false);
             final CountDownLatch serverApnDoneLatch = new CountDownLatch(1);
             final CountDownLatch clientApnDoneLatch = new CountDownLatch(1);
 
@@ -363,6 +417,8 @@ public class SniHandlerTest {
                         p.addLast(new ApplicationProtocolNegotiationHandler("foo") {
                             @Override
                             protected void configurePipeline(ChannelHandlerContext ctx, String protocol) {
+                                // addresses issue #9131
+                                serverApnCtx.set(ctx.pipeline().context(this) != null);
                                 serverApnDoneLatch.countDown();
                             }
                         });
@@ -381,6 +437,8 @@ public class SniHandlerTest {
                         ch.pipeline().addLast(new ApplicationProtocolNegotiationHandler("foo") {
                             @Override
                             protected void configurePipeline(ChannelHandlerContext ctx, String protocol) {
+                                // addresses issue #9131
+                                clientApnCtx.set(ctx.pipeline().context(this) != null);
                                 clientApnDoneLatch.countDown();
                             }
                         });
@@ -395,6 +453,8 @@ public class SniHandlerTest {
 
                 assertTrue(serverApnDoneLatch.await(5, TimeUnit.SECONDS));
                 assertTrue(clientApnDoneLatch.await(5, TimeUnit.SECONDS));
+                assertTrue(serverApnCtx.get());
+                assertTrue(clientApnCtx.get());
                 assertThat(handler.hostname(), is("sni.fake.site"));
                 assertThat(handler.sslContext(), is(sniContext));
             } finally {
@@ -448,6 +508,7 @@ public class SniHandlerTest {
 
                             boolean success = false;
                             try {
+                                assertEquals(1, ((ReferenceCountedOpenSslContext) sslContext).refCnt());
                                 // The SniHandler's replaceHandler() method allows us to implement custom behavior.
                                 // As an example, we want to release() the SslContext upon channelInactive() or rather
                                 // when the SslHandler closes it's SslEngine. If you take a close look at SslHandler
@@ -455,6 +516,7 @@ public class SniHandlerTest {
 
                                 SSLEngine sslEngine = sslContext.newEngine(ctx.alloc());
                                 try {
+                                    assertEquals(2, ((ReferenceCountedOpenSslContext) sslContext).refCnt());
                                     SslHandler customSslHandler = new CustomSslHandler(sslContext, sslEngine) {
                                         @Override
                                         public void handlerRemoved0(ChannelHandlerContext ctx) throws Exception {
@@ -501,8 +563,9 @@ public class SniHandlerTest {
                     cc.writeAndFlush(Unpooled.wrappedBuffer("Hello, World!".getBytes()))
                             .syncUninterruptibly();
 
-                    // Notice how the server's SslContext refCnt is 1
-                    assertEquals(1, ((ReferenceCounted) sslServerContext).refCnt());
+                    // Notice how the server's SslContext refCnt is 2 as it is incremented when the SSLEngine is created
+                    // and only decremented once it is destroyed.
+                    assertEquals(2, ((ReferenceCounted) sslServerContext).refCnt());
 
                     // The client disconnects
                     cc.close().syncUninterruptibly();
@@ -542,7 +605,7 @@ public class SniHandlerTest {
     private static class CustomSslHandler extends SslHandler {
         private final SslContext sslContext;
 
-        public CustomSslHandler(SslContext sslContext, SSLEngine sslEngine) {
+        CustomSslHandler(SslContext sslContext, SSLEngine sslEngine) {
             super(sslEngine);
             this.sslContext = ObjectUtil.checkNotNull(sslContext, "sslContext");
         }
@@ -559,4 +622,79 @@ public class SniHandlerTest {
             ReferenceCountUtil.release(ctx);
         }
     }
+
+    @Test
+    public void testNonFragmented() throws Exception {
+        testWithFragmentSize(Integer.MAX_VALUE);
+    }
+    @Test
+    public void testFragmented() throws Exception {
+        testWithFragmentSize(50);
+    }
+
+    private void testWithFragmentSize(final int maxFragmentSize) throws Exception {
+        final String sni = "netty.io";
+        SelfSignedCertificate cert = new SelfSignedCertificate();
+        final SslContext context = SslContextBuilder.forServer(cert.key(), cert.cert())
+                .sslProvider(provider)
+                .build();
+        try {
+            @SuppressWarnings("unchecked") final EmbeddedChannel server = new EmbeddedChannel(
+                    new SniHandler(Mockito.mock(DomainNameMapping.class)) {
+                @Override
+                protected Future<SslContext> lookup(final ChannelHandlerContext ctx, final String hostname) {
+                    assertEquals(sni, hostname);
+                    return ctx.executor().newSucceededFuture(context);
+                }
+            });
+
+            final List<ByteBuf> buffers = clientHelloInMultipleFragments(provider, sni, maxFragmentSize);
+            for (ByteBuf buffer : buffers) {
+                server.writeInbound(buffer);
+            }
+            assertTrue(server.finishAndReleaseAll());
+        } finally {
+            releaseAll(context);
+            cert.delete();
+        }
+    }
+
+    private static List<ByteBuf> clientHelloInMultipleFragments(
+            SslProvider provider, String hostname, int maxTlsPlaintextSize) throws SSLException {
+        final EmbeddedChannel client = new EmbeddedChannel();
+        final SslContext ctx = SslContextBuilder.forClient()
+                .sslProvider(provider)
+                .trustManager(InsecureTrustManagerFactory.INSTANCE)
+                .build();
+        try {
+            final SslHandler sslHandler = ctx.newHandler(client.alloc(), hostname, -1);
+            client.pipeline().addLast(sslHandler);
+            final ByteBuf clientHello = client.readOutbound();
+            List<ByteBuf> buffers = split(clientHello, maxTlsPlaintextSize);
+            assertTrue(client.finishAndReleaseAll());
+            return buffers;
+        } finally {
+            releaseAll(ctx);
+        }
+    }
+
+    private static List<ByteBuf> split(ByteBuf clientHello, int maxSize) {
+        final int type = clientHello.readUnsignedByte();
+        final int version = clientHello.readUnsignedShort();
+        final int length = clientHello.readUnsignedShort();
+        assertEquals(length, clientHello.readableBytes());
+
+        final List<ByteBuf> result = new ArrayList<ByteBuf>();
+        while (clientHello.readableBytes() > 0) {
+            final int toRead = Math.min(maxSize, clientHello.readableBytes());
+            final ByteBuf bb = clientHello.alloc().buffer(SslUtils.SSL_RECORD_HEADER_LENGTH + toRead);
+            bb.writeByte(type);
+            bb.writeShort(version);
+            bb.writeShort(toRead);
+            bb.writeBytes(clientHello, toRead);
+            result.add(bb);
+        }
+        clientHello.release();
+        return result;
+    }
 }
diff --git a/handler/src/test/java/io/netty/handler/ssl/SslContextBuilderTest.java b/handler/src/test/java/io/netty/handler/ssl/SslContextBuilderTest.java
index 20f2ccb..e3bc1f4 100644
--- a/handler/src/test/java/io/netty/handler/ssl/SslContextBuilderTest.java
+++ b/handler/src/test/java/io/netty/handler/ssl/SslContextBuilderTest.java
@@ -17,13 +17,22 @@ package io.netty.handler.ssl;
 
 import io.netty.buffer.UnpooledByteBufAllocator;
 import io.netty.handler.ssl.util.SelfSignedCertificate;
+import io.netty.util.CharsetUtil;
 import org.junit.Assume;
-import org.junit.Ignore;
-import org.junit.Rule;
 import org.junit.Test;
 
+import javax.net.ssl.KeyManager;
 import javax.net.ssl.SSLEngine;
 import javax.net.ssl.SSLException;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509ExtendedKeyManager;
+import javax.net.ssl.X509ExtendedTrustManager;
+import java.io.ByteArrayInputStream;
+import java.net.Socket;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
 import java.util.Collections;
 
 import static org.junit.Assert.*;
@@ -52,6 +61,17 @@ public class SslContextBuilderTest {
         testClientContext(SslProvider.OPENSSL);
     }
 
+    @Test
+    public void testKeyStoreTypeJdk() throws Exception {
+        testKeyStoreType(SslProvider.JDK);
+    }
+
+    @Test
+    public void testKeyStoreTypeOpenssl() throws Exception {
+        Assume.assumeTrue(OpenSsl.isAvailable());
+        testKeyStoreType(SslProvider.OPENSSL);
+    }
+
     @Test
     public void testServerContextFromFileJdk() throws Exception {
         testServerContextFromFile(SslProvider.JDK);
@@ -74,6 +94,63 @@ public class SslContextBuilderTest {
         testServerContext(SslProvider.OPENSSL);
     }
 
+    @Test
+    public void testContextFromManagersJdk() throws Exception {
+        testContextFromManagers(SslProvider.JDK);
+    }
+
+    @Test
+    public void testContextFromManagersOpenssl() throws Exception {
+        Assume.assumeTrue(OpenSsl.isAvailable());
+        testContextFromManagers(SslProvider.OPENSSL);
+    }
+
+    @Test(expected = SSLException.class)
+    public void testUnsupportedPrivateKeyFailsFastForServer() throws Exception {
+        Assume.assumeTrue(OpenSsl.isBoringSSL());
+        testUnsupportedPrivateKeyFailsFast(true);
+    }
+
+    @Test(expected = SSLException.class)
+    public void testUnsupportedPrivateKeyFailsFastForClient() throws Exception {
+        Assume.assumeTrue(OpenSsl.isBoringSSL());
+        testUnsupportedPrivateKeyFailsFast(false);
+    }
+    private static void testUnsupportedPrivateKeyFailsFast(boolean server) throws Exception {
+        Assume.assumeTrue(OpenSsl.isBoringSSL());
+        String cert = "-----BEGIN CERTIFICATE-----\n" +
+                "MIICODCCAY2gAwIBAgIEXKTrajAKBggqhkjOPQQDBDBUMQswCQYDVQQGEwJVUzEM\n" +
+                "MAoGA1UECAwDTi9hMQwwCgYDVQQHDANOL2ExDDAKBgNVBAoMA04vYTEMMAoGA1UE\n" +
+                "CwwDTi9hMQ0wCwYDVQQDDARUZXN0MB4XDTE5MDQwMzE3MjA0MloXDTIwMDQwMjE3\n" +
+                "MjA0MlowVDELMAkGA1UEBhMCVVMxDDAKBgNVBAgMA04vYTEMMAoGA1UEBwwDTi9h\n" +
+                "MQwwCgYDVQQKDANOL2ExDDAKBgNVBAsMA04vYTENMAsGA1UEAwwEVGVzdDCBpzAQ\n" +
+                "BgcqhkjOPQIBBgUrgQQAJwOBkgAEBPYWoTjlS2pCMGEM2P8qZnmURWA5e7XxPfIh\n" +
+                "HA876sjmgjJluPgT0OkweuxI4Y/XjzcPnnEBONgzAV1X93UmXdtRiIau/zvsAeFb\n" +
+                "j/q+6sfj1jdnUk6QsMx22kAwplXHmdz1z5ShXQ7mDZPxDbhCPEAUXzIzOqvWIZyA\n" +
+                "HgFxZXmQKEhExA8nxgSIvzQ3ucMwMAoGCCqGSM49BAMEA4GYADCBlAJIAdPD6jaN\n" +
+                "vGxkxcsIbcHn2gSfP1F1G8iNJYrXIN91KbQm8OEp4wxqnBwX8gb/3rmSoEhIU/te\n" +
+                "CcHuFs0guBjfgRWtJ/eDnKB/AkgDbkqrB5wqJFBmVd/rJ5QdwUVNuGP/vDjFVlb6\n" +
+                "Esny6//gTL7jYubLUKHOPIMftCZ2Jn4b+5l0kAs62HD5XkZLPDTwRbf7VCE=\n" +
+                "-----END CERTIFICATE-----";
+        String key = "-----BEGIN PRIVATE KEY-----\n" +
+                "MIIBCQIBADAQBgcqhkjOPQIBBgUrgQQAJwSB8TCB7gIBAQRIALNClTXqQWWlYDHw\n" +
+                "LjNxXpLk17iPepkmablhbxmYX/8CNzoz1o2gcUidoIO2DM9hm7adI/W31EOmSiUJ\n" +
+                "+UsC/ZH3i2qr0wn+oAcGBSuBBAAnoYGVA4GSAAQE9hahOOVLakIwYQzY/ypmeZRF\n" +
+                "YDl7tfE98iEcDzvqyOaCMmW4+BPQ6TB67Ejhj9ePNw+ecQE42DMBXVf3dSZd21GI\n" +
+                "hq7/O+wB4VuP+r7qx+PWN2dSTpCwzHbaQDCmVceZ3PXPlKFdDuYNk/ENuEI8QBRf\n" +
+                "MjM6q9YhnIAeAXFleZAoSETEDyfGBIi/NDe5wzA=\n" +
+                "-----END PRIVATE KEY-----";
+        if (server) {
+            SslContextBuilder.forServer(new ByteArrayInputStream(cert.getBytes(CharsetUtil.US_ASCII)),
+                    new ByteArrayInputStream(key.getBytes(CharsetUtil.US_ASCII)), null)
+                    .sslProvider(SslProvider.OPENSSL).build();
+        } else {
+            SslContextBuilder.forClient().keyManager(new ByteArrayInputStream(cert.getBytes(CharsetUtil.US_ASCII)),
+                new ByteArrayInputStream(key.getBytes(CharsetUtil.US_ASCII)), null)
+                    .sslProvider(SslProvider.OPENSSL).build();
+        }
+    }
+
     @Test(expected = IllegalArgumentException.class)
     public void testInvalidCipherJdk() throws Exception {
         Assume.assumeTrue(OpenSsl.isAvailable());
@@ -95,6 +172,17 @@ public class SslContextBuilderTest {
         }
     }
 
+    private static void testKeyStoreType(SslProvider provider) throws Exception {
+        SelfSignedCertificate cert = new SelfSignedCertificate();
+        SslContextBuilder builder = SslContextBuilder.forServer(cert.certificate(), cert.privateKey())
+                .sslProvider(provider)
+                .keyStoreType("PKCS12");
+        SslContext context = builder.build();
+        SSLEngine engine = context.newEngine(UnpooledByteBufAllocator.DEFAULT);
+        engine.closeInbound();
+        engine.closeOutbound();
+    }
+
     private static void testInvalidCipher(SslProvider provider) throws Exception {
         SelfSignedCertificate cert = new SelfSignedCertificate();
         SslContextBuilder builder = SslContextBuilder.forClient()
@@ -165,4 +253,104 @@ public class SslContextBuilderTest {
         engine.closeInbound();
         engine.closeOutbound();
     }
+
+    private static void testContextFromManagers(SslProvider provider) throws Exception {
+        final SelfSignedCertificate cert = new SelfSignedCertificate();
+        KeyManager customKeyManager = new X509ExtendedKeyManager() {
+            @Override
+            public String[] getClientAliases(String s,
+                                             Principal[] principals) {
+                return new String[0];
+            }
+
+            @Override
+            public String chooseClientAlias(String[] strings,
+                                            Principal[] principals,
+                                            Socket socket) {
+                return "cert_sent_to_server";
+            }
+
+            @Override
+            public String[] getServerAliases(String s,
+                                             Principal[] principals) {
+                return new String[0];
+            }
+
+            @Override
+            public String chooseServerAlias(String s,
+                                            Principal[] principals,
+                                            Socket socket) {
+                return null;
+            }
+
+            @Override
+            public X509Certificate[] getCertificateChain(String s) {
+                X509Certificate[] certificates = new X509Certificate[1];
+                certificates[0] = cert.cert();
+                return new X509Certificate[0];
+            }
+
+            @Override
+            public PrivateKey getPrivateKey(String s) {
+                return cert.key();
+            }
+        };
+        TrustManager customTrustManager = new X509ExtendedTrustManager() {
+            @Override
+            public void checkClientTrusted(
+                    X509Certificate[] x509Certificates, String s,
+                    Socket socket) throws CertificateException { }
+
+            @Override
+            public void checkServerTrusted(
+                    X509Certificate[] x509Certificates, String s,
+                    Socket socket) throws CertificateException { }
+
+            @Override
+            public void checkClientTrusted(
+                    X509Certificate[] x509Certificates, String s,
+                    SSLEngine sslEngine) throws CertificateException { }
+
+            @Override
+            public void checkServerTrusted(
+                    X509Certificate[] x509Certificates, String s,
+                    SSLEngine sslEngine) throws CertificateException { }
+
+            @Override
+            public void checkClientTrusted(
+                    X509Certificate[] x509Certificates, String s)
+                    throws CertificateException { }
+
+            @Override
+            public void checkServerTrusted(
+                    X509Certificate[] x509Certificates, String s)
+                    throws CertificateException { }
+
+            @Override
+            public X509Certificate[] getAcceptedIssuers() {
+                return new X509Certificate[0];
+            }
+        };
+        SslContextBuilder client_builder = SslContextBuilder.forClient()
+                                                     .sslProvider(provider)
+                                                     .keyManager(customKeyManager)
+                                                     .trustManager(customTrustManager)
+                                                     .clientAuth(ClientAuth.OPTIONAL);
+        SslContext client_context = client_builder.build();
+        SSLEngine client_engine = client_context.newEngine(UnpooledByteBufAllocator.DEFAULT);
+        assertFalse(client_engine.getWantClientAuth());
+        assertFalse(client_engine.getNeedClientAuth());
+        client_engine.closeInbound();
+        client_engine.closeOutbound();
+        SslContextBuilder server_builder = SslContextBuilder.forServer(customKeyManager)
+                                                     .sslProvider(provider)
+                                                     .trustManager(customTrustManager)
+                                                     .clientAuth(ClientAuth.REQUIRE);
+        SslContext server_context = server_builder.build();
+        SSLEngine server_engine = server_context.newEngine(UnpooledByteBufAllocator.DEFAULT);
+        assertFalse(server_engine.getWantClientAuth());
+        assertTrue(server_engine.getNeedClientAuth());
+        server_engine.closeInbound();
+        server_engine.closeOutbound();
+    }
 }
diff --git a/handler/src/test/java/io/netty/handler/ssl/SslContextTrustManagerTest.java b/handler/src/test/java/io/netty/handler/ssl/SslContextTrustManagerTest.java
index 97b90f2..04b6a8e 100644
--- a/handler/src/test/java/io/netty/handler/ssl/SslContextTrustManagerTest.java
+++ b/handler/src/test/java/io/netty/handler/ssl/SslContextTrustManagerTest.java
@@ -110,7 +110,7 @@ public class SslContextTrustManagerTest {
             throws Exception {
         X509Certificate[] certCollection = loadCertCollection(resourceNames);
         TrustManagerFactory tmf = SslContext.buildTrustManagerFactory(
-                certCollection, null);
+                certCollection, null, null);
 
         for (TrustManager tm : tmf.getTrustManagers()) {
             if (tm instanceof X509TrustManager) {
diff --git a/handler/src/test/java/io/netty/handler/ssl/SslErrorTest.java b/handler/src/test/java/io/netty/handler/ssl/SslErrorTest.java
index 9f20582..6e24b56 100644
--- a/handler/src/test/java/io/netty/handler/ssl/SslErrorTest.java
+++ b/handler/src/test/java/io/netty/handler/ssl/SslErrorTest.java
@@ -64,7 +64,8 @@ import java.util.Locale;
 @RunWith(Parameterized.class)
 public class SslErrorTest {
 
-    @Parameterized.Parameters(name = "{index}: serverProvider = {0}, clientProvider = {1}, exception = {2}")
+    @Parameterized.Parameters(
+            name = "{index}: serverProvider = {0}, clientProvider = {1}, exception = {2}, serverProduceError = {3}")
     public static Collection<Object[]> data() {
         List<SslProvider> serverProviders = new ArrayList<SslProvider>(2);
         List<SslProvider> clientProviders = new ArrayList<SslProvider>(3);
@@ -95,7 +96,8 @@ public class SslErrorTest {
         for (SslProvider serverProvider: serverProviders) {
             for (SslProvider clientProvider: clientProviders) {
                 for (CertificateException exception: exceptions) {
-                    params.add(new Object[] { serverProvider, clientProvider, exception});
+                    params.add(new Object[] { serverProvider, clientProvider, exception, true });
+                    params.add(new Object[] { serverProvider, clientProvider, exception, false });
                 }
             }
         }
@@ -110,11 +112,14 @@ public class SslErrorTest {
     private final SslProvider serverProvider;
     private final SslProvider clientProvider;
     private final CertificateException exception;
+    private final boolean serverProduceError;
 
-    public SslErrorTest(SslProvider serverProvider, SslProvider clientProvider, CertificateException exception) {
+    public SslErrorTest(SslProvider serverProvider, SslProvider clientProvider,
+                        CertificateException exception, boolean serverProduceError) {
         this.serverProvider = serverProvider;
         this.clientProvider = clientProvider;
         this.exception = exception;
+        this.serverProduceError = serverProduceError;
     }
 
     @Test(timeout = 30000)
@@ -124,56 +129,41 @@ public class SslErrorTest {
         Assume.assumeTrue(OpenSsl.isAvailable());
 
         SelfSignedCertificate ssc = new SelfSignedCertificate();
-        final SslContext sslServerCtx =
-                SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
-                                 .sslProvider(serverProvider)
-                                 .trustManager(new SimpleTrustManagerFactory() {
-            @Override
-            protected void engineInit(KeyStore keyStore) { }
-            @Override
-            protected void engineInit(ManagerFactoryParameters managerFactoryParameters) { }
-
-            @Override
-            protected TrustManager[] engineGetTrustManagers() {
-                return new TrustManager[] { new X509TrustManager() {
-
-                    @Override
-                    public void checkClientTrusted(X509Certificate[] x509Certificates, String s)
-                            throws CertificateException {
-                        throw exception;
-                    }
-
-                    @Override
-                    public void checkServerTrusted(X509Certificate[] x509Certificates, String s)
-                            throws CertificateException {
-                        // NOOP
-                    }
 
-                    @Override
-                    public X509Certificate[] getAcceptedIssuers() {
-                        return EmptyArrays.EMPTY_X509_CERTIFICATES;
-                    }
-                } };
-            }
-        }).clientAuth(ClientAuth.REQUIRE).build();
-
-        final SslContext sslClientCtx = SslContextBuilder.forClient()
-                .trustManager(InsecureTrustManagerFactory.INSTANCE)
+        SslContextBuilder sslServerCtxBuilder = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
+                .sslProvider(serverProvider)
+                .clientAuth(ClientAuth.REQUIRE);
+        SslContextBuilder sslClientCtxBuilder =  SslContextBuilder.forClient()
                 .keyManager(new File(getClass().getResource("test.crt").getFile()),
                         new File(getClass().getResource("test_unencrypted.pem").getFile()))
-                .sslProvider(clientProvider).build();
+                .sslProvider(clientProvider);
+
+        if (serverProduceError) {
+            sslServerCtxBuilder.trustManager(new ExceptionTrustManagerFactory());
+            sslClientCtxBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE);
+        } else {
+            sslServerCtxBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE);
+            sslClientCtxBuilder.trustManager(new ExceptionTrustManagerFactory());
+        }
+
+        final SslContext sslServerCtx = sslServerCtxBuilder.build();
+        final SslContext sslClientCtx = sslClientCtxBuilder.build();
 
         Channel serverChannel = null;
         Channel clientChannel = null;
         EventLoopGroup group = new NioEventLoopGroup();
+        final Promise<Void> promise = group.next().newPromise();
         try {
             serverChannel = new ServerBootstrap().group(group)
                     .channel(NioServerSocketChannel.class)
                     .handler(new LoggingHandler(LogLevel.INFO))
                     .childHandler(new ChannelInitializer<Channel>() {
                         @Override
-                        protected void initChannel(Channel ch) throws Exception {
+                        protected void initChannel(Channel ch) {
                             ch.pipeline().addLast(sslServerCtx.newHandler(ch.alloc()));
+                            if (!serverProduceError) {
+                                ch.pipeline().addLast(new AlertValidationHandler(promise));
+                            }
                             ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
 
                                 @Override
@@ -184,48 +174,20 @@ public class SslErrorTest {
                         }
                     }).bind(0).sync().channel();
 
-            final Promise<Void> promise = group.next().newPromise();
-
             clientChannel = new Bootstrap().group(group)
                     .channel(NioSocketChannel.class)
                     .handler(new ChannelInitializer<Channel>() {
                         @Override
-                        protected void initChannel(Channel ch) throws Exception {
+                        protected void initChannel(Channel ch) {
                             ch.pipeline().addLast(sslClientCtx.newHandler(ch.alloc()));
+                            if (serverProduceError) {
+                                ch.pipeline().addLast(new AlertValidationHandler(promise));
+                            }
                             ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
+
                                 @Override
                                 public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
-                                    // Unwrap as its wrapped by a DecoderException
-                                    Throwable unwrappedCause = cause.getCause();
-                                    if (unwrappedCause instanceof SSLException) {
-                                        if (exception instanceof TestCertificateException) {
-                                            CertPathValidatorException.Reason reason =
-                                                    ((CertPathValidatorException) exception.getCause()).getReason();
-                                            if (reason == CertPathValidatorException.BasicReason.EXPIRED) {
-                                                verifyException(unwrappedCause, "expired", promise);
-                                            } else if (reason == CertPathValidatorException.BasicReason.NOT_YET_VALID) {
-                                                // BoringSSL uses "expired" in this case while others use "bad"
-                                                if (OpenSsl.isBoringSSL()) {
-                                                    verifyException(unwrappedCause, "expired", promise);
-                                                } else {
-                                                    verifyException(unwrappedCause, "bad", promise);
-                                                }
-                                            } else if (reason == CertPathValidatorException.BasicReason.REVOKED) {
-                                                verifyException(unwrappedCause, "revoked", promise);
-                                            }
-                                        } else if (exception instanceof CertificateExpiredException) {
-                                            verifyException(unwrappedCause, "expired", promise);
-                                        } else if (exception instanceof CertificateNotYetValidException) {
-                                            // BoringSSL uses "expired" in this case while others use "bad"
-                                            if (OpenSsl.isBoringSSL()) {
-                                                verifyException(unwrappedCause, "expired", promise);
-                                            } else {
-                                                verifyException(unwrappedCause, "bad", promise);
-                                            }
-                                        } else if (exception instanceof CertificateRevokedException) {
-                                            verifyException(unwrappedCause, "revoked", promise);
-                                        }
-                                    }
+                                    ctx.close();
                                 }
                             });
                         }
@@ -246,11 +208,88 @@ public class SslErrorTest {
         }
     }
 
+    private final class ExceptionTrustManagerFactory extends SimpleTrustManagerFactory {
+        @Override
+        protected void engineInit(KeyStore keyStore) { }
+        @Override
+        protected void engineInit(ManagerFactoryParameters managerFactoryParameters) { }
+
+        @Override
+        protected TrustManager[] engineGetTrustManagers() {
+            return new TrustManager[] { new X509TrustManager() {
+
+                @Override
+                public void checkClientTrusted(X509Certificate[] x509Certificates, String s)
+                        throws CertificateException {
+                    throw exception;
+                }
+
+                @Override
+                public void checkServerTrusted(X509Certificate[] x509Certificates, String s)
+                        throws CertificateException {
+                    throw exception;
+                }
+
+                @Override
+                public X509Certificate[] getAcceptedIssuers() {
+                    return EmptyArrays.EMPTY_X509_CERTIFICATES;
+                }
+            } };
+        }
+    }
+
+    private final class AlertValidationHandler extends ChannelInboundHandlerAdapter {
+        private final Promise<Void> promise;
+
+        AlertValidationHandler(Promise<Void> promise) {
+            this.promise = promise;
+        }
+
+        @Override
+        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+            // Unwrap as its wrapped by a DecoderException
+            Throwable unwrappedCause = cause.getCause();
+            if (unwrappedCause instanceof SSLException) {
+                if (exception instanceof TestCertificateException) {
+                    CertPathValidatorException.Reason reason =
+                            ((CertPathValidatorException) exception.getCause()).getReason();
+                    if (reason == CertPathValidatorException.BasicReason.EXPIRED) {
+                        verifyException(unwrappedCause, "expired", promise);
+                    } else if (reason == CertPathValidatorException.BasicReason.NOT_YET_VALID) {
+                        // BoringSSL uses "expired" in this case while others use "bad"
+                        if (OpenSsl.isBoringSSL()) {
+                            verifyException(unwrappedCause, "expired", promise);
+                        } else {
+                            verifyException(unwrappedCause, "bad", promise);
+                        }
+                    } else if (reason == CertPathValidatorException.BasicReason.REVOKED) {
+                        verifyException(unwrappedCause, "revoked", promise);
+                    }
+                } else if (exception instanceof CertificateExpiredException) {
+                    verifyException(unwrappedCause, "expired", promise);
+                } else if (exception instanceof CertificateNotYetValidException) {
+                    // BoringSSL uses "expired" in this case while others use "bad"
+                    if (OpenSsl.isBoringSSL()) {
+                        verifyException(unwrappedCause, "expired", promise);
+                    } else {
+                        verifyException(unwrappedCause, "bad", promise);
+                    }
+                } else if (exception instanceof CertificateRevokedException) {
+                    verifyException(unwrappedCause, "revoked", promise);
+                }
+            }
+        }
+    }
+
     // Its a bit hacky to verify against the message that is part of the exception but there is no other way
     // at the moment as there are no different exceptions for the different alerts.
-    private static void verifyException(Throwable cause, String messagePart, Promise<Void> promise) {
+    private void verifyException(Throwable cause, String messagePart, Promise<Void> promise) {
         String message = cause.getMessage();
-        if (message.toLowerCase(Locale.UK).contains(messagePart.toLowerCase(Locale.UK))) {
+        if (message.toLowerCase(Locale.UK).contains(messagePart.toLowerCase(Locale.UK)) ||
+                // When the error is produced on the client side and the client side uses JDK as provider it will always
+                // use "certificate unknown".
+                !serverProduceError && clientProvider == SslProvider.JDK &&
+                        message.toLowerCase(Locale.UK).contains("unknown")) {
             promise.setSuccess(null);
         } else {
             Throwable error = new AssertionError("message not contains '" + messagePart + "': " + message);
@@ -262,7 +301,7 @@ public class SslErrorTest {
     private static final class TestCertificateException extends CertificateException {
         private static final long serialVersionUID = -5816338303868751410L;
 
-        public TestCertificateException(Throwable cause) {
+        TestCertificateException(Throwable cause) {
             super(cause);
         }
     }
diff --git a/handler/src/test/java/io/netty/handler/ssl/SslHandlerTest.java b/handler/src/test/java/io/netty/handler/ssl/SslHandlerTest.java
index 772a15f..11347c4 100644
--- a/handler/src/test/java/io/netty/handler/ssl/SslHandlerTest.java
+++ b/handler/src/test/java/io/netty/handler/ssl/SslHandlerTest.java
@@ -23,6 +23,7 @@ import io.netty.buffer.ByteBufAllocator;
 import io.netty.buffer.Unpooled;
 import io.netty.buffer.UnpooledByteBufAllocator;
 import io.netty.channel.Channel;
+import io.netty.channel.ChannelDuplexHandler;
 import io.netty.channel.ChannelFuture;
 import io.netty.channel.ChannelFutureListener;
 import io.netty.channel.ChannelHandler;
@@ -42,6 +43,7 @@ import io.netty.channel.local.LocalServerChannel;
 import io.netty.channel.nio.NioEventLoopGroup;
 import io.netty.channel.socket.nio.NioServerSocketChannel;
 import io.netty.channel.socket.nio.NioSocketChannel;
+import io.netty.handler.codec.ByteToMessageDecoder;
 import io.netty.handler.codec.CodecException;
 import io.netty.handler.codec.DecoderException;
 import io.netty.handler.codec.UnsupportedMessageTypeException;
@@ -53,16 +55,25 @@ import io.netty.util.ReferenceCountUtil;
 import io.netty.util.ReferenceCounted;
 import io.netty.util.concurrent.Future;
 import io.netty.util.concurrent.FutureListener;
+import io.netty.util.concurrent.ImmediateEventExecutor;
+import io.netty.util.concurrent.ImmediateExecutor;
 import io.netty.util.concurrent.Promise;
+import io.netty.util.internal.PlatformDependent;
 import org.hamcrest.CoreMatchers;
 import org.junit.Test;
 
 import java.net.InetSocketAddress;
 import java.nio.channels.ClosedChannelException;
 import java.security.NoSuchAlgorithmException;
+import java.util.List;
+import java.util.Queue;
 import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -74,9 +85,13 @@ import javax.net.ssl.SSLException;
 import javax.net.ssl.SSLProtocolException;
 
 import static io.netty.buffer.Unpooled.wrappedBuffer;
-import static org.hamcrest.CoreMatchers.*;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.nullValue;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
@@ -84,6 +99,62 @@ import static org.junit.Assume.assumeTrue;
 
 public class SslHandlerTest {
 
+    @Test(timeout = 5000)
+    public void testNonApplicationDataFailureFailsQueuedWrites() throws NoSuchAlgorithmException, InterruptedException {
+        final CountDownLatch writeLatch = new CountDownLatch(1);
+        final Queue<ChannelPromise> writesToFail = new ConcurrentLinkedQueue<ChannelPromise>();
+        SSLEngine engine = newClientModeSSLEngine();
+        SslHandler handler = new SslHandler(engine) {
+            @Override
+            public void write(final ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
+                super.write(ctx, msg, promise);
+                writeLatch.countDown();
+            }
+        };
+        EmbeddedChannel ch = new EmbeddedChannel(new ChannelDuplexHandler() {
+            @Override
+            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
+                if (msg instanceof ByteBuf) {
+                    if (((ByteBuf) msg).isReadable()) {
+                        writesToFail.add(promise);
+                    } else {
+                        promise.setSuccess();
+                    }
+                }
+                ReferenceCountUtil.release(msg);
+            }
+        }, handler);
+
+        try {
+            final CountDownLatch writeCauseLatch = new CountDownLatch(1);
+            final AtomicReference<Throwable> failureRef = new AtomicReference<Throwable>();
+            ch.write(Unpooled.wrappedBuffer(new byte[]{1})).addListener(new ChannelFutureListener() {
+                @Override
+                public void operationComplete(ChannelFuture future) {
+                    failureRef.compareAndSet(null, future.cause());
+                    writeCauseLatch.countDown();
+                }
+            });
+            writeLatch.await();
+
+            // Simulate failing the SslHandler non-application writes after there are applications writes queued.
+            ChannelPromise promiseToFail;
+            while ((promiseToFail = writesToFail.poll()) != null) {
+                promiseToFail.setFailure(new RuntimeException("fake exception"));
+            }
+
+            writeCauseLatch.await();
+            Throwable writeCause = failureRef.get();
+            assertNotNull(writeCause);
+            assertThat(writeCause, is(CoreMatchers.<Throwable>instanceOf(SSLException.class)));
+            Throwable cause = handler.handshakeFuture().cause();
+            assertNotNull(cause);
+            assertThat(cause, is(CoreMatchers.<Throwable>instanceOf(SSLException.class)));
+        } finally {
+            assertFalse(ch.finishAndReleaseAll());
+        }
+    }
+
     @Test
     public void testNoSslHandshakeEventWhenNoHandshake() throws Exception {
         final AtomicBoolean inActive = new AtomicBoolean(false);
@@ -123,12 +194,12 @@ public class SslHandlerTest {
         assertFalse(ch.finishAndReleaseAll());
     }
 
-    @Test(expected = SSLException.class, timeout = 3000)
+    @Test(expected = SslHandshakeTimeoutException.class, timeout = 3000)
     public void testClientHandshakeTimeout() throws Exception {
         testHandshakeTimeout(true);
     }
 
-    @Test(expected = SSLException.class, timeout = 3000)
+    @Test(expected = SslHandshakeTimeoutException.class, timeout = 3000)
     public void testServerHandshakeTimeout() throws Exception {
         testHandshakeTimeout(false);
     }
@@ -143,6 +214,16 @@ public class SslHandlerTest {
         return engine;
     }
 
+    private static SSLEngine newClientModeSSLEngine() throws NoSuchAlgorithmException {
+        SSLEngine engine = SSLContext.getDefault().createSSLEngine();
+        // Set the mode before we try to do the handshake as otherwise it may throw an IllegalStateException.
+        // See:
+        //  - https://docs.oracle.com/javase/10/docs/api/javax/net/ssl/SSLEngine.html#beginHandshake()
+        //  - http://mail.openjdk.java.net/pipermail/security-dev/2018-July/017715.html
+        engine.setUseClientMode(true);
+        return engine;
+    }
+
     private static void testHandshakeTimeout(boolean client) throws Exception {
         SSLEngine engine = SSLContext.getDefault().createSSLEngine();
         engine.setUseClientMode(client);
@@ -250,10 +331,11 @@ public class SslHandlerTest {
                 .sslProvider(SslProvider.OPENSSL)
                 .build();
             try {
+                assertEquals(1, ((ReferenceCounted) sslContext).refCnt());
                 SSLEngine sslEngine = sslContext.newEngine(ByteBufAllocator.DEFAULT);
                 EmbeddedChannel ch = new EmbeddedChannel(new SslHandler(sslEngine));
 
-                assertEquals(1, ((ReferenceCounted) sslContext).refCnt());
+                assertEquals(2, ((ReferenceCounted) sslContext).refCnt());
                 assertEquals(1, ((ReferenceCounted) sslEngine).refCnt());
 
                 assertTrue(ch.finishAndReleaseAll());
@@ -824,4 +906,278 @@ public class SslHandlerTest {
             ReferenceCountUtil.release(sslClientCtx);
         }
     }
+
+    @Test
+    public void testHandshakeWithExecutorThatExecuteDirecty() throws Exception {
+        testHandshakeWithExecutor(new Executor() {
+            @Override
+            public void execute(Runnable command) {
+                command.run();
+            }
+        });
+    }
+
+    @Test
+    public void testHandshakeWithImmediateExecutor() throws Exception {
+        testHandshakeWithExecutor(ImmediateExecutor.INSTANCE);
+    }
+
+    @Test
+    public void testHandshakeWithImmediateEventExecutor() throws Exception {
+        testHandshakeWithExecutor(ImmediateEventExecutor.INSTANCE);
+    }
+
+    @Test
+    public void testHandshakeWithExecutor() throws Exception {
+        ExecutorService executorService = Executors.newCachedThreadPool();
+        try {
+            testHandshakeWithExecutor(executorService);
+        } finally {
+            executorService.shutdown();
+        }
+    }
+
+    private static void testHandshakeWithExecutor(Executor executor) throws Exception {
+        final SslContext sslClientCtx = SslContextBuilder.forClient()
+                .trustManager(InsecureTrustManagerFactory.INSTANCE)
+                .sslProvider(SslProvider.JDK).build();
+
+        final SelfSignedCertificate cert = new SelfSignedCertificate();
+        final SslContext sslServerCtx = SslContextBuilder.forServer(cert.key(), cert.cert())
+                .sslProvider(SslProvider.JDK).build();
+
+        EventLoopGroup group = new NioEventLoopGroup();
+        Channel sc = null;
+        Channel cc = null;
+        final SslHandler clientSslHandler = sslClientCtx.newHandler(UnpooledByteBufAllocator.DEFAULT, executor);
+        final SslHandler serverSslHandler = sslServerCtx.newHandler(UnpooledByteBufAllocator.DEFAULT, executor);
+
+        try {
+            sc = new ServerBootstrap()
+                    .group(group)
+                    .channel(NioServerSocketChannel.class)
+                    .childHandler(serverSslHandler)
+                    .bind(new InetSocketAddress(0)).syncUninterruptibly().channel();
+
+            ChannelFuture future = new Bootstrap()
+                    .group(group)
+                    .channel(NioSocketChannel.class)
+                    .handler(new ChannelInitializer<Channel>() {
+                        @Override
+                        protected void initChannel(Channel ch) {
+                            ch.pipeline().addLast(clientSslHandler);
+                        }
+                    }).connect(sc.localAddress());
+            cc = future.syncUninterruptibly().channel();
+
+            assertTrue(clientSslHandler.handshakeFuture().await().isSuccess());
+            assertTrue(serverSslHandler.handshakeFuture().await().isSuccess());
+        } finally {
+            if (cc != null) {
+                cc.close().syncUninterruptibly();
+            }
+            if (sc != null) {
+                sc.close().syncUninterruptibly();
+            }
+            group.shutdownGracefully();
+            ReferenceCountUtil.release(sslClientCtx);
+        }
+    }
+
+    @Test
+    public void testClientHandshakeTimeoutBecauseExecutorNotExecute() throws Exception {
+        testHandshakeTimeoutBecauseExecutorNotExecute(true);
+    }
+
+    @Test
+    public void testServerHandshakeTimeoutBecauseExecutorNotExecute() throws Exception {
+        testHandshakeTimeoutBecauseExecutorNotExecute(false);
+    }
+
+    private static void testHandshakeTimeoutBecauseExecutorNotExecute(final boolean client) throws Exception {
+        final SslContext sslClientCtx = SslContextBuilder.forClient()
+                .trustManager(InsecureTrustManagerFactory.INSTANCE)
+                .sslProvider(SslProvider.JDK).build();
+
+        final SelfSignedCertificate cert = new SelfSignedCertificate();
+        final SslContext sslServerCtx = SslContextBuilder.forServer(cert.key(), cert.cert())
+                .sslProvider(SslProvider.JDK).build();
+
+        EventLoopGroup group = new NioEventLoopGroup();
+        Channel sc = null;
+        Channel cc = null;
+        final SslHandler clientSslHandler = sslClientCtx.newHandler(UnpooledByteBufAllocator.DEFAULT, new Executor() {
+            @Override
+            public void execute(Runnable command) {
+                if (!client) {
+                    command.run();
+                }
+                // Do nothing to simulate slow execution.
+            }
+        });
+        if (client) {
+            clientSslHandler.setHandshakeTimeout(100, TimeUnit.MILLISECONDS);
+        }
+        final SslHandler serverSslHandler = sslServerCtx.newHandler(UnpooledByteBufAllocator.DEFAULT, new Executor() {
+            @Override
+            public void execute(Runnable command) {
+                if (client) {
+                    command.run();
+                }
+                // Do nothing to simulate slow execution.
+            }
+        });
+        if (!client) {
+            serverSslHandler.setHandshakeTimeout(100, TimeUnit.MILLISECONDS);
+        }
+        try {
+            sc = new ServerBootstrap()
+                    .group(group)
+                    .channel(NioServerSocketChannel.class)
+                    .childHandler(serverSslHandler)
+                    .bind(new InetSocketAddress(0)).syncUninterruptibly().channel();
+
+            ChannelFuture future = new Bootstrap()
+                    .group(group)
+                    .channel(NioSocketChannel.class)
+                    .handler(new ChannelInitializer<Channel>() {
+                        @Override
+                        protected void initChannel(Channel ch) {
+                            ch.pipeline().addLast(clientSslHandler);
+                        }
+                    }).connect(sc.localAddress());
+            cc = future.syncUninterruptibly().channel();
+
+            if (client) {
+                Throwable cause = clientSslHandler.handshakeFuture().await().cause();
+                assertThat(cause, CoreMatchers.<Throwable>instanceOf(SSLException.class));
+                assertThat(cause.getMessage(), containsString("timed out"));
+                assertFalse(serverSslHandler.handshakeFuture().await().isSuccess());
+            } else {
+                Throwable cause = serverSslHandler.handshakeFuture().await().cause();
+                assertThat(cause, CoreMatchers.<Throwable>instanceOf(SSLException.class));
+                assertThat(cause.getMessage(), containsString("timed out"));
+                assertFalse(clientSslHandler.handshakeFuture().await().isSuccess());
+            }
+        } finally {
+            if (cc != null) {
+                cc.close().syncUninterruptibly();
+            }
+            if (sc != null) {
+                sc.close().syncUninterruptibly();
+            }
+            group.shutdownGracefully();
+            ReferenceCountUtil.release(sslClientCtx);
+        }
+    }
+
+    @Test(timeout = 5000L)
+    public void testSessionTicketsWithTLSv12() throws Throwable {
+        testSessionTickets(SslUtils.PROTOCOL_TLS_V1_2);
+    }
+
+    @Test(timeout = 5000L)
+    public void testSessionTicketsWithTLSv13() throws Throwable {
+        assumeTrue(OpenSsl.isTlsv13Supported());
+        testSessionTickets(SslUtils.PROTOCOL_TLS_V1_3);
+    }
+
+    private static void testSessionTickets(String protocol) throws Throwable {
+        assumeTrue(OpenSsl.isAvailable());
+        final SslContext sslClientCtx = SslContextBuilder.forClient()
+                .trustManager(InsecureTrustManagerFactory.INSTANCE)
+                .sslProvider(SslProvider.OPENSSL)
+                .protocols(protocol)
+                .build();
+
+        final SelfSignedCertificate cert = new SelfSignedCertificate();
+        final SslContext sslServerCtx = SslContextBuilder.forServer(cert.key(), cert.cert())
+                .sslProvider(SslProvider.OPENSSL)
+                .protocols(protocol)
+                .build();
+
+        OpenSslSessionTicketKey key = new OpenSslSessionTicketKey(new byte[OpenSslSessionTicketKey.NAME_SIZE],
+                new byte[OpenSslSessionTicketKey.HMAC_KEY_SIZE], new byte[OpenSslSessionTicketKey.AES_KEY_SIZE]);
+        ((OpenSslSessionContext) sslClientCtx.sessionContext()).setTicketKeys(key);
+        ((OpenSslSessionContext) sslServerCtx.sessionContext()).setTicketKeys(key);
+
+        EventLoopGroup group = new NioEventLoopGroup();
+        Channel sc = null;
+        Channel cc = null;
+        final SslHandler clientSslHandler = sslClientCtx.newHandler(UnpooledByteBufAllocator.DEFAULT);
+        final SslHandler serverSslHandler = sslServerCtx.newHandler(UnpooledByteBufAllocator.DEFAULT);
+
+        final BlockingQueue<Object> queue = new LinkedBlockingQueue<Object>();
+        final byte[] bytes = new byte[96];
+        PlatformDependent.threadLocalRandom().nextBytes(bytes);
+        try {
+            sc = new ServerBootstrap()
+                    .group(group)
+                    .channel(NioServerSocketChannel.class)
+                    .childHandler(new ChannelInitializer<Channel>() {
+                        @Override
+                        protected void initChannel(Channel ch) {
+                            ch.pipeline().addLast(serverSslHandler);
+                            ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
+                                @Override
+                                public void userEventTriggered(ChannelHandlerContext ctx, Object evt)  {
+                                    if (evt instanceof SslHandshakeCompletionEvent) {
+                                        ctx.writeAndFlush(Unpooled.wrappedBuffer(bytes));
+                                    }
+                                }
+                            });
+                        }
+                    })
+                    .bind(new InetSocketAddress(0)).syncUninterruptibly().channel();
+
+            ChannelFuture future = new Bootstrap()
+                    .group(group)
+                    .channel(NioSocketChannel.class)
+                    .handler(new ChannelInitializer<Channel>() {
+                        @Override
+                        protected void initChannel(Channel ch) {
+                            ch.pipeline().addLast(clientSslHandler);
+                            ch.pipeline().addLast(new ByteToMessageDecoder() {
+
+                                @Override
+                                protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
+                                    if (in.readableBytes() == bytes.length) {
+                                        queue.add(in.readBytes(bytes.length));
+                                    }
+                                }
+
+                                @Override
+                                public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+                                    queue.add(cause);
+                                }
+                            });
+                        }
+                    }).connect(sc.localAddress());
+            cc = future.syncUninterruptibly().channel();
+
+            assertTrue(clientSslHandler.handshakeFuture().await().isSuccess());
+            assertTrue(serverSslHandler.handshakeFuture().await().isSuccess());
+            Object obj = queue.take();
+            if (obj instanceof ByteBuf) {
+                ByteBuf buffer = (ByteBuf) obj;
+                ByteBuf expected = Unpooled.wrappedBuffer(bytes);
+                try {
+                    assertEquals(expected, buffer);
+                } finally {
+                    expected.release();
+                }
+            } else {
+                throw (Throwable) obj;
+            }
+        } finally {
+            if (cc != null) {
+                cc.close().syncUninterruptibly();
+            }
+            if (sc != null) {
+                sc.close().syncUninterruptibly();
+            }
+            group.shutdownGracefully();
+            ReferenceCountUtil.release(sslClientCtx);
+        }
+    }
 }
diff --git a/handler/src/test/java/io/netty/handler/ssl/ocsp/OcspTest.java b/handler/src/test/java/io/netty/handler/ssl/ocsp/OcspTest.java
index 161f52a..cfed824 100644
--- a/handler/src/test/java/io/netty/handler/ssl/ocsp/OcspTest.java
+++ b/handler/src/test/java/io/netty/handler/ssl/ocsp/OcspTest.java
@@ -459,7 +459,7 @@ public class OcspTest {
 
         private volatile byte[] response;
 
-        public TestClientOcspContext(boolean valid) {
+        TestClientOcspContext(boolean valid) {
             this.valid = valid;
         }
 
@@ -481,7 +481,7 @@ public class OcspTest {
 
         private final OcspClientCallback callback;
 
-        public OcspClientCallbackHandler(ReferenceCountedOpenSslEngine engine, OcspClientCallback callback) {
+        OcspClientCallbackHandler(ReferenceCountedOpenSslEngine engine, OcspClientCallback callback) {
             super(engine);
             this.callback = callback;
         }
@@ -496,7 +496,7 @@ public class OcspTest {
     private static final class OcspTestException extends IllegalStateException {
         private static final long serialVersionUID = 4516426833250228159L;
 
-        public OcspTestException(String message) {
+        OcspTestException(String message) {
             super(message);
         }
     }
diff --git a/handler/src/test/java/io/netty/handler/stream/ChunkedWriteHandlerTest.java b/handler/src/test/java/io/netty/handler/stream/ChunkedWriteHandlerTest.java
index 5b03048..7d08feb 100644
--- a/handler/src/test/java/io/netty/handler/stream/ChunkedWriteHandlerTest.java
+++ b/handler/src/test/java/io/netty/handler/stream/ChunkedWriteHandlerTest.java
@@ -21,21 +21,27 @@ import io.netty.buffer.Unpooled;
 import io.netty.channel.ChannelFuture;
 import io.netty.channel.ChannelFutureListener;
 import io.netty.channel.ChannelHandlerContext;
-import io.netty.channel.ChannelPromise;
 import io.netty.channel.ChannelOutboundHandlerAdapter;
+import io.netty.channel.ChannelPromise;
 import io.netty.channel.embedded.EmbeddedChannel;
 import io.netty.util.CharsetUtil;
 import io.netty.util.ReferenceCountUtil;
+import org.junit.Assert;
 import org.junit.Test;
 
 import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.RandomAccessFile;
 import java.nio.channels.Channels;
+import java.nio.channels.ClosedChannelException;
+import java.nio.channels.FileChannel;
+import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 
+import static java.util.concurrent.TimeUnit.*;
 import static org.junit.Assert.*;
 
 public class ChunkedWriteHandlerTest {
@@ -100,6 +106,41 @@ public class ChunkedWriteHandlerTest {
         check(new ChunkedNioFile(TMP), new ChunkedNioFile(TMP), new ChunkedNioFile(TMP));
     }
 
+    @Test
+    public void testChunkedNioFileLeftPositionUnchanged() throws IOException {
+        FileChannel in = null;
+        final long expectedPosition = 10;
+        try {
+            in = new RandomAccessFile(TMP, "r").getChannel();
+            in.position(expectedPosition);
+            check(new ChunkedNioFile(in) {
+                @Override
+                public void close() throws Exception {
+                    //no op
+                }
+            });
+            Assert.assertTrue(in.isOpen());
+            Assert.assertEquals(expectedPosition, in.position());
+        } finally {
+            if (in != null) {
+                in.close();
+            }
+        }
+    }
+
+    @Test(expected = ClosedChannelException.class)
+    public void testChunkedNioFileFailOnClosedFileChannel() throws IOException {
+        final FileChannel in = new RandomAccessFile(TMP, "r").getChannel();
+        in.close();
+        check(new ChunkedNioFile(in) {
+            @Override
+            public void close() throws Exception {
+                //no op
+            }
+        });
+        Assert.fail();
+    }
+
     @Test
     public void testUnchunkedData() throws IOException {
         check(Unpooled.wrappedBuffer(BYTES));
@@ -433,6 +474,142 @@ public class ChunkedWriteHandlerTest {
         assertEquals(1, chunks.get());
     }
 
+    @Test
+    public void testCloseSuccessfulChunkedInput() {
+        int chunks = 10;
+        TestChunkedInput input = new TestChunkedInput(chunks);
+        EmbeddedChannel ch = new EmbeddedChannel(new ChunkedWriteHandler());
+
+        assertTrue(ch.writeOutbound(input));
+
+        for (int i = 0; i < chunks; i++) {
+            ByteBuf buf = ch.readOutbound();
+            assertEquals(i, buf.readInt());
+            buf.release();
+        }
+
+        assertTrue(input.isClosed());
+        assertFalse(ch.finish());
+    }
+
+    @Test
+    public void testCloseFailedChunkedInput() {
+        Exception error = new Exception("Unable to produce a chunk");
+        ThrowingChunkedInput input = new ThrowingChunkedInput(error);
+
+        EmbeddedChannel ch = new EmbeddedChannel(new ChunkedWriteHandler());
+
+        try {
+            ch.writeOutbound(input);
+            fail("Exception expected");
+        } catch (Exception e) {
+            assertEquals(error, e);
+        }
+
+        assertTrue(input.isClosed());
+        assertFalse(ch.finish());
+    }
+
+    @Test
+    public void testWriteListenerInvokedAfterSuccessfulChunkedInputClosed() throws Exception {
+        final TestChunkedInput input = new TestChunkedInput(2);
+        EmbeddedChannel ch = new EmbeddedChannel(new ChunkedWriteHandler());
+
+        final AtomicBoolean inputClosedWhenListenerInvoked = new AtomicBoolean();
+        final CountDownLatch listenerInvoked = new CountDownLatch(1);
+
+        ChannelFuture writeFuture = ch.write(input);
+        writeFuture.addListener(new ChannelFutureListener() {
+            @Override
+            public void operationComplete(ChannelFuture future) {
+                inputClosedWhenListenerInvoked.set(input.isClosed());
+                listenerInvoked.countDown();
+            }
+        });
+        ch.flush();
+
+        assertTrue(listenerInvoked.await(10, SECONDS));
+        assertTrue(writeFuture.isSuccess());
+        assertTrue(inputClosedWhenListenerInvoked.get());
+        assertTrue(ch.finishAndReleaseAll());
+    }
+
+    @Test
+    public void testWriteListenerInvokedAfterFailedChunkedInputClosed() throws Exception {
+        final ThrowingChunkedInput input = new ThrowingChunkedInput(new RuntimeException());
+        EmbeddedChannel ch = new EmbeddedChannel(new ChunkedWriteHandler());
+
+        final AtomicBoolean inputClosedWhenListenerInvoked = new AtomicBoolean();
+        final CountDownLatch listenerInvoked = new CountDownLatch(1);
+
+        ChannelFuture writeFuture = ch.write(input);
+        writeFuture.addListener(new ChannelFutureListener() {
+            @Override
+            public void operationComplete(ChannelFuture future) {
+                inputClosedWhenListenerInvoked.set(input.isClosed());
+                listenerInvoked.countDown();
+            }
+        });
+        ch.flush();
+
+        assertTrue(listenerInvoked.await(10, SECONDS));
+        assertFalse(writeFuture.isSuccess());
+        assertTrue(inputClosedWhenListenerInvoked.get());
+        assertFalse(ch.finish());
+    }
+
+    @Test
+    public void testWriteListenerInvokedAfterChannelClosedAndInputFullyConsumed() throws Exception {
+        // use empty input which has endOfInput = true
+        final TestChunkedInput input = new TestChunkedInput(0);
+        EmbeddedChannel ch = new EmbeddedChannel(new ChunkedWriteHandler());
+
+        final AtomicBoolean inputClosedWhenListenerInvoked = new AtomicBoolean();
+        final CountDownLatch listenerInvoked = new CountDownLatch(1);
+
+        ChannelFuture writeFuture = ch.write(input);
+        writeFuture.addListener(new ChannelFutureListener() {
+            @Override
+            public void operationComplete(ChannelFuture future) {
+                inputClosedWhenListenerInvoked.set(input.isClosed());
+                listenerInvoked.countDown();
+            }
+        });
+        ch.close(); // close channel to make handler discard the input on subsequent flush
+        ch.flush();
+
+        assertTrue(listenerInvoked.await(10, SECONDS));
+        assertTrue(writeFuture.isSuccess());
+        assertTrue(inputClosedWhenListenerInvoked.get());
+        assertFalse(ch.finish());
+    }
+
+    @Test
+    public void testWriteListenerInvokedAfterChannelClosedAndInputNotFullyConsumed() throws Exception {
+        // use non-empty input which has endOfInput = false
+        final TestChunkedInput input = new TestChunkedInput(42);
+        EmbeddedChannel ch = new EmbeddedChannel(new ChunkedWriteHandler());
+
+        final AtomicBoolean inputClosedWhenListenerInvoked = new AtomicBoolean();
+        final CountDownLatch listenerInvoked = new CountDownLatch(1);
+
+        ChannelFuture writeFuture = ch.write(input);
+        writeFuture.addListener(new ChannelFutureListener() {
+            @Override
+            public void operationComplete(ChannelFuture future) {
+                inputClosedWhenListenerInvoked.set(input.isClosed());
+                listenerInvoked.countDown();
+            }
+        });
+        ch.close(); // close channel to make handler discard the input on subsequent flush
+        ch.flush();
+
+        assertTrue(listenerInvoked.await(10, SECONDS));
+        assertFalse(writeFuture.isSuccess());
+        assertTrue(inputClosedWhenListenerInvoked.get());
+        assertFalse(ch.finish());
+    }
+
     private static void check(Object... inputs) {
         EmbeddedChannel ch = new EmbeddedChannel(new ChunkedWriteHandler());
 
@@ -524,4 +701,96 @@ public class ChunkedWriteHandlerTest {
 
         assertEquals(BYTES.length, read);
     }
+
+    private static final class TestChunkedInput implements ChunkedInput<ByteBuf> {
+        private final int chunksToProduce;
+
+        private int chunksProduced;
+        private volatile boolean closed;
+
+        TestChunkedInput(int chunksToProduce) {
+            this.chunksToProduce = chunksToProduce;
+        }
+
+        @Override
+        public boolean isEndOfInput() {
+            return chunksProduced >= chunksToProduce;
+        }
+
+        @Override
+        public void close() {
+            closed = true;
+        }
+
+        @Override
+        public ByteBuf readChunk(ChannelHandlerContext ctx) {
+            return readChunk(ctx.alloc());
+        }
+
+        @Override
+        public ByteBuf readChunk(ByteBufAllocator allocator) {
+            ByteBuf buf = allocator.buffer();
+            buf.writeInt(chunksProduced);
+            chunksProduced++;
+            return buf;
+        }
+
+        @Override
+        public long length() {
+            return chunksToProduce;
+        }
+
+        @Override
+        public long progress() {
+            return chunksProduced;
+        }
+
+        boolean isClosed() {
+            return closed;
+        }
+    }
+
+    private static final class ThrowingChunkedInput implements ChunkedInput<ByteBuf> {
+        private final Exception error;
+
+        private volatile boolean closed;
+
+        ThrowingChunkedInput(Exception error) {
+            this.error = error;
+        }
+
+        @Override
+        public boolean isEndOfInput() {
+            return false;
+        }
+
+        @Override
+        public void close() {
+            closed = true;
+        }
+
+        @Override
+        public ByteBuf readChunk(ChannelHandlerContext ctx) throws Exception {
+            return readChunk(ctx.alloc());
+        }
+
+        @Override
+        public ByteBuf readChunk(ByteBufAllocator allocator) throws Exception {
+            throw error;
+        }
+
+        @Override
+        public long length() {
+            return -1;
+        }
+
+        @Override
+        public long progress() {
+            return -1;
+        }
+
+        boolean isClosed() {
+            return closed;
+        }
+    }
 }
diff --git a/handler/src/test/java/io/netty/handler/timeout/IdleStateEventTest.java b/handler/src/test/java/io/netty/handler/timeout/IdleStateEventTest.java
new file mode 100644
index 0000000..5e2d25e
--- /dev/null
+++ b/handler/src/test/java/io/netty/handler/timeout/IdleStateEventTest.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2019 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.timeout;
+
+import org.junit.Test;
+
+import static io.netty.handler.timeout.IdleStateEvent.*;
+import static org.hamcrest.Matchers.hasToString;
+import static org.junit.Assert.*;
+
+public class IdleStateEventTest {
+    @Test
+    public void testHumanReadableToString() {
+        assertThat(FIRST_READER_IDLE_STATE_EVENT, hasToString("IdleStateEvent(READER_IDLE, first)"));
+        assertThat(READER_IDLE_STATE_EVENT, hasToString("IdleStateEvent(READER_IDLE)"));
+        assertThat(FIRST_WRITER_IDLE_STATE_EVENT, hasToString("IdleStateEvent(WRITER_IDLE, first)"));
+        assertThat(WRITER_IDLE_STATE_EVENT, hasToString("IdleStateEvent(WRITER_IDLE)"));
+        assertThat(FIRST_ALL_IDLE_STATE_EVENT, hasToString("IdleStateEvent(ALL_IDLE, first)"));
+        assertThat(ALL_IDLE_STATE_EVENT, hasToString("IdleStateEvent(ALL_IDLE)"));
+    }
+}
diff --git a/handler/src/test/java/io/netty/handler/timeout/IdleStateHandlerTest.java b/handler/src/test/java/io/netty/handler/timeout/IdleStateHandlerTest.java
index 0a5b627..668f3f1 100644
--- a/handler/src/test/java/io/netty/handler/timeout/IdleStateHandlerTest.java
+++ b/handler/src/test/java/io/netty/handler/timeout/IdleStateHandlerTest.java
@@ -236,6 +236,7 @@ public class IdleStateHandlerTest {
             channel.writeAndFlush(Unpooled.wrappedBuffer(new byte[] { 1 }));
             channel.writeAndFlush(Unpooled.wrappedBuffer(new byte[] { 2 }));
             channel.writeAndFlush(Unpooled.wrappedBuffer(new byte[] { 3 }));
+            channel.writeAndFlush(Unpooled.wrappedBuffer(new byte[5 * 1024]));
 
             // Establish a baseline. We're not consuming anything and let it idle once.
             idleStateHandler.tickRun();
@@ -283,6 +284,30 @@ public class IdleStateHandlerTest {
             assertEquals(0, events.size());
             assertEquals(26L, idleStateHandler.tick(TimeUnit.SECONDS)); // 23s + 2s + 1s
 
+            // Consume part of the message every 2 seconds, then be idle for 1 seconds,
+            // then run the task and we should get an IdleStateEvent because the first trigger
+            idleStateHandler.tick(2L, TimeUnit.SECONDS);
+            assertNotNullAndRelease(channel.consumePart(1024));
+            idleStateHandler.tick(2L, TimeUnit.SECONDS);
+            assertNotNullAndRelease(channel.consumePart(1024));
+            idleStateHandler.tickRun(1L, TimeUnit.SECONDS);
+            assertEquals(1, events.size());
+            assertEquals(31L, idleStateHandler.tick(TimeUnit.SECONDS)); // 26s + 2s + 2s + 1s
+            events.clear();
+
+            // Consume part of the message every 2 seconds, then be idle for 1 seconds,
+            // then consume all the rest of the message, then run the task and we shouldn't
+            // get an IdleStateEvent because the data is flowing and we haven't been idle for long enough!
+            idleStateHandler.tick(2L, TimeUnit.SECONDS);
+            assertNotNullAndRelease(channel.consumePart(1024));
+            idleStateHandler.tick(2L, TimeUnit.SECONDS);
+            assertNotNullAndRelease(channel.consumePart(1024));
+            idleStateHandler.tickRun(1L, TimeUnit.SECONDS);
+            assertEquals(0, events.size());
+            assertEquals(36L, idleStateHandler.tick(TimeUnit.SECONDS)); // 31s + 2s + 2s + 1s
+            idleStateHandler.tick(2L, TimeUnit.SECONDS);
+            assertNotNullAndRelease(channel.consumePart(1024));
+
             // There are no messages left! Advance the ticker by 3 seconds,
             // attempt a consume() but it will be null, then advance the
             // ticker by an another 2 seconds and we should get an IdleStateEvent
@@ -292,7 +317,7 @@ public class IdleStateHandlerTest {
 
             idleStateHandler.tickRun(2L, TimeUnit.SECONDS);
             assertEquals(1, events.size());
-            assertEquals(31L, idleStateHandler.tick(TimeUnit.SECONDS)); // 26s + 3s + 2s
+            assertEquals(43L, idleStateHandler.tick(TimeUnit.SECONDS)); // 36s + 2s + 3s + 2s
 
             // q.e.d.
         } finally {
@@ -317,7 +342,7 @@ public class IdleStateHandlerTest {
 
         private long ticksInNanos;
 
-        public TestableIdleStateHandler(boolean observeOutput,
+        TestableIdleStateHandler(boolean observeOutput,
                 long readerIdleTime, long writerIdleTime, long allIdleTime,
                 TimeUnit unit) {
             super(observeOutput, readerIdleTime, writerIdleTime, allIdleTime, unit);
@@ -369,7 +394,7 @@ public class IdleStateHandlerTest {
 
     private static class ObservableChannel extends EmbeddedChannel {
 
-        public ObservableChannel(ChannelHandler... handlers) {
+        ObservableChannel(ChannelHandler... handlers) {
             super(handlers);
         }
 
@@ -379,7 +404,7 @@ public class IdleStateHandlerTest {
             // the messages in the ChannelOutboundBuffer.
         }
 
-        public Object consume() {
+        private Object consume() {
             ChannelOutboundBuffer buf = unsafe().outboundBuffer();
             if (buf != null) {
                 Object msg = buf.current();
@@ -391,5 +416,24 @@ public class IdleStateHandlerTest {
             }
             return null;
         }
+
+        /**
+         * Consume the part of a message.
+         *
+         * @param byteCount count of byte to be consumed
+         * @return the message currently being consumed
+         */
+        private Object consumePart(int byteCount) {
+            ChannelOutboundBuffer buf = unsafe().outboundBuffer();
+            if (buf != null) {
+                Object msg = buf.current();
+                if (msg != null) {
+                    ReferenceCountUtil.retain(msg);
+                    buf.removeBytes(byteCount);
+                    return msg;
+                }
+            }
+            return null;
+        }
     }
 }
diff --git a/license/LICENSE.dnsinfo.txt b/license/LICENSE.dnsinfo.txt
new file mode 100644
index 0000000..7554838
--- /dev/null
+++ b/license/LICENSE.dnsinfo.txt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2004-2006, 2008, 2009, 2011 Apple Inc. All rights reserved.
+ *
+ * @APPLE_LICENSE_HEADER_START@
+ * 
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apple Public Source License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://www.opensource.apple.com/apsl/ and read it before using this
+ * file.
+ * 
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ * 
+ * @APPLE_LICENSE_HEADER_END@
+ */
diff --git a/license/LICENSE.hyper-hpack.txt b/license/LICENSE.hyper-hpack.txt
new file mode 100644
index 0000000..d24c351
--- /dev/null
+++ b/license/LICENSE.hyper-hpack.txt
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Cory Benfield
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/license/LICENSE.jboss-marshalling.txt b/license/LICENSE.jboss-marshalling.txt
index d80fdbe..e454a52 100644
--- a/license/LICENSE.jboss-marshalling.txt
+++ b/license/LICENSE.jboss-marshalling.txt
@@ -1,504 +1,178 @@
 
-	            GNU LESSER GENERAL PUBLIC LICENSE
-		       Version 2.1, February 1999
-
- Copyright (C) 1991, 1999 Free Software Foundation, Inc.
- 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
-[This is the first released version of the Lesser GPL.  It also counts
- as the successor of the GNU Library Public License, version 2, hence
- the version number 2.1.]
-
-			    Preamble
-
-  The licenses for most software are designed to take away your
-freedom to share and change it.  By contrast, the GNU General Public
-Licenses are intended to guarantee your freedom to share and change
-free software--to make sure the software is free for all its users.
-
-  This license, the Lesser General Public License, applies to some
-specially designated software packages--typically libraries--of the
-Free Software Foundation and other authors who decide to use it.  You
-can use it too, but we suggest you first think carefully about whether
-this license or the ordinary General Public License is the better
-strategy to use in any particular case, based on the explanations below.
-
-  When we speak of free software, we are referring to freedom of use,
-not price.  Our General Public Licenses are designed to make sure that
-you have the freedom to distribute copies of free software (and charge
-for this service if you wish); that you receive source code or can get
-it if you want it; that you can change the software and use pieces of
-it in new free programs; and that you are informed that you can do
-these things.
-
-  To protect your rights, we need to make restrictions that forbid
-distributors to deny you these rights or to ask you to surrender these
-rights.  These restrictions translate to certain responsibilities for
-you if you distribute copies of the library or if you modify it.
-
-  For example, if you distribute copies of the library, whether gratis
-or for a fee, you must give the recipients all the rights that we gave
-you.  You must make sure that they, too, receive or can get the source
-code.  If you link other code with the library, you must provide
-complete object files to the recipients, so that they can relink them
-with the library after making changes to the library and recompiling
-it.  And you must show them these terms so they know their rights.
-
-  We protect your rights with a two-step method: (1) we copyright the
-library, and (2) we offer you this license, which gives you legal
-permission to copy, distribute and/or modify the library.
-
-  To protect each distributor, we want to make it very clear that
-there is no warranty for the free library.  Also, if the library is
-modified by someone else and passed on, the recipients should know
-that what they have is not the original version, so that the original
-author's reputation will not be affected by problems that might be
-introduced by others.
-
-  Finally, software patents pose a constant threat to the existence of
-any free program.  We wish to make sure that a company cannot
-effectively restrict the users of a free program by obtaining a
-restrictive license from a patent holder.  Therefore, we insist that
-any patent license obtained for a version of the library must be
-consistent with the full freedom of use specified in this license.
-
-  Most GNU software, including some libraries, is covered by the
-ordinary GNU General Public License.  This license, the GNU Lesser
-General Public License, applies to certain designated libraries, and
-is quite different from the ordinary General Public License.  We use
-this license for certain libraries in order to permit linking those
-libraries into non-free programs.
-
-  When a program is linked with a library, whether statically or using
-a shared library, the combination of the two is legally speaking a
-combined work, a derivative of the original library.  The ordinary
-General Public License therefore permits such linking only if the
-entire combination fits its criteria of freedom.  The Lesser General
-Public License permits more lax criteria for linking other code with
-the library.
-
-  We call this license the "Lesser" General Public License because it
-does Less to protect the user's freedom than the ordinary General
-Public License.  It also provides other free software developers Less
-of an advantage over competing non-free programs.  These disadvantages
-are the reason we use the ordinary General Public License for many
-libraries.  However, the Lesser license provides advantages in certain
-special circumstances.
-
-  For example, on rare occasions, there may be a special need to
-encourage the widest possible use of a certain library, so that it becomes
-a de-facto standard.  To achieve this, non-free programs must be
-allowed to use the library.  A more frequent case is that a free
-library does the same job as widely used non-free libraries.  In this
-case, there is little to gain by limiting the free library to free
-software only, so we use the Lesser General Public License.
-
-  In other cases, permission to use a particular library in non-free
-programs enables a greater number of people to use a large body of
-free software.  For example, permission to use the GNU C Library in
-non-free programs enables many more people to use the whole GNU
-operating system, as well as its variant, the GNU/Linux operating
-system.
-
-  Although the Lesser General Public License is Less protective of the
-users' freedom, it does ensure that the user of a program that is
-linked with the Library has the freedom and the wherewithal to run
-that program using a modified version of the Library.
-
-  The precise terms and conditions for copying, distribution and
-modification follow.  Pay close attention to the difference between a
-"work based on the library" and a "work that uses the library".  The
-former contains code derived from the library, whereas the latter must
-be combined with the library in order to run.
-
-		  GNU LESSER GENERAL PUBLIC LICENSE
-   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
-
-  0. This License Agreement applies to any software library or other
-program which contains a notice placed by the copyright holder or
-other authorized party saying it may be distributed under the terms of
-this Lesser General Public License (also called "this License").
-Each licensee is addressed as "you".
-
-  A "library" means a collection of software functions and/or data
-prepared so as to be conveniently linked with application programs
-(which use some of those functions and data) to form executables.
-
-  The "Library", below, refers to any such software library or work
-which has been distributed under these terms.  A "work based on the
-Library" means either the Library or any derivative work under
-copyright law: that is to say, a work containing the Library or a
-portion of it, either verbatim or with modifications and/or translated
-straightforwardly into another language.  (Hereinafter, translation is
-included without limitation in the term "modification".)
-
-  "Source code" for a work means the preferred form of the work for
-making modifications to it.  For a library, complete source code means
-all the source code for all modules it contains, plus any associated
-interface definition files, plus the scripts used to control compilation
-and installation of the library.
-
-  Activities other than copying, distribution and modification are not
-covered by this License; they are outside its scope.  The act of
-running a program using the Library is not restricted, and output from
-such a program is covered only if its contents constitute a work based
-on the Library (independent of the use of the Library in a tool for
-writing it).  Whether that is true depends on what the Library does
-and what the program that uses the Library does.
-  
-  1. You may copy and distribute verbatim copies of the Library's
-complete source code as you receive it, in any medium, provided that
-you conspicuously and appropriately publish on each copy an
-appropriate copyright notice and disclaimer of warranty; keep intact
-all the notices that refer to this License and to the absence of any
-warranty; and distribute a copy of this License along with the
-Library.
-
-  You may charge a fee for the physical act of transferring a copy,
-and you may at your option offer warranty protection in exchange for a
-fee.
-
-  2. You may modify your copy or copies of the Library or any portion
-of it, thus forming a work based on the Library, and copy and
-distribute such modifications or work under the terms of Section 1
-above, provided that you also meet all of these conditions:
-
-    a) The modified work must itself be a software library.
-
-    b) You must cause the files modified to carry prominent notices
-    stating that you changed the files and the date of any change.
-
-    c) You must cause the whole of the work to be licensed at no
-    charge to all third parties under the terms of this License.
-
-    d) If a facility in the modified Library refers to a function or a
-    table of data to be supplied by an application program that uses
-    the facility, other than as an argument passed when the facility
-    is invoked, then you must make a good faith effort to ensure that,
-    in the event an application does not supply such function or
-    table, the facility still operates, and performs whatever part of
-    its purpose remains meaningful.
-
-    (For example, a function in a library to compute square roots has
-    a purpose that is entirely well-defined independent of the
-    application.  Therefore, Subsection 2d requires that any
-    application-supplied function or table used by this function must
-    be optional: if the application does not supply it, the square
-    root function must still compute square roots.)
-
-These requirements apply to the modified work as a whole.  If
-identifiable sections of that work are not derived from the Library,
-and can be reasonably considered independent and separate works in
-themselves, then this License, and its terms, do not apply to those
-sections when you distribute them as separate works.  But when you
-distribute the same sections as part of a whole which is a work based
-on the Library, the distribution of the whole must be on the terms of
-this License, whose permissions for other licensees extend to the
-entire whole, and thus to each and every part regardless of who wrote
-it.
-
-Thus, it is not the intent of this section to claim rights or contest
-your rights to work written entirely by you; rather, the intent is to
-exercise the right to control the distribution of derivative or
-collective works based on the Library.
-
-In addition, mere aggregation of another work not based on the Library
-with the Library (or with a work based on the Library) on a volume of
-a storage or distribution medium does not bring the other work under
-the scope of this License.
-
-  3. You may opt to apply the terms of the ordinary GNU General Public
-License instead of this License to a given copy of the Library.  To do
-this, you must alter all the notices that refer to this License, so
-that they refer to the ordinary GNU General Public License, version 2,
-instead of to this License.  (If a newer version than version 2 of the
-ordinary GNU General Public License has appeared, then you can specify
-that version instead if you wish.)  Do not make any other change in
-these notices.
-
-  Once this change is made in a given copy, it is irreversible for
-that copy, so the ordinary GNU General Public License applies to all
-subsequent copies and derivative works made from that copy.
-
-  This option is useful when you wish to copy part of the code of
-the Library into a program that is not a library.
-
-  4. You may copy and distribute the Library (or a portion or
-derivative of it, under Section 2) in object code or executable form
-under the terms of Sections 1 and 2 above provided that you accompany
-it with the complete corresponding machine-readable source code, which
-must be distributed under the terms of Sections 1 and 2 above on a
-medium customarily used for software interchange.
-
-  If distribution of object code is made by offering access to copy
-from a designated place, then offering equivalent access to copy the
-source code from the same place satisfies the requirement to
-distribute the source code, even though third parties are not
-compelled to copy the source along with the object code.
-
-  5. A program that contains no derivative of any portion of the
-Library, but is designed to work with the Library by being compiled or
-linked with it, is called a "work that uses the Library".  Such a
-work, in isolation, is not a derivative work of the Library, and
-therefore falls outside the scope of this License.
-
-  However, linking a "work that uses the Library" with the Library
-creates an executable that is a derivative of the Library (because it
-contains portions of the Library), rather than a "work that uses the
-library".  The executable is therefore covered by this License.
-Section 6 states terms for distribution of such executables.
-
-  When a "work that uses the Library" uses material from a header file
-that is part of the Library, the object code for the work may be a
-derivative work of the Library even though the source code is not.
-Whether this is true is especially significant if the work can be
-linked without the Library, or if the work is itself a library.  The
-threshold for this to be true is not precisely defined by law.
-
-  If such an object file uses only numerical parameters, data
-structure layouts and accessors, and small macros and small inline
-functions (ten lines or less in length), then the use of the object
-file is unrestricted, regardless of whether it is legally a derivative
-work.  (Executables containing this object code plus portions of the
-Library will still fall under Section 6.)
-
-  Otherwise, if the work is a derivative of the Library, you may
-distribute the object code for the work under the terms of Section 6.
-Any executables containing that work also fall under Section 6,
-whether or not they are linked directly with the Library itself.
-
-  6. As an exception to the Sections above, you may also combine or
-link a "work that uses the Library" with the Library to produce a
-work containing portions of the Library, and distribute that work
-under terms of your choice, provided that the terms permit
-modification of the work for the customer's own use and reverse
-engineering for debugging such modifications.
-
-  You must give prominent notice with each copy of the work that the
-Library is used in it and that the Library and its use are covered by
-this License.  You must supply a copy of this License.  If the work
-during execution displays copyright notices, you must include the
-copyright notice for the Library among them, as well as a reference
-directing the user to the copy of this License.  Also, you must do one
-of these things:
-
-    a) Accompany the work with the complete corresponding
-    machine-readable source code for the Library including whatever
-    changes were used in the work (which must be distributed under
-    Sections 1 and 2 above); and, if the work is an executable linked
-    with the Library, with the complete machine-readable "work that
-    uses the Library", as object code and/or source code, so that the
-    user can modify the Library and then relink to produce a modified
-    executable containing the modified Library.  (It is understood
-    that the user who changes the contents of definitions files in the
-    Library will not necessarily be able to recompile the application
-    to use the modified definitions.)
-
-    b) Use a suitable shared library mechanism for linking with the
-    Library.  A suitable mechanism is one that (1) uses at run time a
-    copy of the library already present on the user's computer system,
-    rather than copying library functions into the executable, and (2)
-    will operate properly with a modified version of the library, if
-    the user installs one, as long as the modified version is
-    interface-compatible with the version that the work was made with.
-
-    c) Accompany the work with a written offer, valid for at
-    least three years, to give the same user the materials
-    specified in Subsection 6a, above, for a charge no more
-    than the cost of performing this distribution.
-
-    d) If distribution of the work is made by offering access to copy
-    from a designated place, offer equivalent access to copy the above
-    specified materials from the same place.
-
-    e) Verify that the user has already received a copy of these
-    materials or that you have already sent this user a copy.
-
-  For an executable, the required form of the "work that uses the
-Library" must include any data and utility programs needed for
-reproducing the executable from it.  However, as a special exception,
-the materials to be distributed need not include anything that is
-normally distributed (in either source or binary form) with the major
-components (compiler, kernel, and so on) of the operating system on
-which the executable runs, unless that component itself accompanies
-the executable.
-
-  It may happen that this requirement contradicts the license
-restrictions of other proprietary libraries that do not normally
-accompany the operating system.  Such a contradiction means you cannot
-use both them and the Library together in an executable that you
-distribute.
-
-  7. You may place library facilities that are a work based on the
-Library side-by-side in a single library together with other library
-facilities not covered by this License, and distribute such a combined
-library, provided that the separate distribution of the work based on
-the Library and of the other library facilities is otherwise
-permitted, and provided that you do these two things:
-
-    a) Accompany the combined library with a copy of the same work
-    based on the Library, uncombined with any other library
-    facilities.  This must be distributed under the terms of the
-    Sections above.
-
-    b) Give prominent notice with the combined library of the fact
-    that part of it is a work based on the Library, and explaining
-    where to find the accompanying uncombined form of the same work.
-
-  8. You may not copy, modify, sublicense, link with, or distribute
-the Library except as expressly provided under this License.  Any
-attempt otherwise to copy, modify, sublicense, link with, or
-distribute the Library is void, and will automatically terminate your
-rights under this License.  However, parties who have received copies,
-or rights, from you under this License will not have their licenses
-terminated so long as such parties remain in full compliance.
-
-  9. You are not required to accept this License, since you have not
-signed it.  However, nothing else grants you permission to modify or
-distribute the Library or its derivative works.  These actions are
-prohibited by law if you do not accept this License.  Therefore, by
-modifying or distributing the Library (or any work based on the
-Library), you indicate your acceptance of this License to do so, and
-all its terms and conditions for copying, distributing or modifying
-the Library or works based on it.
-
-  10. Each time you redistribute the Library (or any work based on the
-Library), the recipient automatically receives a license from the
-original licensor to copy, distribute, link with or modify the Library
-subject to these terms and conditions.  You may not impose any further
-restrictions on the recipients' exercise of the rights granted herein.
-You are not responsible for enforcing compliance by third parties with
-this License.
-
-  11. If, as a consequence of a court judgment or allegation of patent
-infringement or for any other reason (not limited to patent issues),
-conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License.  If you cannot
-distribute so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you
-may not distribute the Library at all.  For example, if a patent
-license would not permit royalty-free redistribution of the Library by
-all those who receive copies directly or indirectly through you, then
-the only way you could satisfy both it and this License would be to
-refrain entirely from distribution of the Library.
-
-If any portion of this section is held invalid or unenforceable under any
-particular circumstance, the balance of the section is intended to apply,
-and the section as a whole is intended to apply in other circumstances.
-
-It is not the purpose of this section to induce you to infringe any
-patents or other property right claims or to contest validity of any
-such claims; this section has the sole purpose of protecting the
-integrity of the free software distribution system which is
-implemented by public license practices.  Many people have made
-generous contributions to the wide range of software distributed
-through that system in reliance on consistent application of that
-system; it is up to the author/donor to decide if he or she is willing
-to distribute software through any other system and a licensee cannot
-impose that choice.
-
-This section is intended to make thoroughly clear what is believed to
-be a consequence of the rest of this License.
-
-  12. If the distribution and/or use of the Library is restricted in
-certain countries either by patents or by copyrighted interfaces, the
-original copyright holder who places the Library under this License may add
-an explicit geographical distribution limitation excluding those countries,
-so that distribution is permitted only in or among countries not thus
-excluded.  In such case, this License incorporates the limitation as if
-written in the body of this License.
-
-  13. The Free Software Foundation may publish revised and/or new
-versions of the Lesser General Public License from time to time.
-Such new versions will be similar in spirit to the present version,
-but may differ in detail to address new problems or concerns.
-
-Each version is given a distinguishing version number.  If the Library
-specifies a version number of this License which applies to it and
-"any later version", you have the option of following the terms and
-conditions either of that version or of any later version published by
-the Free Software Foundation.  If the Library does not specify a
-license version number, you may choose any version ever published by
-the Free Software Foundation.
-
-  14. If you wish to incorporate parts of the Library into other free
-programs whose distribution conditions are incompatible with these,
-write to the author to ask for permission.  For software which is
-copyrighted by the Free Software Foundation, write to the Free
-Software Foundation; we sometimes make exceptions for this.  Our
-decision will be guided by the two goals of preserving the free status
-of all derivatives of our free software and of promoting the sharing
-and reuse of software generally.
-
-			    NO WARRANTY
-
-  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
-WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
-EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
-OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
-KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
-LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
-THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
-  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
-WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
-AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
-FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
-CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
-LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
-RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
-FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
-SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
-DAMAGES.
-
-		     END OF TERMS AND CONDITIONS
-
-           How to Apply These Terms to Your New Libraries
-
-  If you develop a new library, and you want it to be of the greatest
-possible use to the public, we recommend making it free software that
-everyone can redistribute and change.  You can do so by permitting
-redistribution under these terms (or, alternatively, under the terms of the
-ordinary General Public License).
-
-  To apply these terms, attach the following notices to the library.  It is
-safest to attach them to the start of each source file to most effectively
-convey the exclusion of warranty; and each file should have at least the
-"copyright" line and a pointer to where the full notice is found.
-
-    <one line to give the library's name and a brief idea of what it does.>
-    Copyright (C) <year>  <name of author>
-
-    This library is free software; you can redistribute it and/or
-    modify it under the terms of the GNU Lesser General Public
-    License as published by the Free Software Foundation; either
-    version 2.1 of the License, or (at your option) any later version.
-
-    This library is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-    Lesser General Public License for more details.
-
-    You should have received a copy of the GNU Lesser General Public
-    License along with this library; if not, write to the Free Software
-    Foundation, Inc., 51 Franklin Street, 5th Floor, Boston, MA 02110-1301 USA
-
-Also add information on how to contact you by electronic and paper mail.
-
-You should also get your employer (if you work as a programmer) or your
-school, if any, to sign a "copyright disclaimer" for the library, if
-necessary.  Here is a sample; alter the names:
-
-  Yoyodyne, Inc., hereby disclaims all copyright interest in the
-  library `Frob' (a library for tweaking knobs) written by James Random Hacker.
-
-  <signature of Ty Coon>, 1 April 1990
-  Ty Coon, President of Vice
-
-That's all there is to it!
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
 
diff --git a/license/LICENSE.nghttp2-hpack.txt b/license/LICENSE.nghttp2-hpack.txt
new file mode 100644
index 0000000..8020179
--- /dev/null
+++ b/license/LICENSE.nghttp2-hpack.txt
@@ -0,0 +1,23 @@
+The MIT License
+
+Copyright (c) 2012, 2014, 2015, 2016 Tatsuhiro Tsujikawa
+Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/microbench/README.md b/microbench/README.md
index b109a14..4505f1d 100644
--- a/microbench/README.md
+++ b/microbench/README.md
@@ -1,4 +1,4 @@
 ## Microbenchmark tests
 
-See [our wiki page](http://netty.io/wiki/microbenchmarks.html).
+See [our wiki page](https://netty.io/wiki/microbenchmarks.html).
 
diff --git a/microbench/pom.xml b/microbench/pom.xml
index f2ce65a..acc7472 100644
--- a/microbench/pom.xml
+++ b/microbench/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>io.netty</groupId>
     <artifactId>netty-parent</artifactId>
-    <version>4.1.33.Final</version>
+    <version>4.1.48.Final</version>
   </parent>
 
   <artifactId>netty-microbench</artifactId>
@@ -31,15 +31,15 @@
   <properties>
     <!-- Skip tests by default; run only if -DskipTests=false is specified -->
     <skipTests>true</skipTests>
-    <jmh.version>1.21</jmh.version>
+    <jmh.version>1.22</jmh.version>
     <!-- This only be set when run on linux as on other platforms we just want to include the jar without native
          code -->
     <epoll.classifier />
     <!-- This only be set when run on mac as on other platforms we just want to include the jar without native
          code -->
     <kqueue.classifier />
+    <skipJapicmp>true</skipJapicmp>
   </properties>
-
   <profiles>
     <profile>
       <id>linux</id>
diff --git a/microbench/src/main/java/io/netty/buffer/AbstractReferenceCountedByteBufBenchmark.java b/microbench/src/main/java/io/netty/buffer/AbstractReferenceCountedByteBufBenchmark.java
index 985eb70..a9a2677 100644
--- a/microbench/src/main/java/io/netty/buffer/AbstractReferenceCountedByteBufBenchmark.java
+++ b/microbench/src/main/java/io/netty/buffer/AbstractReferenceCountedByteBufBenchmark.java
@@ -49,7 +49,7 @@ public class AbstractReferenceCountedByteBufBenchmark extends AbstractMicrobench
     }
 
     @Benchmark
-    @BenchmarkMode(Mode.SampleTime)
+    @BenchmarkMode(Mode.AverageTime)
     @OutputTimeUnit(TimeUnit.NANOSECONDS)
     public boolean retainReleaseUncontended() {
         buf.retain();
@@ -58,9 +58,9 @@ public class AbstractReferenceCountedByteBufBenchmark extends AbstractMicrobench
     }
 
     @Benchmark
-    @BenchmarkMode(Mode.SampleTime)
+    @BenchmarkMode(Mode.AverageTime)
     @OutputTimeUnit(TimeUnit.NANOSECONDS)
-    @GroupThreads(6)
+    @GroupThreads(4)
     public boolean retainReleaseContended() {
         buf.retain();
         Blackhole.consumeCPU(delay);
diff --git a/microbench/src/main/java/io/netty/buffer/ByteBufAccessBenchmark.java b/microbench/src/main/java/io/netty/buffer/ByteBufAccessBenchmark.java
new file mode 100644
index 0000000..1271895
--- /dev/null
+++ b/microbench/src/main/java/io/netty/buffer/ByteBufAccessBenchmark.java
@@ -0,0 +1,165 @@
+/*
+* Copyright 2019 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.buffer;
+
+import java.nio.ByteBuffer;
+import java.util.concurrent.TimeUnit;
+
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.annotations.Warmup;
+
+import io.netty.microbench.util.AbstractMicrobenchmark;
+import io.netty.util.internal.PlatformDependent;
+
+@Warmup(iterations = 5, time = 1500, timeUnit = TimeUnit.MILLISECONDS)
+@Measurement(iterations = 10, time = 1500, timeUnit = TimeUnit.MILLISECONDS)
+@Fork(3)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+public class ByteBufAccessBenchmark extends AbstractMicrobenchmark {
+
+    static final class NioFacade extends WrappedByteBuf {
+        private final ByteBuffer byteBuffer;
+        NioFacade(ByteBuffer byteBuffer) {
+            super(Unpooled.EMPTY_BUFFER);
+            this.byteBuffer = byteBuffer;
+        }
+        @Override
+        public ByteBuf setLong(int index, long value) {
+            byteBuffer.putLong(index, value);
+            return this;
+        }
+        @Override
+        public long getLong(int index) {
+            return byteBuffer.getLong(index);
+        }
+        @Override
+        public byte readByte() {
+            return byteBuffer.get();
+        }
+        @Override
+        public ByteBuf touch() {
+            // hack since WrappedByteBuf.readerIndex(int) is final
+            byteBuffer.position(0);
+            return this;
+        }
+        @Override
+        public boolean release() {
+            PlatformDependent.freeDirectBuffer(byteBuffer);
+            return true;
+        }
+    }
+
+    public enum ByteBufType {
+        UNSAFE {
+            @Override
+            ByteBuf newBuffer() {
+                return new UnpooledUnsafeDirectByteBuf(
+                        UnpooledByteBufAllocator.DEFAULT, 64, 64).setIndex(0, 64);
+            }
+        },
+        UNSAFE_SLICE {
+            @Override
+            ByteBuf newBuffer() {
+                return UNSAFE.newBuffer().slice(16, 48);
+            }
+        },
+        HEAP {
+            @Override
+            ByteBuf newBuffer() {
+                return new UnpooledUnsafeHeapByteBuf(
+                        UnpooledByteBufAllocator.DEFAULT, 64, 64).setIndex(0,  64);
+            }
+        },
+        COMPOSITE {
+            @Override
+            ByteBuf newBuffer() {
+                return Unpooled.wrappedBuffer(UNSAFE.newBuffer(), HEAP.newBuffer());
+            }
+        },
+        NIO {
+            @Override
+            ByteBuf newBuffer() {
+                return new NioFacade(ByteBuffer.allocateDirect(64));
+            }
+        };
+        abstract ByteBuf newBuffer();
+    }
+
+    @Param
+    public ByteBufType bufferType;
+
+    @Param({ "true", "false" })
+    public String checkAccessible;
+
+    @Param({ "true", "false" })
+    public String checkBounds;
+
+    @Param({ "8" })
+    public int batchSize; // applies only to readBatch benchmark
+
+    @Setup
+    public void setup() {
+        System.setProperty("io.netty.buffer.checkAccessible", checkAccessible);
+        System.setProperty("io.netty.buffer.checkBounds", checkBounds);
+        buffer = bufferType.newBuffer();
+    }
+
+    private ByteBuf buffer;
+
+    @TearDown
+    public void tearDown() {
+        buffer.release();
+        System.clearProperty("io.netty.buffer.checkAccessible");
+        System.clearProperty("io.netty.buffer.checkBounds");
+    }
+
+    @Benchmark
+    public long setGetLong() {
+        return buffer.setLong(0, 1).getLong(0);
+    }
+
+    @Benchmark
+    public ByteBuf setLong() {
+        return buffer.setLong(0, 1);
+    }
+
+    @Benchmark
+    public int readBatch() {
+        buffer.readerIndex(0).touch();
+        int result = 0;
+        // WARNING!
+        // Please do not replace this sum loop with a BlackHole::consume loop:
+        // BlackHole::consume could prevent the JVM to perform certain optimizations
+        // forcing ByteBuf::readByte to be executed in order.
+        // The purpose of the benchmark is to mimic accesses on ByteBuf
+        // as in a real (single-threaded) case ie without (compiler) memory barriers that would
+        // disable certain optimizations or would make bounds checks (if enabled)
+        // to happen on each access.
+        for (int i = 0, size = batchSize; i < size; i++) {
+            result += buffer.readByte();
+        }
+        return result;
+    }
+}
diff --git a/microbench/src/main/java/io/netty/handler/codec/DateFormatter2Benchmark.java b/microbench/src/main/java/io/netty/handler/codec/DateFormatter2Benchmark.java
new file mode 100644
index 0000000..dd44ed6
--- /dev/null
+++ b/microbench/src/main/java/io/netty/handler/codec/DateFormatter2Benchmark.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2019 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;
+
+import io.netty.microbench.util.AbstractMicrobenchmark;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.annotations.Warmup;
+
+import java.util.Date;
+
+@Threads(1)
+@Warmup(iterations = 5)
+@Measurement(iterations = 5)
+public class DateFormatter2Benchmark extends AbstractMicrobenchmark {
+
+    @Param({"Sun, 27 Jan 2016 19:18:46 GMT", "Sun, 27 Dec 2016 19:18:46 GMT"})
+    String DATE_STRING;
+
+    @Benchmark
+    public Date parseHttpHeaderDateFormatterNew() {
+        return DateFormatter.parseHttpDate(DATE_STRING);
+    }
+
+    /*
+    @Benchmark
+    public Date parseHttpHeaderDateFormatter() {
+        return DateFormatterOld.parseHttpDate(DATE_STRING);
+    }
+    */
+
+    /*
+     * Benchmark                        (DATE_STRING)   Mode  Cnt        Score       Error  Units
+     * parseHttpHeaderDateFormatter     Sun, 27 Jan 2016 19:18:46 GMT  thrpt    6  4142781.221 ± 82155.002  ops/s
+     * parseHttpHeaderDateFormatter     Sun, 27 Dec 2016 19:18:46 GMT  thrpt    6  3781810.558 ± 38679.061  ops/s
+     * parseHttpHeaderDateFormatterNew  Sun, 27 Jan 2016 19:18:46 GMT  thrpt    6  4372569.705 ± 30257.537  ops/s
+     * parseHttpHeaderDateFormatterNew  Sun, 27 Dec 2016 19:18:46 GMT  thrpt    6  4339785.100 ± 57542.660  ops/s
+     */
+
+    /*Old DateFormatter.tryParseMonth method:
+    private boolean tryParseMonth(CharSequence txt, int tokenStart, int tokenEnd) {
+        int len = tokenEnd - tokenStart;
+
+        if (len != 3) {
+            return false;
+        }
+
+        if (matchMonth("Jan", txt, tokenStart)) {
+            month = Calendar.JANUARY;
+        } else if (matchMonth("Feb", txt, tokenStart)) {
+            month = Calendar.FEBRUARY;
+        } else if (matchMonth("Mar", txt, tokenStart)) {
+            month = Calendar.MARCH;
+        } else if (matchMonth("Apr", txt, tokenStart)) {
+            month = Calendar.APRIL;
+        } else if (matchMonth("May", txt, tokenStart)) {
+            month = Calendar.MAY;
+        } else if (matchMonth("Jun", txt, tokenStart)) {
+            month = Calendar.JUNE;
+        } else if (matchMonth("Jul", txt, tokenStart)) {
+            month = Calendar.JULY;
+        } else if (matchMonth("Aug", txt, tokenStart)) {
+            month = Calendar.AUGUST;
+        } else if (matchMonth("Sep", txt, tokenStart)) {
+            month = Calendar.SEPTEMBER;
+        } else if (matchMonth("Oct", txt, tokenStart)) {
+            month = Calendar.OCTOBER;
+        } else if (matchMonth("Nov", txt, tokenStart)) {
+            month = Calendar.NOVEMBER;
+        } else if (matchMonth("Dec", txt, tokenStart)) {
+            month = Calendar.DECEMBER;
+        } else {
+            return false;
+        }
+
+        return true;
+    }
+    */
+
+}
diff --git a/microbench/src/main/java/io/netty/handler/codec/http/DecodeHexBenchmark.java b/microbench/src/main/java/io/netty/handler/codec/http/DecodeHexBenchmark.java
new file mode 100644
index 0000000..61afbac
--- /dev/null
+++ b/microbench/src/main/java/io/netty/handler/codec/http/DecodeHexBenchmark.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2019 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.http;
+
+import io.netty.microbench.util.AbstractMicrobenchmark;
+import io.netty.util.internal.PlatformDependent;
+import io.netty.util.internal.StringUtil;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
+
+@State(Scope.Benchmark)
+@Warmup(iterations = 5, time = 1)
+@Measurement(iterations = 5, time = 1)
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+public class DecodeHexBenchmark extends AbstractMicrobenchmark {
+
+    @Param({
+            //with HEX chars
+            "135aBa9BBCEA030b947d79fCcaf48Bde",
+            //with HEX chars + 'g'
+            "4DDeA5gDD1C6fE567E1b6gf0C40FEcDg",
+    })
+    private String hex;
+    private char[] hexDigits;
+
+    @Setup
+    public void init() {
+        hexDigits = hex.toCharArray();
+    }
+
+    @Benchmark
+    public long hexDigits() {
+        long v = 0;
+        final char[] hexDigits = this.hexDigits;
+        for (int i = 0, size = hexDigits.length; i < size; i++) {
+            v += StringUtil.decodeHexNibble(hexDigits[i]);
+        }
+        return v;
+    }
+
+    @Benchmark
+    public long hexDigitsWithChecks() {
+        long v = 0;
+        final char[] hexDigits = this.hexDigits;
+        for (int i = 0, size = hexDigits.length; i < size; i++) {
+            v += decodeHexNibbleWithCheck(hexDigits[i]);
+        }
+        return v;
+    }
+
+    @Benchmark
+    public long hexDigitsOriginal() {
+        long v = 0;
+        final char[] hexDigits = this.hexDigits;
+        for (int i = 0, size = hexDigits.length; i < size; i++) {
+            v += decodeHexNibble(hexDigits[i]);
+        }
+        return v;
+    }
+
+    private static int decodeHexNibble(final char c) {
+        if (c >= '0' && c <= '9') {
+            return c - '0';
+        }
+        if (c >= 'A' && c <= 'F') {
+            return c - ('A' - 0xA);
+        }
+        if (c >= 'a' && c <= 'f') {
+            return c - ('a' - 0xA);
+        }
+        return -1;
+    }
+
+    private static final byte[] HEX2B;
+
+    static {
+        HEX2B = new byte['f' + 1];
+        Arrays.fill(HEX2B, (byte) -1);
+        HEX2B['0'] = (byte) 0;
+        HEX2B['1'] = (byte) 1;
+        HEX2B['2'] = (byte) 2;
+        HEX2B['3'] = (byte) 3;
+        HEX2B['4'] = (byte) 4;
+        HEX2B['5'] = (byte) 5;
+        HEX2B['6'] = (byte) 6;
+        HEX2B['7'] = (byte) 7;
+        HEX2B['8'] = (byte) 8;
+        HEX2B['9'] = (byte) 9;
+        HEX2B['A'] = (byte) 10;
+        HEX2B['B'] = (byte) 11;
+        HEX2B['C'] = (byte) 12;
+        HEX2B['D'] = (byte) 13;
+        HEX2B['E'] = (byte) 14;
+        HEX2B['F'] = (byte) 15;
+        HEX2B['a'] = (byte) 10;
+        HEX2B['b'] = (byte) 11;
+        HEX2B['c'] = (byte) 12;
+        HEX2B['d'] = (byte) 13;
+        HEX2B['e'] = (byte) 14;
+        HEX2B['f'] = (byte) 15;
+    }
+
+    private static int decodeHexNibbleWithCheck(final char c) {
+        final int index = c;
+        if (index >= HEX2B.length) {
+            return -1;
+        }
+        if (PlatformDependent.hasUnsafe()) {
+            return PlatformDependent.getByte(HEX2B, index);
+        }
+        return HEX2B[index];
+    }
+
+}
diff --git a/microbench/src/main/java/io/netty/handler/codec/http/HttpMethodMapBenchmark.java b/microbench/src/main/java/io/netty/handler/codec/http/HttpMethodMapBenchmark.java
index 69dae0f..08bd84e 100644
--- a/microbench/src/main/java/io/netty/handler/codec/http/HttpMethodMapBenchmark.java
+++ b/microbench/src/main/java/io/netty/handler/codec/http/HttpMethodMapBenchmark.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2017 The Netty Project
+ * Copyright 2019 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
@@ -22,6 +22,7 @@ import org.openjdk.jmh.annotations.OutputTimeUnit;
 import org.openjdk.jmh.annotations.Scope;
 import org.openjdk.jmh.annotations.State;
 import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.infra.Blackhole;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -151,68 +152,56 @@ public class HttpMethodMapBenchmark extends AbstractMicrobenchmark {
     }
 
     @Benchmark
-    public int oldMapKnownMethods() throws Exception {
-        int x = 0;
+    public void oldMapKnownMethods(Blackhole bh) throws Exception {
         for (int i = 0; i < KNOWN_METHODS.length; ++i) {
-            x += OLD_MAP.get(KNOWN_METHODS[i]).toString().length();
+            bh.consume(OLD_MAP.get(KNOWN_METHODS[i]));
         }
-        return x;
     }
 
     @Benchmark
-    public int newMapKnownMethods() throws Exception {
-        int x = 0;
+    public void newMapKnownMethods(Blackhole bh) throws Exception {
         for (int i = 0; i < KNOWN_METHODS.length; ++i) {
-            x += NEW_MAP.get(KNOWN_METHODS[i]).toString().length();
+            bh.consume(NEW_MAP.get(KNOWN_METHODS[i]));
         }
-        return x;
     }
 
     @Benchmark
-    public int oldMapMixMethods() throws Exception {
-        int x = 0;
+    public void oldMapMixMethods(Blackhole bh) throws Exception {
         for (int i = 0; i < MIXED_METHODS.length; ++i) {
             HttpMethod method = OLD_MAP.get(MIXED_METHODS[i]);
             if (method != null) {
-                x += method.toString().length();
+                bh.consume(method);
             }
         }
-        return x;
     }
 
     @Benchmark
-    public int newMapMixMethods() throws Exception {
-        int x = 0;
+    public void newMapMixMethods(Blackhole bh) throws Exception {
         for (int i = 0; i < MIXED_METHODS.length; ++i) {
             HttpMethod method = NEW_MAP.get(MIXED_METHODS[i]);
             if (method != null) {
-                x += method.toString().length();
+                bh.consume(method);
             }
         }
-        return x;
     }
 
     @Benchmark
-    public int oldMapUnknownMethods() throws Exception {
-        int x = 0;
+    public void oldMapUnknownMethods(Blackhole bh) throws Exception {
         for (int i = 0; i < UNKNOWN_METHODS.length; ++i) {
             HttpMethod method = OLD_MAP.get(UNKNOWN_METHODS[i]);
             if (method != null) {
-                x += method.toString().length();
+                bh.consume(method);
             }
         }
-        return x;
     }
 
     @Benchmark
-    public int newMapUnknownMethods() throws Exception {
-        int x = 0;
+    public void newMapUnknownMethods(Blackhole bh) throws Exception {
         for (int i = 0; i < UNKNOWN_METHODS.length; ++i) {
             HttpMethod method = NEW_MAP.get(UNKNOWN_METHODS[i]);
             if (method != null) {
-                x += method.toString().length();
+                bh.consume(method);
             }
         }
-        return x;
     }
 }
diff --git a/microbench/src/main/java/io/netty/handler/codec/http/QueryStringDecoderBenchmark.java b/microbench/src/main/java/io/netty/handler/codec/http/QueryStringDecoderBenchmark.java
new file mode 100644
index 0000000..efd1dae
--- /dev/null
+++ b/microbench/src/main/java/io/netty/handler/codec/http/QueryStringDecoderBenchmark.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2019 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.http;
+
+import io.netty.microbench.util.AbstractMicrobenchmark;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.annotations.Warmup;
+
+import java.nio.charset.Charset;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+@Threads(1)
+@Warmup(iterations = 3)
+@Measurement(iterations = 3)
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+public class QueryStringDecoderBenchmark extends AbstractMicrobenchmark {
+
+    private static final Charset SHIFT_JIS = Charset.forName("Shift-JIS");
+
+    @Benchmark
+    public Map<String, List<String>> noDecoding() {
+        return new QueryStringDecoder("foo=bar&cat=dog", false).parameters();
+    }
+
+    @Benchmark
+    public Map<String, List<String>> onlyDecoding() {
+        // ほげ=ぼけ&ねこ=いぬ
+        return new QueryStringDecoder("%E3%81%BB%E3%81%92=%E3%81%BC%E3%81%91&%E3%81%AD%E3%81%93=%E3%81%84%E3%81%AC",
+                                      false)
+                .parameters();
+    }
+
+    @Benchmark
+    public Map<String, List<String>> mixedDecoding() {
+        // foo=bar&ほげ=ぼけ&cat=dog&ねこ=いぬ
+        return new QueryStringDecoder("foo=bar%E3%81%BB%E3%81%92=%E3%81%BC%E3%81%91&cat=dog&" +
+                                      "&%E3%81%AD%E3%81%93=%E3%81%84%E3%81%AC", false)
+                .parameters();
+    }
+
+    @Benchmark
+    public Map<String, List<String>> nonStandardDecoding() {
+        // ほげ=ぼけ&ねこ=いぬ in Shift-JIS
+        return new QueryStringDecoder("%82%D9%82%B0=%82%DA%82%AF&%82%CB%82%B1=%82%A2%82%CA",
+                                      SHIFT_JIS, false)
+                .parameters();
+    }
+}
diff --git a/microbench/src/main/java/io/netty/handler/codec/http/QueryStringEncoderBenchmark.java b/microbench/src/main/java/io/netty/handler/codec/http/QueryStringEncoderBenchmark.java
new file mode 100644
index 0000000..88013eb
--- /dev/null
+++ b/microbench/src/main/java/io/netty/handler/codec/http/QueryStringEncoderBenchmark.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2019 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.http;
+
+import io.netty.microbench.util.AbstractMicrobenchmark;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.annotations.Warmup;
+
+import java.util.concurrent.TimeUnit;
+
+@Threads(1)
+@Warmup(iterations = 3)
+@Measurement(iterations = 3)
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+public class QueryStringEncoderBenchmark extends AbstractMicrobenchmark {
+    private String shortAscii;
+    private String shortUtf8;
+    private String shortAsciiFirst;
+
+    private String longAscii;
+    private String longUtf8;
+    private String longAsciiFirst;
+
+    @Setup
+    public void setUp() {
+        // Avoid constant pool for strings since it's common for at least values to not be constant.
+        shortAscii = new String("foo".toCharArray());
+        shortUtf8 = new String("ほげほげ".toCharArray());
+        shortAsciiFirst = shortAscii + shortUtf8;
+        longAscii = repeat(shortAscii, 100);
+        longUtf8 = repeat(shortUtf8, 100);
+        longAsciiFirst = longAscii + longUtf8;
+    }
+
+    @Benchmark
+    public String shortAscii() {
+        return encode(shortAscii);
+    }
+
+    @Benchmark
+    public String shortUtf8() {
+        return encode(shortUtf8);
+    }
+
+    @Benchmark
+    public String shortAsciiFirst() {
+        return encode(shortAsciiFirst);
+    }
+
+    @Benchmark
+    public String longAscii() {
+        return encode(longAscii);
+    }
+
+    @Benchmark
+    public String longUtf8() {
+        return encode(longUtf8);
+    }
+
+    @Benchmark
+    public String longAsciiFirst() {
+        return encode(longAsciiFirst);
+    }
+
+    private static String encode(String s) {
+        QueryStringEncoder encoder = new QueryStringEncoder("");
+        encoder.addParam(s, s);
+        return encoder.toString();
+    }
+
+    private static String repeat(String s, int num) {
+        StringBuilder sb = new StringBuilder(num * s.length());
+        for (int i = 0; i < num; i++) {
+            sb.append(s);
+        }
+        return sb.toString();
+    }
+}
diff --git a/microbench/src/main/java/io/netty/handler/codec/http2/HpackBenchmarkUtil.java b/microbench/src/main/java/io/netty/handler/codec/http2/HpackBenchmarkUtil.java
index c94bc59..e8a3a56 100644
--- a/microbench/src/main/java/io/netty/handler/codec/http2/HpackBenchmarkUtil.java
+++ b/microbench/src/main/java/io/netty/handler/codec/http2/HpackBenchmarkUtil.java
@@ -49,7 +49,7 @@ public final class HpackBenchmarkUtil {
         final HpackHeadersSize size;
         final boolean limitToAscii;
 
-        public HeadersKey(HpackHeadersSize size, boolean limitToAscii) {
+        HeadersKey(HpackHeadersSize size, boolean limitToAscii) {
             this.size = size;
             this.limitToAscii = limitToAscii;
         }
diff --git a/microbench/src/main/java/io/netty/handler/codec/http2/HpackDecoderBenchmark.java b/microbench/src/main/java/io/netty/handler/codec/http2/HpackDecoderBenchmark.java
index 3ecf1bc..8a82414 100644
--- a/microbench/src/main/java/io/netty/handler/codec/http2/HpackDecoderBenchmark.java
+++ b/microbench/src/main/java/io/netty/handler/codec/http2/HpackDecoderBenchmark.java
@@ -72,7 +72,7 @@ public class HpackDecoderBenchmark extends AbstractMicrobenchmark {
     @Benchmark
     @BenchmarkMode(Mode.Throughput)
     public void decode(final Blackhole bh) throws Http2Exception {
-        HpackDecoder hpackDecoder = new HpackDecoder(DEFAULT_HEADER_LIST_SIZE, 32);
+        HpackDecoder hpackDecoder = new HpackDecoder(Integer.MAX_VALUE);
         @SuppressWarnings("unchecked")
         Http2Headers headers =
                 new DefaultHttp2Headers() {
diff --git a/microbench/src/main/java/io/netty/handler/codec/http2/HpackHeader.java b/microbench/src/main/java/io/netty/handler/codec/http2/HpackHeader.java
index c0b6359..482db41 100644
--- a/microbench/src/main/java/io/netty/handler/codec/http2/HpackHeader.java
+++ b/microbench/src/main/java/io/netty/handler/codec/http2/HpackHeader.java
@@ -40,14 +40,14 @@ import java.util.Random;
 /**
  * Helper class representing a single header entry. Used by the benchmarks.
  */
-class HpackHeader {
+final class HpackHeader {
     private static final String ALPHABET =
             "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
 
     final CharSequence name;
     final CharSequence value;
 
-    HpackHeader(byte[] name, byte[] value) {
+    private HpackHeader(byte[] name, byte[] value) {
         this.name = new AsciiString(name, false);
         this.value = new AsciiString(value, false);
     }
@@ -59,7 +59,8 @@ class HpackHeader {
                                            boolean limitToAscii) {
         List<HpackHeader> hpackHeaders = new ArrayList<HpackHeader>(numHeaders);
         for (int i = 0; i < numHeaders; ++i) {
-            byte[] name = randomBytes(new byte[nameLength], limitToAscii);
+            // Force always ascii for header names
+            byte[] name = randomBytes(new byte[nameLength], true);
             byte[] value = randomBytes(new byte[valueLength], limitToAscii);
             hpackHeaders.add(new HpackHeader(name, value));
         }
diff --git a/microbench/src/main/java/io/netty/microbench/channel/DefaultChannelPipelineBenchmark.java b/microbench/src/main/java/io/netty/microbench/channel/DefaultChannelPipelineBenchmark.java
new file mode 100644
index 0000000..c8a2409
--- /dev/null
+++ b/microbench/src/main/java/io/netty/microbench/channel/DefaultChannelPipelineBenchmark.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2019 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.microbench.channel;
+
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.channel.ChannelPipeline;
+import io.netty.channel.embedded.EmbeddedChannel;
+import io.netty.microbench.util.AbstractMicrobenchmark;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.infra.Blackhole;
+
+@Warmup(iterations = 5)
+@Measurement(iterations = 5)
+@State(Scope.Benchmark)
+public class DefaultChannelPipelineBenchmark extends AbstractMicrobenchmark {
+
+    private static final ChannelHandler NOOP_HANDLER = new ChannelInboundHandlerAdapter() {
+        @Override
+        public boolean isSharable() {
+            return true;
+        }
+    };
+
+    private static final ChannelHandler CONSUMING_HANDLER = new ChannelInboundHandlerAdapter() {
+        @Override
+        public void channelReadComplete(ChannelHandlerContext ctx) {
+            // NOOP
+        }
+
+        @Override
+        public boolean isSharable() {
+            return true;
+        }
+    };
+
+    @Param({ "4" })
+    public int extraHandlers;
+
+    private ChannelPipeline pipeline;
+
+    @Setup(Level.Iteration)
+    public void setup() {
+        pipeline = new EmbeddedChannel().pipeline();
+        for (int i = 0; i < extraHandlers; i++) {
+            pipeline.addLast(NOOP_HANDLER);
+        }
+        pipeline.addLast(CONSUMING_HANDLER);
+    }
+
+    @TearDown
+    public void tearDown() {
+        pipeline.channel().close();
+    }
+
+    @Benchmark
+    public void propagateEvent(Blackhole hole) {
+        for (int i = 0; i < 100; i++) {
+            hole.consume(pipeline.fireChannelReadComplete());
+        }
+    }
+}
diff --git a/microbench/src/main/java/io/netty/microbench/channel/EmbeddedChannelHandlerContext.java b/microbench/src/main/java/io/netty/microbench/channel/EmbeddedChannelHandlerContext.java
index 059458a..c724ca3 100644
--- a/microbench/src/main/java/io/netty/microbench/channel/EmbeddedChannelHandlerContext.java
+++ b/microbench/src/main/java/io/netty/microbench/channel/EmbeddedChannelHandlerContext.java
@@ -45,7 +45,7 @@ public abstract class EmbeddedChannelHandlerContext implements ChannelHandlerCon
         this.alloc = checkNotNull(alloc, "alloc");
         this.channel = checkNotNull(channel, "channel");
         this.handler = checkNotNull(handler, "handler");
-        eventLoop = checkNotNull(channel.eventLoop(), "eventLoop");
+        this.eventLoop = checkNotNull(channel.eventLoop(), "eventLoop");
     }
 
     protected abstract void handleException(Throwable t);
diff --git a/microbench/src/main/java/io/netty/microbench/channel/EmbeddedChannelWriteReleaseHandlerContext.java b/microbench/src/main/java/io/netty/microbench/channel/EmbeddedChannelWriteReleaseHandlerContext.java
index b50c53c..6884bc9 100644
--- a/microbench/src/main/java/io/netty/microbench/channel/EmbeddedChannelWriteReleaseHandlerContext.java
+++ b/microbench/src/main/java/io/netty/microbench/channel/EmbeddedChannelWriteReleaseHandlerContext.java
@@ -31,6 +31,7 @@ public abstract class EmbeddedChannelWriteReleaseHandlerContext extends Embedded
         super(alloc, handler, channel);
     }
 
+    @Override
     protected abstract void handleException(Throwable t);
 
     @Override
diff --git a/microbench/src/main/java/io/netty/microbench/channel/epoll/EpollSocketChannelBenchmark.java b/microbench/src/main/java/io/netty/microbench/channel/epoll/EpollSocketChannelBenchmark.java
index 5ecd186..f4708bd 100644
--- a/microbench/src/main/java/io/netty/microbench/channel/epoll/EpollSocketChannelBenchmark.java
+++ b/microbench/src/main/java/io/netty/microbench/channel/epoll/EpollSocketChannelBenchmark.java
@@ -30,10 +30,15 @@ import io.netty.microbench.util.AbstractMicrobenchmark;
 import io.netty.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
 import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.GroupThreads;
 import org.openjdk.jmh.annotations.Setup;
 import org.openjdk.jmh.annotations.TearDown;
 
 public class EpollSocketChannelBenchmark extends AbstractMicrobenchmark {
+    private static final Runnable runnable = new Runnable() {
+        @Override
+        public void run() { }
+    };
 
     private EpollEventLoopGroup group;
     private Channel serverChan;
@@ -136,4 +141,15 @@ public class EpollSocketChannelBenchmark extends AbstractMicrobenchmark {
     public Object pingPong() throws Exception {
         return chan.pipeline().writeAndFlush(abyte.retainedSlice()).sync();
     }
+
+    @Benchmark
+    public Object executeSingle() throws Exception {
+        return chan.eventLoop().submit(runnable).get();
+    }
+
+    @Benchmark
+    @GroupThreads(3)
+    public Object executeMulti() throws Exception {
+        return chan.eventLoop().submit(runnable).get();
+    }
 }
diff --git a/microbench/src/main/java/io/netty/microbench/concurrent/BurstCostExecutorsBenchmark.java b/microbench/src/main/java/io/netty/microbench/concurrent/BurstCostExecutorsBenchmark.java
index 11164d2..acab0f0 100644
--- a/microbench/src/main/java/io/netty/microbench/concurrent/BurstCostExecutorsBenchmark.java
+++ b/microbench/src/main/java/io/netty/microbench/concurrent/BurstCostExecutorsBenchmark.java
@@ -67,7 +67,7 @@ public class BurstCostExecutorsBenchmark extends AbstractMicrobenchmark {
         private final AtomicBoolean poisoned = new AtomicBoolean();
         private final Thread executorThread;
 
-        public SpinExecutorService(int maxTasks) {
+        SpinExecutorService(int maxTasks) {
             tasks = PlatformDependent.newFixedMpscQueue(maxTasks);
             executorThread = new Thread(new Runnable() {
                 @Override
diff --git a/microbench/src/main/java/io/netty/microbench/concurrent/FastThreadLocalFastPathBenchmark.java b/microbench/src/main/java/io/netty/microbench/concurrent/FastThreadLocalFastPathBenchmark.java
index c0073fb..c371ad1 100644
--- a/microbench/src/main/java/io/netty/microbench/concurrent/FastThreadLocalFastPathBenchmark.java
+++ b/microbench/src/main/java/io/netty/microbench/concurrent/FastThreadLocalFastPathBenchmark.java
@@ -20,6 +20,7 @@ import io.netty.util.concurrent.FastThreadLocal;
 import org.openjdk.jmh.annotations.Benchmark;
 import org.openjdk.jmh.annotations.Measurement;
 import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.infra.Blackhole;
 
 import java.util.Random;
 
@@ -56,20 +57,16 @@ public class FastThreadLocalFastPathBenchmark extends AbstractMicrobenchmark {
     }
 
     @Benchmark
-    public int jdkThreadLocalGet() {
-        int result = 0;
+    public void jdkThreadLocalGet(Blackhole bh) {
         for (ThreadLocal<Integer> i: jdkThreadLocals) {
-            result += i.get();
+            bh.consume(i.get());
         }
-        return result;
     }
 
     @Benchmark
-    public int fastThreadLocal() {
-        int result = 0;
+    public void fastThreadLocal(Blackhole bh) {
         for (FastThreadLocal<Integer> i: fastThreadLocals) {
-            result += i.get();
+            bh.consume(i.get());
         }
-        return result;
     }
 }
diff --git a/microbench/src/main/java/io/netty/microbench/concurrent/FastThreadLocalSlowPathBenchmark.java b/microbench/src/main/java/io/netty/microbench/concurrent/FastThreadLocalSlowPathBenchmark.java
index 20d414a..70a19a0 100644
--- a/microbench/src/main/java/io/netty/microbench/concurrent/FastThreadLocalSlowPathBenchmark.java
+++ b/microbench/src/main/java/io/netty/microbench/concurrent/FastThreadLocalSlowPathBenchmark.java
@@ -20,6 +20,7 @@ import io.netty.util.concurrent.FastThreadLocal;
 import org.openjdk.jmh.annotations.Benchmark;
 import org.openjdk.jmh.annotations.Measurement;
 import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.infra.Blackhole;
 
 import java.util.Random;
 
@@ -60,20 +61,16 @@ public class FastThreadLocalSlowPathBenchmark extends AbstractMicrobenchmark {
     }
 
     @Benchmark
-    public int jdkThreadLocalGet() {
-        int result = 0;
+    public void jdkThreadLocalGet(Blackhole bh) {
         for (ThreadLocal<Integer> i: jdkThreadLocals) {
-            result += i.get();
+            bh.consume(i.get());
         }
-        return result;
     }
 
     @Benchmark
-    public int fastThreadLocal() {
-        int result = 0;
+    public void fastThreadLocal(Blackhole bh) {
         for (FastThreadLocal<Integer> i: fastThreadLocals) {
-            result += i.get();
+            bh.consume(i.get());
         }
-        return result;
     }
 }
diff --git a/microbench/src/main/java/io/netty/microbench/headers/ReadOnlyHttp2HeadersBenchmark.java b/microbench/src/main/java/io/netty/microbench/headers/ReadOnlyHttp2HeadersBenchmark.java
index 9efc2f3..920da6f 100644
--- a/microbench/src/main/java/io/netty/microbench/headers/ReadOnlyHttp2HeadersBenchmark.java
+++ b/microbench/src/main/java/io/netty/microbench/headers/ReadOnlyHttp2HeadersBenchmark.java
@@ -35,6 +35,7 @@ import org.openjdk.jmh.annotations.Setup;
 import org.openjdk.jmh.annotations.State;
 import org.openjdk.jmh.annotations.Threads;
 import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.infra.Blackhole;
 
 import java.util.Map;
 import java.util.UUID;
@@ -68,23 +69,23 @@ public class ReadOnlyHttp2HeadersBenchmark extends AbstractMicrobenchmark {
 
     @Benchmark
     @BenchmarkMode(Mode.AverageTime)
-    public int defaultTrailers() {
+    public void defaultTrailers(Blackhole bh) {
         Http2Headers headers = new DefaultHttp2Headers(false);
         for (int i = 0; i < headerCount; ++i) {
             headers.add(headerNames[i], headerValues[i]);
         }
-        return iterate(headers);
+        iterate(headers, bh);
     }
 
     @Benchmark
     @BenchmarkMode(Mode.AverageTime)
-    public int readOnlyTrailers() {
-        return iterate(ReadOnlyHttp2Headers.trailers(false, buildPairs()));
+    public void readOnlyTrailers(Blackhole bh) {
+        iterate(ReadOnlyHttp2Headers.trailers(false, buildPairs()), bh);
     }
 
     @Benchmark
     @BenchmarkMode(Mode.AverageTime)
-    public int defaultClientHeaders() {
+    public void defaultClientHeaders(Blackhole bh) {
         Http2Headers headers = new DefaultHttp2Headers(false);
         for (int i = 0; i < headerCount; ++i) {
             headers.add(headerNames[i], headerValues[i]);
@@ -93,39 +94,37 @@ public class ReadOnlyHttp2HeadersBenchmark extends AbstractMicrobenchmark {
         headers.scheme(HttpScheme.HTTPS.name());
         headers.path(path);
         headers.authority(authority);
-        return iterate(headers);
+        iterate(headers, bh);
     }
 
     @Benchmark
     @BenchmarkMode(Mode.AverageTime)
-    public int readOnlyClientHeaders() {
-        return iterate(ReadOnlyHttp2Headers.clientHeaders(false, HttpMethod.POST.asciiName(), path,
-                                                          HttpScheme.HTTPS.name(), authority, buildPairs()));
+    public void readOnlyClientHeaders(Blackhole bh) {
+        iterate(ReadOnlyHttp2Headers.clientHeaders(false, HttpMethod.POST.asciiName(), path,
+                                                          HttpScheme.HTTPS.name(), authority, buildPairs()), bh);
     }
 
     @Benchmark
     @BenchmarkMode(Mode.AverageTime)
-    public int defaultServerHeaders() {
+    public void defaultServerHeaders(Blackhole bh) {
         Http2Headers headers = new DefaultHttp2Headers(false);
         for (int i = 0; i < headerCount; ++i) {
             headers.add(headerNames[i], headerValues[i]);
         }
         headers.status(HttpResponseStatus.OK.codeAsText());
-        return iterate(headers);
+        iterate(headers, bh);
     }
 
     @Benchmark
     @BenchmarkMode(Mode.AverageTime)
-    public int readOnlyServerHeaders() {
-        return iterate(ReadOnlyHttp2Headers.serverHeaders(false, HttpResponseStatus.OK.codeAsText(), buildPairs()));
+    public void readOnlyServerHeaders(Blackhole bh) {
+        iterate(ReadOnlyHttp2Headers.serverHeaders(false, HttpResponseStatus.OK.codeAsText(), buildPairs()), bh);
     }
 
-    private static int iterate(Http2Headers headers) {
-        int length = 0;
+    private static void iterate(Http2Headers headers, Blackhole bh) {
         for (Map.Entry<CharSequence, CharSequence> entry : headers) {
-            length += entry.getKey().length() + entry.getValue().length();
+            bh.consume(entry);
         }
-        return length;
     }
 
     private AsciiString[] buildPairs() {
diff --git a/microbench/src/main/java/io/netty/microbench/http/HttpRequestDecoderBenchmark.java b/microbench/src/main/java/io/netty/microbench/http/HttpRequestDecoderBenchmark.java
index 191b992..fa0ba5c 100644
--- a/microbench/src/main/java/io/netty/microbench/http/HttpRequestDecoderBenchmark.java
+++ b/microbench/src/main/java/io/netty/microbench/http/HttpRequestDecoderBenchmark.java
@@ -98,14 +98,14 @@ public class HttpRequestDecoderBenchmark extends AbstractMicrobenchmark {
                 amount = headerLength -  a;
             }
 
-            // if header is done it should produce a HttpRequest
-            channel.writeInbound(Unpooled.wrappedBuffer(content, a, amount));
+            // if header is done it should produce an HttpRequest
+            channel.writeInbound(Unpooled.wrappedBuffer(content, a, amount).asReadOnly());
             a += amount;
         }
 
         for (int i = CONTENT_LENGTH; i > 0; i --) {
             // Should produce HttpContent
-            channel.writeInbound(Unpooled.wrappedBuffer(content, content.length - i, 1));
+            channel.writeInbound(Unpooled.wrappedBuffer(content, content.length - i, 1).asReadOnly());
         }
     }
 }
diff --git a/microbench/src/main/java/io/netty/microbench/internal/RecyclableArrayListBenchmark.java b/microbench/src/main/java/io/netty/microbench/internal/RecyclableArrayListBenchmark.java
index dfb95a6..32fe2bb 100644
--- a/microbench/src/main/java/io/netty/microbench/internal/RecyclableArrayListBenchmark.java
+++ b/microbench/src/main/java/io/netty/microbench/internal/RecyclableArrayListBenchmark.java
@@ -36,8 +36,8 @@ public class RecyclableArrayListBenchmark extends AbstractMicrobenchmark {
     public int size;
 
     @Benchmark
-    public void recycleSameThread() {
+    public boolean recycleSameThread() {
         RecyclableArrayList list = RecyclableArrayList.newInstance(size);
-        list.recycle();
+        return list.recycle();
     }
 }
diff --git a/microbench/src/main/java/io/netty/util/concurrent/ScheduleFutureTaskBenchmark.java b/microbench/src/main/java/io/netty/util/concurrent/ScheduleFutureTaskBenchmark.java
new file mode 100644
index 0000000..d5fdd5b
--- /dev/null
+++ b/microbench/src/main/java/io/netty/util/concurrent/ScheduleFutureTaskBenchmark.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2019 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.util.concurrent;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.annotations.Warmup;
+
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.microbench.util.AbstractMicrobenchmark;
+
+@Warmup(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 10, time = 3, timeUnit = TimeUnit.SECONDS)
+@State(Scope.Benchmark)
+public class ScheduleFutureTaskBenchmark extends AbstractMicrobenchmark {
+
+    static final Callable<Void> NO_OP = new Callable<Void>() {
+        @Override
+        public Void call() throws Exception {
+            return null;
+        }
+    };
+
+    @State(Scope.Thread)
+    public static class ThreadState {
+
+        @Param({ "100000" })
+        int num;
+
+        AbstractScheduledEventExecutor eventLoop;
+
+        @Setup(Level.Trial)
+        public void reset() {
+            eventLoop = (AbstractScheduledEventExecutor) new NioEventLoopGroup(1).next();
+        }
+
+        @Setup(Level.Invocation)
+        public void clear() {
+            eventLoop.submit(new Runnable() {
+                @Override
+                public void run() {
+                    eventLoop.cancelScheduledTasks();
+                }
+            }).awaitUninterruptibly();
+        }
+
+        @TearDown(Level.Trial)
+        public void shutdown() {
+            clear();
+            eventLoop.parent().shutdownGracefully().awaitUninterruptibly();
+        }
+    }
+
+    @Benchmark
+    @Threads(3)
+    public Future<?> scheduleLots(final ThreadState threadState) {
+        return threadState.eventLoop.submit(new Runnable() {
+            @Override
+            public void run() {
+                for (int i = 1; i <= threadState.num; i++) {
+                    threadState.eventLoop.schedule(NO_OP, i, TimeUnit.HOURS);
+                }
+            }
+        }).syncUninterruptibly();
+    }
+
+    @Benchmark
+    @Threads(1)
+    public Future<?> scheduleLotsOutsideLoop(final ThreadState threadState) {
+        final AbstractScheduledEventExecutor eventLoop = threadState.eventLoop;
+        for (int i = 1; i <= threadState.num; i++) {
+            eventLoop.schedule(NO_OP, i, TimeUnit.HOURS);
+        }
+        return null;
+    }
+
+    @Benchmark
+    @Threads(1)
+    public Future<?> scheduleCancelLotsOutsideLoop(final ThreadState threadState) {
+        final AbstractScheduledEventExecutor eventLoop = threadState.eventLoop;
+        for (int i = 1; i <= threadState.num; i++) {
+            eventLoop.schedule(NO_OP, i, TimeUnit.HOURS).cancel(false);
+        }
+        return null;
+    }
+}
diff --git a/microbench/src/main/java/io/netty/util/concurrent/package-info.java b/microbench/src/main/java/io/netty/util/concurrent/package-info.java
new file mode 100644
index 0000000..5059d12
--- /dev/null
+++ b/microbench/src/main/java/io/netty/util/concurrent/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2019 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.
+ */
+/**
+ * Benchmarks for {@link io.netty.util.concurrent}.
+ */
+package io.netty.util.concurrent;
diff --git a/pom.xml b/pom.xml
index 60aa19d..96016e1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -26,10 +26,10 @@
   <groupId>io.netty</groupId>
   <artifactId>netty-parent</artifactId>
   <packaging>pom</packaging>
-  <version>4.1.33.Final</version>
+  <version>4.1.48.Final</version>
 
   <name>Netty</name>
-  <url>http://netty.io/</url>
+  <url>https://netty.io/</url>
   <description>
     Netty is an asynchronous event-driven network application framework for
     rapid development of maintainable high performance protocol servers and
@@ -38,7 +38,7 @@
 
   <organization>
     <name>The Netty Project</name>
-    <url>http://netty.io/</url>
+    <url>https://netty.io/</url>
   </organization>
 
   <licenses>
@@ -53,7 +53,7 @@
     <url>https://github.com/netty/netty</url>
     <connection>scm:git:git://github.com/netty/netty.git</connection>
     <developerConnection>scm:git:ssh://git@github.com/netty/netty.git</developerConnection>
-    <tag>netty-4.1.33.Final</tag>
+    <tag>netty-4.1.48.Final</tag>
   </scm>
 
   <developers>
@@ -61,13 +61,42 @@
       <id>netty.io</id>
       <name>The Netty Project Contributors</name>
       <email>netty@googlegroups.com</email>
-      <url>http://netty.io/</url>
+      <url>https://netty.io/</url>
       <organization>The Netty Project</organization>
-      <organizationUrl>http://netty.io/</organizationUrl>
+      <organizationUrl>https://netty.io/</organizationUrl>
     </developer>
   </developers>
 
   <profiles>
+    <profile>
+      <id>not_x86_64</id>
+      <activation>
+        <os>
+          <arch>!x86_64</arch>
+        </os>
+      </activation>
+      <properties>
+        <!-- Use no classifier as we only support x86_64 atm-->
+        <tcnative.classifier />
+        <skipShadingTestsuite>true</skipShadingTestsuite>
+      </properties>
+    </profile>
+
+    <!-- Detect if we use GraalVM and if so enable the native image testsuite -->
+    <profile>
+      <id>graal</id>
+      <activation>
+        <file>
+          <!-- GraalVM Component Updater should exists when using GraalVM-->
+          <exists>${java.home}/bin/gu</exists>
+        </file>
+      </activation>
+      <properties>
+        <skipNativeImageTestsuite>false</skipNativeImageTestsuite>
+        <forbiddenapis.skip>true</forbiddenapis.skip>
+        <testJvm />
+      </properties>
+    </profile>
     <!-- JDK13 -->
     <profile>
       <id>java13</id>
@@ -97,6 +126,9 @@
         <jdk>12</jdk>
       </activation>
       <properties>
+        <argLine.java9.extras />
+        <!-- Export some stuff which is used during our tests -->
+        <argLine.java9>--illegal-access=deny ${argLine.java9.extras}</argLine.java9>
         <!-- Not use alpn agent as Java11 supports alpn out of the box -->
         <argLine.alpnAgent />
         <forbiddenapis.skip>true</forbiddenapis.skip>
@@ -119,6 +151,9 @@
         <jdk>11</jdk>
       </activation>
       <properties>
+        <argLine.java9.extras />
+        <!-- Export some stuff which is used during our tests -->
+        <argLine.java9>--illegal-access=deny ${argLine.java9.extras}</argLine.java9>
         <!-- Not use alpn agent as Java11 supports alpn out of the box -->
         <argLine.alpnAgent />
         <forbiddenapis.skip>true</forbiddenapis.skip>
@@ -138,6 +173,9 @@
         <jdk>10</jdk>
       </activation>
       <properties>
+        <argLine.java9.extras />
+        <!-- Export some stuff which is used during our tests -->
+        <argLine.java9>--illegal-access=deny --add-modules java.xml.bind ${argLine.java9.extras}</argLine.java9>
         <!-- Not use alpn agent as Java10 supports alpn out of the box -->
         <argLine.alpnAgent />
         <forbiddenapis.skip>true</forbiddenapis.skip>
@@ -154,10 +192,10 @@
       <properties>
         <argLine.java9.extras />
         <!-- Export some stuff which is used during our tests -->
-        <argLine.java9>--add-modules java.xml.bind ${argLine.java9.extras}</argLine.java9>
+        <argLine.java9>--illegal-access=deny --add-modules java.xml.bind ${argLine.java9.extras}</argLine.java9>
         <!-- Not use alpn agent as Java9 supports alpn out of the box -->
         <argLine.alpnAgent />
-        <!-- Skip as maven plugin not works with Java9 yet --> 
+        <!-- Skip as maven plugin not works with Java9 yet -->
         <forbiddenapis.skip>true</forbiddenapis.skip>
         <!-- Needed because of https://issues.apache.org/jira/browse/MENFORCER-275 -->
         <enforcer.plugin.version>3.0.0-M1</enforcer.plugin.version>
@@ -177,7 +215,7 @@
     <profile>
       <id>leak</id>
       <properties>
-        <argLine.leak>-Dio.netty.leakDetectionLevel=paranoid -Dio.netty.leakDetection.maxRecords=32</argLine.leak>
+        <argLine.leak>-Dio.netty.leakDetectionLevel=paranoid -Dio.netty.leakDetection.targetRecords=32</argLine.leak>
       </properties>
     </profile>
     <profile>
@@ -253,7 +291,7 @@
     <netty.dev.tools.directory>${project.build.directory}/dev-tools</netty.dev.tools.directory>
     <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
-    <netty.build.version>22</netty.build.version>
+    <netty.build.version>26</netty.build.version>
     <jboss.marshalling.version>1.4.11.Final</jboss.marshalling.version>
     <jetty.alpnAgent.version>2.0.8</jetty.alpnAgent.version>
     <jetty.alpnAgent.path>"${settings.localRepository}"/org/mortbay/jetty/alpn/jetty-alpn-agent/${jetty.alpnAgent.version}/jetty-alpn-agent-${jetty.alpnAgent.version}.jar</jetty.alpnAgent.path>
@@ -272,9 +310,11 @@
     <argLine.javaProperties>-D_</argLine.javaProperties>
     <!-- Configure the os-maven-plugin extension to expand the classifier on                  -->
     <!-- Fedora-"like" systems. This is currently only used for the netty-tcnative dependency -->
-    <os.detection.classifierWithLikes>fedora</os.detection.classifierWithLikes>
+    <osmaven.version>1.6.2</osmaven.version>
+    <!-- keep in sync with PlatformDependent#ALLOWED_LINUX_OS_CLASSIFIERS -->
+    <os.detection.classifierWithLikes>fedora,suse,arch</os.detection.classifierWithLikes>
     <tcnative.artifactId>netty-tcnative</tcnative.artifactId>
-    <tcnative.version>2.0.20.Final</tcnative.version>
+    <tcnative.version>2.0.29.Final</tcnative.version>
     <tcnative.classifier>${os.detected.classifier}</tcnative.classifier>
     <conscrypt.groupId>org.conscrypt</conscrypt.groupId>
     <conscrypt.artifactId>conscrypt-openjdk-uber</conscrypt.artifactId>
@@ -285,10 +325,16 @@
     <logging.logLevel>debug</logging.logLevel>
     <log4j2.version>2.6.2</log4j2.version>
     <enforcer.plugin.version>1.4.1</enforcer.plugin.version>
-    <testJavaHome>${env.JAVA_HOME}</testJavaHome>
+    <testJavaHome>${java.home}</testJavaHome>
+    <testJvm>${testJavaHome}/bin/java</testJvm>
     <skipOsgiTestsuite>false</skipOsgiTestsuite>
     <skipAutobahnTestsuite>false</skipAutobahnTestsuite>
     <skipHttp2Testsuite>false</skipHttp2Testsuite>
+    <skipJapicmp>false</skipJapicmp>
+    <graalvm.version>19.0.0</graalvm.version>
+    <!-- By default skip native testsuite as it requires a custom environment with graalvm installed -->
+    <skipNativeImageTestsuite>true</skipNativeImageTestsuite>
+    <skipShadingTestsuite>false</skipShadingTestsuite>
   </properties>
 
   <modules>
@@ -310,6 +356,7 @@
     <module>codec-xml</module>
     <module>resolver</module>
     <module>resolver-dns</module>
+    <module>resolver-dns-native-macos</module>
     <module>tarball</module>
     <module>transport</module>
     <module>transport-native-unix-common-tests</module>
@@ -327,6 +374,8 @@
     <module>testsuite-http2</module>
     <module>testsuite-osgi</module>
     <module>testsuite-shading</module>
+    <module>testsuite-native-image</module>
+    <module>transport-blockhound-tests</module>
     <module>microbench</module>
     <module>bom</module>
   </modules>
@@ -423,7 +472,7 @@
         <optional>true</optional>
       </dependency>
 
-      <!-- 
+      <!--
         Completely optional and only needed for OCSP stapling to construct and
         parse OCSP requests and responses.
       -->
@@ -466,7 +515,7 @@
       <dependency>
         <groupId>org.jctools</groupId>
         <artifactId>jctools-core</artifactId>
-        <version>2.1.1</version>
+        <version>3.0.0</version>
       </dependency>
 
       <dependency>
@@ -592,7 +641,7 @@
       <dependency>
         <groupId>org.apache.commons</groupId>
         <artifactId>commons-compress</artifactId>
-        <version>1.18</version>
+        <version>1.19</version>
         <scope>test</scope>
       </dependency>
 
@@ -626,6 +675,13 @@
         <version>${log4j2.version}</version>
         <scope>test</scope>
       </dependency>
+
+      <!-- BlockHound integration -->
+      <dependency>
+        <groupId>io.projectreactor.tools</groupId>
+        <artifactId>blockhound</artifactId>
+        <version>1.0.2.RELEASE</version>
+      </dependency>
     </dependencies>
   </dependencyManagement>
 
@@ -665,11 +721,42 @@
       <extension>
         <groupId>kr.motd.maven</groupId>
         <artifactId>os-maven-plugin</artifactId>
-        <version>1.6.0</version>
+        <version>${osmaven.version}</version>
       </extension>
     </extensions>
 
     <plugins>
+      <plugin>
+        <groupId>com.github.siom79.japicmp</groupId>
+        <artifactId>japicmp-maven-plugin</artifactId>
+        <version>0.14.3</version>
+        <configuration>
+          <parameter>
+            <ignoreMissingOldVersion>true</ignoreMissingOldVersion>
+            <breakBuildOnBinaryIncompatibleModifications>true</breakBuildOnBinaryIncompatibleModifications>
+            <breakBuildOnSourceIncompatibleModifications>true</breakBuildOnSourceIncompatibleModifications>
+            <oldVersionPattern>\d+\.\d+\.\d+\.Final</oldVersionPattern>
+            <ignoreMissingClassesByRegularExpressions>
+              <!-- ignore everything which is not part of netty itself as the plugin can not handle optional dependencies -->
+              <ignoreMissingClassesByRegularExpression>^(?!io\.netty\.).*</ignoreMissingClassesByRegularExpression>
+              <ignoreMissingClassesByRegularExpression>^io\.netty\.internal\.tcnative\..*</ignoreMissingClassesByRegularExpression>
+            </ignoreMissingClassesByRegularExpressions>
+            <excludes>
+              <exclude>@io.netty.util.internal.UnstableApi</exclude>
+              <exclude>io.netty.util.internal.shaded</exclude>
+            </excludes>
+          </parameter>
+          <skip>${skipJapicmp}</skip>
+        </configuration>
+        <executions>
+          <execution>
+            <phase>verify</phase>
+            <goals>
+              <goal>cmp</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
       <plugin>
         <artifactId>maven-enforcer-plugin</artifactId>
         <version>${enforcer.plugin.version}</version>
@@ -691,10 +778,10 @@
                 </requireMavenVersion>
                 <requireProperty>
                   <regexMessage>
-                    x86_64/AARCH64 JDK must be used.
+		     x86_64/AARCH64/PPCLE64/s390x_64 JDK must be used.
                   </regexMessage>
                   <property>os.detected.arch</property>
-                  <regex>^(x86_64|aarch_64)$</regex>
+                  <regex>^(x86_64|aarch_64|ppcle_64|s390_64)$</regex>
                 </requireProperty>
               </rules>
             </configuration>
@@ -742,78 +829,7 @@
             <version>1.1</version>
           </signature>
           <ignores>
-            <ignore>sun.misc.Unsafe</ignore>
-            <ignore>sun.misc.Cleaner</ignore>
-            <ignore>sun.nio.ch.DirectBuffer</ignore>
-
-            <ignore>java.util.zip.Deflater</ignore>
-
-            <!-- Used for NIO -->
-            <ignore>java.nio.channels.DatagramChannel</ignore>
-            <ignore>java.nio.channels.MembershipKey</ignore>
-            <ignore>java.nio.channels.ServerSocketChannel</ignore>
-            <ignore>java.nio.channels.SocketChannel</ignore>
-            <ignore>java.net.StandardProtocolFamily</ignore>
-            <ignore>java.nio.channels.spi.SelectorProvider</ignore>
-            <ignore>java.net.SocketOption</ignore> 
-            <ignore>java.net.StandardSocketOptions</ignore> 
-            <ignore>java.nio.channels.NetworkChannel</ignore>
-
-            <!-- Self-signed certificate generation -->
-            <ignore>sun.security.x509.AlgorithmId</ignore>
-            <ignore>sun.security.x509.CertificateAlgorithmId</ignore>
-            <ignore>sun.security.x509.CertificateIssuerName</ignore>
-            <ignore>sun.security.x509.CertificateSerialNumber</ignore>
-            <ignore>sun.security.x509.CertificateSubjectName</ignore>
-            <ignore>sun.security.x509.CertificateValidity</ignore>
-            <ignore>sun.security.x509.CertificateVersion</ignore>
-            <ignore>sun.security.x509.CertificateX509Key</ignore>
-            <ignore>sun.security.x509.X500Name</ignore>
-            <ignore>sun.security.x509.X509CertInfo</ignore>
-            <ignore>sun.security.x509.X509CertImpl</ignore>
-
-            <!-- SSLSession implementation -->
-            <ignore>javax.net.ssl.SSLEngine</ignore>
-            <ignore>javax.net.ssl.ExtendedSSLSession</ignore>
-            <ignore>javax.net.ssl.X509ExtendedTrustManager</ignore>
-            <ignore>javax.net.ssl.SSLParameters</ignore>
-            <ignore>javax.net.ssl.SNIServerName</ignore>
-            <ignore>javax.net.ssl.SNIHostName</ignore>
-            <ignore>javax.net.ssl.SNIMatcher</ignore>
-            <ignore>java.security.AlgorithmConstraints</ignore>
-            <ignore>java.security.cert.CertificateRevokedException</ignore>
-            <ignore>java.security.cert.CertPathValidatorException</ignore>
-            <ignore>java.security.cert.CertPathValidatorException$Reason</ignore>
-            <ignore>java.security.cert.CertPathValidatorException$BasicReason</ignore>
-
-            <ignore>java.util.concurrent.ConcurrentLinkedDeque</ignore>
-            <ignore>java.util.concurrent.ThreadLocalRandom</ignore>
-
-            <!-- Compression -->
-            <ignore>java.util.zip.CRC32</ignore>
-            <ignore>java.util.zip.Adler32</ignore>
-
-            <!-- NioDatagramChannel implementation -->
-            <ignore>java.net.ProtocolFamily</ignore>
-
-            <!-- JDK 9 -->
             <ignore>java.nio.ByteBuffer</ignore>
-            <ignore>java.nio.CharBuffer</ignore>
-
-            <!-- JDK 8 -->
-            <ignore>java.util.concurrent.atomic.LongAdder</ignore>
-            <ignore>java.util.function.BiFunction</ignore>
-            <ignore>java.security.cert.X509Certificate</ignore>
-
-            <!-- Resolver -->
-            <ignore>java.net.InetAddress</ignore>
-
-            <!-- NoexecVolumeDetector -->
-            <ignore>java.nio.file.attribute.PosixFilePermission</ignore>
-            <ignore>java.nio.file.Files</ignore>
-            <ignore>java.nio.file.LinkOption</ignore>
-            <ignore>java.nio.file.Path</ignore>
-            <ignore>java.io.File</ignore>
           </ignores>
           <annotations>
             <annotation>io.netty.util.internal.SuppressJava6Requirement</annotation>
@@ -830,7 +846,7 @@
       </plugin>
       <plugin>
         <artifactId>maven-checkstyle-plugin</artifactId>
-        <version>2.12.1</version>
+        <version>3.1.0</version>
         <executions>
           <execution>
             <id>check-style</id>
@@ -844,11 +860,19 @@
               <failsOnError>true</failsOnError>
               <failOnViolation>true</failOnViolation>
               <configLocation>io/netty/checkstyle.xml</configLocation>
-              <includeTestSourceDirectory>true</includeTestSourceDirectory>
+              <sourceDirectories>
+                <sourceDirectory>${project.build.sourceDirectory}</sourceDirectory>
+                <sourceDirectory>${project.build.testSourceDirectory}</sourceDirectory>
+              </sourceDirectories>
             </configuration>
           </execution>
         </executions>
         <dependencies>
+          <dependency>
+            <groupId>com.puppycrawl.tools</groupId>
+            <artifactId>checkstyle</artifactId>
+            <version>8.29</version>
+          </dependency>
           <dependency>
             <groupId>${project.groupId}</groupId>
             <artifactId>netty-build</artifactId>
@@ -922,8 +946,8 @@
               <value>io.netty.build.junit.TimedOutTestsListener</value>
             </property>
           </properties>
-          <jvm>${testJavaHome}/bin/java</jvm>
-           <!-- Ensure the whole stacktrace is preserved when an exception is thrown. See https://issues.apache.org/jira/browse/SUREFIRE-1457 --> 
+          <jvm>${testJvm}</jvm>
+           <!-- Ensure the whole stacktrace is preserved when an exception is thrown. See https://issues.apache.org/jira/browse/SUREFIRE-1457 -->
           <trimStackTrace>false</trimStackTrace>
         </configuration>
       </plugin>
@@ -972,7 +996,7 @@
 
       <plugin>
         <artifactId>maven-source-plugin</artifactId>
-        <version>3.0.1</version>
+        <version>3.2.0</version>
         <!-- Eclipse-related OSGi manifests
              See https://github.com/netty/netty/issues/3886
              More information: http://rajakannappan.blogspot.ie/2010/03/automating-eclipse-source-bundle.html -->
@@ -990,23 +1014,18 @@
         </configuration>
 
         <executions>
-          <!--
-            ~ This workaround prevents Maven from executing the 'generate-sources' phase twice.
-            ~ See http://jira.codehaus.org/browse/MSOURCES-13
-            ~ and http://blog.peterlynch.ca/2010/05/maven-how-to-prevent-generate-sources.html
-            -->
           <execution>
             <id>attach-sources</id>
-            <phase>invalid</phase>
+            <phase>prepare-package</phase>
             <goals>
-              <goal>jar</goal>
+              <goal>jar-no-fork</goal>
             </goals>
           </execution>
           <execution>
-            <id>attach-sources-no-fork</id>
-            <phase>package</phase>
+            <id>attach-test-sources</id>
+            <phase>prepare-package</phase>
             <goals>
-              <goal>jar-no-fork</goal>
+              <goal>test-jar-no-fork</goal>
             </goals>
           </execution>
         </executions>
@@ -1211,6 +1230,11 @@
                 </archive>
               </configuration>
             </execution>
+            <execution>
+              <goals>
+                <goal>test-jar</goal>
+              </goals>
+            </execution>
           </executions>
         </plugin>
         <plugin>
@@ -1261,7 +1285,7 @@
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-shade-plugin</artifactId>
-          <version>3.1.0</version>
+          <version>3.2.1</version>
         </plugin>
 
         <!-- Workaround for the 'M2E plugin execution not covered' problem.
diff --git a/resolver-dns-native-macos/pom.xml b/resolver-dns-native-macos/pom.xml
new file mode 100644
index 0000000..639b04a
--- /dev/null
+++ b/resolver-dns-native-macos/pom.xml
@@ -0,0 +1,185 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2019 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.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>io.netty</groupId>
+    <artifactId>netty-parent</artifactId>
+    <version>4.1.48.Final</version>
+  </parent>
+  <artifactId>netty-resolver-dns-native-macos</artifactId>
+
+  <name>Netty/Resolver/DNS/MacOS</name>
+  <packaging>jar</packaging>
+
+  <profiles>
+    <profile>
+      <id>mac</id>
+      <activation>
+        <os>
+          <family>mac</family>
+        </os>
+      </activation>
+      <properties>
+        <jni.compiler.args.ldflags>LDFLAGS=-Wl,-weak_library,${unix.common.lib.unpacked.dir}/lib${unix.common.lib.name}.a</jni.compiler.args.ldflags>
+        <skipTests>false</skipTests>
+      </properties>
+      <build>
+        <plugins>
+          <plugin>
+            <artifactId>maven-dependency-plugin</artifactId>
+            <executions>
+              <!-- unpack the unix-common static library and include files -->
+              <execution>
+                <id>unpack</id>
+                <phase>generate-sources</phase>
+                <goals>
+                  <goal>unpack-dependencies</goal>
+                </goals>
+                <configuration>
+                  <includeGroupIds>${project.groupId}</includeGroupIds>
+                  <includeArtifactIds>netty-transport-native-unix-common</includeArtifactIds>
+                  <classifier>${jni.classifier}</classifier>
+                  <outputDirectory>${unix.common.lib.dir}</outputDirectory>
+                  <includes>META-INF/native/**</includes>
+                  <overWriteReleases>false</overWriteReleases>
+                  <overWriteSnapshots>true</overWriteSnapshots>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+
+          <plugin>
+            <groupId>org.fusesource.hawtjni</groupId>
+            <artifactId>maven-hawtjni-plugin</artifactId>
+            <executions>
+              <execution>
+                <id>build-native-lib</id>
+                <configuration>
+                  <name>netty_resolver_dns_native_macos_${os.detected.arch}</name>
+                  <nativeSourceDirectory>${project.basedir}/src/main/c</nativeSourceDirectory>
+                  <libDirectory>${project.build.outputDirectory}</libDirectory>
+                  <!-- We use Maven's artifact classifier instead.
+                       This hack will make the hawtjni plugin to put the native library
+                       under 'META-INF/native' rather than 'META-INF/native/${platform}'. -->
+                  <platform>.</platform>
+                  <configureArgs>
+                    <arg>${jni.compiler.args.ldflags}</arg>
+                    <arg>${jni.compiler.args.cflags}</arg>
+                  </configureArgs>
+                </configuration>
+                <goals>
+                  <goal>generate</goal>
+                  <goal>build</goal>
+                </goals>
+              </execution>
+            </executions>
+          </plugin>
+
+          <plugin>
+            <artifactId>maven-jar-plugin</artifactId>
+            <executions>
+              <!-- Generate the JAR that contains the native library in it. -->
+              <execution>
+                <id>native-jar</id>
+                <goals>
+                  <goal>jar</goal>
+                </goals>
+                <configuration>
+                  <archive>
+                    <manifest>
+                      <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
+                    </manifest>
+                    <manifestEntries>
+                      <Bundle-NativeCode>META-INF/native/libnetty_resolver_dns_native_macos_${os.detected.arch}.jnilib; osname=MacOSX, processor=${os.detected.arch}"</Bundle-NativeCode>
+                      <Automatic-Module-Name>${javaModuleName}</Automatic-Module-Name>
+                    </manifestEntries>
+                    <index>true</index>
+                    <manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile>
+                  </archive>
+                  <classifier>${jni.classifier}</classifier>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+      <dependencies>
+        <dependency>
+          <groupId>io.netty</groupId>
+          <artifactId>netty-transport-native-unix-common</artifactId>
+          <version>${project.version}</version>
+          <classifier>${jni.classifier}</classifier>
+          <!--
+          The unix-common with classifier dependency is optional because it is not a runtime dependency, but a build time
+          dependency to get the static library which is built directly into the shared library generated by this project.
+          -->
+          <optional>true</optional>
+        </dependency>
+      </dependencies>
+    </profile>
+  </profiles>
+
+  <properties>
+    <javaModuleName>io.netty.resolver.dns.macos</javaModuleName>
+    <!-- Needed as we use SelfSignedCertificate in our tests -->
+    <unix.common.lib.name>netty-unix-common</unix.common.lib.name>
+    <unix.common.lib.dir>${project.build.directory}/unix-common-lib</unix.common.lib.dir>
+    <unix.common.lib.unpacked.dir>${unix.common.lib.dir}/META-INF/native/lib</unix.common.lib.unpacked.dir>
+    <unix.common.include.unpacked.dir>${unix.common.lib.dir}/META-INF/native/include</unix.common.include.unpacked.dir>
+    <jni.compiler.args.cflags>CFLAGS=-O3 -Werror -fno-omit-frame-pointer -Wunused-variable -fvisibility=hidden -I${unix.common.include.unpacked.dir}</jni.compiler.args.cflags>
+    <jni.compiler.args.ldflags>LDFLAGS=-z now -L${unix.common.lib.unpacked.dir} -Wl,--whole-archive -l${unix.common.lib.name} -Wl,--no-whole-archive</jni.compiler.args.ldflags>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>io.netty</groupId>
+      <artifactId>netty-common</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>io.netty</groupId>
+      <artifactId>netty-resolver-dns</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>io.netty</groupId>
+      <artifactId>netty-transport-native-unix-common</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <artifactId>maven-jar-plugin</artifactId>
+        <executions>
+          <!-- Generate the fallback JAR that does not contain the native library. -->
+          <execution>
+            <id>default-jar</id>
+            <configuration>
+              <excludes>
+                <exclude>META-INF/native/**</exclude>
+              </excludes>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>
+
diff --git a/resolver-dns-native-macos/src/main/c/dnsinfo.h b/resolver-dns-native-macos/src/main/c/dnsinfo.h
new file mode 100644
index 0000000..8b10e8d
--- /dev/null
+++ b/resolver-dns-native-macos/src/main/c/dnsinfo.h
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2019 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.
+ */
+/*
+ * Copyright (c) 2004-2006, 2008, 2009, 2011 Apple Inc. All rights reserved.
+ *
+ * @APPLE_LICENSE_HEADER_START@
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apple Public Source License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://www.opensource.apple.com/apsl/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ *
+ * @APPLE_LICENSE_HEADER_END@
+ */
+#ifndef __DNSINFO_H__
+#define __DNSINFO_H__
+/*
+ * These routines provide access to the systems DNS configuration
+ */
+#include <sys/cdefs.h>
+#include <stdint.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#define	DNSINFO_VERSION		20111104
+#define DEFAULT_SEARCH_ORDER    200000   /* search order for the "default" resolver domain name */
+#define	DNS_PTR(type, name)				\
+	union {						\
+		type		name;			\
+		uint64_t	_ ## name ## _p;	\
+	}
+#define	DNS_VAR(type, name)				\
+	type	name
+#pragma pack(4)
+typedef struct {
+	struct in_addr	address;
+	struct in_addr	mask;
+} dns_sortaddr_t;
+#pragma pack()
+#pragma pack(4)
+typedef struct {
+	DNS_PTR(char *,			domain);	/* domain */
+	DNS_VAR(int32_t,		n_nameserver);	/* # nameserver */
+	DNS_PTR(struct sockaddr **,	nameserver);
+	DNS_VAR(uint16_t,		port);		/* port (in host byte order) */
+	DNS_VAR(int32_t,		n_search);	/* # search */
+	DNS_PTR(char **,		search);
+	DNS_VAR(int32_t,		n_sortaddr);	/* # sortaddr */
+	DNS_PTR(dns_sortaddr_t **,	sortaddr);
+	DNS_PTR(char *,			options);	/* options */
+	DNS_VAR(uint32_t,		timeout);	/* timeout */
+	DNS_VAR(uint32_t,		search_order);	/* search_order */
+	DNS_VAR(uint32_t,		if_index);
+	DNS_VAR(uint32_t,		flags);
+	DNS_VAR(uint32_t,		reach_flags);	/* SCNetworkReachabilityFlags */
+	DNS_VAR(uint32_t,		reserved[5]);
+} dns_resolver_t;
+#pragma pack()
+#define DNS_RESOLVER_FLAGS_SCOPED	1		/* configuration is for scoped questions */
+#pragma pack(4)
+typedef struct {
+	DNS_VAR(int32_t,		n_resolver);		/* resolver configurations */
+	DNS_PTR(dns_resolver_t **,	resolver);
+	DNS_VAR(int32_t,		n_scoped_resolver);	/* "scoped" resolver configurations */
+	DNS_PTR(dns_resolver_t **,	scoped_resolver);
+	DNS_VAR(uint32_t,		reserved[5]);
+} dns_config_t;
+#pragma pack()
+__BEGIN_DECLS
+/*
+ * DNS configuration access APIs
+ */
+const char *
+dns_configuration_notify_key    ();
+dns_config_t *
+dns_configuration_copy		();
+void
+dns_configuration_free		(dns_config_t	*config);
+void
+_dns_configuration_ack		(dns_config_t	*config,
+				 const char	*bundle_id);
+__END_DECLS
+#endif	/* __DNSINFO_H__ */
\ No newline at end of file
diff --git a/resolver-dns-native-macos/src/main/c/netty_resolver_dns_macos.c b/resolver-dns-native-macos/src/main/c/netty_resolver_dns_macos.c
new file mode 100644
index 0000000..68df75c
--- /dev/null
+++ b/resolver-dns-native-macos/src/main/c/netty_resolver_dns_macos.c
@@ -0,0 +1,263 @@
+/*
+ * Copyright 2019 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.
+ */
+#include <errno.h>
+#include <string.h>
+#include <unistd.h>
+#include <dlfcn.h>
+#include <sys/socket.h>
+#include <stdlib.h>
+#include "dnsinfo.h"
+#include "netty_unix_jni.h"
+#include "netty_unix_util.h"
+#include "netty_unix_socket.h"
+#include "netty_unix_errors.h"
+
+static jclass dnsResolverClass = NULL;
+static jclass byteArrayClass = NULL;
+static jclass stringClass = NULL;
+static jmethodID dnsResolverMethodId = NULL;
+
+// JNI Registered Methods Begin
+
+// We use the same API as mDNSResponder and Chromium to retrieve the current nameserver configuration for the system:
+// See:
+//     https://src.chromium.org/viewvc/chrome?revision=218617&view=revision
+//     https://opensource.apple.com/tarballs/mDNSResponder/
+static jobjectArray netty_resolver_dns_macos_resolvers(JNIEnv* env, jclass clazz) {
+    dns_config_t* config = dns_configuration_copy();
+
+    jobjectArray array = (*env)->NewObjectArray(env, config->n_resolver, dnsResolverClass, NULL);
+    if (array == NULL) {
+        goto error;
+    }
+
+    for (int i = 0; i < config->n_resolver; i++) {
+        dns_resolver_t* resolver = config->resolver[i];
+        jstring domain = NULL;
+
+        if (resolver->domain != NULL) {
+            domain = (*env)->NewStringUTF(env, resolver->domain);
+            if (domain == NULL) {
+                goto error;
+            }
+        }
+
+        jobjectArray addressArray = (*env)->NewObjectArray(env, resolver->n_nameserver, byteArrayClass, NULL);
+        if (addressArray == NULL) {
+            goto error;
+        }
+
+        for (int a = 0; a < resolver->n_nameserver; a++) {
+            jbyteArray address = netty_unix_socket_createInetSocketAddressArray(env, (const struct sockaddr_storage *) resolver->nameserver[a]);
+            if (address == NULL) {
+                netty_unix_errors_throwOutOfMemoryError(env);
+                goto error;
+            }
+            (*env)->SetObjectArrayElement(env, addressArray, a, address);
+        }
+
+        jint port = resolver->port;
+
+        jobjectArray searchArray = (*env)->NewObjectArray(env, resolver->n_search, stringClass, NULL);
+        if (searchArray == NULL) {
+            goto error;
+        }
+
+        for (int a = 0; a < resolver->n_search; a++) {
+            jstring search = (*env)->NewStringUTF(env, resolver->search[a]);
+            if (search == NULL) {
+                goto error;
+            }
+
+            (*env)->SetObjectArrayElement(env, searchArray, a, search);
+        }
+
+        jstring options = NULL;
+        if (resolver->options != NULL) {
+            options = (*env)->NewStringUTF(env, resolver->options);
+            if (options == NULL) {
+                goto error;
+            }
+        }
+
+        jint timeout = resolver->timeout;
+        jint searchOrder = resolver->search_order;
+
+        jobject java_resolver = (*env)->NewObject(env, dnsResolverClass, dnsResolverMethodId, domain,
+                addressArray, port, searchArray, options, timeout, searchOrder);
+        if (java_resolver == NULL) {
+            goto error;
+        }
+        (*env)->SetObjectArrayElement(env, array, i, java_resolver);
+    }
+
+    dns_configuration_free(config);
+    return array;
+error:
+    dns_configuration_free(config);
+    return NULL;
+}
+
+
+// JNI Method Registration Table Begin
+
+static JNINativeMethod* createDynamicMethodsTable(const char* packagePrefix) {
+    JNINativeMethod* dynamicMethods = malloc(sizeof(JNINativeMethod) * 1);
+
+    char* dynamicTypeName = netty_unix_util_prepend(packagePrefix, "io/netty/resolver/dns/macos/DnsResolver;");
+    JNINativeMethod* dynamicMethod = &dynamicMethods[0];
+    dynamicMethod->name = "resolvers";
+    dynamicMethod->signature = netty_unix_util_prepend("()[L", dynamicTypeName);
+    dynamicMethod->fnPtr = (void *) netty_resolver_dns_macos_resolvers;
+    free(dynamicTypeName);
+    return dynamicMethods;
+}
+
+static void freeDynamicMethodsTable(JNINativeMethod* dynamicMethods) {
+    free(dynamicMethods[0].signature);
+    free(dynamicMethods);
+}
+
+// JNI Method Registration Table End
+
+
+static void JNI_OnUnload_netty_resolver_dns_native_macos0(JavaVM* vm, void* reserved) {
+    JNIEnv* env;
+    if ((*vm)->GetEnv(vm, (void**) &env, NETTY_JNI_VERSION) != JNI_OK) {
+        // Something is wrong but nothing we can do about this :(
+        return;
+    }
+
+    if (byteArrayClass != NULL) {
+        (*env)->DeleteGlobalRef(env, byteArrayClass);
+        byteArrayClass = NULL;
+    }
+
+    if (stringClass != NULL) {
+        (*env)->DeleteGlobalRef(env, stringClass);
+        stringClass = NULL;
+    }
+}
+
+static void netty_resolver_dns_native_macos0_OnUnLoad(JNIEnv* env) {
+
+}
+
+static jint JNI_OnLoad_netty_resolver_dns_native_macos0(JavaVM* vm, void* reserved) {
+    JNIEnv* env;
+    if ((*vm)->GetEnv(vm, (void**) &env, NETTY_JNI_VERSION) != JNI_OK) {
+        return JNI_ERR;
+    }
+
+#ifndef NETTY_BUILD_STATIC
+    Dl_info dlinfo;
+    jint status = 0;
+    // We need to use an address of a function that is uniquely part of this library, so choose a static
+    // function. See https://github.com/netty/netty/issues/4840.
+    if (!dladdr((void*) netty_resolver_dns_native_macos0_OnUnLoad, &dlinfo)) {
+        fprintf(stderr, "FATAL: resolver-dns-native-macos JNI call to dladdr failed!\n");
+        return JNI_ERR;
+    }
+    char* packagePrefix = netty_unix_util_parse_package_prefix(dlinfo.dli_fname, "netty_resolver_dns_native_macos", &status);
+    if (status == JNI_ERR) {
+        fprintf(stderr, "FATAL: resolver-dns-native-macos JNI encountered unexpected dlinfo.dli_fname: %s\n", dlinfo.dli_fname);
+        return JNI_ERR;
+    }
+#endif /* NETTY_BUILD_STATIC */
+
+    // Register the methods which are not referenced by static member variables
+    JNINativeMethod* dynamicMethods = createDynamicMethodsTable(packagePrefix);
+    if (netty_unix_util_register_natives(env,
+            packagePrefix,
+            "io/netty/resolver/dns/macos/MacOSDnsServerAddressStreamProvider",
+            dynamicMethods, 1) != 0) {
+        freeDynamicMethodsTable(dynamicMethods);
+        fprintf(stderr, "FATAL: Couldnt register natives");
+
+        return JNI_ERR;
+    }
+    freeDynamicMethodsTable(dynamicMethods);
+    dynamicMethods = NULL;
+
+
+    char* nettyClassName = netty_unix_util_prepend(packagePrefix, "io/netty/resolver/dns/macos/DnsResolver");
+    jclass localDnsResolverClass = (*env)->FindClass(env, nettyClassName);
+    free(nettyClassName);
+    nettyClassName = NULL;
+    if (localDnsResolverClass == NULL) {
+        // pending exception...
+        return JNI_ERR;
+    }
+    dnsResolverClass = (jclass) (*env)->NewGlobalRef(env, localDnsResolverClass);
+    if (dnsResolverClass == NULL) {
+        return JNI_ERR;
+    }
+    dnsResolverMethodId = (*env)->GetMethodID(env, dnsResolverClass, "<init>", "(Ljava/lang/String;[[BI[Ljava/lang/String;Ljava/lang/String;II)V");
+    if (dnsResolverMethodId == NULL) {
+        netty_unix_errors_throwRuntimeException(env, "failed to get method ID: DnsResolver.<init>(String, byte[][], String[], String, int, int)");
+        return JNI_ERR;
+    }
+
+    if (packagePrefix != NULL) {
+        free(packagePrefix);
+        packagePrefix = NULL;
+    }
+
+    jclass byteArrayCls = (*env)->FindClass(env, "[B");
+    if (byteArrayCls == NULL) {
+        // pending exception...
+        return JNI_ERR;
+    }
+    byteArrayClass = (jclass) (*env)->NewGlobalRef(env, byteArrayCls);
+    if (byteArrayClass == NULL) {
+        return JNI_ERR;
+    }
+
+    jclass stringCls = (*env)->FindClass(env, "java/lang/String");
+    if (stringCls == NULL) {
+        // pending exception...
+        return JNI_ERR;
+    }
+    stringClass = (jclass) (*env)->NewGlobalRef(env, stringCls);
+    if (stringClass == NULL) {
+        return JNI_ERR;
+    }
+
+    return NETTY_JNI_VERSION;
+}
+
+// We build with -fvisibility=hidden so ensure we mark everything that needs to be visible with JNIEXPORT
+// http://mail.openjdk.java.net/pipermail/core-libs-dev/2013-February/014549.html
+
+// Invoked by the JVM when statically linked
+JNIEXPORT jint JNI_OnLoad_netty_resolver_dns_native_macos(JavaVM* vm, void* reserved) {
+    return JNI_OnLoad_netty_resolver_dns_native_macos0(vm, reserved);
+}
+
+// Invoked by the JVM when statically linked
+JNIEXPORT void JNI_OnUnload_netty_resolver_dns_native_macos(JavaVM* vm, void* reserved) {
+    JNI_OnUnload_netty_resolver_dns_native_macos0(vm, reserved);
+}
+
+#ifndef NETTY_BUILD_STATIC
+JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
+    return JNI_OnLoad_netty_resolver_dns_native_macos0(vm, reserved);
+}
+
+JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved) {
+    return JNI_OnUnload_netty_resolver_dns_native_macos0(vm, reserved);
+}
+#endif /* NETTY_BUILD_STATIC */
diff --git a/resolver-dns-native-macos/src/main/java/io/netty/resolver/dns/macos/DnsResolver.java b/resolver-dns-native-macos/src/main/java/io/netty/resolver/dns/macos/DnsResolver.java
new file mode 100644
index 0000000..9744c65
--- /dev/null
+++ b/resolver-dns-native-macos/src/main/java/io/netty/resolver/dns/macos/DnsResolver.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2019 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.resolver.dns.macos;
+
+import io.netty.channel.unix.NativeInetAddress;
+
+import java.net.InetSocketAddress;
+
+/**
+ * Represent the {@code dns_resolver_t} struct.
+ */
+final class DnsResolver {
+
+    private final String domain;
+    private final InetSocketAddress[] nameservers;
+    private final int port;
+    private final String[] searches;
+    private final String options;
+    private final int timeout;
+    private final int searchOrder;
+
+    DnsResolver(String domain, byte[][] nameservers, int port,
+                String[] searches, String options, int timeout, int searchOrder) {
+        this.domain = domain;
+        if (nameservers == null) {
+            this.nameservers = new InetSocketAddress[0];
+        } else {
+            this.nameservers = new InetSocketAddress[nameservers.length];
+            for (int i = 0; i < nameservers.length; i++) {
+                byte[] addr = nameservers[i];
+                this.nameservers[i] = NativeInetAddress.address(addr, 0, addr.length);
+            }
+        }
+        this.port = port;
+        this.searches = searches;
+        this.options = options;
+        this.timeout = timeout;
+        this.searchOrder = searchOrder;
+    }
+
+    String domain() {
+        return domain;
+    }
+
+    InetSocketAddress[] nameservers() {
+        return nameservers;
+    }
+
+    int port() {
+        return port;
+    }
+
+    String[] searches() {
+        return searches;
+    }
+
+    String options() {
+        return options;
+    }
+
+    int timeout() {
+        return timeout;
+    }
+
+    int searchOrder() {
+        return searchOrder;
+    }
+}
diff --git a/resolver-dns-native-macos/src/main/java/io/netty/resolver/dns/macos/MacOSDnsServerAddressStreamProvider.java b/resolver-dns-native-macos/src/main/java/io/netty/resolver/dns/macos/MacOSDnsServerAddressStreamProvider.java
new file mode 100644
index 0000000..6cdcff6
--- /dev/null
+++ b/resolver-dns-native-macos/src/main/java/io/netty/resolver/dns/macos/MacOSDnsServerAddressStreamProvider.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2019 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.resolver.dns.macos;
+
+import io.netty.resolver.dns.DnsServerAddressStream;
+import io.netty.resolver.dns.DnsServerAddressStreamProvider;
+import io.netty.resolver.dns.DnsServerAddressStreamProviders;
+import io.netty.resolver.dns.DnsServerAddresses;
+import io.netty.util.internal.NativeLibraryLoader;
+import io.netty.util.internal.PlatformDependent;
+import io.netty.util.internal.StringUtil;
+import io.netty.util.internal.SystemPropertyUtil;
+import io.netty.util.internal.ThrowableUtil;
+import io.netty.util.internal.logging.InternalLogger;
+import io.netty.util.internal.logging.InternalLoggerFactory;
+
+import java.net.InetSocketAddress;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * {@link DnsServerAddressStreamProvider} implementation which makes use of the same mechanism as
+ * <a href="https://opensource.apple.com/tarballs/mDNSResponder/">Apple's open source mDNSResponder</a> to retrieve the
+ * current nameserver configuration of the system.
+ */
+public final class MacOSDnsServerAddressStreamProvider implements DnsServerAddressStreamProvider {
+
+    private static final Throwable UNAVAILABILITY_CAUSE;
+
+    private static final InternalLogger logger =
+            InternalLoggerFactory.getInstance(MacOSDnsServerAddressStreamProvider.class);
+
+    // Let's refresh every 10 seconds.
+    private static final long REFRESH_INTERVAL = TimeUnit.SECONDS.toNanos(10);
+
+    static {
+        Throwable cause = null;
+        try {
+            loadNativeLibrary();
+        } catch (Throwable error) {
+            cause = error;
+        }
+        UNAVAILABILITY_CAUSE = cause;
+    }
+
+    private static void loadNativeLibrary() {
+        String name = SystemPropertyUtil.get("os.name").toLowerCase(Locale.UK).trim();
+        if (!name.startsWith("mac")) {
+            throw new IllegalStateException("Only supported on MacOS");
+        }
+        String staticLibName = "netty_resolver_dns_native_macos";
+        String sharedLibName = staticLibName + '_' + PlatformDependent.normalizedArch();
+        ClassLoader cl = PlatformDependent.getClassLoader(MacOSDnsServerAddressStreamProvider.class);
+        try {
+            NativeLibraryLoader.load(sharedLibName, cl);
+        } catch (UnsatisfiedLinkError e1) {
+            try {
+                NativeLibraryLoader.load(staticLibName, cl);
+                logger.debug("Failed to load {}", sharedLibName, e1);
+            } catch (UnsatisfiedLinkError e2) {
+                ThrowableUtil.addSuppressed(e1, e2);
+                throw e1;
+            }
+        }
+    }
+
+    public static boolean isAvailable() {
+        return UNAVAILABILITY_CAUSE == null;
+    }
+
+    public static void ensureAvailability() {
+        if (UNAVAILABILITY_CAUSE != null) {
+            throw (Error) new UnsatisfiedLinkError(
+                    "failed to load the required native library").initCause(UNAVAILABILITY_CAUSE);
+        }
+    }
+
+    public static Throwable unavailabilityCause() {
+        return UNAVAILABILITY_CAUSE;
+    }
+
+    public MacOSDnsServerAddressStreamProvider() {
+        ensureAvailability();
+    }
+
+    private volatile Map<String, DnsServerAddresses> currentMappings = retrieveCurrentMappings();
+    private final AtomicLong lastRefresh = new AtomicLong(System.nanoTime());
+
+    private static Map<String, DnsServerAddresses> retrieveCurrentMappings() {
+        DnsResolver[] resolvers = resolvers();
+
+        if (resolvers == null || resolvers.length == 0) {
+            return Collections.emptyMap();
+        }
+        Map<String, DnsServerAddresses> resolverMap = new HashMap<String, DnsServerAddresses>(resolvers.length);
+        for (DnsResolver resolver: resolvers) {
+            // Skip mdns
+            if ("mdns".equalsIgnoreCase(resolver.options())) {
+                continue;
+            }
+            InetSocketAddress[] nameservers = resolver.nameservers();
+            if (nameservers == null || nameservers.length == 0) {
+                continue;
+            }
+            String domain = resolver.domain();
+            if (domain == null) {
+                // Default mapping.
+                domain = StringUtil.EMPTY_STRING;
+            }
+            InetSocketAddress[] servers = resolver.nameservers();
+            for (int a = 0; a < servers.length; a++) {
+                InetSocketAddress address = servers[a];
+                // Check if the default port should be used
+                if (address.getPort() == 0) {
+                    int port = resolver.port();
+                    if (port == 0) {
+                        port = 53;
+                    }
+                    servers[a] = new InetSocketAddress(address.getAddress(), port);
+                }
+            }
+
+            resolverMap.put(domain, DnsServerAddresses.sequential(servers));
+        }
+        return resolverMap;
+    }
+
+    @Override
+    public DnsServerAddressStream nameServerAddressStream(String hostname) {
+        long last = lastRefresh.get();
+        Map<String, DnsServerAddresses> resolverMap = currentMappings;
+        if (System.nanoTime() - last > REFRESH_INTERVAL) {
+            // This is slightly racy which means it will be possible still use the old configuration for a small
+            // amount of time, but that's ok.
+            if (lastRefresh.compareAndSet(last, System.nanoTime())) {
+                resolverMap = currentMappings = retrieveCurrentMappings();
+            }
+        }
+
+        final String originalHostname = hostname;
+        for (;;) {
+            int i = hostname.indexOf('.', 1);
+            if (i < 0 || i == hostname.length() - 1) {
+                // Try access default mapping.
+                DnsServerAddresses addresses = resolverMap.get(StringUtil.EMPTY_STRING);
+                if (addresses != null) {
+                    return addresses.stream();
+                }
+                return DnsServerAddressStreamProviders.unixDefault().nameServerAddressStream(originalHostname);
+            }
+
+            DnsServerAddresses addresses = resolverMap.get(hostname);
+            if (addresses != null) {
+                return addresses.stream();
+            }
+
+            hostname = hostname.substring(i + 1);
+        }
+    }
+
+    private static native DnsResolver[] resolvers();
+}
diff --git a/resolver-dns-native-macos/src/main/java/io/netty/resolver/dns/macos/package-info.java b/resolver-dns-native-macos/src/main/java/io/netty/resolver/dns/macos/package-info.java
new file mode 100644
index 0000000..56a806f
--- /dev/null
+++ b/resolver-dns-native-macos/src/main/java/io/netty/resolver/dns/macos/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2019 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.
+ */
+
+/**
+ * MacOS specific nameserver resolution.
+ */
+package io.netty.resolver.dns.macos;
diff --git a/resolver-dns-native-macos/src/test/java/io/netty/resolver/dns/macos/MacOSDnsServerAddressStreamProviderTest.java b/resolver-dns-native-macos/src/test/java/io/netty/resolver/dns/macos/MacOSDnsServerAddressStreamProviderTest.java
new file mode 100644
index 0000000..424716f
--- /dev/null
+++ b/resolver-dns-native-macos/src/test/java/io/netty/resolver/dns/macos/MacOSDnsServerAddressStreamProviderTest.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2019 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.resolver.dns.macos;
+
+import io.netty.resolver.dns.DnsServerAddressStream;
+import io.netty.resolver.dns.DnsServerAddressStreamProvider;
+import io.netty.resolver.dns.DnsServerAddressStreamProviders;
+import org.hamcrest.Matchers;
+import org.junit.Assert;
+import org.junit.Assume;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class MacOSDnsServerAddressStreamProviderTest {
+
+    @BeforeClass
+    public static void assume() {
+        Assume.assumeTrue(MacOSDnsServerAddressStreamProvider.isAvailable());
+    }
+
+    @Test
+    public void testStream() {
+        DnsServerAddressStreamProvider provider = new MacOSDnsServerAddressStreamProvider();
+        DnsServerAddressStream stream = provider.nameServerAddressStream("netty.io");
+        Assert.assertNotNull(stream);
+        Assert.assertNotEquals(0, stream.size());
+
+        for (int i = 0; i < stream.size(); i++) {
+            Assert.assertNotEquals(0, stream.next().getPort());
+        }
+    }
+
+    @Test
+    public void testDefaultUseCorrectInstance() {
+        Assert.assertThat(DnsServerAddressStreamProviders.platformDefault(),
+                Matchers.instanceOf(MacOSDnsServerAddressStreamProvider.class));
+    }
+
+}
diff --git a/resolver-dns/pom.xml b/resolver-dns/pom.xml
index 1f81cca..d18cc6a 100644
--- a/resolver-dns/pom.xml
+++ b/resolver-dns/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>io.netty</groupId>
     <artifactId>netty-parent</artifactId>
-    <version>4.1.33.Final</version>
+    <version>4.1.48.Final</version>
   </parent>
 
   <artifactId>netty-resolver-dns</artifactId>
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/AuthoritativeDnsServerCache.java b/resolver-dns/src/main/java/io/netty/resolver/dns/AuthoritativeDnsServerCache.java
index b011066..f4a443e 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/AuthoritativeDnsServerCache.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/AuthoritativeDnsServerCache.java
@@ -16,14 +16,12 @@
 package io.netty.resolver.dns;
 
 import io.netty.channel.EventLoop;
-import io.netty.util.internal.UnstableApi;
 
 import java.net.InetSocketAddress;
 
 /**
  * Cache which stores the nameservers that should be used to resolve a specific hostname.
  */
-@UnstableApi
 public interface AuthoritativeDnsServerCache {
 
     /**
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/AuthoritativeDnsServerCacheAdapter.java b/resolver-dns/src/main/java/io/netty/resolver/dns/AuthoritativeDnsServerCacheAdapter.java
index 470106e..5fd7840 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/AuthoritativeDnsServerCacheAdapter.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/AuthoritativeDnsServerCacheAdapter.java
@@ -17,7 +17,6 @@ package io.netty.resolver.dns;
 
 import io.netty.channel.EventLoop;
 import io.netty.handler.codec.dns.DnsRecord;
-import io.netty.util.internal.UnstableApi;
 
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
@@ -30,7 +29,6 @@ import static io.netty.util.internal.ObjectUtil.checkNotNull;
  * {@link AuthoritativeDnsServerCache} implementation which delegates all operations to a wrapped {@link DnsCache}.
  * This implementation is only present to preserve a upgrade story.
  */
-@UnstableApi
 final class AuthoritativeDnsServerCacheAdapter implements AuthoritativeDnsServerCache {
 
     private static final DnsRecord[] EMPTY = new DnsRecord[0];
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/BiDnsQueryLifecycleObserver.java b/resolver-dns/src/main/java/io/netty/resolver/dns/BiDnsQueryLifecycleObserver.java
index e4b4c35..cd596c1 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/BiDnsQueryLifecycleObserver.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/BiDnsQueryLifecycleObserver.java
@@ -18,7 +18,6 @@ package io.netty.resolver.dns;
 import io.netty.channel.ChannelFuture;
 import io.netty.handler.codec.dns.DnsQuestion;
 import io.netty.handler.codec.dns.DnsResponseCode;
-import io.netty.util.internal.UnstableApi;
 
 import java.net.InetSocketAddress;
 import java.util.List;
@@ -28,7 +27,6 @@ import static io.netty.util.internal.ObjectUtil.checkNotNull;
 /**
  * Combines two {@link DnsQueryLifecycleObserver} into a single {@link DnsQueryLifecycleObserver}.
  */
-@UnstableApi
 public final class BiDnsQueryLifecycleObserver implements DnsQueryLifecycleObserver {
     private final DnsQueryLifecycleObserver a;
     private final DnsQueryLifecycleObserver b;
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/BiDnsQueryLifecycleObserverFactory.java b/resolver-dns/src/main/java/io/netty/resolver/dns/BiDnsQueryLifecycleObserverFactory.java
index 6745a36..b7125bc 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/BiDnsQueryLifecycleObserverFactory.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/BiDnsQueryLifecycleObserverFactory.java
@@ -16,14 +16,12 @@
 package io.netty.resolver.dns;
 
 import io.netty.handler.codec.dns.DnsQuestion;
-import io.netty.util.internal.UnstableApi;
 
 import static io.netty.util.internal.ObjectUtil.checkNotNull;
 
 /**
  * Combines two {@link DnsQueryLifecycleObserverFactory} into a single {@link DnsQueryLifecycleObserverFactory}.
  */
-@UnstableApi
 public final class BiDnsQueryLifecycleObserverFactory implements DnsQueryLifecycleObserverFactory {
     private final DnsQueryLifecycleObserverFactory a;
     private final DnsQueryLifecycleObserverFactory b;
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DatagramDnsQueryContext.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DatagramDnsQueryContext.java
new file mode 100644
index 0000000..0ac80b9
--- /dev/null
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DatagramDnsQueryContext.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2019 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.resolver.dns;
+
+import io.netty.channel.AddressedEnvelope;
+import io.netty.channel.Channel;
+import io.netty.handler.codec.dns.DatagramDnsQuery;
+import io.netty.handler.codec.dns.DnsQuery;
+import io.netty.handler.codec.dns.DnsQuestion;
+import io.netty.handler.codec.dns.DnsRecord;
+import io.netty.handler.codec.dns.DnsResponse;
+import io.netty.util.concurrent.Promise;
+
+import java.net.InetSocketAddress;
+
+final class DatagramDnsQueryContext extends DnsQueryContext {
+
+    DatagramDnsQueryContext(DnsNameResolver parent, InetSocketAddress nameServerAddr, DnsQuestion question,
+                            DnsRecord[] additionals,
+                            Promise<AddressedEnvelope<DnsResponse, InetSocketAddress>> promise) {
+        super(parent, nameServerAddr, question, additionals, promise);
+    }
+
+    @Override
+    protected DnsQuery newQuery(int id) {
+        return new DatagramDnsQuery(null, nameServerAddr(), id);
+    }
+
+    @Override
+    protected Channel channel() {
+        return parent().ch;
+    }
+
+    @Override
+    protected String protocol() {
+        return "UDP";
+    }
+}
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultAuthoritativeDnsServerCache.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultAuthoritativeDnsServerCache.java
index 6bc29a6..7f8a9b1 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultAuthoritativeDnsServerCache.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultAuthoritativeDnsServerCache.java
@@ -17,7 +17,6 @@ package io.netty.resolver.dns;
 
 import io.netty.channel.EventLoop;
 import io.netty.util.internal.PlatformDependent;
-import io.netty.util.internal.UnstableApi;
 
 import java.net.InetSocketAddress;
 import java.util.Collections;
@@ -30,7 +29,6 @@ import static io.netty.util.internal.ObjectUtil.*;
 /**
  * Default implementation of {@link AuthoritativeDnsServerCache}, backed by a {@link ConcurrentMap}.
  */
-@UnstableApi
 public class DefaultAuthoritativeDnsServerCache implements AuthoritativeDnsServerCache {
 
     private final int minTtl;
@@ -117,9 +115,7 @@ public class DefaultAuthoritativeDnsServerCache implements AuthoritativeDnsServe
 
     @Override
     public boolean clear(String hostname) {
-        checkNotNull(hostname, "hostname");
-
-        return resolveCache.clear(hostname);
+        return resolveCache.clear(checkNotNull(hostname, "hostname"));
     }
 
     @Override
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsCache.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsCache.java
index c197839..9d85768 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsCache.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsCache.java
@@ -18,7 +18,6 @@ package io.netty.resolver.dns;
 import io.netty.channel.EventLoop;
 import io.netty.handler.codec.dns.DnsRecord;
 import io.netty.util.internal.StringUtil;
-import io.netty.util.internal.UnstableApi;
 
 import java.net.InetAddress;
 import java.util.Collections;
@@ -32,7 +31,6 @@ import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
  * Default implementation of {@link DnsCache}, backed by a {@link ConcurrentMap}.
  * If any additional {@link DnsRecord} is used, no caching takes place.
  */
-@UnstableApi
 public class DefaultDnsCache implements DnsCache {
 
     private final Cache<DefaultDnsCacheEntry> resolveCache = new Cache<DefaultDnsCacheEntry>() {
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsCnameCache.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsCnameCache.java
index 8638808..88429b9 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsCnameCache.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsCnameCache.java
@@ -17,7 +17,6 @@ package io.netty.resolver.dns;
 
 import io.netty.channel.EventLoop;
 import io.netty.util.AsciiString;
-import io.netty.util.internal.UnstableApi;
 
 import java.util.List;
 
@@ -26,7 +25,6 @@ import static io.netty.util.internal.ObjectUtil.*;
 /**
  * Default implementation of a {@link DnsCnameCache}.
  */
-@UnstableApi
 public final class DefaultDnsCnameCache implements DnsCnameCache {
     private final int minTtl;
     private final int maxTtl;
@@ -69,8 +67,7 @@ public final class DefaultDnsCnameCache implements DnsCnameCache {
     @SuppressWarnings("unchecked")
     @Override
     public String get(String hostname) {
-        checkNotNull(hostname, "hostname");
-        List<? extends String> cached =  cache.get(hostname);
+        List<? extends String> cached =  cache.get(checkNotNull(hostname, "hostname"));
         if (cached == null || cached.isEmpty()) {
             return null;
         }
@@ -93,7 +90,6 @@ public final class DefaultDnsCnameCache implements DnsCnameCache {
 
     @Override
     public boolean clear(String hostname) {
-        checkNotNull(hostname, "hostname");
-        return cache.clear(hostname);
+        return cache.clear(checkNotNull(hostname, "hostname"));
     }
 }
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsServerAddressStreamProvider.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsServerAddressStreamProvider.java
index 00be072..6bc491c 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsServerAddressStreamProvider.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsServerAddressStreamProvider.java
@@ -18,7 +18,6 @@ package io.netty.resolver.dns;
 import io.netty.util.NetUtil;
 import io.netty.util.internal.PlatformDependent;
 import io.netty.util.internal.SocketUtils;
-import io.netty.util.internal.UnstableApi;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 
@@ -37,7 +36,6 @@ import static io.netty.resolver.dns.DnsServerAddresses.sequential;
  * <p>
  * This may use the JDK's blocking DNS resolution to bootstrap the default DNS server addresses.
  */
-@UnstableApi
 public final class DefaultDnsServerAddressStreamProvider implements DnsServerAddressStreamProvider {
     private static final InternalLogger logger =
             InternalLoggerFactory.getInstance(DefaultDnsServerAddressStreamProvider.class);
@@ -55,7 +53,9 @@ public final class DefaultDnsServerAddressStreamProvider implements DnsServerAdd
             DirContextUtils.addNameServers(defaultNameServers, DNS_PORT);
         }
 
-        if (defaultNameServers.isEmpty()) {
+        // Only try when using Java8 and lower as otherwise it will produce:
+        // WARNING: Illegal reflective access by io.netty.resolver.dns.DefaultDnsServerAddressStreamProvider
+        if (PlatformDependent.javaVersion() < 9 && defaultNameServers.isEmpty()) {
             try {
                 Class<?> configClass = Class.forName("sun.net.dns.ResolverConfiguration");
                 Method open = configClass.getMethod("open");
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressResolveContext.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressResolveContext.java
index 85078c6..944e4d7 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressResolveContext.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressResolveContext.java
@@ -19,7 +19,7 @@ import static io.netty.resolver.dns.DnsAddressDecoder.decodeAddress;
 
 import java.net.InetAddress;
 import java.net.UnknownHostException;
-import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 import io.netty.channel.EventLoop;
@@ -31,13 +31,16 @@ final class DnsAddressResolveContext extends DnsResolveContext<InetAddress> {
 
     private final DnsCache resolveCache;
     private final AuthoritativeDnsServerCache authoritativeDnsServerCache;
+    private final boolean completeEarlyIfPossible;
 
     DnsAddressResolveContext(DnsNameResolver parent, String hostname, DnsRecord[] additionals,
                              DnsServerAddressStream nameServerAddrs, DnsCache resolveCache,
-                             AuthoritativeDnsServerCache authoritativeDnsServerCache) {
+                             AuthoritativeDnsServerCache authoritativeDnsServerCache,
+                             boolean completeEarlyIfPossible) {
         super(parent, hostname, DnsRecord.CLASS_IN, parent.resolveRecordTypes(), additionals, nameServerAddrs);
         this.resolveCache = resolveCache;
         this.authoritativeDnsServerCache = authoritativeDnsServerCache;
+        this.completeEarlyIfPossible = completeEarlyIfPossible;
     }
 
     @Override
@@ -46,7 +49,7 @@ final class DnsAddressResolveContext extends DnsResolveContext<InetAddress> {
                                                       DnsRecord[] additionals,
                                                       DnsServerAddressStream nameServerAddrs) {
         return new DnsAddressResolveContext(parent, hostname, additionals, nameServerAddrs, resolveCache,
-                authoritativeDnsServerCache);
+                authoritativeDnsServerCache, completeEarlyIfPossible);
     }
 
     @Override
@@ -56,27 +59,19 @@ final class DnsAddressResolveContext extends DnsResolveContext<InetAddress> {
 
     @Override
     List<InetAddress> filterResults(List<InetAddress> unfiltered) {
-        final Class<? extends InetAddress> inetAddressType = parent.preferredAddressType().addressType();
-        final int size = unfiltered.size();
-        int numExpected = 0;
-        for (int i = 0; i < size; i++) {
-            InetAddress address = unfiltered.get(i);
-            if (inetAddressType.isInstance(address)) {
-                numExpected++;
-            }
-        }
-        if (numExpected == size || numExpected == 0) {
-            // If all the results are the preferred type, or none of them are, then we don't need to do any filtering.
-            return unfiltered;
-        }
-        List<InetAddress> filtered = new ArrayList<InetAddress>(numExpected);
-        for (int i = 0; i < size; i++) {
-            InetAddress address = unfiltered.get(i);
-            if (inetAddressType.isInstance(address)) {
-                filtered.add(address);
-            }
-        }
-        return filtered;
+        Collections.sort(unfiltered, PreferredAddressTypeComparator.comparator(parent.preferredAddressType()));
+        return unfiltered;
+    }
+
+    @Override
+    boolean isCompleteEarly(InetAddress resolved) {
+        return completeEarlyIfPossible && parent.preferredAddressType().addressType() == resolved.getClass();
+    }
+
+    @Override
+    boolean isDuplicateAllowed() {
+        // We don't want include duplicates to mimic JDK behaviour.
+        return false;
     }
 
     @Override
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressResolverGroup.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressResolverGroup.java
index 303c42a..69ece07 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressResolverGroup.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressResolverGroup.java
@@ -18,7 +18,6 @@ package io.netty.resolver.dns;
 
 import io.netty.channel.ChannelFactory;
 import io.netty.channel.EventLoop;
-import io.netty.channel.ReflectiveChannelFactory;
 import io.netty.channel.socket.DatagramChannel;
 import io.netty.resolver.AddressResolver;
 import io.netty.resolver.AddressResolverGroup;
@@ -27,7 +26,6 @@ import io.netty.resolver.NameResolver;
 import io.netty.util.concurrent.EventExecutor;
 import io.netty.util.concurrent.Promise;
 import io.netty.util.internal.StringUtil;
-import io.netty.util.internal.UnstableApi;
 
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
@@ -39,7 +37,6 @@ import static io.netty.util.internal.PlatformDependent.newConcurrentHashMap;
 /**
  * A {@link AddressResolverGroup} of {@link DnsNameResolver}s.
  */
-@UnstableApi
 public class DnsAddressResolverGroup extends AddressResolverGroup<InetSocketAddress> {
 
     private final DnsNameResolverBuilder dnsResolverBuilder;
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsCache.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsCache.java
index 28e8989..1ddaa53 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsCache.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsCache.java
@@ -17,7 +17,6 @@ package io.netty.resolver.dns;
 
 import io.netty.channel.EventLoop;
 import io.netty.handler.codec.dns.DnsRecord;
-import io.netty.util.internal.UnstableApi;
 
 import java.net.InetAddress;
 import java.util.List;
@@ -25,7 +24,6 @@ import java.util.List;
 /**
  * A cache for DNS resolution entries.
  */
-@UnstableApi
 public interface DnsCache {
 
     /**
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsCacheEntry.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsCacheEntry.java
index 129402b..25eaec0 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsCacheEntry.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsCacheEntry.java
@@ -15,14 +15,11 @@
  */
 package io.netty.resolver.dns;
 
-import io.netty.util.internal.UnstableApi;
-
 import java.net.InetAddress;
 
 /**
  * Represents the results from a previous DNS query which can be cached.
  */
-@UnstableApi
 public interface DnsCacheEntry {
     /**
      * Get the resolved address.
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsCnameCache.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsCnameCache.java
index 820ef67..0c1b370 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsCnameCache.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsCnameCache.java
@@ -16,12 +16,10 @@
 package io.netty.resolver.dns;
 
 import io.netty.channel.EventLoop;
-import io.netty.util.internal.UnstableApi;
 
 /**
  * A cache for {@code CNAME}s.
  */
-@UnstableApi
 public interface DnsCnameCache {
 
     /**
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolver.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolver.java
index c8384ff..625320d 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolver.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolver.java
@@ -31,7 +31,9 @@ import io.netty.channel.ChannelPromise;
 import io.netty.channel.EventLoop;
 import io.netty.channel.FixedRecvByteBufAllocator;
 import io.netty.channel.socket.DatagramChannel;
+import io.netty.channel.socket.DatagramPacket;
 import io.netty.channel.socket.InternetProtocolFamily;
+import io.netty.channel.socket.SocketChannel;
 import io.netty.handler.codec.dns.DatagramDnsQueryEncoder;
 import io.netty.handler.codec.dns.DatagramDnsResponse;
 import io.netty.handler.codec.dns.DatagramDnsResponseDecoder;
@@ -41,12 +43,13 @@ import io.netty.handler.codec.dns.DnsRawRecord;
 import io.netty.handler.codec.dns.DnsRecord;
 import io.netty.handler.codec.dns.DnsRecordType;
 import io.netty.handler.codec.dns.DnsResponse;
+import io.netty.handler.codec.dns.TcpDnsQueryEncoder;
+import io.netty.handler.codec.dns.TcpDnsResponseDecoder;
 import io.netty.resolver.HostsFileEntries;
 import io.netty.resolver.HostsFileEntriesResolver;
 import io.netty.resolver.InetNameResolver;
 import io.netty.resolver.ResolvedAddressTypes;
 import io.netty.util.NetUtil;
-import io.netty.util.ReferenceCountUtil;
 import io.netty.util.concurrent.EventExecutor;
 import io.netty.util.concurrent.FastThreadLocal;
 import io.netty.util.concurrent.Future;
@@ -55,7 +58,6 @@ import io.netty.util.concurrent.Promise;
 import io.netty.util.internal.EmptyArrays;
 import io.netty.util.internal.PlatformDependent;
 import io.netty.util.internal.StringUtil;
-import io.netty.util.internal.UnstableApi;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 
@@ -65,10 +67,14 @@ import java.net.Inet4Address;
 import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
+import java.net.NetworkInterface;
+import java.net.SocketAddress;
+import java.net.SocketException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.Enumeration;
 import java.util.Iterator;
 import java.util.List;
 
@@ -80,7 +86,6 @@ import static io.netty.util.internal.ObjectUtil.checkPositive;
 /**
  * A DNS-based {@link InetNameResolver}.
  */
-@UnstableApi
 public class DnsNameResolver extends InetNameResolver {
 
     private static final InternalLogger logger = InternalLoggerFactory.getInstance(DnsNameResolver.class);
@@ -109,7 +114,7 @@ public class DnsNameResolver extends InetNameResolver {
     private static final int DEFAULT_NDOTS;
 
     static {
-        if (NetUtil.isIpV4StackPreferred()) {
+        if (NetUtil.isIpV4StackPreferred() || !anyInterfaceSupportsIpV6()) {
             DEFAULT_RESOLVE_ADDRESS_TYPES = ResolvedAddressTypes.IPV4_ONLY;
             LOCALHOST_ADDRESS = NetUtil.LOCALHOST4;
         } else {
@@ -145,20 +150,66 @@ public class DnsNameResolver extends InetNameResolver {
         DEFAULT_NDOTS = ndots;
     }
 
+    /**
+     * Returns {@code true} if any {@link NetworkInterface} supports {@code IPv6}, {@code false} otherwise.
+     */
+    private static boolean anyInterfaceSupportsIpV6() {
+        try {
+            Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
+            while (interfaces.hasMoreElements()) {
+                NetworkInterface iface = interfaces.nextElement();
+                Enumeration<InetAddress> addresses = iface.getInetAddresses();
+                while (addresses.hasMoreElements()) {
+                    if (addresses.nextElement() instanceof Inet6Address) {
+                        return true;
+                    }
+                }
+            }
+        } catch (SocketException e) {
+            logger.debug("Unable to detect if any interface supports IPv6, assuming IPv4-only", e);
+            // ignore
+        }
+        return false;
+    }
+
     @SuppressWarnings("unchecked")
     private static List<String> getSearchDomainsHack() throws Exception {
-        // This code on Java 9+ yields a warning about illegal reflective access that will be denied in
-        // a future release. There doesn't seem to be a better way to get search domains for Windows yet.
-        Class<?> configClass = Class.forName("sun.net.dns.ResolverConfiguration");
-        Method open = configClass.getMethod("open");
-        Method nameservers = configClass.getMethod("searchlist");
-        Object instance = open.invoke(null);
-
-        return (List<String>) nameservers.invoke(instance);
+        // Only try if not using Java9 and later
+        // See https://github.com/netty/netty/issues/9500
+        if (PlatformDependent.javaVersion() < 9) {
+            // This code on Java 9+ yields a warning about illegal reflective access that will be denied in
+            // a future release. There doesn't seem to be a better way to get search domains for Windows yet.
+            Class<?> configClass = Class.forName("sun.net.dns.ResolverConfiguration");
+            Method open = configClass.getMethod("open");
+            Method nameservers = configClass.getMethod("searchlist");
+            Object instance = open.invoke(null);
+
+            return (List<String>) nameservers.invoke(instance);
+        }
+        return Collections.emptyList();
     }
 
-    private static final DatagramDnsResponseDecoder DECODER = new DatagramDnsResponseDecoder();
-    private static final DatagramDnsQueryEncoder ENCODER = new DatagramDnsQueryEncoder();
+    private static final DatagramDnsResponseDecoder DATAGRAM_DECODER = new DatagramDnsResponseDecoder() {
+        @Override
+        protected DnsResponse decodeResponse(ChannelHandlerContext ctx, DatagramPacket packet) throws Exception {
+            DnsResponse response = super.decodeResponse(ctx, packet);
+            if (packet.content().isReadable()) {
+                // If there is still something to read we did stop parsing because of a truncated message.
+                // This can happen if we enabled EDNS0 but our MTU is not big enough to handle all the
+                // data.
+                response.setTruncated(true);
+
+                if (logger.isDebugEnabled()) {
+                    logger.debug(
+                            "{} RECEIVED: UDP truncated packet received, consider adjusting maxPayloadSize for the {}.",
+                            ctx.channel(), StringUtil.simpleClassName(DnsNameResolver.class));
+                }
+            }
+            return response;
+        }
+    };
+    private static final DatagramDnsQueryEncoder DATAGRAM_ENCODER = new DatagramDnsQueryEncoder();
+    private static final TcpDnsQueryEncoder TCP_ENCODER = new TcpDnsQueryEncoder();
 
     final Future<Channel> channelFuture;
     final Channel ch;
@@ -202,6 +253,8 @@ public class DnsNameResolver extends InetNameResolver {
     private final DnsRecordType[] resolveRecordTypes;
     private final boolean decodeIdn;
     private final DnsQueryLifecycleObserverFactory dnsQueryLifecycleObserverFactory;
+    private final boolean completeOncePreferredResolved;
+    private final ChannelFactory<? extends SocketChannel> socketChannelFactory;
 
     /**
      * Creates a new DNS-based name resolver that communicates with the specified list of DNS servers.
@@ -300,15 +353,16 @@ public class DnsNameResolver extends InetNameResolver {
             String[] searchDomains,
             int ndots,
             boolean decodeIdn) {
-        this(eventLoop, channelFactory, resolveCache, NoopDnsCnameCache.INSTANCE, authoritativeDnsServerCache,
+        this(eventLoop, channelFactory, null, resolveCache, NoopDnsCnameCache.INSTANCE, authoritativeDnsServerCache,
              dnsQueryLifecycleObserverFactory, queryTimeoutMillis, resolvedAddressTypes, recursionDesired,
              maxQueriesPerResolve, traceEnabled, maxPayloadSize, optResourceEnabled, hostsFileEntriesResolver,
-             dnsServerAddressStreamProvider, searchDomains, ndots, decodeIdn);
+             dnsServerAddressStreamProvider, searchDomains, ndots, decodeIdn, false);
     }
 
     DnsNameResolver(
             EventLoop eventLoop,
             ChannelFactory<? extends DatagramChannel> channelFactory,
+            ChannelFactory<? extends SocketChannel> socketChannelFactory,
             final DnsCache resolveCache,
             final DnsCnameCache cnameCache,
             final AuthoritativeDnsServerCache authoritativeDnsServerCache,
@@ -324,7 +378,8 @@ public class DnsNameResolver extends InetNameResolver {
             DnsServerAddressStreamProvider dnsServerAddressStreamProvider,
             String[] searchDomains,
             int ndots,
-            boolean decodeIdn) {
+            boolean decodeIdn,
+            boolean completeOncePreferredResolved) {
         super(eventLoop);
         this.queryTimeoutMillis = checkPositive(queryTimeoutMillis, "queryTimeoutMillis");
         this.resolvedAddressTypes = resolvedAddressTypes != null ? resolvedAddressTypes : DEFAULT_RESOLVE_ADDRESS_TYPES;
@@ -346,7 +401,8 @@ public class DnsNameResolver extends InetNameResolver {
         this.searchDomains = searchDomains != null ? searchDomains.clone() : DEFAULT_SEARCH_DOMAINS;
         this.ndots = ndots >= 0 ? ndots : DEFAULT_NDOTS;
         this.decodeIdn = decodeIdn;
-
+        this.completeOncePreferredResolved = completeOncePreferredResolved;
+        this.socketChannelFactory = socketChannelFactory;
         switch (this.resolvedAddressTypes) {
             case IPV4_ONLY:
                 supportsAAAARecords = false;
@@ -386,8 +442,8 @@ public class DnsNameResolver extends InetNameResolver {
         final DnsResponseHandler responseHandler = new DnsResponseHandler(executor().<Channel>newPromise());
         b.handler(new ChannelInitializer<DatagramChannel>() {
             @Override
-            protected void initChannel(DatagramChannel ch) throws Exception {
-                ch.pipeline().addLast(DECODER, ENCODER, responseHandler);
+            protected void initChannel(DatagramChannel ch) {
+                ch.pipeline().addLast(DATAGRAM_ENCODER, DATAGRAM_DECODER, responseHandler);
             }
         });
 
@@ -826,7 +882,7 @@ public class DnsNameResolver extends InetNameResolver {
         }
 
         if (!doResolveCached(hostname, additionals, promise, resolveCache)) {
-            doResolveUncached(hostname, additionals, promise, resolveCache);
+            doResolveUncached(hostname, additionals, promise, resolveCache, true);
         }
     }
 
@@ -861,22 +917,28 @@ public class DnsNameResolver extends InetNameResolver {
 
     static <T> void trySuccess(Promise<T> promise, T result) {
         if (!promise.trySuccess(result)) {
-            logger.warn("Failed to notify success ({}) to a promise: {}", result, promise);
+            // There is nothing really wrong with not be able to notify the promise as we may have raced here because
+            // of multiple queries that have been executed. Log it with trace level anyway just in case the user
+            // wants to better understand what happened.
+            logger.trace("Failed to notify success ({}) to a promise: {}", result, promise);
         }
     }
 
     private static void tryFailure(Promise<?> promise, Throwable cause) {
         if (!promise.tryFailure(cause)) {
-            logger.warn("Failed to notify failure to a promise: {}", promise, cause);
+            // There is nothing really wrong with not be able to notify the promise as we may have raced here because
+            // of multiple queries that have been executed. Log it with trace level anyway just in case the user
+            // wants to better understand what happened.
+            logger.trace("Failed to notify failure to a promise: {}", promise, cause);
         }
     }
 
     private void doResolveUncached(String hostname,
                                    DnsRecord[] additionals,
                                    final Promise<InetAddress> promise,
-                                   DnsCache resolveCache) {
+                                   DnsCache resolveCache, boolean completeEarlyIfPossible) {
         final Promise<List<InetAddress>> allPromise = executor().newPromise();
-        doResolveAllUncached(hostname, additionals, allPromise, resolveCache);
+        doResolveAllUncached(hostname, additionals, allPromise, resolveCache, true);
         allPromise.addListener(new FutureListener<List<InetAddress>>() {
             @Override
             public void operationComplete(Future<List<InetAddress>> future) {
@@ -923,7 +985,7 @@ public class DnsNameResolver extends InetNameResolver {
         }
 
         if (!doResolveAllCached(hostname, additionals, promise, resolveCache, resolvedInternetProtocolFamilies)) {
-            doResolveAllUncached(hostname, additionals, promise, resolveCache);
+            doResolveAllUncached(hostname, additionals, promise, resolveCache, completeOncePreferredResolved);
         }
     }
 
@@ -966,33 +1028,35 @@ public class DnsNameResolver extends InetNameResolver {
     private void doResolveAllUncached(final String hostname,
                                       final DnsRecord[] additionals,
                                       final Promise<List<InetAddress>> promise,
-                                      final DnsCache resolveCache) {
+                                      final DnsCache resolveCache,
+                                      final boolean completeEarlyIfPossible) {
         // Call doResolveUncached0(...) in the EventLoop as we may need to submit multiple queries which would need
         // to submit multiple Runnable at the end if we are not already on the EventLoop.
         EventExecutor executor = executor();
         if (executor.inEventLoop()) {
-            doResolveAllUncached0(hostname, additionals, promise, resolveCache);
+            doResolveAllUncached0(hostname, additionals, promise, resolveCache, completeEarlyIfPossible);
         } else {
             executor.execute(new Runnable() {
                 @Override
                 public void run() {
-                    doResolveAllUncached0(hostname, additionals, promise, resolveCache);
+                    doResolveAllUncached0(hostname, additionals, promise, resolveCache, completeEarlyIfPossible);
                 }
             });
         }
     }
 
     private void doResolveAllUncached0(String hostname,
-                                      DnsRecord[] additionals,
-                                      Promise<List<InetAddress>> promise,
-                                      DnsCache resolveCache) {
+                                       DnsRecord[] additionals,
+                                       Promise<List<InetAddress>> promise,
+                                       DnsCache resolveCache,
+                                       boolean completeEarlyIfPossible) {
 
         assert executor().inEventLoop();
 
         final DnsServerAddressStream nameServerAddrs =
                 dnsServerAddressStreamProvider.nameServerAddressStream(hostname);
-        new DnsAddressResolveContext(this, hostname, additionals, nameServerAddrs,
-                                     resolveCache, authoritativeDnsServerCache).resolve(promise);
+        new DnsAddressResolveContext(this, hostname, additionals, nameServerAddrs, resolveCache,
+                authoritativeDnsServerCache, completeEarlyIfPossible).resolve(promise);
     }
 
     private static String hostname(String inetHost) {
@@ -1105,7 +1169,7 @@ public class DnsNameResolver extends InetNameResolver {
         final Promise<AddressedEnvelope<DnsResponse, InetSocketAddress>> castPromise = cast(
                 checkNotNull(promise, "promise"));
         try {
-            new DnsQueryContext(this, nameServerAddr, question, additionals, castPromise)
+            new DatagramDnsQueryContext(this, nameServerAddr, question, additionals, castPromise)
                     .query(flush, writePromise);
             return castPromise;
         } catch (Exception e) {
@@ -1132,24 +1196,107 @@ public class DnsNameResolver extends InetNameResolver {
 
         @Override
         public void channelRead(ChannelHandlerContext ctx, Object msg) {
-            try {
-                final DatagramDnsResponse res = (DatagramDnsResponse) msg;
-                final int queryId = res.id();
+            final DatagramDnsResponse res = (DatagramDnsResponse) msg;
+            final int queryId = res.id();
 
-                if (logger.isDebugEnabled()) {
-                    logger.debug("{} RECEIVED: [{}: {}], {}", ch, queryId, res.sender(), res);
-                }
+            if (logger.isDebugEnabled()) {
+                logger.debug("{} RECEIVED: UDP [{}: {}], {}", ch, queryId, res.sender(), res);
+            }
 
-                final DnsQueryContext qCtx = queryContextManager.get(res.sender(), queryId);
-                if (qCtx == null) {
-                    logger.warn("{} Received a DNS response with an unknown ID: {}", ch, queryId);
-                    return;
-                }
+            final DnsQueryContext qCtx = queryContextManager.get(res.sender(), queryId);
+            if (qCtx == null) {
+                logger.warn("{} Received a DNS response with an unknown ID: {}", ch, queryId);
+                res.release();
+                return;
+            }
 
+            // Check if the response was truncated and if we can fallback to TCP to retry.
+            if (!res.isTruncated() || socketChannelFactory == null) {
                 qCtx.finish(res);
-            } finally {
-                ReferenceCountUtil.safeRelease(msg);
+                return;
             }
+
+            Bootstrap bs = new Bootstrap();
+            bs.option(ChannelOption.SO_REUSEADDR, true)
+            .group(executor())
+            .channelFactory(socketChannelFactory)
+            .handler(TCP_ENCODER);
+            bs.connect(res.sender()).addListener(new ChannelFutureListener() {
+                @Override
+                public void operationComplete(ChannelFuture future) {
+                    if (!future.isSuccess()) {
+                        if (logger.isDebugEnabled()) {
+                            logger.debug("{} Unable to fallback to TCP [{}]", queryId, future.cause());
+                        }
+
+                        // TCP fallback failed, just use the truncated response.
+                        qCtx.finish(res);
+                        return;
+                    }
+                    final Channel channel = future.channel();
+
+                    Promise<AddressedEnvelope<DnsResponse, InetSocketAddress>> promise =
+                            channel.eventLoop().newPromise();
+                    final TcpDnsQueryContext tcpCtx = new TcpDnsQueryContext(DnsNameResolver.this, channel,
+                            (InetSocketAddress) channel.remoteAddress(), qCtx.question(),
+                            EMPTY_ADDITIONALS, promise);
+
+                    channel.pipeline().addLast(new TcpDnsResponseDecoder());
+                    channel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
+                        @Override
+                        public void channelRead(ChannelHandlerContext ctx, Object msg) {
+                            Channel channel = ctx.channel();
+                            DnsResponse response = (DnsResponse) msg;
+                            int queryId = response.id();
+
+                            if (logger.isDebugEnabled()) {
+                                logger.debug("{} RECEIVED: TCP [{}: {}], {}", channel, queryId,
+                                        channel.remoteAddress(), response);
+                            }
+
+                            DnsQueryContext foundCtx = queryContextManager.get(res.sender(), queryId);
+                            if (foundCtx == tcpCtx) {
+                                tcpCtx.finish(new AddressedEnvelopeAdapter(
+                                        (InetSocketAddress) ctx.channel().remoteAddress(),
+                                        (InetSocketAddress) ctx.channel().localAddress(),
+                                        response));
+                            } else {
+                                response.release();
+                                tcpCtx.tryFailure("Received TCP response with unexpected ID", null, false);
+                                logger.warn("{} Received a DNS response with an unexpected ID: {}",
+                                        channel, queryId);
+                            }
+                        }
+
+                        @Override
+                        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+                            if (tcpCtx.tryFailure("TCP fallback error", cause, false) && logger.isDebugEnabled()) {
+                                logger.debug("{} Error during processing response: TCP [{}: {}]",
+                                        ctx.channel(), queryId,
+                                        ctx.channel().remoteAddress(), cause);
+                            }
+                        }
+                    });
+
+                    promise.addListener(
+                            new FutureListener<AddressedEnvelope<DnsResponse, InetSocketAddress>>() {
+                        @Override
+                        public void operationComplete(
+                                Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> future) {
+                            channel.close();
+
+                            if (future.isSuccess()) {
+                                qCtx.finish(future.getNow());
+                                res.release();
+                            } else {
+                                // TCP fallback failed, just use the truncated response.
+                                qCtx.finish(res);
+                            }
+                        }
+                    });
+                    tcpCtx.query(true, future.channel().newPromise());
+                }
+            });
         }
 
         @Override
@@ -1160,7 +1307,116 @@ public class DnsNameResolver extends InetNameResolver {
 
         @Override
         public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
-            logger.warn("{} Unexpected exception: ", ch, cause);
+            logger.warn("{} Unexpected exception: ", ctx.channel(), cause);
+        }
+    }
+
+    private static final class AddressedEnvelopeAdapter implements AddressedEnvelope<DnsResponse, InetSocketAddress> {
+        private final InetSocketAddress sender;
+        private final InetSocketAddress recipient;
+        private final DnsResponse response;
+
+        AddressedEnvelopeAdapter(InetSocketAddress sender, InetSocketAddress recipient, DnsResponse response) {
+            this.sender = sender;
+            this.recipient = recipient;
+            this.response = response;
+        }
+
+        @Override
+        public DnsResponse content() {
+            return response;
+        }
+
+        @Override
+        public InetSocketAddress sender() {
+            return sender;
+        }
+
+        @Override
+        public InetSocketAddress recipient() {
+            return recipient;
+        }
+
+        @Override
+        public AddressedEnvelope<DnsResponse, InetSocketAddress> retain() {
+            response.retain();
+            return this;
+        }
+
+        @Override
+        public AddressedEnvelope<DnsResponse, InetSocketAddress> retain(int increment) {
+            response.retain(increment);
+            return this;
+        }
+
+        @Override
+        public AddressedEnvelope<DnsResponse, InetSocketAddress> touch() {
+            response.touch();
+            return this;
+        }
+
+        @Override
+        public AddressedEnvelope<DnsResponse, InetSocketAddress> touch(Object hint) {
+            response.touch(hint);
+            return this;
+        }
+
+        @Override
+        public int refCnt() {
+            return response.refCnt();
+        }
+
+        @Override
+        public boolean release() {
+            return response.release();
+        }
+
+        @Override
+        public boolean release(int decrement) {
+            return response.release(decrement);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+
+            if (!(obj instanceof AddressedEnvelope)) {
+                return false;
+            }
+
+            @SuppressWarnings("unchecked")
+            final AddressedEnvelope<?, SocketAddress> that = (AddressedEnvelope<?, SocketAddress>) obj;
+            if (sender() == null) {
+                if (that.sender() != null) {
+                    return false;
+                }
+            } else if (!sender().equals(that.sender())) {
+                return false;
+            }
+
+            if (recipient() == null) {
+                if (that.recipient() != null) {
+                    return false;
+                }
+            } else if (!recipient().equals(that.recipient())) {
+                return false;
+            }
+
+            return response.equals(obj);
+        }
+
+        @Override
+        public int hashCode() {
+            int hashCode = response.hashCode();
+            if (sender() != null) {
+                hashCode = hashCode * 31 + sender().hashCode();
+            }
+            if (recipient() != null) {
+                hashCode = hashCode * 31 + recipient().hashCode();
+            }
+            return hashCode;
         }
     }
 }
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverBuilder.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverBuilder.java
index 06266cc..4d39612 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverBuilder.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverBuilder.java
@@ -20,9 +20,10 @@ import io.netty.channel.EventLoop;
 import io.netty.channel.ReflectiveChannelFactory;
 import io.netty.channel.socket.DatagramChannel;
 import io.netty.channel.socket.InternetProtocolFamily;
+import io.netty.channel.socket.SocketChannel;
 import io.netty.resolver.HostsFileEntriesResolver;
 import io.netty.resolver.ResolvedAddressTypes;
-import io.netty.util.internal.UnstableApi;
+import io.netty.util.concurrent.Future;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -35,10 +36,10 @@ import static io.netty.util.internal.ObjectUtil.intValue;
 /**
  * A {@link DnsNameResolver} builder.
  */
-@UnstableApi
 public final class DnsNameResolverBuilder {
     private EventLoop eventLoop;
     private ChannelFactory<? extends DatagramChannel> channelFactory;
+    private ChannelFactory<? extends SocketChannel> socketChannelFactory;
     private DnsCache resolveCache;
     private DnsCnameCache cnameCache;
     private AuthoritativeDnsServerCache authoritativeDnsServerCache;
@@ -47,6 +48,7 @@ public final class DnsNameResolverBuilder {
     private Integer negativeTtl;
     private long queryTimeoutMillis = 5000;
     private ResolvedAddressTypes resolvedAddressTypes = DnsNameResolver.DEFAULT_RESOLVE_ADDRESS_TYPES;
+    private boolean completeOncePreferredResolved;
     private boolean recursionDesired = true;
     private int maxQueriesPerResolve = 16;
     private boolean traceEnabled;
@@ -113,6 +115,35 @@ public final class DnsNameResolverBuilder {
         return channelFactory(new ReflectiveChannelFactory<DatagramChannel>(channelType));
     }
 
+    /**
+     * Sets the {@link ChannelFactory} that will create a {@link SocketChannel} for
+     * <a href="https://tools.ietf.org/html/rfc7766">TCP fallback</a> if needed.
+     *
+     * @param channelFactory the {@link ChannelFactory} or {@code null}
+     *                       if <a href="https://tools.ietf.org/html/rfc7766">TCP fallback</a> should not be supported.
+     * @return {@code this}
+     */
+    public DnsNameResolverBuilder socketChannelFactory(ChannelFactory<? extends SocketChannel> channelFactory) {
+        this.socketChannelFactory = channelFactory;
+        return this;
+    }
+
+    /**
+     * Sets the {@link ChannelFactory} as a {@link ReflectiveChannelFactory} of this type for
+     * <a href="https://tools.ietf.org/html/rfc7766">TCP fallback</a> if needed.
+     * Use as an alternative to {@link #socketChannelFactory(ChannelFactory)}.
+     *
+     * @param channelType the type or {@code null} if <a href="https://tools.ietf.org/html/rfc7766">TCP fallback</a>
+     *                    should not be supported.
+     * @return {@code this}
+     */
+    public DnsNameResolverBuilder socketChannelType(Class<? extends SocketChannel> channelType) {
+        if (channelType == null) {
+            return socketChannelFactory(null);
+        }
+        return socketChannelFactory(new ReflectiveChannelFactory<SocketChannel>(channelType));
+    }
+
     /**
      * Sets the cache for resolution results.
      *
@@ -253,6 +284,18 @@ public final class DnsNameResolverBuilder {
         return this;
     }
 
+    /**
+     * If {@code true} {@link DnsNameResolver#resolveAll(String)} will notify the returned {@link Future} as
+     * soon as all queries for the preferred address-type are complete.
+     *
+     * @param completeOncePreferredResolved {@code true} to enable, {@code false} to disable.
+     * @return {@code this}
+     */
+    public DnsNameResolverBuilder completeOncePreferredResolved(boolean completeOncePreferredResolved) {
+        this.completeOncePreferredResolved = completeOncePreferredResolved;
+        return this;
+    }
+
     /**
      * Sets if this resolver has to send a DNS query with the RD (recursion desired) flag set.
      *
@@ -430,6 +473,7 @@ public final class DnsNameResolverBuilder {
         return new DnsNameResolver(
                 eventLoop,
                 channelFactory,
+                socketChannelFactory,
                 resolveCache,
                 cnameCache,
                 authoritativeDnsServerCache,
@@ -445,7 +489,8 @@ public final class DnsNameResolverBuilder {
                 dnsServerAddressStreamProvider,
                 searchDomains,
                 ndots,
-                decodeIdn);
+                decodeIdn,
+                completeOncePreferredResolved);
     }
 
     /**
@@ -464,6 +509,10 @@ public final class DnsNameResolverBuilder {
             copiedBuilder.channelFactory(channelFactory);
         }
 
+        if (socketChannelFactory != null) {
+            copiedBuilder.socketChannelFactory(socketChannelFactory);
+        }
+
         if (resolveCache != null) {
             copiedBuilder.resolveCache(resolveCache);
         }
@@ -506,6 +555,7 @@ public final class DnsNameResolverBuilder {
 
         copiedBuilder.ndots(ndots);
         copiedBuilder.decodeIdn(decodeIdn);
+        copiedBuilder.completeOncePreferredResolved(completeOncePreferredResolved);
 
         return copiedBuilder;
     }
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverException.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverException.java
index 77e3474..5cbe283 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverException.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverException.java
@@ -18,14 +18,12 @@ package io.netty.resolver.dns;
 import io.netty.handler.codec.dns.DnsQuestion;
 import io.netty.util.internal.EmptyArrays;
 import io.netty.util.internal.ObjectUtil;
-import io.netty.util.internal.UnstableApi;
 
 import java.net.InetSocketAddress;
 
 /**
  * A {@link RuntimeException} raised when {@link DnsNameResolver} failed to perform a successful query.
  */
-@UnstableApi
 public class DnsNameResolverException extends RuntimeException {
 
     private static final long serialVersionUID = -8826717909627131850L;
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverTimeoutException.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverTimeoutException.java
index e1ad13d..7d3e721 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverTimeoutException.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverTimeoutException.java
@@ -16,7 +16,6 @@
 package io.netty.resolver.dns;
 
 import io.netty.handler.codec.dns.DnsQuestion;
-import io.netty.util.internal.UnstableApi;
 
 import java.net.InetSocketAddress;
 
@@ -24,7 +23,6 @@ import java.net.InetSocketAddress;
  * A {@link DnsNameResolverException} raised when {@link DnsNameResolver} failed to perform a successful query because
  * of an timeout. In this case you may want to retry the operation.
  */
-@UnstableApi
 public final class DnsNameResolverTimeoutException extends DnsNameResolverException {
     private static final long serialVersionUID = -8826717969627131854L;
 
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryContext.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryContext.java
index 08bbcb6..d476c1f 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryContext.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryContext.java
@@ -20,7 +20,6 @@ import io.netty.channel.Channel;
 import io.netty.channel.ChannelFuture;
 import io.netty.channel.ChannelFutureListener;
 import io.netty.channel.ChannelPromise;
-import io.netty.handler.codec.dns.DatagramDnsQuery;
 import io.netty.handler.codec.dns.AbstractDnsOptPseudoRrRecord;
 import io.netty.handler.codec.dns.DnsQuery;
 import io.netty.handler.codec.dns.DnsQuestion;
@@ -40,7 +39,7 @@ import java.util.concurrent.TimeUnit;
 
 import static io.netty.util.internal.ObjectUtil.checkNotNull;
 
-final class DnsQueryContext implements FutureListener<AddressedEnvelope<DnsResponse, InetSocketAddress>> {
+abstract class DnsQueryContext implements FutureListener<AddressedEnvelope<DnsResponse, InetSocketAddress>> {
 
     private static final InternalLogger logger = InternalLoggerFactory.getInstance(DnsQueryContext.class);
 
@@ -89,10 +88,18 @@ final class DnsQueryContext implements FutureListener<AddressedEnvelope<DnsRespo
         return question;
     }
 
+    DnsNameResolver parent() {
+        return parent;
+    }
+
+    protected abstract DnsQuery newQuery(int id);
+    protected abstract Channel channel();
+    protected abstract String protocol();
+
     void query(boolean flush, ChannelPromise writePromise) {
         final DnsQuestion question = question();
         final InetSocketAddress nameServerAddr = nameServerAddr();
-        final DatagramDnsQuery query = new DatagramDnsQuery(null, nameServerAddr, id);
+        final DnsQuery query = newQuery(id);
 
         query.setRecursionDesired(recursionDesired);
 
@@ -107,7 +114,7 @@ final class DnsQueryContext implements FutureListener<AddressedEnvelope<DnsRespo
         }
 
         if (logger.isDebugEnabled()) {
-            logger.debug("{} WRITE: [{}: {}], {}", parent.ch, id, nameServerAddr, question);
+            logger.debug("{} WRITE: {}, [{}: {}], {}", channel(), protocol(), id, nameServerAddr, question);
         }
 
         sendQuery(query, flush, writePromise);
@@ -136,8 +143,8 @@ final class DnsQueryContext implements FutureListener<AddressedEnvelope<DnsRespo
     }
 
     private void writeQuery(final DnsQuery query, final boolean flush, final ChannelPromise writePromise) {
-        final ChannelFuture writeFuture = flush ? parent.ch.writeAndFlush(query, writePromise) :
-                parent.ch.write(query, writePromise);
+        final ChannelFuture writeFuture = flush ? channel().writeAndFlush(query, writePromise) :
+                channel().write(query, writePromise);
         if (writeFuture.isDone()) {
             onQueryWriteCompletion(writeFuture);
         } else {
@@ -152,7 +159,7 @@ final class DnsQueryContext implements FutureListener<AddressedEnvelope<DnsRespo
 
     private void onQueryWriteCompletion(ChannelFuture writeFuture) {
         if (!writeFuture.isSuccess()) {
-            setFailure("failed to send a query", writeFuture.cause());
+            tryFailure("failed to send a query via " + protocol(), writeFuture.cause(), false);
             return;
         }
 
@@ -167,39 +174,37 @@ final class DnsQueryContext implements FutureListener<AddressedEnvelope<DnsRespo
                         return;
                     }
 
-                    setFailure("query timed out after " + queryTimeoutMillis + " milliseconds", null);
+                    tryFailure("query via " + protocol() + " timed out after " +
+                            queryTimeoutMillis + " milliseconds", null, true);
                 }
             }, queryTimeoutMillis, TimeUnit.MILLISECONDS);
         }
     }
 
+    /**
+     * Takes ownership of passed envelope
+     */
     void finish(AddressedEnvelope<? extends DnsResponse, InetSocketAddress> envelope) {
         final DnsResponse res = envelope.content();
         if (res.count(DnsSection.QUESTION) != 1) {
             logger.warn("Received a DNS response with invalid number of questions: {}", envelope);
-            return;
-        }
-
-        if (!question().equals(res.recordAt(DnsSection.QUESTION))) {
+        } else if (!question().equals(res.recordAt(DnsSection.QUESTION))) {
             logger.warn("Received a mismatching DNS response: {}", envelope);
-            return;
+        } else if (trySuccess(envelope)) {
+            return; // Ownership transferred, don't release
         }
-
-        setSuccess(envelope);
+        envelope.release();
     }
 
-    private void setSuccess(AddressedEnvelope<? extends DnsResponse, InetSocketAddress> envelope) {
-        Promise<AddressedEnvelope<DnsResponse, InetSocketAddress>> promise = this.promise;
-        @SuppressWarnings("unchecked")
-        AddressedEnvelope<DnsResponse, InetSocketAddress> castResponse =
-                (AddressedEnvelope<DnsResponse, InetSocketAddress>) envelope.retain();
-        if (!promise.trySuccess(castResponse)) {
-            // We failed to notify the promise as it was failed before, thus we need to release the envelope
-            envelope.release();
-        }
+    @SuppressWarnings("unchecked")
+    private boolean trySuccess(AddressedEnvelope<? extends DnsResponse, InetSocketAddress> envelope) {
+        return promise.trySuccess((AddressedEnvelope<DnsResponse, InetSocketAddress>) envelope);
     }
 
-    private void setFailure(String message, Throwable cause) {
+    boolean tryFailure(String message, Throwable cause, boolean timeout) {
+        if (promise.isDone()) {
+            return false;
+        }
         final InetSocketAddress nameServerAddr = nameServerAddr();
 
         final StringBuilder buf = new StringBuilder(message.length() + 64);
@@ -210,14 +215,14 @@ final class DnsQueryContext implements FutureListener<AddressedEnvelope<DnsRespo
            .append(" (no stack trace available)");
 
         final DnsNameResolverException e;
-        if (cause == null) {
+        if (timeout) {
             // This was caused by an timeout so use DnsNameResolverTimeoutException to allow the user to
             // handle it special (like retry the query).
             e = new DnsNameResolverTimeoutException(nameServerAddr, question(), buf.toString());
         } else {
             e = new DnsNameResolverException(nameServerAddr, question(), buf.toString(), cause);
         }
-        promise.tryFailure(e);
+        return promise.tryFailure(e);
     }
 
     @Override
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryLifecycleObserver.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryLifecycleObserver.java
index 39ce19d..3ad2ebc 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryLifecycleObserver.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryLifecycleObserver.java
@@ -19,7 +19,6 @@ import io.netty.channel.ChannelFuture;
 import io.netty.handler.codec.dns.DnsQuestion;
 import io.netty.handler.codec.dns.DnsRecordType;
 import io.netty.handler.codec.dns.DnsResponseCode;
-import io.netty.util.internal.UnstableApi;
 
 import java.net.InetSocketAddress;
 import java.util.List;
@@ -43,7 +42,6 @@ import java.util.List;
  * return an object of type {@link DnsQueryLifecycleObserver}. Implementations may use this to build a query tree to
  * understand the "sub queries" generated by a single query.
  */
-@UnstableApi
 public interface DnsQueryLifecycleObserver {
     /**
      * The query has been written.
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryLifecycleObserverFactory.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryLifecycleObserverFactory.java
index 6b5d822..afe36f9 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryLifecycleObserverFactory.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryLifecycleObserverFactory.java
@@ -16,12 +16,10 @@
 package io.netty.resolver.dns;
 
 import io.netty.handler.codec.dns.DnsQuestion;
-import io.netty.util.internal.UnstableApi;
 
 /**
  * Used to generate new instances of {@link DnsQueryLifecycleObserver}.
  */
-@UnstableApi
 public interface DnsQueryLifecycleObserverFactory {
     /**
      * Create a new instance of a {@link DnsQueryLifecycleObserver}. This will be called at the start of a new query.
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsRecordResolveContext.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsRecordResolveContext.java
index e633eb8..93814e2 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsRecordResolveContext.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsRecordResolveContext.java
@@ -58,6 +58,16 @@ final class DnsRecordResolveContext extends DnsResolveContext<DnsRecord> {
         return unfiltered;
     }
 
+    @Override
+    boolean isCompleteEarly(DnsRecord resolved) {
+        return false;
+    }
+
+    @Override
+    boolean isDuplicateAllowed() {
+        return true;
+    }
+
     @Override
     void cache(String hostname, DnsRecord[] additionals, DnsRecord result, DnsRecord convertedResult) {
         // Do not cache.
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsResolveContext.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsResolveContext.java
index f625be9..38fbafc 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsResolveContext.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsResolveContext.java
@@ -39,6 +39,7 @@ import io.netty.util.concurrent.Promise;
 import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.PlatformDependent;
 import io.netty.util.internal.StringUtil;
+import io.netty.util.internal.SuppressJava6Requirement;
 import io.netty.util.internal.ThrowableUtil;
 
 import java.net.InetAddress;
@@ -62,25 +63,16 @@ import static java.lang.Math.min;
 
 abstract class DnsResolveContext<T> {
 
-    private static final FutureListener<AddressedEnvelope<DnsResponse, InetSocketAddress>> RELEASE_RESPONSE =
-            new FutureListener<AddressedEnvelope<DnsResponse, InetSocketAddress>>() {
-                @Override
-                public void operationComplete(Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> future) {
-                    if (future.isSuccess()) {
-                        future.getNow().release();
-                    }
-                }
-            };
     private static final RuntimeException NXDOMAIN_QUERY_FAILED_EXCEPTION = ThrowableUtil.unknownStackTrace(
-            new RuntimeException("No answer found and NXDOMAIN response code returned"),
+            DnsResolveContextException.newStatic("No answer found and NXDOMAIN response code returned"),
             DnsResolveContext.class,
             "onResponse(..)");
     private static final RuntimeException CNAME_NOT_FOUND_QUERY_FAILED_EXCEPTION = ThrowableUtil.unknownStackTrace(
-            new RuntimeException("No matching CNAME record found"),
+            DnsResolveContextException.newStatic("No matching CNAME record found"),
             DnsResolveContext.class,
             "onResponseCNAME(..)");
     private static final RuntimeException NO_MATCHING_RECORD_QUERY_FAILED_EXCEPTION = ThrowableUtil.unknownStackTrace(
-            new RuntimeException("No matching record type found"),
+            DnsResolveContextException.newStatic("No matching record type found"),
             DnsResolveContext.class,
             "onResponseAorAAAA(..)");
     private static final RuntimeException UNRECOGNIZED_TYPE_QUERY_FAILED_EXCEPTION = ThrowableUtil.unknownStackTrace(
@@ -88,7 +80,7 @@ abstract class DnsResolveContext<T> {
             DnsResolveContext.class,
             "onResponse(..)");
     private static final RuntimeException NAME_SERVERS_EXHAUSTED_EXCEPTION = ThrowableUtil.unknownStackTrace(
-            new RuntimeException("No name servers returned an answer"),
+            DnsResolveContextException.newStatic("No name servers returned an answer"),
             DnsResolveContext.class,
             "tryToFinishResolve(..)");
 
@@ -107,6 +99,7 @@ abstract class DnsResolveContext<T> {
     private List<T> finalResult;
     private int allowedQueries;
     private boolean triedCNAME;
+    private boolean completeEarly;
 
     DnsResolveContext(DnsNameResolver parent,
                       String hostname, int dnsClass, DnsRecordType[] expectedTypes,
@@ -125,6 +118,27 @@ abstract class DnsResolveContext<T> {
         allowedQueries = maxAllowedQueries;
     }
 
+    static final class DnsResolveContextException extends RuntimeException {
+
+        private DnsResolveContextException(String message) {
+            super(message);
+        }
+
+        @SuppressJava6Requirement(reason = "uses Java 7+ Exception.<init>(String, Throwable, boolean, boolean)" +
+                " but is guarded by version checks")
+        private DnsResolveContextException(String message, boolean shared) {
+            super(message, null, false, true);
+            assert shared;
+        }
+
+        static DnsResolveContextException newStatic(String message) {
+            if (PlatformDependent.javaVersion() >= 7) {
+                return new DnsResolveContextException(message, true);
+            }
+            return new DnsResolveContextException(message);
+        }
+    }
+
     /**
      * The {@link DnsCache} to use while resolving.
      */
@@ -165,6 +179,14 @@ abstract class DnsResolveContext<T> {
      */
     abstract List<T> filterResults(List<T> unfiltered);
 
+    abstract boolean isCompleteEarly(T resolved);
+
+    /**
+     * Returns {@code true} if we should allow duplicates in the result or {@code false} if no duplicates should
+     * be included.
+     */
+    abstract boolean isDuplicateAllowed();
+
     /**
      * Caches a successful resolution.
      */
@@ -329,7 +351,8 @@ abstract class DnsResolveContext<T> {
                        final boolean flush,
                        final Promise<List<T>> promise,
                        final Throwable cause) {
-        if (nameServerAddrStreamIndex >= nameServerAddrStream.size() || allowedQueries == 0 || promise.isCancelled()) {
+        if (completeEarly || nameServerAddrStreamIndex >= nameServerAddrStream.size() ||
+                allowedQueries == 0 || promise.isCancelled()) {
             tryToFinishResolve(nameServerAddrStream, nameServerAddrStreamIndex, question, queryLifecycleObserver,
                                promise, cause);
             return;
@@ -456,7 +479,7 @@ abstract class DnsResolveContext<T> {
                 public boolean clear(String hostname) {
                     return authoritativeDnsServerCache.clear(hostname);
                 }
-            }).resolve(resolverPromise);
+            }, false).resolve(resolverPromise);
         }
     }
 
@@ -655,6 +678,7 @@ abstract class DnsResolveContext<T> {
         final int answerCount = response.count(DnsSection.ANSWER);
 
         boolean found = false;
+        boolean completeEarly = this.completeEarly;
         for (int i = 0; i < answerCount; i ++) {
             final DnsRecord r = response.recordAt(DnsSection.ANSWER, i);
             final DnsRecordType type = r.type();
@@ -695,19 +719,41 @@ abstract class DnsResolveContext<T> {
                 continue;
             }
 
+            boolean shouldRelease = false;
+            // Check if we did determine we wanted to complete early before. If this is the case we want to not
+            // include the result
+            if (!completeEarly) {
+                completeEarly = isCompleteEarly(converted);
+            }
+
+            // We want to ensure we do not have duplicates in finalResult as this may be unexpected.
+            //
+            // While using a LinkedHashSet or HashSet may sound like the perfect fit for this we will use an
+            // ArrayList here as duplicates should be found quite unfrequently in the wild and we dont want to pay
+            // for the extra memory copy and allocations in this cases later on.
             if (finalResult == null) {
                 finalResult = new ArrayList<T>(8);
+                finalResult.add(converted);
+            } else if (isDuplicateAllowed() || !finalResult.contains(converted)) {
+                finalResult.add(converted);
+            } else {
+                shouldRelease = true;
             }
-            finalResult.add(converted);
 
             cache(hostname, additionals, r, converted);
             found = true;
 
+            if (shouldRelease) {
+                ReferenceCountUtil.release(converted);
+            }
             // Note that we do not break from the loop here, so we decode/cache all A/AAAA records.
         }
 
         if (cnames.isEmpty()) {
             if (found) {
+                if (completeEarly) {
+                    this.completeEarly = true;
+                }
                 queryLifecycleObserver.querySucceed();
                 return;
             }
@@ -793,7 +839,7 @@ abstract class DnsResolveContext<T> {
                                     final Throwable cause) {
 
         // There are no queries left to try.
-        if (!queriesInProgress.isEmpty()) {
+        if (!completeEarly && !queriesInProgress.isEmpty()) {
             queryLifecycleObserver.queryCancelled(allowedQueries);
 
             // There are still some queries in process, we will try to notify once the next one finishes until
@@ -839,22 +885,24 @@ abstract class DnsResolveContext<T> {
     }
 
     private void finishResolve(Promise<List<T>> promise, Throwable cause) {
-        if (!queriesInProgress.isEmpty()) {
+        // If completeEarly was true we still want to continue processing the queries to ensure we still put everything
+        // in the cache eventually.
+        if (!completeEarly && !queriesInProgress.isEmpty()) {
             // If there are queries in progress, we should cancel it because we already finished the resolution.
             for (Iterator<Future<AddressedEnvelope<DnsResponse, InetSocketAddress>>> i = queriesInProgress.iterator();
                  i.hasNext();) {
                 Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> f = i.next();
                 i.remove();
 
-                if (!f.cancel(false)) {
-                    f.addListener(RELEASE_RESPONSE);
-                }
+                f.cancel(false);
             }
         }
 
         if (finalResult != null) {
-            // Found at least one resolved record.
-            DnsNameResolver.trySuccess(promise, filterResults(finalResult));
+            if (!promise.isDone()) {
+                // Found at least one resolved record.
+                DnsNameResolver.trySuccess(promise, filterResults(finalResult));
+            }
             return;
         }
 
@@ -1225,7 +1273,7 @@ abstract class DnsResolveContext<T> {
         void update(InetSocketAddress address, long ttl) {
             assert this.address == null || this.address.isUnresolved();
             this.address = address;
-            this.ttl = min(ttl, ttl);
+            this.ttl = min(this.ttl, ttl);
         }
 
         void update(InetSocketAddress address) {
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStream.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStream.java
index af014f9..c5d0453 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStream.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStream.java
@@ -16,14 +16,11 @@
 
 package io.netty.resolver.dns;
 
-import io.netty.util.internal.UnstableApi;
-
 import java.net.InetSocketAddress;
 
 /**
  * An infinite stream of DNS server addresses.
  */
-@UnstableApi
 public interface DnsServerAddressStream {
     /**
      * Retrieves the next DNS server address from the stream.
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStreamProvider.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStreamProvider.java
index 05285e9..e0384ea 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStreamProvider.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStreamProvider.java
@@ -15,8 +15,6 @@
  */
 package io.netty.resolver.dns;
 
-import io.netty.util.internal.UnstableApi;
-
 /**
  * Provides an opportunity to override which {@link DnsServerAddressStream} is used to resolve a specific hostname.
  * <p>
@@ -24,7 +22,6 @@ import io.netty.util.internal.UnstableApi;
  * <a href="https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man5/resolver.5.html">
  * /etc/resolver</a>.
  */
-@UnstableApi
 public interface DnsServerAddressStreamProvider {
     /**
      * Ask this provider for the name servers to query for {@code hostname}.
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStreamProviders.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStreamProviders.java
index fbcd8cc..8df2492 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStreamProviders.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStreamProviders.java
@@ -16,47 +16,64 @@
 package io.netty.resolver.dns;
 
 import io.netty.util.internal.PlatformDependent;
-import io.netty.util.internal.UnstableApi;
+import io.netty.util.internal.logging.InternalLogger;
+import io.netty.util.internal.logging.InternalLoggerFactory;
 
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicLong;
 
 /**
  * Utility methods related to {@link DnsServerAddressStreamProvider}.
  */
-@UnstableApi
 public final class DnsServerAddressStreamProviders {
-    // We use 5 minutes which is the same as what OpenJDK is using in sun.net.dns.ResolverConfigurationImpl.
-    private static final long REFRESH_INTERVAL = TimeUnit.MINUTES.toNanos(5);
 
-    // TODO(scott): how is this done on Windows? This may require a JNI call to GetNetworkParams
-    // https://msdn.microsoft.com/en-us/library/aa365968(VS.85).aspx.
-    private static final DnsServerAddressStreamProvider DEFAULT_DNS_SERVER_ADDRESS_STREAM_PROVIDER =
-            new DnsServerAddressStreamProvider() {
-        private volatile DnsServerAddressStreamProvider currentProvider = provider();
-        private final AtomicLong lastRefresh = new AtomicLong(System.nanoTime());
+    private static final InternalLogger LOGGER =
+            InternalLoggerFactory.getInstance(DnsServerAddressStreamProviders.class);
+    private static final Constructor<? extends DnsServerAddressStreamProvider> STREAM_PROVIDER_CONSTRUCTOR;
 
-        @Override
-        public DnsServerAddressStream nameServerAddressStream(String hostname) {
-            long last = lastRefresh.get();
-            DnsServerAddressStreamProvider current = currentProvider;
-            if (System.nanoTime() - last > REFRESH_INTERVAL) {
-                // This is slightly racy which means it will be possible still use the old configuration for a small
-                // amount of time, but that's ok.
-                if (lastRefresh.compareAndSet(last, System.nanoTime())) {
-                    current = currentProvider = provider();
+    static {
+        Constructor<? extends DnsServerAddressStreamProvider> constructor = null;
+        if (PlatformDependent.isOsx()) {
+            try {
+                // As MacOSDnsServerAddressStreamProvider is contained in another jar which depends on this jar
+                // we use reflection to use it if its on the classpath.
+                Object maybeProvider = AccessController.doPrivileged(new PrivilegedAction<Object>() {
+                    @Override
+                    public Object run() {
+                        try {
+                            return Class.forName(
+                                    "io.netty.resolver.dns.macos.MacOSDnsServerAddressStreamProvider",
+                                    true,
+                                    DnsServerAddressStreamProviders.class.getClassLoader());
+                        } catch (Throwable cause) {
+                            return cause;
+                        }
+                    }
+                });
+                if (maybeProvider instanceof Class) {
+                    @SuppressWarnings("unchecked")
+                    Class<? extends DnsServerAddressStreamProvider> providerClass =
+                            (Class<? extends DnsServerAddressStreamProvider>) maybeProvider;
+                    Method method = providerClass.getMethod("ensureAvailability");
+                    method.invoke(null);
+                    constructor = providerClass.getConstructor();
+                    constructor.newInstance();
+                } else if (!(maybeProvider instanceof ClassNotFoundException)) {
+                    throw (Throwable) maybeProvider;
                 }
+            } catch (Throwable cause) {
+                LOGGER.debug(
+                        "Unable to use MacOSDnsServerAddressStreamProvider, fallback to system defaults", cause);
+                constructor = null;
             }
-            return current.nameServerAddressStream(hostname);
         }
-
-        private DnsServerAddressStreamProvider provider() {
-            // If on windows just use the DefaultDnsServerAddressStreamProvider.INSTANCE as otherwise
-            // we will log some error which may be confusing.
-            return PlatformDependent.isWindows() ? DefaultDnsServerAddressStreamProvider.INSTANCE :
-                    UnixResolverDnsServerAddressStreamProvider.parseSilently();
-        }
-    };
+        STREAM_PROVIDER_CONSTRUCTOR = constructor;
+    }
 
     private DnsServerAddressStreamProviders() {
     }
@@ -69,6 +86,57 @@ public final class DnsServerAddressStreamProviders {
      * configuration.
      */
     public static DnsServerAddressStreamProvider platformDefault() {
-        return DEFAULT_DNS_SERVER_ADDRESS_STREAM_PROVIDER;
+        if (STREAM_PROVIDER_CONSTRUCTOR != null) {
+            try {
+                return STREAM_PROVIDER_CONSTRUCTOR.newInstance();
+            } catch (IllegalAccessException e) {
+                // ignore
+            } catch (InstantiationException e) {
+                // ignore
+            } catch (InvocationTargetException e) {
+                // ignore
+            }
+        }
+        return unixDefault();
+    }
+
+    public static DnsServerAddressStreamProvider unixDefault() {
+        return DefaultProviderHolder.DEFAULT_DNS_SERVER_ADDRESS_STREAM_PROVIDER;
+    }
+
+    // We use a Holder class to only initialize DEFAULT_DNS_SERVER_ADDRESS_STREAM_PROVIDER if we really
+    // need it.
+    private static final class DefaultProviderHolder {
+        // We use 5 minutes which is the same as what OpenJDK is using in sun.net.dns.ResolverConfigurationImpl.
+        private static final long REFRESH_INTERVAL = TimeUnit.MINUTES.toNanos(5);
+
+        // TODO(scott): how is this done on Windows? This may require a JNI call to GetNetworkParams
+        // https://msdn.microsoft.com/en-us/library/aa365968(VS.85).aspx.
+        static final DnsServerAddressStreamProvider DEFAULT_DNS_SERVER_ADDRESS_STREAM_PROVIDER =
+                new DnsServerAddressStreamProvider() {
+                    private volatile DnsServerAddressStreamProvider currentProvider = provider();
+                    private final AtomicLong lastRefresh = new AtomicLong(System.nanoTime());
+
+                    @Override
+                    public DnsServerAddressStream nameServerAddressStream(String hostname) {
+                        long last = lastRefresh.get();
+                        DnsServerAddressStreamProvider current = currentProvider;
+                        if (System.nanoTime() - last > REFRESH_INTERVAL) {
+                            // This is slightly racy which means it will be possible still use the old configuration
+                            // for a small amount of time, but that's ok.
+                            if (lastRefresh.compareAndSet(last, System.nanoTime())) {
+                                current = currentProvider = provider();
+                            }
+                        }
+                        return current.nameServerAddressStream(hostname);
+                    }
+
+                    private DnsServerAddressStreamProvider provider() {
+                        // If on windows just use the DefaultDnsServerAddressStreamProvider.INSTANCE as otherwise
+                        // we will log some error which may be confusing.
+                        return PlatformDependent.isWindows() ? DefaultDnsServerAddressStreamProvider.INSTANCE :
+                                UnixResolverDnsServerAddressStreamProvider.parseSilently();
+                    }
+                };
     }
 }
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddresses.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddresses.java
index 72c0acd..12c34ce 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddresses.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddresses.java
@@ -16,7 +16,7 @@
 
 package io.netty.resolver.dns;
 
-import io.netty.util.internal.UnstableApi;
+import io.netty.util.internal.ObjectUtil;
 
 import java.net.InetSocketAddress;
 import java.util.ArrayList;
@@ -26,7 +26,6 @@ import java.util.List;
 /**
  * Provides an infinite sequence of DNS server addresses to {@link DnsNameResolver}.
  */
-@UnstableApi
 @SuppressWarnings("IteratorNextCanNotThrowNoSuchElementException")
 public abstract class DnsServerAddresses {
     /**
@@ -149,9 +148,7 @@ public abstract class DnsServerAddresses {
      * Returns the {@link DnsServerAddresses} that yields only a single {@code address}.
      */
     public static DnsServerAddresses singleton(final InetSocketAddress address) {
-        if (address == null) {
-            throw new NullPointerException("address");
-        }
+        ObjectUtil.checkNotNull(address, "address");
         if (address.isUnresolved()) {
             throw new IllegalArgumentException("cannot use an unresolved DNS server address: " + address);
         }
@@ -160,9 +157,7 @@ public abstract class DnsServerAddresses {
     }
 
     private static List<InetSocketAddress> sanitize(Iterable<? extends InetSocketAddress> addresses) {
-        if (addresses == null) {
-            throw new NullPointerException("addresses");
-        }
+        ObjectUtil.checkNotNull(addresses, "addresses");
 
         final List<InetSocketAddress> list;
         if (addresses instanceof Collection) {
@@ -189,9 +184,7 @@ public abstract class DnsServerAddresses {
     }
 
     private static List<InetSocketAddress> sanitize(InetSocketAddress[] addresses) {
-        if (addresses == null) {
-            throw new NullPointerException("addresses");
-        }
+        ObjectUtil.checkNotNull(addresses, "addresses");
 
         List<InetSocketAddress> list = new ArrayList<InetSocketAddress>(addresses.length);
         for (InetSocketAddress a: addresses) {
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/MultiDnsServerAddressStreamProvider.java b/resolver-dns/src/main/java/io/netty/resolver/dns/MultiDnsServerAddressStreamProvider.java
index 87e1aaa..915e70e 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/MultiDnsServerAddressStreamProvider.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/MultiDnsServerAddressStreamProvider.java
@@ -15,15 +15,12 @@
  */
 package io.netty.resolver.dns;
 
-import io.netty.util.internal.UnstableApi;
-
 import java.util.List;
 
 /**
  * A {@link DnsServerAddressStreamProvider} which iterates through a collection of
  * {@link DnsServerAddressStreamProvider} until the first non-{@code null} result is found.
  */
-@UnstableApi
 public final class MultiDnsServerAddressStreamProvider implements DnsServerAddressStreamProvider {
     private final DnsServerAddressStreamProvider[] providers;
 
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/NoopAuthoritativeDnsServerCache.java b/resolver-dns/src/main/java/io/netty/resolver/dns/NoopAuthoritativeDnsServerCache.java
index 0cdce77..dcc1172 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/NoopAuthoritativeDnsServerCache.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/NoopAuthoritativeDnsServerCache.java
@@ -16,16 +16,12 @@
 package io.netty.resolver.dns;
 
 import io.netty.channel.EventLoop;
-import io.netty.util.internal.UnstableApi;
 
 import java.net.InetSocketAddress;
-import java.util.Collections;
-import java.util.List;
 
 /**
  * A noop {@link AuthoritativeDnsServerCache} that actually never caches anything.
  */
-@UnstableApi
 public final class NoopAuthoritativeDnsServerCache implements AuthoritativeDnsServerCache {
     public static final NoopAuthoritativeDnsServerCache INSTANCE = new NoopAuthoritativeDnsServerCache();
 
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsCache.java b/resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsCache.java
index ec36c84..9d008a2 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsCache.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsCache.java
@@ -17,7 +17,6 @@ package io.netty.resolver.dns;
 
 import io.netty.channel.EventLoop;
 import io.netty.handler.codec.dns.DnsRecord;
-import io.netty.util.internal.UnstableApi;
 
 import java.net.InetAddress;
 import java.util.Collections;
@@ -26,7 +25,6 @@ import java.util.List;
 /**
  * A noop DNS cache that actually never caches anything.
  */
-@UnstableApi
 public final class NoopDnsCache implements DnsCache {
 
     public static final NoopDnsCache INSTANCE = new NoopDnsCache();
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsCnameCache.java b/resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsCnameCache.java
index 54113c4..a869228 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsCnameCache.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsCnameCache.java
@@ -16,9 +16,7 @@
 package io.netty.resolver.dns;
 
 import io.netty.channel.EventLoop;
-import io.netty.util.internal.UnstableApi;
 
-@UnstableApi
 public final class NoopDnsCnameCache implements DnsCnameCache {
 
     public static final NoopDnsCnameCache INSTANCE = new NoopDnsCnameCache();
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsQueryLifecycleObserverFactory.java b/resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsQueryLifecycleObserverFactory.java
index d53e9cd..2dbcfa5 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsQueryLifecycleObserverFactory.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsQueryLifecycleObserverFactory.java
@@ -16,9 +16,7 @@
 package io.netty.resolver.dns;
 
 import io.netty.handler.codec.dns.DnsQuestion;
-import io.netty.util.internal.UnstableApi;
 
-@UnstableApi
 public final class NoopDnsQueryLifecycleObserverFactory implements DnsQueryLifecycleObserverFactory {
     public static final NoopDnsQueryLifecycleObserverFactory INSTANCE = new NoopDnsQueryLifecycleObserverFactory();
 
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/PreferredAddressTypeComparator.java b/resolver-dns/src/main/java/io/netty/resolver/dns/PreferredAddressTypeComparator.java
new file mode 100644
index 0000000..bcf4dc7
--- /dev/null
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/PreferredAddressTypeComparator.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2019 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.resolver.dns;
+
+import io.netty.channel.socket.InternetProtocolFamily;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.util.Comparator;
+
+final class PreferredAddressTypeComparator implements Comparator<InetAddress> {
+
+    private static final PreferredAddressTypeComparator IPv4 = new PreferredAddressTypeComparator(Inet4Address.class);
+    private static final PreferredAddressTypeComparator IPv6 = new PreferredAddressTypeComparator(Inet6Address.class);
+
+    static PreferredAddressTypeComparator comparator(InternetProtocolFamily family) {
+        switch (family) {
+            case IPv4:
+                return IPv4;
+            case IPv6:
+                return IPv6;
+            default:
+                throw new IllegalArgumentException();
+        }
+    }
+
+    private final Class<? extends InetAddress> preferredAddressType;
+
+    private PreferredAddressTypeComparator(Class<? extends InetAddress> preferredAddressType) {
+        this.preferredAddressType = preferredAddressType;
+    }
+
+    @Override
+    public int compare(InetAddress o1, InetAddress o2) {
+        if (o1.getClass() == o2.getClass()) {
+            return 0;
+        }
+        return preferredAddressType.isAssignableFrom(o1.getClass()) ? -1 : 1;
+    }
+}
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/RoundRobinDnsAddressResolverGroup.java b/resolver-dns/src/main/java/io/netty/resolver/dns/RoundRobinDnsAddressResolverGroup.java
index 68c14fe..e725fc8 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/RoundRobinDnsAddressResolverGroup.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/RoundRobinDnsAddressResolverGroup.java
@@ -23,7 +23,6 @@ import io.netty.resolver.AddressResolver;
 import io.netty.resolver.AddressResolverGroup;
 import io.netty.resolver.NameResolver;
 import io.netty.resolver.RoundRobinInetAddressResolver;
-import io.netty.util.internal.UnstableApi;
 
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
@@ -33,7 +32,6 @@ import java.net.InetSocketAddress;
  * multiple are provided by the nameserver. This is ideal for use in applications that use a pool of connections, for
  * which connecting to a single resolved address would be inefficient.
  */
-@UnstableApi
 public class RoundRobinDnsAddressResolverGroup extends DnsAddressResolverGroup {
 
     public RoundRobinDnsAddressResolverGroup(DnsNameResolverBuilder dnsResolverBuilder) {
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/SequentialDnsServerAddressStreamProvider.java b/resolver-dns/src/main/java/io/netty/resolver/dns/SequentialDnsServerAddressStreamProvider.java
index a23aeba..24f4ebb 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/SequentialDnsServerAddressStreamProvider.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/SequentialDnsServerAddressStreamProvider.java
@@ -15,8 +15,6 @@
  */
 package io.netty.resolver.dns;
 
-import io.netty.util.internal.UnstableApi;
-
 import java.net.InetSocketAddress;
 
 import static io.netty.resolver.dns.DnsServerAddresses.sequential;
@@ -24,7 +22,6 @@ import static io.netty.resolver.dns.DnsServerAddresses.sequential;
 /**
  * A {@link DnsServerAddressStreamProvider} which is backed by a sequential list of DNS servers.
  */
-@UnstableApi
 public final class SequentialDnsServerAddressStreamProvider extends UniSequentialDnsServerAddressStreamProvider {
     /**
      * Create a new instance.
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/ShuffledDnsServerAddressStream.java b/resolver-dns/src/main/java/io/netty/resolver/dns/ShuffledDnsServerAddressStream.java
index 9b56f23..9f9b9cf 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/ShuffledDnsServerAddressStream.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/ShuffledDnsServerAddressStream.java
@@ -21,7 +21,6 @@ import io.netty.util.internal.PlatformDependent;
 import java.net.InetSocketAddress;
 import java.util.Collections;
 import java.util.List;
-import java.util.Random;
 
 final class ShuffledDnsServerAddressStream implements DnsServerAddressStream {
 
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/SingletonDnsServerAddressStreamProvider.java b/resolver-dns/src/main/java/io/netty/resolver/dns/SingletonDnsServerAddressStreamProvider.java
index cfd5f8b..1b7ccff 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/SingletonDnsServerAddressStreamProvider.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/SingletonDnsServerAddressStreamProvider.java
@@ -15,14 +15,11 @@
  */
 package io.netty.resolver.dns;
 
-import io.netty.util.internal.UnstableApi;
-
 import java.net.InetSocketAddress;
 
 /**
  * A {@link DnsServerAddressStreamProvider} which always uses a single DNS server for resolution.
  */
-@UnstableApi
 public final class SingletonDnsServerAddressStreamProvider extends UniSequentialDnsServerAddressStreamProvider {
     /**
      * Create a new instance.
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/TcpDnsQueryContext.java b/resolver-dns/src/main/java/io/netty/resolver/dns/TcpDnsQueryContext.java
new file mode 100644
index 0000000..65a1d05
--- /dev/null
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/TcpDnsQueryContext.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2019 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.resolver.dns;
+
+import io.netty.channel.AddressedEnvelope;
+import io.netty.channel.Channel;
+import io.netty.handler.codec.dns.DefaultDnsQuery;
+import io.netty.handler.codec.dns.DnsQuery;
+import io.netty.handler.codec.dns.DnsQuestion;
+import io.netty.handler.codec.dns.DnsRecord;
+import io.netty.handler.codec.dns.DnsResponse;
+import io.netty.util.concurrent.Promise;
+
+import java.net.InetSocketAddress;
+
+final class TcpDnsQueryContext extends DnsQueryContext {
+
+    private final Channel channel;
+
+    TcpDnsQueryContext(DnsNameResolver parent, Channel channel, InetSocketAddress nameServerAddr, DnsQuestion question,
+                       DnsRecord[] additionals, Promise<AddressedEnvelope<DnsResponse, InetSocketAddress>> promise) {
+        super(parent, nameServerAddr, question, additionals, promise);
+        this.channel = channel;
+    }
+
+    @Override
+    protected DnsQuery newQuery(int id) {
+        return new DefaultDnsQuery(id);
+    }
+
+    @Override
+    protected Channel channel() {
+        return channel;
+    }
+
+    @Override
+    protected String protocol() {
+        return "TCP";
+    }
+}
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/UnixResolverDnsServerAddressStreamProvider.java b/resolver-dns/src/main/java/io/netty/resolver/dns/UnixResolverDnsServerAddressStreamProvider.java
index afe2931..feac4b9 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/UnixResolverDnsServerAddressStreamProvider.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/UnixResolverDnsServerAddressStreamProvider.java
@@ -17,7 +17,6 @@ package io.netty.resolver.dns;
 
 import io.netty.util.NetUtil;
 import io.netty.util.internal.SocketUtils;
-import io.netty.util.internal.UnstableApi;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 
@@ -37,13 +36,13 @@ import java.util.regex.Pattern;
 import static io.netty.resolver.dns.DefaultDnsServerAddressStreamProvider.DNS_PORT;
 import static io.netty.util.internal.ObjectUtil.checkNotNull;
 import static io.netty.util.internal.StringUtil.indexOfNonWhiteSpace;
+import static io.netty.util.internal.StringUtil.indexOfWhiteSpace;
 
 /**
  * Able to parse files such as <a href="https://linux.die.net/man/5/resolver">/etc/resolv.conf</a> and
  * <a href="https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man5/resolver.5.html">
  * /etc/resolver</a> to respect the system default domain servers.
  */
-@UnstableApi
 public final class UnixResolverDnsServerAddressStreamProvider implements DnsServerAddressStreamProvider {
     private static final InternalLogger logger =
             InternalLoggerFactory.getInstance(UnixResolverDnsServerAddressStreamProvider.class);
@@ -72,7 +71,9 @@ public final class UnixResolverDnsServerAddressStreamProvider implements DnsServ
             return nameServerCache.mayOverrideNameServers() ? nameServerCache
                                                             : DefaultDnsServerAddressStreamProvider.INSTANCE;
         } catch (Exception e) {
-            logger.debug("failed to parse {} and/or {}", ETC_RESOLV_CONF_FILE, ETC_RESOLVER_DIR, e);
+            if (logger.isDebugEnabled()) {
+                logger.debug("failed to parse {} and/or {}", ETC_RESOLV_CONF_FILE, ETC_RESOLVER_DIR, e);
+            }
             return DefaultDnsServerAddressStreamProvider.INSTANCE;
         }
     }
@@ -167,48 +168,65 @@ public final class UnixResolverDnsServerAddressStreamProvider implements DnsServ
                 String line;
                 while ((line = br.readLine()) != null) {
                     line = line.trim();
-                    char c;
-                    if (line.isEmpty() || (c = line.charAt(0)) == '#' || c == ';') {
-                        continue;
-                    }
-                    if (line.startsWith(NAMESERVER_ROW_LABEL)) {
-                        int i = indexOfNonWhiteSpace(line, NAMESERVER_ROW_LABEL.length());
-                        if (i < 0) {
-                            throw new IllegalArgumentException("error parsing label " + NAMESERVER_ROW_LABEL +
-                                    " in file " + etcResolverFile + ". value: " + line);
+                    try {
+                        char c;
+                        if (line.isEmpty() || (c = line.charAt(0)) == '#' || c == ';') {
+                            continue;
                         }
-                        String maybeIP = line.substring(i);
-                        // There may be a port appended onto the IP address so we attempt to extract it.
-                        if (!NetUtil.isValidIpV4Address(maybeIP) && !NetUtil.isValidIpV6Address(maybeIP)) {
-                            i = maybeIP.lastIndexOf('.');
-                            if (i + 1 >= maybeIP.length()) {
+                        if (line.startsWith(NAMESERVER_ROW_LABEL)) {
+                            int i = indexOfNonWhiteSpace(line, NAMESERVER_ROW_LABEL.length());
+                            if (i < 0) {
                                 throw new IllegalArgumentException("error parsing label " + NAMESERVER_ROW_LABEL +
-                                        " in file " + etcResolverFile + ". invalid IP value: " + line);
+                                        " in file " + etcResolverFile + ". value: " + line);
                             }
-                            port = Integer.parseInt(maybeIP.substring(i + 1));
-                            maybeIP = maybeIP.substring(0, i);
-                        }
-                        addresses.add(SocketUtils.socketAddress(maybeIP, port));
-                    } else if (line.startsWith(DOMAIN_ROW_LABEL)) {
-                        int i = indexOfNonWhiteSpace(line, DOMAIN_ROW_LABEL.length());
-                        if (i < 0) {
-                            throw new IllegalArgumentException("error parsing label " + DOMAIN_ROW_LABEL +
-                                    " in file " + etcResolverFile + " value: " + line);
-                        }
-                        domainName = line.substring(i);
-                        if (!addresses.isEmpty()) {
-                            putIfAbsent(domainToNameServerStreamMap, domainName, addresses);
-                        }
-                        addresses = new ArrayList<InetSocketAddress>(2);
-                    } else if (line.startsWith(PORT_ROW_LABEL)) {
-                        int i = indexOfNonWhiteSpace(line, PORT_ROW_LABEL.length());
-                        if (i < 0) {
-                            throw new IllegalArgumentException("error parsing label " + PORT_ROW_LABEL +
-                                    " in file " + etcResolverFile + " value: " + line);
+                            String maybeIP;
+                            int x = indexOfWhiteSpace(line, i);
+                            if (x == -1) {
+                                maybeIP = line.substring(i);
+                            } else {
+                                // ignore comments
+                                int idx = indexOfNonWhiteSpace(line, x);
+                                if (idx == -1 || line.charAt(idx) != '#') {
+                                    throw new IllegalArgumentException("error parsing label " + NAMESERVER_ROW_LABEL +
+                                            " in file " + etcResolverFile + ". value: " + line);
+                                }
+                                maybeIP = line.substring(i, x);
+                            }
+
+                            // There may be a port appended onto the IP address so we attempt to extract it.
+                            if (!NetUtil.isValidIpV4Address(maybeIP) && !NetUtil.isValidIpV6Address(maybeIP)) {
+                                i = maybeIP.lastIndexOf('.');
+                                if (i + 1 >= maybeIP.length()) {
+                                    throw new IllegalArgumentException("error parsing label " + NAMESERVER_ROW_LABEL +
+                                            " in file " + etcResolverFile + ". invalid IP value: " + line);
+                                }
+                                port = Integer.parseInt(maybeIP.substring(i + 1));
+                                maybeIP = maybeIP.substring(0, i);
+                            }
+                            addresses.add(SocketUtils.socketAddress(maybeIP, port));
+                        } else if (line.startsWith(DOMAIN_ROW_LABEL)) {
+                            int i = indexOfNonWhiteSpace(line, DOMAIN_ROW_LABEL.length());
+                            if (i < 0) {
+                                throw new IllegalArgumentException("error parsing label " + DOMAIN_ROW_LABEL +
+                                        " in file " + etcResolverFile + " value: " + line);
+                            }
+                            domainName = line.substring(i);
+                            if (!addresses.isEmpty()) {
+                                putIfAbsent(domainToNameServerStreamMap, domainName, addresses);
+                            }
+                            addresses = new ArrayList<InetSocketAddress>(2);
+                        } else if (line.startsWith(PORT_ROW_LABEL)) {
+                            int i = indexOfNonWhiteSpace(line, PORT_ROW_LABEL.length());
+                            if (i < 0) {
+                                throw new IllegalArgumentException("error parsing label " + PORT_ROW_LABEL +
+                                        " in file " + etcResolverFile + " value: " + line);
+                            }
+                            port = Integer.parseInt(line.substring(i));
+                        } else if (line.startsWith(SORTLIST_ROW_LABEL)) {
+                            logger.info("row type {} not supported. Ignoring line: {}", SORTLIST_ROW_LABEL, line);
                         }
-                        port = Integer.parseInt(line.substring(i));
-                    } else if (line.startsWith(SORTLIST_ROW_LABEL)) {
-                        logger.info("row type {} not supported. ignoring line: {}", SORTLIST_ROW_LABEL, line);
+                    } catch (IllegalArgumentException e) {
+                        logger.warn("Could not parse entry. Ignoring line: {}", line, e);
                     }
                 }
                 if (!addresses.isEmpty()) {
@@ -238,8 +256,10 @@ public final class UnixResolverDnsServerAddressStreamProvider implements DnsServ
         DnsServerAddresses existingAddresses = domainToNameServerStreamMap.put(domainName, addresses);
         if (existingAddresses != null) {
             domainToNameServerStreamMap.put(domainName, existingAddresses);
-            logger.debug("Domain name {} already maps to addresses {} so new addresses {} will be discarded",
-                    domainName, existingAddresses, addresses);
+            if (logger.isDebugEnabled()) {
+                logger.debug("Domain name {} already maps to addresses {} so new addresses {} will be discarded",
+                        domainName, existingAddresses, addresses);
+            }
         }
     }
 
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/package-info.java b/resolver-dns/src/main/java/io/netty/resolver/dns/package-info.java
index 3edc178..63825ba 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/package-info.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/package-info.java
@@ -18,7 +18,4 @@
  * An alternative to Java's built-in domain name lookup mechanism that resolves a domain name asynchronously,
  * which supports the queries of an arbitrary DNS record type as well.
  */
-@UnstableApi
 package io.netty.resolver.dns;
-
-import io.netty.util.internal.UnstableApi;
diff --git a/resolver-dns/src/test/java/io/netty/resolver/dns/DefaultAuthoritativeDnsServerCacheTest.java b/resolver-dns/src/test/java/io/netty/resolver/dns/DefaultAuthoritativeDnsServerCacheTest.java
index ada0eae..4b4d23c 100644
--- a/resolver-dns/src/test/java/io/netty/resolver/dns/DefaultAuthoritativeDnsServerCacheTest.java
+++ b/resolver-dns/src/test/java/io/netty/resolver/dns/DefaultAuthoritativeDnsServerCacheTest.java
@@ -25,7 +25,6 @@ import org.junit.Test;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.util.Comparator;
-import java.util.List;
 import java.util.concurrent.Callable;
 import java.util.concurrent.TimeUnit;
 
diff --git a/resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverTest.java b/resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverTest.java
index d03ee87..3e35e28 100644
--- a/resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverTest.java
+++ b/resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverTest.java
@@ -20,13 +20,17 @@ import io.netty.buffer.ByteBufHolder;
 import io.netty.channel.AddressedEnvelope;
 import io.netty.channel.ChannelFactory;
 import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
 import io.netty.channel.EventLoop;
 import io.netty.channel.EventLoopGroup;
 import io.netty.channel.ReflectiveChannelFactory;
 import io.netty.channel.nio.NioEventLoopGroup;
 import io.netty.channel.socket.DatagramChannel;
+import io.netty.channel.socket.DatagramPacket;
 import io.netty.channel.socket.InternetProtocolFamily;
 import io.netty.channel.socket.nio.NioDatagramChannel;
+import io.netty.channel.socket.nio.NioSocketChannel;
 import io.netty.handler.codec.dns.DefaultDnsQuestion;
 import io.netty.handler.codec.dns.DnsQuestion;
 import io.netty.handler.codec.dns.DnsRawRecord;
@@ -37,6 +41,7 @@ import io.netty.handler.codec.dns.DnsResponseCode;
 import io.netty.handler.codec.dns.DnsSection;
 import io.netty.resolver.HostsFileEntriesResolver;
 import io.netty.resolver.ResolvedAddressTypes;
+import io.netty.util.CharsetUtil;
 import io.netty.util.NetUtil;
 import io.netty.util.ReferenceCountUtil;
 import io.netty.util.concurrent.Future;
@@ -46,7 +51,9 @@ import io.netty.util.internal.StringUtil;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 import org.apache.directory.server.dns.DnsException;
+import org.apache.directory.server.dns.io.encoder.DnsMessageEncoder;
 import org.apache.directory.server.dns.messages.DnsMessage;
+import org.apache.directory.server.dns.messages.DnsMessageModifier;
 import org.apache.directory.server.dns.messages.QuestionRecord;
 import org.apache.directory.server.dns.messages.RecordClass;
 import org.apache.directory.server.dns.messages.RecordType;
@@ -55,6 +62,7 @@ import org.apache.directory.server.dns.messages.ResourceRecordModifier;
 import org.apache.directory.server.dns.messages.ResponseCode;
 import org.apache.directory.server.dns.store.DnsAttribute;
 import org.apache.directory.server.dns.store.RecordStore;
+import org.apache.mina.core.buffer.IoBuffer;
 import org.hamcrest.Matchers;
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
@@ -63,10 +71,15 @@ import org.junit.Test;
 import org.junit.rules.ExpectedException;
 
 import java.io.IOException;
+import java.io.InputStream;
 import java.net.DatagramSocket;
+import java.net.Inet4Address;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
 import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -96,7 +109,6 @@ import static io.netty.handler.codec.dns.DnsRecordType.AAAA;
 import static io.netty.handler.codec.dns.DnsRecordType.CNAME;
 import static io.netty.resolver.dns.DnsServerAddresses.sequential;
 import static java.util.Collections.singletonList;
-import static java.util.Collections.singletonMap;
 import static org.hamcrest.Matchers.greaterThan;
 import static org.hamcrest.Matchers.hasSize;
 import static org.hamcrest.Matchers.instanceOf;
@@ -1232,6 +1244,15 @@ public class DnsNameResolverTest {
             } else {
                 assertEquals(ipv6Address, resolved.getHostAddress());
             }
+            InetAddress ipv4InetAddress = InetAddress.getByAddress("netty.com",
+                    InetAddress.getByName(ipv4Address).getAddress());
+            InetAddress ipv6InetAddress = InetAddress.getByAddress("netty.com",
+                    InetAddress.getByName(ipv6Address).getAddress());
+
+            List<InetAddress> resolvedAll = resolver.resolveAll("netty.com").syncUninterruptibly().getNow();
+            List<InetAddress> expected = types == ResolvedAddressTypes.IPV4_PREFERRED ?
+                    Arrays.asList(ipv4InetAddress, ipv6InetAddress) :  Arrays.asList(ipv6InetAddress, ipv4InetAddress);
+            assertEquals(expected, resolvedAll);
         } finally {
             nonCompliantDnsServer.stop();
         }
@@ -2467,4 +2488,422 @@ public class DnsNameResolverTest {
                 true // decodeIdn
         ).close();
     }
+
+    @Test
+    public void testQueryTxt() throws Exception {
+        final String hostname = "txt.netty.io";
+        final String txt1 = "some text";
+        final String txt2 = "some more text";
+
+        TestDnsServer server = new TestDnsServer(new RecordStore() {
+
+            @Override
+            public Set<ResourceRecord> getRecords(QuestionRecord question) {
+                if (question.getDomainName().equals(hostname)) {
+                    Map<String, Object> map1 = new HashMap<String, Object>();
+                    map1.put(DnsAttribute.CHARACTER_STRING.toLowerCase(), txt1);
+
+                    Map<String, Object> map2 = new HashMap<String, Object>();
+                    map2.put(DnsAttribute.CHARACTER_STRING.toLowerCase(), txt2);
+
+                    Set<ResourceRecord> records = new HashSet<ResourceRecord>();
+                    records.add(new TestDnsServer.TestResourceRecord(question.getDomainName(), RecordType.TXT, map1));
+                    records.add(new TestDnsServer.TestResourceRecord(question.getDomainName(), RecordType.TXT, map2));
+                    return records;
+                }
+                return Collections.emptySet();
+            }
+        });
+        server.start();
+        DnsNameResolver resolver = newResolver(ResolvedAddressTypes.IPV4_ONLY)
+                .nameServerProvider(new SingletonDnsServerAddressStreamProvider(server.localAddress()))
+                .build();
+        try {
+            AddressedEnvelope<DnsResponse, InetSocketAddress> envelope = resolver.query(
+                    new DefaultDnsQuestion(hostname, DnsRecordType.TXT)).syncUninterruptibly().getNow();
+            assertNotNull(envelope.sender());
+
+            DnsResponse response = envelope.content();
+            assertNotNull(response);
+
+            assertEquals(DnsResponseCode.NOERROR, response.code());
+            int count = response.count(DnsSection.ANSWER);
+
+            assertEquals(2, count);
+            List<String> txts = new ArrayList<String>();
+
+            for (int i = 0; i < 2; i++) {
+                txts.addAll(decodeTxt(response.recordAt(DnsSection.ANSWER, i)));
+            }
+            assertTrue(txts.contains(txt1));
+            assertTrue(txts.contains(txt2));
+            envelope.release();
+        } finally {
+            resolver.close();
+            server.stop();
+        }
+    }
+
+    private static List<String> decodeTxt(DnsRecord record) {
+        if (!(record instanceof DnsRawRecord)) {
+            return Collections.emptyList();
+        }
+        List<String> list = new ArrayList<String>();
+        ByteBuf data = ((DnsRawRecord) record).content();
+        int idx = data.readerIndex();
+        int wIdx = data.writerIndex();
+        while (idx < wIdx) {
+            int len = data.getUnsignedByte(idx++);
+            list.add(data.toString(idx, len, CharsetUtil.UTF_8));
+            idx += len;
+        }
+        return list;
+    }
+
+    @Test
+    public void testNotIncludeDuplicates() throws IOException {
+        final String name = "netty.io";
+        final String ipv4Addr = "1.2.3.4";
+        TestDnsServer dnsServer2 = new TestDnsServer(new RecordStore() {
+            @Override
+            public Set<ResourceRecord> getRecords(QuestionRecord question) {
+                Set<ResourceRecord> records = new LinkedHashSet<ResourceRecord>(4);
+                String qName = question.getDomainName().toLowerCase();
+                if (qName.equals(name)) {
+                    records.add(new TestDnsServer.TestResourceRecord(
+                            qName, RecordType.CNAME,
+                            Collections.<String, Object>singletonMap(
+                                    DnsAttribute.DOMAIN_NAME.toLowerCase(), "cname.netty.io")));
+                    records.add(new TestDnsServer.TestResourceRecord(qName,
+                            RecordType.A, Collections.<String, Object>singletonMap(
+                            DnsAttribute.IP_ADDRESS.toLowerCase(), ipv4Addr)));
+                } else {
+                    records.add(new TestDnsServer.TestResourceRecord(qName,
+                            RecordType.A, Collections.<String, Object>singletonMap(
+                            DnsAttribute.IP_ADDRESS.toLowerCase(), ipv4Addr)));
+                }
+                return records;
+            }
+        });
+        dnsServer2.start();
+        DnsNameResolver resolver = null;
+        try {
+            DnsNameResolverBuilder builder = newResolver()
+                    .recursionDesired(true)
+                    .maxQueriesPerResolve(16)
+                    .nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer2.localAddress()));
+            builder.resolvedAddressTypes(ResolvedAddressTypes.IPV4_ONLY);
+
+            resolver = builder.build();
+            List<InetAddress> resolvedAddresses = resolver.resolveAll(name).syncUninterruptibly().getNow();
+            assertEquals(Collections.singletonList(InetAddress.getByAddress(name, new byte[] { 1, 2, 3, 4 })),
+                    resolvedAddresses);
+        } finally {
+            dnsServer2.stop();
+            if (resolver != null) {
+                resolver.close();
+            }
+        }
+    }
+
+    @Test
+    public void testIncludeDuplicates() throws IOException {
+        final String name = "netty.io";
+        final String ipv4Addr = "1.2.3.4";
+        TestDnsServer dnsServer2 = new TestDnsServer(new RecordStore() {
+            @Override
+            public Set<ResourceRecord> getRecords(QuestionRecord question) {
+                Set<ResourceRecord> records = new LinkedHashSet<ResourceRecord>(2);
+                String qName = question.getDomainName().toLowerCase();
+                records.add(new TestDnsServer.TestResourceRecord(qName,
+                        RecordType.A, Collections.<String, Object>singletonMap(
+                        DnsAttribute.IP_ADDRESS.toLowerCase(), ipv4Addr)));
+                records.add(new TestDnsServer.TestResourceRecord(qName,
+                        RecordType.A, Collections.<String, Object>singletonMap(
+                        DnsAttribute.IP_ADDRESS.toLowerCase(), ipv4Addr)));
+                return records;
+            }
+        });
+        dnsServer2.start();
+        DnsNameResolver resolver = null;
+        try {
+            DnsNameResolverBuilder builder = newResolver()
+                    .recursionDesired(true)
+                    .maxQueriesPerResolve(16)
+                    .nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer2.localAddress()));
+            builder.resolvedAddressTypes(ResolvedAddressTypes.IPV4_ONLY);
+
+            resolver = builder.build();
+            List<DnsRecord> resolvedAddresses = resolver.resolveAll(new DefaultDnsQuestion(name, A))
+                    .syncUninterruptibly().getNow();
+            assertEquals(2, resolvedAddresses.size());
+            for (DnsRecord record: resolvedAddresses) {
+                ReferenceCountUtil.release(record);
+            }
+        } finally {
+            dnsServer2.stop();
+            if (resolver != null) {
+                resolver.close();
+            }
+        }
+    }
+
+    @Test
+    public void testDropAAAA() throws IOException {
+        String host = "somehost.netty.io";
+        TestDnsServer dnsServer2 = new TestDnsServer(Collections.singleton(host));
+        dnsServer2.start(true);
+        DnsNameResolver resolver = null;
+        try {
+            DnsNameResolverBuilder builder = newResolver()
+                    .recursionDesired(false)
+                    .queryTimeoutMillis(500)
+                    .resolvedAddressTypes(ResolvedAddressTypes.IPV4_PREFERRED)
+                    .maxQueriesPerResolve(16)
+                    .nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer2.localAddress()));
+
+            resolver = builder.build();
+            List<InetAddress> addressList = resolver.resolveAll(host).syncUninterruptibly().getNow();
+            assertEquals(1, addressList.size());
+            assertEquals(host, addressList.get(0).getHostName());
+        } finally {
+            dnsServer2.stop();
+            if (resolver != null) {
+                resolver.close();
+            }
+        }
+    }
+
+    @Test(timeout = 2000)
+    public void testDropAAAAResolveFast() throws IOException {
+        String host = "somehost.netty.io";
+        TestDnsServer dnsServer2 = new TestDnsServer(Collections.singleton(host));
+        dnsServer2.start(true);
+        DnsNameResolver resolver = null;
+        try {
+            DnsNameResolverBuilder builder = newResolver()
+                    .recursionDesired(false)
+                    .queryTimeoutMillis(10000)
+                    .resolvedAddressTypes(ResolvedAddressTypes.IPV4_PREFERRED)
+                    .maxQueriesPerResolve(16)
+                    .nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer2.localAddress()));
+
+            resolver = builder.build();
+            InetAddress address = resolver.resolve(host).syncUninterruptibly().getNow();
+            assertEquals(host, address.getHostName());
+        } finally {
+            dnsServer2.stop();
+            if (resolver != null) {
+                resolver.close();
+            }
+        }
+    }
+
+    @Test(timeout = 2000)
+    public void testDropAAAAResolveAllFast() throws IOException {
+        final String host = "somehost.netty.io";
+        TestDnsServer dnsServer2 = new TestDnsServer(new RecordStore() {
+            @Override
+            public Set<ResourceRecord> getRecords(QuestionRecord question) throws DnsException {
+                String name = question.getDomainName();
+                if (name.equals(host)) {
+                    Set<ResourceRecord> records = new HashSet<ResourceRecord>(2);
+                    records.add(new TestDnsServer.TestResourceRecord(name, RecordType.A,
+                            Collections.<String, Object>singletonMap(DnsAttribute.IP_ADDRESS.toLowerCase(),
+                                    "10.0.0.1")));
+                    records.add(new TestDnsServer.TestResourceRecord(name, RecordType.A,
+                            Collections.<String, Object>singletonMap(DnsAttribute.IP_ADDRESS.toLowerCase(),
+                                    "10.0.0.2")));
+                    return records;
+                }
+                return null;
+            }
+        });
+        dnsServer2.start(true);
+        DnsNameResolver resolver = null;
+        try {
+            DnsNameResolverBuilder builder = newResolver()
+                    .recursionDesired(false)
+                    .queryTimeoutMillis(10000)
+                    .resolvedAddressTypes(ResolvedAddressTypes.IPV4_PREFERRED)
+                    .completeOncePreferredResolved(true)
+                    .maxQueriesPerResolve(16)
+                    .nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer2.localAddress()));
+
+            resolver = builder.build();
+            List<InetAddress> addresses = resolver.resolveAll(host).syncUninterruptibly().getNow();
+            assertEquals(2, addresses.size());
+            for (InetAddress address: addresses) {
+                assertThat(address, instanceOf(Inet4Address.class));
+                assertEquals(host, address.getHostName());
+            }
+        } finally {
+            dnsServer2.stop();
+            if (resolver != null) {
+                resolver.close();
+            }
+        }
+    }
+
+    @Test(timeout = 5000)
+    public void testTruncatedWithoutTcpFallback() throws IOException {
+        testTruncated0(false, false);
+    }
+
+    @Test(timeout = 5000)
+    public void testTruncatedWithTcpFallback() throws IOException {
+        testTruncated0(true, false);
+    }
+
+    @Test(timeout = 5000)
+    public void testTruncatedWithTcpFallbackBecauseOfMtu() throws IOException {
+        testTruncated0(true, true);
+    }
+
+    private static DnsMessageModifier modifierFrom(DnsMessage message) {
+        DnsMessageModifier modifier = new DnsMessageModifier();
+        modifier.setAcceptNonAuthenticatedData(message.isAcceptNonAuthenticatedData());
+        modifier.setAdditionalRecords(message.getAdditionalRecords());
+        modifier.setAnswerRecords(message.getAnswerRecords());
+        modifier.setAuthoritativeAnswer(message.isAuthoritativeAnswer());
+        modifier.setAuthorityRecords(message.getAuthorityRecords());
+        modifier.setMessageType(message.getMessageType());
+        modifier.setOpCode(message.getOpCode());
+        modifier.setQuestionRecords(message.getQuestionRecords());
+        modifier.setRecursionAvailable(message.isRecursionAvailable());
+        modifier.setRecursionDesired(message.isRecursionDesired());
+        modifier.setReserved(message.isReserved());
+        modifier.setResponseCode(message.getResponseCode());
+        modifier.setTransactionId(message.getTransactionId());
+        modifier.setTruncated(message.isTruncated());
+        return modifier;
+    }
+
+    private static void testTruncated0(boolean tcpFallback, final boolean truncatedBecauseOfMtu) throws IOException {
+        final String host = "somehost.netty.io";
+        final String txt = "this is a txt record";
+        final AtomicReference<DnsMessage> messageRef = new AtomicReference<DnsMessage>();
+
+        TestDnsServer dnsServer2 = new TestDnsServer(new RecordStore() {
+            @Override
+            public Set<ResourceRecord> getRecords(QuestionRecord question) {
+                String name = question.getDomainName();
+                if (name.equals(host)) {
+                    return Collections.<ResourceRecord>singleton(
+                            new TestDnsServer.TestResourceRecord(name, RecordType.TXT,
+                                    Collections.<String, Object>singletonMap(
+                                            DnsAttribute.CHARACTER_STRING.toLowerCase(), txt)));
+                }
+                return null;
+            }
+        }) {
+            @Override
+            protected DnsMessage filterMessage(DnsMessage message) {
+                // Store a original message so we can replay it later on.
+                messageRef.set(message);
+
+                if (!truncatedBecauseOfMtu) {
+                    // Create a copy of the message but set the truncated flag.
+                    DnsMessageModifier modifier = modifierFrom(message);
+                    modifier.setTruncated(true);
+                    return modifier.getDnsMessage();
+                }
+                return message;
+            }
+        };
+        dnsServer2.start();
+        DnsNameResolver resolver = null;
+        ServerSocket serverSocket = null;
+        try {
+            DnsNameResolverBuilder builder = newResolver()
+                    .queryTimeoutMillis(10000)
+                    .resolvedAddressTypes(ResolvedAddressTypes.IPV4_PREFERRED)
+                    .maxQueriesPerResolve(16)
+                    .nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer2.localAddress()));
+
+            if (tcpFallback) {
+                // If we are configured to use TCP as a fallback also bind a TCP socket
+                serverSocket = new ServerSocket(dnsServer2.localAddress().getPort());
+                serverSocket.setReuseAddress(true);
+
+                builder.socketChannelType(NioSocketChannel.class);
+            }
+            resolver = builder.build();
+            if (truncatedBecauseOfMtu) {
+                resolver.ch.pipeline().addFirst(new ChannelInboundHandlerAdapter() {
+                    @Override
+                    public void channelRead(ChannelHandlerContext ctx, Object msg) {
+                        if (msg instanceof DatagramPacket) {
+                            // Truncate the packet by 1 byte.
+                            DatagramPacket packet = (DatagramPacket) msg;
+                            packet.content().writerIndex(packet.content().writerIndex() - 1);
+                        }
+                        ctx.fireChannelRead(msg);
+                    }
+                });
+            }
+            Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> envelopeFuture = resolver.query(
+                    new DefaultDnsQuestion(host, DnsRecordType.TXT));
+
+            if (tcpFallback) {
+                // If we are configured to use TCP as a fallback lets replay the dns message over TCP
+                Socket socket = serverSocket.accept();
+
+                InputStream in = socket.getInputStream();
+                assertTrue((in.read() << 8 | (in.read() & 0xff)) > 2); // skip length field
+                int txnId = in.read() << 8 | (in.read() & 0xff);
+
+                IoBuffer ioBuffer = IoBuffer.allocate(1024);
+                // Must replace the transactionId with the one from the TCP request
+                DnsMessageModifier modifier = modifierFrom(messageRef.get());
+                modifier.setTransactionId(txnId);
+                new DnsMessageEncoder().encode(ioBuffer, modifier.getDnsMessage());
+                ioBuffer.flip();
+
+                ByteBuffer lenBuffer = ByteBuffer.allocate(2);
+                lenBuffer.putShort((short) ioBuffer.remaining());
+                lenBuffer.flip();
+
+                while (lenBuffer.hasRemaining()) {
+                    socket.getOutputStream().write(lenBuffer.get());
+                }
+
+                while (ioBuffer.hasRemaining()) {
+                    socket.getOutputStream().write(ioBuffer.get());
+                }
+                socket.getOutputStream().flush();
+                // Let's wait until we received the envelope before closing the socket.
+                envelopeFuture.syncUninterruptibly();
+
+                socket.close();
+                serverSocket.close();
+            }
+
+            AddressedEnvelope<DnsResponse, InetSocketAddress> envelope = envelopeFuture.syncUninterruptibly().getNow();
+            assertNotNull(envelope.sender());
+
+            DnsResponse response = envelope.content();
+            assertNotNull(response);
+
+            assertEquals(DnsResponseCode.NOERROR, response.code());
+            int count = response.count(DnsSection.ANSWER);
+
+            assertEquals(1, count);
+            List<String> texts = decodeTxt(response.recordAt(DnsSection.ANSWER, 0));
+            assertEquals(1, texts.size());
+            assertEquals(txt, texts.get(0));
+
+            if (tcpFallback) {
+                assertFalse(envelope.content().isTruncated());
+            } else {
+                assertTrue(envelope.content().isTruncated());
+            }
+            assertTrue(envelope.release());
+        } finally {
+            dnsServer2.stop();
+            if (resolver != null) {
+                resolver.close();
+            }
+        }
+    }
 }
diff --git a/resolver-dns/src/test/java/io/netty/resolver/dns/DnsServerAddressStreamProvidersTest.java b/resolver-dns/src/test/java/io/netty/resolver/dns/DnsServerAddressStreamProvidersTest.java
new file mode 100644
index 0000000..57be3ad
--- /dev/null
+++ b/resolver-dns/src/test/java/io/netty/resolver/dns/DnsServerAddressStreamProvidersTest.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2020 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.resolver.dns;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class DnsServerAddressStreamProvidersTest {
+
+    @Test
+    public void testUseCorrectProvider() {
+        Assert.assertSame(DnsServerAddressStreamProviders.unixDefault(),
+                DnsServerAddressStreamProviders.platformDefault());
+    }
+}
diff --git a/resolver-dns/src/test/java/io/netty/resolver/dns/PreferredAddressTypeComparatorTest.java b/resolver-dns/src/test/java/io/netty/resolver/dns/PreferredAddressTypeComparatorTest.java
new file mode 100644
index 0000000..7dcaba8
--- /dev/null
+++ b/resolver-dns/src/test/java/io/netty/resolver/dns/PreferredAddressTypeComparatorTest.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2019 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.resolver.dns;
+
+import io.netty.channel.socket.InternetProtocolFamily;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+public class PreferredAddressTypeComparatorTest {
+
+    @Test
+    public void testIpv4() throws UnknownHostException {
+        InetAddress ipv4Address1 = InetAddress.getByName("10.0.0.1");
+        InetAddress ipv4Address2 = InetAddress.getByName("10.0.0.2");
+        InetAddress ipv4Address3 = InetAddress.getByName("10.0.0.3");
+        InetAddress ipv6Address1 = InetAddress.getByName("::1");
+        InetAddress ipv6Address2 = InetAddress.getByName("::2");
+        InetAddress ipv6Address3 = InetAddress.getByName("::3");
+
+        PreferredAddressTypeComparator ipv4 = PreferredAddressTypeComparator.comparator(InternetProtocolFamily.IPv4);
+
+        List<InetAddress> addressList = new ArrayList<InetAddress>();
+        Collections.addAll(addressList, ipv4Address1, ipv4Address2, ipv6Address1,
+                ipv6Address2, ipv4Address3, ipv6Address3);
+        Collections.sort(addressList, ipv4);
+
+        Assert.assertEquals(Arrays.asList(ipv4Address1, ipv4Address2, ipv4Address3, ipv6Address1,
+                ipv6Address2, ipv6Address3), addressList);
+    }
+
+    @Test
+    public void testIpv6() throws UnknownHostException {
+        InetAddress ipv4Address1 = InetAddress.getByName("10.0.0.1");
+        InetAddress ipv4Address2 = InetAddress.getByName("10.0.0.2");
+        InetAddress ipv4Address3 = InetAddress.getByName("10.0.0.3");
+        InetAddress ipv6Address1 = InetAddress.getByName("::1");
+        InetAddress ipv6Address2 = InetAddress.getByName("::2");
+        InetAddress ipv6Address3 = InetAddress.getByName("::3");
+
+        PreferredAddressTypeComparator ipv4 = PreferredAddressTypeComparator.comparator(InternetProtocolFamily.IPv6);
+
+        List<InetAddress> addressList = new ArrayList<InetAddress>();
+        Collections.addAll(addressList, ipv4Address1, ipv4Address2, ipv6Address1,
+                ipv6Address2, ipv4Address3, ipv6Address3);
+        Collections.sort(addressList, ipv4);
+
+        Assert.assertEquals(Arrays.asList(ipv6Address1,
+                ipv6Address2, ipv6Address3, ipv4Address1, ipv4Address2, ipv4Address3), addressList);
+    }
+}
diff --git a/resolver-dns/src/test/java/io/netty/resolver/dns/TestDnsServer.java b/resolver-dns/src/test/java/io/netty/resolver/dns/TestDnsServer.java
index 229ea98..2bca6bd 100644
--- a/resolver-dns/src/test/java/io/netty/resolver/dns/TestDnsServer.java
+++ b/resolver-dns/src/test/java/io/netty/resolver/dns/TestDnsServer.java
@@ -18,6 +18,7 @@ package io.netty.resolver.dns;
 import io.netty.util.NetUtil;
 import io.netty.util.internal.PlatformDependent;
 import org.apache.directory.server.dns.DnsServer;
+import org.apache.directory.server.dns.io.decoder.DnsMessageDecoder;
 import org.apache.directory.server.dns.io.encoder.DnsMessageEncoder;
 import org.apache.directory.server.dns.io.encoder.ResourceRecordEncoder;
 import org.apache.directory.server.dns.messages.DnsMessage;
@@ -28,7 +29,6 @@ import org.apache.directory.server.dns.messages.ResourceRecord;
 import org.apache.directory.server.dns.messages.ResourceRecordImpl;
 import org.apache.directory.server.dns.messages.ResourceRecordModifier;
 import org.apache.directory.server.dns.protocol.DnsProtocolHandler;
-import org.apache.directory.server.dns.protocol.DnsUdpDecoder;
 import org.apache.directory.server.dns.protocol.DnsUdpEncoder;
 import org.apache.directory.server.dns.store.DnsAttribute;
 import org.apache.directory.server.dns.store.RecordStore;
@@ -38,6 +38,8 @@ import org.apache.mina.core.session.IoSession;
 import org.apache.mina.filter.codec.ProtocolCodecFactory;
 import org.apache.mina.filter.codec.ProtocolCodecFilter;
 import org.apache.mina.filter.codec.ProtocolDecoder;
+import org.apache.mina.filter.codec.ProtocolDecoderAdapter;
+import org.apache.mina.filter.codec.ProtocolDecoderOutput;
 import org.apache.mina.filter.codec.ProtocolEncoder;
 import org.apache.mina.filter.codec.ProtocolEncoderOutput;
 import org.apache.mina.transport.socket.DatagramAcceptor;
@@ -83,6 +85,13 @@ class TestDnsServer extends DnsServer {
 
     @Override
     public void start() throws IOException {
+        start(false);
+    }
+
+    /**
+     * Start the {@link TestDnsServer} but drop all {@code AAAA} queries and not send any response to these at all.
+     */
+    public void start(final boolean dropAAAAQueries) throws IOException {
         InetSocketAddress address = new InetSocketAddress(NetUtil.LOCALHOST4, 0);
         UdpTransport transport = new UdpTransport(address.getHostName(), address.getPort());
         setTransports(transport);
@@ -94,7 +103,8 @@ class TestDnsServer extends DnsServer {
             public void sessionCreated(IoSession session) {
                 // USe our own codec to support AAAA testing
                 session.getFilterChain()
-                    .addFirst("codec", new ProtocolCodecFilter(new TestDnsProtocolUdpCodecFactory()));
+                        .addFirst("codec", new ProtocolCodecFilter(
+                                new TestDnsProtocolUdpCodecFactory(dropAAAAQueries)));
             }
         });
 
@@ -142,6 +152,11 @@ class TestDnsServer extends DnsServer {
     private final class TestDnsProtocolUdpCodecFactory implements ProtocolCodecFactory {
         private final DnsMessageEncoder encoder = new DnsMessageEncoder();
         private final TestAAAARecordEncoder recordEncoder = new TestAAAARecordEncoder();
+        private final boolean dropAAAArecords;
+
+        TestDnsProtocolUdpCodecFactory(boolean dropAAAArecords) {
+            this.dropAAAArecords = dropAAAArecords;
+        }
 
         @Override
         public ProtocolEncoder getEncoder(IoSession session) {
@@ -175,7 +190,22 @@ class TestDnsServer extends DnsServer {
 
         @Override
         public ProtocolDecoder getDecoder(IoSession session) {
-            return new DnsUdpDecoder();
+            return new ProtocolDecoderAdapter() {
+                private DnsMessageDecoder decoder = new DnsMessageDecoder();
+
+                @Override
+                public void decode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws IOException {
+                    DnsMessage message = decoder.decode(in);
+                    if (dropAAAArecords) {
+                        for (QuestionRecord record: message.getQuestionRecords()) {
+                            if (record.getRecordType() == RecordType.AAAA) {
+                                return;
+                            }
+                        }
+                    }
+                    out.write(message);
+                }
+            };
         }
 
         private final class TestAAAARecordEncoder extends ResourceRecordEncoder {
diff --git a/resolver-dns/src/test/java/io/netty/resolver/dns/UnixResolverDnsServerAddressStreamProviderTest.java b/resolver-dns/src/test/java/io/netty/resolver/dns/UnixResolverDnsServerAddressStreamProviderTest.java
index 3399667..6bb133a 100644
--- a/resolver-dns/src/test/java/io/netty/resolver/dns/UnixResolverDnsServerAddressStreamProviderTest.java
+++ b/resolver-dns/src/test/java/io/netty/resolver/dns/UnixResolverDnsServerAddressStreamProviderTest.java
@@ -164,6 +164,19 @@ public class UnixResolverDnsServerAddressStreamProviderTest {
         assertEquals(Collections.singletonList("squarecorp.local"), domains);
     }
 
+    @Test
+    public void ignoreInvalidEntries() throws Exception {
+        File f = buildFile("domain netty.local\n" +
+                "nameserver nil\n" +
+                "nameserver 127.0.0.3\n");
+        UnixResolverDnsServerAddressStreamProvider p =
+                new UnixResolverDnsServerAddressStreamProvider(f, null);
+
+        DnsServerAddressStream stream = p.nameServerAddressStream("somehost");
+        assertEquals(1, stream.size());
+        assertHostNameEquals("127.0.0.3", stream.next());
+    }
+
     private File buildFile(String contents) throws IOException {
         File f = folder.newFile();
         OutputStream out = new FileOutputStream(f);
@@ -175,6 +188,17 @@ public class UnixResolverDnsServerAddressStreamProviderTest {
         return f;
     }
 
+    @Test
+    public void ignoreComments() throws Exception {
+        File f = buildFile("domain linecorp.local\n" +
+                "nameserver 127.0.0.2 #somecomment\n");
+        UnixResolverDnsServerAddressStreamProvider p =
+                new UnixResolverDnsServerAddressStreamProvider(f, null);
+
+        DnsServerAddressStream stream = p.nameServerAddressStream("somehost");
+        assertHostNameEquals("127.0.0.2", stream.next());
+    }
+
     private static void assertHostNameEquals(String expectedHostname, InetSocketAddress next) {
         assertEquals("unexpected hostname: " + next, expectedHostname, next.getHostString());
     }
diff --git a/resolver/pom.xml b/resolver/pom.xml
index aaa891a..18d4e6b 100644
--- a/resolver/pom.xml
+++ b/resolver/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>io.netty</groupId>
     <artifactId>netty-parent</artifactId>
-    <version>4.1.33.Final</version>
+    <version>4.1.48.Final</version>
   </parent>
 
   <artifactId>netty-resolver</artifactId>
diff --git a/resolver/src/main/java/io/netty/resolver/AbstractAddressResolver.java b/resolver/src/main/java/io/netty/resolver/AbstractAddressResolver.java
index fa034d0..b9fea2e 100644
--- a/resolver/src/main/java/io/netty/resolver/AbstractAddressResolver.java
+++ b/resolver/src/main/java/io/netty/resolver/AbstractAddressResolver.java
@@ -20,7 +20,6 @@ import io.netty.util.concurrent.EventExecutor;
 import io.netty.util.concurrent.Future;
 import io.netty.util.concurrent.Promise;
 import io.netty.util.internal.TypeParameterMatcher;
-import io.netty.util.internal.UnstableApi;
 
 import java.net.SocketAddress;
 import java.nio.channels.UnsupportedAddressTypeException;
@@ -32,7 +31,6 @@ import static io.netty.util.internal.ObjectUtil.checkNotNull;
 /**
  * A skeletal {@link AddressResolver} implementation.
  */
-@UnstableApi
 public abstract class AbstractAddressResolver<T extends SocketAddress> implements AddressResolver<T> {
 
     private final EventExecutor executor;
@@ -44,7 +42,7 @@ public abstract class AbstractAddressResolver<T extends SocketAddress> implement
      */
     protected AbstractAddressResolver(EventExecutor executor) {
         this.executor = checkNotNull(executor, "executor");
-        matcher = TypeParameterMatcher.find(this, AbstractAddressResolver.class, "T");
+        this.matcher = TypeParameterMatcher.find(this, AbstractAddressResolver.class, "T");
     }
 
     /**
@@ -54,7 +52,7 @@ public abstract class AbstractAddressResolver<T extends SocketAddress> implement
      */
     protected AbstractAddressResolver(EventExecutor executor, Class<? extends T> addressType) {
         this.executor = checkNotNull(executor, "executor");
-        matcher = TypeParameterMatcher.get(addressType);
+        this.matcher = TypeParameterMatcher.get(addressType);
     }
 
     /**
diff --git a/resolver/src/main/java/io/netty/resolver/AddressResolver.java b/resolver/src/main/java/io/netty/resolver/AddressResolver.java
index 042affd..adb613a 100644
--- a/resolver/src/main/java/io/netty/resolver/AddressResolver.java
+++ b/resolver/src/main/java/io/netty/resolver/AddressResolver.java
@@ -17,7 +17,6 @@ package io.netty.resolver;
 
 import io.netty.util.concurrent.Future;
 import io.netty.util.concurrent.Promise;
-import io.netty.util.internal.UnstableApi;
 
 import java.io.Closeable;
 import java.net.SocketAddress;
@@ -27,7 +26,6 @@ import java.util.List;
 /**
  * Resolves a possibility unresolved {@link SocketAddress}.
  */
-@UnstableApi
 public interface AddressResolver<T extends SocketAddress> extends Closeable {
 
   /**
diff --git a/resolver/src/main/java/io/netty/resolver/AddressResolverGroup.java b/resolver/src/main/java/io/netty/resolver/AddressResolverGroup.java
index e9bb9df..595f53e 100644
--- a/resolver/src/main/java/io/netty/resolver/AddressResolverGroup.java
+++ b/resolver/src/main/java/io/netty/resolver/AddressResolverGroup.java
@@ -19,7 +19,8 @@ package io.netty.resolver;
 import io.netty.util.concurrent.EventExecutor;
 import io.netty.util.concurrent.Future;
 import io.netty.util.concurrent.FutureListener;
-import io.netty.util.internal.UnstableApi;
+import io.netty.util.concurrent.GenericFutureListener;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 
@@ -32,7 +33,6 @@ import java.util.concurrent.ConcurrentMap;
 /**
  * Creates and manages {@link NameResolver}s so that each {@link EventExecutor} has its own resolver instance.
  */
-@UnstableApi
 public abstract class AddressResolverGroup<T extends SocketAddress> implements Closeable {
 
     private static final InternalLogger logger = InternalLoggerFactory.getInstance(AddressResolverGroup.class);
@@ -43,18 +43,19 @@ public abstract class AddressResolverGroup<T extends SocketAddress> implements C
     private final Map<EventExecutor, AddressResolver<T>> resolvers =
             new IdentityHashMap<EventExecutor, AddressResolver<T>>();
 
+    private final Map<EventExecutor, GenericFutureListener<Future<Object>>> executorTerminationListeners =
+            new IdentityHashMap<EventExecutor, GenericFutureListener<Future<Object>>>();
+
     protected AddressResolverGroup() { }
 
     /**
      * Returns the {@link AddressResolver} associated with the specified {@link EventExecutor}. If there's no associated
-     * resolved found, this method creates and returns a new resolver instance created by
+     * resolver found, this method creates and returns a new resolver instance created by
      * {@link #newResolver(EventExecutor)} so that the new resolver is reused on another
-     * {@link #getResolver(EventExecutor)} call with the same {@link EventExecutor}.
+     * {@code #getResolver(EventExecutor)} call with the same {@link EventExecutor}.
      */
     public AddressResolver<T> getResolver(final EventExecutor executor) {
-        if (executor == null) {
-            throw new NullPointerException("executor");
-        }
+        ObjectUtil.checkNotNull(executor, "executor");
 
         if (executor.isShuttingDown()) {
             throw new IllegalStateException("executor not accepting a task");
@@ -72,15 +73,20 @@ public abstract class AddressResolverGroup<T extends SocketAddress> implements C
                 }
 
                 resolvers.put(executor, newResolver);
-                executor.terminationFuture().addListener(new FutureListener<Object>() {
+
+                final FutureListener<Object> terminationListener = new FutureListener<Object>() {
                     @Override
-                    public void operationComplete(Future<Object> future) throws Exception {
+                    public void operationComplete(Future<Object> future) {
                         synchronized (resolvers) {
                             resolvers.remove(executor);
+                            executorTerminationListeners.remove(executor);
                         }
                         newResolver.close();
                     }
-                });
+                };
+
+                executorTerminationListeners.put(executor, terminationListener);
+                executor.terminationFuture().addListener(terminationListener);
 
                 r = newResolver;
             }
@@ -101,12 +107,20 @@ public abstract class AddressResolverGroup<T extends SocketAddress> implements C
     @SuppressWarnings({ "unchecked", "SuspiciousToArrayCall" })
     public void close() {
         final AddressResolver<T>[] rArray;
+        final Map.Entry<EventExecutor, GenericFutureListener<Future<Object>>>[] listeners;
+
         synchronized (resolvers) {
             rArray = (AddressResolver<T>[]) resolvers.values().toArray(new AddressResolver[0]);
             resolvers.clear();
+            listeners = executorTerminationListeners.entrySet().toArray(new Map.Entry[0]);
+            executorTerminationListeners.clear();
+        }
+
+        for (final Map.Entry<EventExecutor, GenericFutureListener<Future<Object>>> entry : listeners) {
+            entry.getKey().terminationFuture().removeListener(entry.getValue());
         }
 
-        for (AddressResolver<T> r: rArray) {
+        for (final AddressResolver<T> r: rArray) {
             try {
                 r.close();
             } catch (Throwable t) {
diff --git a/resolver/src/main/java/io/netty/resolver/CompositeNameResolver.java b/resolver/src/main/java/io/netty/resolver/CompositeNameResolver.java
index 8074c3d..e93547e 100644
--- a/resolver/src/main/java/io/netty/resolver/CompositeNameResolver.java
+++ b/resolver/src/main/java/io/netty/resolver/CompositeNameResolver.java
@@ -19,19 +19,18 @@ import io.netty.util.concurrent.EventExecutor;
 import io.netty.util.concurrent.Future;
 import io.netty.util.concurrent.FutureListener;
 import io.netty.util.concurrent.Promise;
-import io.netty.util.internal.UnstableApi;
+import io.netty.util.internal.ObjectUtil;
 
 import java.util.Arrays;
 import java.util.List;
 
-import static io.netty.util.internal.ObjectUtil.*;
+import static io.netty.util.internal.ObjectUtil.checkNotNull;
 
 /**
  * A composite {@link SimpleNameResolver} that resolves a host name against a sequence of {@link NameResolver}s.
  *
  * In case of a failure, only the last one will be reported.
  */
-@UnstableApi
 public final class CompositeNameResolver<T> extends SimpleNameResolver<T> {
 
     private final NameResolver<T>[] resolvers;
@@ -45,9 +44,7 @@ public final class CompositeNameResolver<T> extends SimpleNameResolver<T> {
         super(executor);
         checkNotNull(resolvers, "resolvers");
         for (int i = 0; i < resolvers.length; i++) {
-            if (resolvers[i] == null) {
-                throw new NullPointerException("resolvers[" + i + ']');
-            }
+            ObjectUtil.checkNotNull(resolvers[i], "resolvers[" + i + ']');
         }
         if (resolvers.length < 2) {
             throw new IllegalArgumentException("resolvers: " + Arrays.asList(resolvers) +
diff --git a/resolver/src/main/java/io/netty/resolver/DefaultAddressResolverGroup.java b/resolver/src/main/java/io/netty/resolver/DefaultAddressResolverGroup.java
index 9e22b8f..1b6fb53 100644
--- a/resolver/src/main/java/io/netty/resolver/DefaultAddressResolverGroup.java
+++ b/resolver/src/main/java/io/netty/resolver/DefaultAddressResolverGroup.java
@@ -17,14 +17,12 @@
 package io.netty.resolver;
 
 import io.netty.util.concurrent.EventExecutor;
-import io.netty.util.internal.UnstableApi;
 
 import java.net.InetSocketAddress;
 
 /**
  * A {@link AddressResolverGroup} of {@link DefaultNameResolver}s.
  */
-@UnstableApi
 public final class DefaultAddressResolverGroup extends AddressResolverGroup<InetSocketAddress> {
 
     public static final DefaultAddressResolverGroup INSTANCE = new DefaultAddressResolverGroup();
diff --git a/resolver/src/main/java/io/netty/resolver/DefaultHostsFileEntriesResolver.java b/resolver/src/main/java/io/netty/resolver/DefaultHostsFileEntriesResolver.java
index 9051262..97ca29b 100644
--- a/resolver/src/main/java/io/netty/resolver/DefaultHostsFileEntriesResolver.java
+++ b/resolver/src/main/java/io/netty/resolver/DefaultHostsFileEntriesResolver.java
@@ -17,7 +17,6 @@ package io.netty.resolver;
 
 import io.netty.util.CharsetUtil;
 import io.netty.util.internal.PlatformDependent;
-import io.netty.util.internal.UnstableApi;
 
 import java.net.Inet4Address;
 import java.net.Inet6Address;
@@ -29,7 +28,6 @@ import java.util.Map;
 /**
  * Default {@link HostsFileEntriesResolver} that resolves hosts file entries only once.
  */
-@UnstableApi
 public final class DefaultHostsFileEntriesResolver implements HostsFileEntriesResolver {
 
     private final Map<String, Inet4Address> inet4Entries;
diff --git a/resolver/src/main/java/io/netty/resolver/DefaultNameResolver.java b/resolver/src/main/java/io/netty/resolver/DefaultNameResolver.java
index d6d3aea..38ae0eb 100644
--- a/resolver/src/main/java/io/netty/resolver/DefaultNameResolver.java
+++ b/resolver/src/main/java/io/netty/resolver/DefaultNameResolver.java
@@ -19,7 +19,6 @@ package io.netty.resolver;
 import io.netty.util.internal.SocketUtils;
 import io.netty.util.concurrent.EventExecutor;
 import io.netty.util.concurrent.Promise;
-import io.netty.util.internal.UnstableApi;
 
 import java.net.InetAddress;
 import java.net.UnknownHostException;
@@ -30,7 +29,6 @@ import java.util.List;
  * A {@link InetNameResolver} that resolves using JDK's built-in domain name lookup mechanism.
  * Note that this resolver performs a blocking name lookup from the caller thread.
  */
-@UnstableApi
 public class DefaultNameResolver extends InetNameResolver {
 
     public DefaultNameResolver(EventExecutor executor) {
diff --git a/resolver/src/main/java/io/netty/resolver/HostsFileEntries.java b/resolver/src/main/java/io/netty/resolver/HostsFileEntries.java
index 03f555c..7ea76a4 100644
--- a/resolver/src/main/java/io/netty/resolver/HostsFileEntries.java
+++ b/resolver/src/main/java/io/netty/resolver/HostsFileEntries.java
@@ -15,8 +15,6 @@
  */
 package io.netty.resolver;
 
-import io.netty.util.internal.UnstableApi;
-
 import java.net.Inet4Address;
 import java.net.Inet6Address;
 import java.util.Collections;
@@ -26,7 +24,6 @@ import java.util.Map;
 /**
  * A container of hosts file entries
  */
-@UnstableApi
 public final class HostsFileEntries {
 
     /**
diff --git a/resolver/src/main/java/io/netty/resolver/HostsFileEntriesResolver.java b/resolver/src/main/java/io/netty/resolver/HostsFileEntriesResolver.java
index 753467d..feedbd9 100644
--- a/resolver/src/main/java/io/netty/resolver/HostsFileEntriesResolver.java
+++ b/resolver/src/main/java/io/netty/resolver/HostsFileEntriesResolver.java
@@ -15,14 +15,11 @@
  */
 package io.netty.resolver;
 
-import io.netty.util.internal.UnstableApi;
-
 import java.net.InetAddress;
 
 /**
  * Resolves a hostname against the hosts file entries.
  */
-@UnstableApi
 public interface HostsFileEntriesResolver {
 
     /**
diff --git a/resolver/src/main/java/io/netty/resolver/HostsFileParser.java b/resolver/src/main/java/io/netty/resolver/HostsFileParser.java
index 16eaba6..113572d 100644
--- a/resolver/src/main/java/io/netty/resolver/HostsFileParser.java
+++ b/resolver/src/main/java/io/netty/resolver/HostsFileParser.java
@@ -17,7 +17,6 @@ package io.netty.resolver;
 
 import io.netty.util.NetUtil;
 import io.netty.util.internal.PlatformDependent;
-import io.netty.util.internal.UnstableApi;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 
@@ -43,7 +42,6 @@ import static io.netty.util.internal.ObjectUtil.*;
 /**
  * A parser for hosts files.
  */
-@UnstableApi
 public final class HostsFileParser {
 
     private static final String WINDOWS_DEFAULT_SYSTEM_ROOT = "C:\\Windows";
diff --git a/resolver/src/main/java/io/netty/resolver/InetNameResolver.java b/resolver/src/main/java/io/netty/resolver/InetNameResolver.java
index eb5cfe0..f90cd10 100644
--- a/resolver/src/main/java/io/netty/resolver/InetNameResolver.java
+++ b/resolver/src/main/java/io/netty/resolver/InetNameResolver.java
@@ -17,7 +17,6 @@ package io.netty.resolver;
 
 import io.netty.util.concurrent.EventExecutor;
 import io.netty.util.concurrent.Future;
-import io.netty.util.internal.UnstableApi;
 
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
@@ -25,7 +24,6 @@ import java.net.InetSocketAddress;
 /**
  * A skeletal {@link NameResolver} implementation that resolves {@link InetAddress}.
  */
-@UnstableApi
 public abstract class InetNameResolver extends SimpleNameResolver<InetAddress> {
     private volatile AddressResolver<InetSocketAddress> addressResolver;
 
diff --git a/resolver/src/main/java/io/netty/resolver/InetSocketAddressResolver.java b/resolver/src/main/java/io/netty/resolver/InetSocketAddressResolver.java
index c7db25d..7f5f5b2 100644
--- a/resolver/src/main/java/io/netty/resolver/InetSocketAddressResolver.java
+++ b/resolver/src/main/java/io/netty/resolver/InetSocketAddressResolver.java
@@ -19,7 +19,6 @@ import io.netty.util.concurrent.EventExecutor;
 import io.netty.util.concurrent.Future;
 import io.netty.util.concurrent.FutureListener;
 import io.netty.util.concurrent.Promise;
-import io.netty.util.internal.UnstableApi;
 
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
@@ -29,7 +28,6 @@ import java.util.List;
 /**
  * A {@link AbstractAddressResolver} that resolves {@link InetSocketAddress}.
  */
-@UnstableApi
 public class InetSocketAddressResolver extends AbstractAddressResolver<InetSocketAddress> {
 
     final NameResolver<InetAddress> nameResolver;
diff --git a/resolver/src/main/java/io/netty/resolver/NameResolver.java b/resolver/src/main/java/io/netty/resolver/NameResolver.java
index 53a56b4..818122c 100644
--- a/resolver/src/main/java/io/netty/resolver/NameResolver.java
+++ b/resolver/src/main/java/io/netty/resolver/NameResolver.java
@@ -18,7 +18,6 @@ package io.netty.resolver;
 
 import io.netty.util.concurrent.Future;
 import io.netty.util.concurrent.Promise;
-import io.netty.util.internal.UnstableApi;
 
 import java.io.Closeable;
 import java.util.List;
@@ -26,7 +25,6 @@ import java.util.List;
 /**
  * Resolves an arbitrary string that represents the name of an endpoint into an address.
  */
-@UnstableApi
 public interface NameResolver<T> extends Closeable {
 
     /**
diff --git a/resolver/src/main/java/io/netty/resolver/NoopAddressResolver.java b/resolver/src/main/java/io/netty/resolver/NoopAddressResolver.java
index 9551469..c34bb57 100644
--- a/resolver/src/main/java/io/netty/resolver/NoopAddressResolver.java
+++ b/resolver/src/main/java/io/netty/resolver/NoopAddressResolver.java
@@ -18,7 +18,6 @@ package io.netty.resolver;
 
 import io.netty.util.concurrent.EventExecutor;
 import io.netty.util.concurrent.Promise;
-import io.netty.util.internal.UnstableApi;
 
 import java.net.SocketAddress;
 import java.util.Collections;
@@ -28,7 +27,6 @@ import java.util.List;
  * A {@link AddressResolver} that does not perform any resolution but always reports successful resolution.
  * This resolver is useful when name resolution is performed by a handler in a pipeline, such as a proxy handler.
  */
-@UnstableApi
 public class NoopAddressResolver extends AbstractAddressResolver<SocketAddress> {
 
     public NoopAddressResolver(EventExecutor executor) {
diff --git a/resolver/src/main/java/io/netty/resolver/NoopAddressResolverGroup.java b/resolver/src/main/java/io/netty/resolver/NoopAddressResolverGroup.java
index 7727652..980cc50 100644
--- a/resolver/src/main/java/io/netty/resolver/NoopAddressResolverGroup.java
+++ b/resolver/src/main/java/io/netty/resolver/NoopAddressResolverGroup.java
@@ -17,14 +17,12 @@
 package io.netty.resolver;
 
 import io.netty.util.concurrent.EventExecutor;
-import io.netty.util.internal.UnstableApi;
 
 import java.net.SocketAddress;
 
 /**
  * A {@link AddressResolverGroup} of {@link NoopAddressResolver}s.
  */
-@UnstableApi
 public final class NoopAddressResolverGroup extends AddressResolverGroup<SocketAddress> {
 
     public static final NoopAddressResolverGroup INSTANCE = new NoopAddressResolverGroup();
diff --git a/resolver/src/main/java/io/netty/resolver/ResolvedAddressTypes.java b/resolver/src/main/java/io/netty/resolver/ResolvedAddressTypes.java
index ae3e55d..ea849bf 100644
--- a/resolver/src/main/java/io/netty/resolver/ResolvedAddressTypes.java
+++ b/resolver/src/main/java/io/netty/resolver/ResolvedAddressTypes.java
@@ -15,12 +15,9 @@
  */
 package io.netty.resolver;
 
-import io.netty.util.internal.UnstableApi;
-
 /**
  * Defined resolved address types.
  */
-@UnstableApi
 public enum ResolvedAddressTypes {
     /**
      * Only resolve IPv4 addresses
diff --git a/resolver/src/main/java/io/netty/resolver/RoundRobinInetAddressResolver.java b/resolver/src/main/java/io/netty/resolver/RoundRobinInetAddressResolver.java
index f505328..9c26f38 100644
--- a/resolver/src/main/java/io/netty/resolver/RoundRobinInetAddressResolver.java
+++ b/resolver/src/main/java/io/netty/resolver/RoundRobinInetAddressResolver.java
@@ -20,7 +20,6 @@ import io.netty.util.concurrent.Future;
 import io.netty.util.concurrent.FutureListener;
 import io.netty.util.concurrent.Promise;
 import io.netty.util.internal.PlatformDependent;
-import io.netty.util.internal.UnstableApi;
 
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
@@ -35,7 +34,6 @@ import java.util.List;
  * if multiple are returned by the {@link NameResolver}.
  * Use {@link #asAddressResolver()} to create a {@link InetSocketAddress} resolver
  */
-@UnstableApi
 public class RoundRobinInetAddressResolver extends InetNameResolver {
     private final NameResolver<InetAddress> nameResolver;
 
@@ -100,4 +98,9 @@ public class RoundRobinInetAddressResolver extends InetNameResolver {
     private static int randomIndex(int numAddresses) {
         return numAddresses == 1 ? 0 : PlatformDependent.threadLocalRandom().nextInt(numAddresses);
     }
+
+    @Override
+    public void close() {
+        nameResolver.close();
+    }
 }
diff --git a/resolver/src/main/java/io/netty/resolver/SimpleNameResolver.java b/resolver/src/main/java/io/netty/resolver/SimpleNameResolver.java
index 923530f..c8af37a 100644
--- a/resolver/src/main/java/io/netty/resolver/SimpleNameResolver.java
+++ b/resolver/src/main/java/io/netty/resolver/SimpleNameResolver.java
@@ -19,7 +19,6 @@ package io.netty.resolver;
 import io.netty.util.concurrent.EventExecutor;
 import io.netty.util.concurrent.Future;
 import io.netty.util.concurrent.Promise;
-import io.netty.util.internal.UnstableApi;
 
 import java.util.List;
 
@@ -28,7 +27,6 @@ import static io.netty.util.internal.ObjectUtil.*;
 /**
  * A skeletal {@link NameResolver} implementation.
  */
-@UnstableApi
 public abstract class SimpleNameResolver<T> implements NameResolver<T> {
 
     private final EventExecutor executor;
diff --git a/resolver/src/main/java/io/netty/resolver/package-info.java b/resolver/src/main/java/io/netty/resolver/package-info.java
index 0dd9ab2..a924598 100644
--- a/resolver/src/main/java/io/netty/resolver/package-info.java
+++ b/resolver/src/main/java/io/netty/resolver/package-info.java
@@ -17,7 +17,4 @@
 /**
  * Resolves an arbitrary string that represents the name of an endpoint into an address.
  */
-@UnstableApi
 package io.netty.resolver;
-
-import io.netty.util.internal.UnstableApi;
diff --git a/tarball/pom.xml b/tarball/pom.xml
index d55b673..401aca9 100644
--- a/tarball/pom.xml
+++ b/tarball/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>io.netty</groupId>
     <artifactId>netty-parent</artifactId>
-    <version>4.1.33.Final</version>
+    <version>4.1.48.Final</version>
   </parent>
 
   <artifactId>netty-tarball</artifactId>
@@ -123,6 +123,14 @@
           <scope>compile</scope>
           <optional>true</optional>
         </dependency>
+        <dependency>
+          <groupId>${project.groupId}</groupId>
+          <artifactId>netty-resolver-dns-native-macos</artifactId>
+          <version>${project.version}</version>
+          <classifier>osx-x86_64</classifier>
+          <scope>compile</scope>
+          <optional>true</optional>
+        </dependency>
       </dependencies>
     </profile>
     <profile>
@@ -145,6 +153,14 @@
           <scope>compile</scope>
           <optional>true</optional>
         </dependency>
+        <dependency>
+          <groupId>${project.groupId}</groupId>
+          <artifactId>netty-resolver-dns-native-macos</artifactId>
+          <version>${project.version}</version>
+          <classifier>osx-x86_64</classifier>
+          <scope>compile</scope>
+          <optional>true</optional>
+        </dependency>
       </dependencies>
     </profile>
 
diff --git a/testsuite-autobahn/pom.xml b/testsuite-autobahn/pom.xml
index c85f7e1..c4c7edb 100644
--- a/testsuite-autobahn/pom.xml
+++ b/testsuite-autobahn/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>io.netty</groupId>
     <artifactId>netty-parent</artifactId>
-    <version>4.1.33.Final</version>
+    <version>4.1.48.Final</version>
   </parent>
 
   <artifactId>netty-testsuite-autobahn</artifactId>
@@ -28,6 +28,10 @@
 
   <name>Netty/Testsuite/Autobahn</name>
 
+  <properties>
+    <skipJapicmp>true</skipJapicmp>
+  </properties>
+
   <dependencies>
     <dependency>
       <groupId>${project.groupId}</groupId>
diff --git a/testsuite-autobahn/src/main/java/io/netty/testsuite/autobahn/AutobahnServerHandler.java b/testsuite-autobahn/src/main/java/io/netty/testsuite/autobahn/AutobahnServerHandler.java
index 75f4062..2febe9e 100644
--- a/testsuite-autobahn/src/main/java/io/netty/testsuite/autobahn/AutobahnServerHandler.java
+++ b/testsuite-autobahn/src/main/java/io/netty/testsuite/autobahn/AutobahnServerHandler.java
@@ -73,13 +73,13 @@ public class AutobahnServerHandler extends ChannelInboundHandlerAdapter {
             throws Exception {
         // Handle a bad request.
         if (!req.decoderResult().isSuccess()) {
-            sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST));
+            sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST, ctx.alloc().buffer(0)));
             return;
         }
 
         // Allow only GET methods.
-        if (req.method() != GET) {
-            sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN));
+        if (!GET.equals(req.method())) {
+            sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN, ctx.alloc().buffer(0)));
             return;
         }
 
diff --git a/testsuite-http2/pom.xml b/testsuite-http2/pom.xml
index 061cd94..802b278 100644
--- a/testsuite-http2/pom.xml
+++ b/testsuite-http2/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>io.netty</groupId>
     <artifactId>netty-parent</artifactId>
-    <version>4.1.33.Final</version>
+    <version>4.1.48.Final</version>
   </parent>
 
   <artifactId>netty-testsuite-http2</artifactId>
@@ -28,6 +28,10 @@
 
   <name>Netty/Testsuite/Http2</name>
 
+  <properties>
+    <skipJapicmp>true</skipJapicmp>
+  </properties>
+
   <dependencies>
     <dependency>
       <groupId>${project.groupId}</groupId>
diff --git a/testsuite-http2/src/main/java/io/netty/testsuite/http2/HelloWorldHttp1Handler.java b/testsuite-http2/src/main/java/io/netty/testsuite/http2/HelloWorldHttp1Handler.java
index 493e31b..cf15818 100644
--- a/testsuite-http2/src/main/java/io/netty/testsuite/http2/HelloWorldHttp1Handler.java
+++ b/testsuite-http2/src/main/java/io/netty/testsuite/http2/HelloWorldHttp1Handler.java
@@ -47,7 +47,7 @@ public class HelloWorldHttp1Handler extends SimpleChannelInboundHandler<FullHttp
     @Override
     public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception {
         if (HttpUtil.is100ContinueExpected(req)) {
-            ctx.write(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE));
+            ctx.write(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE, ctx.alloc().buffer(0)));
         }
         boolean keepAlive = HttpUtil.isKeepAlive(req);
 
diff --git a/testsuite-http2/src/main/java/io/netty/testsuite/http2/Http2Server.java b/testsuite-http2/src/main/java/io/netty/testsuite/http2/Http2Server.java
index 360cd24..e562b00 100644
--- a/testsuite-http2/src/main/java/io/netty/testsuite/http2/Http2Server.java
+++ b/testsuite-http2/src/main/java/io/netty/testsuite/http2/Http2Server.java
@@ -26,7 +26,7 @@ import io.netty.handler.logging.LogLevel;
 import io.netty.handler.logging.LoggingHandler;
 
 /**
- * A HTTP/2 Server that responds to requests with a Hello World. Once started, you can test the
+ * An HTTP/2 Server that responds to requests with a Hello World. Once started, you can test the
  * server with the example client.
  */
 public final class Http2Server {
diff --git a/testsuite-http2/src/main/java/io/netty/testsuite/http2/Http2ServerInitializer.java b/testsuite-http2/src/main/java/io/netty/testsuite/http2/Http2ServerInitializer.java
index 7b67fc9..582de34 100644
--- a/testsuite-http2/src/main/java/io/netty/testsuite/http2/Http2ServerInitializer.java
+++ b/testsuite-http2/src/main/java/io/netty/testsuite/http2/Http2ServerInitializer.java
@@ -16,6 +16,8 @@
 
 package io.netty.testsuite.http2;
 
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
+
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelInboundHandlerAdapter;
 import io.netty.channel.ChannelInitializer;
@@ -31,7 +33,6 @@ import io.netty.handler.codec.http.HttpServerUpgradeHandler.UpgradeCodecFactory;
 import io.netty.handler.codec.http2.CleartextHttp2ServerUpgradeHandler;
 import io.netty.handler.codec.http2.Http2CodecUtil;
 import io.netty.handler.codec.http2.Http2ServerUpgradeCodec;
-import io.netty.handler.ssl.SslContext;
 import io.netty.util.AsciiString;
 import io.netty.util.ReferenceCountUtil;
 
@@ -59,9 +60,7 @@ public class Http2ServerInitializer extends ChannelInitializer<SocketChannel> {
     }
 
     Http2ServerInitializer(int maxHttpContentLength) {
-        if (maxHttpContentLength < 0) {
-            throw new IllegalArgumentException("maxHttpContentLength (expected >= 0): " + maxHttpContentLength);
-        }
+        checkPositiveOrZero(maxHttpContentLength, "maxHttpContentLength");
         this.maxHttpContentLength = maxHttpContentLength;
     }
 
diff --git a/testsuite-native-image/pom.xml b/testsuite-native-image/pom.xml
new file mode 100644
index 0000000..f0fbde8
--- /dev/null
+++ b/testsuite-native-image/pom.xml
@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2017 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.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>io.netty</groupId>
+    <artifactId>netty-parent</artifactId>
+    <version>4.1.48.Final</version>
+  </parent>
+
+  <artifactId>netty-testsuite-native-image</artifactId>
+  <packaging>jar</packaging>
+
+  <name>Netty/Testsuite/NativeImage</name>
+
+  <properties>
+    <skipJapicmp>true</skipJapicmp>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>netty-common</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>netty-buffer</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>netty-transport</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>netty-handler</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>netty-codec-http</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+  </dependencies>
+
+  <profiles>
+    <profile>
+      <id>skipTests</id>
+      <activation>
+        <property>
+          <name>skipTests</name>
+        </property>
+      </activation>
+      <properties>
+        <skipNativeImageTestsuite>true</skipNativeImageTestsuite>
+      </properties>
+    </profile>
+  </profiles>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>com.oracle.substratevm</groupId>
+        <artifactId>native-image-maven-plugin</artifactId>
+        <version>${graalvm.version}</version>
+        <executions>
+          <execution>
+            <goals>
+              <goal>native-image</goal>
+            </goals>
+            <phase>package</phase>
+          </execution>
+        </executions>
+        <configuration>
+          <skip>${skipNativeImageTestsuite}</skip>
+          <imageName>${project.artifactId}</imageName>
+          <mainClass>io.netty.testsuite.svm.HttpNativeServer</mainClass>
+          <buildArgs>--report-unsupported-elements-at-runtime --allow-incomplete-classpath</buildArgs>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>exec-maven-plugin</artifactId>
+        <version>1.6.0</version>
+        <executions>
+          <!-- This will do a whitesmoke test: if the substitutions are missing the binary will fail to run -->
+          <!-- If the metadata is missing the build above will fail -->
+          <execution>
+            <id>verify-native-image</id>
+            <phase>verify</phase>
+            <goals>
+              <goal>exec</goal>
+            </goals>
+          </execution>
+        </executions>
+        <configuration>
+          <skip>${skipNativeImageTestsuite}</skip>
+          <executable>${project.build.directory}/${project.artifactId}</executable>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/testsuite-native-image/src/main/java/io/netty/testsuite/svm/HttpNativeServer.java b/testsuite-native-image/src/main/java/io/netty/testsuite/svm/HttpNativeServer.java
new file mode 100644
index 0000000..3676cd0
--- /dev/null
+++ b/testsuite-native-image/src/main/java/io/netty/testsuite/svm/HttpNativeServer.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2019 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.testsuite.svm;
+
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.handler.logging.LogLevel;
+import io.netty.handler.logging.LoggingHandler;
+
+/**
+ * An HTTP server that sends back the content of the received HTTP request
+ * in a pretty plaintext form.
+ */
+public final class HttpNativeServer {
+
+    /**
+     * Main entry point (not instantiable)
+     */
+    private HttpNativeServer() {
+    }
+
+    public static void main(String[] args) throws Exception {
+        // Configure the server.
+        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
+        EventLoopGroup workerGroup = new NioEventLoopGroup();
+        // Control status.
+        boolean serverStartSucess = false;
+        try {
+            ServerBootstrap b = new ServerBootstrap();
+            b.option(ChannelOption.SO_BACKLOG, 1024);
+            b.group(bossGroup, workerGroup)
+             .channel(NioServerSocketChannel.class)
+             .handler(new LoggingHandler(LogLevel.INFO))
+             .childHandler(new HttpNativeServerInitializer());
+
+            Channel channel = b.bind(0).sync().channel();
+            System.err.println("Server started, will shutdown now.");
+            channel.close().sync();
+            serverStartSucess = true;
+        } finally {
+            bossGroup.shutdownGracefully();
+            workerGroup.shutdownGracefully();
+        }
+        // return the right system exit code to signal success
+        System.exit(serverStartSucess ? 0 : 1);
+    }
+}
diff --git a/testsuite-native-image/src/main/java/io/netty/testsuite/svm/HttpNativeServerHandler.java b/testsuite-native-image/src/main/java/io/netty/testsuite/svm/HttpNativeServerHandler.java
new file mode 100644
index 0000000..33531dc
--- /dev/null
+++ b/testsuite-native-image/src/main/java/io/netty/testsuite/svm/HttpNativeServerHandler.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2013 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.testsuite.svm;
+
+import io.netty.buffer.Unpooled;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.handler.codec.http.DefaultFullHttpResponse;
+import io.netty.handler.codec.http.FullHttpResponse;
+import io.netty.handler.codec.http.HttpObject;
+import io.netty.handler.codec.http.HttpUtil;
+import io.netty.handler.codec.http.HttpRequest;
+import io.netty.util.AsciiString;
+
+import static io.netty.handler.codec.http.HttpHeaderNames.*;
+import static io.netty.handler.codec.http.HttpResponseStatus.*;
+import static io.netty.handler.codec.http.HttpVersion.*;
+
+public class HttpNativeServerHandler extends SimpleChannelInboundHandler<HttpObject> {
+    private static final byte[] CONTENT = { 'H', 'e', 'l', 'l', 'o', ' ', 'N', 'a', 't', 'i', 'v', 'e' };
+
+    private static final AsciiString KEEP_ALIVE = AsciiString.cached("keep-alive");
+
+    @Override
+    public void channelReadComplete(ChannelHandlerContext ctx) {
+        ctx.flush();
+    }
+
+    @Override
+    public void channelRead0(ChannelHandlerContext ctx, HttpObject msg) {
+        if (msg instanceof HttpRequest) {
+            HttpRequest req = (HttpRequest) msg;
+
+            boolean keepAlive = HttpUtil.isKeepAlive(req);
+            FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK, Unpooled.wrappedBuffer(CONTENT));
+            response.headers().set(CONTENT_TYPE, "text/plain");
+            response.headers().setInt(CONTENT_LENGTH, response.content().readableBytes());
+
+            if (!keepAlive) {
+                ctx.write(response).addListener(ChannelFutureListener.CLOSE);
+            } else {
+                response.headers().set(CONNECTION, KEEP_ALIVE);
+                ctx.write(response);
+            }
+        }
+    }
+
+    @Override
+    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+        cause.printStackTrace();
+        ctx.close();
+    }
+}
diff --git a/testsuite-native-image/src/main/java/io/netty/testsuite/svm/HttpNativeServerInitializer.java b/testsuite-native-image/src/main/java/io/netty/testsuite/svm/HttpNativeServerInitializer.java
new file mode 100644
index 0000000..80a53a0
--- /dev/null
+++ b/testsuite-native-image/src/main/java/io/netty/testsuite/svm/HttpNativeServerInitializer.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2019 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.testsuite.svm;
+
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelPipeline;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.handler.codec.http.HttpServerCodec;
+import io.netty.handler.codec.http.HttpServerExpectContinueHandler;
+
+public class HttpNativeServerInitializer extends ChannelInitializer<SocketChannel> {
+
+    @Override
+    public void initChannel(SocketChannel ch) {
+        ChannelPipeline p = ch.pipeline();
+        p.addLast(new HttpServerCodec());
+        p.addLast(new HttpServerExpectContinueHandler());
+        p.addLast(new HttpNativeServerHandler());
+    }
+}
diff --git a/testsuite-native-image/src/main/java/io/netty/testsuite/svm/package-info.java b/testsuite-native-image/src/main/java/io/netty/testsuite/svm/package-info.java
new file mode 100644
index 0000000..89c639a
--- /dev/null
+++ b/testsuite-native-image/src/main/java/io/netty/testsuite/svm/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2019 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.
+ */
+
+/**
+ * A hello world server that should be compiled to native.
+ */
+package io.netty.testsuite.svm;
diff --git a/testsuite-osgi/pom.xml b/testsuite-osgi/pom.xml
index 767d801..153da53 100644
--- a/testsuite-osgi/pom.xml
+++ b/testsuite-osgi/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>io.netty</groupId>
     <artifactId>netty-parent</artifactId>
-    <version>4.1.33.Final</version>
+    <version>4.1.48.Final</version>
   </parent>
 
   <artifactId>netty-testsuite-osgi</artifactId>
@@ -31,6 +31,7 @@
   <properties>
     <exam.version>4.13.0</exam.version>
     <argLine.java9.extras>--add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/jdk.internal.loader=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED --add-opens=java.base/java.security=ALL-UNNAMED</argLine.java9.extras>
+    <skipJapicmp>true</skipJapicmp>
   </properties>
 
   <profiles>
@@ -45,6 +46,40 @@
         <skipOsgiTestsuite>true</skipOsgiTestsuite>
       </properties>
     </profile>
+
+    <profile>
+      <id>linux</id>
+      <activation>
+        <os>
+          <family>linux</family>
+        </os>
+      </activation>
+      <dependencies>
+        <dependency>
+          <groupId>${project.groupId}</groupId>
+          <artifactId>netty-transport-native-epoll</artifactId>
+          <version>${project.version}</version>
+          <scope>test</scope>
+        </dependency>
+      </dependencies>
+    </profile>
+
+    <profile>
+      <id>mac</id>
+      <activation>
+        <os>
+          <family>mac</family>
+        </os>
+      </activation>
+      <dependencies>
+        <dependency>
+          <groupId>${project.groupId}</groupId>
+          <artifactId>netty-transport-native-kqueue</artifactId>
+          <version>${project.version}</version>
+          <scope>test</scope>
+        </dependency>
+      </dependencies>
+    </profile>
   </profiles>
 
   <dependencies>
@@ -145,12 +180,6 @@
       <version>${project.version}</version>
       <scope>test</scope>
     </dependency>
-    <dependency>
-      <groupId>${project.groupId}</groupId>
-      <artifactId>netty-transport-rxtx</artifactId>
-      <version>${project.version}</version>
-      <scope>test</scope>
-    </dependency>
     <dependency>
       <groupId>${project.groupId}</groupId>
       <artifactId>netty-transport-sctp</artifactId>
@@ -165,67 +194,54 @@
     </dependency>
 
     <dependency>
-      <groupId>org.ops4j.pax.exam</groupId>
-      <artifactId>pax-exam-container-native</artifactId>
-      <version>${exam.version}</version>
-      <scope>test</scope>
+      <groupId>org.apache.felix</groupId>
+      <artifactId>org.apache.felix.configadmin</artifactId>
+      <version>1.9.14</version>
     </dependency>
     <dependency>
-      <groupId>org.ops4j.pax.exam</groupId>
-      <artifactId>pax-exam-junit4</artifactId>
-      <version>${exam.version}</version>
+      <groupId>org.apache.felix</groupId>
+      <artifactId>org.apache.felix.framework</artifactId>
+      <version>6.0.2</version>
       <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.ops4j.pax.exam</groupId>
-      <artifactId>pax-exam</artifactId>
+      <artifactId>pax-exam-junit4</artifactId>
       <version>${exam.version}</version>
       <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.ops4j.pax.exam</groupId>
-      <artifactId>pax-exam-spi</artifactId>
+      <artifactId>pax-exam-container-native</artifactId>
       <version>${exam.version}</version>
       <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.ops4j.pax.exam</groupId>
-      <artifactId>pax-exam-link-mvn</artifactId>
+      <artifactId>pax-exam-link-assembly</artifactId>
       <version>${exam.version}</version>
       <scope>test</scope>
     </dependency>
-    <dependency>
-      <groupId>org.ops4j.pax.url</groupId>
-      <artifactId>pax-url-wrap</artifactId>
-      <version>2.4.7</version>
-    </dependency>
-
-    <dependency>
-      <groupId>org.osgi</groupId>
-      <artifactId>org.osgi.core</artifactId>
-      <version>6.0.0</version>
-      <scope>test</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.apache.felix</groupId>
-      <artifactId>org.apache.felix.framework</artifactId>
-      <version>5.6.10</version>
-      <scope>test</scope>
-    </dependency>
-
   </dependencies>
 
   <build>
     <plugins>
       <plugin>
-        <groupId>org.ops4j.pax.exam</groupId>
-        <artifactId>maven-paxexam-plugin</artifactId>
+        <groupId>com.github.veithen.alta</groupId>
+        <artifactId>alta-maven-plugin</artifactId>
+        <version>0.6.2</version>
         <executions>
           <execution>
-            <id>generate-config</id>
             <goals>
-              <goal>generate-depends-file</goal>
+              <goal>generate-test-resources</goal>
             </goals>
+            <configuration>
+              <name>%bundle.symbolicName%.link</name>
+              <value>%url%</value>
+              <dependencySet>
+                <scope>test</scope>
+              </dependencySet>
+            </configuration>
           </execution>
         </executions>
       </plugin>
@@ -233,6 +249,9 @@
         <artifactId>maven-surefire-plugin</artifactId>
         <configuration>
           <skip>${skipOsgiTestsuite}</skip>
+          <additionalClasspathElements>
+            <additionalClasspathElement>${project.build.directory}/generated-test-resources/alta</additionalClasspathElement>
+          </additionalClasspathElements>
         </configuration>
       </plugin>
     </plugins>
diff --git a/testsuite-osgi/src/test/java/io/netty/osgitests/OsgiBundleTest.java b/testsuite-osgi/src/test/java/io/netty/osgitests/OsgiBundleTest.java
index b56cd6c..aa86ffc 100644
--- a/testsuite-osgi/src/test/java/io/netty/osgitests/OsgiBundleTest.java
+++ b/testsuite-osgi/src/test/java/io/netty/osgitests/OsgiBundleTest.java
@@ -19,21 +19,16 @@ package io.netty.osgitests;
 import static org.junit.Assert.assertFalse;
 import static org.ops4j.pax.exam.CoreOptions.frameworkProperty;
 import static org.ops4j.pax.exam.CoreOptions.junitBundles;
-import static org.ops4j.pax.exam.CoreOptions.mavenBundle;
 import static org.ops4j.pax.exam.CoreOptions.systemProperty;
-import static org.ops4j.pax.exam.CoreOptions.wrappedBundle;
+import static org.ops4j.pax.exam.CoreOptions.url;
 import static org.osgi.framework.Constants.FRAMEWORK_BOOTDELEGATION;
 
-import java.io.BufferedReader;
 import java.io.File;
-import java.io.FileReader;
-import java.io.IOException;
+import java.io.FilenameFilter;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.Set;
-import java.util.regex.Pattern;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -44,45 +39,25 @@ import io.netty.util.internal.PlatformDependent;
 
 @RunWith(PaxExam.class)
 public class OsgiBundleTest {
-    private static final Pattern SLASH = Pattern.compile("/", Pattern.LITERAL);
-    private static final String DEPENCIES_LINE = "# dependencies";
-    private static final String GROUP = "io.netty";
-    private static final Collection<String> BUNDLES;
+    private static final Collection<String> LINKS;
 
     static {
-        final Set<String> artifacts = new HashSet<String>();
-        final File f = new File("target/classes/META-INF/maven/dependencies.properties");
-        try {
-            final BufferedReader r = new BufferedReader(new FileReader(f));
-            try {
-                boolean haveDeps = false;
+        final Set<String> links = new HashSet<String>();
 
-                while (true) {
-                    final String line = r.readLine();
-                    if (line == null) {
-                        // End-of-file
-                        break;
-                    }
-
-                    // We need to ignore any lines up to the dependencies
-                    // line, otherwise we would include ourselves.
-                    if (DEPENCIES_LINE.equals(line)) {
-                        haveDeps = true;
-                    } else if (haveDeps && line.startsWith(GROUP)) {
-                        final String[] split = SLASH.split(line);
-                        if (split.length > 1) {
-                            artifacts.add(split[1]);
-                        }
-                    }
-                }
-            } finally {
-                r.close();
+        final File directory = new File("target/generated-test-resources/alta/");
+        File[] files = directory.listFiles(new FilenameFilter() {
+            @Override
+            public boolean accept(File dir, String name) {
+                return (name.startsWith("io.netty") || name.startsWith("com.barchart.udt")) && name.endsWith(".link");
             }
-        } catch (IOException e) {
-            throw new ExceptionInInitializerError(e);
+        });
+        if (files == null) {
+            throw new IllegalStateException(directory + " is not found or is not a directory");
         }
-
-        BUNDLES = artifacts;
+        for (File f: files) {
+            links.add(f.getName());
+        }
+        LINKS = links;
     }
 
     @Configuration
@@ -92,13 +67,10 @@ public class OsgiBundleTest {
         // Avoid boot delegating sun.misc which would fail testCanLoadPlatformDependent()
         options.add(frameworkProperty(FRAMEWORK_BOOTDELEGATION).value("com.sun.*"));
         options.add(systemProperty("pax.exam.osgi.unresolved.fail").value("true"));
-        options.addAll(Arrays.asList(junitBundles()));
-
-        options.add(mavenBundle("com.barchart.udt", "barchart-udt-bundle").versionAsInProject());
-        options.add(wrappedBundle(mavenBundle("org.rxtx", "rxtx").versionAsInProject()));
+        options.add(junitBundles());
 
-        for (String name : BUNDLES) {
-            options.add(mavenBundle(GROUP, name).versionAsInProject());
+        for (String link : LINKS) {
+            options.add(url("link:classpath:" + link));
         }
 
         return options.toArray(new Option[0]);
@@ -107,11 +79,11 @@ public class OsgiBundleTest {
     @Test
     public void testResolvedBundles() {
         // No-op, as we just want the bundles to be resolved. Just check if we tested something
-        assertFalse("At least one bundle needs to be tested", BUNDLES.isEmpty());
+        assertFalse("At least one bundle needs to be tested", LINKS.isEmpty());
     }
 
     @Test
     public void testCanLoadPlatformDependent() {
-        assertFalse(PlatformDependent.hasUnsafe());
+        assertFalse(PlatformDependent.addressSize() == 0);
     }
 }
diff --git a/testsuite-shading/pom.xml b/testsuite-shading/pom.xml
index 890105e..3ea27d9 100644
--- a/testsuite-shading/pom.xml
+++ b/testsuite-shading/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>io.netty</groupId>
     <artifactId>netty-parent</artifactId>
-    <version>4.1.33.Final</version>
+    <version>4.1.48.Final</version>
   </parent>
 
   <artifactId>netty-testsuite-shading</artifactId>
@@ -38,6 +38,7 @@
 
     <jarName>${project.artifactId}-${project.version}.jar</jarName>
     <shadedPackagePrefix>io.netty.</shadedPackagePrefix>
+    <skipJapicmp>true</skipJapicmp>
   </properties>
 
   <build>
@@ -65,6 +66,17 @@
     </dependency>
   </dependencies>
   <profiles>
+    <profile>
+      <id>skipTests</id>
+      <activation>
+        <property>
+          <name>skipTests</name>
+        </property>
+      </activation>
+      <properties>
+        <skipShadingTestsuite>true</skipShadingTestsuite>
+      </properties>
+    </profile>
     <profile>
       <id>windows</id>
       <activation>
@@ -192,6 +204,7 @@
                   <goal>run</goal>
                 </goals>
                 <configuration>
+                  <skip>${skipShadingTestsuite}</skip>
                   <target>
                     <unzip dest="${classesShadedDir}/">
                       <fileset dir="${project.build.directory}/">
@@ -221,6 +234,7 @@
             <groupId>org.apache.maven.plugins</groupId>
             <artifactId>maven-failsafe-plugin</artifactId>
             <configuration>
+              <skip>${skipShadingTestsuite}</skip>
               <systemPropertyVariables>
                 <shadingPrefix>${shadingPrefix}</shadingPrefix>
                 <shadingPrefix2>${shadingPrefix2}</shadingPrefix2>
@@ -336,6 +350,7 @@
                   <goal>run</goal>
                 </goals>
                 <configuration>
+                  <skip>${skipShadingTestsuite}</skip>
                   <target>
                     <unzip dest="${classesShadedDir}/">
                       <fileset dir="${project.build.directory}/">
@@ -365,6 +380,7 @@
             <groupId>org.apache.maven.plugins</groupId>
             <artifactId>maven-failsafe-plugin</artifactId>
             <configuration>
+              <skip>${skipShadingTestsuite}</skip>
               <systemPropertyVariables>
                 <shadingPrefix>${shadingPrefix}</shadingPrefix>
                 <shadingPrefix2>${shadingPrefix2}</shadingPrefix2>
diff --git a/testsuite/pom.xml b/testsuite/pom.xml
index 366f620..fd5ba93 100644
--- a/testsuite/pom.xml
+++ b/testsuite/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>io.netty</groupId>
     <artifactId>netty-parent</artifactId>
-    <version>4.1.33.Final</version>
+    <version>4.1.48.Final</version>
   </parent>
 
   <artifactId>netty-testsuite</artifactId>
@@ -104,6 +104,7 @@
   <properties>
     <!-- Needed for SSL tests as these use the SelfSignedCertificate --> 
     <argLine.java9.extras>--add-exports java.base/sun.security.x509=ALL-UNNAMED</argLine.java9.extras>
+    <skipJapicmp>true</skipJapicmp>
   </properties>
 
   <build>
diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/AbstractSingleThreadEventLoopTest.java b/testsuite/src/main/java/io/netty/testsuite/transport/AbstractSingleThreadEventLoopTest.java
new file mode 100644
index 0000000..80442c9
--- /dev/null
+++ b/testsuite/src/main/java/io/netty/testsuite/transport/AbstractSingleThreadEventLoopTest.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2019 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.testsuite.transport;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.Test;
+
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.channel.EventLoop;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.ServerChannel;
+import io.netty.channel.SingleThreadEventLoop;
+import io.netty.channel.local.LocalAddress;
+import io.netty.channel.local.LocalServerChannel;
+import io.netty.util.concurrent.EventExecutor;
+import io.netty.util.concurrent.Future;
+
+public abstract class AbstractSingleThreadEventLoopTest {
+
+    @Test
+    public void testChannelsRegistered() throws Exception {
+        EventLoopGroup group = newEventLoopGroup();
+        final SingleThreadEventLoop loop = (SingleThreadEventLoop) group.next();
+
+        try {
+            final Channel ch1 = newChannel();
+            final Channel ch2 = newChannel();
+
+            int rc = registeredChannels(loop);
+            boolean channelCountSupported = rc != -1;
+
+            if (channelCountSupported) {
+                assertEquals(0, registeredChannels(loop));
+            }
+
+            assertTrue(loop.register(ch1).syncUninterruptibly().isSuccess());
+            assertTrue(loop.register(ch2).syncUninterruptibly().isSuccess());
+            if (channelCountSupported) {
+                assertEquals(2, registeredChannels(loop));
+            }
+
+            assertTrue(ch1.deregister().syncUninterruptibly().isSuccess());
+            if (channelCountSupported) {
+                assertEquals(1, registeredChannels(loop));
+            }
+        } finally {
+            group.shutdownGracefully();
+        }
+    }
+
+    // Only reliable if run from event loop
+    private static int registeredChannels(final SingleThreadEventLoop loop) throws Exception {
+        return loop.submit(new Callable<Integer>() {
+            @Override
+            public Integer call() {
+                return loop.registeredChannels();
+            }
+        }).get(1, TimeUnit.SECONDS);
+    }
+
+    @Test
+    @SuppressWarnings("deprecation")
+    public void shutdownBeforeStart() throws Exception {
+        EventLoopGroup group = newEventLoopGroup();
+        assertFalse(group.awaitTermination(2, TimeUnit.MILLISECONDS));
+        group.shutdown();
+        assertTrue(group.awaitTermination(200, TimeUnit.MILLISECONDS));
+    }
+
+    @Test
+    public void shutdownGracefullyZeroQuietBeforeStart() throws Exception {
+        EventLoopGroup group = newEventLoopGroup();
+        assertTrue(group.shutdownGracefully(0L, 2L, TimeUnit.SECONDS).await(200L));
+    }
+
+    // Copied from AbstractEventLoopTest
+    @Test(timeout = 5000)
+    public void testShutdownGracefullyNoQuietPeriod() throws Exception {
+        EventLoopGroup loop = newEventLoopGroup();
+        ServerBootstrap b = new ServerBootstrap();
+        b.group(loop)
+        .channel(serverChannelClass())
+        .childHandler(new ChannelInboundHandlerAdapter());
+
+        // Not close the Channel to ensure the EventLoop is still shutdown in time.
+        ChannelFuture cf = serverChannelClass() == LocalServerChannel.class
+                ? b.bind(new LocalAddress("local")) : b.bind(0);
+        cf.sync().channel();
+
+        Future<?> f = loop.shutdownGracefully(0, 1, TimeUnit.MINUTES);
+        assertTrue(loop.awaitTermination(600, TimeUnit.MILLISECONDS));
+        assertTrue(f.syncUninterruptibly().isSuccess());
+        assertTrue(loop.isShutdown());
+        assertTrue(loop.isTerminated());
+    }
+
+    @Test
+    public void shutdownGracefullyBeforeStart() throws Exception {
+        EventLoopGroup group = newEventLoopGroup();
+        assertTrue(group.shutdownGracefully(200L, 1000L, TimeUnit.MILLISECONDS).await(500L));
+    }
+
+    @Test
+    public void gracefulShutdownAfterStart() throws Exception {
+        EventLoop loop = newEventLoopGroup().next();
+        final CountDownLatch latch = new CountDownLatch(1);
+        loop.execute(new Runnable() {
+            @Override
+            public void run() {
+                latch.countDown();
+            }
+        });
+
+        // Wait for the event loop thread to start.
+        latch.await();
+
+        // Request the event loop thread to stop.
+        loop.shutdownGracefully(200L, 3000L, TimeUnit.MILLISECONDS);
+
+        // Wait until the event loop is terminated.
+        assertTrue(loop.awaitTermination(500L, TimeUnit.MILLISECONDS));
+
+        assertRejection(loop);
+    }
+
+    private static final Runnable NOOP = new Runnable() {
+        @Override
+        public void run() { }
+    };
+
+    private static void assertRejection(EventExecutor loop) {
+        try {
+            loop.execute(NOOP);
+            fail("A task must be rejected after shutdown() is called.");
+        } catch (RejectedExecutionException e) {
+            // Expected
+        }
+    }
+
+    protected abstract EventLoopGroup newEventLoopGroup();
+    protected abstract Channel newChannel();
+    protected abstract Class<? extends ServerChannel> serverChannelClass();
+}
diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/DefaultEventLoopTest.java b/testsuite/src/main/java/io/netty/testsuite/transport/DefaultEventLoopTest.java
new file mode 100644
index 0000000..cb13b80
--- /dev/null
+++ b/testsuite/src/main/java/io/netty/testsuite/transport/DefaultEventLoopTest.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2019 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.testsuite.transport;
+
+import io.netty.channel.Channel;
+import io.netty.channel.DefaultEventLoopGroup;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.ServerChannel;
+import io.netty.channel.local.LocalChannel;
+import io.netty.channel.local.LocalServerChannel;
+
+public class DefaultEventLoopTest extends AbstractSingleThreadEventLoopTest {
+
+    @Override
+    protected EventLoopGroup newEventLoopGroup() {
+        return new DefaultEventLoopGroup();
+    }
+
+    @Override
+    protected Channel newChannel() {
+        return new LocalChannel();
+    }
+
+    @Override
+    protected Class<? extends ServerChannel> serverChannelClass() {
+        return LocalServerChannel.class;
+    }
+}
diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/NioEventLoopTest.java b/testsuite/src/main/java/io/netty/testsuite/transport/NioEventLoopTest.java
new file mode 100644
index 0000000..e4bc928
--- /dev/null
+++ b/testsuite/src/main/java/io/netty/testsuite/transport/NioEventLoopTest.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2019 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.testsuite.transport;
+
+import io.netty.channel.Channel;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.ServerChannel;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.channel.socket.nio.NioSocketChannel;
+
+public class NioEventLoopTest extends AbstractSingleThreadEventLoopTest {
+
+    @Override
+    protected EventLoopGroup newEventLoopGroup() {
+        return new NioEventLoopGroup();
+    }
+
+    @Override
+    protected Channel newChannel() {
+        return new NioSocketChannel();
+    }
+
+    @Override
+    protected Class<? extends ServerChannel> serverChannelClass() {
+        return NioServerSocketChannel.class;
+    }
+}
diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/socket/AbstractDatagramTest.java b/testsuite/src/main/java/io/netty/testsuite/transport/socket/AbstractDatagramTest.java
index 2d19f3d..ebf597b 100644
--- a/testsuite/src/main/java/io/netty/testsuite/transport/socket/AbstractDatagramTest.java
+++ b/testsuite/src/main/java/io/netty/testsuite/transport/socket/AbstractDatagramTest.java
@@ -18,6 +18,7 @@ package io.netty.testsuite.transport.socket;
 import io.netty.bootstrap.Bootstrap;
 import io.netty.buffer.ByteBufAllocator;
 import io.netty.channel.ChannelOption;
+import io.netty.channel.socket.InternetProtocolFamily;
 import io.netty.testsuite.transport.AbstractComboTestsuiteTest;
 import io.netty.testsuite.transport.TestsuitePermutation;
 import io.netty.util.NetUtil;
@@ -34,7 +35,7 @@ public abstract class AbstractDatagramTest extends AbstractComboTestsuiteTest<Bo
 
     @Override
     protected List<TestsuitePermutation.BootstrapComboFactory<Bootstrap, Bootstrap>> newFactories() {
-        return SocketTestPermutation.INSTANCE.datagram();
+        return SocketTestPermutation.INSTANCE.datagram(internetProtocolFamily());
     }
 
     @Override
@@ -44,11 +45,17 @@ public abstract class AbstractDatagramTest extends AbstractComboTestsuiteTest<Bo
     }
 
     protected SocketAddress newSocketAddress() {
-        // We use LOCALHOST4 as we use InternetProtocolFamily.IPv4 when creating the DatagramChannel and its
-        // not supported to bind to and IPV6 address in this case.
-        //
-        // See also http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/e74259b3eadc/
-        // src/share/classes/sun/nio/ch/DatagramChannelImpl.java#l684
-        return new InetSocketAddress(NetUtil.LOCALHOST4, 0);
+        switch (internetProtocolFamily()) {
+            case IPv4:
+                return new InetSocketAddress(NetUtil.LOCALHOST4, 0);
+            case IPv6:
+                return new InetSocketAddress(NetUtil.LOCALHOST6, 0);
+            default:
+                throw new AssertionError();
+        }
+    }
+
+    protected InternetProtocolFamily internetProtocolFamily() {
+        return InternetProtocolFamily.IPv4;
     }
 }
diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/socket/AbstractSocketReuseFdTest.java b/testsuite/src/main/java/io/netty/testsuite/transport/socket/AbstractSocketReuseFdTest.java
new file mode 100644
index 0000000..6e8ae87
--- /dev/null
+++ b/testsuite/src/main/java/io/netty/testsuite/transport/socket/AbstractSocketReuseFdTest.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2019 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.testsuite.transport.socket;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelOption;
+import io.netty.testsuite.transport.TestsuitePermutation;
+import io.netty.util.CharsetUtil;
+import io.netty.util.concurrent.ImmediateEventExecutor;
+import io.netty.util.concurrent.Promise;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.net.SocketAddress;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+public abstract class AbstractSocketReuseFdTest extends AbstractSocketTest {
+    @Override
+    protected abstract SocketAddress newSocketAddress();
+
+    @Override
+    protected abstract List<TestsuitePermutation.BootstrapComboFactory<ServerBootstrap, Bootstrap>> newFactories();
+
+    @Test(timeout = 60000)
+    public void testReuseFd() throws Throwable {
+        run();
+    }
+
+    public void testReuseFd(ServerBootstrap sb, Bootstrap cb) throws Throwable {
+        sb.childOption(ChannelOption.AUTO_READ, true);
+        cb.option(ChannelOption.AUTO_READ, true);
+
+        // Use a number which will typically not exceed /proc/sys/net/core/somaxconn (which is 128 on linux by default
+        // often).
+        int numChannels = 100;
+        final AtomicReference<Throwable> globalException = new AtomicReference<Throwable>();
+        final AtomicInteger serverRemaining = new AtomicInteger(numChannels);
+        final AtomicInteger clientRemaining = new AtomicInteger(numChannels);
+        final Promise<Void> serverDonePromise = ImmediateEventExecutor.INSTANCE.newPromise();
+        final Promise<Void> clientDonePromise = ImmediateEventExecutor.INSTANCE.newPromise();
+
+        sb.childHandler(new ChannelInitializer<Channel>() {
+            @Override
+            public void initChannel(Channel sch) {
+                ReuseFdHandler sh = new ReuseFdHandler(
+                    false,
+                    globalException,
+                    serverRemaining,
+                    serverDonePromise);
+                sch.pipeline().addLast("handler", sh);
+            }
+        });
+
+        cb.handler(new ChannelInitializer<Channel>() {
+            @Override
+            public void initChannel(Channel sch) {
+                ReuseFdHandler ch = new ReuseFdHandler(
+                    true,
+                    globalException,
+                    clientRemaining,
+                    clientDonePromise);
+                sch.pipeline().addLast("handler", ch);
+            }
+        });
+
+        ChannelFutureListener listener = new ChannelFutureListener() {
+            @Override
+            public void operationComplete(ChannelFuture future) {
+                if (!future.isSuccess()) {
+                    clientDonePromise.tryFailure(future.cause());
+                }
+            }
+        };
+
+        Channel sc = sb.bind().sync().channel();
+        for (int i = 0; i < numChannels; i++) {
+            cb.connect(sc.localAddress()).addListener(listener);
+        }
+
+        clientDonePromise.sync();
+        serverDonePromise.sync();
+        sc.close().sync();
+
+        if (globalException.get() != null && !(globalException.get() instanceof IOException)) {
+            throw globalException.get();
+        }
+    }
+
+    static class ReuseFdHandler extends ChannelInboundHandlerAdapter {
+        private static final String EXPECTED_PAYLOAD = "payload";
+
+        private final Promise<Void> donePromise;
+        private final AtomicInteger remaining;
+        private final boolean client;
+        volatile Channel channel;
+        final AtomicReference<Throwable> globalException;
+        final AtomicReference<Throwable> exception = new AtomicReference<Throwable>();
+        final StringBuilder received = new StringBuilder();
+
+        ReuseFdHandler(
+            boolean client,
+            AtomicReference<Throwable> globalException,
+            AtomicInteger remaining,
+            Promise<Void> donePromise) {
+            this.client = client;
+            this.globalException = globalException;
+            this.remaining = remaining;
+            this.donePromise = donePromise;
+        }
+
+        @Override
+        public void channelActive(ChannelHandlerContext ctx) {
+            channel = ctx.channel();
+            if (client) {
+                ctx.writeAndFlush(Unpooled.copiedBuffer(EXPECTED_PAYLOAD, CharsetUtil.US_ASCII));
+            }
+        }
+
+        @Override
+        public void channelRead(ChannelHandlerContext ctx, Object msg) {
+            if (msg instanceof ByteBuf) {
+                ByteBuf buf = (ByteBuf) msg;
+                received.append(buf.toString(CharsetUtil.US_ASCII));
+                buf.release();
+
+                if (received.toString().equals(EXPECTED_PAYLOAD)) {
+                    if (client) {
+                        ctx.close();
+                    } else {
+                        ctx.writeAndFlush(Unpooled.copiedBuffer(EXPECTED_PAYLOAD, CharsetUtil.US_ASCII));
+                    }
+                }
+            }
+        }
+
+        @Override
+        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+            if (exception.compareAndSet(null, cause)) {
+                donePromise.tryFailure(new IllegalStateException("exceptionCaught: " + ctx.channel(), cause));
+                ctx.close();
+            }
+            globalException.compareAndSet(null, cause);
+        }
+
+        @Override
+        public void channelInactive(ChannelHandlerContext ctx) {
+            if (remaining.decrementAndGet() == 0) {
+                if (received.toString().equals(EXPECTED_PAYLOAD)) {
+                    donePromise.setSuccess(null);
+                } else {
+                    donePromise.tryFailure(new Exception("Unexpected payload:" + received));
+                }
+            }
+        }
+    }
+}
diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/socket/AbstractSocketShutdownOutputByPeerTest.java b/testsuite/src/main/java/io/netty/testsuite/transport/socket/AbstractSocketShutdownOutputByPeerTest.java
new file mode 100644
index 0000000..6e895a4
--- /dev/null
+++ b/testsuite/src/main/java/io/netty/testsuite/transport/socket/AbstractSocketShutdownOutputByPeerTest.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2019 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.testsuite.transport.socket;
+
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.channel.socket.ChannelInputShutdownEvent;
+import io.netty.channel.socket.DuplexChannel;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.net.SocketAddress;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.Assert.*;
+
+public abstract class AbstractSocketShutdownOutputByPeerTest<Socket> extends AbstractServerSocketTest {
+
+    @Test(timeout = 30000)
+    public void testShutdownOutput() throws Throwable {
+        run();
+    }
+
+    public void testShutdownOutput(ServerBootstrap sb) throws Throwable {
+        TestHandler h = new TestHandler();
+        Socket s = newSocket();
+        Channel sc = null;
+        try {
+            sc = sb.childHandler(h).childOption(ChannelOption.ALLOW_HALF_CLOSURE, true).bind().sync().channel();
+
+            connect(s, sc.localAddress());
+            write(s, 1);
+
+            assertEquals(1, (int) h.queue.take());
+
+            assertTrue(h.ch.isOpen());
+            assertTrue(h.ch.isActive());
+            assertFalse(h.ch.isInputShutdown());
+            assertFalse(h.ch.isOutputShutdown());
+
+            shutdownOutput(s);
+
+            h.halfClosure.await();
+
+            assertTrue(h.ch.isOpen());
+            assertTrue(h.ch.isActive());
+            assertTrue(h.ch.isInputShutdown());
+            assertFalse(h.ch.isOutputShutdown());
+
+            while (h.closure.getCount() != 1 && h.halfClosureCount.intValue() != 1) {
+                Thread.sleep(100);
+            }
+        } finally {
+            if (sc != null) {
+                sc.close();
+            }
+            close(s);
+        }
+    }
+
+    @Test(timeout = 30000)
+    public void testShutdownOutputWithoutOption() throws Throwable {
+        run();
+    }
+
+    public void testShutdownOutputWithoutOption(ServerBootstrap sb) throws Throwable {
+        TestHandler h = new TestHandler();
+        Socket s = newSocket();
+        Channel sc = null;
+        try {
+            sc = sb.childHandler(h).bind().sync().channel();
+
+            connect(s, sc.localAddress());
+            write(s, 1);
+
+            assertEquals(1, (int) h.queue.take());
+
+            assertTrue(h.ch.isOpen());
+            assertTrue(h.ch.isActive());
+            assertFalse(h.ch.isInputShutdown());
+            assertFalse(h.ch.isOutputShutdown());
+
+            shutdownOutput(s);
+
+            h.closure.await();
+
+            assertFalse(h.ch.isOpen());
+            assertFalse(h.ch.isActive());
+            assertTrue(h.ch.isInputShutdown());
+            assertTrue(h.ch.isOutputShutdown());
+
+            while (h.halfClosure.getCount() != 1 && h.halfClosureCount.intValue() != 0) {
+                Thread.sleep(100);
+            }
+        } finally {
+            if (sc != null) {
+                sc.close();
+            }
+            close(s);
+        }
+    }
+
+    protected abstract void shutdownOutput(Socket s) throws IOException;
+
+    protected abstract void connect(Socket s, SocketAddress address) throws IOException;
+
+    protected abstract void close(Socket s) throws IOException;
+
+    protected abstract void write(Socket s, int data) throws IOException;
+
+    protected abstract Socket newSocket();
+
+    private static class TestHandler extends SimpleChannelInboundHandler<ByteBuf> {
+        volatile DuplexChannel ch;
+        final BlockingQueue<Byte> queue = new LinkedBlockingQueue<Byte>();
+        final CountDownLatch halfClosure = new CountDownLatch(1);
+        final CountDownLatch closure = new CountDownLatch(1);
+        final AtomicInteger halfClosureCount = new AtomicInteger();
+
+        @Override
+        public void channelActive(ChannelHandlerContext ctx) throws Exception {
+            ch = (DuplexChannel) ctx.channel();
+        }
+
+        @Override
+        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
+            closure.countDown();
+        }
+
+        @Override
+        public void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
+            queue.offer(msg.readByte());
+        }
+
+        @Override
+        public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
+            if (evt instanceof ChannelInputShutdownEvent) {
+                halfClosureCount.incrementAndGet();
+                halfClosure.countDown();
+            }
+        }
+    }
+}
diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/socket/CompositeBufferGatheringWriteTest.java b/testsuite/src/main/java/io/netty/testsuite/transport/socket/CompositeBufferGatheringWriteTest.java
index 3538d8b..c8c6132 100644
--- a/testsuite/src/main/java/io/netty/testsuite/transport/socket/CompositeBufferGatheringWriteTest.java
+++ b/testsuite/src/main/java/io/netty/testsuite/transport/socket/CompositeBufferGatheringWriteTest.java
@@ -27,7 +27,6 @@ import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelInboundHandlerAdapter;
 import io.netty.channel.ChannelInitializer;
 import io.netty.channel.ChannelOption;
-import io.netty.channel.socket.nio.NioSocketChannel;
 import io.netty.util.ReferenceCountUtil;
 import org.junit.Test;
 
diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramMulticastIPv6Test.java b/testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramMulticastIPv6Test.java
new file mode 100644
index 0000000..aa85e65
--- /dev/null
+++ b/testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramMulticastIPv6Test.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2019 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.testsuite.transport.socket;
+
+import io.netty.channel.socket.InternetProtocolFamily;
+
+public class DatagramMulticastIPv6Test extends DatagramMulticastTest {
+
+    @Override
+    protected InternetProtocolFamily internetProtocolFamily() {
+        return InternetProtocolFamily.IPv6;
+    }
+}
diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramMulticastTest.java b/testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramMulticastTest.java
index 881e9a5..6ea3209 100644
--- a/testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramMulticastTest.java
+++ b/testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramMulticastTest.java
@@ -17,18 +17,26 @@ package io.netty.testsuite.transport.socket;
 
 import io.netty.bootstrap.Bootstrap;
 import io.netty.buffer.Unpooled;
-import io.netty.channel.Channel;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelOption;
 import io.netty.channel.SimpleChannelInboundHandler;
 import io.netty.channel.socket.DatagramChannel;
 import io.netty.channel.socket.DatagramPacket;
+import io.netty.channel.socket.InternetProtocolFamily;
 import io.netty.channel.socket.oio.OioDatagramChannel;
-import io.netty.util.NetUtil;
+import io.netty.testsuite.transport.TestsuitePermutation;
 import io.netty.util.internal.SocketUtils;
+import org.junit.Assume;
 import org.junit.Test;
 
+import java.io.IOException;
+import java.net.InetAddress;
 import java.net.InetSocketAddress;
+import java.net.MulticastSocket;
+import java.net.NetworkInterface;
+import java.net.UnknownHostException;
+import java.util.Enumeration;
+import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
@@ -42,6 +50,10 @@ public class DatagramMulticastTest extends AbstractDatagramTest {
     }
 
     public void testMulticast(Bootstrap sb, Bootstrap cb) throws Throwable {
+        NetworkInterface iface = multicastNetworkInterface();
+        Assume.assumeNotNull("No NetworkInterface found that supports multicast and " +
+                internetProtocolFamily(), iface);
+
         MulticastTestHandler mhandler = new MulticastTestHandler();
 
         sb.handler(new SimpleChannelInboundHandler<Object>() {
@@ -53,14 +65,17 @@ public class DatagramMulticastTest extends AbstractDatagramTest {
 
         cb.handler(mhandler);
 
-        sb.option(ChannelOption.IP_MULTICAST_IF, NetUtil.LOOPBACK_IF);
+        sb.option(ChannelOption.IP_MULTICAST_IF, iface);
         sb.option(ChannelOption.SO_REUSEADDR, true);
-        cb.option(ChannelOption.IP_MULTICAST_IF, NetUtil.LOOPBACK_IF);
+
+        cb.option(ChannelOption.IP_MULTICAST_IF, iface);
         cb.option(ChannelOption.SO_REUSEADDR, true);
 
-        Channel sc = sb.bind(newSocketAddress()).sync().channel();
+        DatagramChannel sc = (DatagramChannel) sb.bind(newSocketAddress(iface)).sync().channel();
+        assertEquals(iface, sc.config().getNetworkInterface());
+        assertInterfaceAddress(iface, sc.config().getInterface());
 
-        InetSocketAddress addr = (InetSocketAddress) sc.localAddress();
+        InetSocketAddress addr = sc.localAddress();
         cb.localAddress(addr.getPort());
 
         if (sc instanceof OioDatagramChannel) {
@@ -71,17 +86,18 @@ public class DatagramMulticastTest extends AbstractDatagramTest {
             return;
         }
         DatagramChannel cc = (DatagramChannel) cb.bind().sync().channel();
+        assertEquals(iface, cc.config().getNetworkInterface());
+        assertInterfaceAddress(iface, cc.config().getInterface());
 
-        String group = "230.0.0.1";
-        InetSocketAddress groupAddress = SocketUtils.socketAddress(group, addr.getPort());
+        InetSocketAddress groupAddress = SocketUtils.socketAddress(groupAddress(), addr.getPort());
 
-        cc.joinGroup(groupAddress, NetUtil.LOOPBACK_IF).sync();
+        cc.joinGroup(groupAddress, iface).sync();
 
         sc.writeAndFlush(new DatagramPacket(Unpooled.copyInt(1), groupAddress)).sync();
         assertTrue(mhandler.await());
 
         // leave the group
-        cc.leaveGroup(groupAddress, NetUtil.LOOPBACK_IF).sync();
+        cc.leaveGroup(groupAddress, iface).sync();
 
         // sleep a second to make sure we left the group
         Thread.sleep(1000);
@@ -90,10 +106,32 @@ public class DatagramMulticastTest extends AbstractDatagramTest {
         sc.writeAndFlush(new DatagramPacket(Unpooled.copyInt(1), groupAddress)).sync();
         mhandler.await();
 
+        cc.config().setLoopbackModeDisabled(false);
+        sc.config().setLoopbackModeDisabled(false);
+
+        assertFalse(cc.config().isLoopbackModeDisabled());
+        assertFalse(sc.config().isLoopbackModeDisabled());
+
+        cc.config().setLoopbackModeDisabled(true);
+        sc.config().setLoopbackModeDisabled(true);
+
+        assertTrue(cc.config().isLoopbackModeDisabled());
+        assertTrue(sc.config().isLoopbackModeDisabled());
+
         sc.close().awaitUninterruptibly();
         cc.close().awaitUninterruptibly();
     }
 
+    private static void assertInterfaceAddress(NetworkInterface networkInterface, InetAddress expected) {
+        Enumeration<InetAddress> addresses = networkInterface.getInetAddresses();
+        while (addresses.hasMoreElements()) {
+            if (expected.equals(addresses.nextElement())) {
+                return;
+            }
+        }
+        fail();
+    }
+
     private static final class MulticastTestHandler extends SimpleChannelInboundHandler<DatagramPacket> {
         private final CountDownLatch latch = new CountDownLatch(1);
 
@@ -123,4 +161,64 @@ public class DatagramMulticastTest extends AbstractDatagramTest {
             return success;
         }
     }
+
+    @Override
+    protected List<TestsuitePermutation.BootstrapComboFactory<Bootstrap, Bootstrap>> newFactories() {
+        return SocketTestPermutation.INSTANCE.datagram(internetProtocolFamily());
+    }
+
+    private InetSocketAddress newAnySocketAddress() throws UnknownHostException {
+        switch (internetProtocolFamily()) {
+            case IPv4:
+                return new InetSocketAddress(InetAddress.getByName("0.0.0.0"), 0);
+            case IPv6:
+                return new InetSocketAddress(InetAddress.getByName("::"), 0);
+            default:
+                throw new AssertionError();
+        }
+    }
+
+    private InetSocketAddress newSocketAddress(NetworkInterface iface) {
+        Enumeration<InetAddress> addresses = iface.getInetAddresses();
+        while (addresses.hasMoreElements()) {
+            InetAddress address = addresses.nextElement();
+            if (internetProtocolFamily().addressType().isAssignableFrom(address.getClass())) {
+                return new InetSocketAddress(address, 0);
+            }
+        }
+        throw new AssertionError();
+    }
+
+    private NetworkInterface multicastNetworkInterface() throws IOException {
+        Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
+        while (interfaces.hasMoreElements()) {
+            NetworkInterface iface = interfaces.nextElement();
+            if (iface.isUp() && iface.supportsMulticast()) {
+                Enumeration<InetAddress> addresses = iface.getInetAddresses();
+                while (addresses.hasMoreElements()) {
+                    InetAddress address = addresses.nextElement();
+                    if (internetProtocolFamily().addressType().isAssignableFrom(address.getClass())) {
+                        MulticastSocket socket = new MulticastSocket(newAnySocketAddress());
+                        socket.setReuseAddress(true);
+                        socket.setNetworkInterface(iface);
+                        try {
+                            socket.send(new java.net.DatagramPacket(new byte[] { 1, 2, 3, 4 }, 4,
+                                    new InetSocketAddress(groupAddress(), 12345)));
+                            return iface;
+                        } catch (IOException ignore) {
+                            // Try the next interface
+                        } finally {
+                            socket.close();
+                        }
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    private String groupAddress() {
+        return internetProtocolFamily() == InternetProtocolFamily.IPv4 ?
+                "230.0.0.1" : "FF01:0:0:0:0:0:0:101";
+    }
 }
diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramUnicastIPv6MappedTest.java b/testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramUnicastIPv6MappedTest.java
new file mode 100644
index 0000000..8abc9ca
--- /dev/null
+++ b/testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramUnicastIPv6MappedTest.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2019 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.testsuite.transport.socket;
+
+import io.netty.util.NetUtil;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+
+public class DatagramUnicastIPv6MappedTest extends DatagramUnicastIPv6Test {
+
+    @Override
+    protected SocketAddress newSocketAddress() {
+        return new InetSocketAddress(0);
+    }
+
+    @Override
+    protected InetSocketAddress sendToAddress(InetSocketAddress serverAddress) {
+        InetAddress addr = serverAddress.getAddress();
+        if (addr.isAnyLocalAddress()) {
+            return new InetSocketAddress(NetUtil.LOCALHOST4, serverAddress.getPort());
+        }
+        return serverAddress;
+    }
+}
diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramUnicastIPv6Test.java b/testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramUnicastIPv6Test.java
new file mode 100644
index 0000000..533384f
--- /dev/null
+++ b/testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramUnicastIPv6Test.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2019 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.testsuite.transport.socket;
+
+import io.netty.channel.socket.InternetProtocolFamily;
+import io.netty.util.internal.PlatformDependent;
+import io.netty.util.internal.SuppressJava6Requirement;
+import org.junit.Assume;
+import org.junit.BeforeClass;
+
+import java.io.IOException;
+import java.net.StandardProtocolFamily;
+import java.nio.channels.Channel;
+import java.nio.channels.spi.SelectorProvider;
+
+public class DatagramUnicastIPv6Test extends DatagramUnicastTest {
+
+    @SuppressJava6Requirement(reason = "Guarded by java version check")
+    @BeforeClass
+    public static void assumeIpv6Supported() {
+        try {
+            if (PlatformDependent.javaVersion() < 7) {
+                throw new UnsupportedOperationException();
+            }
+            Channel channel = SelectorProvider.provider().openDatagramChannel(StandardProtocolFamily.INET6);
+            channel.close();
+        } catch (UnsupportedOperationException e) {
+            Assume.assumeNoException("IPv6 not supported", e);
+        } catch (IOException ignore) {
+            // Ignore
+        }
+    }
+    @Override
+    protected InternetProtocolFamily internetProtocolFamily() {
+        return InternetProtocolFamily.IPv6;
+    }
+}
diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramUnicastTest.java b/testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramUnicastTest.java
index b49f678..aca8b60 100644
--- a/testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramUnicastTest.java
+++ b/testsuite/src/main/java/io/netty/testsuite/transport/socket/DatagramUnicastTest.java
@@ -27,12 +27,19 @@ import io.netty.channel.ChannelOption;
 import io.netty.channel.SimpleChannelInboundHandler;
 import io.netty.channel.socket.DatagramChannel;
 import io.netty.channel.socket.DatagramPacket;
+import io.netty.util.NetUtil;
 import org.junit.Test;
 
+import java.net.Inet6Address;
+import java.net.InetAddress;
 import java.net.InetSocketAddress;
+import java.net.SocketAddress;
 import java.nio.channels.NotYetConnectedException;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
 
 import static org.junit.Assert.*;
 
@@ -157,36 +164,38 @@ public class DatagramUnicastTest extends AbstractDatagramTest {
                 }
             });
 
-            final CountDownLatch latch = new CountDownLatch(count);
-            sc = setupServerChannel(sb, bytes, latch);
+            final SocketAddress sender;
             if (bindClient) {
                 cc = cb.bind(newSocketAddress()).sync().channel();
+                sender = cc.localAddress();
             } else {
                 cb.option(ChannelOption.DATAGRAM_CHANNEL_ACTIVE_ON_REGISTRATION, true);
                 cc = cb.register().sync().channel();
+                sender = null;
             }
-            InetSocketAddress addr = (InetSocketAddress) sc.localAddress();
+
+            final CountDownLatch latch = new CountDownLatch(count);
+            AtomicReference<Throwable> errorRef = new AtomicReference<Throwable>();
+            sc = setupServerChannel(sb, bytes, sender, latch, errorRef, false);
+
+            InetSocketAddress addr = sendToAddress((InetSocketAddress) sc.localAddress());
+            List<ChannelFuture> futures = new ArrayList<ChannelFuture>(count);
             for (int i = 0; i < count; i++) {
-                switch (wrapType) {
-                    case DUP:
-                        cc.write(new DatagramPacket(buf.retainedDuplicate(), addr));
-                        break;
-                    case SLICE:
-                        cc.write(new DatagramPacket(buf.retainedSlice(), addr));
-                        break;
-                    case READ_ONLY:
-                        cc.write(new DatagramPacket(buf.retain().asReadOnly(), addr));
-                        break;
-                    case NONE:
-                        cc.write(new DatagramPacket(buf.retain(), addr));
-                        break;
-                    default:
-                        throw new Error("unknown wrap type: " + wrapType);
-                }
+                futures.add(write(cc, buf, addr, wrapType));
             }
             // release as we used buf.retain() before
             cc.flush();
-            assertTrue(latch.await(10, TimeUnit.SECONDS));
+
+            for (ChannelFuture future: futures) {
+                future.sync();
+            }
+            if (!latch.await(10, TimeUnit.SECONDS)) {
+                Throwable error = errorRef.get();
+                if (error != null) {
+                    throw error;
+                }
+                fail();
+            }
         } finally {
             // release as we used buf.retain() before
             buf.release();
@@ -196,6 +205,20 @@ public class DatagramUnicastTest extends AbstractDatagramTest {
         }
     }
 
+    private static ChannelFuture write(Channel cc, ByteBuf buf, InetSocketAddress remote, WrapType wrapType) {
+        switch (wrapType) {
+            case DUP:
+                return cc.write(new DatagramPacket(buf.retainedDuplicate(), remote));
+            case SLICE:
+                return cc.write(new DatagramPacket(buf.retainedSlice(), remote));
+            case READ_ONLY:
+                return cc.write(new DatagramPacket(buf.retain().asReadOnly(), remote));
+            case NONE:
+                return cc.write(new DatagramPacket(buf.retain(), remote));
+            default:
+                throw new Error("unknown wrap type: " + wrapType);
+        }
+    }
     private void testSimpleSendWithConnect(Bootstrap sb, Bootstrap cb, ByteBuf buf, final byte[] bytes, int count)
             throws Throwable {
         try {
@@ -209,10 +232,33 @@ public class DatagramUnicastTest extends AbstractDatagramTest {
 
     private void testSimpleSendWithConnect0(Bootstrap sb, Bootstrap cb, ByteBuf buf, final byte[] bytes, int count,
                                             WrapType wrapType) throws Throwable {
-        cb.handler(new SimpleChannelInboundHandler<Object>() {
+        final CountDownLatch clientLatch = new CountDownLatch(count);
+        final AtomicReference<Throwable> clientErrorRef = new AtomicReference<Throwable>();
+        cb.handler(new SimpleChannelInboundHandler<DatagramPacket>() {
+            @Override
+            public void channelRead0(ChannelHandlerContext ctx, DatagramPacket msg) throws Exception {
+                try {
+                    ByteBuf buf = msg.content();
+                    assertEquals(bytes.length, buf.readableBytes());
+                    for (int i = 0; i < bytes.length; i++) {
+                        assertEquals(bytes[i], buf.getByte(buf.readerIndex() + i));
+                    }
+
+                    InetSocketAddress localAddress = (InetSocketAddress) ctx.channel().localAddress();
+                    if (localAddress.getAddress().isAnyLocalAddress()) {
+                        assertEquals(localAddress.getPort(), msg.recipient().getPort());
+                    } else {
+                        // Test that the channel's localAddress is equal to the message's recipient
+                        assertEquals(localAddress, msg.recipient());
+                    }
+                } finally {
+                    clientLatch.countDown();
+                }
+            }
+
             @Override
-            public void channelRead0(ChannelHandlerContext ctx, Object msgs) throws Exception {
-                // Nothing will be sent.
+            public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+                clientErrorRef.compareAndSet(null, cause);
             }
         });
 
@@ -220,35 +266,46 @@ public class DatagramUnicastTest extends AbstractDatagramTest {
         DatagramChannel cc = null;
         try {
             final CountDownLatch latch = new CountDownLatch(count);
-            sc = setupServerChannel(sb, bytes, latch);
-            cc = (DatagramChannel) cb.connect(sc.localAddress()).sync().channel();
+            final AtomicReference<Throwable> errorRef = new AtomicReference<Throwable>();
+            cc = (DatagramChannel) cb.bind(newSocketAddress()).sync().channel();
+            sc = setupServerChannel(sb, bytes, cc.localAddress(), latch, errorRef, true);
+
+            cc.connect(sendToAddress((InetSocketAddress) sc.localAddress())).syncUninterruptibly();
 
+            List<ChannelFuture> futures = new ArrayList<ChannelFuture>();
             for (int i = 0; i < count; i++) {
-                switch (wrapType) {
-                    case DUP:
-                        cc.write(buf.retainedDuplicate());
-                        break;
-                    case SLICE:
-                        cc.write(buf.retainedSlice());
-                        break;
-                    case READ_ONLY:
-                        cc.write(buf.retain().asReadOnly());
-                        break;
-                    case NONE:
-                        cc.write(buf.retain());
-                        break;
-                    default:
-                        throw new Error("unknown wrap type: " + wrapType);
-                }
+                futures.add(write(cc, buf, wrapType));
             }
             cc.flush();
-            assertTrue(latch.await(10, TimeUnit.SECONDS));
 
+            for (ChannelFuture future: futures) {
+                future.sync();
+            }
+
+            if (!latch.await(10, TimeUnit.SECONDS)) {
+                Throwable cause = errorRef.get();
+                if (cause != null) {
+                    throw cause;
+                }
+                fail();
+            }
+            if (!clientLatch.await(10, TimeUnit.SECONDS)) {
+                Throwable cause = clientErrorRef.get();
+                if (cause != null) {
+                    throw cause;
+                }
+                fail();
+            }
             assertTrue(cc.isConnected());
 
+            assertNotNull(cc.localAddress());
+            assertNotNull(cc.remoteAddress());
+
             // Test what happens when we call disconnect()
             cc.disconnect().syncUninterruptibly();
             assertFalse(cc.isConnected());
+            assertNotNull(cc.localAddress());
+            assertNull(cc.remoteAddress());
 
             ChannelFuture future = cc.writeAndFlush(
                     buf.retain().duplicate()).awaitUninterruptibly();
@@ -263,8 +320,25 @@ public class DatagramUnicastTest extends AbstractDatagramTest {
         }
     }
 
+    private static ChannelFuture write(Channel cc, ByteBuf buf, WrapType wrapType) {
+        switch (wrapType) {
+            case DUP:
+                return cc.write(buf.retainedDuplicate());
+            case SLICE:
+                return cc.write(buf.retainedSlice());
+            case READ_ONLY:
+                return cc.write(buf.retain().asReadOnly());
+            case NONE:
+                return cc.write(buf.retain());
+            default:
+                throw new Error("unknown wrap type: " + wrapType);
+        }
+    }
+
     @SuppressWarnings("deprecation")
-    private Channel setupServerChannel(Bootstrap sb, final byte[] bytes, final CountDownLatch latch)
+    private Channel setupServerChannel(Bootstrap sb, final byte[] bytes, final SocketAddress sender,
+                                       final CountDownLatch latch, final AtomicReference<Throwable> errorRef,
+                                       final boolean echo)
             throws Throwable {
         sb.handler(new ChannelInitializer<Channel>() {
             @Override
@@ -272,16 +346,38 @@ public class DatagramUnicastTest extends AbstractDatagramTest {
                 ch.pipeline().addLast(new SimpleChannelInboundHandler<DatagramPacket>() {
                     @Override
                     public void channelRead0(ChannelHandlerContext ctx, DatagramPacket msg) throws Exception {
-                        ByteBuf buf = msg.content();
-                        assertEquals(bytes.length, buf.readableBytes());
-                        for (byte b : bytes) {
-                            assertEquals(b, buf.readByte());
+                        try {
+                            if (sender == null) {
+                                assertNotNull(msg.sender());
+                            } else {
+                                InetSocketAddress senderAddress = (InetSocketAddress) sender;
+                                if (senderAddress.getAddress().isAnyLocalAddress()) {
+                                    assertEquals(senderAddress.getPort(), msg.sender().getPort());
+                                } else {
+                                    assertEquals(sender, msg.sender());
+                                }
+                            }
+
+                            ByteBuf buf = msg.content();
+                            assertEquals(bytes.length, buf.readableBytes());
+                            for (int i = 0; i < bytes.length; i++) {
+                                assertEquals(bytes[i], buf.getByte(buf.readerIndex() + i));
+                            }
+
+                            // Test that the channel's localAddress is equal to the message's recipient
+                            assertEquals(ctx.channel().localAddress(), msg.recipient());
+
+                            if (echo) {
+                                ctx.writeAndFlush(new DatagramPacket(buf.retainedDuplicate(), msg.sender()));
+                            }
+                        } finally {
+                            latch.countDown();
                         }
+                    }
 
-                        // Test that the channel's localAddress is equal to the message's recipient
-                        assertEquals(ctx.channel().localAddress(), msg.recipient());
-
-                        latch.countDown();
+                    @Override
+                    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+                        errorRef.compareAndSet(null, cause);
                     }
                 });
             }
@@ -294,4 +390,15 @@ public class DatagramUnicastTest extends AbstractDatagramTest {
             channel.close().sync();
         }
     }
+
+    protected InetSocketAddress sendToAddress(InetSocketAddress serverAddress) {
+        InetAddress addr = serverAddress.getAddress();
+        if (addr.isAnyLocalAddress()) {
+            if (addr instanceof Inet6Address) {
+                return new InetSocketAddress(NetUtil.LOCALHOST6, serverAddress.getPort());
+            }
+            return new InetSocketAddress(NetUtil.LOCALHOST4, serverAddress.getPort());
+        }
+        return serverAddress;
+    }
 }
diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketConnectTest.java b/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketConnectTest.java
index 0f96d04..f26bc05 100644
--- a/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketConnectTest.java
+++ b/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketConnectTest.java
@@ -21,8 +21,6 @@ import io.netty.channel.Channel;
 import io.netty.channel.ChannelFutureListener;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelInboundHandlerAdapter;
-import io.netty.testsuite.util.TestUtils;
-import io.netty.util.NetUtil;
 import io.netty.util.concurrent.ImmediateEventExecutor;
 import io.netty.util.concurrent.Promise;
 import org.junit.Test;
diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketFileRegionTest.java b/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketFileRegionTest.java
index 53deb6c..ae85825 100644
--- a/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketFileRegionTest.java
+++ b/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketFileRegionTest.java
@@ -22,17 +22,19 @@ import io.netty.buffer.Unpooled;
 import io.netty.channel.Channel;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelInboundHandler;
+import io.netty.channel.ChannelInboundHandlerAdapter;
 import io.netty.channel.ChannelOption;
 import io.netty.channel.DefaultFileRegion;
 import io.netty.channel.FileRegion;
 import io.netty.channel.SimpleChannelInboundHandler;
 import io.netty.util.internal.PlatformDependent;
+import org.hamcrest.CoreMatchers;
 import org.junit.Test;
 
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.RandomAccessFile;
 import java.nio.channels.WritableByteChannel;
 import java.util.Random;
 import java.util.concurrent.atomic.AtomicReference;
@@ -73,6 +75,11 @@ public class SocketFileRegionTest extends AbstractSocketTest {
         run();
     }
 
+    @Test
+    public void testFileRegionCountLargerThenFile() throws Throwable {
+        run();
+    }
+
     public void testFileRegion(ServerBootstrap sb, Bootstrap cb) throws Throwable {
         testFileRegion0(sb, cb, false, true, true);
     }
@@ -93,6 +100,34 @@ public class SocketFileRegionTest extends AbstractSocketTest {
         testFileRegion0(sb, cb, true, false, true);
     }
 
+    public void testFileRegionCountLargerThenFile(ServerBootstrap sb, Bootstrap cb) throws Throwable {
+        File file = File.createTempFile("netty-", ".tmp");
+        file.deleteOnExit();
+
+        final FileOutputStream out = new FileOutputStream(file);
+        out.write(data);
+        out.close();
+
+        sb.childHandler(new SimpleChannelInboundHandler<ByteBuf>() {
+            @Override
+            protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) {
+                // Just drop the message.
+            }
+        });
+        cb.handler(new ChannelInboundHandlerAdapter());
+
+        Channel sc = sb.bind().sync().channel();
+        Channel cc = cb.connect(sc.localAddress()).sync().channel();
+
+        // Request file region which is bigger then the underlying file.
+        FileRegion region = new DefaultFileRegion(
+                new RandomAccessFile(file, "r").getChannel(), 0, data.length + 1024);
+
+        assertThat(cc.writeAndFlush(region).await().cause(), CoreMatchers.<Throwable>instanceOf(IOException.class));
+        cc.close().sync();
+        sc.close().sync();
+    }
+
     private static void testFileRegion0(
             ServerBootstrap sb, Bootstrap cb, boolean voidPromise, final boolean autoRead, boolean defaultFileRegion)
             throws Throwable {
@@ -148,8 +183,8 @@ public class SocketFileRegionTest extends AbstractSocketTest {
 
         Channel cc = cb.connect(sc.localAddress()).sync().channel();
         FileRegion region = new DefaultFileRegion(
-                new FileInputStream(file).getChannel(), startOffset, data.length - bufferSize);
-        FileRegion emptyRegion = new DefaultFileRegion(new FileInputStream(file).getChannel(), 0, 0);
+                new RandomAccessFile(file, "r").getChannel(), startOffset, data.length - bufferSize);
+        FileRegion emptyRegion = new DefaultFileRegion(new RandomAccessFile(file, "r").getChannel(), 0, 0);
 
         if (!defaultFileRegion) {
             region = new FileRegionWrapper(region);
diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketShutdownOutputByPeerTest.java b/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketShutdownOutputByPeerTest.java
index f18109e..d42a3c0 100644
--- a/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketShutdownOutputByPeerTest.java
+++ b/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketShutdownOutputByPeerTest.java
@@ -15,138 +15,37 @@
  */
 package io.netty.testsuite.transport.socket;
 
-import io.netty.bootstrap.ServerBootstrap;
-import io.netty.buffer.ByteBuf;
-import io.netty.channel.Channel;
-import io.netty.channel.ChannelHandlerContext;
-import io.netty.channel.ChannelOption;
-import io.netty.channel.SimpleChannelInboundHandler;
 import io.netty.util.internal.SocketUtils;
-import io.netty.channel.socket.ChannelInputShutdownEvent;
-import io.netty.channel.socket.SocketChannel;
-import org.junit.Test;
 
+import java.io.IOException;
 import java.net.Socket;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.atomic.AtomicInteger;
+import java.net.SocketAddress;
 
-import static org.junit.Assert.*;
+public class SocketShutdownOutputByPeerTest extends AbstractSocketShutdownOutputByPeerTest<Socket> {
 
-public class SocketShutdownOutputByPeerTest extends AbstractServerSocketTest {
-
-    @Test(timeout = 30000)
-    public void testShutdownOutput() throws Throwable {
-        run();
+    @Override
+    protected void shutdownOutput(Socket s) throws IOException {
+        s.shutdownOutput();
     }
 
-    public void testShutdownOutput(ServerBootstrap sb) throws Throwable {
-        TestHandler h = new TestHandler();
-        Socket s = new Socket();
-        Channel sc = null;
-        try {
-            sc = sb.childHandler(h).childOption(ChannelOption.ALLOW_HALF_CLOSURE, true).bind().sync().channel();
-
-            SocketUtils.connect(s, sc.localAddress(), 10000);
-            s.getOutputStream().write(1);
-
-            assertEquals(1, (int) h.queue.take());
-
-            assertTrue(h.ch.isOpen());
-            assertTrue(h.ch.isActive());
-            assertFalse(h.ch.isInputShutdown());
-            assertFalse(h.ch.isOutputShutdown());
-
-            s.shutdownOutput();
-
-            h.halfClosure.await();
-
-            assertTrue(h.ch.isOpen());
-            assertTrue(h.ch.isActive());
-            assertTrue(h.ch.isInputShutdown());
-            assertFalse(h.ch.isOutputShutdown());
-            assertEquals(1, h.closure.getCount());
-            Thread.sleep(100);
-            assertEquals(1, h.halfClosureCount.intValue());
-        } finally {
-            if (sc != null) {
-                sc.close();
-            }
-            s.close();
-        }
+    @Override
+    protected void connect(Socket s, SocketAddress address) throws IOException {
+        SocketUtils.connect(s, address, 10000);
     }
 
-    @Test(timeout = 30000)
-    public void testShutdownOutputWithoutOption() throws Throwable {
-        run();
+    @Override
+    protected void close(Socket s) throws IOException {
+        s.close();
     }
 
-    public void testShutdownOutputWithoutOption(ServerBootstrap sb) throws Throwable {
-        TestHandler h = new TestHandler();
-        Socket s = new Socket();
-        Channel sc = null;
-        try {
-            sc = sb.childHandler(h).bind().sync().channel();
-
-            SocketUtils.connect(s, sc.localAddress(), 10000);
-            s.getOutputStream().write(1);
-
-            assertEquals(1, (int) h.queue.take());
-
-            assertTrue(h.ch.isOpen());
-            assertTrue(h.ch.isActive());
-            assertFalse(h.ch.isInputShutdown());
-            assertFalse(h.ch.isOutputShutdown());
-
-            s.shutdownOutput();
-
-            h.closure.await();
-
-            assertFalse(h.ch.isOpen());
-            assertFalse(h.ch.isActive());
-            assertTrue(h.ch.isInputShutdown());
-            assertTrue(h.ch.isOutputShutdown());
-
-            assertEquals(1, h.halfClosure.getCount());
-            Thread.sleep(100);
-            assertEquals(0, h.halfClosureCount.intValue());
-        } finally {
-            if (sc != null) {
-                sc.close();
-            }
-            s.close();
-        }
+    @Override
+    protected void write(Socket s, int data) throws IOException {
+        s.getOutputStream().write(data);
     }
 
-    private static class TestHandler extends SimpleChannelInboundHandler<ByteBuf> {
-        volatile SocketChannel ch;
-        final BlockingQueue<Byte> queue = new LinkedBlockingQueue<Byte>();
-        final CountDownLatch halfClosure = new CountDownLatch(1);
-        final CountDownLatch closure = new CountDownLatch(1);
-        final AtomicInteger halfClosureCount = new AtomicInteger();
-
-        @Override
-        public void channelActive(ChannelHandlerContext ctx) throws Exception {
-            ch = (SocketChannel) ctx.channel();
-        }
-
-        @Override
-        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
-            closure.countDown();
-        }
-
-        @Override
-        public void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
-            queue.offer(msg.readByte());
-        }
-
-        @Override
-        public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
-            if (evt instanceof ChannelInputShutdownEvent) {
-                halfClosureCount.incrementAndGet();
-                halfClosure.countDown();
-            }
-        }
+    @Override
+    protected Socket newSocket() {
+        return new Socket();
     }
+
 }
diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketSslClientRenegotiateTest.java b/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketSslClientRenegotiateTest.java
index 8036f08..4574ca7 100644
--- a/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketSslClientRenegotiateTest.java
+++ b/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketSslClientRenegotiateTest.java
@@ -18,6 +18,7 @@ package io.netty.testsuite.transport.socket;
 import io.netty.bootstrap.Bootstrap;
 import io.netty.bootstrap.ServerBootstrap;
 import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufAllocator;
 import io.netty.channel.Channel;
 import io.netty.channel.ChannelHandler.Sharable;
 import io.netty.channel.ChannelHandlerContext;
@@ -46,6 +47,9 @@ import java.security.cert.CertificateException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 import java.util.concurrent.atomic.AtomicReference;
 
 import javax.net.ssl.SSLHandshakeException;
@@ -73,7 +77,7 @@ public class SocketSslClientRenegotiateTest extends AbstractSocketTest {
         KEY_FILE = ssc.privateKey();
     }
 
-    @Parameters(name = "{index}: serverEngine = {0}, clientEngine = {1}")
+    @Parameters(name = "{index}: serverEngine = {0}, clientEngine = {1}, delegate = {2}")
     public static Collection<Object[]> data() throws Exception {
         List<SslContext> serverContexts = new ArrayList<SslContext>();
         List<SslContext> clientContexts = new ArrayList<SslContext>();
@@ -91,7 +95,8 @@ public class SocketSslClientRenegotiateTest extends AbstractSocketTest {
         for (SslContext sc: serverContexts) {
             for (SslContext cc: clientContexts) {
                 for (int i = 0; i < 32; i++) {
-                    params.add(new Object[] { sc, cc});
+                    params.add(new Object[] { sc, cc, true});
+                    params.add(new Object[] { sc, cc, false});
                 }
             }
         }
@@ -101,6 +106,7 @@ public class SocketSslClientRenegotiateTest extends AbstractSocketTest {
 
     private final SslContext serverCtx;
     private final SslContext clientCtx;
+    private final boolean delegate;
 
     private final AtomicReference<Throwable> clientException = new AtomicReference<Throwable>();
     private final AtomicReference<Throwable> serverException = new AtomicReference<Throwable>();
@@ -116,9 +122,10 @@ public class SocketSslClientRenegotiateTest extends AbstractSocketTest {
     private final TestHandler serverHandler = new TestHandler(serverException);
 
     public SocketSslClientRenegotiateTest(
-            SslContext serverCtx, SslContext clientCtx) {
+            SslContext serverCtx, SslContext clientCtx, boolean delegate) {
         this.serverCtx = serverCtx;
         this.clientCtx = clientCtx;
+        this.delegate = delegate;
     }
 
     @Test(timeout = 30000)
@@ -129,58 +136,74 @@ public class SocketSslClientRenegotiateTest extends AbstractSocketTest {
         run();
     }
 
+    private static SslHandler newSslHandler(SslContext sslCtx, ByteBufAllocator allocator, Executor executor) {
+        if (executor == null) {
+            return sslCtx.newHandler(allocator);
+        } else {
+            return sslCtx.newHandler(allocator, executor);
+        }
+    }
+
     public void testSslRenegotiationRejected(ServerBootstrap sb, Bootstrap cb) throws Throwable {
         reset();
 
-        sb.childHandler(new ChannelInitializer<Channel>() {
-            @Override
-            @SuppressWarnings("deprecation")
-            public void initChannel(Channel sch) throws Exception {
-                serverChannel = sch;
-                serverSslHandler = serverCtx.newHandler(sch.alloc());
-                // As we test renegotiation we should use a protocol that support it.
-                serverSslHandler.engine().setEnabledProtocols(new String[] { "TLSv1.2" });
-                sch.pipeline().addLast("ssl", serverSslHandler);
-                sch.pipeline().addLast("handler", serverHandler);
-            }
-        });
-
-        cb.handler(new ChannelInitializer<Channel>() {
-            @Override
-            @SuppressWarnings("deprecation")
-            public void initChannel(Channel sch) throws Exception {
-                clientChannel = sch;
-                clientSslHandler = clientCtx.newHandler(sch.alloc());
-                // As we test renegotiation we should use a protocol that support it.
-                clientSslHandler.engine().setEnabledProtocols(new String[] { "TLSv1.2" });
-                sch.pipeline().addLast("ssl", clientSslHandler);
-                sch.pipeline().addLast("handler", clientHandler);
-            }
-        });
-
-        Channel sc = sb.bind().sync().channel();
-        cb.connect(sc.localAddress()).sync();
+        final ExecutorService executorService = delegate ? Executors.newCachedThreadPool() : null;
 
-        Future<Channel> clientHandshakeFuture = clientSslHandler.handshakeFuture();
-        clientHandshakeFuture.sync();
-
-        String renegotiation = clientSslHandler.engine().getEnabledCipherSuites()[0];
-        // Use the first previous enabled ciphersuite and try to renegotiate.
-        clientSslHandler.engine().setEnabledCipherSuites(new String[] { renegotiation });
-        clientSslHandler.renegotiate().await();
-        serverChannel.close().awaitUninterruptibly();
-        clientChannel.close().awaitUninterruptibly();
-        sc.close().awaitUninterruptibly();
         try {
-            if (serverException.get() != null) {
-                throw serverException.get();
+            sb.childHandler(new ChannelInitializer<Channel>() {
+                @Override
+                @SuppressWarnings("deprecation")
+                public void initChannel(Channel sch) throws Exception {
+                    serverChannel = sch;
+                    serverSslHandler = newSslHandler(serverCtx, sch.alloc(), executorService);
+                    // As we test renegotiation we should use a protocol that support it.
+                    serverSslHandler.engine().setEnabledProtocols(new String[]{"TLSv1.2"});
+                    sch.pipeline().addLast("ssl", serverSslHandler);
+                    sch.pipeline().addLast("handler", serverHandler);
+                }
+            });
+
+            cb.handler(new ChannelInitializer<Channel>() {
+                @Override
+                @SuppressWarnings("deprecation")
+                public void initChannel(Channel sch) throws Exception {
+                    clientChannel = sch;
+                    clientSslHandler = newSslHandler(clientCtx, sch.alloc(), executorService);
+                    // As we test renegotiation we should use a protocol that support it.
+                    clientSslHandler.engine().setEnabledProtocols(new String[]{"TLSv1.2"});
+                    sch.pipeline().addLast("ssl", clientSslHandler);
+                    sch.pipeline().addLast("handler", clientHandler);
+                }
+            });
+
+            Channel sc = sb.bind().sync().channel();
+            cb.connect(sc.localAddress()).sync();
+
+            Future<Channel> clientHandshakeFuture = clientSslHandler.handshakeFuture();
+            clientHandshakeFuture.sync();
+
+            String renegotiation = clientSslHandler.engine().getEnabledCipherSuites()[0];
+            // Use the first previous enabled ciphersuite and try to renegotiate.
+            clientSslHandler.engine().setEnabledCipherSuites(new String[]{renegotiation});
+            clientSslHandler.renegotiate().await();
+            serverChannel.close().awaitUninterruptibly();
+            clientChannel.close().awaitUninterruptibly();
+            sc.close().awaitUninterruptibly();
+            try {
+                if (serverException.get() != null) {
+                    throw serverException.get();
+                }
+                fail();
+            } catch (DecoderException e) {
+                assertTrue(e.getCause() instanceof SSLHandshakeException);
+            }
+            if (clientException.get() != null) {
+                throw clientException.get();
+            }
+        } finally {
+            if (executorService != null) {
+                executorService.shutdown();
             }
-            fail();
-        } catch (DecoderException e) {
-            assertTrue(e.getCause() instanceof SSLHandshakeException);
-        }
-        if (clientException.get() != null) {
-            throw clientException.get();
         }
     }
 
diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketSslGreetingTest.java b/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketSslGreetingTest.java
index d7db90b..b3f0e46 100644
--- a/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketSslGreetingTest.java
+++ b/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketSslGreetingTest.java
@@ -18,6 +18,7 @@ package io.netty.testsuite.transport.socket;
 import io.netty.bootstrap.Bootstrap;
 import io.netty.bootstrap.ServerBootstrap;
 import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufAllocator;
 import io.netty.channel.Channel;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelInitializer;
@@ -48,6 +49,9 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 import java.util.concurrent.atomic.AtomicReference;
 
 import static org.junit.Assert.assertEquals;
@@ -74,7 +78,7 @@ public class SocketSslGreetingTest extends AbstractSocketTest {
         KEY_FILE = ssc.privateKey();
     }
 
-    @Parameters(name = "{index}: serverEngine = {0}, clientEngine = {1}")
+    @Parameters(name = "{index}: serverEngine = {0}, clientEngine = {1}, delegate = {2}")
     public static Collection<Object[]> data() throws Exception {
         List<SslContext> serverContexts = new ArrayList<SslContext>();
         serverContexts.add(SslContextBuilder.forServer(CERT_FILE, KEY_FILE).sslProvider(SslProvider.JDK).build());
@@ -95,7 +99,8 @@ public class SocketSslGreetingTest extends AbstractSocketTest {
         List<Object[]> params = new ArrayList<Object[]>();
         for (SslContext sc: serverContexts) {
             for (SslContext cc: clientContexts) {
-                params.add(new Object[] { sc, cc });
+                params.add(new Object[] { sc, cc, true });
+                params.add(new Object[] { sc, cc, false });
             }
         }
         return params;
@@ -103,10 +108,20 @@ public class SocketSslGreetingTest extends AbstractSocketTest {
 
     private final SslContext serverCtx;
     private final SslContext clientCtx;
+    private final boolean delegate;
 
-    public SocketSslGreetingTest(SslContext serverCtx, SslContext clientCtx) {
+    public SocketSslGreetingTest(SslContext serverCtx, SslContext clientCtx, boolean delegate) {
         this.serverCtx = serverCtx;
         this.clientCtx = clientCtx;
+        this.delegate = delegate;
+    }
+
+    private static SslHandler newSslHandler(SslContext sslCtx, ByteBufAllocator allocator, Executor executor) {
+        if (executor == null) {
+            return sslCtx.newHandler(allocator);
+        } else {
+            return sslCtx.newHandler(allocator, executor);
+        }
     }
 
     // Test for https://github.com/netty/netty/pull/2437
@@ -119,46 +134,53 @@ public class SocketSslGreetingTest extends AbstractSocketTest {
         final ServerHandler sh = new ServerHandler();
         final ClientHandler ch = new ClientHandler();
 
-        sb.childHandler(new ChannelInitializer<Channel>() {
-            @Override
-            public void initChannel(Channel sch) throws Exception {
-                ChannelPipeline p = sch.pipeline();
-                p.addLast(serverCtx.newHandler(sch.alloc()));
-                p.addLast(new LoggingHandler(LOG_LEVEL));
-                p.addLast(sh);
-            }
-        });
-
-        cb.handler(new ChannelInitializer<Channel>() {
-            @Override
-            public void initChannel(Channel sch) throws Exception {
-                ChannelPipeline p = sch.pipeline();
-                p.addLast(clientCtx.newHandler(sch.alloc()));
-                p.addLast(new LoggingHandler(LOG_LEVEL));
-                p.addLast(ch);
-            }
-        });
+        final ExecutorService executorService = delegate ? Executors.newCachedThreadPool() : null;
+        try {
+            sb.childHandler(new ChannelInitializer<Channel>() {
+                @Override
+                public void initChannel(Channel sch) throws Exception {
+                    ChannelPipeline p = sch.pipeline();
+                    p.addLast(newSslHandler(serverCtx, sch.alloc(), executorService));
+                    p.addLast(new LoggingHandler(LOG_LEVEL));
+                    p.addLast(sh);
+                }
+            });
+
+            cb.handler(new ChannelInitializer<Channel>() {
+                @Override
+                public void initChannel(Channel sch) throws Exception {
+                    ChannelPipeline p = sch.pipeline();
+                    p.addLast(newSslHandler(clientCtx, sch.alloc(), executorService));
+                    p.addLast(new LoggingHandler(LOG_LEVEL));
+                    p.addLast(ch);
+                }
+            });
 
-        Channel sc = sb.bind().sync().channel();
-        Channel cc = cb.connect(sc.localAddress()).sync().channel();
+            Channel sc = sb.bind().sync().channel();
+            Channel cc = cb.connect(sc.localAddress()).sync().channel();
 
-        ch.latch.await();
+            ch.latch.await();
 
-        sh.channel.close().awaitUninterruptibly();
-        cc.close().awaitUninterruptibly();
-        sc.close().awaitUninterruptibly();
+            sh.channel.close().awaitUninterruptibly();
+            cc.close().awaitUninterruptibly();
+            sc.close().awaitUninterruptibly();
 
-        if (sh.exception.get() != null && !(sh.exception.get() instanceof IOException)) {
-            throw sh.exception.get();
-        }
-        if (ch.exception.get() != null && !(ch.exception.get() instanceof IOException)) {
-            throw ch.exception.get();
-        }
-        if (sh.exception.get() != null) {
-            throw sh.exception.get();
-        }
-        if (ch.exception.get() != null) {
-            throw ch.exception.get();
+            if (sh.exception.get() != null && !(sh.exception.get() instanceof IOException)) {
+                throw sh.exception.get();
+            }
+            if (ch.exception.get() != null && !(ch.exception.get() instanceof IOException)) {
+                throw ch.exception.get();
+            }
+            if (sh.exception.get() != null) {
+                throw sh.exception.get();
+            }
+            if (ch.exception.get() != null) {
+                throw ch.exception.get();
+            }
+        } finally {
+            if (executorService != null) {
+                executorService.shutdown();
+            }
         }
     }
 
diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketTestPermutation.java b/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketTestPermutation.java
index 3bf9883..8c41f37 100644
--- a/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketTestPermutation.java
+++ b/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketTestPermutation.java
@@ -70,6 +70,7 @@ public class SocketTestPermutation {
             new OioEventLoopGroup(Integer.MAX_VALUE, new DefaultThreadFactory("testsuite-oio-worker", true));
 
     protected <A extends AbstractBootstrap<?, ?>, B extends AbstractBootstrap<?, ?>>
+
     List<BootstrapComboFactory<A, B>> combo(List<BootstrapFactory<A>> sbfs, List<BootstrapFactory<B>> cbfs) {
 
         List<BootstrapComboFactory<A, B>> list = new ArrayList<BootstrapComboFactory<A, B>>();
@@ -112,7 +113,7 @@ public class SocketTestPermutation {
         return list;
     }
 
-    public List<BootstrapComboFactory<Bootstrap, Bootstrap>> datagram() {
+    public List<BootstrapComboFactory<Bootstrap, Bootstrap>> datagram(final InternetProtocolFamily family) {
         // Make the list of Bootstrap factories.
         List<BootstrapFactory<Bootstrap>> bfs = Arrays.asList(
                 new BootstrapFactory<Bootstrap>() {
@@ -121,7 +122,7 @@ public class SocketTestPermutation {
                         return new Bootstrap().group(nioWorkerGroup).channelFactory(new ChannelFactory<Channel>() {
                             @Override
                             public Channel newChannel() {
-                                return new NioDatagramChannel(InternetProtocolFamily.IPv4);
+                                return new NioDatagramChannel(family);
                             }
 
                             @Override
diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/udt/UDTClientServerConnectionTest.java b/testsuite/src/main/java/io/netty/testsuite/transport/udt/UDTClientServerConnectionTest.java
index c878643..67aae95 100644
--- a/testsuite/src/main/java/io/netty/testsuite/transport/udt/UDTClientServerConnectionTest.java
+++ b/testsuite/src/main/java/io/netty/testsuite/transport/udt/UDTClientServerConnectionTest.java
@@ -35,6 +35,9 @@ import io.netty.util.CharsetUtil;
 import io.netty.util.NetUtil;
 import io.netty.util.concurrent.DefaultThreadFactory;
 import io.netty.util.concurrent.GlobalEventExecutor;
+import io.netty.util.internal.PlatformDependent;
+import org.junit.Assume;
+import org.junit.BeforeClass;
 import org.junit.Test;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -333,6 +336,22 @@ public class UDTClientServerConnectionTest {
     static final int WAIT_COUNT = 50;
     static final int WAIT_SLEEP = 100;
 
+    @BeforeClass
+    public static void assumeUdt() {
+        Assume.assumeTrue("com.barchart.udt.SocketUDT can not be loaded and initialized", canLoadAndInit());
+        Assume.assumeFalse("Not supported on J9 JVM", PlatformDependent.isJ9Jvm());
+    }
+
+    private static boolean canLoadAndInit() {
+        try {
+            Class.forName("com.barchart.udt.SocketUDT", true,
+                    UDTClientServerConnectionTest.class.getClassLoader());
+            return true;
+        } catch (Throwable e) {
+            return false;
+        }
+    }
+
     /**
      * Verify UDT client/server connect and disconnect.
      */
diff --git a/testsuite/src/main/java/io/netty/testsuite/util/TestUtils.java b/testsuite/src/main/java/io/netty/testsuite/util/TestUtils.java
index 98cb333..c75236b 100644
--- a/testsuite/src/main/java/io/netty/testsuite/util/TestUtils.java
+++ b/testsuite/src/main/java/io/netty/testsuite/util/TestUtils.java
@@ -16,6 +16,7 @@
 package io.netty.testsuite.util;
 
 import io.netty.util.CharsetUtil;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 import org.junit.rules.TestName;
@@ -112,9 +113,7 @@ public final class TestUtils {
 
     public static void dump(String filenamePrefix) throws IOException {
 
-        if (filenamePrefix == null) {
-            throw new NullPointerException("filenamePrefix");
-        }
+        ObjectUtil.checkNotNull(filenamePrefix, "filenamePrefix");
 
         final String timestamp = timestamp();
         final File heapDumpFile = new File(filenamePrefix + '.' + timestamp + ".hprof");
@@ -142,6 +141,10 @@ public final class TestUtils {
                 return name.endsWith(".hprof");
             }
         });
+        if (files == null) {
+            logger.warn("failed to find heap dump due to I/O error!");
+            return;
+        }
 
         final byte[] buf = new byte[65536];
         final LZMA2Options options = new LZMA2Options(LZMA2Options.PRESET_DEFAULT);
diff --git a/transport-blockhound-tests/pom.xml b/transport-blockhound-tests/pom.xml
new file mode 100644
index 0000000..9bbc2a9
--- /dev/null
+++ b/transport-blockhound-tests/pom.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2019 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.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>io.netty</groupId>
+    <artifactId>netty-parent</artifactId>
+    <version>4.1.48.Final</version>
+  </parent>
+
+  <artifactId>netty-transport-blockhound-tests</artifactId>
+  <packaging>jar</packaging>
+  <description>
+    Tests for the BlockHound integration.
+  </description>
+
+  <name>Netty/Transport/BlockHound/Tests</name>
+
+  <profiles>
+    <profile>
+      <id>java13</id>
+      <activation>
+        <jdk>13</jdk>
+      </activation>
+      <properties>
+        <argLine.common>-XX:+AllowRedefinitionToAddDeleteMethods</argLine.common>
+      </properties>
+    </profile>
+  </profiles>
+
+  <properties>
+    <maven.compiler.source>1.8</maven.compiler.source>
+    <maven.compiler.target>1.8</maven.compiler.target>
+    <!-- Needed for SelfSignedCertificate -->
+    <argLine.java9.extras>--add-exports java.base/sun.security.x509=ALL-UNNAMED</argLine.java9.extras>
+    <skipJapicmp>true</skipJapicmp>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>netty-transport</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>netty-handler</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.bouncycastle</groupId>
+      <artifactId>bcpkix-jdk15on</artifactId>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>io.projectreactor.tools</groupId>
+      <artifactId>blockhound</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/transport-blockhound-tests/src/test/java/io/netty/util/internal/NettyBlockHoundIntegrationTest.java b/transport-blockhound-tests/src/test/java/io/netty/util/internal/NettyBlockHoundIntegrationTest.java
new file mode 100644
index 0000000..817257b
--- /dev/null
+++ b/transport-blockhound-tests/src/test/java/io/netty/util/internal/NettyBlockHoundIntegrationTest.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright 2019 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.util.internal;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.buffer.UnpooledByteBufAllocator;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.channel.socket.nio.NioSocketChannel;
+import io.netty.handler.ssl.SslContext;
+import io.netty.handler.ssl.SslContextBuilder;
+import io.netty.handler.ssl.SslHandler;
+import io.netty.handler.ssl.SslHandshakeCompletionEvent;
+import io.netty.handler.ssl.SslProvider;
+import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
+import io.netty.handler.ssl.util.SelfSignedCertificate;
+import io.netty.util.ReferenceCountUtil;
+import io.netty.util.concurrent.DefaultThreadFactory;
+import io.netty.util.concurrent.EventExecutor;
+import io.netty.util.concurrent.GlobalEventExecutor;
+import io.netty.util.concurrent.ImmediateEventExecutor;
+import io.netty.util.concurrent.ImmediateExecutor;
+import io.netty.util.concurrent.ScheduledFuture;
+import io.netty.util.concurrent.SingleThreadEventExecutor;
+import io.netty.util.internal.Hidden.NettyBlockHoundIntegration;
+import org.hamcrest.Matchers;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import reactor.blockhound.BlockHound;
+import reactor.blockhound.BlockingOperationError;
+import reactor.blockhound.integration.BlockHoundIntegration;
+
+import java.net.InetSocketAddress;
+import java.util.ServiceLoader;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class NettyBlockHoundIntegrationTest {
+
+    @BeforeClass
+    public static void setUpClass() {
+        BlockHound.install();
+    }
+
+    @Test
+    public void testServiceLoader() {
+        for (BlockHoundIntegration integration : ServiceLoader.load(BlockHoundIntegration.class)) {
+            if (integration instanceof NettyBlockHoundIntegration) {
+                return;
+            }
+        }
+
+        fail("NettyBlockHoundIntegration cannot be loaded with ServiceLoader");
+    }
+
+    @Test
+    public void testBlockingCallsInNettyThreads() throws Exception {
+        final FutureTask<Void> future = new FutureTask<>(() -> {
+            Thread.sleep(0);
+            return null;
+        });
+        GlobalEventExecutor.INSTANCE.execute(future);
+
+        try {
+            future.get(5, TimeUnit.SECONDS);
+            fail("Expected an exception due to a blocking call but none was thrown");
+        } catch (ExecutionException e) {
+            assertThat(e.getCause(), Matchers.instanceOf(BlockingOperationError.class));
+        }
+    }
+
+    @Test(timeout = 5000L)
+    public void testGlobalEventExecutorTakeTask() throws InterruptedException {
+        testEventExecutorTakeTask(GlobalEventExecutor.INSTANCE);
+    }
+
+    @Test(timeout = 5000L)
+    public void testSingleThreadEventExecutorTakeTask() throws InterruptedException {
+        SingleThreadEventExecutor executor =
+                new SingleThreadEventExecutor(null, new DefaultThreadFactory("test"), true) {
+                    @Override
+                    protected void run() {
+                        while (!confirmShutdown()) {
+                            Runnable task = takeTask();
+                            if (task != null) {
+                                task.run();
+                            }
+                        }
+                    }
+                };
+        testEventExecutorTakeTask(executor);
+    }
+
+    private static void testEventExecutorTakeTask(EventExecutor eventExecutor) throws InterruptedException {
+        CountDownLatch latch = new CountDownLatch(1);
+        ScheduledFuture<?> f = eventExecutor.schedule(latch::countDown, 10, TimeUnit.MILLISECONDS);
+        f.sync();
+        latch.await();
+    }
+
+    // Tests copied from io.netty.handler.ssl.SslHandlerTest
+    @Test
+    public void testHandshakeWithExecutorThatExecuteDirectory() throws Exception {
+        testHandshakeWithExecutor(Runnable::run);
+    }
+
+    @Test
+    public void testHandshakeWithImmediateExecutor() throws Exception {
+        testHandshakeWithExecutor(ImmediateExecutor.INSTANCE);
+    }
+
+    @Test
+    public void testHandshakeWithImmediateEventExecutor() throws Exception {
+        testHandshakeWithExecutor(ImmediateEventExecutor.INSTANCE);
+    }
+
+    @Test
+    public void testHandshakeWithExecutor() throws Exception {
+        ExecutorService executorService = Executors.newCachedThreadPool();
+        try {
+            testHandshakeWithExecutor(executorService);
+        } finally {
+            executorService.shutdown();
+        }
+    }
+
+    private static void testHandshakeWithExecutor(Executor executor) throws Exception {
+        final SslContext sslClientCtx = SslContextBuilder.forClient()
+                .trustManager(InsecureTrustManagerFactory.INSTANCE)
+                .sslProvider(SslProvider.JDK).build();
+
+        final SelfSignedCertificate cert = new SelfSignedCertificate();
+        final SslContext sslServerCtx = SslContextBuilder.forServer(cert.key(), cert.cert())
+                .sslProvider(SslProvider.JDK).build();
+
+        EventLoopGroup group = new NioEventLoopGroup();
+        Channel sc = null;
+        Channel cc = null;
+        final SslHandler clientSslHandler = sslClientCtx.newHandler(UnpooledByteBufAllocator.DEFAULT, executor);
+        final SslHandler serverSslHandler = sslServerCtx.newHandler(UnpooledByteBufAllocator.DEFAULT, executor);
+
+        try {
+            sc = new ServerBootstrap()
+                    .group(group)
+                    .channel(NioServerSocketChannel.class)
+                    .childHandler(serverSslHandler)
+                    .bind(new InetSocketAddress(0)).syncUninterruptibly().channel();
+
+            ChannelFuture future = new Bootstrap()
+                    .group(group)
+                    .channel(NioSocketChannel.class)
+                    .handler(new ChannelInitializer<Channel>() {
+                        @Override
+                        protected void initChannel(Channel ch) {
+                            ch.pipeline()
+                              .addLast(clientSslHandler)
+                              .addLast(new ChannelInboundHandlerAdapter() {
+
+                                  @Override
+                                  public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
+                                      if (evt instanceof SslHandshakeCompletionEvent &&
+                                              ((SslHandshakeCompletionEvent) evt).cause() != null) {
+                                          ((SslHandshakeCompletionEvent) evt).cause().printStackTrace();
+                                      }
+                                      ctx.fireUserEventTriggered(evt);
+                                  }
+                              });
+                        }
+                    }).connect(sc.localAddress());
+            cc = future.syncUninterruptibly().channel();
+
+            assertTrue(clientSslHandler.handshakeFuture().await().isSuccess());
+            assertTrue(serverSslHandler.handshakeFuture().await().isSuccess());
+        } finally {
+            if (cc != null) {
+                cc.close().syncUninterruptibly();
+            }
+            if (sc != null) {
+                sc.close().syncUninterruptibly();
+            }
+            group.shutdownGracefully();
+            ReferenceCountUtil.release(sslClientCtx);
+        }
+    }
+}
diff --git a/transport-native-epoll/README.md b/transport-native-epoll/README.md
index 43f97ee..ebc7960 100644
--- a/transport-native-epoll/README.md
+++ b/transport-native-epoll/README.md
@@ -1,3 +1,3 @@
 # Native transport for Linux
 
-See [our wiki page](http://netty.io/wiki/native-transports.html).
+See [our wiki page](https://netty.io/wiki/native-transports.html).
diff --git a/transport-native-epoll/pom.xml b/transport-native-epoll/pom.xml
index 4f1f3b8..424c359 100644
--- a/transport-native-epoll/pom.xml
+++ b/transport-native-epoll/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>io.netty</groupId>
     <artifactId>netty-parent</artifactId>
-    <version>4.1.33.Final</version>
+    <version>4.1.48.Final</version>
   </parent>
   <artifactId>netty-transport-native-epoll</artifactId>
 
@@ -28,13 +28,15 @@
 
   <properties>
     <javaModuleName>io.netty.transport.epoll</javaModuleName>
-    <!-- Needed by the native transport as we need the memoryAddress of the ByteBuffer -->
-    <argLine.java9.extras>--add-exports java.base/sun.security.x509=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED</argLine.java9.extras>
+    <!-- Needed as we use SelfSignedCertificate in our tests -->
+    <argLine.java9.extras>--add-exports java.base/sun.security.x509=ALL-UNNAMED</argLine.java9.extras>
     <unix.common.lib.name>netty-unix-common</unix.common.lib.name>
     <unix.common.lib.dir>${project.build.directory}/unix-common-lib</unix.common.lib.dir>
     <unix.common.lib.unpacked.dir>${unix.common.lib.dir}/META-INF/native/lib</unix.common.lib.unpacked.dir>
     <unix.common.include.unpacked.dir>${unix.common.lib.dir}/META-INF/native/include</unix.common.include.unpacked.dir>
+    <jni.compiler.args.cflags>CFLAGS=-O3 -Werror -fno-omit-frame-pointer -Wunused-variable -fvisibility=hidden -I${unix.common.include.unpacked.dir}</jni.compiler.args.cflags>
     <jni.compiler.args.ldflags>LDFLAGS=-L${unix.common.lib.unpacked.dir} -Wl,--no-as-needed -lrt -Wl,--whole-archive -l${unix.common.lib.name} -Wl,--no-whole-archive</jni.compiler.args.ldflags>
+    <nativeSourceDirectory>${project.basedir}/src/main/c</nativeSourceDirectory>
     <skipTests>true</skipTests>
   </properties>
 
@@ -146,7 +148,7 @@
                 <id>build-native-lib</id>
                 <configuration>
                   <name>netty_transport_native_epoll_${os.detected.arch}</name>
-                  <nativeSourceDirectory>${project.basedir}/src/main/c</nativeSourceDirectory>
+                  <nativeSourceDirectory>${nativeSourceDirectory}</nativeSourceDirectory>
                   <libDirectory>${project.build.outputDirectory}</libDirectory>
                   <!-- We use Maven's artifact classifier instead.
                        This hack will make the hawtjni plugin to put the native library
@@ -155,13 +157,13 @@
                   <configureArgs>
                     <arg>${jni.compiler.args.ldflags}</arg>
                     <arg>${jni.compiler.args.cflags}</arg>
+                    <configureArg>--libdir=${project.build.directory}/native-build/target/lib</configureArg>
                   </configureArgs>
                 </configuration>
                 <goals>
                   <goal>generate</goal>
                   <goal>build</goal>
                 </goals>
-                <phase>compile</phase>
               </execution>
             </executions>
           </plugin>
@@ -191,102 +193,6 @@
               </execution>
             </executions>
           </plugin>
-
-          <plugin>
-            <artifactId>maven-antrun-plugin</artifactId>
-            <executions>
-              <execution>
-                <!-- Phase must be before regex-glibc-sendmmsg and regex-linux-sendmmsg -->
-                <phase>validate</phase>
-                <goals>
-                  <goal>run</goal>
-                </goals>
-                <id>ant-get-systeminfo</id>
-                <configuration>
-                  <exportAntProperties>true</exportAntProperties>
-                  <tasks>
-                    <exec executable="sh" outputproperty="ldd_version">
-                      <arg value="-c" />
-                      <arg value="ldd --version | head -1" />
-                    </exec>
-                    <exec executable="uname" outputproperty="uname_os_version">
-                      <arg value="-r" />
-                    </exec>
-                  </tasks>
-                </configuration>
-              </execution>
-            </executions>
-          </plugin>
-          <plugin>
-            <groupId>org.codehaus.mojo</groupId>
-            <artifactId>build-helper-maven-plugin</artifactId>
-            <executions>
-              <execution>
-                <!-- Phase must be before regex-combined-sendmmsg -->
-                <phase>initialize</phase>
-                <id>regex-glibc-sendmmsg</id>
-                <goals>
-                  <goal>regex-property</goal>
-                </goals>
-                <configuration>
-                  <name>glibc.sendmmsg.support</name>
-                  <value>${ldd_version}</value>
-                  <!-- Version must be >= 2.14 - set to IO_NETTY_SENDMSSG_NOT_FOUND if this version is not satisfied -->
-                  <regex>^((?!^[^)]+\)\s+(0*2\.1[4-9]|0*2\.[2-9][0-9]+|0*[3-9][0-9]*|0*[1-9]+[0-9]+).*).)*$</regex>
-                  <replacement>IO_NETTY_SENDMSSG_NOT_FOUND</replacement>
-                  <failIfNoMatch>false</failIfNoMatch>
-                </configuration>
-              </execution>
-              <execution>
-                <!-- Phase must be before regex-combined-sendmmsg -->
-                <phase>initialize</phase>
-                <id>regex-linux-sendmmsg</id>
-                <goals>
-                  <goal>regex-property</goal>
-                </goals>
-                <configuration>
-                  <name>linux.sendmmsg.support</name>
-                  <value>${uname_os_version}</value>
-                  <!-- Version must be >= 3 - set to IO_NETTY_SENDMSSG_NOT_FOUND if this version is not satisfied -->
-                  <regex>^((?!^[0-9]*[3-9]\.?.*).)*$</regex>
-                  <replacement>IO_NETTY_SENDMSSG_NOT_FOUND</replacement>
-                  <failIfNoMatch>false</failIfNoMatch>
-                </configuration>
-              </execution>
-              <execution>
-                <!-- Phase must be before regex-unset-if-needed-sendmmsg -->
-                <phase>generate-sources</phase>
-                <id>regex-combined-sendmmsg</id>
-                <goals>
-                  <goal>regex-property</goal>
-                </goals>
-                <configuration>
-                  <name>jni.compiler.args.cflags</name>
-                  <value>${linux.sendmmsg.support}${glibc.sendmmsg.support}</value>
-                  <!-- If glibc and linux kernel are both not sufficient...then define the CFLAGS -->
-                  <regex>.*IO_NETTY_SENDMSSG_NOT_FOUND.*</regex>
-                  <replacement>CFLAGS=-O3 -DIO_NETTY_SENDMMSG_NOT_FOUND -Werror -fno-omit-frame-pointer -Wunused-variable -fvisibility=hidden -I${unix.common.include.unpacked.dir}</replacement>
-                  <failIfNoMatch>false</failIfNoMatch>
-                </configuration>
-              </execution>
-              <execution>
-                <!-- Phase must be before build-native-lib -->
-                <phase>generate-sources</phase>
-                <id>regex-unset-if-needed-sendmmsg</id>
-                <goals>
-                  <goal>regex-property</goal>
-                </goals>
-                <configuration>
-                  <name>jni.compiler.args.cflags</name>
-                  <value>${jni.compiler.args.cflags}</value>
-                  <!-- If glibc and linux kernel are both not sufficient...then define the CFLAGS -->
-                  <regex>^((?!CFLAGS=).)*$</regex>
-                  <replacement>CFLAGS=-O3 -Werror -fno-omit-frame-pointer -Wunused-variable -fvisibility=hidden -I${unix.common.include.unpacked.dir}</replacement>
-                  <failIfNoMatch>false</failIfNoMatch>
-                </configuration>
-              </execution>
-            </executions>
-          </plugin>
         </plugins>
       </build>
   
@@ -350,6 +256,24 @@
 
   <build>
     <plugins>
+      <!-- Also include c files in source jar -->
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>build-helper-maven-plugin</artifactId>
+        <executions>
+          <execution>
+            <phase>generate-sources</phase>
+            <goals>
+              <goal>add-source</goal>
+            </goals>
+            <configuration>
+              <sources>
+                <source>${nativeSourceDirectory}</source>
+              </sources>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
       <plugin>
         <artifactId>maven-jar-plugin</artifactId>
         <executions>
diff --git a/transport-native-epoll/src/main/c/netty_epoll_linuxsocket.c b/transport-native-epoll/src/main/c/netty_epoll_linuxsocket.c
index 05d889f..3bb7b20 100644
--- a/transport-native-epoll/src/main/c/netty_epoll_linuxsocket.c
+++ b/transport-native-epoll/src/main/c/netty_epoll_linuxsocket.c
@@ -64,6 +64,44 @@ static jfieldID fdFieldId = NULL;
 static jfieldID fileDescriptorFieldId = NULL;
 
 // JNI Registered Methods Begin
+static void netty_epoll_linuxsocket_setTimeToLive(JNIEnv* env, jclass clazz, jint fd, jint optval) {
+    netty_unix_socket_setOption(env, fd, IPPROTO_IP, IP_TTL, &optval, sizeof(optval));
+}
+
+static void netty_epoll_linuxsocket_setIpMulticastLoop(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6, jint optval) {
+    if (ipv6 == JNI_TRUE) {
+        u_int val = (u_int) optval;
+        netty_unix_socket_setOption(env, fd, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, &val, sizeof(val));
+    } else {
+        u_char val = (u_char) optval;
+        netty_unix_socket_setOption(env, fd, IPPROTO_IP, IP_MULTICAST_LOOP, &val, sizeof(val));
+    }
+}
+
+static void netty_epoll_linuxsocket_setInterface(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6, jbyteArray interfaceAddress, jint scopeId, jint interfaceIndex) {
+    struct sockaddr_storage interfaceAddr;
+    socklen_t interfaceAddrSize;
+    struct sockaddr_in* interfaceIpAddr;
+
+    memset(&interfaceAddr, 0, sizeof(interfaceAddr));
+
+    if (ipv6 == JNI_TRUE) {
+        if (interfaceIndex == -1) {
+           netty_unix_errors_throwIOException(env, "Unable to find network index");
+           return;
+        }
+        netty_unix_socket_setOption(env, fd, IPPROTO_IPV6, IPV6_MULTICAST_IF, &interfaceIndex, sizeof(interfaceIndex));
+    } else {
+        if (netty_unix_socket_initSockaddr(env, ipv6, interfaceAddress, scopeId, 0, &interfaceAddr, &interfaceAddrSize) == -1) {
+            netty_unix_errors_throwIOException(env, "Could not init sockaddr");
+            return;
+        }
+
+        interfaceIpAddr = (struct sockaddr_in*) &interfaceAddr;
+        netty_unix_socket_setOption(env, fd, IPPROTO_IP, IP_MULTICAST_IF, &interfaceIpAddr->sin_addr, sizeof(interfaceIpAddr->sin_addr));
+    }
+}
+
 static void netty_epoll_linuxsocket_setTcpCork(JNIEnv* env, jclass clazz, jint fd, jint optval) {
     netty_unix_socket_setOption(env, fd, IPPROTO_TCP, TCP_CORK, &optval, sizeof(optval));
 }
@@ -120,10 +158,235 @@ static void netty_epoll_linuxsocket_setSoBusyPoll(JNIEnv* env, jclass clazz, jin
     netty_unix_socket_setOption(env, fd, SOL_SOCKET, SO_BUSY_POLL, &optval, sizeof(optval));
 }
 
-static void netty_epoll_linuxsocket_setTcpMd5Sig(JNIEnv* env, jclass clazz, jint fd, jbyteArray address, jint scopeId, jbyteArray key) {
+static void netty_epoll_linuxsocket_joinGroup(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6, jbyteArray groupAddress, jbyteArray interfaceAddress, jint scopeId, jint interfaceIndex) {
+    struct sockaddr_storage groupAddr;
+    socklen_t groupAddrSize;
+    struct sockaddr_storage interfaceAddr;
+    socklen_t interfaceAddrSize;
+    struct sockaddr_in* groupIpAddr;
+    struct sockaddr_in* interfaceIpAddr;
+    struct ip_mreq mreq;
+
+    struct sockaddr_in6* groupIp6Addr;
+    struct ipv6_mreq mreq6;
+
+    memset(&groupAddr, 0, sizeof(groupAddr));
+    memset(&interfaceAddr, 0, sizeof(interfaceAddr));
+
+    if (netty_unix_socket_initSockaddr(env, ipv6, groupAddress, scopeId, 0, &groupAddr, &groupAddrSize) == -1) {
+        netty_unix_errors_throwIOException(env, "Could not init sockaddr for groupAddress");
+        return;
+    }
+
+    switch (groupAddr.ss_family) {
+    case AF_INET:
+        if (netty_unix_socket_initSockaddr(env, ipv6, interfaceAddress, scopeId, 0, &interfaceAddr, &interfaceAddrSize) == -1) {
+            netty_unix_errors_throwIOException(env, "Could not init sockaddr for interfaceAddr");
+            return;
+        }
+
+        interfaceIpAddr = (struct sockaddr_in*) &interfaceAddr;
+        groupIpAddr = (struct sockaddr_in*) &groupAddr;
+
+        memcpy(&mreq.imr_multiaddr, &groupIpAddr->sin_addr, sizeof(groupIpAddr->sin_addr));
+        memcpy(&mreq.imr_interface, &interfaceIpAddr->sin_addr, sizeof(interfaceIpAddr->sin_addr));
+        netty_unix_socket_setOption(env, fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
+        break;
+    case AF_INET6:
+        if (interfaceIndex == -1) {
+            netty_unix_errors_throwIOException(env, "Unable to find network index");
+            return;
+        }
+        mreq6.ipv6mr_interface = interfaceIndex;
+
+        groupIp6Addr = (struct sockaddr_in6*) &groupAddr;
+        memcpy(&mreq6.ipv6mr_multiaddr, &groupIp6Addr->sin6_addr, sizeof(groupIp6Addr->sin6_addr));
+        netty_unix_socket_setOption(env, fd, IPPROTO_IPV6, IPV6_JOIN_GROUP, &mreq6, sizeof(mreq6));
+        break;
+    default:
+        netty_unix_errors_throwIOException(env, "Address family not supported");
+        break;
+    }
+}
+
+static void netty_epoll_linuxsocket_joinSsmGroup(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6, jbyteArray groupAddress, jbyteArray interfaceAddress, jint scopeId, jint interfaceIndex, jbyteArray sourceAddress) {
+    struct sockaddr_storage groupAddr;
+    socklen_t groupAddrSize;
+    struct sockaddr_storage interfaceAddr;
+    socklen_t interfaceAddrSize;
+    struct sockaddr_storage sourceAddr;
+    socklen_t sourceAddrSize;
+    struct sockaddr_in* groupIpAddr;
+    struct sockaddr_in* interfaceIpAddr;
+    struct sockaddr_in* sourceIpAddr;
+    struct ip_mreq_source mreq;
+
+    struct group_source_req mreq6;
+
+    memset(&groupAddr, 0, sizeof(groupAddr));
+    memset(&sourceAddr, 0, sizeof(sourceAddr));
+    memset(&interfaceAddr, 0, sizeof(interfaceAddr));
+
+    if (netty_unix_socket_initSockaddr(env, ipv6, groupAddress, scopeId, 0, &groupAddr, &groupAddrSize) == -1) {
+        netty_unix_errors_throwIOException(env, "Could not init sockaddr for groupAddress");
+        return;
+    }
+
+    if (netty_unix_socket_initSockaddr(env, ipv6, sourceAddress, scopeId, 0, &sourceAddr, &sourceAddrSize) == -1) {
+        netty_unix_errors_throwIOException(env, "Could not init sockaddr for sourceAddress");
+        return;
+    }
+
+    switch (groupAddr.ss_family) {
+    case AF_INET:
+        if (netty_unix_socket_initSockaddr(env, ipv6, interfaceAddress, scopeId, 0, &interfaceAddr, &interfaceAddrSize) == -1) {
+            netty_unix_errors_throwIOException(env, "Could not init sockaddr for interfaceAddress");
+            return;
+        }
+        interfaceIpAddr = (struct sockaddr_in*) &interfaceAddr;
+        groupIpAddr = (struct sockaddr_in*) &groupAddr;
+        sourceIpAddr = (struct sockaddr_in*) &sourceAddr;
+        memcpy(&mreq.imr_multiaddr, &groupIpAddr->sin_addr, sizeof(groupIpAddr->sin_addr));
+        memcpy(&mreq.imr_interface, &interfaceIpAddr->sin_addr, sizeof(interfaceIpAddr->sin_addr));
+        memcpy(&mreq.imr_sourceaddr, &sourceIpAddr->sin_addr, sizeof(sourceIpAddr->sin_addr));
+        netty_unix_socket_setOption(env, fd, IPPROTO_IP, IP_ADD_SOURCE_MEMBERSHIP, &mreq, sizeof(mreq));
+        break;
+    case AF_INET6:
+        if (interfaceIndex == -1) {
+            netty_unix_errors_throwIOException(env, "Unable to find network index");
+            return;
+        }
+        mreq6.gsr_group = groupAddr;
+        mreq6.gsr_interface = interfaceIndex;
+        mreq6.gsr_source = sourceAddr;
+        netty_unix_socket_setOption(env, fd, IPPROTO_IPV6, MCAST_JOIN_SOURCE_GROUP, &mreq6, sizeof(mreq6));
+        break;
+    default:
+        netty_unix_errors_throwIOException(env, "Address family not supported");
+        break;
+    }
+}
+
+static void netty_epoll_linuxsocket_leaveGroup(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6, jbyteArray groupAddress, jbyteArray interfaceAddress, jint scopeId, jint interfaceIndex) {
+    struct sockaddr_storage groupAddr;
+    socklen_t groupAddrSize;
+
+    struct sockaddr_storage interfaceAddr;
+    socklen_t interfaceAddrSize;
+    struct sockaddr_in* groupIpAddr;
+    struct sockaddr_in* interfaceIpAddr;
+    struct ip_mreq mreq;
+
+    struct sockaddr_in6* groupIp6Addr;
+    struct ipv6_mreq mreq6;
+
+    memset(&groupAddr, 0, sizeof(groupAddr));
+    memset(&interfaceAddr, 0, sizeof(interfaceAddr));
+
+    if (netty_unix_socket_initSockaddr(env, ipv6, groupAddress, scopeId, 0, &groupAddr, &groupAddrSize) == -1) {
+        netty_unix_errors_throwIOException(env, "Could not init sockaddr for groupAddress");
+        return;
+    }
+
+    switch (groupAddr.ss_family) {
+    case AF_INET:
+        if (netty_unix_socket_initSockaddr(env, ipv6, interfaceAddress, scopeId, 0, &interfaceAddr, &interfaceAddrSize) == -1) {
+            netty_unix_errors_throwIOException(env, "Could not init sockaddr for interfaceAddress");
+            return;
+        }
+        interfaceIpAddr = (struct sockaddr_in*) &interfaceAddr;
+        groupIpAddr = (struct sockaddr_in*) &groupAddr;
+
+        memcpy(&mreq.imr_multiaddr, &groupIpAddr->sin_addr, sizeof(groupIpAddr->sin_addr));
+        memcpy(&mreq.imr_interface, &interfaceIpAddr->sin_addr, sizeof(interfaceIpAddr->sin_addr));
+        netty_unix_socket_setOption(env, fd, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(mreq));
+        break;
+    case AF_INET6:
+        if (interfaceIndex == -1) {
+            netty_unix_errors_throwIOException(env, "Unable to find network index");
+            return;
+        }
+        mreq6.ipv6mr_interface = interfaceIndex;
+
+        groupIp6Addr = (struct sockaddr_in6*) &groupAddr;
+        memcpy(&mreq6.ipv6mr_multiaddr, &groupIp6Addr->sin6_addr, sizeof(groupIp6Addr->sin6_addr));
+        netty_unix_socket_setOption(env, fd, IPPROTO_IPV6, IPV6_LEAVE_GROUP, &mreq6, sizeof(mreq6));
+        break;
+    default:
+        netty_unix_errors_throwIOException(env, "Address family not supported");
+        break;
+    }
+}
+
+static void netty_epoll_linuxsocket_leaveSsmGroup(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6, jbyteArray groupAddress, jbyteArray interfaceAddress, jint scopeId, jint interfaceIndex, jbyteArray sourceAddress) {
+    struct sockaddr_storage groupAddr;
+    socklen_t groupAddrSize;
+    struct sockaddr_storage interfaceAddr;
+    socklen_t interfaceAddrSize;
+    struct sockaddr_storage sourceAddr;
+    socklen_t sourceAddrSize;
+    struct sockaddr_in* groupIpAddr;
+    struct sockaddr_in* interfaceIpAddr;
+    struct sockaddr_in* sourceIpAddr;
+
+    struct ip_mreq_source mreq;
+    struct group_source_req mreq6;
+
+    memset(&groupAddr, 0, sizeof(groupAddr));
+    memset(&sourceAddr, 0, sizeof(sourceAddr));
+    memset(&interfaceAddr, 0, sizeof(interfaceAddr));
+
+
+    if (netty_unix_socket_initSockaddr(env, ipv6, groupAddress, scopeId, 0, &groupAddr, &groupAddrSize) == -1) {
+        netty_unix_errors_throwIOException(env, "Could not init sockaddr for groupAddress");
+        return;
+    }
+
+    if (netty_unix_socket_initSockaddr(env, ipv6, sourceAddress, scopeId, 0, &sourceAddr, &sourceAddrSize) == -1) {
+        netty_unix_errors_throwIOException(env, "Could not init sockaddr for sourceAddress");
+        return;
+    }
+
+    switch (groupAddr.ss_family) {
+    case AF_INET:
+        if (netty_unix_socket_initSockaddr(env, ipv6, interfaceAddress, scopeId, 0, &interfaceAddr, &interfaceAddrSize) == -1) {
+            netty_unix_errors_throwIOException(env, "Could not init sockaddr for interfaceAddress");
+            return;
+        }
+        interfaceIpAddr = (struct sockaddr_in*) &interfaceAddr;
+
+        groupIpAddr = (struct sockaddr_in*) &groupAddr;
+        sourceIpAddr = (struct sockaddr_in*) &sourceAddr;
+        memcpy(&mreq.imr_multiaddr, &groupIpAddr->sin_addr, sizeof(groupIpAddr->sin_addr));
+        memcpy(&mreq.imr_interface, &interfaceIpAddr->sin_addr, sizeof(interfaceIpAddr->sin_addr));
+        memcpy(&mreq.imr_sourceaddr, &sourceIpAddr->sin_addr, sizeof(sourceIpAddr->sin_addr));
+        netty_unix_socket_setOption(env, fd, IPPROTO_IP, IP_DROP_SOURCE_MEMBERSHIP, &mreq, sizeof(mreq));
+        break;
+    case AF_INET6:
+        if (interfaceIndex == -1) {
+            netty_unix_errors_throwIOException(env, "Unable to find network index");
+            return;
+        }
+
+        mreq6.gsr_group = groupAddr;
+        mreq6.gsr_interface = interfaceIndex;
+        mreq6.gsr_source = sourceAddr;
+        netty_unix_socket_setOption(env, fd, IPPROTO_IPV6, MCAST_LEAVE_SOURCE_GROUP, &mreq6, sizeof(mreq6));
+        break;
+    default:
+        netty_unix_errors_throwIOException(env, "Address family not supported");
+        break;
+    }
+}
+
+static void netty_epoll_linuxsocket_setTcpMd5Sig(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6, jbyteArray address, jint scopeId, jbyteArray key) {
     struct sockaddr_storage addr;
     socklen_t addrSize;
-    if (netty_unix_socket_initSockaddr(env, address, scopeId, 0, &addr, &addrSize) == -1) {
+
+    memset(&addr, 0, sizeof(addr));
+
+    if (netty_unix_socket_initSockaddr(env, ipv6, address, scopeId, 0, &addr, &addrSize) == -1) {
+        netty_unix_errors_throwIOException(env, "Could not init sockaddr");
         return;
     }
 
@@ -154,7 +417,49 @@ static void netty_epoll_linuxsocket_setTcpMd5Sig(JNIEnv* env, jclass clazz, jint
     }
 
     if (setsockopt(fd, IPPROTO_TCP, TCP_MD5SIG, &md5sig, sizeof(md5sig)) < 0) {
-        netty_unix_errors_throwChannelExceptionErrorNo(env, "setsockopt() failed: ", errno);
+        netty_unix_errors_throwIOExceptionErrorNo(env, "setsockopt() failed: ", errno);
+    }
+}
+
+static int netty_epoll_linuxsocket_getInterface(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6) {
+    if (ipv6 == JNI_TRUE) {
+        int optval;
+        if (netty_unix_socket_getOption(env, fd, IPPROTO_IPV6, IPV6_MULTICAST_IF, &optval, sizeof(optval)) == -1) {
+            return -1;
+        }
+        return optval;
+    } else {
+        struct in_addr optval;
+        if (netty_unix_socket_getOption(env, fd, IPPROTO_IP, IP_MULTICAST_IF, &optval, sizeof(optval)) == -1) {
+            return -1;
+        }
+
+        return ntohl(optval.s_addr);
+    }
+}
+
+static jint netty_epoll_linuxsocket_getTimeToLive(JNIEnv* env, jclass clazz, jint fd) {
+    int optval;
+    if (netty_unix_socket_getOption(env, fd, IPPROTO_IP, IP_TTL, &optval, sizeof(optval)) == -1) {
+        return -1;
+    }
+    return optval;
+}
+
+
+static jint netty_epoll_linuxsocket_getIpMulticastLoop(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6) {
+    if (ipv6 == JNI_TRUE) {
+        u_int optval;
+        if (netty_unix_socket_getOption(env, fd, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, &optval, sizeof(optval)) == -1) {
+            return -1;
+        }
+        return (jint) optval;
+    } else {
+        u_char optval;
+        if (netty_unix_socket_getOption(env, fd, IPPROTO_IP, IP_MULTICAST_LOOP, &optval, sizeof(optval)) == -1) {
+            return -1;
+        }
+        return (jint) optval;
     }
 }
 
@@ -357,6 +662,12 @@ static jlong netty_epoll_linuxsocket_sendFile(JNIEnv* env, jclass clazz, jint fd
 
 // JNI Method Registration Table Begin
 static const JNINativeMethod fixed_method_table[] = {
+  { "setTimeToLive", "(II)V", (void *) netty_epoll_linuxsocket_setTimeToLive },
+  { "getTimeToLive", "(I)I", (void *) netty_epoll_linuxsocket_getTimeToLive },
+  { "setInterface", "(IZ[BII)V", (void *) netty_epoll_linuxsocket_setInterface },
+  { "getInterface", "(IZ)I", (void *) netty_epoll_linuxsocket_getInterface },
+  { "setIpMulticastLoop", "(IZI)V", (void * ) netty_epoll_linuxsocket_setIpMulticastLoop },
+  { "getIpMulticastLoop", "(IZ)I", (void * ) netty_epoll_linuxsocket_getIpMulticastLoop },
   { "setTcpCork", "(II)V", (void *) netty_epoll_linuxsocket_setTcpCork },
   { "setSoBusyPoll", "(II)V", (void *) netty_epoll_linuxsocket_setSoBusyPoll },
   { "setTcpQuickAck", "(II)V", (void *) netty_epoll_linuxsocket_setTcpQuickAck },
@@ -385,7 +696,11 @@ static const JNINativeMethod fixed_method_table[] = {
   { "isIpTransparent", "(I)I", (void *) netty_epoll_linuxsocket_isIpTransparent },
   { "isIpRecvOrigDestAddr", "(I)I", (void *) netty_epoll_linuxsocket_isIpRecvOrigDestAddr },
   { "getTcpInfo", "(I[J)V", (void *) netty_epoll_linuxsocket_getTcpInfo },
-  { "setTcpMd5Sig", "(I[BI[B)V", (void *) netty_epoll_linuxsocket_setTcpMd5Sig }
+  { "setTcpMd5Sig", "(IZ[BI[B)V", (void *) netty_epoll_linuxsocket_setTcpMd5Sig },
+  { "joinGroup", "(IZ[B[BII)V", (void *) netty_epoll_linuxsocket_joinGroup },
+  { "joinSsmGroup", "(IZ[B[BII[B)V", (void *) netty_epoll_linuxsocket_joinSsmGroup },
+  { "leaveGroup", "(IZ[B[BII)V", (void *) netty_epoll_linuxsocket_leaveGroup },
+  { "leaveSsmGroup", "(IZ[B[BII[B)V", (void *) netty_epoll_linuxsocket_leaveSsmGroup }
   // "sendFile" has a dynamic signature
 };
 
@@ -396,113 +711,83 @@ static jint dynamicMethodsTableSize() {
 }
 
 static JNINativeMethod* createDynamicMethodsTable(const char* packagePrefix) {
-    JNINativeMethod* dynamicMethods = malloc(sizeof(JNINativeMethod) * dynamicMethodsTableSize());
+    char* dynamicTypeName = NULL;
+    size_t size = sizeof(JNINativeMethod) * dynamicMethodsTableSize();
+    JNINativeMethod* dynamicMethods = malloc(size);
+    if (dynamicMethods == NULL) {
+        return NULL;
+    }
+    memset(dynamicMethods, 0, size);
     memcpy(dynamicMethods, fixed_method_table, sizeof(fixed_method_table));
+  
     JNINativeMethod* dynamicMethod = &dynamicMethods[fixed_method_table_size];
-    char* dynamicTypeName = netty_unix_util_prepend(packagePrefix, "io/netty/channel/unix/PeerCredentials;");
+    NETTY_PREPEND(packagePrefix, "io/netty/channel/unix/PeerCredentials;", dynamicTypeName, error);
+    NETTY_PREPEND("(I)L", dynamicTypeName,  dynamicMethod->signature, error);
     dynamicMethod->name = "getPeerCredentials";
-    dynamicMethod->signature = netty_unix_util_prepend("(I)L", dynamicTypeName);
     dynamicMethod->fnPtr = (void *) netty_epoll_linuxsocket_getPeerCredentials;
-    free(dynamicTypeName);
+    netty_unix_util_free_dynamic_name(&dynamicTypeName);
 
     ++dynamicMethod;
-    dynamicTypeName = netty_unix_util_prepend(packagePrefix, "io/netty/channel/DefaultFileRegion;JJJ)J");
+    NETTY_PREPEND(packagePrefix, "io/netty/channel/DefaultFileRegion;JJJ)J", dynamicTypeName, error);
+    NETTY_PREPEND("(IL", dynamicTypeName,  dynamicMethod->signature, error);
     dynamicMethod->name = "sendFile";
-    dynamicMethod->signature = netty_unix_util_prepend("(IL", dynamicTypeName);
     dynamicMethod->fnPtr = (void *) netty_epoll_linuxsocket_sendFile;
-    free(dynamicTypeName);
+    netty_unix_util_free_dynamic_name(&dynamicTypeName);
     return dynamicMethods;
+error:
+    free(dynamicTypeName);
+    netty_unix_util_free_dynamic_methods_table(dynamicMethods, fixed_method_table_size, dynamicMethodsTableSize());
+    return NULL;
 }
 
-static void freeDynamicMethodsTable(JNINativeMethod* dynamicMethods) {
-    jint fullMethodTableSize = dynamicMethodsTableSize();
-    jint i = fixed_method_table_size;
-    for (; i < fullMethodTableSize; ++i) {
-        free(dynamicMethods[i].signature);
-    }
-    free(dynamicMethods);
-}
 // JNI Method Registration Table End
 
 jint netty_epoll_linuxsocket_JNI_OnLoad(JNIEnv* env, const char* packagePrefix) {
+    int ret = JNI_ERR;
+    char* nettyClassName = NULL;
+    jclass fileRegionCls = NULL;
+    jclass fileChannelCls = NULL;
+    jclass fileDescriptorCls = NULL;
+    // Register the methods which are not referenced by static member variables
     JNINativeMethod* dynamicMethods = createDynamicMethodsTable(packagePrefix);
+    if (dynamicMethods == NULL) {
+        goto done;
+    }
     if (netty_unix_util_register_natives(env,
             packagePrefix,
             "io/netty/channel/epoll/LinuxSocket",
             dynamicMethods,
             dynamicMethodsTableSize()) != 0) {
-        freeDynamicMethodsTable(dynamicMethods);
-        return JNI_ERR;
+        goto done;
     }
-    freeDynamicMethodsTable(dynamicMethods);
-    dynamicMethods = NULL;
 
-    char* nettyClassName = netty_unix_util_prepend(packagePrefix, "io/netty/channel/unix/PeerCredentials");
-    jclass localPeerCredsClass = (*env)->FindClass(env, nettyClassName);
-    free(nettyClassName);
-    nettyClassName = NULL;
-    if (localPeerCredsClass == NULL) {
-        // pending exception...
-        return JNI_ERR;
-    }
-    peerCredentialsClass = (jclass) (*env)->NewGlobalRef(env, localPeerCredsClass);
-    if (peerCredentialsClass == NULL) {
-        // out-of-memory!
-        netty_unix_errors_throwOutOfMemoryError(env);
-        return JNI_ERR;
-    }
-    peerCredentialsMethodId = (*env)->GetMethodID(env, peerCredentialsClass, "<init>", "(II[I)V");
-    if (peerCredentialsMethodId == NULL) {
-        netty_unix_errors_throwRuntimeException(env, "failed to get method ID: PeerCredentials.<init>(int, int, int[])");
-        return JNI_ERR;
-    }
+    NETTY_PREPEND(packagePrefix, "io/netty/channel/unix/PeerCredentials", nettyClassName, done);
+    NETTY_LOAD_CLASS(env, peerCredentialsClass, nettyClassName, done);
+    netty_unix_util_free_dynamic_name(&nettyClassName);
 
-    nettyClassName = netty_unix_util_prepend(packagePrefix, "io/netty/channel/DefaultFileRegion");
-    jclass fileRegionCls = (*env)->FindClass(env, nettyClassName);
-    free(nettyClassName);
-    nettyClassName = NULL;
-    if (fileRegionCls == NULL) {
-        return JNI_ERR;
-    }
-    fileChannelFieldId = (*env)->GetFieldID(env, fileRegionCls, "file", "Ljava/nio/channels/FileChannel;");
-    if (fileChannelFieldId == NULL) {
-        netty_unix_errors_throwRuntimeException(env, "failed to get field ID: DefaultFileRegion.file");
-        return JNI_ERR;
-    }
-    transferredFieldId = (*env)->GetFieldID(env, fileRegionCls, "transferred", "J");
-    if (transferredFieldId == NULL) {
-        netty_unix_errors_throwRuntimeException(env, "failed to get field ID: DefaultFileRegion.transferred");
-        return JNI_ERR;
-    }
+    NETTY_GET_METHOD(env, peerCredentialsClass, peerCredentialsMethodId, "<init>", "(II[I)V", done);
 
-    jclass fileChannelCls = (*env)->FindClass(env, "sun/nio/ch/FileChannelImpl");
-    if (fileChannelCls == NULL) {
-        // pending exception...
-        return JNI_ERR;
-    }
-    fileDescriptorFieldId = (*env)->GetFieldID(env, fileChannelCls, "fd", "Ljava/io/FileDescriptor;");
-    if (fileDescriptorFieldId == NULL) {
-        netty_unix_errors_throwRuntimeException(env, "failed to get field ID: FileChannelImpl.fd");
-        return JNI_ERR;
-    }
+    NETTY_PREPEND(packagePrefix, "io/netty/channel/DefaultFileRegion", nettyClassName, done);
+    NETTY_FIND_CLASS(env, fileRegionCls, nettyClassName, done);
+    netty_unix_util_free_dynamic_name(&nettyClassName);
 
-    jclass fileDescriptorCls = (*env)->FindClass(env, "java/io/FileDescriptor");
-    if (fileDescriptorCls == NULL) {
-        // pending exception...
-        return JNI_ERR;
-    }
-    fdFieldId = (*env)->GetFieldID(env, fileDescriptorCls, "fd", "I");
-    if (fdFieldId == NULL) {
-        netty_unix_errors_throwRuntimeException(env, "failed to get field ID: FileDescriptor.fd");
-        return JNI_ERR;
-    }
+    NETTY_GET_FIELD(env, fileRegionCls, fileChannelFieldId, "file", "Ljava/nio/channels/FileChannel;", done);
+    NETTY_GET_FIELD(env, fileRegionCls, transferredFieldId, "transferred", "J", done);
+
+    NETTY_FIND_CLASS(env, fileChannelCls, "sun/nio/ch/FileChannelImpl", done);
+    NETTY_GET_FIELD(env, fileChannelCls, fileDescriptorFieldId, "fd", "Ljava/io/FileDescriptor;", done);
 
-    return NETTY_JNI_VERSION;
+    NETTY_FIND_CLASS(env, fileDescriptorCls, "java/io/FileDescriptor", done);
+    NETTY_GET_FIELD(env, fileDescriptorCls, fdFieldId, "fd", "I", done);
+
+    ret = NETTY_JNI_VERSION;
+done:
+    netty_unix_util_free_dynamic_methods_table(dynamicMethods, fixed_method_table_size, dynamicMethodsTableSize());
+    free(nettyClassName);
+
+    return ret;
 }
 
 void netty_epoll_linuxsocket_JNI_OnUnLoad(JNIEnv* env) {
-    if (peerCredentialsClass != NULL) {
-        (*env)->DeleteGlobalRef(env, peerCredentialsClass);
-        peerCredentialsClass = NULL;
-    }
+    NETTY_UNLOAD_CLASS(env, peerCredentialsClass);
 }
diff --git a/transport-native-epoll/src/main/c/netty_epoll_native.c b/transport-native-epoll/src/main/c/netty_epoll_native.c
index 96e55e1..42c9446 100644
--- a/transport-native-epoll/src/main/c/netty_epoll_native.c
+++ b/transport-native-epoll/src/main/c/netty_epoll_native.c
@@ -36,6 +36,9 @@
 #include <inttypes.h>
 #include <link.h>
 #include <time.h>
+// Needed to be able to use syscalls directly and so not depend on newer GLIBC versions
+#include <linux/net.h>
+#include <sys/syscall.h>
 
 #include "netty_epoll_linuxsocket.h"
 #include "netty_unix_buffer.h"
@@ -54,19 +57,43 @@
 // optional
 extern int epoll_create1(int flags) __attribute__((weak));
 
-#ifdef IO_NETTY_SENDMMSG_NOT_FOUND
-extern int sendmmsg(int sockfd, struct mmsghdr* msgvec, unsigned int vlen, unsigned int flags) __attribute__((weak));
-
 #ifndef __USE_GNU
 struct mmsghdr {
     struct msghdr msg_hdr;  /* Message header */
     unsigned int  msg_len;  /* Number of bytes transmitted */
 };
 #endif
+
+// All linux syscall numbers are stable so this is safe.
+#ifndef SYS_recvmmsg
+// Only support SYS_recvmmsg for __x86_64__ / __i386__ for now
+#if defined(__x86_64__)
+// See https://github.com/torvalds/linux/blob/v5.4/arch/x86/entry/syscalls/syscall_64.tbl
+#define SYS_recvmmsg 299
+#elif defined(__i386__)
+// See https://github.com/torvalds/linux/blob/v5.4/arch/x86/entry/syscalls/syscall_32.tbl
+#define SYS_recvmmsg 337
+#else
+#define SYS_recvmmsg -1
 #endif
+#endif // SYS_recvmmsg
+
+#ifndef SYS_sendmmsg
+// Only support SYS_sendmmsg for __x86_64__ / __i386__ for now
+#if defined(__x86_64__)
+// See https://github.com/torvalds/linux/blob/v5.4/arch/x86/entry/syscalls/syscall_64.tbl
+#define SYS_sendmmsg 307
+#elif defined(__i386__)
+// See https://github.com/torvalds/linux/blob/v5.4/arch/x86/entry/syscalls/syscall_32.tbl
+#define SYS_sendmmsg 345
+#else
+#define SYS_sendmmsg -1
+#endif
+#endif // SYS_sendmmsg
 
 // Those are initialized in the init(...) method and cached for performance reasons
 static jfieldID packetAddrFieldId = NULL;
+static jfieldID packetAddrLenFieldId = NULL;
 static jfieldID packetScopeIdFieldId = NULL;
 static jfieldID packetPortFieldId = NULL;
 static jfieldID packetMemoryAddressFieldId = NULL;
@@ -116,10 +143,27 @@ static jint netty_epoll_native_timerFd(JNIEnv* env, jclass clazz) {
 }
 
 static void netty_epoll_native_eventFdWrite(JNIEnv* env, jclass clazz, jint fd, jlong value) {
-    jint eventFD = eventfd_write(fd, (eventfd_t) value);
-
-    if (eventFD < 0) {
-        netty_unix_errors_throwChannelExceptionErrorNo(env, "eventfd_write() failed: ", errno);
+    uint64_t val;
+
+    for (;;) {
+        jint ret = eventfd_write(fd, (eventfd_t) value);
+
+        if (ret < 0) {
+            // We need to read before we can write again, let's try to read and then write again and if this
+            // fails we will bail out.
+            //
+            // See http://man7.org/linux/man-pages/man2/eventfd.2.html.
+            if (errno == EAGAIN) {
+                if (eventfd_read(fd, &val) == 0 || errno == EAGAIN) {
+                    // Try again
+                    continue;
+                }
+                netty_unix_errors_throwChannelExceptionErrorNo(env, "eventfd_read(...) failed: ", errno);
+            } else {
+                netty_unix_errors_throwChannelExceptionErrorNo(env, "eventfd_write(...) failed: ", errno);
+            }
+        }
+        break;
     }
 }
 
@@ -169,50 +213,46 @@ static jint netty_epoll_native_epollCreate(JNIEnv* env, jclass clazz) {
     return efd;
 }
 
-static jint netty_epoll_native_epollWait0(JNIEnv* env, jclass clazz, jint efd, jlong address, jint len, jint timerFd, jint tvSec, jint tvNsec) {
+static void netty_epoll_native_timerFdSetTime(JNIEnv* env, jclass clazz, jint timerFd, jint tvSec, jint tvNsec) {
+    struct itimerspec ts;
+    memset(&ts.it_interval, 0, sizeof(struct timespec));
+    ts.it_value.tv_sec = tvSec;
+    ts.it_value.tv_nsec = tvNsec;
+    if (timerfd_settime(timerFd, 0, &ts, NULL) < 0) {
+        netty_unix_errors_throwIOExceptionErrorNo(env, "timerfd_settime() failed: ", errno);
+    }
+}
+
+static jint netty_epoll_native_epollWait(JNIEnv* env, jclass clazz, jint efd, jlong address, jint len, jint timeout) {
     struct epoll_event *ev = (struct epoll_event*) (intptr_t) address;
     int result, err;
 
-    if (tvSec == 0 && tvNsec == 0) {
-        // Zeros = poll (aka return immediately).
-        do {
-            result = epoll_wait(efd, ev, len, 0);
-            if (result >= 0) {
-                return result;
-            }
-        } while((err = errno) == EINTR);
-    } else {
-        // only reschedule the timer if there is a newer event.
-        // -1 is a special value used by EpollEventLoop.
-        if (tvSec != ((jint) -1) && tvNsec != ((jint) -1)) {
-            struct itimerspec ts;
-            memset(&ts.it_interval, 0, sizeof(struct timespec));
-            ts.it_value.tv_sec = tvSec;
-            ts.it_value.tv_nsec = tvNsec;
-            if (timerfd_settime(timerFd, 0, &ts, NULL) < 0) {
-                netty_unix_errors_throwChannelExceptionErrorNo(env, "timerfd_settime() failed: ", errno);
-                return -1;
-            }
+    do {
+        result = epoll_wait(efd, ev, len, timeout);
+        if (result >= 0) {
+            return result;
         }
-        do {
-            result = epoll_wait(efd, ev, len, -1);
-            if (result > 0) {
-                // Detect timeout, and preserve the epoll_wait API.
-                if (result == 1 && ev[0].data.fd == timerFd) {
-                    // We assume that timerFD is in ET mode. So we must consume this event to ensure we are notified
-                    // of future timer events because ET mode only notifies a single time until the event is consumed.
-                    uint64_t timerFireCount;
-                    // We don't care what the result is. We just want to consume the wakeup event and reset ET.
-                    result = read(timerFd, &timerFireCount, sizeof(uint64_t));
-                    return 0;
-                }
-                return result;
-            }
-        } while((err = errno) == EINTR);
-    }
+    } while((err = errno) == EINTR);
     return -err;
 }
 
+// This method is deprecated!
+static jint netty_epoll_native_epollWait0(JNIEnv* env, jclass clazz, jint efd, jlong address, jint len, jint timerFd, jint tvSec, jint tvNsec) {
+    // only reschedule the timer if there is a newer event.
+    // -1 is a special value used by EpollEventLoop.
+    if (tvSec != ((jint) -1) && tvNsec != ((jint) -1)) {
+    	struct itimerspec ts;
+    	memset(&ts.it_interval, 0, sizeof(struct timespec));
+    	ts.it_value.tv_sec = tvSec;
+    	ts.it_value.tv_nsec = tvNsec;
+    	if (timerfd_settime(timerFd, 0, &ts, NULL) < 0) {
+    		netty_unix_errors_throwChannelExceptionErrorNo(env, "timerfd_settime() failed: ", errno);
+    		return -1;
+    	}
+    }
+    return netty_epoll_native_epollWait(env, clazz, efd, address, len, -1);
+}
+
 static inline void cpu_relax() {
 #if defined(__x86_64__)
     asm volatile("pause\n": : :"memory");
@@ -265,7 +305,7 @@ static jint netty_epoll_native_epollCtlDel0(JNIEnv* env, jclass clazz, jint efd,
     return res;
 }
 
-static jint netty_epoll_native_sendmmsg0(JNIEnv* env, jclass clazz, jint fd, jobjectArray packets, jint offset, jint len) {
+static jint netty_epoll_native_sendmmsg0(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6, jobjectArray packets, jint offset, jint len) {
     struct mmsghdr msg[len];
     struct sockaddr_storage addr[len];
     socklen_t addrSize;
@@ -277,24 +317,28 @@ static jint netty_epoll_native_sendmmsg0(JNIEnv* env, jclass clazz, jint fd, job
 
         jobject packet = (*env)->GetObjectArrayElement(env, packets, i + offset);
         jbyteArray address = (jbyteArray) (*env)->GetObjectField(env, packet, packetAddrFieldId);
-        jint scopeId = (*env)->GetIntField(env, packet, packetScopeIdFieldId);
-        jint port = (*env)->GetIntField(env, packet, packetPortFieldId);
+        jint addrLen = (*env)->GetIntField(env, packet, packetAddrLenFieldId);
 
-        if (netty_unix_socket_initSockaddr(env, address, scopeId, port, &addr[i], &addrSize) == -1) {
-            return -1;
-        }
+        if (addrLen != 0) {
+            jint scopeId = (*env)->GetIntField(env, packet, packetScopeIdFieldId);
+            jint port = (*env)->GetIntField(env, packet, packetPortFieldId);
 
-        msg[i].msg_hdr.msg_name = &addr[i];
-        msg[i].msg_hdr.msg_namelen = addrSize;
+           if (netty_unix_socket_initSockaddr(env, ipv6, address, scopeId, port, &addr[i], &addrSize) == -1) {
+              return -1;
+           }
+           msg[i].msg_hdr.msg_name = &addr[i];
+           msg[i].msg_hdr.msg_namelen = addrSize;
+        }
 
         msg[i].msg_hdr.msg_iov = (struct iovec*) (intptr_t) (*env)->GetLongField(env, packet, packetMemoryAddressFieldId);
-        msg[i].msg_hdr.msg_iovlen = (*env)->GetIntField(env, packet, packetCountFieldId);;
+        msg[i].msg_hdr.msg_iovlen = (*env)->GetIntField(env, packet, packetCountFieldId);
     }
 
     ssize_t res;
     int err;
     do {
-       res = sendmmsg(fd, msg, len, 0);
+       // We directly use the syscall to prevent depending on GLIBC 2.14.
+       res = syscall(SYS_sendmmsg, fd, msg, len, 0);
        // keep on writing if it was interrupted
     } while (res == -1 && ((err = errno) == EINTR));
 
@@ -304,6 +348,72 @@ static jint netty_epoll_native_sendmmsg0(JNIEnv* env, jclass clazz, jint fd, job
     return (jint) res;
 }
 
+static jint netty_epoll_native_recvmmsg0(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6, jobjectArray packets, jint offset, jint len) {
+    struct mmsghdr msg[len];
+    memset(msg, 0, sizeof(msg));
+    struct sockaddr_storage addr[len];
+    int addrSize = sizeof(addr);
+    memset(addr, 0, addrSize);
+
+    int i;
+
+    for (i = 0; i < len; i++) {
+        jobject packet = (*env)->GetObjectArrayElement(env, packets, i + offset);
+        msg[i].msg_hdr.msg_iov = (struct iovec*) (intptr_t) (*env)->GetLongField(env, packet, packetMemoryAddressFieldId);
+        msg[i].msg_hdr.msg_iovlen = (*env)->GetIntField(env, packet, packetCountFieldId);
+
+        msg[i].msg_hdr.msg_name = addr + i;
+        msg[i].msg_hdr.msg_namelen = (socklen_t) addrSize;
+    }
+
+    ssize_t res;
+    int err;
+    do {
+        // We directly use the syscall to prevent depending on GLIBC 2.12.
+        res = syscall(SYS_recvmmsg, fd, &msg, len, 0, NULL);
+
+        // keep on reading if it was interrupted
+    } while (res == -1 && ((err = errno) == EINTR));
+
+    if (res < 0) {
+        return -err;
+    }
+
+    for (i = 0; i < res; i++) {
+        jobject packet = (*env)->GetObjectArrayElement(env, packets, i + offset);
+        jbyteArray address = (jbyteArray) (*env)->GetObjectField(env, packet, packetAddrFieldId);
+
+        (*env)->SetIntField(env, packet, packetCountFieldId, msg[i].msg_len);
+
+        struct sockaddr_storage* addr = (struct sockaddr_storage*) msg[i].msg_hdr.msg_name;
+
+        if (addr->ss_family == AF_INET) {
+            struct sockaddr_in* ipaddr = (struct sockaddr_in*) addr;
+
+            (*env)->SetByteArrayRegion(env, address, 0, 4, (jbyte*) &ipaddr->sin_addr.s_addr);
+            (*env)->SetIntField(env, packet, packetAddrLenFieldId, 4);
+            (*env)->SetIntField(env, packet, packetScopeIdFieldId, 0);
+            (*env)->SetIntField(env, packet, packetPortFieldId, ntohs(ipaddr->sin_port));
+        } else {
+              int addrLen = netty_unix_socket_ipAddressLength(addr);
+              struct sockaddr_in6* ip6addr = (struct sockaddr_in6*) addr;
+
+              if (addrLen == 4) {
+                  // IPV4 mapped IPV6 address
+                  jbyte* addr = (jbyte*) &ip6addr->sin6_addr.s6_addr;
+                  (*env)->SetByteArrayRegion(env, address, 0, 4, addr + 12);
+              } else {
+                  (*env)->SetByteArrayRegion(env, address, 0, 16, (jbyte*) &ip6addr->sin6_addr.s6_addr);
+              }
+              (*env)->SetIntField(env, packet, packetAddrLenFieldId, addrLen);
+              (*env)->SetIntField(env, packet, packetScopeIdFieldId, ip6addr->sin6_scope_id);
+              (*env)->SetIntField(env, packet, packetPortFieldId, ntohs(ip6addr->sin6_port));
+        }
+    }
+
+    return (jint) res;
+}
+
 static jstring netty_epoll_native_kernelVersion(JNIEnv* env, jclass clazz) {
     struct utsname name;
 
@@ -314,14 +424,28 @@ static jstring netty_epoll_native_kernelVersion(JNIEnv* env, jclass clazz) {
     netty_unix_errors_throwRuntimeExceptionErrorNo(env, "uname() failed: ", errno);
     return NULL;
 }
-
 static jboolean netty_epoll_native_isSupportingSendmmsg(JNIEnv* env, jclass clazz) {
-    // Use & to avoid warnings with -Wtautological-pointer-compare when sendmmsg is
-    // not weakly defined.
-    if (&sendmmsg != NULL) {
-        return JNI_TRUE;
+    if (SYS_sendmmsg == -1) {
+        return JNI_FALSE;
     }
-    return JNI_FALSE;
+    if (syscall(SYS_sendmmsg, -1, NULL, 0, 0) == -1) {
+        if (errno == ENOSYS) {
+            return JNI_FALSE;
+        }
+    }
+    return JNI_TRUE;
+}
+
+static jboolean netty_epoll_native_isSupportingRecvmmsg(JNIEnv* env, jclass clazz) {
+    if (SYS_recvmmsg == -1) {
+        return JNI_FALSE;
+    }
+    if (syscall(SYS_recvmmsg, -1, NULL, 0, 0, NULL) == -1) {
+        if (errno == ENOSYS) {
+            return JNI_FALSE;
+        }
+    }
+    return JNI_TRUE;
 }
 
 static jboolean netty_epoll_native_isSupportingTcpFastopen(JNIEnv* env, jclass clazz) {
@@ -368,7 +492,7 @@ static jint netty_epoll_native_splice0(JNIEnv* env, jclass clazz, jint fd, jlong
     loff_t off_out = (loff_t) offOut;
 
     loff_t* p_off_in = off_in >= 0 ? &off_in : NULL;
-    loff_t* p_off_out = off_in >= 0 ? &off_out : NULL;
+    loff_t* p_off_out = off_out >= 0 ? &off_out : NULL;
 
     do {
        res = splice(fd, p_off_in, fdOut, p_off_out, (size_t) len, SPLICE_F_NONBLOCK | SPLICE_F_MOVE);
@@ -402,6 +526,7 @@ static const JNINativeMethod statically_referenced_fixed_method_table[] = {
   { "epollerr", "()I", (void *) netty_epoll_native_epollerr },
   { "tcpMd5SigMaxKeyLen", "()I", (void *) netty_epoll_native_tcpMd5SigMaxKeyLen },
   { "isSupportingSendmmsg", "()Z", (void *) netty_epoll_native_isSupportingSendmmsg },
+  { "isSupportingRecvmmsg", "()Z", (void *) netty_epoll_native_isSupportingRecvmmsg },
   { "isSupportingTcpFastopen", "()Z", (void *) netty_epoll_native_isSupportingTcpFastopen },
   { "kernelVersion", "()Ljava/lang/String;", (void *) netty_epoll_native_kernelVersion }
 };
@@ -412,8 +537,10 @@ static const JNINativeMethod fixed_method_table[] = {
   { "eventFdWrite", "(IJ)V", (void *) netty_epoll_native_eventFdWrite },
   { "eventFdRead", "(I)V", (void *) netty_epoll_native_eventFdRead },
   { "timerFdRead", "(I)V", (void *) netty_epoll_native_timerFdRead },
+  { "timerFdSetTime", "(III)V", (void *) netty_epoll_native_timerFdSetTime },
   { "epollCreate", "()I", (void *) netty_epoll_native_epollCreate },
-  { "epollWait0", "(IJIIII)I", (void *) netty_epoll_native_epollWait0 },
+  { "epollWait0", "(IJIIII)I", (void *) netty_epoll_native_epollWait0 }, // This method is deprecated!
+  { "epollWait", "(IJII)I", (void *) netty_epoll_native_epollWait },
   { "epollBusyWait0", "(IJI)I", (void *) netty_epoll_native_epollBusyWait0 },
   { "epollCtlAdd0", "(III)I", (void *) netty_epoll_native_epollCtlAdd0 },
   { "epollCtlMod0", "(III)I", (void *) netty_epoll_native_epollCtlMod0 },
@@ -426,38 +553,53 @@ static const JNINativeMethod fixed_method_table[] = {
 static const jint fixed_method_table_size = sizeof(fixed_method_table) / sizeof(fixed_method_table[0]);
 
 static jint dynamicMethodsTableSize() {
-    return fixed_method_table_size + 1; // 1 is for the dynamic method signatures.
+    return fixed_method_table_size + 2; // 2 is for the dynamic method signatures.
 }
 
 static JNINativeMethod* createDynamicMethodsTable(const char* packagePrefix) {
-    JNINativeMethod* dynamicMethods = malloc(sizeof(JNINativeMethod) * dynamicMethodsTableSize());
+    char* dynamicTypeName = NULL;
+    size_t size = sizeof(JNINativeMethod) * dynamicMethodsTableSize();
+    JNINativeMethod* dynamicMethods = malloc(size);
+    if (dynamicMethods == NULL) {
+        return NULL;
+    }
+    memset(dynamicMethods, 0, size);
     memcpy(dynamicMethods, fixed_method_table, sizeof(fixed_method_table));
-    char* dynamicTypeName = netty_unix_util_prepend(packagePrefix, "io/netty/channel/epoll/NativeDatagramPacketArray$NativeDatagramPacket;II)I");
+    
     JNINativeMethod* dynamicMethod = &dynamicMethods[fixed_method_table_size];
+    NETTY_PREPEND(packagePrefix, "io/netty/channel/epoll/NativeDatagramPacketArray$NativeDatagramPacket;II)I", dynamicTypeName, error);
+    NETTY_PREPEND("(IZ[L", dynamicTypeName,  dynamicMethod->signature, error);
     dynamicMethod->name = "sendmmsg0";
-    dynamicMethod->signature = netty_unix_util_prepend("(I[L", dynamicTypeName);
     dynamicMethod->fnPtr = (void *) netty_epoll_native_sendmmsg0;
-    free(dynamicTypeName);
+    netty_unix_util_free_dynamic_name(&dynamicTypeName);
+
+    ++dynamicMethod;
+    NETTY_PREPEND(packagePrefix, "io/netty/channel/epoll/NativeDatagramPacketArray$NativeDatagramPacket;II)I", dynamicTypeName, error);
+    NETTY_PREPEND("(IZ[L", dynamicTypeName,  dynamicMethod->signature, error);
+    dynamicMethod->name = "recvmmsg0";
+    dynamicMethod->fnPtr = (void *) netty_epoll_native_recvmmsg0;
+    netty_unix_util_free_dynamic_name(&dynamicTypeName);
+
     return dynamicMethods;
+error:
+    free(dynamicTypeName);
+    netty_unix_util_free_dynamic_methods_table(dynamicMethods, fixed_method_table_size, dynamicMethodsTableSize());
+    return NULL;
 }
 
-static void freeDynamicMethodsTable(JNINativeMethod* dynamicMethods) {
-    jint fullMethodTableSize = dynamicMethodsTableSize();
-    jint i = fixed_method_table_size;
-    for (; i < fullMethodTableSize; ++i) {
-        free(dynamicMethods[i].signature);
-    }
-    free(dynamicMethods);
-}
 // JNI Method Registration Table End
 
 static jint netty_epoll_native_JNI_OnLoad(JNIEnv* env, const char* packagePrefix) {
+    int ret = JNI_ERR;
     int limitsOnLoadCalled = 0;
     int errorsOnLoadCalled = 0;
     int filedescriptorOnLoadCalled = 0;
     int socketOnLoadCalled = 0;
     int bufferOnLoadCalled = 0;
     int linuxsocketOnLoadCalled = 0;
+    char* nettyClassName = NULL;
+    jclass nativeDatagramPacketCls = NULL;
+    JNINativeMethod* dynamicMethods = NULL;
 
     // We must register the statically referenced methods first!
     if (netty_unix_util_register_natives(env,
@@ -465,116 +607,97 @@ static jint netty_epoll_native_JNI_OnLoad(JNIEnv* env, const char* packagePrefix
             "io/netty/channel/epoll/NativeStaticallyReferencedJniMethods",
             statically_referenced_fixed_method_table,
             statically_referenced_fixed_method_table_size) != 0) {
-        goto error;
+        goto done;
     }
     // Register the methods which are not referenced by static member variables
-    JNINativeMethod* dynamicMethods = createDynamicMethodsTable(packagePrefix);
+    dynamicMethods = createDynamicMethodsTable(packagePrefix);
+    if (dynamicMethods == NULL) {
+        goto done;
+    }
+
     if (netty_unix_util_register_natives(env,
             packagePrefix,
             "io/netty/channel/epoll/Native",
             dynamicMethods,
             dynamicMethodsTableSize()) != 0) {
-        freeDynamicMethodsTable(dynamicMethods);
-        goto error;
+        goto done;
     }
-    freeDynamicMethodsTable(dynamicMethods);
-    dynamicMethods = NULL;
     // Load all c modules that we depend upon
     if (netty_unix_limits_JNI_OnLoad(env, packagePrefix) == JNI_ERR) {
-        goto error;
+        goto done;
     }
     limitsOnLoadCalled = 1;
 
     if (netty_unix_errors_JNI_OnLoad(env, packagePrefix) == JNI_ERR) {
-        goto error;
+        goto done;
     }
     errorsOnLoadCalled = 1;
 
     if (netty_unix_filedescriptor_JNI_OnLoad(env, packagePrefix) == JNI_ERR) {
-        goto error;
+        goto done;
     }
     filedescriptorOnLoadCalled = 1;
 
     if (netty_unix_socket_JNI_OnLoad(env, packagePrefix) == JNI_ERR) {
-        goto error;
+        goto done;
     }
     socketOnLoadCalled = 1;
 
     if (netty_unix_buffer_JNI_OnLoad(env, packagePrefix) == JNI_ERR) {
-        goto error;
+        goto done;
     }
     bufferOnLoadCalled = 1;
 
     if (netty_epoll_linuxsocket_JNI_OnLoad(env, packagePrefix) == JNI_ERR) {
-        goto error;
+        goto done;
     }
     linuxsocketOnLoadCalled = 1;
 
     // Initialize this module
-    char* nettyClassName = netty_unix_util_prepend(packagePrefix, "io/netty/channel/epoll/NativeDatagramPacketArray$NativeDatagramPacket");
-    jclass nativeDatagramPacketCls = (*env)->FindClass(env, nettyClassName);
-    free(nettyClassName);
-    nettyClassName = NULL;
-    if (nativeDatagramPacketCls == NULL) {
-        // pending exception...
-        goto error;
-    }
+    NETTY_PREPEND(packagePrefix, "io/netty/channel/epoll/NativeDatagramPacketArray$NativeDatagramPacket", nettyClassName, done);
+    NETTY_FIND_CLASS(env, nativeDatagramPacketCls, nettyClassName, done);
+    netty_unix_util_free_dynamic_name(&nettyClassName);
 
-    packetAddrFieldId = (*env)->GetFieldID(env, nativeDatagramPacketCls, "addr", "[B");
-    if (packetAddrFieldId == NULL) {
-        netty_unix_errors_throwRuntimeException(env, "failed to get field ID: NativeDatagramPacket.addr");
-        goto error;
-    }
-    packetScopeIdFieldId = (*env)->GetFieldID(env, nativeDatagramPacketCls, "scopeId", "I");
-    if (packetScopeIdFieldId == NULL) {
-        netty_unix_errors_throwRuntimeException(env, "failed to get field ID: NativeDatagramPacket.scopeId");
-        goto error;
-    }
-    packetPortFieldId = (*env)->GetFieldID(env, nativeDatagramPacketCls, "port", "I");
-    if (packetPortFieldId == NULL) {
-        netty_unix_errors_throwRuntimeException(env, "failed to get field ID: NativeDatagramPacket.port");
-        goto error;
-    }
-    packetMemoryAddressFieldId = (*env)->GetFieldID(env, nativeDatagramPacketCls, "memoryAddress", "J");
-    if (packetMemoryAddressFieldId == NULL) {
-        netty_unix_errors_throwRuntimeException(env, "failed to get field ID: NativeDatagramPacket.memoryAddress");
-        goto error;
-    }
+    NETTY_GET_FIELD(env, nativeDatagramPacketCls, packetAddrFieldId, "addr", "[B", done);
+    NETTY_GET_FIELD(env, nativeDatagramPacketCls, packetAddrLenFieldId, "addrLen", "I", done);
+    NETTY_GET_FIELD(env, nativeDatagramPacketCls, packetScopeIdFieldId, "scopeId", "I", done);
+    NETTY_GET_FIELD(env, nativeDatagramPacketCls, packetPortFieldId, "port", "I", done);
+    NETTY_GET_FIELD(env, nativeDatagramPacketCls, packetMemoryAddressFieldId, "memoryAddress", "J", done);
+    NETTY_GET_FIELD(env, nativeDatagramPacketCls, packetCountFieldId, "count", "I", done);
 
-    packetCountFieldId = (*env)->GetFieldID(env, nativeDatagramPacketCls, "count", "I");
-    if (packetCountFieldId == NULL) {
-        netty_unix_errors_throwRuntimeException(env, "failed to get field ID: NativeDatagramPacket.count");
-        goto error;
-    }
+    ret = NETTY_JNI_VERSION;
+done:
 
-    return NETTY_JNI_VERSION;
+    netty_unix_util_free_dynamic_methods_table(dynamicMethods, fixed_method_table_size, dynamicMethodsTableSize());
+    free(nettyClassName);
 
-error:
-   if (limitsOnLoadCalled == 1) {
-       netty_unix_limits_JNI_OnUnLoad(env);
-   }
-   if (errorsOnLoadCalled == 1) {
-       netty_unix_errors_JNI_OnUnLoad(env);
-   }
-   if (filedescriptorOnLoadCalled == 1) {
-       netty_unix_filedescriptor_JNI_OnUnLoad(env);
-   }
-   if (socketOnLoadCalled == 1) {
-       netty_unix_socket_JNI_OnUnLoad(env);
-   }
-   if (bufferOnLoadCalled == 1) {
-       netty_unix_buffer_JNI_OnUnLoad(env);
-   }
-   if (linuxsocketOnLoadCalled == 1) {
-       netty_epoll_linuxsocket_JNI_OnUnLoad(env);
-   }
-   packetAddrFieldId = NULL;
-   packetScopeIdFieldId = NULL;
-   packetPortFieldId = NULL;
-   packetMemoryAddressFieldId = NULL;
-   packetCountFieldId = NULL;
-
-   return JNI_ERR;
+    if (ret == JNI_ERR) {
+        if (limitsOnLoadCalled == 1) {
+            netty_unix_limits_JNI_OnUnLoad(env);
+        }
+        if (errorsOnLoadCalled == 1) {
+            netty_unix_errors_JNI_OnUnLoad(env);
+        }
+        if (filedescriptorOnLoadCalled == 1) {
+            netty_unix_filedescriptor_JNI_OnUnLoad(env);
+        }
+        if (socketOnLoadCalled == 1) {
+            netty_unix_socket_JNI_OnUnLoad(env);
+        }
+        if (bufferOnLoadCalled == 1) {
+            netty_unix_buffer_JNI_OnUnLoad(env);
+        }
+        if (linuxsocketOnLoadCalled == 1) {
+            netty_epoll_linuxsocket_JNI_OnUnLoad(env);
+        }
+        packetAddrFieldId = NULL;
+        packetAddrLenFieldId = NULL;
+        packetScopeIdFieldId = NULL;
+        packetPortFieldId = NULL;
+        packetMemoryAddressFieldId = NULL;
+        packetCountFieldId = NULL;
+    }
+    return ret;
 }
 
 static void netty_epoll_native_JNI_OnUnLoad(JNIEnv* env) {
@@ -586,6 +709,7 @@ static void netty_epoll_native_JNI_OnUnLoad(JNIEnv* env) {
     netty_epoll_linuxsocket_JNI_OnUnLoad(env);
 
     packetAddrFieldId = NULL;
+    packetAddrLenFieldId = NULL;
     packetScopeIdFieldId = NULL;
     packetPortFieldId = NULL;
     packetMemoryAddressFieldId = NULL;
@@ -616,11 +740,7 @@ static jint JNI_OnLoad_netty_transport_native_epoll0(JavaVM* vm, void* reserved)
 #endif /* NETTY_BUILD_STATIC */
     jint ret = netty_epoll_native_JNI_OnLoad(env, packagePrefix);
 
-    if (packagePrefix != NULL) {
-      free(packagePrefix);
-      packagePrefix = NULL;
-    }
-
+    free(packagePrefix);
     return ret;
 }
 
diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/AbstractEpollChannel.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/AbstractEpollChannel.java
index 25ae95b..8e05153 100644
--- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/AbstractEpollChannel.java
+++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/AbstractEpollChannel.java
@@ -38,7 +38,6 @@ import io.netty.channel.unix.FileDescriptor;
 import io.netty.channel.unix.Socket;
 import io.netty.channel.unix.UnixChannel;
 import io.netty.util.ReferenceCountUtil;
-import io.netty.util.internal.ThrowableUtil;
 
 import java.io.IOException;
 import java.net.InetSocketAddress;
@@ -57,8 +56,6 @@ import static io.netty.channel.unix.UnixChannelUtil.computeRemoteAddr;
 import static io.netty.util.internal.ObjectUtil.checkNotNull;
 
 abstract class AbstractEpollChannel extends AbstractChannel implements UnixChannel {
-    private static final ClosedChannelException DO_CLOSE_CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace(
-            new ClosedChannelException(), AbstractEpollChannel.class, "doClose()");
     private static final ChannelMetadata METADATA = new ChannelMetadata(false);
     final LinuxSocket socket;
     /**
@@ -84,24 +81,24 @@ abstract class AbstractEpollChannel extends AbstractChannel implements UnixChann
 
     AbstractEpollChannel(Channel parent, LinuxSocket fd, boolean active) {
         super(parent);
-        socket = checkNotNull(fd, "fd");
+        this.socket = checkNotNull(fd, "fd");
         this.active = active;
         if (active) {
             // Directly cache the remote and local addresses
             // See https://github.com/netty/netty/issues/2359
-            local = fd.localAddress();
-            remote = fd.remoteAddress();
+            this.local = fd.localAddress();
+            this.remote = fd.remoteAddress();
         }
     }
 
     AbstractEpollChannel(Channel parent, LinuxSocket fd, SocketAddress remote) {
         super(parent);
-        socket = checkNotNull(fd, "fd");
-        active = true;
+        this.socket = checkNotNull(fd, "fd");
+        this.active = true;
         // Directly cache the remote and local addresses
         // See https://github.com/netty/netty/issues/2359
         this.remote = remote;
-        local = fd.localAddress();
+        this.local = fd.localAddress();
     }
 
     static boolean isSoErrorZero(Socket fd) {
@@ -158,7 +155,7 @@ abstract class AbstractEpollChannel extends AbstractChannel implements UnixChann
             ChannelPromise promise = connectPromise;
             if (promise != null) {
                 // Use tryFailure() instead of setFailure() to avoid the race against cancel().
-                promise.tryFailure(DO_CLOSE_CLOSED_CHANNEL_EXCEPTION);
+                promise.tryFailure(new ClosedChannelException());
                 connectPromise = null;
             }
 
@@ -194,6 +191,11 @@ abstract class AbstractEpollChannel extends AbstractChannel implements UnixChann
         }
     }
 
+    void resetCachedAddresses() {
+        local = socket.localAddress();
+        remote = socket.remoteAddress();
+    }
+
     @Override
     protected void doDisconnect() throws Exception {
         doClose();
@@ -237,6 +239,9 @@ abstract class AbstractEpollChannel extends AbstractChannel implements UnixChann
     }
 
     private static boolean isAllowHalfClosure(ChannelConfig config) {
+        if (config instanceof EpollDomainSocketChannelConfig) {
+            return ((EpollDomainSocketChannelConfig) config).isAllowHalfClosure();
+        }
         return config instanceof SocketChannelConfig &&
                 ((SocketChannelConfig) config).isAllowHalfClosure();
     }
@@ -388,7 +393,9 @@ abstract class AbstractEpollChannel extends AbstractChannel implements UnixChann
          */
         abstract void epollInReady();
 
-        final void epollInBefore() { maybeMoreDataToRead = false; }
+        final void epollInBefore() {
+            maybeMoreDataToRead = false;
+        }
 
         final void epollInFinally(ChannelConfig config) {
             maybeMoreDataToRead = allocHandle.maybeMoreDataToRead();
diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/AbstractEpollStreamChannel.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/AbstractEpollStreamChannel.java
index 70208d2..95e31b3 100644
--- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/AbstractEpollStreamChannel.java
+++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/AbstractEpollStreamChannel.java
@@ -37,7 +37,6 @@ import io.netty.channel.unix.SocketWritableByteChannel;
 import io.netty.channel.unix.UnixChannelUtil;
 import io.netty.util.internal.PlatformDependent;
 import io.netty.util.internal.StringUtil;
-import io.netty.util.internal.ThrowableUtil;
 import io.netty.util.internal.UnstableApi;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
@@ -54,6 +53,7 @@ import static io.netty.channel.internal.ChannelUtils.MAX_BYTES_PER_GATHERING_WRI
 import static io.netty.channel.internal.ChannelUtils.WRITE_STATUS_SNDBUF_FULL;
 import static io.netty.channel.unix.FileDescriptor.pipe;
 import static io.netty.util.internal.ObjectUtil.checkNotNull;
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
 
 public abstract class AbstractEpollStreamChannel extends AbstractEpollChannel implements DuplexChannel {
     private static final ChannelMetadata METADATA = new ChannelMetadata(false, 16);
@@ -61,15 +61,7 @@ public abstract class AbstractEpollStreamChannel extends AbstractEpollChannel im
             " (expected: " + StringUtil.simpleClassName(ByteBuf.class) + ", " +
                     StringUtil.simpleClassName(DefaultFileRegion.class) + ')';
     private static final InternalLogger logger = InternalLoggerFactory.getInstance(AbstractEpollStreamChannel.class);
-    private static final ClosedChannelException CLEAR_SPLICE_QUEUE_CLOSED_CHANNEL_EXCEPTION =
-            ThrowableUtil.unknownStackTrace(new ClosedChannelException(),
-                    AbstractEpollStreamChannel.class, "clearSpliceQueue()");
-    private static final ClosedChannelException SPLICE_TO_CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace(
-            new ClosedChannelException(),
-            AbstractEpollStreamChannel.class, "spliceTo(...)");
-    private static final ClosedChannelException FAIL_SPLICE_IF_CLOSED_CLOSED_CHANNEL_EXCEPTION =
-            ThrowableUtil.unknownStackTrace(new ClosedChannelException(),
-            AbstractEpollStreamChannel.class, "failSpliceIfClosed(...)");
+
     private final Runnable flushTask = new Runnable() {
         @Override
         public void run() {
@@ -78,9 +70,9 @@ public abstract class AbstractEpollStreamChannel extends AbstractEpollChannel im
             ((AbstractEpollUnsafe) unsafe()).flush0();
         }
     };
-    private Queue<SpliceInTask> spliceQueue;
 
     // Lazy init these if we need to splice(...)
+    private volatile Queue<SpliceInTask> spliceQueue;
     private FileDescriptor pipeIn;
     private FileDescriptor pipeOut;
 
@@ -163,16 +155,14 @@ public abstract class AbstractEpollStreamChannel extends AbstractEpollChannel im
         if (ch.eventLoop() != eventLoop()) {
             throw new IllegalArgumentException("EventLoops are not the same.");
         }
-        if (len < 0) {
-            throw new IllegalArgumentException("len: " + len + " (expected: >= 0)");
-        }
+        checkPositiveOrZero(len, "len");
         if (ch.config().getEpollMode() != EpollMode.LEVEL_TRIGGERED
                 || config().getEpollMode() != EpollMode.LEVEL_TRIGGERED) {
             throw new IllegalStateException("spliceTo() supported only when using " + EpollMode.LEVEL_TRIGGERED);
         }
         checkNotNull(promise, "promise");
         if (!isOpen()) {
-            promise.tryFailure(SPLICE_TO_CLOSED_CHANNEL_EXCEPTION);
+            promise.tryFailure(new ClosedChannelException());
         } else {
             addToSpliceQueue(new SpliceInChannelTask(ch, len, promise));
             failSpliceIfClosed(promise);
@@ -214,18 +204,14 @@ public abstract class AbstractEpollStreamChannel extends AbstractEpollChannel im
      */
     public final ChannelFuture spliceTo(final FileDescriptor ch, final int offset, final int len,
                                         final ChannelPromise promise) {
-        if (len < 0) {
-            throw new IllegalArgumentException("len: " + len + " (expected: >= 0)");
-        }
-        if (offset < 0) {
-            throw new IllegalArgumentException("offset must be >= 0 but was " + offset);
-        }
+        checkPositiveOrZero(len, "len");
+        checkPositiveOrZero(offset, "offset");
         if (config().getEpollMode() != EpollMode.LEVEL_TRIGGERED) {
             throw new IllegalStateException("spliceTo() supported only when using " + EpollMode.LEVEL_TRIGGERED);
         }
         checkNotNull(promise, "promise");
         if (!isOpen()) {
-            promise.tryFailure(SPLICE_TO_CLOSED_CHANNEL_EXCEPTION);
+            promise.tryFailure(new ClosedChannelException());
         } else {
             addToSpliceQueue(new SpliceFdTask(ch, offset, len, promise));
             failSpliceIfClosed(promise);
@@ -237,7 +223,7 @@ public abstract class AbstractEpollStreamChannel extends AbstractEpollChannel im
         if (!isOpen()) {
             // Seems like the Channel was closed in the meantime try to fail the promise to prevent any
             // cases where a future may not be notified otherwise.
-            if (promise.tryFailure(FAIL_SPLICE_IF_CLOSED_CLOSED_CHANNEL_EXCEPTION)) {
+            if (promise.tryFailure(new ClosedChannelException())) {
                 eventLoop().execute(new Runnable() {
                     @Override
                     public void run() {
@@ -372,13 +358,13 @@ public abstract class AbstractEpollStreamChannel extends AbstractEpollChannel im
      * </ul>
      */
     private int writeDefaultFileRegion(ChannelOutboundBuffer in, DefaultFileRegion region) throws Exception {
+        final long offset = region.transferred();
         final long regionCount = region.count();
-        if (region.transferred() >= regionCount) {
+        if (offset >= regionCount) {
             in.remove();
             return 0;
         }
 
-        final long offset = region.transferred();
         final long flushedAmount = socket.sendFile(region, region.position(), offset, regionCount - offset);
         if (flushedAmount > 0) {
             in.progress(flushedAmount);
@@ -386,6 +372,8 @@ public abstract class AbstractEpollStreamChannel extends AbstractEpollChannel im
                 in.remove();
             }
             return 1;
+        } else if (flushedAmount == 0) {
+            validateFileRegion(region, offset);
         }
         return WRITE_STATUS_SNDBUF_FULL;
     }
@@ -690,15 +678,21 @@ public abstract class AbstractEpollStreamChannel extends AbstractEpollChannel im
     }
 
     private void clearSpliceQueue() {
-        if (spliceQueue == null) {
+        Queue<SpliceInTask> sQueue = spliceQueue;
+        if (sQueue == null) {
             return;
         }
+        ClosedChannelException exception = null;
+
         for (;;) {
-            SpliceInTask task = spliceQueue.poll();
+            SpliceInTask task = sQueue.poll();
             if (task == null) {
                 break;
             }
-            task.promise.tryFailure(CLEAR_SPLICE_QUEUE_CLOSED_CHANNEL_EXCEPTION);
+            if (exception == null) {
+                exception = new ClosedChannelException();
+            }
+            task.promise.tryFailure(exception);
         }
     }
 
@@ -707,9 +701,7 @@ public abstract class AbstractEpollStreamChannel extends AbstractEpollChannel im
             try {
                 fd.close();
             } catch (IOException e) {
-                if (logger.isWarnEnabled()) {
-                    logger.warn("Error while closing a pipe", e);
-                }
+                logger.warn("Error while closing a pipe", e);
             }
         }
     }
@@ -762,15 +754,16 @@ public abstract class AbstractEpollStreamChannel extends AbstractEpollChannel im
             ByteBuf byteBuf = null;
             boolean close = false;
             try {
+                Queue<SpliceInTask> sQueue = null;
                 do {
-                    if (spliceQueue != null) {
-                        SpliceInTask spliceTask = spliceQueue.peek();
+                    if (sQueue != null || (sQueue = spliceQueue) != null) {
+                        SpliceInTask spliceTask = sQueue.peek();
                         if (spliceTask != null) {
                             if (spliceTask.spliceIn(allocHandle)) {
                                 // We need to check if it is still active as if not we removed all SpliceTasks in
                                 // doClose(...)
                                 if (isActive()) {
-                                    spliceQueue.remove();
+                                    sQueue.remove();
                                 }
                                 continue;
                             } else {
@@ -830,24 +823,16 @@ public abstract class AbstractEpollStreamChannel extends AbstractEpollChannel im
     }
 
     private void addToSpliceQueue(final SpliceInTask task) {
-        EventLoop eventLoop = eventLoop();
-        if (eventLoop.inEventLoop()) {
-            addToSpliceQueue0(task);
-        } else {
-            eventLoop.execute(new Runnable() {
-                @Override
-                public void run() {
-                    addToSpliceQueue0(task);
+        Queue<SpliceInTask> sQueue = spliceQueue;
+        if (sQueue == null) {
+            synchronized (this) {
+                sQueue = spliceQueue;
+                if (sQueue == null) {
+                    spliceQueue = sQueue = PlatformDependent.newMpscQueue();
                 }
-            });
-        }
-    }
-
-    private void addToSpliceQueue0(SpliceInTask task) {
-        if (spliceQueue == null) {
-            spliceQueue = PlatformDependent.newMpscQueue();
+            }
         }
-        spliceQueue.add(task);
+        sQueue.add(task);
     }
 
     protected abstract class SpliceInTask {
@@ -990,7 +975,7 @@ public abstract class AbstractEpollStreamChannel extends AbstractEpollChannel im
     private final class SpliceFdTask extends SpliceInTask {
         private final FileDescriptor fd;
         private final ChannelPromise promise;
-        private final int offset;
+        private int offset;
 
         SpliceFdTask(FileDescriptor fd, int offset, int len, ChannelPromise promise) {
             super(len, promise);
@@ -1020,6 +1005,7 @@ public abstract class AbstractEpollStreamChannel extends AbstractEpollChannel im
                         }
                         do {
                             int splicedOut = Native.splice(pipeIn.intValue(), -1, fd.intValue(), offset, splicedIn);
+                            offset += splicedOut;
                             splicedIn -= splicedOut;
                         } while (splicedIn > 0);
                         if (len == 0) {
diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/Epoll.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/Epoll.java
index e4ecf42..f2b0231 100644
--- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/Epoll.java
+++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/Epoll.java
@@ -19,13 +19,14 @@ import io.netty.channel.unix.FileDescriptor;
 import io.netty.util.internal.SystemPropertyUtil;
 
 /**
- * Tells if <a href="http://netty.io/wiki/native-transports.html">{@code netty-transport-native-epoll}</a> is supported.
+ * Tells if <a href="https://netty.io/wiki/native-transports.html">{@code netty-transport-native-epoll}</a> is
+ * supported.
  */
 public final class Epoll {
 
     private static final Throwable UNAVAILABILITY_CAUSE;
 
-    static  {
+    static {
         Throwable cause = null;
 
         if (SystemPropertyUtil.getBoolean("io.netty.transport.noNative", false)) {
@@ -61,15 +62,15 @@ public final class Epoll {
     }
 
     /**
-     * Returns {@code true} if and only if the
-     * <a href="http://netty.io/wiki/native-transports.html">{@code netty-transport-native-epoll}</a> is available.
+     * Returns {@code true} if and only if the <a href="https://netty.io/wiki/native-transports.html">{@code
+     * netty-transport-native-epoll}</a> is available.
      */
     public static boolean isAvailable() {
         return UNAVAILABILITY_CAUSE == null;
     }
 
     /**
-     * Ensure that <a href="http://netty.io/wiki/native-transports.html">{@code netty-transport-native-epoll}</a> is
+     * Ensure that <a href="https://netty.io/wiki/native-transports.html">{@code netty-transport-native-epoll}</a> is
      * available.
      *
      * @throws UnsatisfiedLinkError if unavailable
@@ -82,8 +83,8 @@ public final class Epoll {
     }
 
     /**
-     * Returns the cause of unavailability of
-     * <a href="http://netty.io/wiki/native-transports.html">{@code netty-transport-native-epoll}</a>.
+     * Returns the cause of unavailability of <a href="https://netty.io/wiki/native-transports.html">
+     * {@code netty-transport-native-epoll}</a>.
      *
      * @return the cause if unavailable. {@code null} if available.
      */
@@ -91,5 +92,6 @@ public final class Epoll {
         return UNAVAILABILITY_CAUSE;
     }
 
-    private Epoll() { }
+    private Epoll() {
+    }
 }
diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollChannelConfig.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollChannelConfig.java
index 2d2610c..c3c4dfd 100644
--- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollChannelConfig.java
+++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollChannelConfig.java
@@ -22,6 +22,7 @@ import io.netty.channel.DefaultChannelConfig;
 import io.netty.channel.MessageSizeEstimator;
 import io.netty.channel.RecvByteBufAllocator;
 import io.netty.channel.WriteBufferWaterMark;
+import io.netty.util.internal.ObjectUtil;
 
 import java.io.IOException;
 import java.util.Map;
@@ -147,9 +148,8 @@ public class EpollChannelConfig extends DefaultChannelConfig {
      * <strong>Be aware this config setting can only be adjusted before the channel was registered.</strong>
      */
     public EpollChannelConfig setEpollMode(EpollMode mode) {
-        if (mode == null) {
-            throw new NullPointerException("mode");
-        }
+        ObjectUtil.checkNotNull(mode, "mode");
+
         try {
             switch (mode) {
             case EDGE_TRIGGERED:
diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollChannelOption.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollChannelOption.java
index 1f5127c..765eea2 100644
--- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollChannelOption.java
+++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollChannelOption.java
@@ -45,6 +45,8 @@ public final class EpollChannelOption<T> extends UnixChannelOption<T> {
 
     public static final ChannelOption<Map<InetAddress, byte[]>> TCP_MD5SIG = valueOf("TCP_MD5SIG");
 
+    public static final ChannelOption<Integer> MAX_DATAGRAM_PAYLOAD_SIZE = valueOf("MAX_DATAGRAM_PAYLOAD_SIZE");
+
     @SuppressWarnings({ "unused", "deprecation" })
     private EpollChannelOption() {
     }
diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollDatagramChannel.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollDatagramChannel.java
index 714f612..8e28501 100644
--- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollDatagramChannel.java
+++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollDatagramChannel.java
@@ -17,6 +17,7 @@ package io.netty.channel.epoll;
 
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.ByteBufAllocator;
+import io.netty.buffer.Unpooled;
 import io.netty.channel.AddressedEnvelope;
 import io.netty.channel.ChannelFuture;
 import io.netty.channel.ChannelMetadata;
@@ -27,17 +28,25 @@ import io.netty.channel.DefaultAddressedEnvelope;
 import io.netty.channel.socket.DatagramChannel;
 import io.netty.channel.socket.DatagramChannelConfig;
 import io.netty.channel.socket.DatagramPacket;
+import io.netty.channel.socket.InternetProtocolFamily;
 import io.netty.channel.unix.DatagramSocketAddress;
+import io.netty.channel.unix.Errors;
+import io.netty.channel.unix.Errors.NativeIoException;
 import io.netty.channel.unix.IovArray;
+import io.netty.channel.unix.Socket;
 import io.netty.channel.unix.UnixChannelUtil;
+import io.netty.util.ReferenceCountUtil;
+import io.netty.util.internal.ObjectUtil;
+import io.netty.util.internal.RecyclableArrayList;
 import io.netty.util.internal.StringUtil;
 
 import java.io.IOException;
+import java.net.Inet4Address;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.NetworkInterface;
+import java.net.PortUnreachableException;
 import java.net.SocketAddress;
-import java.net.SocketException;
 import java.nio.ByteBuffer;
 
 import static io.netty.channel.epoll.LinuxSocket.newSocketDgram;
@@ -58,17 +67,34 @@ public final class EpollDatagramChannel extends AbstractEpollChannel implements
     private final EpollDatagramChannelConfig config;
     private volatile boolean connected;
 
+    /**
+     * Create a new instance which selects the {@link InternetProtocolFamily} to use depending
+     * on the Operation Systems default which will be chosen.
+     */
     public EpollDatagramChannel() {
-        super(newSocketDgram());
-        config = new EpollDatagramChannelConfig(this);
+        this(null);
+    }
+
+    /**
+     * Create a new instance using the given {@link InternetProtocolFamily}. If {@code null} is used it will depend
+     * on the Operation Systems default which will be chosen.
+     */
+    public EpollDatagramChannel(InternetProtocolFamily family) {
+        this(family == null ?
+                newSocketDgram(Socket.isIPv6Preferred()) : newSocketDgram(family == InternetProtocolFamily.IPv6),
+                false);
     }
 
+    /**
+     * Create a new instance which selects the {@link InternetProtocolFamily} to use depending
+     * on the Operation Systems default which will be chosen.
+     */
     public EpollDatagramChannel(int fd) {
-        this(new LinuxSocket(fd));
+        this(new LinuxSocket(fd), true);
     }
 
-    EpollDatagramChannel(LinuxSocket fd) {
-        super(null, fd, true);
+    private EpollDatagramChannel(LinuxSocket fd, boolean active) {
+        super(null, fd, active);
         config = new EpollDatagramChannelConfig(this);
     }
 
@@ -109,7 +135,7 @@ public final class EpollDatagramChannel extends AbstractEpollChannel implements
             return joinGroup(
                     multicastAddress,
                     NetworkInterface.getByInetAddress(localAddress().getAddress()), null, promise);
-        } catch (SocketException e) {
+        } catch (IOException e) {
             promise.setFailure(e);
         }
         return promise;
@@ -139,15 +165,15 @@ public final class EpollDatagramChannel extends AbstractEpollChannel implements
             final InetAddress multicastAddress, final NetworkInterface networkInterface,
             final InetAddress source, final ChannelPromise promise) {
 
-        if (multicastAddress == null) {
-            throw new NullPointerException("multicastAddress");
-        }
+        ObjectUtil.checkNotNull(multicastAddress, "multicastAddress");
+        ObjectUtil.checkNotNull(networkInterface, "networkInterface");
 
-        if (networkInterface == null) {
-            throw new NullPointerException("networkInterface");
+        try {
+            socket.joinGroup(multicastAddress, networkInterface, source);
+            promise.setSuccess();
+        } catch (IOException e) {
+            promise.setFailure(e);
         }
-
-        promise.setFailure(new UnsupportedOperationException("Multicast not supported"));
         return promise;
     }
 
@@ -161,7 +187,7 @@ public final class EpollDatagramChannel extends AbstractEpollChannel implements
         try {
             return leaveGroup(
                     multicastAddress, NetworkInterface.getByInetAddress(localAddress().getAddress()), null, promise);
-        } catch (SocketException e) {
+        } catch (IOException e) {
             promise.setFailure(e);
         }
         return promise;
@@ -190,15 +216,15 @@ public final class EpollDatagramChannel extends AbstractEpollChannel implements
     public ChannelFuture leaveGroup(
             final InetAddress multicastAddress, final NetworkInterface networkInterface, final InetAddress source,
             final ChannelPromise promise) {
-        if (multicastAddress == null) {
-            throw new NullPointerException("multicastAddress");
-        }
-        if (networkInterface == null) {
-            throw new NullPointerException("networkInterface");
-        }
-
-        promise.setFailure(new UnsupportedOperationException("Multicast not supported"));
+        ObjectUtil.checkNotNull(multicastAddress, "multicastAddress");
+        ObjectUtil.checkNotNull(networkInterface, "networkInterface");
 
+        try {
+            socket.leaveGroup(multicastAddress, networkInterface, source);
+            promise.setSuccess();
+        } catch (IOException e) {
+            promise.setFailure(e);
+        }
         return promise;
     }
 
@@ -213,16 +239,10 @@ public final class EpollDatagramChannel extends AbstractEpollChannel implements
     public ChannelFuture block(
             final InetAddress multicastAddress, final NetworkInterface networkInterface,
             final InetAddress sourceToBlock, final ChannelPromise promise) {
-        if (multicastAddress == null) {
-            throw new NullPointerException("multicastAddress");
-        }
-        if (sourceToBlock == null) {
-            throw new NullPointerException("sourceToBlock");
-        }
+        ObjectUtil.checkNotNull(multicastAddress, "multicastAddress");
+        ObjectUtil.checkNotNull(sourceToBlock, "sourceToBlock");
+        ObjectUtil.checkNotNull(networkInterface, "networkInterface");
 
-        if (networkInterface == null) {
-            throw new NullPointerException("networkInterface");
-        }
         promise.setFailure(new UnsupportedOperationException("Multicast not supported"));
         return promise;
     }
@@ -253,6 +273,13 @@ public final class EpollDatagramChannel extends AbstractEpollChannel implements
 
     @Override
     protected void doBind(SocketAddress localAddress) throws Exception {
+        if (localAddress instanceof InetSocketAddress) {
+            InetSocketAddress socketAddress = (InetSocketAddress) localAddress;
+            if (socketAddress.getAddress().isAnyLocalAddress() &&
+                    socketAddress.getAddress() instanceof Inet4Address && Socket.isIPv6Preferred()) {
+                localAddress = new InetSocketAddress(LinuxSocket.INET6_ANY, socketAddress.getPort());
+            }
+        }
         super.doBind(localAddress);
         active = true;
     }
@@ -270,8 +297,8 @@ public final class EpollDatagramChannel extends AbstractEpollChannel implements
             try {
                 // Check if sendmmsg(...) is supported which is only the case for GLIBC 2.14+
                 if (Native.IS_SUPPORTING_SENDMMSG && in.size() > 1) {
-                    NativeDatagramPacketArray array = ((EpollEventLoop) eventLoop()).cleanDatagramPacketArray();
-                    in.forEachFlushedMessage(array);
+                    NativeDatagramPacketArray array = cleanDatagramPacketArray();
+                    array.add(in, isConnected());
                     int cnt = array.count();
 
                     if (cnt >= 1) {
@@ -280,7 +307,7 @@ public final class EpollDatagramChannel extends AbstractEpollChannel implements
                         NativeDatagramPacketArray.NativeDatagramPacket[] packets = array.packets();
 
                         while (cnt > 0) {
-                            int send = Native.sendmmsg(socket.intValue(), packets, offset, cnt);
+                            int send = socket.sendmmsg(packets, offset, cnt);
                             if (send == 0) {
                                 // Did not write all messages.
                                 setFlag(Native.EPOLLOUT);
@@ -349,7 +376,7 @@ public final class EpollDatagramChannel extends AbstractEpollChannel implements
             }
         } else if (data.nioBufferCount() > 1) {
             IovArray array = ((EpollEventLoop) eventLoop()).cleanIovArray();
-            array.add(data);
+            array.add(data, data.readerIndex(), data.readableBytes());
             int cnt = array.count();
             assert cnt != 0;
 
@@ -412,6 +439,7 @@ public final class EpollDatagramChannel extends AbstractEpollChannel implements
     protected void doDisconnect() throws Exception {
         socket.disconnect();
         connected = active = false;
+        resetCachedAddresses();
     }
 
     @Override
@@ -449,47 +477,43 @@ public final class EpollDatagramChannel extends AbstractEpollChannel implements
 
             Throwable exception = null;
             try {
-                ByteBuf data = null;
                 try {
+                    boolean connected = isConnected();
                     do {
-                        data = allocHandle.allocate(allocator);
-                        allocHandle.attemptedBytesRead(data.writableBytes());
-                        final DatagramSocketAddress remoteAddress;
-                        if (data.hasMemoryAddress()) {
-                            // has a memory address so use optimized call
-                            remoteAddress = socket.recvFromAddress(data.memoryAddress(), data.writerIndex(),
-                                                                 data.capacity());
-                        } else {
-                            ByteBuffer nioData = data.internalNioBuffer(data.writerIndex(), data.writableBytes());
-                            remoteAddress = socket.recvFrom(nioData, nioData.position(), nioData.limit());
+                        ByteBuf byteBuf = allocHandle.allocate(allocator);
+                        final boolean read;
+                        int datagramSize = config().getMaxDatagramPayloadSize();
+
+                        // Only try to use recvmmsg if its really supported by the running system.
+                        int numDatagram = Native.IS_SUPPORTING_RECVMMSG ?
+                                datagramSize == 0 ? 1 : byteBuf.writableBytes() / datagramSize :
+                                0;
+
+                        try {
+                            if (numDatagram <= 1) {
+                                if (connected) {
+                                    read = connectedRead(allocHandle, byteBuf, datagramSize);
+                                } else {
+                                    read = read(allocHandle, byteBuf, datagramSize);
+                                }
+                            } else {
+                                // Try to use scattering reads via recvmmsg(...) syscall.
+                                read = scatteringRead(allocHandle, byteBuf, datagramSize, numDatagram);
+                            }
+                        } catch (NativeIoException e) {
+                            if (connected) {
+                                throw translateForConnected(e);
+                            }
+                            throw e;
                         }
 
-                        if (remoteAddress == null) {
-                            allocHandle.lastBytesRead(-1);
-                            data.release();
-                            data = null;
+                        if (read) {
+                            readPending = false;
+                        } else {
                             break;
                         }
-
-                        InetSocketAddress localAddress = remoteAddress.localAddress();
-                        if (localAddress == null) {
-                            localAddress = (InetSocketAddress) localAddress();
-                        }
-
-                        allocHandle.incMessagesRead(1);
-                        allocHandle.lastBytesRead(remoteAddress.receivedAmount());
-                        data.writerIndex(data.writerIndex() + allocHandle.lastBytesRead());
-
-                        readPending = false;
-                        pipeline.fireChannelRead(
-                                new DatagramPacket(data, localAddress, remoteAddress));
-
-                        data = null;
                     } while (allocHandle.continueReading());
                 } catch (Throwable t) {
-                    if (data != null) {
-                        data.release();
-                    }
                     exception = t;
                 }
 
@@ -504,4 +528,165 @@ public final class EpollDatagramChannel extends AbstractEpollChannel implements
             }
         }
     }
+
+    private boolean connectedRead(EpollRecvByteAllocatorHandle allocHandle, ByteBuf byteBuf, int maxDatagramPacketSize)
+            throws Exception {
+        try {
+            int writable = maxDatagramPacketSize != 0 ? Math.min(byteBuf.writableBytes(), maxDatagramPacketSize)
+                    : byteBuf.writableBytes();
+            allocHandle.attemptedBytesRead(writable);
+
+            int writerIndex = byteBuf.writerIndex();
+            int localReadAmount;
+            if (byteBuf.hasMemoryAddress()) {
+                localReadAmount = socket.readAddress(byteBuf.memoryAddress(), writerIndex, writerIndex + writable);
+            } else {
+                ByteBuffer buf = byteBuf.internalNioBuffer(writerIndex, writable);
+                localReadAmount = socket.read(buf, buf.position(), buf.limit());
+            }
+
+            if (localReadAmount <= 0) {
+                allocHandle.lastBytesRead(localReadAmount);
+
+                // nothing was read, release the buffer.
+                return false;
+            }
+            byteBuf.writerIndex(writerIndex + localReadAmount);
+
+            allocHandle.lastBytesRead(maxDatagramPacketSize <= 0 ?
+                    localReadAmount : writable);
+
+            DatagramPacket packet = new DatagramPacket(byteBuf, localAddress(), remoteAddress());
+            allocHandle.incMessagesRead(1);
+
+            pipeline().fireChannelRead(packet);
+            byteBuf = null;
+            return true;
+        } finally {
+            if (byteBuf != null) {
+                byteBuf.release();
+            }
+        }
+    }
+
+    private IOException translateForConnected(NativeIoException e) {
+        // We need to correctly translate connect errors to match NIO behaviour.
+        if (e.expectedErr() == Errors.ERROR_ECONNREFUSED_NEGATIVE) {
+            PortUnreachableException error = new PortUnreachableException(e.getMessage());
+            error.initCause(e);
+            return error;
+        }
+        return e;
+    }
+
+    private boolean scatteringRead(EpollRecvByteAllocatorHandle allocHandle,
+            ByteBuf byteBuf, int datagramSize, int numDatagram) throws IOException {
+        RecyclableArrayList bufferPackets = null;
+        try {
+            int offset = byteBuf.writerIndex();
+            NativeDatagramPacketArray array = cleanDatagramPacketArray();
+
+            for (int i = 0; i < numDatagram;  i++, offset += datagramSize) {
+                if (!array.addWritable(byteBuf, offset, datagramSize)) {
+                    break;
+                }
+            }
+
+            allocHandle.attemptedBytesRead(offset - byteBuf.writerIndex());
+
+            NativeDatagramPacketArray.NativeDatagramPacket[] packets = array.packets();
+
+            int received = socket.recvmmsg(packets, 0, array.count());
+            if (received == 0) {
+                allocHandle.lastBytesRead(-1);
+                return false;
+            }
+            int bytesReceived = received * datagramSize;
+            byteBuf.writerIndex(bytesReceived);
+            InetSocketAddress local = localAddress();
+            if (received == 1) {
+                // Single packet fast-path
+                DatagramPacket packet = packets[0].newDatagramPacket(byteBuf, local);
+                allocHandle.lastBytesRead(datagramSize);
+                allocHandle.incMessagesRead(1);
+                pipeline().fireChannelRead(packet);
+                byteBuf = null;
+                return true;
+            }
+
+            // Its important that we process all received data out of the NativeDatagramPacketArray
+            // before we call fireChannelRead(...). This is because the user may call flush()
+            // in a channelRead(...) method and so may re-use the NativeDatagramPacketArray again.
+            bufferPackets = RecyclableArrayList.newInstance();
+            for (int i = 0; i < received; i++) {
+                DatagramPacket packet = packets[i].newDatagramPacket(byteBuf.readRetainedSlice(datagramSize), local);
+                bufferPackets.add(packet);
+            }
+
+            allocHandle.lastBytesRead(bytesReceived);
+            allocHandle.incMessagesRead(received);
+
+            for (int i = 0; i < received; i++) {
+                pipeline().fireChannelRead(bufferPackets.set(i, Unpooled.EMPTY_BUFFER));
+            }
+            bufferPackets.recycle();
+            bufferPackets = null;
+            return true;
+        } finally {
+            if (byteBuf != null) {
+                byteBuf.release();
+            }
+            if (bufferPackets != null) {
+                for (int i = 0; i < bufferPackets.size(); i++) {
+                    ReferenceCountUtil.release(bufferPackets.get(i));
+                }
+                bufferPackets.recycle();
+            }
+        }
+    }
+
+    private boolean read(EpollRecvByteAllocatorHandle allocHandle, ByteBuf byteBuf, int maxDatagramPacketSize)
+            throws IOException {
+        try {
+            int writable = maxDatagramPacketSize != 0 ? Math.min(byteBuf.writableBytes(), maxDatagramPacketSize)
+                    : byteBuf.writableBytes();
+            allocHandle.attemptedBytesRead(writable);
+            int writerIndex = byteBuf.writerIndex();
+            final DatagramSocketAddress remoteAddress;
+            if (byteBuf.hasMemoryAddress()) {
+                // has a memory address so use optimized call
+                remoteAddress = socket.recvFromAddress(
+                        byteBuf.memoryAddress(), writerIndex, writerIndex + writable);
+            } else {
+                ByteBuffer nioData = byteBuf.internalNioBuffer(writerIndex, writable);
+                remoteAddress = socket.recvFrom(nioData, nioData.position(), nioData.limit());
+            }
+
+            if (remoteAddress == null) {
+                allocHandle.lastBytesRead(-1);
+                return false;
+            }
+            InetSocketAddress localAddress = remoteAddress.localAddress();
+            if (localAddress == null) {
+                localAddress = localAddress();
+            }
+            int received = remoteAddress.receivedAmount();
+            allocHandle.lastBytesRead(maxDatagramPacketSize <= 0 ?
+                    received : writable);
+            byteBuf.writerIndex(writerIndex + received);
+            allocHandle.incMessagesRead(1);
+
+            pipeline().fireChannelRead(new DatagramPacket(byteBuf, localAddress, remoteAddress));
+            byteBuf = null;
+            return true;
+        } finally {
+            if (byteBuf != null) {
+                byteBuf.release();
+            }
+        }
+    }
+
+    private NativeDatagramPacketArray cleanDatagramPacketArray() {
+        return ((EpollEventLoop) eventLoop()).cleanDatagramPacketArray();
+    }
 }
diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollDatagramChannelConfig.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollDatagramChannelConfig.java
index 778b555..e97d2c5 100644
--- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollDatagramChannelConfig.java
+++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollDatagramChannelConfig.java
@@ -15,6 +15,7 @@
  */
 package io.netty.channel.epoll;
 
+import io.netty.buffer.ByteBuf;
 import io.netty.buffer.ByteBufAllocator;
 import io.netty.channel.ChannelException;
 import io.netty.channel.ChannelOption;
@@ -23,6 +24,7 @@ import io.netty.channel.MessageSizeEstimator;
 import io.netty.channel.RecvByteBufAllocator;
 import io.netty.channel.WriteBufferWaterMark;
 import io.netty.channel.socket.DatagramChannelConfig;
+import io.netty.util.internal.ObjectUtil;
 
 import java.io.IOException;
 import java.net.InetAddress;
@@ -32,6 +34,7 @@ import java.util.Map;
 public final class EpollDatagramChannelConfig extends EpollChannelConfig implements DatagramChannelConfig {
     private static final RecvByteBufAllocator DEFAULT_RCVBUF_ALLOCATOR = new FixedRecvByteBufAllocator(2048);
     private boolean activeOnOpen;
+    private volatile int maxDatagramSize;
 
     EpollDatagramChannelConfig(EpollDatagramChannel channel) {
         super(channel);
@@ -48,7 +51,7 @@ public final class EpollDatagramChannelConfig extends EpollChannelConfig impleme
                 ChannelOption.IP_MULTICAST_ADDR, ChannelOption.IP_MULTICAST_IF, ChannelOption.IP_MULTICAST_TTL,
                 ChannelOption.IP_TOS, ChannelOption.DATAGRAM_CHANNEL_ACTIVE_ON_REGISTRATION,
                 EpollChannelOption.SO_REUSEPORT, EpollChannelOption.IP_FREEBIND, EpollChannelOption.IP_TRANSPARENT,
-                EpollChannelOption.IP_RECVORIGDSTADDR);
+                EpollChannelOption.IP_RECVORIGDSTADDR, EpollChannelOption.MAX_DATAGRAM_PAYLOAD_SIZE);
     }
 
     @SuppressWarnings({ "unchecked", "deprecation" })
@@ -96,6 +99,9 @@ public final class EpollDatagramChannelConfig extends EpollChannelConfig impleme
         if (option == EpollChannelOption.IP_RECVORIGDSTADDR) {
             return (T) Boolean.valueOf(isIpRecvOrigDestAddr());
         }
+        if (option == EpollChannelOption.MAX_DATAGRAM_PAYLOAD_SIZE) {
+            return (T) Integer.valueOf(getMaxDatagramPayloadSize());
+        }
         return super.getOption(option);
     }
 
@@ -132,6 +138,8 @@ public final class EpollDatagramChannelConfig extends EpollChannelConfig impleme
             setIpTransparent((Boolean) value);
         } else if (option == EpollChannelOption.IP_RECVORIGDSTADDR) {
             setIpRecvOrigDestAddr((Boolean) value);
+        } else if (option == EpollChannelOption.MAX_DATAGRAM_PAYLOAD_SIZE) {
+            setMaxDatagramPayloadSize((Integer) value);
         } else {
             return super.setOption(option, value);
         }
@@ -316,42 +324,79 @@ public final class EpollDatagramChannelConfig extends EpollChannelConfig impleme
 
     @Override
     public boolean isLoopbackModeDisabled() {
-        return false;
+        try {
+            return ((EpollDatagramChannel) channel).socket.isLoopbackModeDisabled();
+        } catch (IOException e) {
+            throw new ChannelException(e);
+        }
     }
 
     @Override
     public DatagramChannelConfig setLoopbackModeDisabled(boolean loopbackModeDisabled) {
-        throw new UnsupportedOperationException("Multicast not supported");
+        try {
+            ((EpollDatagramChannel) channel).socket.setLoopbackModeDisabled(loopbackModeDisabled);
+            return this;
+        } catch (IOException e) {
+            throw new ChannelException(e);
+        }
     }
 
     @Override
     public int getTimeToLive() {
-        return -1;
+        try {
+            return ((EpollDatagramChannel) channel).socket.getTimeToLive();
+        } catch (IOException e) {
+            throw new ChannelException(e);
+        }
     }
 
     @Override
     public EpollDatagramChannelConfig setTimeToLive(int ttl) {
-        throw new UnsupportedOperationException("Multicast not supported");
+        try {
+            ((EpollDatagramChannel) channel).socket.setTimeToLive(ttl);
+            return this;
+        } catch (IOException e) {
+            throw new ChannelException(e);
+        }
     }
 
     @Override
     public InetAddress getInterface() {
-        return null;
+        try {
+            return ((EpollDatagramChannel) channel).socket.getInterface();
+        } catch (IOException e) {
+            throw new ChannelException(e);
+        }
     }
 
     @Override
     public EpollDatagramChannelConfig setInterface(InetAddress interfaceAddress) {
-        throw new UnsupportedOperationException("Multicast not supported");
+        try {
+            ((EpollDatagramChannel) channel).socket.setInterface(interfaceAddress);
+            return this;
+        } catch (IOException e) {
+            throw new ChannelException(e);
+        }
     }
 
     @Override
     public NetworkInterface getNetworkInterface() {
-        return null;
+        try {
+            return ((EpollDatagramChannel) channel).socket.getNetworkInterface();
+        } catch (IOException e) {
+            throw new ChannelException(e);
+        }
     }
 
     @Override
     public EpollDatagramChannelConfig setNetworkInterface(NetworkInterface networkInterface) {
-        throw new UnsupportedOperationException("Multicast not supported");
+        try {
+            EpollDatagramChannel datagramChannel = (EpollDatagramChannel) channel;
+            datagramChannel.socket.setNetworkInterface(networkInterface);
+            return this;
+        } catch (IOException e) {
+            throw new ChannelException(e);
+        }
     }
 
     @Override
@@ -462,4 +507,23 @@ public final class EpollDatagramChannelConfig extends EpollChannelConfig impleme
         }
     }
 
+    /**
+     * Set the maximum {@link io.netty.channel.socket.DatagramPacket} size. This will be used to determine if
+     * {@code recvmmsg} should be used when reading from the underlying socket. When {@code recvmmsg} is used
+     * we may be able to read multiple {@link io.netty.channel.socket.DatagramPacket}s with one syscall and so
+     * greatly improve the performance. This number will be used to slice {@link ByteBuf}s returned by the used
+     * {@link RecvByteBufAllocator}. You can use {@code 0} to disable the usage of recvmmsg, any other bigger value
+     * will enable it.
+     */
+    public EpollDatagramChannelConfig setMaxDatagramPayloadSize(int maxDatagramSize) {
+        this.maxDatagramSize = ObjectUtil.checkPositiveOrZero(maxDatagramSize, "maxDatagramSize");
+        return this;
+    }
+
+    /**
+     * Get the maximum {@link io.netty.channel.socket.DatagramPacket} size.
+     */
+    public int getMaxDatagramPayloadSize() {
+        return maxDatagramSize;
+    }
 }
diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollDomainSocketChannelConfig.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollDomainSocketChannelConfig.java
index ea6c532..22321a4 100644
--- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollDomainSocketChannelConfig.java
+++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollDomainSocketChannelConfig.java
@@ -20,14 +20,23 @@ import io.netty.channel.ChannelOption;
 import io.netty.channel.MessageSizeEstimator;
 import io.netty.channel.RecvByteBufAllocator;
 import io.netty.channel.WriteBufferWaterMark;
+import io.netty.channel.socket.SocketChannelConfig;
 import io.netty.channel.unix.DomainSocketChannelConfig;
 import io.netty.channel.unix.DomainSocketReadMode;
+import io.netty.util.internal.ObjectUtil;
 
+import java.io.IOException;
 import java.util.Map;
 
+import static io.netty.channel.ChannelOption.ALLOW_HALF_CLOSURE;
+import static io.netty.channel.ChannelOption.SO_RCVBUF;
+import static io.netty.channel.ChannelOption.SO_SNDBUF;
+import static io.netty.channel.unix.UnixChannelOption.DOMAIN_SOCKET_READ_MODE;
+
 public final class EpollDomainSocketChannelConfig extends EpollChannelConfig
         implements DomainSocketChannelConfig {
     private volatile DomainSocketReadMode mode = DomainSocketReadMode.BYTES;
+    private volatile boolean allowHalfClosure;
 
     EpollDomainSocketChannelConfig(AbstractEpollChannel channel) {
         super(channel);
@@ -35,15 +44,24 @@ public final class EpollDomainSocketChannelConfig extends EpollChannelConfig
 
     @Override
     public Map<ChannelOption<?>, Object> getOptions() {
-        return getOptions(super.getOptions(), EpollChannelOption.DOMAIN_SOCKET_READ_MODE);
+        return getOptions(super.getOptions(), DOMAIN_SOCKET_READ_MODE, ALLOW_HALF_CLOSURE, SO_SNDBUF, SO_RCVBUF);
     }
 
     @SuppressWarnings("unchecked")
     @Override
     public <T> T getOption(ChannelOption<T> option) {
-        if (option == EpollChannelOption.DOMAIN_SOCKET_READ_MODE) {
+        if (option == DOMAIN_SOCKET_READ_MODE) {
             return (T) getReadMode();
         }
+        if (option == ALLOW_HALF_CLOSURE) {
+            return (T) Boolean.valueOf(isAllowHalfClosure());
+        }
+        if (option == SO_SNDBUF) {
+            return (T) Integer.valueOf(getSendBufferSize());
+        }
+        if (option == SO_RCVBUF) {
+            return (T) Integer.valueOf(getReceiveBufferSize());
+        }
         return super.getOption(option);
     }
 
@@ -51,8 +69,14 @@ public final class EpollDomainSocketChannelConfig extends EpollChannelConfig
     public <T> boolean setOption(ChannelOption<T> option, T value) {
         validate(option, value);
 
-        if (option == EpollChannelOption.DOMAIN_SOCKET_READ_MODE) {
+        if (option == DOMAIN_SOCKET_READ_MODE) {
             setReadMode((DomainSocketReadMode) value);
+        } else if (option == ALLOW_HALF_CLOSURE) {
+            setAllowHalfClosure((Boolean) value);
+        } else if (option == SO_SNDBUF) {
+            setSendBufferSize((Integer) value);
+        } else if (option == SO_RCVBUF) {
+            setReceiveBufferSize((Integer) value);
         } else {
             return super.setOption(option, value);
         }
@@ -137,10 +161,7 @@ public final class EpollDomainSocketChannelConfig extends EpollChannelConfig
 
     @Override
     public EpollDomainSocketChannelConfig setReadMode(DomainSocketReadMode mode) {
-        if (mode == null) {
-            throw new NullPointerException("mode");
-        }
-        this.mode = mode;
+        this.mode = ObjectUtil.checkNotNull(mode, "mode");
         return this;
     }
 
@@ -148,4 +169,53 @@ public final class EpollDomainSocketChannelConfig extends EpollChannelConfig
     public DomainSocketReadMode getReadMode() {
         return mode;
     }
+
+    /**
+     * @see SocketChannelConfig#isAllowHalfClosure()
+     */
+    public boolean isAllowHalfClosure() {
+        return allowHalfClosure;
+    }
+
+    /**
+     * @see SocketChannelConfig#setAllowHalfClosure(boolean)
+     */
+    public EpollDomainSocketChannelConfig setAllowHalfClosure(boolean allowHalfClosure) {
+        this.allowHalfClosure = allowHalfClosure;
+        return this;
+    }
+
+    public int getSendBufferSize() {
+        try {
+            return ((EpollDomainSocketChannel) channel).socket.getSendBufferSize();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public EpollDomainSocketChannelConfig setSendBufferSize(int sendBufferSize) {
+        try {
+            ((EpollDomainSocketChannel) channel).socket.setSendBufferSize(sendBufferSize);
+            return this;
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public int getReceiveBufferSize() {
+        try {
+            return ((EpollDomainSocketChannel) channel).socket.getReceiveBufferSize();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public EpollDomainSocketChannelConfig setReceiveBufferSize(int receiveBufferSize) {
+        try {
+            ((EpollDomainSocketChannel) channel).socket.setReceiveBufferSize(receiveBufferSize);
+            return this;
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
 }
diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollEventLoop.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollEventLoop.java
index 3420d67..526276c 100644
--- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollEventLoop.java
+++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollEventLoop.java
@@ -17,6 +17,7 @@ package io.netty.channel.epoll;
 
 import io.netty.channel.EventLoop;
 import io.netty.channel.EventLoopGroup;
+import io.netty.channel.EventLoopTaskQueueFactory;
 import io.netty.channel.SelectStrategy;
 import io.netty.channel.SingleThreadEventLoop;
 import io.netty.channel.epoll.AbstractEpollChannel.AbstractEpollUnsafe;
@@ -34,7 +35,7 @@ import io.netty.util.internal.logging.InternalLoggerFactory;
 import java.io.IOException;
 import java.util.Queue;
 import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
+import java.util.concurrent.atomic.AtomicLong;
 
 import static java.lang.Math.min;
 
@@ -43,8 +44,6 @@ import static java.lang.Math.min;
  */
 class EpollEventLoop extends SingleThreadEventLoop {
     private static final InternalLogger logger = InternalLoggerFactory.getInstance(EpollEventLoop.class);
-    private static final AtomicIntegerFieldUpdater<EpollEventLoop> WAKEN_UP_UPDATER =
-            AtomicIntegerFieldUpdater.newUpdater(EpollEventLoop.class, "wakenUp");
 
     static {
         // Ensure JNI is initialized by the time this class is loaded by this time!
@@ -52,8 +51,6 @@ class EpollEventLoop extends SingleThreadEventLoop {
         Epoll.ensureAvailability();
     }
 
-    // Pick a number that no task could have previously used.
-    private long prevDeadlineNanos = nanoTime() - 1;
     private final FileDescriptor epollFd;
     private final FileDescriptor eventFd;
     private final FileDescriptor timerFd;
@@ -72,16 +69,26 @@ class EpollEventLoop extends SingleThreadEventLoop {
             return epollWaitNow();
         }
     };
-    @SuppressWarnings("unused") // AtomicIntegerFieldUpdater
-    private volatile int wakenUp;
+
+    private static final long AWAKE = -1L;
+    private static final long NONE = Long.MAX_VALUE;
+
+    // nextWakeupNanos is:
+    //    AWAKE            when EL is awake
+    //    NONE             when EL is waiting with no wakeup scheduled
+    //    other value T    when EL is waiting with wakeup scheduled at time T
+    private final AtomicLong nextWakeupNanos = new AtomicLong(AWAKE);
+    private boolean pendingWakeup;
     private volatile int ioRatio = 50;
 
     // See http://man7.org/linux/man-pages/man2/timerfd_create.2.html.
     private static final long MAX_SCHEDULED_TIMERFD_NS = 999999999;
 
     EpollEventLoop(EventLoopGroup parent, Executor executor, int maxEvents,
-                   SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) {
-        super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler);
+                   SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler,
+                   EventLoopTaskQueueFactory queueFactory) {
+        super(parent, executor, false, newTaskQueue(queueFactory), newTaskQueue(queueFactory),
+                rejectedExecutionHandler);
         selectStrategy = ObjectUtil.checkNotNull(strategy, "strategy");
         if (maxEvents == 0) {
             allowGrowing = true;
@@ -98,12 +105,16 @@ class EpollEventLoop extends SingleThreadEventLoop {
             this.epollFd = epollFd = Native.newEpollCreate();
             this.eventFd = eventFd = Native.newEventFd();
             try {
-                Native.epollCtlAdd(epollFd.intValue(), eventFd.intValue(), Native.EPOLLIN);
+                // It is important to use EPOLLET here as we only want to get the notification once per
+                // wakeup and don't call eventfd_read(...).
+                Native.epollCtlAdd(epollFd.intValue(), eventFd.intValue(), Native.EPOLLIN | Native.EPOLLET);
             } catch (IOException e) {
                 throw new IllegalStateException("Unable to add eventFd filedescriptor to epoll", e);
             }
             this.timerFd = timerFd = Native.newTimerFd();
             try {
+                // It is important to use EPOLLET here as we only want to get the notification once per
+                // wakeup and don't call read(...).
                 Native.epollCtlAdd(epollFd.intValue(), timerFd.intValue(), Native.EPOLLIN | Native.EPOLLET);
             } catch (IOException e) {
                 throw new IllegalStateException("Unable to add timerFd filedescriptor to epoll", e);
@@ -136,6 +147,14 @@ class EpollEventLoop extends SingleThreadEventLoop {
         }
     }
 
+    private static Queue<Runnable> newTaskQueue(
+            EventLoopTaskQueueFactory queueFactory) {
+        if (queueFactory == null) {
+            return newTaskQueue0(DEFAULT_MAX_PENDING_TASKS);
+        }
+        return queueFactory.newTaskQueue(DEFAULT_MAX_PENDING_TASKS);
+    }
+
     /**
      * Return a cleared {@link IovArray} that can be used for writes in this {@link EventLoop}.
      */
@@ -162,12 +181,24 @@ class EpollEventLoop extends SingleThreadEventLoop {
 
     @Override
     protected void wakeup(boolean inEventLoop) {
-        if (!inEventLoop && WAKEN_UP_UPDATER.compareAndSet(this, 0, 1)) {
+        if (!inEventLoop && nextWakeupNanos.getAndSet(AWAKE) != AWAKE) {
             // write to the evfd which will then wake-up epoll_wait(...)
             Native.eventFdWrite(eventFd.intValue(), 1L);
         }
     }
 
+    @Override
+    protected boolean beforeScheduledTaskSubmitted(long deadlineNanos) {
+        // Note this is also correct for the nextWakeupNanos == -1 (AWAKE) case
+        return deadlineNanos < nextWakeupNanos.get();
+    }
+
+    @Override
+    protected boolean afterScheduledTaskSubmitted(long deadlineNanos) {
+        // Note this is also correct for the nextWakeupNanos == -1 (AWAKE) case
+        return deadlineNanos < nextWakeupNanos.get();
+    }
+
     /**
      * Register the given epoll with this {@link EventLoop}.
      */
@@ -175,7 +206,11 @@ class EpollEventLoop extends SingleThreadEventLoop {
         assert inEventLoop();
         int fd = ch.socket.intValue();
         Native.epollCtlAdd(epollFd.intValue(), fd, ch.flags);
-        channels.put(fd, ch);
+        AbstractEpollChannel old = channels.put(fd, ch);
+
+        // We either expect to have no Channel in the map with the same FD or that the FD of the old Channel is already
+        // closed.
+        assert old == null || !old.isOpen();
     }
 
     /**
@@ -191,22 +226,31 @@ class EpollEventLoop extends SingleThreadEventLoop {
      */
     void remove(AbstractEpollChannel ch) throws IOException {
         assert inEventLoop();
+        int fd = ch.socket.intValue();
 
-        if (ch.isOpen()) {
-            int fd = ch.socket.intValue();
-            if (channels.remove(fd) != null) {
-                // Remove the epoll. This is only needed if it's still open as otherwise it will be automatically
-                // removed once the file-descriptor is closed.
-                Native.epollCtlDel(epollFd.intValue(), ch.fd().intValue());
-            }
+        AbstractEpollChannel old = channels.remove(fd);
+        if (old != null && old != ch) {
+            // The Channel mapping was already replaced due FD reuse, put back the stored Channel.
+            channels.put(fd, old);
+
+            // If we found another Channel in the map that is mapped to the same FD the given Channel MUST be closed.
+            assert !ch.isOpen();
+        } else if (ch.isOpen()) {
+            // Remove the epoll. This is only needed if it's still open as otherwise it will be automatically
+            // removed once the file-descriptor is closed.
+            Native.epollCtlDel(epollFd.intValue(), fd);
         }
     }
 
     @Override
     protected Queue<Runnable> newTaskQueue(int maxPendingTasks) {
+        return newTaskQueue0(maxPendingTasks);
+    }
+
+    private static Queue<Runnable> newTaskQueue0(int maxPendingTasks) {
         // This event loop never calls takeTask()
         return maxPendingTasks == Integer.MAX_VALUE ? PlatformDependent.<Runnable>newMpscQueue()
-                                                    : PlatformDependent.<Runnable>newMpscQueue(maxPendingTasks);
+                : PlatformDependent.<Runnable>newMpscQueue(maxPendingTasks);
     }
 
     /**
@@ -227,40 +271,41 @@ class EpollEventLoop extends SingleThreadEventLoop {
         this.ioRatio = ioRatio;
     }
 
-    private int epollWait(boolean oldWakeup) throws IOException {
-        // If a task was submitted when wakenUp value was 1, the task didn't get a chance to produce wakeup event.
-        // So we need to check task queue again before calling epoll_wait. If we don't, the task might be pended
-        // until epoll_wait was timed out. It might be pended until idle timeout if IdleStateHandler existed
-        // in pipeline.
-        if (oldWakeup && hasTasks()) {
-            return epollWaitNow();
-        }
+    @Override
+    public int registeredChannels() {
+        return channels.size();
+    }
 
-        int delaySeconds;
-        int delayNanos;
-        long curDeadlineNanos = deadlineNanos();
-        if (curDeadlineNanos == prevDeadlineNanos) {
-            delaySeconds = -1;
-            delayNanos = -1;
-        } else {
-            long totalDelay = delayNanos(System.nanoTime());
-            prevDeadlineNanos = curDeadlineNanos;
-            delaySeconds = (int) min(totalDelay / 1000000000L, Integer.MAX_VALUE);
-            delayNanos = (int) min(totalDelay - delaySeconds * 1000000000L, MAX_SCHEDULED_TIMERFD_NS);
+    private int epollWait(long deadlineNanos) throws IOException {
+        if (deadlineNanos == NONE) {
+            return Native.epollWait(epollFd, events, timerFd, Integer.MAX_VALUE, 0); // disarm timer
         }
+        long totalDelay = deadlineToDelayNanos(deadlineNanos);
+        int delaySeconds = (int) min(totalDelay / 1000000000L, Integer.MAX_VALUE);
+        int delayNanos = (int) min(totalDelay - delaySeconds * 1000000000L, MAX_SCHEDULED_TIMERFD_NS);
         return Native.epollWait(epollFd, events, timerFd, delaySeconds, delayNanos);
     }
 
+    private int epollWaitNoTimerChange() throws IOException {
+        return Native.epollWait(epollFd, events, false);
+    }
+
     private int epollWaitNow() throws IOException {
-        return Native.epollWait(epollFd, events, timerFd, 0, 0);
+        return Native.epollWait(epollFd, events, true);
     }
 
     private int epollBusyWait() throws IOException {
         return Native.epollBusyWait(epollFd, events);
     }
 
+    private int epollWaitTimeboxed() throws IOException {
+        // Wait with 1 second "safeguard" timeout
+        return Native.epollWait(epollFd, events, 1000);
+    }
+
     @Override
     protected void run() {
+        long prevDeadlineNanos = NONE;
         for (;;) {
             try {
                 int strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
@@ -273,38 +318,45 @@ class EpollEventLoop extends SingleThreadEventLoop {
                         break;
 
                     case SelectStrategy.SELECT:
-                        strategy = epollWait(WAKEN_UP_UPDATER.getAndSet(this, 0) == 1);
-
-                        // 'wakenUp.compareAndSet(false, true)' is always evaluated
-                        // before calling 'selector.wakeup()' to reduce the wake-up
-                        // overhead. (Selector.wakeup() is an expensive operation.)
-                        //
-                        // However, there is a race condition in this approach.
-                        // The race condition is triggered when 'wakenUp' is set to
-                        // true too early.
-                        //
-                        // 'wakenUp' is set to true too early if:
-                        // 1) Selector is waken up between 'wakenUp.set(false)' and
-                        //    'selector.select(...)'. (BAD)
-                        // 2) Selector is waken up between 'selector.select(...)' and
-                        //    'if (wakenUp.get()) { ... }'. (OK)
-                        //
-                        // In the first case, 'wakenUp' is set to true and the
-                        // following 'selector.select(...)' will wake up immediately.
-                        // Until 'wakenUp' is set to false again in the next round,
-                        // 'wakenUp.compareAndSet(false, true)' will fail, and therefore
-                        // any attempt to wake up the Selector will fail, too, causing
-                        // the following 'selector.select(...)' call to block
-                        // unnecessarily.
-                        //
-                        // To fix this problem, we wake up the selector again if wakenUp
-                        // is true immediately after selector.select(...).
-                        // It is inefficient in that it wakes up the selector for both
-                        // the first case (BAD - wake-up required) and the second case
-                        // (OK - no wake-up required).
-
-                        if (wakenUp == 1) {
-                            Native.eventFdWrite(eventFd.intValue(), 1L);
+                        if (pendingWakeup) {
+                            // We are going to be immediately woken so no need to reset wakenUp
+                            // or check for timerfd adjustment.
+                            strategy = epollWaitTimeboxed();
+                            if (strategy != 0) {
+                                break;
+                            }
+                            // We timed out so assume that we missed the write event due to an
+                            // abnormally failed syscall (the write itself or a prior epoll_wait)
+                            logger.warn("Missed eventfd write (not seen after > 1 second)");
+                            pendingWakeup = false;
+                            if (hasTasks()) {
+                                break;
+                            }
+                            // fall-through
+                        }
+
+                        long curDeadlineNanos = nextScheduledTaskDeadlineNanos();
+                        if (curDeadlineNanos == -1L) {
+                            curDeadlineNanos = NONE; // nothing on the calendar
+                        }
+                        nextWakeupNanos.set(curDeadlineNanos);
+                        try {
+                            if (!hasTasks()) {
+                                if (curDeadlineNanos == prevDeadlineNanos) {
+                                    // No timer activity needed
+                                    strategy = epollWaitNoTimerChange();
+                                } else {
+                                    // Timerfd needs to be re-armed or disarmed
+                                    prevDeadlineNanos = curDeadlineNanos;
+                                    strategy = epollWait(curDeadlineNanos);
+                                }
+                            }
+                        } finally {
+                            // Try get() first to avoid much more expensive CAS in the case we
+                            // were woken via the wakeup() method (submitted task)
+                            if (nextWakeupNanos.get() == AWAKE || nextWakeupNanos.getAndSet(AWAKE) == AWAKE) {
+                                pendingWakeup = true;
+                            }
                         }
                         // fallthrough
                     default:
@@ -313,25 +365,26 @@ class EpollEventLoop extends SingleThreadEventLoop {
                 final int ioRatio = this.ioRatio;
                 if (ioRatio == 100) {
                     try {
-                        if (strategy > 0) {
-                            processReady(events, strategy);
+                        if (strategy > 0 && processReady(events, strategy)) {
+                            prevDeadlineNanos = NONE;
                         }
                     } finally {
                         // Ensure we always run tasks.
                         runAllTasks();
                     }
-                } else {
+                } else if (strategy > 0) {
                     final long ioStartTime = System.nanoTime();
-
                     try {
-                        if (strategy > 0) {
-                            processReady(events, strategy);
+                        if (processReady(events, strategy)) {
+                            prevDeadlineNanos = NONE;
                         }
                     } finally {
                         // Ensure we always run tasks.
                         final long ioTime = System.nanoTime() - ioStartTime;
                         runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
                     }
+                } else {
+                    runAllTasks(0); // This will run the minimum number of tasks
                 }
                 if (allowGrowing && strategy == events.length()) {
                     //increase the size of the array as we needed the whole space for the events
@@ -370,29 +423,24 @@ class EpollEventLoop extends SingleThreadEventLoop {
     }
 
     private void closeAll() {
-        try {
-            epollWaitNow();
-        } catch (IOException ignore) {
-            // ignore on close
-        }
         // Using the intermediate collection to prevent ConcurrentModificationException.
         // In the `close()` method, the channel is deleted from `channels` map.
         AbstractEpollChannel[] localChannels = channels.values().toArray(new AbstractEpollChannel[0]);
 
-        for (AbstractEpollChannel ch : localChannels) {
+        for (AbstractEpollChannel ch: localChannels) {
             ch.unsafe().close(ch.unsafe().voidPromise());
         }
     }
 
-    private void processReady(EpollEventArray events, int ready) {
+    // Returns true if a timerFd event was encountered
+    private boolean processReady(EpollEventArray events, int ready) {
+        boolean timerFired = false;
         for (int i = 0; i < ready; i ++) {
             final int fd = events.fd(i);
             if (fd == eventFd.intValue()) {
-                // consume wakeup event.
-                Native.eventFdRead(fd);
+                pendingWakeup = false;
             } else if (fd == timerFd.intValue()) {
-                // consume wakeup event, necessary because the timer is added with ET mode.
-                Native.timerFdRead(fd);
+                timerFired = true;
             } else {
                 final long ev = events.events(i);
 
@@ -446,15 +494,29 @@ class EpollEventLoop extends SingleThreadEventLoop {
                 }
             }
         }
+        return timerFired;
     }
 
     @Override
     protected void cleanup() {
         try {
-            try {
-                epollFd.close();
-            } catch (IOException e) {
-                logger.warn("Failed to close the epoll fd.", e);
+            // Ensure any in-flight wakeup writes have been performed prior to closing eventFd.
+            while (pendingWakeup) {
+                try {
+                    int count = epollWaitTimeboxed();
+                    if (count == 0) {
+                        // We timed-out so assume that the write we're expecting isn't coming
+                        break;
+                    }
+                    for (int i = 0; i < count; i++) {
+                        if (events.fd(i) == eventFd.intValue()) {
+                            pendingWakeup = false;
+                            break;
+                        }
+                    }
+                } catch (IOException ignore) {
+                    // ignore
+                }
             }
             try {
                 eventFd.close();
@@ -466,6 +528,12 @@ class EpollEventLoop extends SingleThreadEventLoop {
             } catch (IOException e) {
                 logger.warn("Failed to close the timer fd.", e);
             }
+
+            try {
+                epollFd.close();
+            } catch (IOException e) {
+                logger.warn("Failed to close the epoll fd.", e);
+            }
         } finally {
             // release native memory
             if (iovArray != null) {
diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollEventLoopGroup.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollEventLoopGroup.java
index acb4212..756093f 100644
--- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollEventLoopGroup.java
+++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollEventLoopGroup.java
@@ -18,9 +18,9 @@ package io.netty.channel.epoll;
 import io.netty.channel.DefaultSelectStrategyFactory;
 import io.netty.channel.EventLoop;
 import io.netty.channel.EventLoopGroup;
+import io.netty.channel.EventLoopTaskQueueFactory;
 import io.netty.channel.MultithreadEventLoopGroup;
 import io.netty.channel.SelectStrategyFactory;
-import io.netty.util.concurrent.EventExecutor;
 import io.netty.util.concurrent.EventExecutorChooserFactory;
 import io.netty.util.concurrent.RejectedExecutionHandler;
 import io.netty.util.concurrent.RejectedExecutionHandlers;
@@ -52,6 +52,14 @@ public final class EpollEventLoopGroup extends MultithreadEventLoopGroup {
         this(nThreads, (ThreadFactory) null);
     }
 
+    /**
+     * Create a new instance using the default number of threads and the given {@link ThreadFactory}.
+     */
+    @SuppressWarnings("deprecation")
+    public EpollEventLoopGroup(ThreadFactory threadFactory) {
+        this(0, threadFactory, 0);
+    }
+
     /**
      * Create a new instance using the specified number of threads and the default {@link ThreadFactory}.
      */
@@ -119,19 +127,28 @@ public final class EpollEventLoopGroup extends MultithreadEventLoopGroup {
         super(nThreads, executor, chooserFactory, 0, selectStrategyFactory, rejectedExecutionHandler);
     }
 
+    public EpollEventLoopGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory,
+                               SelectStrategyFactory selectStrategyFactory,
+                               RejectedExecutionHandler rejectedExecutionHandler,
+                               EventLoopTaskQueueFactory queueFactory) {
+        super(nThreads, executor, chooserFactory, 0, selectStrategyFactory, rejectedExecutionHandler, queueFactory);
+    }
+
     /**
-     * Sets the percentage of the desired amount of time spent for I/O in the child event loops.  The default value is
-     * {@code 50}, which means the event loop will try to spend the same amount of time for I/O as for non-I/O tasks.
+     * @deprecated This method will be removed in future releases, and is not guaranteed to have any impacts.
      */
+    @Deprecated
     public void setIoRatio(int ioRatio) {
-        for (EventExecutor e: this) {
-            ((EpollEventLoop) e).setIoRatio(ioRatio);
+        if (ioRatio <= 0 || ioRatio > 100) {
+            throw new IllegalArgumentException("ioRatio: " + ioRatio + " (expected: 0 < ioRatio <= 100)");
         }
     }
 
     @Override
     protected EventLoop newChild(Executor executor, Object... args) throws Exception {
+        EventLoopTaskQueueFactory queueFactory = args.length == 4 ? (EventLoopTaskQueueFactory) args[3] : null;
         return new EpollEventLoop(this, executor, (Integer) args[0],
-                ((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]);
+                ((SelectStrategyFactory) args[1]).newSelectStrategy(),
+                (RejectedExecutionHandler) args[2], queueFactory);
     }
 }
diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollRecvByteAllocatorHandle.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollRecvByteAllocatorHandle.java
index a500a89..2685369 100644
--- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollRecvByteAllocatorHandle.java
+++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollRecvByteAllocatorHandle.java
@@ -17,16 +17,14 @@ package io.netty.channel.epoll;
 
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.ByteBufAllocator;
-import io.netty.channel.ChannelConfig;
-import io.netty.channel.RecvByteBufAllocator;
+import io.netty.channel.RecvByteBufAllocator.DelegatingHandle;
+import io.netty.channel.RecvByteBufAllocator.ExtendedHandle;
 import io.netty.channel.unix.PreferredDirectByteBufAllocator;
 import io.netty.util.UncheckedBooleanSupplier;
-import io.netty.util.internal.ObjectUtil;
 
-class EpollRecvByteAllocatorHandle implements RecvByteBufAllocator.ExtendedHandle {
+class EpollRecvByteAllocatorHandle extends DelegatingHandle implements ExtendedHandle {
     private final PreferredDirectByteBufAllocator preferredDirectByteBufAllocator =
             new PreferredDirectByteBufAllocator();
-    private final RecvByteBufAllocator.ExtendedHandle delegate;
     private final UncheckedBooleanSupplier defaultMaybeMoreDataSupplier = new UncheckedBooleanSupplier() {
         @Override
         public boolean get() {
@@ -36,8 +34,8 @@ class EpollRecvByteAllocatorHandle implements RecvByteBufAllocator.ExtendedHandl
     private boolean isEdgeTriggered;
     private boolean receivedRdHup;
 
-    EpollRecvByteAllocatorHandle(RecvByteBufAllocator.ExtendedHandle handle) {
-        delegate = ObjectUtil.checkNotNull(handle, "handle");
+    EpollRecvByteAllocatorHandle(ExtendedHandle handle) {
+        super(handle);
     }
 
     final void receivedRdHup() {
@@ -74,57 +72,17 @@ class EpollRecvByteAllocatorHandle implements RecvByteBufAllocator.ExtendedHandl
     public final ByteBuf allocate(ByteBufAllocator alloc) {
         // We need to ensure we always allocate a direct ByteBuf as we can only use a direct buffer to read via JNI.
         preferredDirectByteBufAllocator.updateAllocator(alloc);
-        return delegate.allocate(preferredDirectByteBufAllocator);
-    }
-
-    @Override
-    public final int guess() {
-        return delegate.guess();
-    }
-
-    @Override
-    public final void reset(ChannelConfig config) {
-        delegate.reset(config);
-    }
-
-    @Override
-    public final void incMessagesRead(int numMessages) {
-        delegate.incMessagesRead(numMessages);
-    }
-
-    @Override
-    public final void lastBytesRead(int bytes) {
-        delegate.lastBytesRead(bytes);
-    }
-
-    @Override
-    public final int lastBytesRead() {
-        return delegate.lastBytesRead();
-    }
-
-    @Override
-    public final int attemptedBytesRead() {
-        return delegate.attemptedBytesRead();
-    }
-
-    @Override
-    public final void attemptedBytesRead(int bytes) {
-        delegate.attemptedBytesRead(bytes);
-    }
-
-    @Override
-    public final void readComplete() {
-        delegate.readComplete();
+        return delegate().allocate(preferredDirectByteBufAllocator);
     }
 
     @Override
     public final boolean continueReading(UncheckedBooleanSupplier maybeMoreDataSupplier) {
-        return delegate.continueReading(maybeMoreDataSupplier);
+        return ((ExtendedHandle) delegate()).continueReading(maybeMoreDataSupplier);
     }
 
     @Override
     public final boolean continueReading() {
         // We must override the supplier which determines if there maybe more data to read.
-        return delegate.continueReading(defaultMaybeMoreDataSupplier);
+        return continueReading(defaultMaybeMoreDataSupplier);
     }
 }
diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollRecvByteAllocatorStreamingHandle.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollRecvByteAllocatorStreamingHandle.java
index f6ba5f5..071acd5 100644
--- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollRecvByteAllocatorStreamingHandle.java
+++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollRecvByteAllocatorStreamingHandle.java
@@ -18,7 +18,7 @@ package io.netty.channel.epoll;
 import io.netty.channel.RecvByteBufAllocator;
 
 final class EpollRecvByteAllocatorStreamingHandle extends EpollRecvByteAllocatorHandle {
-    public EpollRecvByteAllocatorStreamingHandle(RecvByteBufAllocator.ExtendedHandle handle) {
+    EpollRecvByteAllocatorStreamingHandle(RecvByteBufAllocator.ExtendedHandle handle) {
         super(handle);
     }
 
diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollServerChannelConfig.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollServerChannelConfig.java
index 514b6c3..6f85573 100644
--- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollServerChannelConfig.java
+++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollServerChannelConfig.java
@@ -30,6 +30,7 @@ import java.util.Map;
 import static io.netty.channel.ChannelOption.SO_BACKLOG;
 import static io.netty.channel.ChannelOption.SO_RCVBUF;
 import static io.netty.channel.ChannelOption.SO_REUSEADDR;
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
 
 public class EpollServerChannelConfig extends EpollChannelConfig implements ServerSocketChannelConfig {
     private volatile int backlog = NetUtil.SOMAXCONN;
@@ -81,6 +82,7 @@ public class EpollServerChannelConfig extends EpollChannelConfig implements Serv
         return true;
     }
 
+    @Override
     public boolean isReuseAddress() {
         try {
             return ((AbstractEpollChannel) channel).socket.isReuseAddress();
@@ -89,6 +91,7 @@ public class EpollServerChannelConfig extends EpollChannelConfig implements Serv
         }
     }
 
+    @Override
     public EpollServerChannelConfig setReuseAddress(boolean reuseAddress) {
         try {
             ((AbstractEpollChannel) channel).socket.setReuseAddress(reuseAddress);
@@ -98,6 +101,7 @@ public class EpollServerChannelConfig extends EpollChannelConfig implements Serv
         }
     }
 
+    @Override
     public int getReceiveBufferSize() {
         try {
             return ((AbstractEpollChannel) channel).socket.getReceiveBufferSize();
@@ -106,6 +110,7 @@ public class EpollServerChannelConfig extends EpollChannelConfig implements Serv
         }
     }
 
+    @Override
     public EpollServerChannelConfig setReceiveBufferSize(int receiveBufferSize) {
         try {
             ((AbstractEpollChannel) channel).socket.setReceiveBufferSize(receiveBufferSize);
@@ -115,14 +120,14 @@ public class EpollServerChannelConfig extends EpollChannelConfig implements Serv
         }
     }
 
+    @Override
     public int getBacklog() {
         return backlog;
     }
 
+    @Override
     public EpollServerChannelConfig setBacklog(int backlog) {
-        if (backlog < 0) {
-            throw new IllegalArgumentException("backlog: " + backlog);
-        }
+        checkPositiveOrZero(backlog, "backlog");
         this.backlog = backlog;
         return this;
     }
@@ -146,9 +151,7 @@ public class EpollServerChannelConfig extends EpollChannelConfig implements Serv
      * @see <a href="https://tools.ietf.org/html/rfc7413">RFC 7413 TCP FastOpen</a>
      */
     public EpollServerChannelConfig setTcpFastopen(int pendingFastOpenRequestsThreshold) {
-        if (this.pendingFastOpenRequestsThreshold < 0) {
-            throw new IllegalArgumentException("pendingFastOpenRequestsThreshold: " + pendingFastOpenRequestsThreshold);
-        }
+        checkPositiveOrZero(this.pendingFastOpenRequestsThreshold, "pendingFastOpenRequestsThreshold");
         this.pendingFastOpenRequestsThreshold = pendingFastOpenRequestsThreshold;
         return this;
     }
diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/LinuxSocket.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/LinuxSocket.java
index d3578b4..db2d246 100644
--- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/LinuxSocket.java
+++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/LinuxSocket.java
@@ -15,35 +15,143 @@
  */
 package io.netty.channel.epoll;
 
+import io.netty.channel.ChannelException;
 import io.netty.channel.DefaultFileRegion;
-import io.netty.channel.unix.Errors.NativeIoException;
 import io.netty.channel.unix.NativeInetAddress;
 import io.netty.channel.unix.PeerCredentials;
 import io.netty.channel.unix.Socket;
-import io.netty.util.internal.ThrowableUtil;
+import io.netty.channel.socket.InternetProtocolFamily;
+import io.netty.util.internal.PlatformDependent;
+import io.netty.util.internal.SocketUtils;
 
 import java.io.IOException;
 import java.net.InetAddress;
-import java.nio.channels.ClosedChannelException;
+import java.net.Inet6Address;
+import java.net.NetworkInterface;
+import java.net.UnknownHostException;
+import java.util.Enumeration;
 
-import static io.netty.channel.unix.Errors.ERRNO_EPIPE_NEGATIVE;
 import static io.netty.channel.unix.Errors.ioResult;
-import static io.netty.channel.unix.Errors.newConnectionResetException;
 
 /**
  * A socket which provides access Linux native methods.
  */
 final class LinuxSocket extends Socket {
+    static final InetAddress INET6_ANY = unsafeInetAddrByName("::");
+    private static final InetAddress INET_ANY = unsafeInetAddrByName("0.0.0.0");
     private static final long MAX_UINT32_T = 0xFFFFFFFFL;
-    private static final NativeIoException SENDFILE_CONNECTION_RESET_EXCEPTION =
-            newConnectionResetException("syscall:sendfile(...)", ERRNO_EPIPE_NEGATIVE);
-    private static final ClosedChannelException SENDFILE_CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace(
-            new ClosedChannelException(), Native.class, "sendfile(...)");
 
-    public LinuxSocket(int fd) {
+    LinuxSocket(int fd) {
         super(fd);
     }
 
+    private InternetProtocolFamily family() {
+        return ipv6 ? InternetProtocolFamily.IPv6 : InternetProtocolFamily.IPv4;
+    }
+
+    int sendmmsg(NativeDatagramPacketArray.NativeDatagramPacket[] msgs,
+                               int offset, int len) throws IOException {
+        return Native.sendmmsg(intValue(), ipv6, msgs, offset, len);
+    }
+
+    int recvmmsg(NativeDatagramPacketArray.NativeDatagramPacket[] msgs,
+                 int offset, int len) throws IOException {
+        return Native.recvmmsg(intValue(), ipv6, msgs, offset, len);
+    }
+
+    void setTimeToLive(int ttl) throws IOException {
+        setTimeToLive(intValue(), ttl);
+    }
+
+    void setInterface(InetAddress address) throws IOException {
+        final NativeInetAddress a = NativeInetAddress.newInstance(address);
+        setInterface(intValue(), ipv6, a.address(), a.scopeId(), interfaceIndex(address));
+    }
+
+    void setNetworkInterface(NetworkInterface netInterface) throws IOException {
+        InetAddress address = deriveInetAddress(netInterface, family() == InternetProtocolFamily.IPv6);
+        if (address.equals(family() == InternetProtocolFamily.IPv4 ? INET_ANY : INET6_ANY)) {
+            throw new IOException("NetworkInterface does not support " + family());
+        }
+        final NativeInetAddress nativeAddress = NativeInetAddress.newInstance(address);
+        setInterface(intValue(), ipv6, nativeAddress.address(), nativeAddress.scopeId(), interfaceIndex(netInterface));
+    }
+
+    InetAddress getInterface() throws IOException {
+        NetworkInterface inf = getNetworkInterface();
+        if (inf != null) {
+            Enumeration<InetAddress> addresses = SocketUtils.addressesFromNetworkInterface(inf);
+            if (addresses.hasMoreElements()) {
+                return addresses.nextElement();
+            }
+        }
+        return null;
+    }
+
+    NetworkInterface getNetworkInterface() throws IOException {
+        int ret = getInterface(intValue(), ipv6);
+        if (ipv6) {
+            return PlatformDependent.javaVersion() >= 7 ? NetworkInterface.getByIndex(ret) : null;
+        }
+        InetAddress address = inetAddress(ret);
+        return address != null ? NetworkInterface.getByInetAddress(address) : null;
+    }
+
+    private static InetAddress inetAddress(int value) {
+        byte[] var1 = {
+                (byte) (value >>> 24 & 255),
+                (byte) (value >>> 16 & 255),
+                (byte) (value >>> 8 & 255),
+                (byte) (value & 255)
+        };
+
+        try {
+            return InetAddress.getByAddress(var1);
+        } catch (UnknownHostException ignore) {
+            return null;
+        }
+    }
+
+    void joinGroup(InetAddress group, NetworkInterface netInterface, InetAddress source) throws IOException {
+        final NativeInetAddress g = NativeInetAddress.newInstance(group);
+        final boolean isIpv6 = group instanceof Inet6Address;
+        final NativeInetAddress i = NativeInetAddress.newInstance(deriveInetAddress(netInterface, isIpv6));
+        if (source != null) {
+            final NativeInetAddress s = NativeInetAddress.newInstance(source);
+            joinSsmGroup(intValue(), ipv6, g.address(), i.address(),
+                    g.scopeId(), interfaceIndex(netInterface), s.address());
+        } else {
+            joinGroup(intValue(), ipv6, g.address(), i.address(), g.scopeId(), interfaceIndex(netInterface));
+        }
+    }
+
+    void leaveGroup(InetAddress group, NetworkInterface netInterface, InetAddress source) throws IOException {
+        final NativeInetAddress g = NativeInetAddress.newInstance(group);
+        final boolean isIpv6 = group instanceof Inet6Address;
+        final NativeInetAddress i = NativeInetAddress.newInstance(deriveInetAddress(netInterface, isIpv6));
+        if (source != null) {
+            final NativeInetAddress s = NativeInetAddress.newInstance(source);
+            leaveSsmGroup(intValue(), ipv6, g.address(), i.address(),
+                    g.scopeId(), interfaceIndex(netInterface), s.address());
+        } else {
+            leaveGroup(intValue(), ipv6, g.address(), i.address(), g.scopeId(), interfaceIndex(netInterface));
+        }
+    }
+
+    private static int interfaceIndex(NetworkInterface networkInterface) {
+        return PlatformDependent.javaVersion() >= 7 ? networkInterface.getIndex() : -1;
+    }
+
+    private static int interfaceIndex(InetAddress address) throws IOException {
+        if (PlatformDependent.javaVersion() >= 7) {
+            NetworkInterface iface = NetworkInterface.getByInetAddress(address);
+            if (iface != null) {
+                return iface.getIndex();
+            }
+        }
+        return -1;
+    }
+
     void setTcpDeferAccept(int deferAccept) throws IOException {
         setTcpDeferAccept(intValue(), deferAccept);
     }
@@ -107,13 +215,17 @@ final class LinuxSocket extends Socket {
         setIpRecvOrigDestAddr(intValue(), enabled ? 1 : 0);
     }
 
+    int getTimeToLive() throws IOException {
+        return getTimeToLive(intValue());
+    }
+
     void getTcpInfo(EpollTcpInfo info) throws IOException {
         getTcpInfo(intValue(), info.info);
     }
 
     void setTcpMd5Sig(InetAddress address, byte[] key) throws IOException {
         final NativeInetAddress a = NativeInetAddress.newInstance(address);
-        setTcpMd5Sig(intValue(), a.address(), a.scopeId(), key);
+        setTcpMd5Sig(intValue(), ipv6, a.address(), a.scopeId(), key);
     }
 
     boolean isTcpCork() throws IOException  {
@@ -168,6 +280,14 @@ final class LinuxSocket extends Socket {
         return getPeerCredentials(intValue());
     }
 
+    boolean isLoopbackModeDisabled() throws IOException {
+        return getIpMulticastLoop(intValue(), ipv6) == 0;
+    }
+
+    void setLoopbackModeDisabled(boolean loopbackModeDisabled) throws IOException {
+        setIpMulticastLoop(intValue(), ipv6, loopbackModeDisabled ? 0 : 1);
+    }
+
     long sendFile(DefaultFileRegion src, long baseOffset, long offset, long length) throws IOException {
         // Open the file-region as it may be created via the lazy constructor. This is needed as we directly access
         // the FileChannel field via JNI.
@@ -177,21 +297,60 @@ final class LinuxSocket extends Socket {
         if (res >= 0) {
             return res;
         }
-        return ioResult("sendfile", (int) res, SENDFILE_CONNECTION_RESET_EXCEPTION, SENDFILE_CLOSED_CHANNEL_EXCEPTION);
+        return ioResult("sendfile", (int) res);
+    }
+
+    private static InetAddress deriveInetAddress(NetworkInterface netInterface, boolean ipv6) {
+        final InetAddress ipAny = ipv6 ? INET6_ANY : INET_ANY;
+        if (netInterface != null) {
+            final Enumeration<InetAddress> ias = netInterface.getInetAddresses();
+            while (ias.hasMoreElements()) {
+                final InetAddress ia = ias.nextElement();
+                final boolean isV6 = ia instanceof Inet6Address;
+                if (isV6 == ipv6) {
+                    return ia;
+                }
+            }
+        }
+        return ipAny;
+    }
+
+    public static LinuxSocket newSocketStream(boolean ipv6) {
+        return new LinuxSocket(newSocketStream0(ipv6));
     }
 
     public static LinuxSocket newSocketStream() {
-        return new LinuxSocket(newSocketStream0());
+        return newSocketStream(isIPv6Preferred());
+    }
+
+    public static LinuxSocket newSocketDgram(boolean ipv6) {
+        return new LinuxSocket(newSocketDgram0(ipv6));
     }
 
     public static LinuxSocket newSocketDgram() {
-        return new LinuxSocket(newSocketDgram0());
+        return newSocketDgram(isIPv6Preferred());
     }
 
     public static LinuxSocket newSocketDomain() {
         return new LinuxSocket(newSocketDomain0());
     }
 
+    private static InetAddress unsafeInetAddrByName(String inetName) {
+        try {
+            return InetAddress.getByName(inetName);
+        } catch (UnknownHostException uhe) {
+            throw new ChannelException(uhe);
+        }
+    }
+
+    private static native void joinGroup(int fd, boolean ipv6, byte[] group, byte[] interfaceAddress,
+                                         int scopeId, int interfaceIndex) throws IOException;
+    private static native void joinSsmGroup(int fd, boolean ipv6, byte[] group, byte[] interfaceAddress,
+                                            int scopeId, int interfaceIndex, byte[] source) throws IOException;
+    private static native void leaveGroup(int fd, boolean ipv6, byte[] group, byte[] interfaceAddress,
+                                          int scopeId, int interfaceIndex) throws IOException;
+    private static native void leaveSsmGroup(int fd, boolean ipv6, byte[] group, byte[] interfaceAddress,
+                                             int scopeId, int interfaceIndex, byte[] source) throws IOException;
     private static native long sendFile(int socketFd, DefaultFileRegion src, long baseOffset,
                                         long offset, long length) throws IOException;
 
@@ -204,6 +363,7 @@ final class LinuxSocket extends Socket {
     private static native int getTcpKeepIntvl(int fd) throws IOException;
     private static native int getTcpKeepCnt(int fd) throws IOException;
     private static native int getTcpUserTimeout(int fd) throws IOException;
+    private static native int getTimeToLive(int fd) throws IOException;
     private static native int isIpFreeBind(int fd) throws IOException;
     private static native int isIpTransparent(int fd) throws IOException;
     private static native int isIpRecvOrigDestAddr(int fd) throws IOException;
@@ -225,5 +385,12 @@ final class LinuxSocket extends Socket {
     private static native void setIpFreeBind(int fd, int freeBind) throws IOException;
     private static native void setIpTransparent(int fd, int transparent) throws IOException;
     private static native void setIpRecvOrigDestAddr(int fd, int transparent) throws IOException;
-    private static native void setTcpMd5Sig(int fd, byte[] address, int scopeId, byte[] key) throws IOException;
+    private static native void setTcpMd5Sig(
+            int fd, boolean ipv6, byte[] address, int scopeId, byte[] key) throws IOException;
+    private static native void setInterface(
+            int fd, boolean ipv6, byte[] interfaceAddress, int scopeId, int networkInterfaceIndex) throws IOException;
+    private static native int getInterface(int fd, boolean ipv6);
+    private static native int getIpMulticastLoop(int fd, boolean ipv6) throws IOException;
+    private static native void setIpMulticastLoop(int fd, boolean ipv6, int enabled) throws IOException;
+    private static native void setTimeToLive(int fd, int ttl) throws IOException;
 }
diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/Native.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/Native.java
index 1eb2e5d..437e0ef 100644
--- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/Native.java
+++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/Native.java
@@ -15,7 +15,6 @@
  */
 package io.netty.channel.epoll;
 
-import io.netty.channel.unix.Errors.NativeIoException;
 import io.netty.channel.unix.FileDescriptor;
 import io.netty.channel.unix.Socket;
 import io.netty.util.internal.NativeLibraryLoader;
@@ -26,7 +25,6 @@ import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 
 import java.io.IOException;
-import java.nio.channels.ClosedChannelException;
 import java.util.Locale;
 
 import static io.netty.channel.epoll.NativeStaticallyReferencedJniMethods.epollerr;
@@ -34,13 +32,12 @@ import static io.netty.channel.epoll.NativeStaticallyReferencedJniMethods.epolle
 import static io.netty.channel.epoll.NativeStaticallyReferencedJniMethods.epollin;
 import static io.netty.channel.epoll.NativeStaticallyReferencedJniMethods.epollout;
 import static io.netty.channel.epoll.NativeStaticallyReferencedJniMethods.epollrdhup;
+import static io.netty.channel.epoll.NativeStaticallyReferencedJniMethods.isSupportingRecvmmsg;
 import static io.netty.channel.epoll.NativeStaticallyReferencedJniMethods.isSupportingSendmmsg;
 import static io.netty.channel.epoll.NativeStaticallyReferencedJniMethods.isSupportingTcpFastopen;
 import static io.netty.channel.epoll.NativeStaticallyReferencedJniMethods.kernelVersion;
 import static io.netty.channel.epoll.NativeStaticallyReferencedJniMethods.tcpMd5SigMaxKeyLen;
-import static io.netty.channel.unix.Errors.ERRNO_EPIPE_NEGATIVE;
 import static io.netty.channel.unix.Errors.ioResult;
-import static io.netty.channel.unix.Errors.newConnectionResetException;
 import static io.netty.channel.unix.Errors.newIOException;
 
 /**
@@ -71,24 +68,12 @@ public final class Native {
     public static final int EPOLLERR = epollerr();
 
     public static final boolean IS_SUPPORTING_SENDMMSG = isSupportingSendmmsg();
+    static final boolean IS_SUPPORTING_RECVMMSG = isSupportingRecvmmsg();
+
     public static final boolean IS_SUPPORTING_TCP_FASTOPEN = isSupportingTcpFastopen();
     public static final int TCP_MD5SIG_MAXKEYLEN = tcpMd5SigMaxKeyLen();
     public static final String KERNEL_VERSION = kernelVersion();
 
-    private static final NativeIoException SENDMMSG_CONNECTION_RESET_EXCEPTION;
-    private static final NativeIoException SPLICE_CONNECTION_RESET_EXCEPTION;
-    private static final ClosedChannelException SENDMMSG_CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace(
-            new ClosedChannelException(), Native.class, "sendmmsg(...)");
-    private static final ClosedChannelException SPLICE_CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace(
-            new ClosedChannelException(), Native.class, "splice(...)");
-
-    static {
-        SENDMMSG_CONNECTION_RESET_EXCEPTION = newConnectionResetException("syscall:sendmmsg(...)",
-                ERRNO_EPIPE_NEGATIVE);
-        SPLICE_CONNECTION_RESET_EXCEPTION = newConnectionResetException("syscall:splice(...)",
-                ERRNO_EPIPE_NEGATIVE);
-    }
-
     public static FileDescriptor newEventFd() {
         return new FileDescriptor(eventFd());
     }
@@ -102,6 +87,7 @@ public final class Native {
     public static native void eventFdWrite(int fd, long value);
     public static native void eventFdRead(int fd);
     static native void timerFdRead(int fd);
+    static native void timerFdSetTime(int fd, int sec, int nsec) throws IOException;
 
     public static FileDescriptor newEpollCreate() {
         return new FileDescriptor(epollCreate());
@@ -109,8 +95,21 @@ public final class Native {
 
     private static native int epollCreate();
 
+    /**
+     * @deprecated this method is no longer supported. This functionality is internal to this package.
+     */
+    @Deprecated
     public static int epollWait(FileDescriptor epollFd, EpollEventArray events, FileDescriptor timerFd,
                                 int timeoutSec, int timeoutNs) throws IOException {
+        if (timeoutSec == 0 && timeoutNs == 0) {
+            // Zero timeout => poll (aka return immediately)
+            return epollWait(epollFd, events, 0);
+        }
+        if (timeoutSec == Integer.MAX_VALUE) {
+            // Max timeout => wait indefinitely: disarm timerfd first
+            timeoutSec = 0;
+            timeoutNs = 0;
+        }
         int ready = epollWait0(epollFd.intValue(), events.memoryAddress(), events.length(), timerFd.intValue(),
                                timeoutSec, timeoutNs);
         if (ready < 0) {
@@ -118,7 +117,21 @@ public final class Native {
         }
         return ready;
     }
-    private static native int epollWait0(int efd, long address, int len, int timerFd, int timeoutSec, int timeoutNs);
+
+    static int epollWait(FileDescriptor epollFd, EpollEventArray events, boolean immediatePoll) throws IOException {
+        return epollWait(epollFd, events, immediatePoll ? 0 : -1);
+    }
+
+    /**
+     * This uses epoll's own timeout and does not reset/re-arm any timerfd
+     */
+    static int epollWait(FileDescriptor epollFd, EpollEventArray events, int timeoutMillis) throws IOException {
+        int ready = epollWait(epollFd.intValue(), events.memoryAddress(), events.length(), timeoutMillis);
+        if (ready < 0) {
+            throw newIOException("epoll_wait", ready);
+        }
+        return ready;
+    }
 
     /**
      * Non-blocking variant of
@@ -133,6 +146,8 @@ public final class Native {
         return ready;
     }
 
+    private static native int epollWait0(int efd, long address, int len, int timerFd, int timeoutSec, int timeoutNs);
+    private static native int epollWait(int efd, long address, int len, int timeout);
     private static native int epollBusyWait0(int efd, long address, int len);
 
     public static void epollCtlAdd(int efd, final int fd, final int flags) throws IOException {
@@ -141,7 +156,7 @@ public final class Native {
             throw newIOException("epoll_ctl", res);
         }
     }
-    private static native int epollCtlAdd0(int efd, final int fd, final int flags);
+    private static native int epollCtlAdd0(int efd, int fd, int flags);
 
     public static void epollCtlMod(int efd, final int fd, final int flags) throws IOException {
         int res = epollCtlMod0(efd, fd, flags);
@@ -149,7 +164,7 @@ public final class Native {
             throw newIOException("epoll_ctl", res);
         }
     }
-    private static native int epollCtlMod0(int efd, final int fd, final int flags);
+    private static native int epollCtlMod0(int efd, int fd, int flags);
 
     public static void epollCtlDel(int efd, final int fd) throws IOException {
         int res = epollCtlDel0(efd, fd);
@@ -157,7 +172,7 @@ public final class Native {
             throw newIOException("epoll_ctl", res);
         }
     }
-    private static native int epollCtlDel0(int efd, final int fd);
+    private static native int epollCtlDel0(int efd, int fd);
 
     // File-descriptor operations
     public static int splice(int fd, long offIn, int fdOut, long offOut, long len) throws IOException {
@@ -165,22 +180,40 @@ public final class Native {
         if (res >= 0) {
             return res;
         }
-        return ioResult("splice", res, SPLICE_CONNECTION_RESET_EXCEPTION, SPLICE_CLOSED_CHANNEL_EXCEPTION);
+        return ioResult("splice", res);
     }
 
     private static native int splice0(int fd, long offIn, int fdOut, long offOut, long len);
 
-    public static int sendmmsg(
-            int fd, NativeDatagramPacketArray.NativeDatagramPacket[] msgs, int offset, int len) throws IOException {
-        int res = sendmmsg0(fd, msgs, offset, len);
+    @Deprecated
+    public static int sendmmsg(int fd, NativeDatagramPacketArray.NativeDatagramPacket[] msgs,
+                               int offset, int len) throws IOException {
+        return sendmmsg(fd, Socket.isIPv6Preferred(), msgs, offset, len);
+    }
+
+    static int sendmmsg(int fd, boolean ipv6, NativeDatagramPacketArray.NativeDatagramPacket[] msgs,
+                               int offset, int len) throws IOException {
+        int res = sendmmsg0(fd, ipv6, msgs, offset, len);
         if (res >= 0) {
             return res;
         }
-        return ioResult("sendmmsg", res, SENDMMSG_CONNECTION_RESET_EXCEPTION, SENDMMSG_CLOSED_CHANNEL_EXCEPTION);
+        return ioResult("sendmmsg", res);
     }
 
     private static native int sendmmsg0(
-            int fd, NativeDatagramPacketArray.NativeDatagramPacket[] msgs, int offset, int len);
+            int fd, boolean ipv6, NativeDatagramPacketArray.NativeDatagramPacket[] msgs, int offset, int len);
+
+    static int recvmmsg(int fd, boolean ipv6, NativeDatagramPacketArray.NativeDatagramPacket[] msgs,
+                        int offset, int len) throws IOException {
+        int res = recvmmsg0(fd, ipv6, msgs, offset, len);
+        if (res >= 0) {
+            return res;
+        }
+        return ioResult("recvmmsg", res);
+    }
+
+    private static native int recvmmsg0(
+            int fd, boolean ipv6, NativeDatagramPacketArray.NativeDatagramPacket[] msgs, int offset, int len);
 
     // epoll_event related
     public static native int sizeofEpollEvent();
diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/NativeDatagramPacketArray.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/NativeDatagramPacketArray.java
index 0bdb9eb..4b0e675 100644
--- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/NativeDatagramPacketArray.java
+++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/NativeDatagramPacketArray.java
@@ -17,22 +17,35 @@ package io.netty.channel.epoll;
 
 import io.netty.buffer.ByteBuf;
 import io.netty.channel.ChannelOutboundBuffer;
+import io.netty.channel.ChannelOutboundBuffer.MessageProcessor;
 import io.netty.channel.socket.DatagramPacket;
 import io.netty.channel.unix.IovArray;
+import io.netty.channel.unix.Limits;
+
 import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
 
 import static io.netty.channel.unix.Limits.UIO_MAX_IOV;
-import static io.netty.channel.unix.NativeInetAddress.ipv4MappedIpv6Address;
+import static io.netty.channel.unix.NativeInetAddress.copyIpv4MappedIpv6Address;
 
 /**
  * Support <a href="http://linux.die.net/man/2/sendmmsg">sendmmsg(...)</a> on linux with GLIBC 2.14+
  */
-final class NativeDatagramPacketArray implements ChannelOutboundBuffer.MessageProcessor {
+final class NativeDatagramPacketArray {
 
     // Use UIO_MAX_IOV as this is the maximum number we can write with one sendmmsg(...) call.
     private final NativeDatagramPacket[] packets = new NativeDatagramPacket[UIO_MAX_IOV];
+
+    // We share one IovArray for all NativeDatagramPackets to reduce memory overhead. This will allow us to write
+    // up to IOV_MAX iovec across all messages in one sendmmsg(...) call.
+    private final IovArray iovArray = new IovArray();
+
+    // temporary array to copy the ipv4 part of ipv6-mapped-ipv4 addresses and then create a Inet4Address out of it.
+    private final byte[] ipv4Bytes = new byte[4];
+    private final MyMessageProcessor processor = new MyMessageProcessor();
+
     private int count;
 
     NativeDatagramPacketArray() {
@@ -41,32 +54,34 @@ final class NativeDatagramPacketArray implements ChannelOutboundBuffer.MessagePr
         }
     }
 
-    /**
-     * Try to add the given {@link DatagramPacket}. Returns {@code true} on success,
-     * {@code false} otherwise.
-     */
-    boolean add(DatagramPacket packet) {
+    boolean addWritable(ByteBuf buf, int index, int len) {
+        return add0(buf, index, len, null);
+    }
+
+    private boolean add0(ByteBuf buf, int index, int len, InetSocketAddress recipient) {
         if (count == packets.length) {
+            // We already filled up to UIO_MAX_IOV messages. This is the max allowed per
+            // recvmmsg(...) / sendmmsg(...) call, we will try again later.
             return false;
         }
-        ByteBuf content = packet.content();
-        int len = content.readableBytes();
         if (len == 0) {
             return true;
         }
-        NativeDatagramPacket p = packets[count];
-        InetSocketAddress recipient = packet.recipient();
-        if (!p.init(content, recipient)) {
+        int offset = iovArray.count();
+        if (offset == Limits.IOV_MAX || !iovArray.add(buf, index, len)) {
+            // Not enough space to hold the whole content, we will try again later.
             return false;
         }
+        NativeDatagramPacket p = packets[count];
+        p.init(iovArray.memoryAddress(offset), iovArray.count() - offset, recipient);
 
         count++;
         return true;
     }
 
-    @Override
-    public boolean processMessage(Object msg) {
-        return msg instanceof DatagramPacket && add((DatagramPacket) msg);
+    void add(ChannelOutboundBuffer buffer, boolean connected) throws Exception {
+        processor.connected = connected;
+        buffer.forEachFlushedMessage(processor);
     }
 
     /**
@@ -85,12 +100,28 @@ final class NativeDatagramPacketArray implements ChannelOutboundBuffer.MessagePr
 
     void clear() {
         this.count = 0;
+        this.iovArray.clear();
     }
 
     void release() {
-        // Release all packets
-        for (NativeDatagramPacket datagramPacket : packets) {
-            datagramPacket.release();
+        iovArray.release();
+    }
+
+    private final class MyMessageProcessor implements MessageProcessor {
+        private boolean connected;
+
+        @Override
+        public boolean processMessage(Object msg) {
+            if (msg instanceof DatagramPacket) {
+                DatagramPacket packet = (DatagramPacket) msg;
+                ByteBuf buf = packet.content();
+                return add0(buf, buf.readerIndex(), buf.readableBytes(), packet.recipient());
+            }
+            if (msg instanceof ByteBuf && connected) {
+                ByteBuf buf = (ByteBuf) msg;
+                return add0(buf, buf.readerIndex(), buf.readableBytes(), null);
+            }
+            return false;
         }
     }
 
@@ -98,46 +129,50 @@ final class NativeDatagramPacketArray implements ChannelOutboundBuffer.MessagePr
      * Used to pass needed data to JNI.
      */
     @SuppressWarnings("unused")
-    static final class NativeDatagramPacket {
-        // Each NativeDatagramPackets holds a IovArray which is used for gathering writes.
-        // This is ok as NativeDatagramPacketArray is always obtained from an EpollEventLoop
-        // field so the memory needed is quite small anyway.
-        private final IovArray array = new IovArray();
+    final class NativeDatagramPacket {
 
         // This is the actual struct iovec*
         private long memoryAddress;
         private int count;
 
-        private byte[] addr;
+        private final byte[] addr = new byte[16];
+
+        private int addrLen;
         private int scopeId;
         private int port;
 
-        private void release() {
-            array.release();
-        }
+        private void init(long memoryAddress, int count, InetSocketAddress recipient) {
+            this.memoryAddress = memoryAddress;
+            this.count = count;
 
-        /**
-         * Init this instance and return {@code true} if the init was successful.
-         */
-        private boolean init(ByteBuf buf, InetSocketAddress recipient) {
-            array.clear();
-            if (!array.add(buf)) {
-                return false;
+            if (recipient == null) {
+                this.scopeId = 0;
+                this.port = 0;
+                this.addrLen = 0;
+            } else {
+                InetAddress address = recipient.getAddress();
+                if (address instanceof Inet6Address) {
+                    System.arraycopy(address.getAddress(), 0, addr, 0, addr.length);
+                    scopeId = ((Inet6Address) address).getScopeId();
+                } else {
+                    copyIpv4MappedIpv6Address(address.getAddress(), addr);
+                    scopeId = 0;
+                }
+                addrLen = addr.length;
+                port = recipient.getPort();
             }
-            // always start from offset 0
-            memoryAddress = array.memoryAddress(0);
-            count = array.count();
-
-            InetAddress address = recipient.getAddress();
-            if (address instanceof Inet6Address) {
-                addr = address.getAddress();
-                scopeId = ((Inet6Address) address).getScopeId();
+        }
+
+        DatagramPacket newDatagramPacket(ByteBuf buffer, InetSocketAddress localAddress) throws UnknownHostException {
+            final InetAddress address;
+            if (addrLen == ipv4Bytes.length) {
+                System.arraycopy(addr, 0, ipv4Bytes, 0, addrLen);
+                address = InetAddress.getByAddress(ipv4Bytes);
             } else {
-                addr = ipv4MappedIpv6Address(address.getAddress());
-                scopeId = 0;
+                address = Inet6Address.getByAddress(null, addr, scopeId);
             }
-            port = recipient.getPort();
-            return true;
+            return new DatagramPacket(buffer.writerIndex(count),
+                    localAddress, new InetSocketAddress(address, port));
         }
     }
 }
diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/NativeStaticallyReferencedJniMethods.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/NativeStaticallyReferencedJniMethods.java
index 4f6f99c..9b20d68 100644
--- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/NativeStaticallyReferencedJniMethods.java
+++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/NativeStaticallyReferencedJniMethods.java
@@ -40,6 +40,7 @@ final class NativeStaticallyReferencedJniMethods {
     static native int iovMax();
     static native int uioMaxIov();
     static native boolean isSupportingSendmmsg();
+    static native boolean isSupportingRecvmmsg();
     static native boolean isSupportingTcpFastopen();
     static native String kernelVersion();
 }
diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/TcpMd5Util.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/TcpMd5Util.java
index 080c835..41509d7 100644
--- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/TcpMd5Util.java
+++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/TcpMd5Util.java
@@ -39,9 +39,7 @@ final class TcpMd5Util {
             if (e.getKey() == null) {
                 throw new IllegalArgumentException("newKeys contains an entry with null address: " + newKeys);
             }
-            if (key == null) {
-                throw new NullPointerException("newKeys[" + e.getKey() + ']');
-            }
+            ObjectUtil.checkNotNull(key, "newKeys[" + e.getKey() + ']');
             if (key.length == 0) {
                 throw new IllegalArgumentException("newKeys[" + e.getKey() + "] has an empty key.");
             }
diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramChannelTest.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramChannelTest.java
new file mode 100644
index 0000000..ce7646a
--- /dev/null
+++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramChannelTest.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2019 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.channel.epoll;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.socket.InternetProtocolFamily;
+import io.netty.channel.unix.Socket;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+
+import static io.netty.util.NetUtil.LOCALHOST;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+public class EpollDatagramChannelTest {
+
+    @Before
+    public void setUp() {
+        Epoll.ensureAvailability();
+    }
+
+    @Test
+    public void testNotActiveNoLocalRemoteAddress() throws IOException {
+        checkNotActiveNoLocalRemoteAddress(new EpollDatagramChannel());
+        checkNotActiveNoLocalRemoteAddress(new EpollDatagramChannel(InternetProtocolFamily.IPv4));
+        checkNotActiveNoLocalRemoteAddress(new EpollDatagramChannel(InternetProtocolFamily.IPv6));
+    }
+
+    @Test
+    public void testActiveHasLocalAddress() throws IOException {
+        Socket socket = Socket.newSocketDgram();
+        EpollDatagramChannel channel = new EpollDatagramChannel(socket.intValue());
+        InetSocketAddress localAddress = channel.localAddress();
+        assertTrue(channel.active);
+        assertNotNull(localAddress);
+        assertEquals(socket.localAddress(), localAddress);
+        channel.fd().close();
+    }
+
+    @Test
+    public void testLocalAddressBeforeAndAfterBind() {
+        EventLoopGroup group = new EpollEventLoopGroup(1);
+        try {
+            TestHandler handler = new TestHandler();
+            InetSocketAddress localAddressBeforeBind = new InetSocketAddress(LOCALHOST, 0);
+
+            Bootstrap bootstrap = new Bootstrap();
+            bootstrap.group(group)
+                    .channel(EpollDatagramChannel.class)
+                    .localAddress(localAddressBeforeBind)
+                    .handler(handler);
+
+            ChannelFuture future = bootstrap.bind().syncUninterruptibly();
+
+            assertNull(handler.localAddress);
+
+            SocketAddress localAddressAfterBind = future.channel().localAddress();
+            assertNotNull(localAddressAfterBind);
+            assertTrue(localAddressAfterBind instanceof InetSocketAddress);
+            assertTrue(((InetSocketAddress) localAddressAfterBind).getPort() != 0);
+
+            future.channel().close().syncUninterruptibly();
+        } finally {
+            group.shutdownGracefully();
+        }
+    }
+
+    private static void checkNotActiveNoLocalRemoteAddress(EpollDatagramChannel channel) throws IOException {
+        assertFalse(channel.active);
+        assertNull(channel.localAddress());
+        assertNull(channel.remoteAddress());
+        channel.fd().close();
+    }
+
+    private static final class TestHandler extends ChannelInboundHandlerAdapter {
+        private volatile SocketAddress localAddress;
+
+        @Override
+        public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
+            this.localAddress = ctx.channel().localAddress();
+            super.channelRegistered(ctx);
+        }
+    }
+}
diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramMulticastIPv6Test.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramMulticastIPv6Test.java
new file mode 100644
index 0000000..9497078
--- /dev/null
+++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramMulticastIPv6Test.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2019 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.channel.epoll;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.testsuite.transport.TestsuitePermutation;
+import io.netty.testsuite.transport.socket.DatagramMulticastIPv6Test;
+
+import java.util.List;
+
+public class EpollDatagramMulticastIPv6Test extends DatagramMulticastIPv6Test {
+
+    @Override
+    protected List<TestsuitePermutation.BootstrapComboFactory<Bootstrap, Bootstrap>> newFactories() {
+        return EpollSocketTestPermutation.INSTANCE.datagram(internetProtocolFamily());
+    }
+}
diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramMulticastTest.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramMulticastTest.java
new file mode 100644
index 0000000..e3ae768
--- /dev/null
+++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramMulticastTest.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2019 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.channel.epoll;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.testsuite.transport.TestsuitePermutation;
+import io.netty.testsuite.transport.socket.DatagramMulticastTest;
+
+import java.util.List;
+
+public class EpollDatagramMulticastTest extends DatagramMulticastTest {
+    @Override
+    protected List<TestsuitePermutation.BootstrapComboFactory<Bootstrap, Bootstrap>> newFactories() {
+        return EpollSocketTestPermutation.INSTANCE.datagram(internetProtocolFamily());
+    }
+}
diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramScatteringReadTest.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramScatteringReadTest.java
new file mode 100644
index 0000000..0c082bd
--- /dev/null
+++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramScatteringReadTest.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright 2019 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.channel.epoll;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.channel.AdaptiveRecvByteBufAllocator;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.channel.socket.DatagramPacket;
+import io.netty.testsuite.transport.TestsuitePermutation;
+import io.netty.testsuite.transport.socket.AbstractDatagramTest;
+import io.netty.util.internal.PlatformDependent;
+import org.junit.Assume;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class EpollDatagramScatteringReadTest extends AbstractDatagramTest  {
+
+    @BeforeClass
+    public static void assumeRecvmmsgSupported() {
+        Assume.assumeTrue(Native.IS_SUPPORTING_RECVMMSG);
+    }
+
+    @Override
+    protected List<TestsuitePermutation.BootstrapComboFactory<Bootstrap, Bootstrap>> newFactories() {
+        return EpollSocketTestPermutation.INSTANCE.epollOnlyDatagram(internetProtocolFamily());
+    }
+
+    @Test
+    public void testScatteringReadPartial() throws Throwable {
+        run();
+    }
+
+    public void testScatteringReadPartial(Bootstrap sb, Bootstrap cb) throws Throwable {
+        testScatteringRead(sb, cb, false, true);
+    }
+
+    @Test
+    public void testScatteringRead() throws Throwable {
+        run();
+    }
+
+    public void testScatteringRead(Bootstrap sb, Bootstrap cb) throws Throwable {
+        testScatteringRead(sb, cb, false, false);
+    }
+
+    @Test
+    public void testScatteringReadConnectedPartial() throws Throwable {
+        run();
+    }
+
+    public void testScatteringReadConnectedPartial(Bootstrap sb, Bootstrap cb) throws Throwable {
+        testScatteringRead(sb, cb, true, true);
+    }
+
+    @Test
+    public void testScatteringConnectedRead() throws Throwable {
+        run();
+    }
+
+    public void testScatteringConnectedRead(Bootstrap sb, Bootstrap cb) throws Throwable {
+        testScatteringRead(sb, cb, true, false);
+    }
+
+    private void testScatteringRead(Bootstrap sb, Bootstrap cb, boolean connected, boolean partial) throws Throwable {
+        int packetSize = 512;
+        int numPackets = 4;
+
+        sb.option(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(
+                packetSize, packetSize * (partial ? numPackets / 2 : numPackets), 64 * 1024));
+        sb.option(EpollChannelOption.MAX_DATAGRAM_PAYLOAD_SIZE, packetSize);
+
+        Channel sc = null;
+        Channel cc = null;
+
+        try {
+            cb.handler(new SimpleChannelInboundHandler<Object>() {
+                @Override
+                public void channelRead0(ChannelHandlerContext ctx, Object msgs) throws Exception {
+                    // Nothing will be sent.
+                }
+            });
+            cc = cb.bind(newSocketAddress()).sync().channel();
+            final SocketAddress ccAddress = cc.localAddress();
+
+            final AtomicReference<Throwable> errorRef = new AtomicReference<Throwable>();
+            final byte[] bytes = new byte[packetSize];
+            PlatformDependent.threadLocalRandom().nextBytes(bytes);
+
+            final CountDownLatch latch = new CountDownLatch(numPackets);
+            sb.handler(new SimpleChannelInboundHandler<DatagramPacket>() {
+                private int counter;
+                @Override
+                public void channelReadComplete(ChannelHandlerContext ctx) {
+                    assertTrue(counter > 1);
+                    counter = 0;
+                    ctx.read();
+                }
+
+                @Override
+                protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket msg) {
+                    assertEquals(ccAddress, msg.sender());
+
+                    assertEquals(bytes.length, msg.content().readableBytes());
+                    byte[] receivedBytes = new byte[bytes.length];
+                    msg.content().readBytes(receivedBytes);
+                    assertArrayEquals(bytes, receivedBytes);
+
+                    counter++;
+                    latch.countDown();
+                }
+
+                @Override
+                public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)  {
+                    errorRef.compareAndSet(null, cause);
+                }
+            });
+
+            sb.option(ChannelOption.AUTO_READ, false);
+            sc = sb.bind(newSocketAddress()).sync().channel();
+
+            if (connected) {
+                sc.connect(cc.localAddress()).syncUninterruptibly();
+            }
+
+            InetSocketAddress addr = (InetSocketAddress) sc.localAddress();
+
+            List<ChannelFuture> futures = new ArrayList<ChannelFuture>(numPackets);
+            for (int i = 0; i < numPackets; i++) {
+                futures.add(cc.write(new DatagramPacket(cc.alloc().directBuffer().writeBytes(bytes), addr)));
+            }
+
+            cc.flush();
+
+            for (ChannelFuture f: futures) {
+                f.sync();
+            }
+
+            // Enable autoread now which also triggers a read, this should cause scattering reads (recvmmsg) to happen.
+            sc.config().setAutoRead(true);
+
+            if (!latch.await(10, TimeUnit.SECONDS)) {
+                Throwable error = errorRef.get();
+                if (error != null) {
+                    throw error;
+                }
+                fail("Timeout while waiting for packets");
+            }
+        } finally {
+            if (cc != null) {
+                cc.close().syncUninterruptibly();
+            }
+            if (sc != null) {
+                sc.close().syncUninterruptibly();
+            }
+        }
+    }
+
+    @Test
+    public void testScatteringReadWithSmallBuffer() throws Throwable {
+        run();
+    }
+
+    public void testScatteringReadWithSmallBuffer(Bootstrap sb, Bootstrap cb) throws Throwable {
+        testScatteringReadWithSmallBuffer0(sb, cb, false);
+    }
+
+    @Test
+    public void testScatteringConnectedReadWithSmallBuffer() throws Throwable {
+        run();
+    }
+
+    public void testScatteringConnectedReadWithSmallBuffer(Bootstrap sb, Bootstrap cb) throws Throwable {
+        testScatteringReadWithSmallBuffer0(sb, cb, true);
+    }
+
+    private void testScatteringReadWithSmallBuffer0(Bootstrap sb, Bootstrap cb, boolean connected) throws Throwable {
+        int packetSize = 16;
+
+        sb.option(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(1400, 1400, 64 * 1024));
+        sb.option(EpollChannelOption.MAX_DATAGRAM_PAYLOAD_SIZE, 1400);
+
+        Channel sc = null;
+        Channel cc = null;
+
+        try {
+            cb.handler(new SimpleChannelInboundHandler<Object>() {
+                @Override
+                public void channelRead0(ChannelHandlerContext ctx, Object msgs) {
+                    // Nothing will be sent.
+                }
+            });
+            cc = cb.bind(newSocketAddress()).sync().channel();
+            final SocketAddress ccAddress = cc.localAddress();
+
+            final AtomicReference<Throwable> errorRef = new AtomicReference<Throwable>();
+            final byte[] bytes = new byte[packetSize];
+            PlatformDependent.threadLocalRandom().nextBytes(bytes);
+
+            final CountDownLatch latch = new CountDownLatch(1);
+            sb.handler(new SimpleChannelInboundHandler<DatagramPacket>() {
+
+                @Override
+                protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket msg) {
+                    assertEquals(ccAddress, msg.sender());
+
+                    assertEquals(bytes.length, msg.content().readableBytes());
+                    byte[] receivedBytes = new byte[bytes.length];
+                    msg.content().readBytes(receivedBytes);
+                    assertArrayEquals(bytes, receivedBytes);
+
+                    latch.countDown();
+                }
+
+                @Override
+                public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)  {
+                    errorRef.compareAndSet(null, cause);
+                }
+            });
+
+            sc = sb.bind(newSocketAddress()).sync().channel();
+
+            if (connected) {
+                sc.connect(cc.localAddress()).syncUninterruptibly();
+            }
+
+            InetSocketAddress addr = (InetSocketAddress) sc.localAddress();
+
+            cc.writeAndFlush(new DatagramPacket(cc.alloc().directBuffer().writeBytes(bytes), addr)).sync();
+
+            if (!latch.await(10, TimeUnit.SECONDS)) {
+                Throwable error = errorRef.get();
+                if (error != null) {
+                    throw error;
+                }
+                fail("Timeout while waiting for packets");
+            }
+        } finally {
+            if (cc != null) {
+                cc.close().syncUninterruptibly();
+            }
+            if (sc != null) {
+                sc.close().syncUninterruptibly();
+            }
+        }
+    }
+}
diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramUnicastIPv6MappedTest.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramUnicastIPv6MappedTest.java
new file mode 100644
index 0000000..fd2a8f3
--- /dev/null
+++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramUnicastIPv6MappedTest.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2019 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.channel.epoll;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.testsuite.transport.TestsuitePermutation.BootstrapComboFactory;
+import io.netty.testsuite.transport.socket.DatagramUnicastIPv6MappedTest;
+
+import java.util.List;
+
+public class EpollDatagramUnicastIPv6MappedTest extends DatagramUnicastIPv6MappedTest {
+    @Override
+    protected List<BootstrapComboFactory<Bootstrap, Bootstrap>> newFactories() {
+        return EpollSocketTestPermutation.INSTANCE.datagram(internetProtocolFamily());
+    }
+}
diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramUnicastIPv6Test.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramUnicastIPv6Test.java
new file mode 100644
index 0000000..29e0a51
--- /dev/null
+++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramUnicastIPv6Test.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2019 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.channel.epoll;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.testsuite.transport.TestsuitePermutation;
+import io.netty.testsuite.transport.socket.DatagramUnicastIPv6Test;
+
+import java.util.List;
+
+public class EpollDatagramUnicastIPv6Test extends DatagramUnicastIPv6Test {
+    @Override
+    protected List<TestsuitePermutation.BootstrapComboFactory<Bootstrap, Bootstrap>> newFactories() {
+        return EpollSocketTestPermutation.INSTANCE.datagram(internetProtocolFamily());
+    }
+}
diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramUnicastTest.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramUnicastTest.java
index a8ed58e..9322a7d 100644
--- a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramUnicastTest.java
+++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDatagramUnicastTest.java
@@ -16,6 +16,7 @@
 package io.netty.channel.epoll;
 
 import io.netty.bootstrap.Bootstrap;
+import io.netty.channel.socket.InternetProtocolFamily;
 import io.netty.testsuite.transport.TestsuitePermutation;
 import io.netty.testsuite.transport.socket.DatagramUnicastTest;
 
@@ -24,7 +25,7 @@ import java.util.List;
 public class EpollDatagramUnicastTest extends DatagramUnicastTest {
     @Override
     protected List<TestsuitePermutation.BootstrapComboFactory<Bootstrap, Bootstrap>> newFactories() {
-        return EpollSocketTestPermutation.INSTANCE.datagram();
+        return EpollSocketTestPermutation.INSTANCE.datagram(InternetProtocolFamily.IPv4);
     }
 
     public void testSimpleSendWithConnect(Bootstrap sb, Bootstrap cb) throws Throwable {
diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDomainSocketReuseFdTest.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDomainSocketReuseFdTest.java
new file mode 100644
index 0000000..487ea64
--- /dev/null
+++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDomainSocketReuseFdTest.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2019 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.channel.epoll;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.testsuite.transport.TestsuitePermutation;
+import io.netty.testsuite.transport.socket.AbstractSocketReuseFdTest;
+
+import java.net.SocketAddress;
+import java.util.List;
+
+public class EpollDomainSocketReuseFdTest extends AbstractSocketReuseFdTest {
+    @Override
+    protected SocketAddress newSocketAddress() {
+        return EpollSocketTestPermutation.newSocketAddress();
+    }
+
+    @Override
+    protected List<TestsuitePermutation.BootstrapComboFactory<ServerBootstrap, Bootstrap>> newFactories() {
+        return EpollSocketTestPermutation.INSTANCE.domainSocket();
+    }
+}
diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDomainSocketShutdownOutputByPeerTest.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDomainSocketShutdownOutputByPeerTest.java
new file mode 100644
index 0000000..7d0d483
--- /dev/null
+++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDomainSocketShutdownOutputByPeerTest.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2019 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.channel.epoll;
+
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.unix.Buffer;
+import io.netty.testsuite.transport.TestsuitePermutation.BootstrapFactory;
+import io.netty.testsuite.transport.socket.AbstractSocketShutdownOutputByPeerTest;
+
+import java.io.IOException;
+import java.net.SocketAddress;
+import java.nio.ByteBuffer;
+import java.util.List;
+
+public class EpollDomainSocketShutdownOutputByPeerTest extends AbstractSocketShutdownOutputByPeerTest<LinuxSocket> {
+
+    @Override
+    protected List<BootstrapFactory<ServerBootstrap>> newFactories() {
+        return EpollSocketTestPermutation.INSTANCE.serverDomainSocket();
+    }
+
+    @Override
+    protected SocketAddress newSocketAddress() {
+        return EpollSocketTestPermutation.newSocketAddress();
+    }
+
+    @Override
+    protected void shutdownOutput(LinuxSocket s) throws IOException {
+        s.shutdown(false, true);
+    }
+
+    @Override
+    protected void connect(LinuxSocket s, SocketAddress address) throws IOException {
+        s.connect(address);
+    }
+
+    @Override
+    protected void close(LinuxSocket s) throws IOException {
+        s.close();
+    }
+
+    @Override
+    protected void write(LinuxSocket s, int data) throws IOException {
+        final ByteBuffer buf = Buffer.allocateDirectWithNativeOrder(4);
+        buf.putInt(data);
+        buf.flip();
+        s.write(buf, buf.position(), buf.limit());
+        Buffer.free(buf);
+    }
+
+    @Override
+    protected LinuxSocket newSocket() {
+        return LinuxSocket.newSocketDomain();
+    }
+}
diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDomainSocketSslClientRenegotiateTest.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDomainSocketSslClientRenegotiateTest.java
new file mode 100644
index 0000000..3f493d0
--- /dev/null
+++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDomainSocketSslClientRenegotiateTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2019 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.channel.epoll;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.handler.ssl.SslContext;
+import io.netty.testsuite.transport.TestsuitePermutation;
+import io.netty.testsuite.transport.socket.SocketSslClientRenegotiateTest;
+
+import java.net.SocketAddress;
+import java.util.List;
+
+public class EpollDomainSocketSslClientRenegotiateTest extends SocketSslClientRenegotiateTest {
+
+    public EpollDomainSocketSslClientRenegotiateTest(SslContext serverCtx, SslContext clientCtx, boolean delegate) {
+        super(serverCtx, clientCtx, delegate);
+    }
+
+    @Override
+    protected List<TestsuitePermutation.BootstrapComboFactory<ServerBootstrap, Bootstrap>> newFactories() {
+        return EpollSocketTestPermutation.INSTANCE.domainSocket();
+    }
+
+    @Override
+    protected SocketAddress newSocketAddress() {
+        return EpollSocketTestPermutation.newSocketAddress();
+    }
+}
diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDomainSocketSslGreetingTest.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDomainSocketSslGreetingTest.java
index a1ed0c5..4683a70 100644
--- a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDomainSocketSslGreetingTest.java
+++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollDomainSocketSslGreetingTest.java
@@ -26,8 +26,8 @@ import java.util.List;
 
 public class EpollDomainSocketSslGreetingTest extends SocketSslGreetingTest {
 
-    public EpollDomainSocketSslGreetingTest(SslContext serverCtx, SslContext clientCtx) {
-        super(serverCtx, clientCtx);
+    public EpollDomainSocketSslGreetingTest(SslContext serverCtx, SslContext clientCtx, boolean delegate) {
+        super(serverCtx, clientCtx, delegate);
     }
 
     @Override
diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollEventLoopTest.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollEventLoopTest.java
index 4e51114..c6bc431 100644
--- a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollEventLoopTest.java
+++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollEventLoopTest.java
@@ -18,20 +18,42 @@ package io.netty.channel.epoll;
 import io.netty.channel.DefaultSelectStrategyFactory;
 import io.netty.channel.EventLoop;
 import io.netty.channel.EventLoopGroup;
+import io.netty.channel.ServerChannel;
+import io.netty.channel.socket.ServerSocketChannel;
+import io.netty.channel.unix.FileDescriptor;
+import io.netty.testsuite.transport.AbstractSingleThreadEventLoopTest;
 import io.netty.util.concurrent.DefaultThreadFactory;
 import io.netty.util.concurrent.Future;
 import io.netty.util.concurrent.RejectedExecutionHandlers;
 import io.netty.util.concurrent.ThreadPerTaskExecutor;
 import org.junit.Test;
 
+import java.io.IOException;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
-public class EpollEventLoopTest {
+public class EpollEventLoopTest extends AbstractSingleThreadEventLoopTest {
+
+    @Override
+    protected EventLoopGroup newEventLoopGroup() {
+        return new EpollEventLoopGroup();
+    }
+
+    @Override
+    protected ServerSocketChannel newChannel() {
+        return new EpollServerSocketChannel();
+    }
+
+    @Override
+    protected Class<? extends ServerChannel> serverChannelClass() {
+        return EpollServerSocketChannel.class;
+    }
 
     @Test
     public void testScheduleBigDelayNotOverflow() {
@@ -39,7 +61,7 @@ public class EpollEventLoopTest {
 
         final EventLoopGroup group = new EpollEventLoop(null,
                 new ThreadPerTaskExecutor(new DefaultThreadFactory(getClass())), 0,
-                DefaultSelectStrategyFactory.INSTANCE.newSelectStrategy(), RejectedExecutionHandlers.reject()) {
+                DefaultSelectStrategyFactory.INSTANCE.newSelectStrategy(), RejectedExecutionHandlers.reject(), null) {
             @Override
             void handleLoopException(Throwable t) {
                 capture.set(t);
@@ -63,4 +85,54 @@ public class EpollEventLoopTest {
             group.shutdownGracefully();
         }
     }
+
+    @Test
+    public void testEventFDETSemantics() throws Throwable {
+        final FileDescriptor epoll = Native.newEpollCreate();
+        final FileDescriptor eventFd = Native.newEventFd();
+        final FileDescriptor timerFd = Native.newTimerFd();
+        final EpollEventArray array = new EpollEventArray(1024);
+        try {
+            Native.epollCtlAdd(epoll.intValue(), eventFd.intValue(), Native.EPOLLIN | Native.EPOLLET);
+            final AtomicReference<Throwable> causeRef = new AtomicReference<Throwable>();
+            final AtomicInteger integer = new AtomicInteger();
+            final Thread t = new Thread(new Runnable() {
+                @Override
+                public void run() {
+                    try {
+                        for (int i = 0; i < 2; i++) {
+                            int ready = Native.epollWait(epoll, array, timerFd, -1, -1);
+                            assertEquals(1, ready);
+                            assertEquals(eventFd.intValue(), array.fd(0));
+                            integer.incrementAndGet();
+                        }
+                    } catch (IOException e) {
+                        causeRef.set(e);
+                    }
+                }
+            });
+            t.start();
+            Native.eventFdWrite(eventFd.intValue(), 1);
+
+            // Spin until we was the wakeup.
+            while (integer.get() != 1) {
+                Thread.sleep(10);
+            }
+            // Sleep for a short moment to ensure there is not other wakeup.
+            Thread.sleep(1000);
+            assertEquals(1, integer.get());
+            Native.eventFdWrite(eventFd.intValue(), 1);
+            t.join();
+            Throwable cause = causeRef.get();
+            if (cause != null) {
+                throw cause;
+            }
+            assertEquals(2, integer.get());
+        } finally {
+            array.free();
+            epoll.close();
+            eventFd.close();
+            timerFd.close();
+        }
+    }
 }
diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollReuseAddrTest.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollReuseAddrTest.java
index c6f47cd..1abad07 100644
--- a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollReuseAddrTest.java
+++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollReuseAddrTest.java
@@ -23,6 +23,8 @@ import io.netty.channel.ChannelHandler;
 import io.netty.channel.ChannelHandlerAdapter;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.handler.logging.LogLevel;
+import io.netty.handler.logging.LoggingHandler;
 import io.netty.util.NetUtil;
 import io.netty.util.ReferenceCountUtil;
 import io.netty.util.ResourceLeakDetector;
@@ -78,7 +80,7 @@ public class EpollReuseAddrTest {
     }
 
     private static void testMultipleBindDatagramChannelWithoutReusePortFails0(AbstractBootstrap<?, ?> bootstrap) {
-        bootstrap.handler(new DummyHandler());
+        bootstrap.handler(new LoggingHandler(LogLevel.ERROR));
         ChannelFuture future = bootstrap.bind().syncUninterruptibly();
         try {
             bootstrap.bind(future.channel().localAddress()).syncUninterruptibly();
diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketSslClientRenegotiateTest.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketSslClientRenegotiateTest.java
new file mode 100644
index 0000000..3f69196
--- /dev/null
+++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketSslClientRenegotiateTest.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2019 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.channel.epoll;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.handler.ssl.SslContext;
+import io.netty.testsuite.transport.TestsuitePermutation;
+import io.netty.testsuite.transport.socket.SocketSslClientRenegotiateTest;
+
+import java.util.List;
+
+public class EpollSocketSslClientRenegotiateTest extends SocketSslClientRenegotiateTest {
+
+    public EpollSocketSslClientRenegotiateTest(SslContext serverCtx, SslContext clientCtx, boolean delegate) {
+        super(serverCtx, clientCtx, delegate);
+    }
+
+    @Override
+    protected List<TestsuitePermutation.BootstrapComboFactory<ServerBootstrap, Bootstrap>> newFactories() {
+        return EpollSocketTestPermutation.INSTANCE.socket();
+    }
+}
diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketSslGreetingTest.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketSslGreetingTest.java
index 21d86b4..34bf98a 100644
--- a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketSslGreetingTest.java
+++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketSslGreetingTest.java
@@ -25,8 +25,8 @@ import java.util.List;
 
 public class EpollSocketSslGreetingTest extends SocketSslGreetingTest {
 
-    public EpollSocketSslGreetingTest(SslContext serverCtx, SslContext clientCtx) {
-        super(serverCtx, clientCtx);
+    public EpollSocketSslGreetingTest(SslContext serverCtx, SslContext clientCtx, boolean delegate) {
+        super(serverCtx, clientCtx, delegate);
     }
 
     @Override
diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketSslSessionReuseTest.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketSslSessionReuseTest.java
new file mode 100644
index 0000000..2b78224
--- /dev/null
+++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketSslSessionReuseTest.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2019 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.channel.epoll;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.handler.ssl.SslContext;
+import io.netty.testsuite.transport.TestsuitePermutation;
+import io.netty.testsuite.transport.socket.SocketSslSessionReuseTest;
+
+import java.util.List;
+
+public class EpollSocketSslSessionReuseTest extends SocketSslSessionReuseTest {
+
+    public EpollSocketSslSessionReuseTest(SslContext serverCtx, SslContext clientCtx) {
+        super(serverCtx, clientCtx);
+    }
+
+    @Override
+    protected List<TestsuitePermutation.BootstrapComboFactory<ServerBootstrap, Bootstrap>> newFactories() {
+        return EpollSocketTestPermutation.INSTANCE.socket();
+    }
+}
diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketTestPermutation.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketTestPermutation.java
index 225c1d2..e5c5ed4 100644
--- a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketTestPermutation.java
+++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketTestPermutation.java
@@ -36,7 +36,6 @@ import io.netty.util.internal.logging.InternalLoggerFactory;
 import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileReader;
-import java.io.IOException;
 import java.security.AccessController;
 import java.security.PrivilegedAction;
 import java.util.ArrayList;
@@ -119,7 +118,8 @@ class EpollSocketTestPermutation extends SocketTestPermutation {
     }
 
     @Override
-    public List<TestsuitePermutation.BootstrapComboFactory<Bootstrap, Bootstrap>> datagram() {
+    public List<TestsuitePermutation.BootstrapComboFactory<Bootstrap, Bootstrap>> datagram(
+            final InternetProtocolFamily family) {
         // Make the list of Bootstrap factories.
         @SuppressWarnings("unchecked")
         List<BootstrapFactory<Bootstrap>> bfs = Arrays.asList(
@@ -129,7 +129,7 @@ class EpollSocketTestPermutation extends SocketTestPermutation {
                         return new Bootstrap().group(nioWorkerGroup).channelFactory(new ChannelFactory<Channel>() {
                             @Override
                             public Channel newChannel() {
-                                return new NioDatagramChannel(InternetProtocolFamily.IPv4);
+                                return new NioDatagramChannel(family);
                             }
 
                             @Override
@@ -142,13 +142,48 @@ class EpollSocketTestPermutation extends SocketTestPermutation {
                 new BootstrapFactory<Bootstrap>() {
                     @Override
                     public Bootstrap newInstance() {
-                        return new Bootstrap().group(EPOLL_WORKER_GROUP).channel(EpollDatagramChannel.class);
+                        return new Bootstrap().group(EPOLL_WORKER_GROUP).channelFactory(new ChannelFactory<Channel>() {
+                            @Override
+                            public Channel newChannel() {
+                                return new EpollDatagramChannel(family);
+                            }
+
+                            @Override
+                            public String toString() {
+                                return InternetProtocolFamily.class.getSimpleName() + ".class";
+                            }
+                        });
                     }
                 }
         );
         return combo(bfs, bfs);
     }
 
+    List<TestsuitePermutation.BootstrapComboFactory<Bootstrap, Bootstrap>> epollOnlyDatagram(
+            final InternetProtocolFamily family) {
+        return combo(Collections.singletonList(datagramBootstrapFactory(family)),
+                Collections.singletonList(datagramBootstrapFactory(family)));
+    }
+
+    private BootstrapFactory<Bootstrap> datagramBootstrapFactory(final InternetProtocolFamily family) {
+        return new BootstrapFactory<Bootstrap>() {
+            @Override
+            public Bootstrap newInstance() {
+                return new Bootstrap().group(EPOLL_WORKER_GROUP).channelFactory(new ChannelFactory<Channel>() {
+                    @Override
+                    public Channel newChannel() {
+                        return new EpollDatagramChannel(family);
+                    }
+
+                    @Override
+                    public String toString() {
+                        return InternetProtocolFamily.class.getSimpleName() + ".class";
+                    }
+                });
+            }
+        };
+    }
+
     public List<TestsuitePermutation.BootstrapComboFactory<ServerBootstrap, Bootstrap>> domainSocket() {
 
         List<TestsuitePermutation.BootstrapComboFactory<ServerBootstrap, Bootstrap>> list =
diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSpliceTest.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSpliceTest.java
index c53ff1e..a8d9d72 100644
--- a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSpliceTest.java
+++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSpliceTest.java
@@ -27,7 +27,6 @@ import io.netty.channel.ChannelInboundHandlerAdapter;
 import io.netty.channel.EventLoopGroup;
 import io.netty.channel.SimpleChannelInboundHandler;
 import io.netty.channel.unix.FileDescriptor;
-import io.netty.testsuite.util.TestUtils;
 import io.netty.util.NetUtil;
 import org.junit.Assert;
 import org.junit.Test;
@@ -190,7 +189,7 @@ public class EpollSpliceTest {
         }
     }
 
-    @Test
+    @Test(timeout = 10000)
     public void spliceToFile() throws Throwable {
         EventLoopGroup group = new EpollEventLoopGroup(1);
         File file = File.createTempFile("netty-splice", null);
@@ -216,7 +215,7 @@ public class EpollSpliceTest {
             i += length;
         }
 
-        while (sh.future == null || !sh.future.isDone()) {
+        while (sh.future2 == null || !sh.future2.isDone() || !sh.future.isDone()) {
             if (sh.exception.get() != null) {
                 break;
             }
@@ -292,22 +291,22 @@ public class EpollSpliceTest {
     private static class SpliceHandler extends ChannelInboundHandlerAdapter {
         private final File file;
 
-        volatile Channel channel;
         volatile ChannelFuture future;
+        volatile ChannelFuture future2;
         final AtomicReference<Throwable> exception = new AtomicReference<Throwable>();
 
-        public SpliceHandler(File file) {
+        SpliceHandler(File file) {
             this.file = file;
         }
 
         @Override
-        public void channelActive(ChannelHandlerContext ctx)
-                throws Exception {
-            channel = ctx.channel();
+        public void channelActive(ChannelHandlerContext ctx) throws Exception {
             final EpollSocketChannel ch = (EpollSocketChannel) ctx.channel();
             final FileDescriptor fd = FileDescriptor.from(file);
 
-            future = ch.spliceTo(fd, 0, data.length);
+            // splice two halves separately to test starting offset
+            future = ch.spliceTo(fd, 0, data.length / 2);
+            future2 = ch.spliceTo(fd, data.length / 2, data.length / 2);
         }
 
         @Override
diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollTest.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollTest.java
index 5a9cb19..f66a55e 100644
--- a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollTest.java
+++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2014 The Netty Project
+ * Copyright 2019 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
@@ -47,7 +47,7 @@ public class EpollTest {
                 @Override
                 public void run() {
                     try {
-                        assertEquals(1, Native.epollWait(epoll, eventArray, timerFd, -1, -1));
+                        assertEquals(1, Native.epollWait(epoll, eventArray, false));
                         // This should have been woken up because of eventfd_write.
                         assertEquals(eventfd.intValue(), eventArray.fd(0));
                     } catch (Throwable cause) {
diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollWriteBeforeRegisteredTest.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollWriteBeforeRegisteredTest.java
new file mode 100644
index 0000000..b1943fd
--- /dev/null
+++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollWriteBeforeRegisteredTest.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2019 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.channel.epoll;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.testsuite.transport.TestsuitePermutation;
+import io.netty.testsuite.transport.socket.WriteBeforeRegisteredTest;
+
+import java.util.List;
+
+public class EpollWriteBeforeRegisteredTest extends WriteBeforeRegisteredTest {
+
+    @Override
+    protected List<TestsuitePermutation.BootstrapFactory<Bootstrap>> newFactories() {
+        return EpollSocketTestPermutation.INSTANCE.clientSocket();
+    }
+}
diff --git a/transport-native-kqueue/pom.xml b/transport-native-kqueue/pom.xml
index 3efef19..af537c1 100644
--- a/transport-native-kqueue/pom.xml
+++ b/transport-native-kqueue/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>io.netty</groupId>
     <artifactId>netty-parent</artifactId>
-    <version>4.1.33.Final</version>
+    <version>4.1.48.Final</version>
   </parent>
   <artifactId>netty-transport-native-kqueue</artifactId>
 
@@ -71,7 +71,7 @@
                 <id>build-native-lib</id>
                 <configuration>
                   <name>netty_transport_native_kqueue_${os.detected.arch}</name>
-                  <nativeSourceDirectory>${project.basedir}/src/main/c</nativeSourceDirectory>
+                  <nativeSourceDirectory>${nativeSourceDirectory}</nativeSourceDirectory>
                   <libDirectory>${project.build.outputDirectory}</libDirectory>
                   <!-- We use Maven's artifact classifier instead.
                        This hack will make the hawtjni plugin to put the native library
@@ -84,13 +84,13 @@
                          explicitly set the target platform. Otherwise we may get fatal link errors due to weakly linked
                          methods which are not expected to be present on MacOS (e.g. accept4). -->
                     <arg>MACOSX_DEPLOYMENT_TARGET=10.6</arg>
+                    <configureArg>--libdir=${project.build.directory}/native-build/target/lib</configureArg>
                   </configureArgs>
                 </configuration>
                 <goals>
                   <goal>generate</goal>
                   <goal>build</goal>
                 </goals>
-                <phase>compile</phase>
               </execution>
             </executions>
           </plugin>
@@ -181,7 +181,7 @@
                 <id>build-native-lib</id>
                 <configuration>
                   <name>netty_transport_native_kqueue_${os.detected.arch}</name>
-                  <nativeSourceDirectory>${project.basedir}/src/main/c</nativeSourceDirectory>
+                  <nativeSourceDirectory>${nativeSourceDirectory}</nativeSourceDirectory>
                   <libDirectory>${project.build.outputDirectory}</libDirectory>
                   <!-- We use Maven's artifact classifier instead.
                        This hack will make the hawtjni plugin to put the native library
@@ -198,7 +198,6 @@
                   <goal>generate</goal>
                   <goal>build</goal>
                 </goals>
-                <phase>compile</phase>
               </execution>
             </executions>
           </plugin>
@@ -288,7 +287,7 @@
                 <id>build-native-lib</id>
                 <configuration>
                   <name>netty_transport_native_kqueue_${os.detected.arch}</name>
-                  <nativeSourceDirectory>${project.basedir}/src/main/c</nativeSourceDirectory>
+                  <nativeSourceDirectory>${nativeSourceDirectory}</nativeSourceDirectory>
                   <libDirectory>${project.build.outputDirectory}</libDirectory>
                   <!-- We use Maven's artifact classifier instead.
                        This hack will make the hawtjni plugin to put the native library
@@ -305,7 +304,6 @@
                   <goal>generate</goal>
                   <goal>build</goal>
                 </goals>
-                <phase>compile</phase>
               </execution>
             </executions>
           </plugin>
@@ -355,14 +353,15 @@
 
   <properties>
     <javaModuleName>io.netty.transport.kqueue</javaModuleName>
-    <!-- Needed by the native transport as we need the memoryAddress of the ByteBuffer -->
-    <argLine.java9.extras>--add-exports java.base/sun.security.x509=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED</argLine.java9.extras>
+    <!-- Needed as we use SelfSignedCertificate in our tests -->
+    <argLine.java9.extras>--add-exports java.base/sun.security.x509=ALL-UNNAMED</argLine.java9.extras>
     <unix.common.lib.name>netty-unix-common</unix.common.lib.name>
     <unix.common.lib.dir>${project.build.directory}/unix-common-lib</unix.common.lib.dir>
     <unix.common.lib.unpacked.dir>${unix.common.lib.dir}/META-INF/native/lib</unix.common.lib.unpacked.dir>
     <unix.common.include.unpacked.dir>${unix.common.lib.dir}/META-INF/native/include</unix.common.include.unpacked.dir>
     <jni.compiler.args.cflags>CFLAGS=-O3 -Werror -fno-omit-frame-pointer -Wunused-variable -fvisibility=hidden -I${unix.common.include.unpacked.dir}</jni.compiler.args.cflags>
     <jni.compiler.args.ldflags>LDFLAGS=-z now -L${unix.common.lib.unpacked.dir} -Wl,--whole-archive -l${unix.common.lib.name} -Wl,--no-whole-archive</jni.compiler.args.ldflags>
+    <nativeSourceDirectory>${project.basedir}/src/main/c</nativeSourceDirectory>
     <skipTests>true</skipTests>
   </properties>
 
@@ -409,6 +408,24 @@
 
   <build>
     <plugins>
+      <!-- Also include c files in source jar -->
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>build-helper-maven-plugin</artifactId>
+        <executions>
+          <execution>
+            <phase>generate-sources</phase>
+            <goals>
+              <goal>add-source</goal>
+            </goals>
+            <configuration>
+              <sources>
+                <source>${nativeSourceDirectory}</source>
+              </sources>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
       <plugin>
         <artifactId>maven-jar-plugin</artifactId>
         <executions>
diff --git a/transport-native-kqueue/src/main/c/netty_kqueue_bsdsocket.c b/transport-native-kqueue/src/main/c/netty_kqueue_bsdsocket.c
index e522896..53a3769 100644
--- a/transport-native-kqueue/src/main/c/netty_kqueue_bsdsocket.c
+++ b/transport-native-kqueue/src/main/c/netty_kqueue_bsdsocket.c
@@ -33,7 +33,7 @@
 #include "netty_unix_util.h"
 
 // Those are initialized in the init(...) method and cached for performance reasons
-static jclass stringCls = NULL;
+static jclass stringClass = NULL;
 static jclass peerCredentialsClass = NULL;
 static jfieldID fileChannelFieldId = NULL;
 static jfieldID transferredFieldId = NULL;
@@ -108,9 +108,20 @@ static jobjectArray netty_kqueue_bsdsocket_getAcceptFilter(JNIEnv* env, jclass c
         netty_unix_errors_throwChannelExceptionErrorNo(env, "getsockopt() failed: ", errno);
         return NULL;
     }
-    jobjectArray resultArray = (*env)->NewObjectArray(env, 2, stringCls, NULL);
-    (*env)->SetObjectArrayElement(env, resultArray, 0, (*env)->NewStringUTF(env, &af.af_name[0]));
-    (*env)->SetObjectArrayElement(env, resultArray, 1, (*env)->NewStringUTF(env, &af.af_arg[0]));
+    jobjectArray resultArray = (*env)->NewObjectArray(env, 2, stringClass, NULL);
+    if (resultArray == NULL) {
+        return NULL;
+    }
+    jstring name = (*env)->NewStringUTF(env, &af.af_name[0]);
+    if (name == NULL) {
+        return NULL;
+    }
+    jstring arg = (*env)->NewStringUTF(env, &af.af_arg[0]);
+    if (arg == NULL) {
+        return NULL;
+    }
+    (*env)->SetObjectArrayElement(env, resultArray, 0, name);
+    (*env)->SetObjectArrayElement(env, resultArray, 1, arg);
     return resultArray;
 #else // No know replacement on MacOS
     // Don't throw here because this is used when getting a list of all options.
@@ -151,16 +162,28 @@ static jobject netty_kqueue_bsdsocket_getPeerCredentials(JNIEnv *env, jclass cla
     }
     jintArray gids = NULL;
     if (credentials.cr_ngroups > 1) {
-        gids = (*env)->NewIntArray(env, credentials.cr_ngroups);
+        if ((gids = (*env)->NewIntArray(env, credentials.cr_ngroups)) == NULL) {
+            return NULL;
+        }
         (*env)->SetIntArrayRegion(env, gids, 0, credentials.cr_ngroups, (jint*) credentials.cr_groups);
     } else {
         // It has been observed on MacOS that cr_ngroups may not be set, but the cr_gid field is set.
-        gids = (*env)->NewIntArray(env, 1);
+        if ((gids = (*env)->NewIntArray(env, 1)) == NULL) {
+            return NULL;
+        }
         (*env)->SetIntArrayRegion(env, gids, 0, 1, (jint*) &credentials.cr_gid);
     }
 
-    // TODO: getting the PID may require reading/sending "ancillary data" via SCM_CREDENTIALS which is not desirable.
-    return (*env)->NewObject(env, peerCredentialsClass, peerCredentialsMethodId, 0, credentials.cr_uid, gids);
+    pid_t pid = 0;
+#ifdef LOCAL_PEERPID
+    socklen_t len = sizeof(pid);
+    // Getting the LOCAL_PEERPID is expected to return error in some cases (e.g. server socket FDs) - just return 0.
+    if (netty_unix_socket_getOption0(fd, SOCK_STREAM, LOCAL_PEERPID, &pid, len) < 0) {
+        pid = 0;
+    }
+#endif
+
+    return (*env)->NewObject(env, peerCredentialsClass, peerCredentialsMethodId, pid, credentials.cr_uid, gids);
 }
 // JNI Registered Methods End
 
@@ -181,121 +204,87 @@ static jint dynamicMethodsTableSize() {
 }
 
 static JNINativeMethod* createDynamicMethodsTable(const char* packagePrefix) {
-    JNINativeMethod* dynamicMethods = malloc(sizeof(JNINativeMethod) * dynamicMethodsTableSize());
+    char* dynamicTypeName = NULL;
+    size_t size = sizeof(JNINativeMethod) * dynamicMethodsTableSize();
+    JNINativeMethod* dynamicMethods = malloc(size);
+    if (dynamicMethods == NULL) {
+        return NULL;
+    }
+    memset(dynamicMethods, 0, size);
     memcpy(dynamicMethods, fixed_method_table, sizeof(fixed_method_table));
-    char* dynamicTypeName = netty_unix_util_prepend(packagePrefix, "io/netty/channel/DefaultFileRegion;JJJ)J");
+
     JNINativeMethod* dynamicMethod = &dynamicMethods[fixed_method_table_size];
+    NETTY_PREPEND(packagePrefix, "io/netty/channel/DefaultFileRegion;JJJ)J", dynamicTypeName, error);
+    NETTY_PREPEND("(IL", dynamicTypeName,  dynamicMethod->signature, error);
     dynamicMethod->name = "sendFile";
-    dynamicMethod->signature = netty_unix_util_prepend("(IL", dynamicTypeName);
     dynamicMethod->fnPtr = (void *) netty_kqueue_bsdsocket_sendFile;
-    free(dynamicTypeName);
+    netty_unix_util_free_dynamic_name(&dynamicTypeName);
 
     ++dynamicMethod;
-    dynamicTypeName = netty_unix_util_prepend(packagePrefix, "io/netty/channel/unix/PeerCredentials;");
+    NETTY_PREPEND(packagePrefix, "io/netty/channel/unix/PeerCredentials;", dynamicTypeName, error);
+    NETTY_PREPEND("(I)L", dynamicTypeName,  dynamicMethod->signature, error);
     dynamicMethod->name = "getPeerCredentials";
-    dynamicMethod->signature = netty_unix_util_prepend("(I)L", dynamicTypeName);
     dynamicMethod->fnPtr = (void *) netty_kqueue_bsdsocket_getPeerCredentials;
-    free(dynamicTypeName);
+    netty_unix_util_free_dynamic_name(&dynamicTypeName);
 
     return dynamicMethods;
+error:
+    free(dynamicTypeName);
+    netty_unix_util_free_dynamic_methods_table(dynamicMethods, fixed_method_table_size, dynamicMethodsTableSize());
+    return NULL;
 }
 
-static void freeDynamicMethodsTable(JNINativeMethod* dynamicMethods) {
-    jint fullMethodTableSize = dynamicMethodsTableSize();
-    jint i = fixed_method_table_size;
-    for (; i < fullMethodTableSize; ++i) {
-        free(dynamicMethods[i].signature);
-    }
-    free(dynamicMethods);
-}
 // JNI Method Registration Table End
 
 jint netty_kqueue_bsdsocket_JNI_OnLoad(JNIEnv* env, const char* packagePrefix) {
+    int ret = JNI_ERR;
+    char* nettyClassName = NULL;
+    jclass fileRegionCls = NULL;
+    jclass fileChannelCls = NULL;
+    jclass fileDescriptorCls = NULL;
     // Register the methods which are not referenced by static member variables
     JNINativeMethod* dynamicMethods = createDynamicMethodsTable(packagePrefix);
+    if (dynamicMethods == NULL) {
+        goto done;
+    }
     if (netty_unix_util_register_natives(env,
             packagePrefix,
             "io/netty/channel/kqueue/BsdSocket",
             dynamicMethods,
             dynamicMethodsTableSize()) != 0) {
-        freeDynamicMethodsTable(dynamicMethods);
-        return JNI_ERR;
+        goto done;
     }
-    freeDynamicMethodsTable(dynamicMethods);
-    dynamicMethods = NULL;
 
     // Initialize this module
-    char* nettyClassName = netty_unix_util_prepend(packagePrefix, "io/netty/channel/DefaultFileRegion");
-    jclass fileRegionCls = (*env)->FindClass(env, nettyClassName);
-    free(nettyClassName);
-    nettyClassName = NULL;
-    if (fileRegionCls == NULL) {
-        return JNI_ERR;
-    }
-    fileChannelFieldId = (*env)->GetFieldID(env, fileRegionCls, "file", "Ljava/nio/channels/FileChannel;");
-    if (fileChannelFieldId == NULL) {
-        netty_unix_errors_throwRuntimeException(env, "failed to get field ID: DefaultFileRegion.file");
-        return JNI_ERR;
-    }
-    transferredFieldId = (*env)->GetFieldID(env, fileRegionCls, "transferred", "J");
-    if (transferredFieldId == NULL) {
-        netty_unix_errors_throwRuntimeException(env, "failed to get field ID: DefaultFileRegion.transferred");
-        return JNI_ERR;
-    }
+    NETTY_PREPEND(packagePrefix, "io/netty/channel/DefaultFileRegion", nettyClassName, done);
+    NETTY_FIND_CLASS(env, fileRegionCls, nettyClassName, done);
+    netty_unix_util_free_dynamic_name(&nettyClassName);
 
-    jclass fileChannelCls = (*env)->FindClass(env, "sun/nio/ch/FileChannelImpl");
-    if (fileChannelCls == NULL) {
-        // pending exception...
-        return JNI_ERR;
-    }
-    fileDescriptorFieldId = (*env)->GetFieldID(env, fileChannelCls, "fd", "Ljava/io/FileDescriptor;");
-    if (fileDescriptorFieldId == NULL) {
-        netty_unix_errors_throwRuntimeException(env, "failed to get field ID: FileChannelImpl.fd");
-        return JNI_ERR;
-    }
+    NETTY_GET_FIELD(env, fileRegionCls, fileChannelFieldId, "file", "Ljava/nio/channels/FileChannel;", done);
+    NETTY_GET_FIELD(env, fileRegionCls, transferredFieldId, "transferred", "J", done);
 
-    jclass fileDescriptorCls = (*env)->FindClass(env, "java/io/FileDescriptor");
-    if (fileDescriptorCls == NULL) {
-        // pending exception...
-        return JNI_ERR;
-    }
-    fdFieldId = (*env)->GetFieldID(env, fileDescriptorCls, "fd", "I");
-    if (fdFieldId == NULL) {
-        netty_unix_errors_throwRuntimeException(env, "failed to get field ID: FileDescriptor.fd");
-        return JNI_ERR;
-    }
-    stringCls = (*env)->FindClass(env, "java/lang/String");
-    if (stringCls == NULL) {
-        // pending exception...
-        return JNI_ERR;
-    }
+    NETTY_FIND_CLASS(env, fileChannelCls, "sun/nio/ch/FileChannelImpl", done);
+    NETTY_GET_FIELD(env, fileChannelCls, fileDescriptorFieldId, "fd", "Ljava/io/FileDescriptor;", done);
+
+    NETTY_FIND_CLASS(env, fileDescriptorCls, "java/io/FileDescriptor", done);
+    NETTY_GET_FIELD(env, fileDescriptorCls, fdFieldId, "fd", "I", done);
+  
+    NETTY_LOAD_CLASS(env, stringClass, "java/lang/String", done);
 
-    nettyClassName = netty_unix_util_prepend(packagePrefix, "io/netty/channel/unix/PeerCredentials");
-    jclass localPeerCredsClass = (*env)->FindClass(env, nettyClassName);
+    NETTY_PREPEND(packagePrefix, "io/netty/channel/unix/PeerCredentials", nettyClassName, done);
+    NETTY_LOAD_CLASS(env, peerCredentialsClass, nettyClassName, done);
+    netty_unix_util_free_dynamic_name(&nettyClassName);
+  
+    NETTY_GET_METHOD(env, peerCredentialsClass, peerCredentialsMethodId, "<init>", "(II[I)V", done);
+    ret = NETTY_JNI_VERSION;
+done:
+    netty_unix_util_free_dynamic_methods_table(dynamicMethods, fixed_method_table_size, dynamicMethodsTableSize());
     free(nettyClassName);
-    nettyClassName = NULL;
-    if (localPeerCredsClass == NULL) {
-        // pending exception...
-        return JNI_ERR;
-    }
-    peerCredentialsClass = (jclass) (*env)->NewGlobalRef(env, localPeerCredsClass);
-    if (peerCredentialsClass == NULL) {
-        // out-of-memory!
-        netty_unix_errors_throwOutOfMemoryError(env);
-        return JNI_ERR;
-    }
-    peerCredentialsMethodId = (*env)->GetMethodID(env, peerCredentialsClass, "<init>", "(II[I)V");
-    if (peerCredentialsMethodId == NULL) {
-        netty_unix_errors_throwRuntimeException(env, "failed to get method ID: PeerCredentials.<init>(int, int, int[])");
-        return JNI_ERR;
-    }
 
-    return NETTY_JNI_VERSION;
+    return ret;
 }
 
 void netty_kqueue_bsdsocket_JNI_OnUnLoad(JNIEnv* env) {
-    if (peerCredentialsClass != NULL) {
-        (*env)->DeleteGlobalRef(env, peerCredentialsClass);
-        peerCredentialsClass = NULL;
-    }
+    NETTY_UNLOAD_CLASS(env, peerCredentialsClass);
+    NETTY_UNLOAD_CLASS(env, stringClass);
 }
diff --git a/transport-native-kqueue/src/main/c/netty_kqueue_native.c b/transport-native-kqueue/src/main/c/netty_kqueue_native.c
index a2a5a87..9e995ec 100644
--- a/transport-native-kqueue/src/main/c/netty_kqueue_native.c
+++ b/transport-native-kqueue/src/main/c/netty_kqueue_native.c
@@ -139,15 +139,11 @@ static jint netty_kqueue_native_keventWait(JNIEnv* env, jclass clazz, jint kqueu
         }
 
         netty_unix_util_clock_gettime(waitClockId, &nowTs);
-        // beforeTs will store the time difference to check for overflow
-        beforeTs.tv_sec = nowTs.tv_sec - beforeTs.tv_sec;
-        beforeTs.tv_nsec = nowTs.tv_nsec - beforeTs.tv_nsec;
-        // Now subtract the time difference
-        timeoutTs.tv_sec -= beforeTs.tv_sec;
-        timeoutTs.tv_nsec -= beforeTs.tv_nsec;
-        if (beforeTs.tv_sec < 0 || beforeTs.tv_nsec < 0 || (timeoutTs.tv_sec <= 0 && timeoutTs.tv_nsec <= 0)) {
+        if (netty_unix_util_timespec_subtract_ns(&timeoutTs,
+              netty_unix_util_timespec_elapsed_ns(&beforeTs, &nowTs))) {
             return 0;
         }
+
         beforeTs = nowTs;
         // https://www.freebsd.org/cgi/man.cgi?query=kqueue&sektion=2
         // When kevent() call fails with EINTR error, all changes in the changelist have been applied.
@@ -392,10 +388,8 @@ static jint JNI_OnLoad_netty_transport_native_kqueue0(JavaVM* vm, void* reserved
 #endif /* NETTY_BUILD_STATIC */
     jint ret = netty_kqueue_native_JNI_OnLoad(env, packagePrefix);
 
-    if (packagePrefix != NULL) {
-      free(packagePrefix);
-      packagePrefix = NULL;
-    }
+    // It's safe to call free(...) with a NULL argument as well.
+    free(packagePrefix);
 
     return ret;
 }
diff --git a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/AbstractKQueueChannel.java b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/AbstractKQueueChannel.java
index 559999c..02d2a1c 100644
--- a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/AbstractKQueueChannel.java
+++ b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/AbstractKQueueChannel.java
@@ -124,35 +124,7 @@ abstract class AbstractKQueueChannel extends AbstractChannel implements UnixChan
         // Even if we allow half closed sockets we should give up on reading. Otherwise we may allow a read attempt on a
         // socket which has not even been connected yet. This has been observed to block during unit tests.
         inputClosedSeenErrorOnRead = true;
-        try {
-            if (isRegistered()) {
-                // The FD will be closed, which should take care of deleting any associated events from kqueue, but
-                // since we rely upon jniSelfRef to be consistent we make sure that we clear this reference out for
-                // all events which are pending in kqueue to avoid referencing a deleted pointer at a later time.
-
-                // Need to check if we are on the EventLoop as doClose() may be triggered by the GlobalEventExecutor
-                // if SO_LINGER is used.
-                //
-                // See https://github.com/netty/netty/issues/7159
-                EventLoop loop = eventLoop();
-                if (loop.inEventLoop()) {
-                    doDeregister();
-                } else {
-                    loop.execute(new Runnable() {
-                        @Override
-                        public void run() {
-                            try {
-                                doDeregister();
-                            } catch (Throwable cause) {
-                                pipeline().fireExceptionCaught(cause);
-                            }
-                        }
-                    });
-                }
-            }
-        } finally {
-            socket.close();
-        }
+        socket.close();
     }
 
     @Override
@@ -160,6 +132,11 @@ abstract class AbstractKQueueChannel extends AbstractChannel implements UnixChan
         doClose();
     }
 
+    void resetCachedAddresses() {
+        local = socket.localAddress();
+        remote = socket.remoteAddress();
+    }
+
     @Override
     protected boolean isCompatible(EventLoop loop) {
         return loop instanceof KQueueEventLoop;
@@ -172,12 +149,19 @@ abstract class AbstractKQueueChannel extends AbstractChannel implements UnixChan
 
     @Override
     protected void doDeregister() throws Exception {
+        ((KQueueEventLoop) eventLoop()).remove(this);
+
+        // As unregisteredFilters() may have not been called because isOpen() returned false we just set both filters
+        // to false to ensure a consistent state in all cases.
+        readFilterEnabled = false;
+        writeFilterEnabled = false;
+    }
+
+    void unregisterFilters() throws Exception {
         // Make sure we unregister our filters from kqueue!
         readFilter(false);
         writeFilter(false);
         evSet0(Native.EVFILT_SOCK, Native.EV_DELETE, 0);
-
-        ((KQueueEventLoop) eventLoop()).remove(this);
     }
 
     @Override
@@ -297,7 +281,7 @@ abstract class AbstractKQueueChannel extends AbstractChannel implements UnixChan
                 return 1;
             }
         } else {
-            final ByteBuffer nioBuf = buf.nioBufferCount() == 1 ?
+            final ByteBuffer nioBuf = buf.nioBufferCount() == 1?
                     buf.internalNioBuffer(buf.readerIndex(), buf.readableBytes()) : buf.nioBuffer();
             int localFlushedAmount = socket.write(nioBuf, nioBuf.position(), nioBuf.limit());
             if (localFlushedAmount > 0) {
@@ -314,6 +298,10 @@ abstract class AbstractKQueueChannel extends AbstractChannel implements UnixChan
     }
 
     private static boolean isAllowHalfClosure(ChannelConfig config) {
+        if (config instanceof KQueueDomainSocketChannelConfig) {
+            return ((KQueueDomainSocketChannelConfig) config).isAllowHalfClosure();
+        }
+
         return config instanceof SocketChannelConfig &&
                 ((SocketChannelConfig) config).isAllowHalfClosure();
     }
@@ -359,7 +347,7 @@ abstract class AbstractKQueueChannel extends AbstractChannel implements UnixChan
     }
 
     private void evSet(short filter, short flags) {
-        if (isOpen() && isRegistered()) {
+        if (isRegistered()) {
             evSet0(filter, flags);
         }
     }
@@ -369,7 +357,10 @@ abstract class AbstractKQueueChannel extends AbstractChannel implements UnixChan
     }
 
     private void evSet0(short filter, short flags, int fflags) {
-        ((KQueueEventLoop) eventLoop()).evSet(this, filter, flags, fflags);
+        // Only try to add to changeList if the FD is still open, if not we already closed it in the meantime.
+        if (isOpen()) {
+            ((KQueueEventLoop) eventLoop()).evSet(this, filter, flags, fflags);
+        }
     }
 
     abstract class AbstractKQueueUnsafe extends AbstractUnsafe {
@@ -392,7 +383,9 @@ abstract class AbstractKQueueChannel extends AbstractChannel implements UnixChan
 
         abstract void readReady(KQueueRecvByteAllocatorHandle allocHandle);
 
-        final void readReadyBefore() { maybeMoreDataToRead = false; }
+        final void readReadyBefore() {
+            maybeMoreDataToRead = false;
+        }
 
         final void readReadyFinally(ChannelConfig config) {
             maybeMoreDataToRead = allocHandle.maybeMoreDataToRead();
@@ -708,7 +701,7 @@ abstract class AbstractKQueueChannel extends AbstractChannel implements UnixChan
 
         boolean connected = doConnect0(remoteAddress);
         if (connected) {
-            remote = remoteSocketAddr == null ?
+            remote = remoteSocketAddr == null?
                     remoteAddress : computeRemoteAddr(remoteSocketAddr, socket.remoteAddress());
         }
         // We always need to set the localAddress even if not connected yet as the bind already took place.
diff --git a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/AbstractKQueueStreamChannel.java b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/AbstractKQueueStreamChannel.java
index 7d604f1..4b7f4e0 100644
--- a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/AbstractKQueueStreamChannel.java
+++ b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/AbstractKQueueStreamChannel.java
@@ -210,12 +210,13 @@ public abstract class AbstractKQueueStreamChannel extends AbstractKQueueChannel
      */
     private int writeDefaultFileRegion(ChannelOutboundBuffer in, DefaultFileRegion region) throws Exception {
         final long regionCount = region.count();
-        if (region.transferred() >= regionCount) {
+        final long offset = region.transferred();
+
+        if (offset >= regionCount) {
             in.remove();
             return 0;
         }
 
-        final long offset = region.transferred();
         final long flushedAmount = socket.sendFile(region, region.position(), offset, regionCount - offset);
         if (flushedAmount > 0) {
             in.progress(flushedAmount);
@@ -223,6 +224,8 @@ public abstract class AbstractKQueueStreamChannel extends AbstractKQueueChannel
                 in.remove();
             }
             return 1;
+        } else if (flushedAmount == 0) {
+            validateFileRegion(region, offset);
         }
         return WRITE_STATUS_SNDBUF_FULL;
     }
diff --git a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/BsdSocket.java b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/BsdSocket.java
index 3de1cea..b712be9 100644
--- a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/BsdSocket.java
+++ b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/BsdSocket.java
@@ -16,26 +16,18 @@
 package io.netty.channel.kqueue;
 
 import io.netty.channel.DefaultFileRegion;
-import io.netty.channel.unix.Errors;
 import io.netty.channel.unix.PeerCredentials;
 import io.netty.channel.unix.Socket;
-import io.netty.util.internal.ThrowableUtil;
 
 import java.io.IOException;
-import java.nio.channels.ClosedChannelException;
 
 import static io.netty.channel.kqueue.AcceptFilter.PLATFORM_UNSUPPORTED;
-import static io.netty.channel.unix.Errors.ERRNO_EPIPE_NEGATIVE;
 import static io.netty.channel.unix.Errors.ioResult;
-import static io.netty.channel.unix.Errors.newConnectionResetException;
 
 /**
  * A socket which provides access BSD native methods.
  */
 final class BsdSocket extends Socket {
-    private static final Errors.NativeIoException SENDFILE_CONNECTION_RESET_EXCEPTION;
-    private static final ClosedChannelException SENDFILE_CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace(
-            new ClosedChannelException(), Native.class, "sendfile(..)");
 
     // These limits are just based on observations. I couldn't find anything in header files which formally
     // define these limits.
@@ -43,11 +35,6 @@ final class BsdSocket extends Socket {
     private static final int FREEBSD_SND_LOW_AT_MAX = 1 << 15;
     static final int BSD_SND_LOW_AT_MAX = Math.min(APPLE_SND_LOW_AT_MAX, FREEBSD_SND_LOW_AT_MAX);
 
-    static {
-        SENDFILE_CONNECTION_RESET_EXCEPTION = newConnectionResetException("syscall:sendfile",
-                ERRNO_EPIPE_NEGATIVE);
-    }
-
     BsdSocket(int fd) {
         super(fd);
     }
@@ -90,7 +77,7 @@ final class BsdSocket extends Socket {
         if (res >= 0) {
             return res;
         }
-        return ioResult("sendfile", (int) res, SENDFILE_CONNECTION_RESET_EXCEPTION, SENDFILE_CLOSED_CHANNEL_EXCEPTION);
+        return ioResult("sendfile", (int) res);
     }
 
     public static BsdSocket newSocketStream() {
diff --git a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueue.java b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueue.java
index b5a772f..37ad6ea 100644
--- a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueue.java
+++ b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueue.java
@@ -25,7 +25,8 @@ import io.netty.util.internal.UnstableApi;
 @UnstableApi
 public final class KQueue {
     private static final Throwable UNAVAILABILITY_CAUSE;
-    static  {
+
+    static {
         Throwable cause = null;
         if (SystemPropertyUtil.getBoolean("io.netty.transport.noNative", false)) {
             cause = new UnsupportedOperationException(
@@ -51,15 +52,15 @@ public final class KQueue {
     }
 
     /**
-     * Returns {@code true} if and only if the
-     * <a href="http://netty.io/wiki/native-transports.html">{@code netty-transport-native-kqueue}</a> is available.
+     * Returns {@code true} if and only if the <a href="https://netty.io/wiki/native-transports.html">{@code
+     * netty-transport-native-kqueue}</a> is available.
      */
     public static boolean isAvailable() {
         return UNAVAILABILITY_CAUSE == null;
     }
 
     /**
-     * Ensure that <a href="http://netty.io/wiki/native-transports.html">{@code netty-transport-native-kqueue}</a> is
+     * Ensure that <a href="https://netty.io/wiki/native-transports.html">{@code netty-transport-native-kqueue}</a> is
      * available.
      *
      * @throws UnsatisfiedLinkError if unavailable
@@ -72,8 +73,8 @@ public final class KQueue {
     }
 
     /**
-     * Returns the cause of unavailability of
-     * <a href="http://netty.io/wiki/native-transports.html">{@code netty-transport-native-kqueue}</a>.
+     * Returns the cause of unavailability of <a href="https://netty.io/wiki/native-transports.html">{@code
+     * netty-transport-native-kqueue}</a>.
      *
      * @return the cause if unavailable. {@code null} if available.
      */
@@ -81,5 +82,6 @@ public final class KQueue {
         return UNAVAILABILITY_CAUSE;
     }
 
-    private KQueue() { }
+    private KQueue() {
+    }
 }
diff --git a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueDatagramChannel.java b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueDatagramChannel.java
index 887951a..db29ebe 100644
--- a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueDatagramChannel.java
+++ b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueDatagramChannel.java
@@ -28,8 +28,10 @@ import io.netty.channel.socket.DatagramChannel;
 import io.netty.channel.socket.DatagramChannelConfig;
 import io.netty.channel.socket.DatagramPacket;
 import io.netty.channel.unix.DatagramSocketAddress;
+import io.netty.channel.unix.Errors;
 import io.netty.channel.unix.IovArray;
 import io.netty.channel.unix.UnixChannelUtil;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.StringUtil;
 import io.netty.util.internal.UnstableApi;
 
@@ -37,11 +39,10 @@ import java.io.IOException;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.NetworkInterface;
+import java.net.PortUnreachableException;
 import java.net.SocketAddress;
 import java.net.SocketException;
 import java.nio.ByteBuffer;
-import java.util.ArrayList;
-import java.util.List;
 
 import static io.netty.channel.kqueue.BsdSocket.newSocketDgram;
 
@@ -140,13 +141,8 @@ public final class KQueueDatagramChannel extends AbstractKQueueChannel implement
             final InetAddress multicastAddress, final NetworkInterface networkInterface,
             final InetAddress source, final ChannelPromise promise) {
 
-        if (multicastAddress == null) {
-            throw new NullPointerException("multicastAddress");
-        }
-
-        if (networkInterface == null) {
-            throw new NullPointerException("networkInterface");
-        }
+        ObjectUtil.checkNotNull(multicastAddress, "multicastAddress");
+        ObjectUtil.checkNotNull(networkInterface, "networkInterface");
 
         promise.setFailure(new UnsupportedOperationException("Multicast not supported"));
         return promise;
@@ -191,12 +187,8 @@ public final class KQueueDatagramChannel extends AbstractKQueueChannel implement
     public ChannelFuture leaveGroup(
             final InetAddress multicastAddress, final NetworkInterface networkInterface, final InetAddress source,
             final ChannelPromise promise) {
-        if (multicastAddress == null) {
-            throw new NullPointerException("multicastAddress");
-        }
-        if (networkInterface == null) {
-            throw new NullPointerException("networkInterface");
-        }
+        ObjectUtil.checkNotNull(multicastAddress, "multicastAddress");
+        ObjectUtil.checkNotNull(networkInterface, "networkInterface");
 
         promise.setFailure(new UnsupportedOperationException("Multicast not supported"));
 
@@ -214,16 +206,9 @@ public final class KQueueDatagramChannel extends AbstractKQueueChannel implement
     public ChannelFuture block(
             final InetAddress multicastAddress, final NetworkInterface networkInterface,
             final InetAddress sourceToBlock, final ChannelPromise promise) {
-        if (multicastAddress == null) {
-            throw new NullPointerException("multicastAddress");
-        }
-        if (sourceToBlock == null) {
-            throw new NullPointerException("sourceToBlock");
-        }
-
-        if (networkInterface == null) {
-            throw new NullPointerException("networkInterface");
-        }
+        ObjectUtil.checkNotNull(multicastAddress, "multicastAddress");
+        ObjectUtil.checkNotNull(sourceToBlock, "sourceToBlock");
+        ObjectUtil.checkNotNull(networkInterface, "networkInterface");
         promise.setFailure(new UnsupportedOperationException("Multicast not supported"));
         return promise;
     }
@@ -323,7 +308,7 @@ public final class KQueueDatagramChannel extends AbstractKQueueChannel implement
             }
         } else if (data.nioBufferCount() > 1) {
             IovArray array = ((KQueueEventLoop) eventLoop()).cleanArray();
-            array.add(data);
+            array.add(data, data.readerIndex(), data.readableBytes());
             int cnt = array.count();
             assert cnt != 0;
 
@@ -333,7 +318,7 @@ public final class KQueueDatagramChannel extends AbstractKQueueChannel implement
                 writtenBytes = socket.sendToAddresses(array.memoryAddress(0), cnt,
                         remoteAddress.getAddress(), remoteAddress.getPort());
             }
-        } else  {
+        } else {
             ByteBuffer nioData = data.internalNioBuffer(data.readerIndex(), data.readableBytes());
             if (remoteAddress == null) {
                 writtenBytes = socket.write(nioData, nioData.position(), nioData.limit());
@@ -351,13 +336,13 @@ public final class KQueueDatagramChannel extends AbstractKQueueChannel implement
         if (msg instanceof DatagramPacket) {
             DatagramPacket packet = (DatagramPacket) msg;
             ByteBuf content = packet.content();
-            return UnixChannelUtil.isBufferCopyNeededForWrite(content)?
+            return UnixChannelUtil.isBufferCopyNeededForWrite(content) ?
                     new DatagramPacket(newDirectBuffer(packet, content), packet.recipient()) : msg;
         }
 
         if (msg instanceof ByteBuf) {
             ByteBuf buf = (ByteBuf) msg;
-            return UnixChannelUtil.isBufferCopyNeededForWrite(buf)? newDirectBuffer(buf) : buf;
+            return UnixChannelUtil.isBufferCopyNeededForWrite(buf) ? newDirectBuffer(buf) : buf;
         }
 
         if (msg instanceof AddressedEnvelope) {
@@ -367,7 +352,7 @@ public final class KQueueDatagramChannel extends AbstractKQueueChannel implement
                     (e.recipient() == null || e.recipient() instanceof InetSocketAddress)) {
 
                 ByteBuf content = (ByteBuf) e.content();
-                return UnixChannelUtil.isBufferCopyNeededForWrite(content)?
+                return UnixChannelUtil.isBufferCopyNeededForWrite(content) ?
                         new DefaultAddressedEnvelope<ByteBuf, InetSocketAddress>(
                                 newDirectBuffer(e, content), (InetSocketAddress) e.recipient()) : e;
             }
@@ -386,6 +371,7 @@ public final class KQueueDatagramChannel extends AbstractKQueueChannel implement
     protected void doDisconnect() throws Exception {
         socket.disconnect();
         connected = active = false;
+        resetCachedAddresses();
     }
 
     @Override
@@ -420,41 +406,72 @@ public final class KQueueDatagramChannel extends AbstractKQueueChannel implement
 
             Throwable exception = null;
             try {
-                ByteBuf data = null;
+                ByteBuf byteBuf = null;
                 try {
+                    boolean connected = isConnected();
                     do {
-                        data = allocHandle.allocate(allocator);
-                        allocHandle.attemptedBytesRead(data.writableBytes());
-                        final DatagramSocketAddress remoteAddress;
-                        if (data.hasMemoryAddress()) {
-                            // has a memory address so use optimized call
-                            remoteAddress = socket.recvFromAddress(data.memoryAddress(), data.writerIndex(),
-                                    data.capacity());
+                        byteBuf = allocHandle.allocate(allocator);
+                        allocHandle.attemptedBytesRead(byteBuf.writableBytes());
+
+                        final DatagramPacket packet;
+                        if (connected) {
+                            try {
+                                allocHandle.lastBytesRead(doReadBytes(byteBuf));
+                            } catch (Errors.NativeIoException e) {
+                                // We need to correctly translate connect errors to match NIO behaviour.
+                                if (e.expectedErr() == Errors.ERROR_ECONNREFUSED_NEGATIVE) {
+                                    PortUnreachableException error = new PortUnreachableException(e.getMessage());
+                                    error.initCause(e);
+                                    throw error;
+                                }
+                                throw e;
+                            }
+                            if (allocHandle.lastBytesRead() <= 0) {
+                                // nothing was read, release the buffer.
+                                byteBuf.release();
+                                byteBuf = null;
+                                break;
+                            }
+                            packet = new DatagramPacket(byteBuf,
+                                    (InetSocketAddress) localAddress(), (InetSocketAddress) remoteAddress());
                         } else {
-                            ByteBuffer nioData = data.internalNioBuffer(data.writerIndex(), data.writableBytes());
-                            remoteAddress = socket.recvFrom(nioData, nioData.position(), nioData.limit());
-                        }
-
-                        if (remoteAddress == null) {
-                            allocHandle.lastBytesRead(-1);
-                            data.release();
-                            data = null;
-                            break;
+                            final DatagramSocketAddress remoteAddress;
+                            if (byteBuf.hasMemoryAddress()) {
+                                // has a memory address so use optimized call
+                                remoteAddress = socket.recvFromAddress(byteBuf.memoryAddress(), byteBuf.writerIndex(),
+                                        byteBuf.capacity());
+                            } else {
+                                ByteBuffer nioData = byteBuf.internalNioBuffer(
+                                        byteBuf.writerIndex(), byteBuf.writableBytes());
+                                remoteAddress = socket.recvFrom(nioData, nioData.position(), nioData.limit());
+                            }
+
+                            if (remoteAddress == null) {
+                                allocHandle.lastBytesRead(-1);
+                                byteBuf.release();
+                                byteBuf = null;
+                                break;
+                            }
+                            InetSocketAddress localAddress = remoteAddress.localAddress();
+                            if (localAddress == null) {
+                                localAddress = (InetSocketAddress) localAddress();
+                            }
+                            allocHandle.lastBytesRead(remoteAddress.receivedAmount());
+                            byteBuf.writerIndex(byteBuf.writerIndex() + allocHandle.lastBytesRead());
+
+                            packet = new DatagramPacket(byteBuf, localAddress, remoteAddress);
                         }
 
                         allocHandle.incMessagesRead(1);
-                        allocHandle.lastBytesRead(remoteAddress.receivedAmount());
-                        data.writerIndex(data.writerIndex() + allocHandle.lastBytesRead());
 
                         readPending = false;
-                        pipeline.fireChannelRead(
-                                new DatagramPacket(data, (InetSocketAddress) localAddress(), remoteAddress));
+                        pipeline.fireChannelRead(packet);
 
-                        data = null;
+                        byteBuf = null;
                     } while (allocHandle.continueReading());
                 } catch (Throwable t) {
-                    if (data != null) {
-                        data.release();
+                    if (byteBuf != null) {
+                        byteBuf.release();
                     }
                     exception = t;
                 }
diff --git a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueDomainSocketChannelConfig.java b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueDomainSocketChannelConfig.java
index eefd4c0..9d343a3 100644
--- a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueDomainSocketChannelConfig.java
+++ b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueDomainSocketChannelConfig.java
@@ -20,17 +20,24 @@ import io.netty.channel.ChannelOption;
 import io.netty.channel.MessageSizeEstimator;
 import io.netty.channel.RecvByteBufAllocator;
 import io.netty.channel.WriteBufferWaterMark;
+import io.netty.channel.socket.SocketChannelConfig;
 import io.netty.channel.unix.DomainSocketChannelConfig;
 import io.netty.channel.unix.DomainSocketReadMode;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.UnstableApi;
 
+import java.io.IOException;
 import java.util.Map;
 
+import static io.netty.channel.ChannelOption.ALLOW_HALF_CLOSURE;
+import static io.netty.channel.ChannelOption.SO_RCVBUF;
+import static io.netty.channel.ChannelOption.SO_SNDBUF;
 import static io.netty.channel.unix.UnixChannelOption.DOMAIN_SOCKET_READ_MODE;
 
 @UnstableApi
 public final class KQueueDomainSocketChannelConfig extends KQueueChannelConfig implements DomainSocketChannelConfig {
     private volatile DomainSocketReadMode mode = DomainSocketReadMode.BYTES;
+    private volatile boolean allowHalfClosure;
 
     KQueueDomainSocketChannelConfig(AbstractKQueueChannel channel) {
         super(channel);
@@ -38,7 +45,7 @@ public final class KQueueDomainSocketChannelConfig extends KQueueChannelConfig i
 
     @Override
     public Map<ChannelOption<?>, Object> getOptions() {
-        return getOptions(super.getOptions(), DOMAIN_SOCKET_READ_MODE);
+        return getOptions(super.getOptions(), DOMAIN_SOCKET_READ_MODE, ALLOW_HALF_CLOSURE, SO_SNDBUF, SO_RCVBUF);
     }
 
     @SuppressWarnings("unchecked")
@@ -47,6 +54,15 @@ public final class KQueueDomainSocketChannelConfig extends KQueueChannelConfig i
         if (option == DOMAIN_SOCKET_READ_MODE) {
             return (T) getReadMode();
         }
+        if (option == ALLOW_HALF_CLOSURE) {
+            return (T) Boolean.valueOf(isAllowHalfClosure());
+        }
+        if (option == SO_SNDBUF) {
+            return (T) Integer.valueOf(getSendBufferSize());
+        }
+        if (option == SO_RCVBUF) {
+            return (T) Integer.valueOf(getReceiveBufferSize());
+        }
         return super.getOption(option);
     }
 
@@ -56,6 +72,12 @@ public final class KQueueDomainSocketChannelConfig extends KQueueChannelConfig i
 
         if (option == DOMAIN_SOCKET_READ_MODE) {
             setReadMode((DomainSocketReadMode) value);
+        } else if (option == ALLOW_HALF_CLOSURE) {
+            setAllowHalfClosure((Boolean) value);
+        } else if (option == SO_SNDBUF) {
+            setSendBufferSize((Integer) value);
+        } else if (option == SO_RCVBUF) {
+            setReceiveBufferSize((Integer) value);
         } else {
             return super.setOption(option, value);
         }
@@ -140,10 +162,7 @@ public final class KQueueDomainSocketChannelConfig extends KQueueChannelConfig i
 
     @Override
     public KQueueDomainSocketChannelConfig setReadMode(DomainSocketReadMode mode) {
-        if (mode == null) {
-            throw new NullPointerException("mode");
-        }
-        this.mode = mode;
+        this.mode = ObjectUtil.checkNotNull(mode, "mode");
         return this;
     }
 
@@ -151,4 +170,53 @@ public final class KQueueDomainSocketChannelConfig extends KQueueChannelConfig i
     public DomainSocketReadMode getReadMode() {
         return mode;
     }
+
+    public int getSendBufferSize() {
+        try {
+            return ((KQueueDomainSocketChannel) channel).socket.getSendBufferSize();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public KQueueDomainSocketChannelConfig setSendBufferSize(int sendBufferSize) {
+        try {
+            ((KQueueDomainSocketChannel) channel).socket.setSendBufferSize(sendBufferSize);
+            return this;
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public int getReceiveBufferSize() {
+        try {
+            return ((KQueueDomainSocketChannel) channel).socket.getReceiveBufferSize();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public KQueueDomainSocketChannelConfig setReceiveBufferSize(int receiveBufferSize) {
+        try {
+            ((KQueueDomainSocketChannel) channel).socket.setReceiveBufferSize(receiveBufferSize);
+            return this;
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * @see SocketChannelConfig#isAllowHalfClosure()
+     */
+    public boolean isAllowHalfClosure() {
+        return allowHalfClosure;
+    }
+
+    /**
+     * @see SocketChannelConfig#setAllowHalfClosure(boolean)
+     */
+    public KQueueDomainSocketChannelConfig setAllowHalfClosure(boolean allowHalfClosure) {
+        this.allowHalfClosure = allowHalfClosure;
+        return this;
+    }
 }
diff --git a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueEventLoop.java b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueEventLoop.java
index 25c50a4..90ef189 100644
--- a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueEventLoop.java
+++ b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueEventLoop.java
@@ -17,6 +17,7 @@ package io.netty.channel.kqueue;
 
 import io.netty.channel.EventLoop;
 import io.netty.channel.EventLoopGroup;
+import io.netty.channel.EventLoopTaskQueueFactory;
 import io.netty.channel.SelectStrategy;
 import io.netty.channel.SingleThreadEventLoop;
 import io.netty.channel.kqueue.AbstractKQueueChannel.AbstractKQueueUnsafe;
@@ -71,9 +72,11 @@ final class KQueueEventLoop extends SingleThreadEventLoop {
     private volatile int ioRatio = 50;
 
     KQueueEventLoop(EventLoopGroup parent, Executor executor, int maxEvents,
-                    SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) {
-        super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler);
-        selectStrategy = ObjectUtil.checkNotNull(strategy, "strategy");
+                    SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler,
+                    EventLoopTaskQueueFactory queueFactory) {
+        super(parent, executor, false, newTaskQueue(queueFactory), newTaskQueue(queueFactory),
+                rejectedExecutionHandler);
+        this.selectStrategy = ObjectUtil.checkNotNull(strategy, "strategy");
         this.kqueueFd = Native.newKQueue();
         if (maxEvents == 0) {
             allowGrowing = true;
@@ -81,8 +84,8 @@ final class KQueueEventLoop extends SingleThreadEventLoop {
         } else {
             allowGrowing = false;
         }
-        changeList = new KQueueEventArray(maxEvents);
-        eventList = new KQueueEventArray(maxEvents);
+        this.changeList = new KQueueEventArray(maxEvents);
+        this.eventList = new KQueueEventArray(maxEvents);
         int result = Native.keventAddUserEvent(kqueueFd.intValue(), KQUEUE_WAKE_UP_IDENT);
         if (result < 0) {
             cleanup();
@@ -90,18 +93,45 @@ final class KQueueEventLoop extends SingleThreadEventLoop {
         }
     }
 
+    private static Queue<Runnable> newTaskQueue(
+            EventLoopTaskQueueFactory queueFactory) {
+        if (queueFactory == null) {
+            return newTaskQueue0(DEFAULT_MAX_PENDING_TASKS);
+        }
+        return queueFactory.newTaskQueue(DEFAULT_MAX_PENDING_TASKS);
+    }
+
     void add(AbstractKQueueChannel ch) {
         assert inEventLoop();
-        channels.put(ch.fd().intValue(), ch);
+        AbstractKQueueChannel old = channels.put(ch.fd().intValue(), ch);
+        // We either expect to have no Channel in the map with the same FD or that the FD of the old Channel is already
+        // closed.
+        assert old == null || !old.isOpen();
     }
 
     void evSet(AbstractKQueueChannel ch, short filter, short flags, int fflags) {
+        assert inEventLoop();
         changeList.evSet(ch, filter, flags, fflags);
     }
 
-    void remove(AbstractKQueueChannel ch) {
+    void remove(AbstractKQueueChannel ch) throws Exception {
         assert inEventLoop();
-        channels.remove(ch.fd().intValue());
+        int fd = ch.fd().intValue();
+
+        AbstractKQueueChannel old = channels.remove(fd);
+        if (old != null && old != ch) {
+            // The Channel mapping was already replaced due FD reuse, put back the stored Channel.
+            channels.put(fd, old);
+
+            // If we found another Channel in the map that is mapped to the same FD the given Channel MUST be closed.
+            assert !ch.isOpen();
+        } else if (ch.isOpen()) {
+            // Remove the filters. This is only needed if it's still open as otherwise it will be automatically
+            // removed once the file-descriptor is closed.
+            //
+            // See also https://www.freebsd.org/cgi/man.cgi?query=kqueue&sektion=2
+            ch.unregisterFilters();
+        }
     }
 
     /**
@@ -286,9 +316,13 @@ final class KQueueEventLoop extends SingleThreadEventLoop {
 
     @Override
     protected Queue<Runnable> newTaskQueue(int maxPendingTasks) {
+        return newTaskQueue0(maxPendingTasks);
+    }
+
+    private static Queue<Runnable> newTaskQueue0(int maxPendingTasks) {
         // This event loop never calls takeTask()
         return maxPendingTasks == Integer.MAX_VALUE ? PlatformDependent.<Runnable>newMpscQueue()
-                                                    : PlatformDependent.<Runnable>newMpscQueue(maxPendingTasks);
+                : PlatformDependent.<Runnable>newMpscQueue(maxPendingTasks);
     }
 
     /**
@@ -309,6 +343,11 @@ final class KQueueEventLoop extends SingleThreadEventLoop {
         this.ioRatio = ioRatio;
     }
 
+    @Override
+    public int registeredChannels() {
+        return channels.size();
+    }
+
     @Override
     protected void cleanup() {
         try {
@@ -330,6 +369,14 @@ final class KQueueEventLoop extends SingleThreadEventLoop {
         } catch (IOException e) {
             // ignore on close
         }
+
+        // Using the intermediate collection to prevent ConcurrentModificationException.
+        // In the `close()` method, the channel is deleted from `channels` map.
+        AbstractKQueueChannel[] localChannels = channels.values().toArray(new AbstractKQueueChannel[0]);
+
+        for (AbstractKQueueChannel ch: localChannels) {
+            ch.unsafe().close(ch.unsafe().voidPromise());
+        }
     }
 
     private static void handleLoopException(Throwable t) {
diff --git a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueEventLoopGroup.java b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueEventLoopGroup.java
index fe32aa5..34e82a1 100644
--- a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueEventLoopGroup.java
+++ b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueEventLoopGroup.java
@@ -17,6 +17,7 @@ package io.netty.channel.kqueue;
 
 import io.netty.channel.DefaultSelectStrategyFactory;
 import io.netty.channel.EventLoop;
+import io.netty.channel.EventLoopTaskQueueFactory;
 import io.netty.channel.MultithreadEventLoopGroup;
 import io.netty.channel.SelectStrategyFactory;
 import io.netty.util.concurrent.EventExecutor;
@@ -48,6 +49,14 @@ public final class KQueueEventLoopGroup extends MultithreadEventLoopGroup {
         this(nThreads, (ThreadFactory) null);
     }
 
+    /**
+     * Create a new instance using the default number of threads and the given {@link ThreadFactory}.
+     */
+    @SuppressWarnings("deprecation")
+    public KQueueEventLoopGroup(ThreadFactory threadFactory) {
+        this(0, threadFactory, 0);
+    }
+
     /**
      * Create a new instance using the specified number of threads and the default {@link ThreadFactory}.
      */
@@ -116,6 +125,14 @@ public final class KQueueEventLoopGroup extends MultithreadEventLoopGroup {
         super(nThreads, executor, chooserFactory, 0, selectStrategyFactory, rejectedExecutionHandler);
     }
 
+    public KQueueEventLoopGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory,
+                                SelectStrategyFactory selectStrategyFactory,
+                                RejectedExecutionHandler rejectedExecutionHandler,
+                                EventLoopTaskQueueFactory queueFactory) {
+        super(nThreads, executor, chooserFactory, 0, selectStrategyFactory,
+                rejectedExecutionHandler, queueFactory);
+    }
+
     /**
      * Sets the percentage of the desired amount of time spent for I/O in the child event loops.  The default value is
      * {@code 50}, which means the event loop will try to spend the same amount of time for I/O as for non-I/O tasks.
@@ -128,7 +145,10 @@ public final class KQueueEventLoopGroup extends MultithreadEventLoopGroup {
 
     @Override
     protected EventLoop newChild(Executor executor, Object... args) throws Exception {
+        EventLoopTaskQueueFactory queueFactory = args.length == 4 ? (EventLoopTaskQueueFactory) args[3] : null;
+
         return new KQueueEventLoop(this, executor, (Integer) args[0],
-                ((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]);
+                ((SelectStrategyFactory) args[1]).newSelectStrategy(),
+                (RejectedExecutionHandler) args[2], queueFactory);
     }
 }
diff --git a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueRecvByteAllocatorHandle.java b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueRecvByteAllocatorHandle.java
index 9eed7d4..e220858 100644
--- a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueRecvByteAllocatorHandle.java
+++ b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueRecvByteAllocatorHandle.java
@@ -18,18 +18,17 @@ package io.netty.channel.kqueue;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.ByteBufAllocator;
 import io.netty.channel.ChannelConfig;
-import io.netty.channel.RecvByteBufAllocator;
+import io.netty.channel.RecvByteBufAllocator.DelegatingHandle;
+import io.netty.channel.RecvByteBufAllocator.ExtendedHandle;
 import io.netty.channel.unix.PreferredDirectByteBufAllocator;
 import io.netty.util.UncheckedBooleanSupplier;
-import io.netty.util.internal.ObjectUtil;
 
 import static java.lang.Math.max;
 import static java.lang.Math.min;
 
-final class KQueueRecvByteAllocatorHandle implements RecvByteBufAllocator.ExtendedHandle {
+final class KQueueRecvByteAllocatorHandle extends DelegatingHandle implements ExtendedHandle {
     private final PreferredDirectByteBufAllocator preferredDirectByteBufAllocator =
             new PreferredDirectByteBufAllocator();
-    private final RecvByteBufAllocator.ExtendedHandle delegate;
 
     private final UncheckedBooleanSupplier defaultMaybeMoreDataSupplier = new UncheckedBooleanSupplier() {
         @Override
@@ -41,24 +40,19 @@ final class KQueueRecvByteAllocatorHandle implements RecvByteBufAllocator.Extend
     private boolean readEOF;
     private long numberBytesPending;
 
-    KQueueRecvByteAllocatorHandle(RecvByteBufAllocator.ExtendedHandle handle) {
-        delegate = ObjectUtil.checkNotNull(handle, "handle");
+    KQueueRecvByteAllocatorHandle(ExtendedHandle handle) {
+        super(handle);
     }
 
     @Override
     public int guess() {
-        return overrideGuess ? guess0() : delegate.guess();
+        return overrideGuess ? guess0() : delegate().guess();
     }
 
     @Override
     public void reset(ChannelConfig config) {
         overrideGuess = ((KQueueChannelConfig) config).getRcvAllocTransportProvidesGuess();
-        delegate.reset(config);
-    }
-
-    @Override
-    public void incMessagesRead(int numMessages) {
-        delegate.incMessagesRead(numMessages);
+        delegate().reset(config);
     }
 
     @Override
@@ -66,44 +60,24 @@ final class KQueueRecvByteAllocatorHandle implements RecvByteBufAllocator.Extend
         // We need to ensure we always allocate a direct ByteBuf as we can only use a direct buffer to read via JNI.
         preferredDirectByteBufAllocator.updateAllocator(alloc);
         return overrideGuess ? preferredDirectByteBufAllocator.ioBuffer(guess0()) :
-                delegate.allocate(preferredDirectByteBufAllocator);
+                delegate().allocate(preferredDirectByteBufAllocator);
     }
 
     @Override
     public void lastBytesRead(int bytes) {
         numberBytesPending = bytes < 0 ? 0 : max(0, numberBytesPending - bytes);
-        delegate.lastBytesRead(bytes);
-    }
-
-    @Override
-    public int lastBytesRead() {
-        return delegate.lastBytesRead();
-    }
-
-    @Override
-    public void attemptedBytesRead(int bytes) {
-        delegate.attemptedBytesRead(bytes);
-    }
-
-    @Override
-    public int attemptedBytesRead() {
-        return delegate.attemptedBytesRead();
-    }
-
-    @Override
-    public void readComplete() {
-        delegate.readComplete();
+        delegate().lastBytesRead(bytes);
     }
 
     @Override
     public boolean continueReading(UncheckedBooleanSupplier maybeMoreDataSupplier) {
-        return delegate.continueReading(maybeMoreDataSupplier);
+        return ((ExtendedHandle) delegate()).continueReading(maybeMoreDataSupplier);
     }
 
     @Override
     public boolean continueReading() {
         // We must override the supplier which determines if there maybe more data to read.
-        return delegate.continueReading(defaultMaybeMoreDataSupplier);
+        return continueReading(defaultMaybeMoreDataSupplier);
     }
 
     void readEOF() {
diff --git a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueServerChannelConfig.java b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueServerChannelConfig.java
index 09291f5..afd19e4 100644
--- a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueServerChannelConfig.java
+++ b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/KQueueServerChannelConfig.java
@@ -31,6 +31,7 @@ import java.util.Map;
 import static io.netty.channel.ChannelOption.SO_BACKLOG;
 import static io.netty.channel.ChannelOption.SO_RCVBUF;
 import static io.netty.channel.ChannelOption.SO_REUSEADDR;
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
 
 @UnstableApi
 public class KQueueServerChannelConfig extends KQueueChannelConfig implements ServerSocketChannelConfig {
@@ -77,6 +78,7 @@ public class KQueueServerChannelConfig extends KQueueChannelConfig implements Se
         return true;
     }
 
+    @Override
     public boolean isReuseAddress() {
         try {
             return ((AbstractKQueueChannel) channel).socket.isReuseAddress();
@@ -85,6 +87,7 @@ public class KQueueServerChannelConfig extends KQueueChannelConfig implements Se
         }
     }
 
+    @Override
     public KQueueServerChannelConfig setReuseAddress(boolean reuseAddress) {
         try {
             ((AbstractKQueueChannel) channel).socket.setReuseAddress(reuseAddress);
@@ -94,6 +97,7 @@ public class KQueueServerChannelConfig extends KQueueChannelConfig implements Se
         }
     }
 
+    @Override
     public int getReceiveBufferSize() {
         try {
             return ((AbstractKQueueChannel) channel).socket.getReceiveBufferSize();
@@ -102,6 +106,7 @@ public class KQueueServerChannelConfig extends KQueueChannelConfig implements Se
         }
     }
 
+    @Override
     public KQueueServerChannelConfig setReceiveBufferSize(int receiveBufferSize) {
         try {
             ((AbstractKQueueChannel) channel).socket.setReceiveBufferSize(receiveBufferSize);
@@ -111,14 +116,14 @@ public class KQueueServerChannelConfig extends KQueueChannelConfig implements Se
         }
     }
 
+    @Override
     public int getBacklog() {
         return backlog;
     }
 
+    @Override
     public KQueueServerChannelConfig setBacklog(int backlog) {
-        if (backlog < 0) {
-            throw new IllegalArgumentException("backlog: " + backlog);
-        }
+        checkPositiveOrZero(backlog, "backlog");
         this.backlog = backlog;
         return this;
     }
diff --git a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/NativeLongArray.java b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/NativeLongArray.java
index 7f83738..edcadf5 100644
--- a/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/NativeLongArray.java
+++ b/transport-native-kqueue/src/main/java/io/netty/channel/kqueue/NativeLongArray.java
@@ -63,6 +63,10 @@ final class NativeLongArray {
         return size == 0;
     }
 
+    int size() {
+        return size;
+    }
+
     void free() {
         Buffer.free(memory);
         memoryAddress = 0;
diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDatagramUnicastIPv6MappedTest.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDatagramUnicastIPv6MappedTest.java
new file mode 100644
index 0000000..a216d4e
--- /dev/null
+++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDatagramUnicastIPv6MappedTest.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2019 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.channel.kqueue;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.testsuite.transport.TestsuitePermutation.BootstrapComboFactory;
+import io.netty.testsuite.transport.socket.DatagramUnicastIPv6MappedTest;
+
+import java.util.List;
+
+public class KQueueDatagramUnicastIPv6MappedTest extends DatagramUnicastIPv6MappedTest {
+    @Override
+    protected List<BootstrapComboFactory<Bootstrap, Bootstrap>> newFactories() {
+        return KQueueSocketTestPermutation.INSTANCE.datagram(internetProtocolFamily());
+    }
+}
diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDatagramUnicastIPv6Test.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDatagramUnicastIPv6Test.java
new file mode 100644
index 0000000..226fc3b
--- /dev/null
+++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDatagramUnicastIPv6Test.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2019 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.channel.kqueue;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.channel.socket.InternetProtocolFamily;
+import io.netty.testsuite.transport.TestsuitePermutation;
+import io.netty.testsuite.transport.socket.DatagramUnicastIPv6Test;
+import io.netty.testsuite.transport.socket.DatagramUnicastTest;
+
+import java.util.List;
+
+public class KQueueDatagramUnicastIPv6Test extends DatagramUnicastIPv6Test {
+    @Override
+    protected List<TestsuitePermutation.BootstrapComboFactory<Bootstrap, Bootstrap>> newFactories() {
+        return KQueueSocketTestPermutation.INSTANCE.datagram(internetProtocolFamily());
+    }
+}
diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDatagramUnicastTest.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDatagramUnicastTest.java
index c805e90..282c4ab 100644
--- a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDatagramUnicastTest.java
+++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDatagramUnicastTest.java
@@ -16,6 +16,7 @@
 package io.netty.channel.kqueue;
 
 import io.netty.bootstrap.Bootstrap;
+import io.netty.channel.socket.InternetProtocolFamily;
 import io.netty.testsuite.transport.TestsuitePermutation;
 import io.netty.testsuite.transport.socket.DatagramUnicastTest;
 
@@ -24,6 +25,6 @@ import java.util.List;
 public class KQueueDatagramUnicastTest extends DatagramUnicastTest {
     @Override
     protected List<TestsuitePermutation.BootstrapComboFactory<Bootstrap, Bootstrap>> newFactories() {
-        return KQueueSocketTestPermutation.INSTANCE.datagram();
+        return KQueueSocketTestPermutation.INSTANCE.datagram(InternetProtocolFamily.IPv4);
     }
 }
diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDomainSocketReuseFdTest.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDomainSocketReuseFdTest.java
new file mode 100644
index 0000000..2e239c2
--- /dev/null
+++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDomainSocketReuseFdTest.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2019 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.channel.kqueue;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.testsuite.transport.TestsuitePermutation;
+import io.netty.testsuite.transport.socket.AbstractSocketReuseFdTest;
+
+import java.net.SocketAddress;
+import java.util.List;
+
+public class KQueueDomainSocketReuseFdTest extends AbstractSocketReuseFdTest {
+    @Override
+    protected SocketAddress newSocketAddress() {
+        return KQueueSocketTestPermutation.newSocketAddress();
+    }
+
+    @Override
+    protected List<TestsuitePermutation.BootstrapComboFactory<ServerBootstrap, Bootstrap>> newFactories() {
+        return KQueueSocketTestPermutation.INSTANCE.domainSocket();
+    }
+}
diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDomainSocketShutdownOutputByPeerTest.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDomainSocketShutdownOutputByPeerTest.java
new file mode 100644
index 0000000..76532ac
--- /dev/null
+++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDomainSocketShutdownOutputByPeerTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2019 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.channel.kqueue;
+
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.unix.Buffer;
+import io.netty.testsuite.transport.TestsuitePermutation.BootstrapFactory;
+import io.netty.testsuite.transport.socket.AbstractSocketShutdownOutputByPeerTest;
+
+import java.io.IOException;
+import java.net.SocketAddress;
+import java.nio.ByteBuffer;
+import java.util.List;
+
+public class KQueueDomainSocketShutdownOutputByPeerTest extends AbstractSocketShutdownOutputByPeerTest<BsdSocket> {
+
+    @Override
+    protected List<BootstrapFactory<ServerBootstrap>> newFactories() {
+        return KQueueSocketTestPermutation.INSTANCE.serverDomainSocket();
+    }
+
+    @Override
+    protected SocketAddress newSocketAddress() {
+        return KQueueSocketTestPermutation.newSocketAddress();
+    }
+
+    @Override
+    protected void shutdownOutput(BsdSocket s) throws IOException {
+        s.shutdown(false, true);
+    }
+
+    @Override
+    protected void connect(BsdSocket s, SocketAddress address) throws IOException {
+        s.connect(address);
+    }
+
+    @Override
+    protected void close(BsdSocket s) throws IOException {
+        s.close();
+    }
+
+    @Override
+    protected void write(BsdSocket s, int data) throws IOException {
+        final ByteBuffer buf = Buffer.allocateDirectWithNativeOrder(4);
+        buf.putInt(data);
+        buf.flip();
+        s.write(buf, buf.position(), buf.limit());
+        Buffer.free(buf);
+    }
+
+    @Override
+    protected BsdSocket newSocket() {
+        return BsdSocket.newSocketDomain();
+    }
+}
diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDomainSocketSslClientRenegotiateTest.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDomainSocketSslClientRenegotiateTest.java
new file mode 100644
index 0000000..a719b7c
--- /dev/null
+++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDomainSocketSslClientRenegotiateTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2019 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.channel.kqueue;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.handler.ssl.SslContext;
+import io.netty.testsuite.transport.TestsuitePermutation;
+import io.netty.testsuite.transport.socket.SocketSslClientRenegotiateTest;
+
+import java.net.SocketAddress;
+import java.util.List;
+
+public class KQueueDomainSocketSslClientRenegotiateTest extends SocketSslClientRenegotiateTest {
+
+    public KQueueDomainSocketSslClientRenegotiateTest(SslContext serverCtx, SslContext clientCtx, boolean delegate) {
+        super(serverCtx, clientCtx, delegate);
+    }
+
+    @Override
+    protected List<TestsuitePermutation.BootstrapComboFactory<ServerBootstrap, Bootstrap>> newFactories() {
+        return KQueueSocketTestPermutation.INSTANCE.domainSocket();
+    }
+
+    @Override
+    protected SocketAddress newSocketAddress() {
+        return KQueueSocketTestPermutation.newSocketAddress();
+    }
+}
diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDomainSocketSslGreetingTest.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDomainSocketSslGreetingTest.java
index 492021a..8cbaa00 100644
--- a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDomainSocketSslGreetingTest.java
+++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueDomainSocketSslGreetingTest.java
@@ -26,8 +26,8 @@ import java.util.List;
 
 public class KQueueDomainSocketSslGreetingTest extends SocketSslGreetingTest {
 
-    public KQueueDomainSocketSslGreetingTest(SslContext serverCtx, SslContext clientCtx) {
-        super(serverCtx, clientCtx);
+    public KQueueDomainSocketSslGreetingTest(SslContext serverCtx, SslContext clientCtx, boolean delegate) {
+        super(serverCtx, clientCtx, delegate);
     }
 
     @Override
diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueETSocketAutoReadTest.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueETSocketAutoReadTest.java
index 27dd392..a8cd58d 100644
--- a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueETSocketAutoReadTest.java
+++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueETSocketAutoReadTest.java
@@ -17,7 +17,6 @@ package io.netty.channel.kqueue;
 
 import io.netty.bootstrap.Bootstrap;
 import io.netty.bootstrap.ServerBootstrap;
-import io.netty.buffer.ByteBufAllocator;
 import io.netty.testsuite.transport.TestsuitePermutation;
 import io.netty.testsuite.transport.socket.SocketAutoReadTest;
 
diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueETSocketExceptionHandlingTest.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueETSocketExceptionHandlingTest.java
index e65bcdd..3a7f151 100644
--- a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueETSocketExceptionHandlingTest.java
+++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueETSocketExceptionHandlingTest.java
@@ -17,7 +17,6 @@ package io.netty.channel.kqueue;
 
 import io.netty.bootstrap.Bootstrap;
 import io.netty.bootstrap.ServerBootstrap;
-import io.netty.buffer.ByteBufAllocator;
 import io.netty.testsuite.transport.TestsuitePermutation;
 import io.netty.testsuite.transport.socket.SocketExceptionHandlingTest;
 
diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueETSocketReadPendingTest.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueETSocketReadPendingTest.java
index 628084c..2197b04 100644
--- a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueETSocketReadPendingTest.java
+++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueETSocketReadPendingTest.java
@@ -17,7 +17,6 @@ package io.netty.channel.kqueue;
 
 import io.netty.bootstrap.Bootstrap;
 import io.netty.bootstrap.ServerBootstrap;
-import io.netty.buffer.ByteBufAllocator;
 import io.netty.testsuite.transport.TestsuitePermutation;
 import io.netty.testsuite.transport.socket.SocketReadPendingTest;
 
diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueEventLoopTest.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueEventLoopTest.java
index 0d44155..55d2e16 100644
--- a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueEventLoopTest.java
+++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueEventLoopTest.java
@@ -17,6 +17,9 @@ package io.netty.channel.kqueue;
 
 import io.netty.channel.EventLoop;
 import io.netty.channel.EventLoopGroup;
+import io.netty.channel.ServerChannel;
+import io.netty.channel.socket.ServerSocketChannel;
+import io.netty.testsuite.transport.AbstractSingleThreadEventLoopTest;
 import io.netty.util.concurrent.Future;
 import org.junit.Test;
 
@@ -25,7 +28,22 @@ import java.util.concurrent.TimeUnit;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
-public class KQueueEventLoopTest {
+public class KQueueEventLoopTest extends AbstractSingleThreadEventLoopTest {
+
+    @Override
+    protected EventLoopGroup newEventLoopGroup() {
+        return new KQueueEventLoopGroup();
+    }
+
+    @Override
+    protected ServerSocketChannel newChannel() {
+        return new KQueueServerSocketChannel();
+    }
+
+    @Override
+    protected Class<? extends ServerChannel> serverChannelClass() {
+        return KQueueServerSocketChannel.class;
+    }
 
     @Test
     public void testScheduleBigDelayNotOverflow() {
diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketConnectionAttemptTest.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketConnectionAttemptTest.java
index 824c47c..8fa23e9 100644
--- a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketConnectionAttemptTest.java
+++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketConnectionAttemptTest.java
@@ -16,13 +16,9 @@
 package io.netty.channel.kqueue;
 
 import io.netty.bootstrap.Bootstrap;
-import io.netty.channel.Channel;
-import io.netty.channel.ChannelInitializer;
 import io.netty.testsuite.transport.TestsuitePermutation;
 import io.netty.testsuite.transport.socket.SocketConnectionAttemptTest;
-import org.junit.Test;
 
-import java.net.InetSocketAddress;
 import java.util.List;
 
 public class KQueueSocketConnectionAttemptTest extends SocketConnectionAttemptTest {
diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketSslClientRenegotiateTest.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketSslClientRenegotiateTest.java
new file mode 100644
index 0000000..a3ba238
--- /dev/null
+++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketSslClientRenegotiateTest.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2019 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.channel.kqueue;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.handler.ssl.SslContext;
+import io.netty.testsuite.transport.TestsuitePermutation;
+import io.netty.testsuite.transport.socket.SocketSslClientRenegotiateTest;
+
+import java.util.List;
+
+public class KQueueSocketSslClientRenegotiateTest extends SocketSslClientRenegotiateTest {
+
+    public KQueueSocketSslClientRenegotiateTest(SslContext serverCtx, SslContext clientCtx, boolean delegate) {
+        super(serverCtx, clientCtx, delegate);
+    }
+
+    @Override
+    protected List<TestsuitePermutation.BootstrapComboFactory<ServerBootstrap, Bootstrap>> newFactories() {
+        return KQueueSocketTestPermutation.INSTANCE.socket();
+    }
+}
diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketSslGreetingTest.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketSslGreetingTest.java
index 9242fc3..6eecc35 100644
--- a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketSslGreetingTest.java
+++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketSslGreetingTest.java
@@ -25,8 +25,8 @@ import java.util.List;
 
 public class KQueueSocketSslGreetingTest extends SocketSslGreetingTest {
 
-    public KQueueSocketSslGreetingTest(SslContext serverCtx, SslContext clientCtx) {
-        super(serverCtx, clientCtx);
+    public KQueueSocketSslGreetingTest(SslContext serverCtx, SslContext clientCtx, boolean delegate) {
+        super(serverCtx, clientCtx, delegate);
     }
 
     @Override
diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketSslSessionReuseTest.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketSslSessionReuseTest.java
new file mode 100644
index 0000000..5508dcb
--- /dev/null
+++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketSslSessionReuseTest.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2019 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.channel.kqueue;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.handler.ssl.SslContext;
+import io.netty.testsuite.transport.TestsuitePermutation;
+import io.netty.testsuite.transport.socket.SocketSslSessionReuseTest;
+
+import java.util.List;
+
+public class KQueueSocketSslSessionReuseTest extends SocketSslSessionReuseTest {
+
+    public KQueueSocketSslSessionReuseTest(SslContext serverCtx, SslContext clientCtx) {
+        super(serverCtx, clientCtx);
+    }
+
+    @Override
+    protected List<TestsuitePermutation.BootstrapComboFactory<ServerBootstrap, Bootstrap>> newFactories() {
+        return KQueueSocketTestPermutation.INSTANCE.socket();
+    }
+}
diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketTest.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketTest.java
index 38f09c0..201fa9e 100644
--- a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketTest.java
+++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketTest.java
@@ -24,8 +24,7 @@ import org.junit.Test;
 
 import java.io.IOException;
 
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.*;
 import static org.junit.Assume.assumeTrue;
 
 public class KQueueSocketTest extends SocketTest<BsdSocket> {
@@ -55,6 +54,33 @@ public class KQueueSocketTest extends SocketTest<BsdSocket> {
         }
     }
 
+    @Test
+    public void testPeerPID() throws IOException {
+        BsdSocket s1 = BsdSocket.newSocketDomain();
+        BsdSocket s2 = BsdSocket.newSocketDomain();
+
+        try {
+            DomainSocketAddress dsa = UnixTestUtils.newSocketAddress();
+            s1.bind(dsa);
+            s1.listen(1);
+
+            // PID of client socket is expected to be 0 before connection
+            assertEquals(0, s2.getPeerCredentials().pid());
+            assertTrue(s2.connect(dsa));
+            byte [] addr = new byte[64];
+            int clientFd = s1.accept(addr);
+            assertNotEquals(-1, clientFd);
+            PeerCredentials pc = new BsdSocket(clientFd).getPeerCredentials();
+            assertNotEquals(0, pc.pid());
+            assertNotEquals(0, s2.getPeerCredentials().pid());
+            // Server socket FDs should not have pid field set:
+            assertEquals(0, s1.getPeerCredentials().pid());
+        } finally {
+            s1.close();
+            s2.close();
+        }
+    }
+
     @Override
     protected BsdSocket newSocket() {
         return BsdSocket.newSocketStream();
diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketTestPermutation.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketTestPermutation.java
index ab2bef9..9abbdec 100644
--- a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketTestPermutation.java
+++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KQueueSocketTestPermutation.java
@@ -103,7 +103,8 @@ class KQueueSocketTestPermutation extends SocketTestPermutation {
     }
 
     @Override
-    public List<TestsuitePermutation.BootstrapComboFactory<Bootstrap, Bootstrap>> datagram() {
+    public List<TestsuitePermutation.BootstrapComboFactory<Bootstrap, Bootstrap>> datagram(
+            final InternetProtocolFamily family) {
         // Make the list of Bootstrap factories.
         @SuppressWarnings("unchecked")
         List<BootstrapFactory<Bootstrap>> bfs = Arrays.asList(
@@ -113,7 +114,7 @@ class KQueueSocketTestPermutation extends SocketTestPermutation {
                         return new Bootstrap().group(nioWorkerGroup).channelFactory(new ChannelFactory<Channel>() {
                             @Override
                             public Channel newChannel() {
-                                return new NioDatagramChannel(InternetProtocolFamily.IPv4);
+                                return new NioDatagramChannel(family);
                             }
 
                             @Override
diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KqueueWriteBeforeRegisteredTest.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KqueueWriteBeforeRegisteredTest.java
new file mode 100644
index 0000000..b8d4475
--- /dev/null
+++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KqueueWriteBeforeRegisteredTest.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2019 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.channel.kqueue;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.testsuite.transport.TestsuitePermutation;
+import io.netty.testsuite.transport.socket.WriteBeforeRegisteredTest;
+
+import java.util.List;
+
+public class KqueueWriteBeforeRegisteredTest extends WriteBeforeRegisteredTest {
+
+    @Override
+    protected List<TestsuitePermutation.BootstrapFactory<Bootstrap>> newFactories() {
+        return KQueueSocketTestPermutation.INSTANCE.clientSocket();
+    }
+}
diff --git a/transport-native-unix-common-tests/pom.xml b/transport-native-unix-common-tests/pom.xml
index 79a2317..032cf74 100644
--- a/transport-native-unix-common-tests/pom.xml
+++ b/transport-native-unix-common-tests/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>io.netty</groupId>
     <artifactId>netty-parent</artifactId>
-    <version>4.1.33.Final</version>
+    <version>4.1.48.Final</version>
   </parent>
   <artifactId>netty-transport-native-unix-common-tests</artifactId>
 
diff --git a/transport-native-unix-common/pom.xml b/transport-native-unix-common/pom.xml
index 586a004..f92050f 100644
--- a/transport-native-unix-common/pom.xml
+++ b/transport-native-unix-common/pom.xml
@@ -19,7 +19,7 @@
   <parent>
     <groupId>io.netty</groupId>
     <artifactId>netty-parent</artifactId>
-    <version>4.1.33.Final</version>
+    <version>4.1.48.Final</version>
   </parent>
   <artifactId>netty-transport-native-unix-common</artifactId>
 
@@ -44,6 +44,29 @@
     <nativeJarFile>${project.build.directory}/${project.build.finalName}-${jni.classifier}.jar</nativeJarFile>
   </properties>
 
+  <build>
+    <plugins>
+      <!-- Also include c files in source jar -->
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>build-helper-maven-plugin</artifactId>
+        <executions>
+          <execution>
+            <phase>generate-sources</phase>
+            <goals>
+              <goal>add-source</goal>
+            </goals>
+            <configuration>
+              <sources>
+                <source>${nativeIncludeDir}</source>
+              </sources>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+
   <profiles>
     <profile>
       <id>mac</id>
diff --git a/transport-native-unix-common/src/main/c/netty_unix_errors.c b/transport-native-unix-common/src/main/c/netty_unix_errors.c
index 8298f91..634c99b 100644
--- a/transport-native-unix-common/src/main/c/netty_unix_errors.c
+++ b/transport-native-unix-common/src/main/c/netty_unix_errors.c
@@ -21,6 +21,7 @@
 #include "netty_unix_jni.h"
 #include "netty_unix_util.h"
 
+static jclass oomErrorClass = NULL;
 static jclass runtimeExceptionClass = NULL;
 static jclass channelExceptionClass = NULL;
 static jclass ioExceptionClass = NULL;
@@ -43,12 +44,18 @@ void netty_unix_errors_throwRuntimeException(JNIEnv* env, char* message) {
 
 void netty_unix_errors_throwRuntimeExceptionErrorNo(JNIEnv* env, char* message, int errorNumber) {
     char* allocatedMessage = exceptionMessage(message, errorNumber);
+    if (allocatedMessage == NULL) {
+        return;
+    }
     (*env)->ThrowNew(env, runtimeExceptionClass, allocatedMessage);
     free(allocatedMessage);
 }
 
 void netty_unix_errors_throwChannelExceptionErrorNo(JNIEnv* env, char* message, int errorNumber) {
     char* allocatedMessage = exceptionMessage(message, errorNumber);
+    if (allocatedMessage == NULL) {
+        return;
+    }
     (*env)->ThrowNew(env, channelExceptionClass, allocatedMessage);
     free(allocatedMessage);
 }
@@ -63,18 +70,23 @@ void netty_unix_errors_throwPortUnreachableException(JNIEnv* env, char* message)
 
 void netty_unix_errors_throwIOExceptionErrorNo(JNIEnv* env, char* message, int errorNumber) {
     char* allocatedMessage = exceptionMessage(message, errorNumber);
+    if (allocatedMessage == NULL) {
+        return;
+    }
     (*env)->ThrowNew(env, ioExceptionClass, allocatedMessage);
     free(allocatedMessage);
 }
 
 void netty_unix_errors_throwClosedChannelException(JNIEnv* env) {
     jobject exception = (*env)->NewObject(env, closedChannelExceptionClass, closedChannelExceptionMethodId);
+    if (exception == NULL) {
+        return;
+    }
     (*env)->Throw(env, exception);
 }
 
 void netty_unix_errors_throwOutOfMemoryError(JNIEnv* env) {
-    jclass exceptionClass = (*env)->FindClass(env, "java/lang/OutOfMemoryError");
-    (*env)->ThrowNew(env, exceptionClass, "");
+    (*env)->ThrowNew(env, oomErrorClass, "");
 }
 
 // JNI Registered Methods Begin
@@ -151,6 +163,7 @@ static const jint statically_referenced_fixed_method_table_size = sizeof(statica
 // JNI Method Registration Table End
 
 jint netty_unix_errors_JNI_OnLoad(JNIEnv* env, const char* packagePrefix) {
+    char* nettyClassName = NULL;
     // We must register the statically referenced methods first!
     if (netty_unix_util_register_natives(env,
             packagePrefix,
@@ -160,98 +173,33 @@ jint netty_unix_errors_JNI_OnLoad(JNIEnv* env, const char* packagePrefix) {
         return JNI_ERR;
     }
 
-    jclass localRuntimeExceptionClass = (*env)->FindClass(env, "java/lang/RuntimeException");
-    if (localRuntimeExceptionClass == NULL) {
-        // pending exception...
-        return JNI_ERR;
-    }
-    runtimeExceptionClass = (jclass) (*env)->NewGlobalRef(env, localRuntimeExceptionClass);
-    if (runtimeExceptionClass == NULL) {
-        // out-of-memory!
-        netty_unix_errors_throwOutOfMemoryError(env);
-        return JNI_ERR;
-    }
+    NETTY_LOAD_CLASS(env, oomErrorClass, "java/lang/OutOfMemoryError", error);
 
-    char* nettyClassName = netty_unix_util_prepend(packagePrefix, "io/netty/channel/ChannelException");
-    jclass localChannelExceptionClass = (*env)->FindClass(env, nettyClassName);
-    free(nettyClassName);
-    nettyClassName = NULL;
-    if (localChannelExceptionClass == NULL) {
-        // pending exception...
-        return JNI_ERR;
-    }
-    channelExceptionClass = (jclass) (*env)->NewGlobalRef(env, localChannelExceptionClass);
-    if (channelExceptionClass == NULL) {
-        // out-of-memory!
-        netty_unix_errors_throwOutOfMemoryError(env);
-        return JNI_ERR;
-    }
+    NETTY_LOAD_CLASS(env, runtimeExceptionClass, "java/lang/RuntimeException", error);
 
-    // cache classes that are used within other jni methods for performance reasons
-    jclass localClosedChannelExceptionClass = (*env)->FindClass(env, "java/nio/channels/ClosedChannelException");
-    if (localClosedChannelExceptionClass == NULL) {
-        // pending exception...
-        return JNI_ERR;
-    }
-    closedChannelExceptionClass = (jclass) (*env)->NewGlobalRef(env, localClosedChannelExceptionClass);
-    if (closedChannelExceptionClass == NULL) {
-        // out-of-memory!
-        netty_unix_errors_throwOutOfMemoryError(env);
-        return JNI_ERR;
-    }
-    closedChannelExceptionMethodId = (*env)->GetMethodID(env, closedChannelExceptionClass, "<init>", "()V");
-    if (closedChannelExceptionMethodId == NULL) {
-        netty_unix_errors_throwRuntimeException(env, "failed to get method ID: ClosedChannelException.<init>()");
-        return JNI_ERR;
-    }
+    NETTY_PREPEND(packagePrefix, "io/netty/channel/ChannelException", nettyClassName, error);
+    NETTY_LOAD_CLASS(env, channelExceptionClass, nettyClassName, error);
+    netty_unix_util_free_dynamic_name(&nettyClassName);
 
-    jclass localIoExceptionClass = (*env)->FindClass(env, "java/io/IOException");
-    if (localIoExceptionClass == NULL) {
-        // pending exception...
-        return JNI_ERR;
-    }
-    ioExceptionClass = (jclass) (*env)->NewGlobalRef(env, localIoExceptionClass);
-    if (ioExceptionClass == NULL) {
-        // out-of-memory!
-        netty_unix_errors_throwOutOfMemoryError(env);
-        return JNI_ERR;
-    }
+    NETTY_LOAD_CLASS(env, closedChannelExceptionClass, "java/nio/channels/ClosedChannelException", error);
+    NETTY_GET_METHOD(env, closedChannelExceptionClass, closedChannelExceptionMethodId, "<init>", "()V", error);
 
-    jclass localPortUnreachableExceptionClass = (*env)->FindClass(env, "java/net/PortUnreachableException");
-    if (localPortUnreachableExceptionClass == NULL) {
-        // pending exception...
-        return JNI_ERR;
-    }
-    portUnreachableExceptionClass = (jclass) (*env)->NewGlobalRef(env, localPortUnreachableExceptionClass);
-    if (portUnreachableExceptionClass == NULL) {
-        // out-of-memory!
-        netty_unix_errors_throwOutOfMemoryError(env);
-        return JNI_ERR;
-    }
+    NETTY_LOAD_CLASS(env, ioExceptionClass, "java/io/IOException", error);
+
+    NETTY_LOAD_CLASS(env, portUnreachableExceptionClass, "java/net/PortUnreachableException", error);
 
     return NETTY_JNI_VERSION;
+error:
+    free(nettyClassName);
+    return JNI_ERR;
 }
 
 void netty_unix_errors_JNI_OnUnLoad(JNIEnv* env) {
     // delete global references so the GC can collect them
-    if (runtimeExceptionClass != NULL) {
-        (*env)->DeleteGlobalRef(env, runtimeExceptionClass);
-        runtimeExceptionClass = NULL;
-    }
-    if (channelExceptionClass != NULL) {
-        (*env)->DeleteGlobalRef(env, channelExceptionClass);
-        channelExceptionClass = NULL;
-    }
-    if (ioExceptionClass != NULL) {
-        (*env)->DeleteGlobalRef(env, ioExceptionClass);
-        ioExceptionClass = NULL;
-    }
-    if (portUnreachableExceptionClass != NULL) {
-        (*env)->DeleteGlobalRef(env, portUnreachableExceptionClass);
-        portUnreachableExceptionClass = NULL;
-    }
-    if (closedChannelExceptionClass != NULL) {
-        (*env)->DeleteGlobalRef(env, closedChannelExceptionClass);
-        closedChannelExceptionClass = NULL;
-    }
+    NETTY_UNLOAD_CLASS(env, oomErrorClass);
+    NETTY_UNLOAD_CLASS(env, runtimeExceptionClass);
+    NETTY_UNLOAD_CLASS(env, channelExceptionClass);
+    NETTY_UNLOAD_CLASS(env, ioExceptionClass);
+    NETTY_UNLOAD_CLASS(env, portUnreachableExceptionClass);
+    NETTY_UNLOAD_CLASS(env, closedChannelExceptionClass);
 }
diff --git a/transport-native-unix-common/src/main/c/netty_unix_filedescriptor.c b/transport-native-unix-common/src/main/c/netty_unix_filedescriptor.c
index d13ffb0..7e08bb5 100644
--- a/transport-native-unix-common/src/main/c/netty_unix_filedescriptor.c
+++ b/transport-native-unix-common/src/main/c/netty_unix_filedescriptor.c
@@ -276,65 +276,41 @@ static const jint method_table_size = sizeof(method_table) / sizeof(method_table
 // JNI Method Registration Table End
 
 jint netty_unix_filedescriptor_JNI_OnLoad(JNIEnv* env, const char* packagePrefix) {
+    int ret = JNI_ERR;
+    void* mem = NULL;
     if (netty_unix_util_register_natives(env, packagePrefix, "io/netty/channel/unix/FileDescriptor", method_table, method_table_size) != 0) {
-        return JNI_ERR;
+        goto done;
     }
-    void* mem = malloc(1);
-    if (mem == NULL) {
-        netty_unix_errors_throwOutOfMemoryError(env);
-        return JNI_ERR;
+    if ((mem = malloc(1)) == NULL) {
+        goto done;
     }
     jobject directBuffer = (*env)->NewDirectByteBuffer(env, mem, 1);
     if (directBuffer == NULL) {
-        free(mem);
-
-        netty_unix_errors_throwOutOfMemoryError(env);
-        return JNI_ERR;
+        goto done;
     }
     if ((*env)->GetDirectBufferAddress(env, directBuffer) == NULL) {
-        free(mem);
-
-        netty_unix_errors_throwRuntimeException(env, "failed to get direct buffer address");
-        return JNI_ERR;
+        goto done;
     }
-
     jclass cls = (*env)->GetObjectClass(env, directBuffer);
-
+    if (cls == NULL) {
+        goto done;
+    }
+ 
     // Get the method id for Buffer.position() and Buffer.limit(). These are used as fallback if
     // it is not possible to obtain the position and limit using the fields directly.
-    posId = (*env)->GetMethodID(env, cls, "position", "()I");
-    if (posId == NULL) {
-        free(mem);
+    NETTY_GET_METHOD(env, cls, posId, "position", "()I", done);
+    NETTY_GET_METHOD(env, cls, limitId, "limit", "()I", done);
 
-        // position method was not found.. something is wrong so bail out
-        netty_unix_errors_throwRuntimeException(env, "failed to get method ID: ByteBuffer.position()");
-        return JNI_ERR;
-    }
-
-    limitId = (*env)->GetMethodID(env, cls, "limit", "()I");
-    if (limitId == NULL) {
-        free(mem);
-
-        // limit method was not found.. something is wrong so bail out
-        netty_unix_errors_throwRuntimeException(env, "failed to get method ID: ByteBuffer.limit()");
-        return JNI_ERR;
-    }
     // Try to get the ids of the position and limit fields. We later then check if we was able
     // to find them and if so use them get the position and limit of the buffer. This is
     // much faster then call back into java via (*env)->CallIntMethod(...).
-    posFieldId = (*env)->GetFieldID(env, cls, "position", "I");
-    if (posFieldId == NULL) {
-        // this is ok as we can still use the method so just clear the exception
-        (*env)->ExceptionClear(env);
-    }
-    limitFieldId = (*env)->GetFieldID(env, cls, "limit", "I");
-    if (limitFieldId == NULL) {
-        // this is ok as we can still use the method so just clear the exception
-        (*env)->ExceptionClear(env);
-    }
+    NETTY_TRY_GET_FIELD(env, cls, posFieldId, "position", "I");
+    NETTY_TRY_GET_FIELD(env, cls, limitFieldId, "limit", "I");
 
+    ret = NETTY_JNI_VERSION;
+done:
     free(mem);
-    return NETTY_JNI_VERSION;
+    return ret;
 }
 
 void netty_unix_filedescriptor_JNI_OnUnLoad(JNIEnv* env) { }
diff --git a/transport-native-unix-common/src/main/c/netty_unix_socket.c b/transport-native-unix-common/src/main/c/netty_unix_socket.c
index 3dd0920..3f04f4b 100644
--- a/transport-native-unix-common/src/main/c/netty_unix_socket.c
+++ b/transport-native-unix-common/src/main/c/netty_unix_socket.c
@@ -16,6 +16,7 @@
 #include <fcntl.h>
 #include <errno.h>
 #include <unistd.h>
+#include <stddef.h>
 #include <stdint.h>
 #include <stdlib.h>
 #include <string.h>
@@ -41,16 +42,16 @@ static jmethodID datagramSocketAddrMethodId = NULL;
 static jmethodID inetSocketAddrMethodId = NULL;
 static jclass inetSocketAddressClass = NULL;
 static int socketType = AF_INET;
-static const char* ip4prefix = "::ffff:";
 static const unsigned char wildcardAddress[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
 static const unsigned char ipv4MappedWildcardAddress[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00 };
+static const unsigned char ipv4MappedAddress[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff };
 
 // Optional external methods
 extern int accept4(int sockFd, struct sockaddr* addr, socklen_t* addrlen, int flags) __attribute__((weak)) __attribute__((weak_import));
 
 // macro to calculate the length of a sockaddr_un struct for a given path length.
 // see sys/un.h#SUN_LEN, this is modified to allow nul bytes
-#define _UNIX_ADDR_LENGTH(path_len) (uintptr_t) (((struct sockaddr_un *) 0)->sun_path) + path_len
+#define _UNIX_ADDR_LENGTH(path_len) ((uintptr_t) offsetof(struct sockaddr_un, sun_path) + (uintptr_t) path_len)
 
 static int nettyNonBlockingSocket(int domain, int type, int protocol) {
 #ifdef SOCK_NONBLOCK
@@ -68,47 +69,61 @@ static int nettyNonBlockingSocket(int domain, int type, int protocol) {
 #endif
 }
 
+int netty_unix_socket_ipAddressLength(const struct sockaddr_storage* addr) {
+    if (addr->ss_family == AF_INET) {
+        return 4;
+    }
+    struct sockaddr_in6* s = (struct sockaddr_in6*) addr;
+    if (memcmp(s->sin6_addr.s6_addr, ipv4MappedAddress, 12) == 0) {
+         // IPv4-mapped-on-IPv6
+         return 4;
+    }
+    return 16;
+}
+
 static jobject createDatagramSocketAddress(JNIEnv* env, const struct sockaddr_storage* addr, int len, jobject local) {
-    char ipstr[INET6_ADDRSTRLEN];
     int port;
-    jstring ipString;
+    int scopeId;
+    int ipLength = netty_unix_socket_ipAddressLength(addr);
+    jbyteArray addressBytes = (*env)->NewByteArray(env, ipLength);
+    if (addressBytes == NULL) {
+        return NULL;
+    }
     if (addr->ss_family == AF_INET) {
         struct sockaddr_in* s = (struct sockaddr_in*) addr;
         port = ntohs(s->sin_port);
-        inet_ntop(AF_INET, &s->sin_addr, ipstr, sizeof ipstr);
-        ipString = (*env)->NewStringUTF(env, ipstr);
+        scopeId = 0;
+        (*env)->SetByteArrayRegion(env, addressBytes, 0, ipLength, (jbyte*) &s->sin_addr.s_addr);
     } else {
         struct sockaddr_in6* s = (struct sockaddr_in6*) addr;
         port = ntohs(s->sin6_port);
-        inet_ntop(AF_INET6, &s->sin6_addr, ipstr, sizeof ipstr);
+        scopeId = s->sin6_scope_id;
 
-        if (strncasecmp(ipstr, ip4prefix, 7) == 0) {
+        int offset;
+        if (ipLength == 4) {
             // IPv4-mapped-on-IPv6.
-            // Cut of ::ffff: prefix to workaround performance issues when parsing these
-            // addresses in InetAddress.getByName(...).
-            //
-            // See https://github.com/netty/netty/issues/2867
-            ipString = (*env)->NewStringUTF(env, &ipstr[7]);
+            offset = 12;
         } else {
-            ipString = (*env)->NewStringUTF(env, ipstr);
+            offset = 0;
         }
+        jbyte* addr = (jbyte*) &s->sin6_addr.s6_addr;
+        (*env)->SetByteArrayRegion(env, addressBytes, 0, ipLength, addr + offset);
+    }
+    jobject obj = (*env)->NewObject(env, datagramSocketAddressClass, datagramSocketAddrMethodId, addressBytes, scopeId, port, len, local);
+    if ((*env)->ExceptionCheck(env) == JNI_TRUE) {
+        return NULL;
     }
-    jobject socketAddr = (*env)->NewObject(env, datagramSocketAddressClass, datagramSocketAddrMethodId, ipString, port, len, local);
-    return socketAddr;
+    return obj;
 }
 
 static jsize addressLength(const struct sockaddr_storage* addr) {
-    if (addr->ss_family == AF_INET) {
-        return 8;
-    }
-    struct sockaddr_in6* s = (struct sockaddr_in6*) addr;
-    if (s->sin6_addr.s6_addr[11] == 0xff && s->sin6_addr.s6_addr[10] == 0xff &&
-        s->sin6_addr.s6_addr[9] == 0x00 && s->sin6_addr.s6_addr[8] == 0x00 && s->sin6_addr.s6_addr[7] == 0x00 && s->sin6_addr.s6_addr[6] == 0x00 && s->sin6_addr.s6_addr[5] == 0x00 &&
-        s->sin6_addr.s6_addr[4] == 0x00 && s->sin6_addr.s6_addr[3] == 0x00 && s->sin6_addr.s6_addr[2] == 0x00 && s->sin6_addr.s6_addr[1] == 0x00 && s->sin6_addr.s6_addr[0] == 0x00) {
-        // IPv4-mapped-on-IPv6
-        return 8;
+    int len = netty_unix_socket_ipAddressLength(addr);
+    if (len == 4) {
+        // Only encode port into it
+        return len + 4;
     }
-    return 24;
+    // we encode port + scope into it
+    return len + 8;
 }
 
 static void initInetSocketAddressArray(JNIEnv* env, const struct sockaddr_storage* addr, jbyteArray bArray, int offset, jsize len) {
@@ -159,10 +174,12 @@ static void initInetSocketAddressArray(JNIEnv* env, const struct sockaddr_storag
     }
 }
 
-static jbyteArray createInetSocketAddressArray(JNIEnv* env, const struct sockaddr_storage* addr) {
+jbyteArray netty_unix_socket_createInetSocketAddressArray(JNIEnv* env, const struct sockaddr_storage* addr) {
     jsize len = addressLength(addr);
     jbyteArray bArray = (*env)->NewByteArray(env, len);
-
+    if (bArray == NULL) {
+        return NULL;
+    }
     initInetSocketAddressArray(env, addr, bArray, 0, len);
     return bArray;
 }
@@ -190,6 +207,22 @@ static void netty_unix_socket_initialize(JNIEnv* env, jclass clazz, jboolean ipv
     }
 }
 
+static jboolean netty_unix_socket_isIPv6Preferred(JNIEnv* env, jclass clazz) {
+    return socketType == AF_INET6;
+}
+
+
+static jboolean netty_unix_socket_isIPv6(JNIEnv* env, jclass clazz, jint fd) {
+    struct sockaddr_storage addr;
+    socklen_t addrlen = sizeof(addr);
+    if (getsockname(fd, (struct sockaddr*) &addr, &addrlen) == 0) {
+        return ((struct sockaddr*) &addr)->sa_family == AF_INET6;
+    }
+
+    netty_unix_errors_throwChannelExceptionErrorNo(env, "getsockname(...) failed: ", errno);
+    return JNI_FALSE;
+}
+
 static void netty_unix_socket_optionHandleError(JNIEnv* env, int err, char* method) {
     if (err == EBADF) {
         netty_unix_errors_throwClosedChannelException(env);
@@ -206,11 +239,11 @@ static int netty_unix_socket_setOption0(jint fd, int level, int optname, const v
     return setsockopt(fd, level, optname, optval, len);
 }
 
-static jint _socket(JNIEnv* env, jclass clazz, int type) {
-    int fd = nettyNonBlockingSocket(socketType, type, 0);
+static jint _socket(JNIEnv* env, jclass clazz, int domain, int type) {
+    int fd = nettyNonBlockingSocket(domain, type, 0);
     if (fd == -1) {
         return -errno;
-    } else if (socketType == AF_INET6) {
+    } else if (domain == AF_INET6) {
         // Try to allow listen /connect ipv4 and ipv6
         int optval = 0;
         if (netty_unix_socket_setOption0(fd, IPPROTO_IPV6, IPV6_V6ONLY, &optval, sizeof(optval)) < 0) {
@@ -229,20 +262,29 @@ static jint _socket(JNIEnv* env, jclass clazz, int type) {
     return fd;
 }
 
-int netty_unix_socket_initSockaddr(JNIEnv* env, jbyteArray address, jint scopeId, jint jport,
+int netty_unix_socket_initSockaddr(JNIEnv* env, jboolean ipv6, jbyteArray address, jint scopeId, jint jport,
                                    const struct sockaddr_storage* addr, socklen_t* addrSize) {
     uint16_t port = htons((uint16_t) jport);
+    // We use 16 bytes as this allows us to fit ipv6, ipv4 and ipv4 mapped ipv6 addresses in the array.
+    jbyte addressBytes[16];
 
-    // Use GetPrimitiveArrayCritical and ReleasePrimitiveArrayCritical to signal the VM that we really would like
-    // to not do a memory copy here. This is ok as we not do any blocking action here anyway.
-    // This is important as the VM may suspend GC for the time!
-    jbyte* addressBytes = (*env)->GetPrimitiveArrayCritical(env, address, 0);
-    if (addressBytes == NULL) {
-        // No memory left ?!?!?
-        netty_unix_errors_throwOutOfMemoryError(env);
+    int len = (*env)->GetArrayLength(env, address);
+
+    if (len > 16) {
+        // This should never happen but let's guard against it anyway.
         return -1;
     }
-    if (socketType == AF_INET6) {
+
+    // We use GetByteArrayRegion(...) and copy into a small stack allocated buffer and NOT GetPrimitiveArrayCritical(...)
+    // as there are still multiple GCLocker related bugs which are not fixed yet.
+    //
+    // For example:
+    //     https://bugs.openjdk.java.net/browse/JDK-8048556
+    //     https://bugs.openjdk.java.net/browse/JDK-8057573
+    //     https://bugs.openjdk.java.net/browse/JDK-8057586
+    (*env)->GetByteArrayRegion(env, address, 0, len, addressBytes);
+
+    if (ipv6 == JNI_TRUE) {
         struct sockaddr_in6* ip6addr = (struct sockaddr_in6*) addr;
         *addrSize = sizeof(struct sockaddr_in6);
         ip6addr->sin6_family = AF_INET6;
@@ -262,15 +304,13 @@ int netty_unix_socket_initSockaddr(JNIEnv* env, jbyteArray address, jint scopeId
         ipaddr->sin_port = port;
         memcpy(&(ipaddr->sin_addr.s_addr), addressBytes + 12, 4);
     }
-
-    (*env)->ReleasePrimitiveArrayCritical(env, address, addressBytes, JNI_ABORT);
     return 0;
 }
 
-static jint _sendTo(JNIEnv* env, jint fd, void* buffer, jint pos, jint limit, jbyteArray address, jint scopeId, jint port) {
+static jint _sendTo(JNIEnv* env, jint fd, jboolean ipv6, void* buffer, jint pos, jint limit, jbyteArray address, jint scopeId, jint port) {
     struct sockaddr_storage addr;
     socklen_t addrSize;
-    if (netty_unix_socket_initSockaddr(env, address, scopeId, port, &addr, &addrSize) == -1) {
+    if (netty_unix_socket_initSockaddr(env, ipv6, address, scopeId, port, &addr, &addrSize) == -1) {
         return -1;
     }
 
@@ -350,11 +390,17 @@ static jobject _recvFrom(JNIEnv* env, jint fd, void* buffer, jint pos, jint limi
     }
 
 #ifdef IP_RECVORIGDSTADDR
+#if !defined(SOL_IP) && defined(IPPROTO_IP)
+#define SOL_IP IPPROTO_IP
+#endif /* !SOL_IP && IPPROTO_IP */
     if (readLocalAddr) {
         for (cmsg = CMSG_FIRSTHDR(&msg); cmsg != NULL; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
             if (cmsg->cmsg_level == SOL_IP && cmsg->cmsg_type == IP_RECVORIGDSTADDR) {
                 memcpy (&daddr, CMSG_DATA(cmsg), sizeof (struct sockaddr_storage));
                 local = createDatagramSocketAddress(env, &daddr, res, NULL);
+                if (local == NULL) {
+                    return NULL;
+                }
                 break;
             }
         }
@@ -405,10 +451,10 @@ static jint netty_unix_socket_shutdown(JNIEnv* env, jclass clazz, jint fd, jbool
     return 0;
 }
 
-static jint netty_unix_socket_bind(JNIEnv* env, jclass clazz, jint fd, jbyteArray address, jint scopeId, jint port) {
+static jint netty_unix_socket_bind(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6, jbyteArray address, jint scopeId, jint port) {
     struct sockaddr_storage addr;
     socklen_t addrSize;
-    if (netty_unix_socket_initSockaddr(env, address, scopeId, port, &addr, &addrSize) == -1) {
+    if (netty_unix_socket_initSockaddr(env, ipv6, address, scopeId, port, &addr, &addrSize) == -1) {
         return -1;
     }
 
@@ -425,10 +471,10 @@ static jint netty_unix_socket_listen(JNIEnv* env, jclass clazz, jint fd, jint ba
     return 0;
 }
 
-static jint netty_unix_socket_connect(JNIEnv* env, jclass clazz, jint fd, jbyteArray address, jint scopeId, jint port) {
+static jint netty_unix_socket_connect(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6, jbyteArray address, jint scopeId, jint port) {
     struct sockaddr_storage addr;
     socklen_t addrSize;
-    if (netty_unix_socket_initSockaddr(env, address, scopeId, port, &addr, &addrSize) == -1) {
+    if (netty_unix_socket_initSockaddr(env, ipv6, address, scopeId, port, &addr, &addrSize) == -1) {
         // A runtime exception was thrown
         return -1;
     }
@@ -463,7 +509,7 @@ static jint netty_unix_socket_finishConnect(JNIEnv* env, jclass clazz, jint fd)
     return -optval;
 }
 
-static jint netty_unix_socket_disconnect(JNIEnv* env, jclass clazz, jint fd) {
+static jint netty_unix_socket_disconnect(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6) {
     struct sockaddr_storage addr;
     int len;
 
@@ -471,7 +517,7 @@ static jint netty_unix_socket_disconnect(JNIEnv* env, jclass clazz, jint fd) {
 
     // You can disconnect connection-less sockets by using AF_UNSPEC.
     // See man 2 connect.
-    if (socketType == AF_INET6) {
+    if (ipv6 == JNI_TRUE) {
         struct sockaddr_in6* ip6addr = (struct sockaddr_in6*) &addr;
         ip6addr->sin6_family = AF_UNSPEC;
         len = sizeof(struct sockaddr_in6);
@@ -498,6 +544,7 @@ static jint netty_unix_socket_disconnect(JNIEnv* env, jclass clazz, jint fd) {
 static jint netty_unix_socket_accept(JNIEnv* env, jclass clazz, jint fd, jbyteArray acceptedAddress) {
     jint socketFd;
     jsize len;
+    jbyte len_b;
     int err;
     struct sockaddr_storage addr;
     socklen_t address_len = sizeof(addr);
@@ -522,9 +569,10 @@ static jint netty_unix_socket_accept(JNIEnv* env, jclass clazz, jint fd, jbyteAr
     }
 
     len = addressLength(&addr);
+    len_b = (jbyte) len;
 
     // Fill in remote address details
-    (*env)->SetByteArrayRegion(env, acceptedAddress, 0, 4, (jbyte*) &len);
+    (*env)->SetByteArrayRegion(env, acceptedAddress, 0, 1, (jbyte*) &len_b);
     initInetSocketAddressArray(env, &addr, acceptedAddress, 1, len);
 
     if (accept4)  {
@@ -543,7 +591,7 @@ static jbyteArray netty_unix_socket_remoteAddress(JNIEnv* env, jclass clazz, jin
     if (getpeername(fd, (struct sockaddr*) &addr, &len) == -1) {
         return NULL;
     }
-    return createInetSocketAddressArray(env, &addr);
+    return netty_unix_socket_createInetSocketAddressArray(env, &addr);
 }
 
 static jbyteArray netty_unix_socket_localAddress(JNIEnv* env, jclass clazz, jint fd) {
@@ -552,15 +600,17 @@ static jbyteArray netty_unix_socket_localAddress(JNIEnv* env, jclass clazz, jint
     if (getsockname(fd, (struct sockaddr*) &addr, &len) == -1) {
         return NULL;
     }
-    return createInetSocketAddressArray(env, &addr);
+    return netty_unix_socket_createInetSocketAddressArray(env, &addr);
 }
 
-static jint netty_unix_socket_newSocketDgramFd(JNIEnv* env, jclass clazz) {
-    return _socket(env, clazz, SOCK_DGRAM);
+static jint netty_unix_socket_newSocketDgramFd(JNIEnv* env, jclass clazz, jboolean ipv6) {
+    int domain = ipv6 == JNI_TRUE ? AF_INET6 : AF_INET;
+    return _socket(env, clazz, domain, SOCK_DGRAM);
 }
 
-static jint netty_unix_socket_newSocketStreamFd(JNIEnv* env, jclass clazz) {
-    return _socket(env, clazz, SOCK_STREAM);
+static jint netty_unix_socket_newSocketStreamFd(JNIEnv* env, jclass clazz, jboolean ipv6) {
+    int domain = ipv6 == JNI_TRUE ? AF_INET6 : AF_INET;
+    return _socket(env, clazz, domain, SOCK_STREAM);
 }
 
 static jint netty_unix_socket_newSocketDomainFd(JNIEnv* env, jclass clazz) {
@@ -571,19 +621,19 @@ static jint netty_unix_socket_newSocketDomainFd(JNIEnv* env, jclass clazz) {
     return fd;
 }
 
-static jint netty_unix_socket_sendTo(JNIEnv* env, jclass clazz, jint fd, jobject jbuffer, jint pos, jint limit, jbyteArray address, jint scopeId, jint port) {
+static jint netty_unix_socket_sendTo(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6, jobject jbuffer, jint pos, jint limit, jbyteArray address, jint scopeId, jint port) {
     // We check that GetDirectBufferAddress will not return NULL in OnLoad
-    return _sendTo(env, fd, (*env)->GetDirectBufferAddress(env, jbuffer), pos, limit, address, scopeId, port);
+    return _sendTo(env, fd, ipv6, (*env)->GetDirectBufferAddress(env, jbuffer), pos, limit, address, scopeId, port);
 }
 
-static jint netty_unix_socket_sendToAddress(JNIEnv* env, jclass clazz, jint fd, jlong memoryAddress, jint pos, jint limit, jbyteArray address, jint scopeId, jint port) {
-    return _sendTo(env, fd, (void *) (intptr_t) memoryAddress, pos, limit, address, scopeId, port);
+static jint netty_unix_socket_sendToAddress(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6, jlong memoryAddress, jint pos, jint limit, jbyteArray address, jint scopeId, jint port) {
+    return _sendTo(env, fd, ipv6, (void *) (intptr_t) memoryAddress, pos, limit, address, scopeId, port);
 }
 
-static jint netty_unix_socket_sendToAddresses(JNIEnv* env, jclass clazz, jint fd, jlong memoryAddress, jint length, jbyteArray address, jint scopeId, jint port) {
+static jint netty_unix_socket_sendToAddresses(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6, jlong memoryAddress, jint length, jbyteArray address, jint scopeId, jint port) {
     struct sockaddr_storage addr;
     socklen_t addrSize;
-    if (netty_unix_socket_initSockaddr(env, address, scopeId, port, &addr, &addrSize) == -1) {
+    if (netty_unix_socket_initSockaddr(env, ipv6, address, scopeId, port, &addr, &addrSize) == -1) {
         return -1;
     }
 
@@ -793,8 +843,8 @@ static void netty_unix_socket_setSoLinger(JNIEnv* env, jclass clazz, jint fd, ji
     netty_unix_socket_setOption(env, fd, SOL_SOCKET, SO_LINGER, &solinger, sizeof(solinger));
 }
 
-static void netty_unix_socket_setTrafficClass(JNIEnv* env, jclass clazz, jint fd, jint optval) {
-    if (socketType == AF_INET6) {
+static void netty_unix_socket_setTrafficClass(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6, jint optval) {
+    if (ipv6 == JNI_TRUE) {
         // This call will put an exception on the stack to be processed once the JNI calls completes if
         // setsockopt failed and return a negative value.
         int rc = netty_unix_socket_setOption(env, fd, IPPROTO_IPV6, IPV6_TCLASS, &optval, sizeof(optval));
@@ -860,9 +910,9 @@ static jint netty_unix_socket_getSoLinger(JNIEnv* env, jclass clazz, jint fd) {
     }
 }
 
-static jint netty_unix_socket_getTrafficClass(JNIEnv* env, jclass clazz, jint fd) {
+static jint netty_unix_socket_getTrafficClass(JNIEnv* env, jclass clazz, jint fd, jboolean ipv6) {
     int optval;
-    if (socketType == AF_INET6) {
+    if (ipv6 == JNI_TRUE) {
         if (netty_unix_socket_getOption0(fd, IPPROTO_IPV6, IPV6_TCLASS, &optval, sizeof(optval)) == -1) {
             if (errno == ENOPROTOOPT) {
                 if (netty_unix_socket_getOption(env, fd, IPPROTO_IP, IP_TOS, &optval, sizeof(optval)) == -1) {
@@ -919,20 +969,20 @@ static jint netty_unix_socket_isBroadcast(JNIEnv* env, jclass clazz, jint fd) {
 // JNI Method Registration Table Begin
 static const JNINativeMethod fixed_method_table[] = {
   { "shutdown", "(IZZ)I", (void *) netty_unix_socket_shutdown },
-  { "bind", "(I[BII)I", (void *) netty_unix_socket_bind },
+  { "bind", "(IZ[BII)I", (void *) netty_unix_socket_bind },
   { "listen", "(II)I", (void *) netty_unix_socket_listen },
-  { "connect", "(I[BII)I", (void *) netty_unix_socket_connect },
+  { "connect", "(IZ[BII)I", (void *) netty_unix_socket_connect },
   { "finishConnect", "(I)I", (void *) netty_unix_socket_finishConnect },
-  { "disconnect", "(I)I", (void *) netty_unix_socket_disconnect},
+  { "disconnect", "(IZ)I", (void *) netty_unix_socket_disconnect},
   { "accept", "(I[B)I", (void *) netty_unix_socket_accept },
   { "remoteAddress", "(I)[B", (void *) netty_unix_socket_remoteAddress },
   { "localAddress", "(I)[B", (void *) netty_unix_socket_localAddress },
-  { "newSocketDgramFd", "()I", (void *) netty_unix_socket_newSocketDgramFd },
-  { "newSocketStreamFd", "()I", (void *) netty_unix_socket_newSocketStreamFd },
+  { "newSocketDgramFd", "(Z)I", (void *) netty_unix_socket_newSocketDgramFd },
+  { "newSocketStreamFd", "(Z)I", (void *) netty_unix_socket_newSocketStreamFd },
   { "newSocketDomainFd", "()I", (void *) netty_unix_socket_newSocketDomainFd },
-  { "sendTo", "(ILjava/nio/ByteBuffer;II[BII)I", (void *) netty_unix_socket_sendTo },
-  { "sendToAddress", "(IJII[BII)I", (void *) netty_unix_socket_sendToAddress },
-  { "sendToAddresses", "(IJI[BII)I", (void *) netty_unix_socket_sendToAddresses },
+  { "sendTo", "(IZLjava/nio/ByteBuffer;II[BII)I", (void *) netty_unix_socket_sendTo },
+  { "sendToAddress", "(IZJII[BII)I", (void *) netty_unix_socket_sendToAddress },
+  { "sendToAddresses", "(IZJI[BII)I", (void *) netty_unix_socket_sendToAddresses },
   // "recvFrom" has a dynamic signature
   // "recvFromAddress" has a dynamic signature
   { "recvFd", "(I)I", (void *) netty_unix_socket_recvFd },
@@ -947,7 +997,7 @@ static const JNINativeMethod fixed_method_table[] = {
   { "setSendBufferSize", "(II)V", (void *) netty_unix_socket_setSendBufferSize },
   { "setKeepAlive", "(II)V", (void *) netty_unix_socket_setKeepAlive },
   { "setSoLinger", "(II)V", (void *) netty_unix_socket_setSoLinger },
-  { "setTrafficClass", "(II)V", (void *) netty_unix_socket_setTrafficClass },
+  { "setTrafficClass", "(IZI)V", (void *) netty_unix_socket_setTrafficClass },
   { "isKeepAlive", "(I)I", (void *) netty_unix_socket_isKeepAlive },
   { "isTcpNoDelay", "(I)I", (void *) netty_unix_socket_isTcpNoDelay },
   { "isBroadcast", "(I)I", (void *) netty_unix_socket_isBroadcast },
@@ -956,9 +1006,11 @@ static const JNINativeMethod fixed_method_table[] = {
   { "getReceiveBufferSize", "(I)I", (void *) netty_unix_socket_getReceiveBufferSize },
   { "getSendBufferSize", "(I)I", (void *) netty_unix_socket_getSendBufferSize },
   { "getSoLinger", "(I)I", (void *) netty_unix_socket_getSoLinger },
-  { "getTrafficClass", "(I)I", (void *) netty_unix_socket_getTrafficClass },
+  { "getTrafficClass", "(IZ)I", (void *) netty_unix_socket_getTrafficClass },
   { "getSoError", "(I)I", (void *) netty_unix_socket_getSoError },
-  { "initialize", "(Z)V", (void *) netty_unix_socket_initialize }
+  { "initialize", "(Z)V", (void *) netty_unix_socket_initialize },
+  { "isIPv6Preferred", "()Z", (void *) netty_unix_socket_isIPv6Preferred },
+  { "isIPv6", "(I)Z", (void *) netty_unix_socket_isIPv6 }
 };
 static const jint fixed_method_table_size = sizeof(fixed_method_table) / sizeof(fixed_method_table[0]);
 
@@ -967,128 +1019,87 @@ static jint dynamicMethodsTableSize() {
 }
 
 static JNINativeMethod* createDynamicMethodsTable(const char* packagePrefix) {
-    JNINativeMethod* dynamicMethods = malloc(sizeof(JNINativeMethod) * dynamicMethodsTableSize());
+    char* dynamicTypeName = NULL;
+    size_t size = sizeof(JNINativeMethod) * dynamicMethodsTableSize();
+    JNINativeMethod* dynamicMethods = malloc(size);
+    if (dynamicMethods == NULL) {
+        return NULL;
+    }
+    memset(dynamicMethods, 0, size);
     memcpy(dynamicMethods, fixed_method_table, sizeof(fixed_method_table));
-    char* dynamicTypeName = netty_unix_util_prepend(packagePrefix, "io/netty/channel/unix/DatagramSocketAddress;");
+
     JNINativeMethod* dynamicMethod = &dynamicMethods[fixed_method_table_size];
+    NETTY_PREPEND(packagePrefix, "io/netty/channel/unix/DatagramSocketAddress;", dynamicTypeName, error);
+    NETTY_PREPEND("(ILjava/nio/ByteBuffer;II)L", dynamicTypeName,  dynamicMethod->signature, error);
     dynamicMethod->name = "recvFrom";
-    dynamicMethod->signature = netty_unix_util_prepend("(ILjava/nio/ByteBuffer;II)L", dynamicTypeName);
     dynamicMethod->fnPtr = (void *) netty_unix_socket_recvFrom;
-    free(dynamicTypeName);
+    netty_unix_util_free_dynamic_name(&dynamicTypeName);
 
     ++dynamicMethod;
-    dynamicTypeName = netty_unix_util_prepend(packagePrefix, "io/netty/channel/unix/DatagramSocketAddress;");
+    NETTY_PREPEND(packagePrefix, "io/netty/channel/unix/DatagramSocketAddress;", dynamicTypeName, error);
+    NETTY_PREPEND("(IJII)L", dynamicTypeName,  dynamicMethod->signature, error);
     dynamicMethod->name = "recvFromAddress";
-    dynamicMethod->signature = netty_unix_util_prepend("(IJII)L", dynamicTypeName);
     dynamicMethod->fnPtr = (void *) netty_unix_socket_recvFromAddress;
-    free(dynamicTypeName);
+    netty_unix_util_free_dynamic_name(&dynamicTypeName);
 
     return dynamicMethods;
+error:
+    free(dynamicTypeName);
+    netty_unix_util_free_dynamic_methods_table(dynamicMethods, fixed_method_table_size, dynamicMethodsTableSize());
+    return NULL;
 }
 
-static void freeDynamicMethodsTable(JNINativeMethod* dynamicMethods) {
-    jint fullMethodTableSize = dynamicMethodsTableSize();
-    jint i = fixed_method_table_size;
-    for (; i < fullMethodTableSize; ++i) {
-        free(dynamicMethods[i].signature);
-    }
-    free(dynamicMethods);
-}
 // JNI Method Registration Table End
 
 jint netty_unix_socket_JNI_OnLoad(JNIEnv* env, const char* packagePrefix) {
+    int ret = JNI_ERR;
+    char* nettyClassName = NULL;
+    void* mem = NULL;
     JNINativeMethod* dynamicMethods = createDynamicMethodsTable(packagePrefix);
+    if (dynamicMethods == NULL) {
+        goto done;
+    }
     if (netty_unix_util_register_natives(env,
             packagePrefix,
             "io/netty/channel/unix/Socket",
             dynamicMethods,
             dynamicMethodsTableSize()) != 0) {
-        freeDynamicMethodsTable(dynamicMethods);
-        return JNI_ERR;
-    }
-    freeDynamicMethodsTable(dynamicMethods);
-    dynamicMethods = NULL;
-    char* nettyClassName = netty_unix_util_prepend(packagePrefix, "io/netty/channel/unix/DatagramSocketAddress");
-    jclass localDatagramSocketAddressClass = (*env)->FindClass(env, nettyClassName);
-    if (localDatagramSocketAddressClass == NULL) {
-        free(nettyClassName);
-        nettyClassName = NULL;
-        // pending exception...
-        return JNI_ERR;
-    }
-    datagramSocketAddressClass = (jclass) (*env)->NewGlobalRef(env, localDatagramSocketAddressClass);
-    if (datagramSocketAddressClass == NULL) {
-        free(nettyClassName);
-        nettyClassName = NULL;
-        // out-of-memory!
-        netty_unix_errors_throwOutOfMemoryError(env);
-        return JNI_ERR;
+        goto done;
     }
+  
+    NETTY_PREPEND(packagePrefix, "io/netty/channel/unix/DatagramSocketAddress", nettyClassName, done);
+    NETTY_LOAD_CLASS(env, datagramSocketAddressClass, nettyClassName, done);
 
     // Respect shading...
     char parameters[1024] = {0};
-    snprintf(parameters, sizeof(parameters), "(Ljava/lang/String;IIL%s;)V", nettyClassName);
+    snprintf(parameters, sizeof(parameters), "([BIIIL%s;)V", nettyClassName);
+    netty_unix_util_free_dynamic_name(&nettyClassName);
+    NETTY_GET_METHOD(env, datagramSocketAddressClass, datagramSocketAddrMethodId, "<init>", parameters, done);
 
-    datagramSocketAddrMethodId = (*env)->GetMethodID(env, datagramSocketAddressClass, "<init>", parameters);
-    if (datagramSocketAddrMethodId == NULL) {
-        char msg[1024] = {0};
-        snprintf(msg, sizeof(msg), "failed to get method ID: %s.<init>(String, int, int, %s)", nettyClassName, nettyClassName);
-        free(nettyClassName);
-        nettyClassName = NULL;
-        netty_unix_errors_throwRuntimeException(env, msg);
-        return JNI_ERR;
-    }
-
-    free(nettyClassName);
-    nettyClassName = NULL;
+    NETTY_LOAD_CLASS(env, inetSocketAddressClass, "java/net/InetSocketAddress", done);
+    NETTY_GET_METHOD(env, inetSocketAddressClass, inetSocketAddrMethodId, "<init>", "(Ljava/lang/String;I)V", done);
 
-    jclass localInetSocketAddressClass = (*env)->FindClass(env, "java/net/InetSocketAddress");
-    if (localInetSocketAddressClass == NULL) {
-        // pending exception...
-        return JNI_ERR;
-    }
-    inetSocketAddressClass = (jclass) (*env)->NewGlobalRef(env, localInetSocketAddressClass);
-    if (inetSocketAddressClass == NULL) {
-        // out-of-memory!
-        netty_unix_errors_throwOutOfMemoryError(env);
-        return JNI_ERR;
-    }
-    inetSocketAddrMethodId = (*env)->GetMethodID(env, inetSocketAddressClass, "<init>", "(Ljava/lang/String;I)V");
-    if (inetSocketAddrMethodId == NULL) {
-        netty_unix_errors_throwRuntimeException(env, "failed to get method ID: InetSocketAddress.<init>(String, int)");
-        return JNI_ERR;
+    if ((mem = malloc(1)) == NULL) {
+        goto done;
     }
 
-    void* mem = malloc(1);
-    if (mem == NULL) {
-        netty_unix_errors_throwOutOfMemoryError(env);
-        return JNI_ERR;
-    }
     jobject directBuffer = (*env)->NewDirectByteBuffer(env, mem, 1);
     if (directBuffer == NULL) {
-        free(mem);
-
-        netty_unix_errors_throwOutOfMemoryError(env);
-        return JNI_ERR;
+        goto done;
     }
     if ((*env)->GetDirectBufferAddress(env, directBuffer) == NULL) {
-        free(mem);
-
-        netty_unix_errors_throwRuntimeException(env, "failed to get direct buffer address");
-        return JNI_ERR;
+        goto done;
     }
-    free(mem);
 
-    return NETTY_JNI_VERSION;
+    ret = NETTY_JNI_VERSION;
+done:
+    netty_unix_util_free_dynamic_methods_table(dynamicMethods, fixed_method_table_size, dynamicMethodsTableSize());
+    free(nettyClassName);
+    free(mem);
+    return ret;
 }
 
 void netty_unix_socket_JNI_OnUnLoad(JNIEnv* env) {
-    if (datagramSocketAddressClass != NULL) {
-        (*env)->DeleteGlobalRef(env, datagramSocketAddressClass);
-        datagramSocketAddressClass = NULL;
-    }
-    if (inetSocketAddressClass != NULL) {
-        (*env)->DeleteGlobalRef(env, inetSocketAddressClass);
-        inetSocketAddressClass = NULL;
-    }
+    NETTY_UNLOAD_CLASS(env, datagramSocketAddressClass);
+    NETTY_UNLOAD_CLASS(env, inetSocketAddressClass);
 }
diff --git a/transport-native-unix-common/src/main/c/netty_unix_socket.h b/transport-native-unix-common/src/main/c/netty_unix_socket.h
index 4c60f6d..96d8794 100644
--- a/transport-native-unix-common/src/main/c/netty_unix_socket.h
+++ b/transport-native-unix-common/src/main/c/netty_unix_socket.h
@@ -20,9 +20,12 @@
 #include <jni.h>
 
 // External C methods
-int netty_unix_socket_initSockaddr(JNIEnv* env, jbyteArray address, jint scopeId, jint jport, const struct sockaddr_storage* addr, socklen_t* addrSize);
+int netty_unix_socket_initSockaddr(JNIEnv* env, jboolean ipv6, jbyteArray address, jint scopeId, jint jport, const struct sockaddr_storage* addr, socklen_t* addrSize);
+jbyteArray netty_unix_socket_createInetSocketAddressArray(JNIEnv* env, const struct sockaddr_storage* addr);
+
 int netty_unix_socket_getOption(JNIEnv* env, jint fd, int level, int optname, void* optval, socklen_t optlen);
 int netty_unix_socket_setOption(JNIEnv* env, jint fd, int level, int optname, const void* optval, socklen_t len);
+int netty_unix_socket_ipAddressLength(const struct sockaddr_storage* addr);
 
 // These method is sometimes needed if you want to special handle some errno value before throwing an exception.
 int netty_unix_socket_getOption0(jint fd, int level, int optname, void* optval, socklen_t optlen);
diff --git a/transport-native-unix-common/src/main/c/netty_unix_util.c b/transport-native-unix-common/src/main/c/netty_unix_util.c
index f21d4c6..3d7bda8 100644
--- a/transport-native-unix-common/src/main/c/netty_unix_util.c
+++ b/transport-native-unix-common/src/main/c/netty_unix_util.c
@@ -19,21 +19,27 @@
 #include <errno.h>
 #include "netty_unix_util.h"
 
+static const uint64_t NETTY_BILLION = 1000000000L;
+
 #ifdef NETTY_USE_MACH_INSTEAD_OF_CLOCK
 
 #include <mach/mach.h>
 #include <mach/mach_time.h>
-static const uint64_t NETTY_BILLION = 1000000000L;
 
 #endif /* NETTY_USE_MACH_INSTEAD_OF_CLOCK */
 
 char* netty_unix_util_prepend(const char* prefix, const char* str) {
+    char* result = NULL;
     if (prefix == NULL) {
-        char* result = (char*) malloc(sizeof(char) * (strlen(str) + 1));
+        if ((result = (char*) malloc(sizeof(char) * (strlen(str) + 1))) == NULL) {
+            return NULL;
+        }
         strcpy(result, str);
         return result;
     }
-    char* result = (char*) malloc(sizeof(char) * (strlen(prefix) + strlen(str) + 1));
+    if ((result = (char*) malloc(sizeof(char) * (strlen(prefix) + strlen(str) + 1))) == NULL) {
+        return NULL;
+    }
     strcpy(result, prefix);
     strcat(result, str);
     return result;
@@ -81,7 +87,10 @@ char* netty_unix_util_parse_package_prefix(const char* libraryPathName, const ch
     // packagePrefix length is > 0
     // Make a copy so we can modify the value without impacting libraryPathName.
     size_t packagePrefixLen = packageNameEnd - packagePrefix;
-    packagePrefix = strndup(packagePrefix, packagePrefixLen);
+    if ((packagePrefix = strndup(packagePrefix, packagePrefixLen)) == NULL) {
+        *status = JNI_ERR;
+        return NULL;
+    }
     // Make sure the packagePrefix is in the correct format for the JNI functions it will be used with.
     char* temp = packagePrefix;
     packageNameEnd = packagePrefix + packagePrefixLen;
@@ -94,13 +103,34 @@ char* netty_unix_util_parse_package_prefix(const char* libraryPathName, const ch
     // Make sure packagePrefix is terminated with the '/' JNI package separator.
     if(*(--temp) != '/') {
         temp = packagePrefix;
-        packagePrefix = netty_unix_util_prepend(packagePrefix, "/");
+        if ((packagePrefix = netty_unix_util_prepend(packagePrefix, "/")) == NULL) {
+            *status = JNI_ERR;
+        }
         free(temp);
     }
     return packagePrefix;
 }
 
 // util methods
+uint64_t netty_unix_util_timespec_elapsed_ns(const struct timespec* begin, const struct timespec* end) {
+  return NETTY_BILLION * (end->tv_sec - begin->tv_sec) + (end->tv_nsec - begin->tv_nsec);
+}
+
+jboolean netty_unix_util_timespec_subtract_ns(struct timespec* ts, uint64_t nanos) {
+  const uint64_t seconds = nanos / NETTY_BILLION;
+  nanos -= seconds * NETTY_BILLION;
+  // If there are too many nanos we steal from seconds to avoid underflow on nanos. This way we
+  // only have to worry about underflow on tv_sec.
+  if (nanos > ts->tv_nsec) {
+    --(ts->tv_sec);
+    ts->tv_nsec += NETTY_BILLION;
+  }
+  const jboolean underflow = ts->tv_sec < seconds;
+  ts->tv_sec -= seconds;
+  ts->tv_nsec -= nanos;
+  return underflow;
+}
+
 int netty_unix_util_clock_gettime(clockid_t clockId, struct timespec* tp) {
 #ifdef NETTY_USE_MACH_INSTEAD_OF_CLOCK
   uint64_t timeNs;
@@ -165,13 +195,34 @@ jboolean netty_unix_util_initialize_wait_clock(clockid_t* clockId) {
 }
 
 jint netty_unix_util_register_natives(JNIEnv* env, const char* packagePrefix, const char* className, const JNINativeMethod* methods, jint numMethods) {
-    char* nettyClassName = netty_unix_util_prepend(packagePrefix, className);
+    char* nettyClassName = NULL;
+    int ret = JNI_ERR;
+    NETTY_PREPEND(packagePrefix, className, nettyClassName, done);
+   
     jclass nativeCls = (*env)->FindClass(env, nettyClassName);
-    free(nettyClassName);
-    nettyClassName = NULL;
     if (nativeCls == NULL) {
-        return JNI_ERR;
+        goto done;
+    }
+
+    ret = (*env)->RegisterNatives(env, nativeCls, methods, numMethods);
+done:
+    free(nettyClassName);
+    return ret;
+}
+
+void netty_unix_util_free_dynamic_methods_table(JNINativeMethod* dynamicMethods, jint fixedMethodTableSize, jint fullMethodTableSize) {
+    if (dynamicMethods != NULL) {
+        jint i = fixedMethodTableSize;
+        for (; i < fullMethodTableSize; ++i) {
+            free(dynamicMethods[i].signature);
+        }
+        free(dynamicMethods);
     }
+}
 
-    return (*env)->RegisterNatives(env, nativeCls, methods, numMethods);
+void netty_unix_util_free_dynamic_name(char** dynamicName) {
+    if (dynamicName != NULL && *dynamicName != NULL) {
+        free(*dynamicName);
+        *dynamicName = NULL;
+    }
 }
diff --git a/transport-native-unix-common/src/main/c/netty_unix_util.h b/transport-native-unix-common/src/main/c/netty_unix_util.h
index 9f75e32..5aa9e7f 100644
--- a/transport-native-unix-common/src/main/c/netty_unix_util.h
+++ b/transport-native-unix-common/src/main/c/netty_unix_util.h
@@ -18,6 +18,7 @@
 #define NETTY_UNIX_UTIL_H_
 
 #include <jni.h>
+#include <stdint.h>
 #include <time.h>
 
 #if defined(__MACH__) && !defined(CLOCK_REALTIME)
@@ -35,6 +36,72 @@ typedef int clockid_t;
 
 #endif /* __MACH__ */
 
+
+#define NETTY_BEGIN_MACRO     if (1) {
+#define NETTY_END_MACRO       } else (void)(0)
+
+#define NETTY_FIND_CLASS(E, C, N, R)                \
+    NETTY_BEGIN_MACRO                               \
+        C = (*(E))->FindClass((E), N);              \
+        if (C == NULL) {                            \
+            (*(E))->ExceptionClear((E));            \
+            goto R;                                 \
+        }                                           \
+    NETTY_END_MACRO
+
+#define NETTY_LOAD_CLASS(E, C, N, R)                \
+    NETTY_BEGIN_MACRO                               \
+        jclass _##C = (*(E))->FindClass((E), N);    \
+        if (_##C == NULL) {                         \
+            (*(E))->ExceptionClear((E));            \
+            goto R;                                 \
+        }                                           \
+        C = (*(E))->NewGlobalRef((E), _##C);        \
+        (*(E))->DeleteLocalRef((E), _##C);          \
+        if (C == NULL) {                            \
+            goto R;                                 \
+        }                                           \
+    NETTY_END_MACRO
+
+#define NETTY_UNLOAD_CLASS(E, C)                    \
+    NETTY_BEGIN_MACRO                               \
+        if (C != NULL) {                            \
+            (*(E))->DeleteGlobalRef((E), (C));      \
+            C = NULL;                               \
+        }                                           \
+    NETTY_END_MACRO
+
+
+#define NETTY_GET_METHOD(E, C, M, N, S, R)          \
+    NETTY_BEGIN_MACRO                               \
+        M = (*(E))->GetMethodID((E), C, N, S);      \
+        if (M == NULL) {                            \
+            goto R;                                 \
+        }                                           \
+    NETTY_END_MACRO
+
+#define NETTY_GET_FIELD(E, C, F, N, S, R)           \
+    NETTY_BEGIN_MACRO                               \
+        F = (*(E))->GetFieldID((E), C, N, S);       \
+        if (F == NULL) {                            \
+            goto R;                                 \
+        }                                           \
+    NETTY_END_MACRO
+
+#define NETTY_TRY_GET_FIELD(E, C, F, N, S)          \
+    NETTY_BEGIN_MACRO                               \
+        F = (*(E))->GetFieldID((E), C, N, S);       \
+        if (F == NULL) {                            \
+            (*(E))->ExceptionClear((E));            \
+        }                                           \
+    NETTY_END_MACRO
+
+#define NETTY_PREPEND(P, S, N, R)                             \
+    NETTY_BEGIN_MACRO                                         \
+        if ((N = netty_unix_util_prepend(P, S)) == NULL) {    \
+            goto R;                                           \
+        }                                                     \
+    NETTY_END_MACRO
 /**
  * Return a new string (caller must free this string) which is equivalent to <pre>prefix + str</pre>.
  *
@@ -64,9 +131,26 @@ jboolean netty_unix_util_initialize_wait_clock(clockid_t* clockId);
  */
 int netty_unix_util_clock_gettime(clockid_t clockId, struct timespec* tp);
 
+/**
+ * Calculate the number of nano seconds elapsed between begin and end.
+ *
+ * Returns the number of nano seconds.
+ */
+uint64_t netty_unix_util_timespec_elapsed_ns(const struct timespec* begin, const struct timespec* end);
+
+/**
+ * Subtract <pre>nanos</pre> nano seconds from a <pre>timespec</pre>.
+ *
+ * Returns true if there is underflow.
+ */
+jboolean netty_unix_util_timespec_subtract_ns(struct timespec* ts, uint64_t nanos);
+
 /**
  * Return type is as defined in http://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/functions.html#wp5833.
  */
 jint netty_unix_util_register_natives(JNIEnv* env, const char* packagePrefix, const char* className, const JNINativeMethod* methods, jint numMethods);
 
+void netty_unix_util_free_dynamic_methods_table(JNINativeMethod* dynamicMethods, jint fixedMethodTableSize, jint fullMethodTableSize);
+void netty_unix_util_free_dynamic_name(char** dynamicName);
+
 #endif /* NETTY_UNIX_UTIL_H_ */
diff --git a/transport-native-unix-common/src/main/java/io/netty/channel/unix/DatagramSocketAddress.java b/transport-native-unix-common/src/main/java/io/netty/channel/unix/DatagramSocketAddress.java
index 874d705..131557c 100644
--- a/transport-native-unix-common/src/main/java/io/netty/channel/unix/DatagramSocketAddress.java
+++ b/transport-native-unix-common/src/main/java/io/netty/channel/unix/DatagramSocketAddress.java
@@ -15,7 +15,10 @@
  */
 package io.netty.channel.unix;
 
+import java.net.Inet6Address;
+import java.net.InetAddress;
 import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
 
 /**
  * Act as special {@link InetSocketAddress} to be able to easily pass all needed data from JNI without the need
@@ -30,8 +33,9 @@ public final class DatagramSocketAddress extends InetSocketAddress {
     private final int receivedAmount;
     private final DatagramSocketAddress localAddress;
 
-    DatagramSocketAddress(String addr, int port, int receivedAmount, DatagramSocketAddress local) {
-        super(addr, port);
+    DatagramSocketAddress(byte[] addr, int scopeId, int port, int receivedAmount, DatagramSocketAddress local)
+            throws UnknownHostException {
+        super(newAddress(addr, scopeId), port);
         this.receivedAmount = receivedAmount;
         localAddress = local;
     }
@@ -43,4 +47,11 @@ public final class DatagramSocketAddress extends InetSocketAddress {
     public int receivedAmount() {
         return receivedAmount;
     }
+
+    private static InetAddress newAddress(byte[] bytes, int scopeId) throws UnknownHostException {
+        if (bytes.length == 4) {
+            return InetAddress.getByAddress(bytes);
+        }
+        return Inet6Address.getByAddress(null, bytes, scopeId);
+    }
 }
diff --git a/transport-native-unix-common/src/main/java/io/netty/channel/unix/DomainSocketAddress.java b/transport-native-unix-common/src/main/java/io/netty/channel/unix/DomainSocketAddress.java
index 496c6a3..b58306c 100644
--- a/transport-native-unix-common/src/main/java/io/netty/channel/unix/DomainSocketAddress.java
+++ b/transport-native-unix-common/src/main/java/io/netty/channel/unix/DomainSocketAddress.java
@@ -15,6 +15,8 @@
  */
 package io.netty.channel.unix;
 
+import io.netty.util.internal.ObjectUtil;
+
 import java.io.File;
 import java.net.SocketAddress;
 
@@ -27,10 +29,7 @@ public final class DomainSocketAddress extends SocketAddress {
     private final String socketPath;
 
     public DomainSocketAddress(String socketPath) {
-        if (socketPath == null) {
-            throw new NullPointerException("socketPath");
-        }
-        this.socketPath = socketPath;
+        this.socketPath = ObjectUtil.checkNotNull(socketPath, "socketPath");
     }
 
     public DomainSocketAddress(File file) {
diff --git a/transport-native-unix-common/src/main/java/io/netty/channel/unix/Errors.java b/transport-native-unix-common/src/main/java/io/netty/channel/unix/Errors.java
index 53c5ff2..e75b580 100644
--- a/transport-native-unix-common/src/main/java/io/netty/channel/unix/Errors.java
+++ b/transport-native-unix-common/src/main/java/io/netty/channel/unix/Errors.java
@@ -62,14 +62,29 @@ public final class Errors {
     public static final class NativeIoException extends IOException {
         private static final long serialVersionUID = 8222160204268655526L;
         private final int expectedErr;
+        private final boolean fillInStackTrace;
+
         public NativeIoException(String method, int expectedErr) {
+            this(method, expectedErr, true);
+        }
+
+        public NativeIoException(String method, int expectedErr, boolean fillInStackTrace) {
             super(method + "(..) failed: " + ERRORS[-expectedErr]);
             this.expectedErr = expectedErr;
+            this.fillInStackTrace = fillInStackTrace;
         }
 
         public int expectedErr() {
             return expectedErr;
         }
+
+        @Override
+        public synchronized Throwable fillInStackTrace() {
+            if (fillInStackTrace) {
+                return super.fillInStackTrace();
+            }
+            return this;
+        }
     }
 
     static final class NativeConnectException extends ConnectException {
@@ -92,11 +107,8 @@ public final class Errors {
         }
     }
 
-    static void throwConnectException(String method, NativeConnectException refusedCause, int err)
+    static void throwConnectException(String method, int err)
             throws IOException {
-        if (err == refusedCause.expectedErr()) {
-            throw refusedCause;
-        }
         if (err == ERROR_EALREADY_NEGATIVE) {
             throw new ConnectionPendingException();
         }
@@ -113,7 +125,7 @@ public final class Errors {
     }
 
     public static NativeIoException newConnectionResetException(String method, int errnoNegative) {
-        NativeIoException exception = newIOException(method, errnoNegative);
+        NativeIoException exception = new NativeIoException(method, errnoNegative, false);
         exception.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE);
         return exception;
     }
@@ -122,6 +134,7 @@ public final class Errors {
         return new NativeIoException(method, err);
     }
 
+    @Deprecated
     public static int ioResult(String method, int err, NativeIoException resetCause,
                                ClosedChannelException closedCause) throws IOException {
         // network stack saturated... try again later
@@ -146,5 +159,23 @@ public final class Errors {
         throw newIOException(method, err);
     }
 
+    public static int ioResult(String method, int err) throws IOException {
+        // network stack saturated... try again later
+        if (err == ERRNO_EAGAIN_NEGATIVE || err == ERRNO_EWOULDBLOCK_NEGATIVE) {
+            return 0;
+        }
+        if (err == ERRNO_EBADF_NEGATIVE) {
+            throw new ClosedChannelException();
+        }
+        if (err == ERRNO_ENOTCONN_NEGATIVE) {
+            throw new NotYetConnectedException();
+        }
+        if (err == ERRNO_ENOENT_NEGATIVE) {
+            throw new FileNotFoundException();
+        }
+
+        throw new NativeIoException(method, err, false);
+    }
+
     private Errors() { }
 }
diff --git a/transport-native-unix-common/src/main/java/io/netty/channel/unix/FileDescriptor.java b/transport-native-unix-common/src/main/java/io/netty/channel/unix/FileDescriptor.java
index cf26a42..0b0a55e 100644
--- a/transport-native-unix-common/src/main/java/io/netty/channel/unix/FileDescriptor.java
+++ b/transport-native-unix-common/src/main/java/io/netty/channel/unix/FileDescriptor.java
@@ -15,18 +15,17 @@
  */
 package io.netty.channel.unix;
 
-import io.netty.util.internal.ThrowableUtil;
 
 import java.io.File;
 import java.io.IOException;
 import java.nio.ByteBuffer;
-import java.nio.channels.ClosedChannelException;
 import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
 
 import static io.netty.channel.unix.Errors.ioResult;
 import static io.netty.channel.unix.Errors.newIOException;
 import static io.netty.channel.unix.Limits.IOV_MAX;
 import static io.netty.util.internal.ObjectUtil.checkNotNull;
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
 import static java.lang.Math.min;
 
 /**
@@ -34,36 +33,6 @@ import static java.lang.Math.min;
  * {@link FileDescriptor} for it.
  */
 public class FileDescriptor {
-    private static final ClosedChannelException WRITE_CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace(
-            new ClosedChannelException(), FileDescriptor.class, "write(..)");
-    private static final ClosedChannelException WRITE_ADDRESS_CLOSED_CHANNEL_EXCEPTION =
-            ThrowableUtil.unknownStackTrace(new ClosedChannelException(), FileDescriptor.class, "writeAddress(..)");
-    private static final ClosedChannelException WRITEV_CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace(
-            new ClosedChannelException(), FileDescriptor.class, "writev(..)");
-    private static final ClosedChannelException WRITEV_ADDRESSES_CLOSED_CHANNEL_EXCEPTION =
-            ThrowableUtil.unknownStackTrace(new ClosedChannelException(), FileDescriptor.class, "writevAddresses(..)");
-    private static final ClosedChannelException READ_CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace(
-            new ClosedChannelException(), FileDescriptor.class, "read(..)");
-    private static final ClosedChannelException READ_ADDRESS_CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace(
-            new ClosedChannelException(), FileDescriptor.class, "readAddress(..)");
-    private static final Errors.NativeIoException WRITE_CONNECTION_RESET_EXCEPTION = ThrowableUtil.unknownStackTrace(
-            Errors.newConnectionResetException("syscall:write", Errors.ERRNO_EPIPE_NEGATIVE),
-            FileDescriptor.class, "write(..)");
-    private static final Errors.NativeIoException WRITE_ADDRESS_CONNECTION_RESET_EXCEPTION =
-            ThrowableUtil.unknownStackTrace(Errors.newConnectionResetException("syscall:write",
-                    Errors.ERRNO_EPIPE_NEGATIVE), FileDescriptor.class, "writeAddress(..)");
-    private static final Errors.NativeIoException WRITEV_CONNECTION_RESET_EXCEPTION = ThrowableUtil.unknownStackTrace(
-            Errors.newConnectionResetException("syscall:writev", Errors.ERRNO_EPIPE_NEGATIVE),
-            FileDescriptor.class, "writev(..)");
-    private static final Errors.NativeIoException WRITEV_ADDRESSES_CONNECTION_RESET_EXCEPTION =
-            ThrowableUtil.unknownStackTrace(Errors.newConnectionResetException("syscall:writev",
-                    Errors.ERRNO_EPIPE_NEGATIVE), FileDescriptor.class, "writeAddresses(..)");
-    private static final Errors.NativeIoException READ_CONNECTION_RESET_EXCEPTION = ThrowableUtil.unknownStackTrace(
-            Errors.newConnectionResetException("syscall:read", Errors.ERRNO_ECONNRESET_NEGATIVE),
-            FileDescriptor.class, "read(..)");
-    private static final Errors.NativeIoException READ_ADDRESS_CONNECTION_RESET_EXCEPTION =
-            ThrowableUtil.unknownStackTrace(Errors.newConnectionResetException("syscall:read",
-                    Errors.ERRNO_ECONNRESET_NEGATIVE), FileDescriptor.class, "readAddress(..)");
 
     private static final AtomicIntegerFieldUpdater<FileDescriptor> stateUpdater =
             AtomicIntegerFieldUpdater.newUpdater(FileDescriptor.class, "state");
@@ -82,9 +51,7 @@ public class FileDescriptor {
     final int fd;
 
     public FileDescriptor(int fd) {
-        if (fd < 0) {
-            throw new IllegalArgumentException("fd must be >= 0");
-        }
+        checkPositiveOrZero(fd, "fd");
         this.fd = fd;
     }
 
@@ -127,7 +94,7 @@ public class FileDescriptor {
         if (res >= 0) {
             return res;
         }
-        return ioResult("write", res, WRITE_CONNECTION_RESET_EXCEPTION, WRITE_CLOSED_CHANNEL_EXCEPTION);
+        return ioResult("write", res);
     }
 
     public final int writeAddress(long address, int pos, int limit) throws IOException {
@@ -135,8 +102,7 @@ public class FileDescriptor {
         if (res >= 0) {
             return res;
         }
-        return ioResult("writeAddress", res,
-                WRITE_ADDRESS_CONNECTION_RESET_EXCEPTION, WRITE_ADDRESS_CLOSED_CHANNEL_EXCEPTION);
+        return ioResult("writeAddress", res);
     }
 
     public final long writev(ByteBuffer[] buffers, int offset, int length, long maxBytesToWrite) throws IOException {
@@ -144,7 +110,7 @@ public class FileDescriptor {
         if (res >= 0) {
             return res;
         }
-        return ioResult("writev", (int) res, WRITEV_CONNECTION_RESET_EXCEPTION, WRITEV_CLOSED_CHANNEL_EXCEPTION);
+        return ioResult("writev", (int) res);
     }
 
     public final long writevAddresses(long memoryAddress, int length) throws IOException {
@@ -152,8 +118,7 @@ public class FileDescriptor {
         if (res >= 0) {
             return res;
         }
-        return ioResult("writevAddresses", (int) res,
-                WRITEV_ADDRESSES_CONNECTION_RESET_EXCEPTION, WRITEV_ADDRESSES_CLOSED_CHANNEL_EXCEPTION);
+        return ioResult("writevAddresses", (int) res);
     }
 
     public final int read(ByteBuffer buf, int pos, int limit) throws IOException {
@@ -164,7 +129,7 @@ public class FileDescriptor {
         if (res == 0) {
             return -1;
         }
-        return ioResult("read", res, READ_CONNECTION_RESET_EXCEPTION, READ_CLOSED_CHANNEL_EXCEPTION);
+        return ioResult("read", res);
     }
 
     public final int readAddress(long address, int pos, int limit) throws IOException {
@@ -175,8 +140,7 @@ public class FileDescriptor {
         if (res == 0) {
             return -1;
         }
-        return ioResult("readAddress", res,
-                READ_ADDRESS_CONNECTION_RESET_EXCEPTION, READ_ADDRESS_CLOSED_CHANNEL_EXCEPTION);
+        return ioResult("readAddress", res);
     }
 
     @Override
@@ -207,8 +171,7 @@ public class FileDescriptor {
      * Open a new {@link FileDescriptor} for the given path.
      */
     public static FileDescriptor from(String path) throws IOException {
-        checkNotNull(path, "path");
-        int res = open(path);
+        int res = open(checkNotNull(path, "path"));
         if (res < 0) {
             throw newIOException("open", res);
         }
diff --git a/transport-native-unix-common/src/main/java/io/netty/channel/unix/IovArray.java b/transport-native-unix-common/src/main/java/io/netty/channel/unix/IovArray.java
index e8b4067..77ec3a7 100644
--- a/transport-native-unix-common/src/main/java/io/netty/channel/unix/IovArray.java
+++ b/transport-native-unix-common/src/main/java/io/netty/channel/unix/IovArray.java
@@ -16,7 +16,6 @@
 package io.netty.channel.unix;
 
 import io.netty.buffer.ByteBuf;
-import io.netty.buffer.CompositeByteBuf;
 import io.netty.channel.ChannelOutboundBuffer.MessageProcessor;
 import io.netty.util.internal.PlatformDependent;
 
@@ -78,33 +77,33 @@ public final class IovArray implements MessageProcessor {
     }
 
     /**
-     * Add a {@link ByteBuf} to this {@link IovArray}.
-     * @param buf The {@link ByteBuf} to add.
-     * @return {@code true} if the entire {@link ByteBuf} has been added to this {@link IovArray}. Note in the event
-     * that {@link ByteBuf} is a {@link CompositeByteBuf} {@code false} may be returned even if some of the components
-     * have been added.
+     * @deprecated Use {@link #add(ByteBuf, int, int)}
      */
+    @Deprecated
     public boolean add(ByteBuf buf) {
+        return add(buf, buf.readerIndex(), buf.readableBytes());
+    }
+
+    public boolean add(ByteBuf buf, int offset, int len) {
         if (count == IOV_MAX) {
             // No more room!
             return false;
         } else if (buf.nioBufferCount() == 1) {
-            final int len = buf.readableBytes();
             if (len == 0) {
                 return true;
             }
             if (buf.hasMemoryAddress()) {
-                return add(buf.memoryAddress(), buf.readerIndex(), len);
+                return add(buf.memoryAddress() + offset, len);
             } else {
-                ByteBuffer nioBuffer = buf.internalNioBuffer(buf.readerIndex(), len);
-                return add(Buffer.memoryAddress(nioBuffer), nioBuffer.position(), len);
+                ByteBuffer nioBuffer = buf.internalNioBuffer(offset, len);
+                return add(Buffer.memoryAddress(nioBuffer) + nioBuffer.position(), len);
             }
         } else {
-            ByteBuffer[] buffers = buf.nioBuffers();
+            ByteBuffer[] buffers = buf.nioBuffers(offset, len);
             for (ByteBuffer nioBuffer : buffers) {
-                final int len = nioBuffer.remaining();
-                if (len != 0 &&
-                    (!add(Buffer.memoryAddress(nioBuffer), nioBuffer.position(), len) || count == IOV_MAX)) {
+                final int remaining = nioBuffer.remaining();
+                if (remaining != 0 &&
+                        (!add(Buffer.memoryAddress(nioBuffer) + nioBuffer.position(), remaining) || count == IOV_MAX)) {
                     return false;
                 }
             }
@@ -112,7 +111,7 @@ public final class IovArray implements MessageProcessor {
         }
     }
 
-    private boolean add(long addr, int offset, int len) {
+    private boolean add(long addr, int len) {
         assert addr != 0;
 
         // If there is at least 1 entry then we enforce the maximum bytes. We want to accept at least one entry so we
@@ -135,19 +134,19 @@ public final class IovArray implements MessageProcessor {
         if (ADDRESS_SIZE == 8) {
             // 64bit
             if (PlatformDependent.hasUnsafe()) {
-                PlatformDependent.putLong(baseOffset + memoryAddress, addr + offset);
+                PlatformDependent.putLong(baseOffset + memoryAddress, addr);
                 PlatformDependent.putLong(lengthOffset + memoryAddress, len);
             } else {
-                memory.putLong(baseOffset, addr + offset);
+                memory.putLong(baseOffset, addr);
                 memory.putLong(lengthOffset, len);
             }
         } else {
             assert ADDRESS_SIZE == 4;
             if (PlatformDependent.hasUnsafe()) {
-                PlatformDependent.putInt(baseOffset + memoryAddress, (int) addr + offset);
+                PlatformDependent.putInt(baseOffset + memoryAddress, (int) addr);
                 PlatformDependent.putInt(lengthOffset + memoryAddress, len);
             } else {
-                memory.putInt(baseOffset, (int) addr + offset);
+                memory.putInt(baseOffset, (int) addr);
                 memory.putInt(lengthOffset, len);
             }
         }
@@ -169,22 +168,22 @@ public final class IovArray implements MessageProcessor {
     }
 
     /**
-     * Set the maximum amount of bytes that can be added to this {@link IovArray} via {@link #add(ByteBuf)}.
+     * Set the maximum amount of bytes that can be added to this {@link IovArray} via {@link #add(ByteBuf, int, int)}
      * <p>
      * This will not impact the existing state of the {@link IovArray}, and only applies to subsequent calls to
      * {@link #add(ByteBuf)}.
      * <p>
      * In order to ensure some progress is made at least one {@link ByteBuf} will be accepted even if it's size exceeds
      * this value.
-     * @param maxBytes the maximum amount of bytes that can be added to this {@link IovArray} via {@link #add(ByteBuf)}.
+     * @param maxBytes the maximum amount of bytes that can be added to this {@link IovArray}.
      */
     public void maxBytes(long maxBytes) {
         this.maxBytes = min(SSIZE_MAX, checkPositive(maxBytes, "maxBytes"));
     }
 
     /**
-     * Get the maximum amount of bytes that can be added to this {@link IovArray} via {@link #add(ByteBuf)}.
-     * @return the maximum amount of bytes that can be added to this {@link IovArray} via {@link #add(ByteBuf)}.
+     * Get the maximum amount of bytes that can be added to this {@link IovArray}.
+     * @return the maximum amount of bytes that can be added to this {@link IovArray}.
      */
     public long maxBytes() {
         return maxBytes;
@@ -206,7 +205,11 @@ public final class IovArray implements MessageProcessor {
 
     @Override
     public boolean processMessage(Object msg) throws Exception {
-        return msg instanceof ByteBuf && add((ByteBuf) msg);
+        if (msg instanceof ByteBuf) {
+            ByteBuf buffer = (ByteBuf) msg;
+            return add(buffer, buffer.readerIndex(), buffer.readableBytes());
+        }
+        return false;
     }
 
     private static int idx(int index) {
diff --git a/transport-native-unix-common/src/main/java/io/netty/channel/unix/NativeInetAddress.java b/transport-native-unix-common/src/main/java/io/netty/channel/unix/NativeInetAddress.java
index 599f823..3e76ab9 100644
--- a/transport-native-unix-common/src/main/java/io/netty/channel/unix/NativeInetAddress.java
+++ b/transport-native-unix-common/src/main/java/io/netty/channel/unix/NativeInetAddress.java
@@ -58,11 +58,15 @@ public final class NativeInetAddress {
 
     public static byte[] ipv4MappedIpv6Address(byte[] ipv4) {
         byte[] address = new byte[16];
-        System.arraycopy(IPV4_MAPPED_IPV6_PREFIX, 0, address, 0, IPV4_MAPPED_IPV6_PREFIX.length);
-        System.arraycopy(ipv4, 0, address, 12, ipv4.length);
+        copyIpv4MappedIpv6Address(ipv4, address);
         return address;
     }
 
+    public static void copyIpv4MappedIpv6Address(byte[] ipv4, byte[] ipv6) {
+        System.arraycopy(IPV4_MAPPED_IPV6_PREFIX, 0, ipv6, 0, IPV4_MAPPED_IPV6_PREFIX.length);
+        System.arraycopy(ipv4, 0, ipv6, 12, ipv4.length);
+    }
+
     public static InetSocketAddress address(byte[] addr, int offset, int len) {
         // The last 4 bytes are always the port
         final int port = decodeInt(addr, offset + len - 4);
diff --git a/transport-native-unix-common/src/main/java/io/netty/channel/unix/Socket.java b/transport-native-unix-common/src/main/java/io/netty/channel/unix/Socket.java
index 01df6a1..10a68d4 100644
--- a/transport-native-unix-common/src/main/java/io/netty/channel/unix/Socket.java
+++ b/transport-native-unix-common/src/main/java/io/netty/channel/unix/Socket.java
@@ -39,44 +39,20 @@ import static io.netty.channel.unix.Errors.throwConnectException;
 import static io.netty.channel.unix.LimitsStaticallyReferencedJniMethods.udsSunPathSize;
 import static io.netty.channel.unix.NativeInetAddress.address;
 import static io.netty.channel.unix.NativeInetAddress.ipv4MappedIpv6Address;
-import static io.netty.util.internal.ThrowableUtil.unknownStackTrace;
 
 /**
  * Provides a JNI bridge to native socket operations.
  * <strong>Internal usage only!</strong>
  */
 public class Socket extends FileDescriptor {
-    private static final ClosedChannelException SHUTDOWN_CLOSED_CHANNEL_EXCEPTION = unknownStackTrace(
-            new ClosedChannelException(), Socket.class, "shutdown(..)");
-    private static final ClosedChannelException SEND_TO_CLOSED_CHANNEL_EXCEPTION = unknownStackTrace(
-            new ClosedChannelException(), Socket.class, "sendTo(..)");
-    private static final ClosedChannelException SEND_TO_ADDRESS_CLOSED_CHANNEL_EXCEPTION =
-            unknownStackTrace(new ClosedChannelException(), Socket.class, "sendToAddress(..)");
-    private static final ClosedChannelException SEND_TO_ADDRESSES_CLOSED_CHANNEL_EXCEPTION =
-            unknownStackTrace(new ClosedChannelException(), Socket.class, "sendToAddresses(..)");
-    private static final Errors.NativeIoException SEND_TO_CONNECTION_RESET_EXCEPTION = unknownStackTrace(
-            Errors.newConnectionResetException("syscall:sendto", Errors.ERRNO_EPIPE_NEGATIVE),
-            Socket.class, "sendTo(..)");
-    private static final Errors.NativeIoException SEND_TO_ADDRESS_CONNECTION_RESET_EXCEPTION =
-            unknownStackTrace(Errors.newConnectionResetException("syscall:sendto",
-                    Errors.ERRNO_EPIPE_NEGATIVE), Socket.class, "sendToAddress");
-    private static final Errors.NativeIoException CONNECTION_RESET_EXCEPTION_SENDMSG = unknownStackTrace(
-            Errors.newConnectionResetException("syscall:sendmsg",
-            Errors.ERRNO_EPIPE_NEGATIVE), Socket.class, "sendToAddresses(..)");
-    private static final Errors.NativeIoException CONNECTION_RESET_SHUTDOWN_EXCEPTION =
-            unknownStackTrace(Errors.newConnectionResetException("syscall:shutdown",
-                    Errors.ERRNO_ECONNRESET_NEGATIVE), Socket.class, "shutdown");
-    private static final Errors.NativeConnectException FINISH_CONNECT_REFUSED_EXCEPTION =
-            unknownStackTrace(new Errors.NativeConnectException("syscall:getsockopt",
-                    Errors.ERROR_ECONNREFUSED_NEGATIVE), Socket.class, "finishConnect(..)");
-    private static final Errors.NativeConnectException CONNECT_REFUSED_EXCEPTION =
-            unknownStackTrace(new Errors.NativeConnectException("syscall:connect",
-                    Errors.ERROR_ECONNREFUSED_NEGATIVE), Socket.class, "connect(..)");
 
     public static final int UDS_SUN_PATH_SIZE = udsSunPathSize();
 
+    protected final boolean ipv6;
+
     public Socket(int fd) {
         super(fd);
+        this.ipv6 = isIPv6(fd);
     }
 
     public final void shutdown() throws IOException {
@@ -111,7 +87,7 @@ public class Socket extends FileDescriptor {
         }
         int res = shutdown(fd, read, write);
         if (res < 0) {
-            ioResult("shutdown", res, CONNECTION_RESET_SHUTDOWN_EXCEPTION, SHUTDOWN_CLOSED_CHANNEL_EXCEPTION);
+            ioResult("shutdown", res);
         }
     }
 
@@ -141,14 +117,14 @@ public class Socket extends FileDescriptor {
             scopeId = 0;
             address = ipv4MappedIpv6Address(addr.getAddress());
         }
-        int res = sendTo(fd, buf, pos, limit, address, scopeId, port);
+        int res = sendTo(fd, ipv6, buf, pos, limit, address, scopeId, port);
         if (res >= 0) {
             return res;
         }
         if (res == ERROR_ECONNREFUSED_NEGATIVE) {
             throw new PortUnreachableException("sendTo failed");
         }
-        return ioResult("sendTo", res, SEND_TO_CONNECTION_RESET_EXCEPTION, SEND_TO_CLOSED_CHANNEL_EXCEPTION);
+        return ioResult("sendTo", res);
     }
 
     public final int sendToAddress(long memoryAddress, int pos, int limit, InetAddress addr, int port)
@@ -165,15 +141,14 @@ public class Socket extends FileDescriptor {
             scopeId = 0;
             address = ipv4MappedIpv6Address(addr.getAddress());
         }
-        int res = sendToAddress(fd, memoryAddress, pos, limit, address, scopeId, port);
+        int res = sendToAddress(fd, ipv6, memoryAddress, pos, limit, address, scopeId, port);
         if (res >= 0) {
             return res;
         }
         if (res == ERROR_ECONNREFUSED_NEGATIVE) {
             throw new PortUnreachableException("sendToAddress failed");
         }
-        return ioResult("sendToAddress", res,
-                SEND_TO_ADDRESS_CONNECTION_RESET_EXCEPTION, SEND_TO_ADDRESS_CLOSED_CHANNEL_EXCEPTION);
+        return ioResult("sendToAddress", res);
     }
 
     public final int sendToAddresses(long memoryAddress, int length, InetAddress addr, int port) throws IOException {
@@ -189,7 +164,7 @@ public class Socket extends FileDescriptor {
             scopeId = 0;
             address = ipv4MappedIpv6Address(addr.getAddress());
         }
-        int res = sendToAddresses(fd, memoryAddress, length, address, scopeId, port);
+        int res = sendToAddresses(fd, ipv6, memoryAddress, length, address, scopeId, port);
         if (res >= 0) {
             return res;
         }
@@ -197,8 +172,7 @@ public class Socket extends FileDescriptor {
         if (res == ERROR_ECONNREFUSED_NEGATIVE) {
             throw new PortUnreachableException("sendToAddresses failed");
         }
-        return ioResult("sendToAddresses", res,
-                CONNECTION_RESET_EXCEPTION_SENDMSG, SEND_TO_ADDRESSES_CLOSED_CHANNEL_EXCEPTION);
+        return ioResult("sendToAddresses", res);
     }
 
     public final DatagramSocketAddress recvFrom(ByteBuffer buf, int pos, int limit) throws IOException {
@@ -242,7 +216,7 @@ public class Socket extends FileDescriptor {
         if (socketAddress instanceof InetSocketAddress) {
             InetSocketAddress inetSocketAddress = (InetSocketAddress) socketAddress;
             NativeInetAddress address = NativeInetAddress.newInstance(inetSocketAddress.getAddress());
-            res = connect(fd, address.address, address.scopeId, inetSocketAddress.getPort());
+            res = connect(fd, ipv6, address.address, address.scopeId, inetSocketAddress.getPort());
         } else if (socketAddress instanceof DomainSocketAddress) {
             DomainSocketAddress unixDomainSocketAddress = (DomainSocketAddress) socketAddress;
             res = connectDomainSocket(fd, unixDomainSocketAddress.path().getBytes(CharsetUtil.UTF_8));
@@ -254,7 +228,7 @@ public class Socket extends FileDescriptor {
                 // connect not complete yet need to wait for EPOLLOUT event
                 return false;
             }
-            throwConnectException("connect", CONNECT_REFUSED_EXCEPTION, res);
+            throwConnectException("connect", res);
         }
         return true;
     }
@@ -266,15 +240,15 @@ public class Socket extends FileDescriptor {
                 // connect still in progress
                 return false;
             }
-            throwConnectException("finishConnect", FINISH_CONNECT_REFUSED_EXCEPTION, res);
+            throwConnectException("finishConnect", res);
         }
         return true;
     }
 
     public final void disconnect() throws IOException {
-        int res = disconnect(fd);
+        int res = disconnect(fd, ipv6);
         if (res < 0) {
-            throwConnectException("disconnect", FINISH_CONNECT_REFUSED_EXCEPTION, res);
+            throwConnectException("disconnect", res);
         }
     }
 
@@ -282,7 +256,7 @@ public class Socket extends FileDescriptor {
         if (socketAddress instanceof InetSocketAddress) {
             InetSocketAddress addr = (InetSocketAddress) socketAddress;
             NativeInetAddress address = NativeInetAddress.newInstance(addr.getAddress());
-            int res = bind(fd, address.address, address.scopeId, addr.getPort());
+            int res = bind(fd, ipv6, address.address, address.scopeId, addr.getPort());
             if (res < 0) {
                 throw newIOException("bind", res);
             }
@@ -367,7 +341,7 @@ public class Socket extends FileDescriptor {
     }
 
     public final int getTrafficClass() throws IOException {
-        return getTrafficClass(fd);
+        return getTrafficClass(fd, ipv6);
     }
 
     public final void setKeepAlive(boolean keepAlive) throws IOException {
@@ -403,9 +377,13 @@ public class Socket extends FileDescriptor {
     }
 
     public final void setTrafficClass(int trafficClass) throws IOException {
-        setTrafficClass(fd, trafficClass);
+        setTrafficClass(fd, ipv6, trafficClass);
     }
 
+    public static native boolean isIPv6Preferred();
+
+    private static native boolean isIPv6(int fd);
+
     @Override
     public String toString() {
         return "Socket{" +
@@ -434,7 +412,11 @@ public class Socket extends FileDescriptor {
     }
 
     protected static int newSocketStream0() {
-        int res = newSocketStreamFd();
+        return newSocketStream0(isIPv6Preferred());
+    }
+
+    protected static int newSocketStream0(boolean ipv6) {
+        int res = newSocketStreamFd(ipv6);
         if (res < 0) {
             throw new ChannelException(newIOException("newSocketStream", res));
         }
@@ -442,7 +424,11 @@ public class Socket extends FileDescriptor {
     }
 
     protected static int newSocketDgram0() {
-        int res = newSocketDgramFd();
+        return newSocketDgram0(isIPv6Preferred());
+    }
+
+    protected static int newSocketDgram0(boolean ipv6) {
+        int res = newSocketDgramFd(ipv6);
         if (res < 0) {
             throw new ChannelException(newIOException("newSocketDgram", res));
         }
@@ -458,11 +444,11 @@ public class Socket extends FileDescriptor {
     }
 
     private static native int shutdown(int fd, boolean read, boolean write);
-    private static native int connect(int fd, byte[] address, int scopeId, int port);
+    private static native int connect(int fd, boolean ipv6, byte[] address, int scopeId, int port);
     private static native int connectDomainSocket(int fd, byte[] path);
     private static native int finishConnect(int fd);
-    private static native int disconnect(int fd);
-    private static native int bind(int fd, byte[] address, int scopeId, int port);
+    private static native int disconnect(int fd, boolean ipv6);
+    private static native int bind(int fd, boolean ipv6, byte[] address, int scopeId, int port);
     private static native int bindDomainSocket(int fd, byte[] path);
     private static native int listen(int fd, int backlog);
     private static native int accept(int fd, byte[] addr);
@@ -471,11 +457,11 @@ public class Socket extends FileDescriptor {
     private static native byte[] localAddress(int fd);
 
     private static native int sendTo(
-            int fd, ByteBuffer buf, int pos, int limit, byte[] address, int scopeId, int port);
+            int fd, boolean ipv6, ByteBuffer buf, int pos, int limit, byte[] address, int scopeId, int port);
     private static native int sendToAddress(
-            int fd, long memoryAddress, int pos, int limit, byte[] address, int scopeId, int port);
+            int fd, boolean ipv6, long memoryAddress, int pos, int limit, byte[] address, int scopeId, int port);
     private static native int sendToAddresses(
-            int fd, long memoryAddress, int length, byte[] address, int scopeId, int port);
+            int fd, boolean ipv6, long memoryAddress, int length, byte[] address, int scopeId, int port);
 
     private static native DatagramSocketAddress recvFrom(
             int fd, ByteBuffer buf, int pos, int limit) throws IOException;
@@ -484,8 +470,8 @@ public class Socket extends FileDescriptor {
     private static native int recvFd(int fd);
     private static native int sendFd(int socketFd, int fd);
 
-    private static native int newSocketStreamFd();
-    private static native int newSocketDgramFd();
+    private static native int newSocketStreamFd(boolean ipv6);
+    private static native int newSocketDgramFd(boolean ipv6);
     private static native int newSocketDomainFd();
 
     private static native int isReuseAddress(int fd) throws IOException;
@@ -497,7 +483,7 @@ public class Socket extends FileDescriptor {
     private static native int isBroadcast(int fd) throws IOException;
     private static native int getSoLinger(int fd) throws IOException;
     private static native int getSoError(int fd) throws IOException;
-    private static native int getTrafficClass(int fd) throws IOException;
+    private static native int getTrafficClass(int fd, boolean ipv6) throws IOException;
 
     private static native void setReuseAddress(int fd, int reuseAddress) throws IOException;
     private static native void setReusePort(int fd, int reuseAddress) throws IOException;
@@ -507,6 +493,6 @@ public class Socket extends FileDescriptor {
     private static native void setTcpNoDelay(int fd, int tcpNoDelay) throws IOException;
     private static native void setSoLinger(int fd, int soLinger) throws IOException;
     private static native void setBroadcast(int fd, int broadcast) throws IOException;
-    private static native void setTrafficClass(int fd, int trafficClass) throws IOException;
+    private static native void setTrafficClass(int fd, boolean ipv6, int trafficClass) throws IOException;
     private static native void initialize(boolean ipv4Preferred);
 }
diff --git a/transport-rxtx/pom.xml b/transport-rxtx/pom.xml
index b1cfd0b..2c241c5 100644
--- a/transport-rxtx/pom.xml
+++ b/transport-rxtx/pom.xml
@@ -21,7 +21,7 @@
   <parent>
     <groupId>io.netty</groupId>
     <artifactId>netty-parent</artifactId>
-    <version>4.1.33.Final</version>
+    <version>4.1.48.Final</version>
   </parent>
 
   <artifactId>netty-transport-rxtx</artifactId>
diff --git a/transport-sctp/pom.xml b/transport-sctp/pom.xml
index f1e1c4f..23337aa 100644
--- a/transport-sctp/pom.xml
+++ b/transport-sctp/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>io.netty</groupId>
     <artifactId>netty-parent</artifactId>
-    <version>4.1.33.Final</version>
+    <version>4.1.48.Final</version>
   </parent>
 
   <artifactId>netty-transport-sctp</artifactId>
diff --git a/transport-sctp/src/main/java/io/netty/channel/sctp/DefaultSctpChannelConfig.java b/transport-sctp/src/main/java/io/netty/channel/sctp/DefaultSctpChannelConfig.java
index c551366..8537e48 100644
--- a/transport-sctp/src/main/java/io/netty/channel/sctp/DefaultSctpChannelConfig.java
+++ b/transport-sctp/src/main/java/io/netty/channel/sctp/DefaultSctpChannelConfig.java
@@ -24,6 +24,7 @@ import io.netty.channel.DefaultChannelConfig;
 import io.netty.channel.MessageSizeEstimator;
 import io.netty.channel.RecvByteBufAllocator;
 import io.netty.channel.WriteBufferWaterMark;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.PlatformDependent;
 
 import java.io.IOException;
@@ -43,10 +44,7 @@ public class DefaultSctpChannelConfig extends DefaultChannelConfig implements Sc
 
     public DefaultSctpChannelConfig(io.netty.channel.sctp.SctpChannel channel, SctpChannel javaChannel) {
         super(channel);
-        if (javaChannel == null) {
-            throw new NullPointerException("javaChannel");
-        }
-        this.javaChannel = javaChannel;
+        this.javaChannel = ObjectUtil.checkNotNull(javaChannel, "javaChannel");
 
         // Enable TCP_NODELAY by default if possible.
         if (PlatformDependent.canEnableTcpNoDelayByDefault()) {
diff --git a/transport-sctp/src/main/java/io/netty/channel/sctp/DefaultSctpServerChannelConfig.java b/transport-sctp/src/main/java/io/netty/channel/sctp/DefaultSctpServerChannelConfig.java
index 4980eb8..d0f0fa6 100644
--- a/transport-sctp/src/main/java/io/netty/channel/sctp/DefaultSctpServerChannelConfig.java
+++ b/transport-sctp/src/main/java/io/netty/channel/sctp/DefaultSctpServerChannelConfig.java
@@ -15,6 +15,8 @@
 */
 package io.netty.channel.sctp;
 
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
+
 import com.sun.nio.sctp.SctpServerChannel;
 import com.sun.nio.sctp.SctpStandardSocketOptions;
 import io.netty.buffer.ByteBufAllocator;
@@ -25,6 +27,7 @@ import io.netty.channel.MessageSizeEstimator;
 import io.netty.channel.RecvByteBufAllocator;
 import io.netty.channel.WriteBufferWaterMark;
 import io.netty.util.NetUtil;
+import io.netty.util.internal.ObjectUtil;
 
 import java.io.IOException;
 import java.util.Map;
@@ -43,10 +46,7 @@ public class DefaultSctpServerChannelConfig extends DefaultChannelConfig impleme
     public DefaultSctpServerChannelConfig(
             io.netty.channel.sctp.SctpServerChannel channel, SctpServerChannel javaChannel) {
         super(channel);
-        if (javaChannel == null) {
-            throw new NullPointerException("javaChannel");
-        }
-        this.javaChannel = javaChannel;
+        this.javaChannel = ObjectUtil.checkNotNull(javaChannel, "javaChannel");
     }
 
     @Override
@@ -152,9 +152,7 @@ public class DefaultSctpServerChannelConfig extends DefaultChannelConfig impleme
 
     @Override
     public SctpServerChannelConfig setBacklog(int backlog) {
-        if (backlog < 0) {
-            throw new IllegalArgumentException("backlog: " + backlog);
-        }
+        checkPositiveOrZero(backlog, "backlog");
         this.backlog = backlog;
         return this;
     }
diff --git a/transport-sctp/src/main/java/io/netty/channel/sctp/SctpMessage.java b/transport-sctp/src/main/java/io/netty/channel/sctp/SctpMessage.java
index 8d48e67..a51aa47 100644
--- a/transport-sctp/src/main/java/io/netty/channel/sctp/SctpMessage.java
+++ b/transport-sctp/src/main/java/io/netty/channel/sctp/SctpMessage.java
@@ -18,6 +18,7 @@ package io.netty.channel.sctp;
 import com.sun.nio.sctp.MessageInfo;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.DefaultByteBufHolder;
+import io.netty.util.internal.ObjectUtil;
 
 /**
  * Representation of SCTP Data Chunk
@@ -61,13 +62,10 @@ public final class SctpMessage extends DefaultByteBufHolder {
      */
     public SctpMessage(MessageInfo msgInfo, ByteBuf payloadBuffer) {
         super(payloadBuffer);
-        if (msgInfo == null) {
-            throw new NullPointerException("msgInfo");
-        }
-        this.msgInfo = msgInfo;
-        streamIdentifier = msgInfo.streamNumber();
-        protocolIdentifier = msgInfo.payloadProtocolID();
-        unordered = msgInfo.isUnordered();
+        this.msgInfo = ObjectUtil.checkNotNull(msgInfo, "msgInfo");
+        this.streamIdentifier = msgInfo.streamNumber();
+        this.protocolIdentifier = msgInfo.payloadProtocolID();
+        this.unordered = msgInfo.isUnordered();
     }
 
     /**
diff --git a/transport-sctp/src/main/java/io/netty/channel/sctp/SctpNotificationHandler.java b/transport-sctp/src/main/java/io/netty/channel/sctp/SctpNotificationHandler.java
index 11a5272..6418a67 100644
--- a/transport-sctp/src/main/java/io/netty/channel/sctp/SctpNotificationHandler.java
+++ b/transport-sctp/src/main/java/io/netty/channel/sctp/SctpNotificationHandler.java
@@ -22,8 +22,8 @@ import com.sun.nio.sctp.Notification;
 import com.sun.nio.sctp.PeerAddressChangeNotification;
 import com.sun.nio.sctp.SendFailedNotification;
 import com.sun.nio.sctp.ShutdownNotification;
-
 import io.netty.channel.ChannelPipeline;
+import io.netty.util.internal.ObjectUtil;
 
 
 /**
@@ -35,10 +35,7 @@ public final class SctpNotificationHandler extends AbstractNotificationHandler<O
     private final SctpChannel sctpChannel;
 
     public SctpNotificationHandler(SctpChannel sctpChannel) {
-        if (sctpChannel == null) {
-            throw new NullPointerException("sctpChannel");
-        }
-        this.sctpChannel = sctpChannel;
+        this.sctpChannel = ObjectUtil.checkNotNull(sctpChannel, "sctpChannel");
     }
 
     @Override
diff --git a/transport-sctp/src/main/java/io/netty/channel/sctp/oio/OioSctpServerChannel.java b/transport-sctp/src/main/java/io/netty/channel/sctp/oio/OioSctpServerChannel.java
index 3c73728..963a1d4 100755
--- a/transport-sctp/src/main/java/io/netty/channel/sctp/oio/OioSctpServerChannel.java
+++ b/transport-sctp/src/main/java/io/netty/channel/sctp/oio/OioSctpServerChannel.java
@@ -25,6 +25,7 @@ import io.netty.channel.ChannelPromise;
 import io.netty.channel.oio.AbstractOioMessageChannel;
 import io.netty.channel.sctp.DefaultSctpServerChannelConfig;
 import io.netty.channel.sctp.SctpServerChannelConfig;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 
@@ -84,11 +85,7 @@ public class OioSctpServerChannel extends AbstractOioMessageChannel
      */
     public OioSctpServerChannel(SctpServerChannel sch) {
         super(null);
-        if (sch == null) {
-            throw new NullPointerException("sctp server channel");
-        }
-
-        this.sch = sch;
+        this.sch = ObjectUtil.checkNotNull(sch, "sctp server channel");
         boolean success = false;
         try {
             sch.configureBlocking(false);
diff --git a/transport-sctp/src/main/java/io/netty/handler/codec/sctp/SctpMessageCompletionHandler.java b/transport-sctp/src/main/java/io/netty/handler/codec/sctp/SctpMessageCompletionHandler.java
index 0a09a5e..7f6de63 100644
--- a/transport-sctp/src/main/java/io/netty/handler/codec/sctp/SctpMessageCompletionHandler.java
+++ b/transport-sctp/src/main/java/io/netty/handler/codec/sctp/SctpMessageCompletionHandler.java
@@ -22,10 +22,10 @@ import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelInboundHandler;
 import io.netty.channel.sctp.SctpMessage;
 import io.netty.handler.codec.MessageToMessageDecoder;
+import io.netty.util.collection.IntObjectHashMap;
+import io.netty.util.collection.IntObjectMap;
 
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 
 /**
  * {@link MessageToMessageDecoder} which will take care of handle fragmented {@link SctpMessage}s, so
@@ -33,7 +33,7 @@ import java.util.Map;
  * {@link ChannelInboundHandler}.
  */
 public class SctpMessageCompletionHandler extends MessageToMessageDecoder<SctpMessage> {
-    private final Map<Integer, ByteBuf> fragments = new HashMap<Integer, ByteBuf>();
+    private final IntObjectMap<ByteBuf> fragments = new IntObjectHashMap<ByteBuf>();
 
     @Override
     protected void decode(ChannelHandlerContext ctx, SctpMessage msg, List<Object> out) throws Exception {
diff --git a/transport-udt/pom.xml b/transport-udt/pom.xml
index 628643d..ddf110c 100644
--- a/transport-udt/pom.xml
+++ b/transport-udt/pom.xml
@@ -21,7 +21,7 @@
   <parent>
     <groupId>io.netty</groupId>
     <artifactId>netty-parent</artifactId>
-    <version>4.1.33.Final</version>
+    <version>4.1.48.Final</version>
   </parent>
 
   <artifactId>netty-transport-udt</artifactId>
diff --git a/transport-udt/src/main/java/io/netty/channel/udt/nio/NioUdtMessageConnectorChannel.java b/transport-udt/src/main/java/io/netty/channel/udt/nio/NioUdtMessageConnectorChannel.java
index ca0f295..204b0b7 100644
--- a/transport-udt/src/main/java/io/netty/channel/udt/nio/NioUdtMessageConnectorChannel.java
+++ b/transport-udt/src/main/java/io/netty/channel/udt/nio/NioUdtMessageConnectorChannel.java
@@ -79,9 +79,7 @@ public class NioUdtMessageConnectorChannel extends AbstractNioMessageChannel imp
             try {
                 channelUDT.close();
             } catch (final Exception e2) {
-                if (logger.isWarnEnabled()) {
-                    logger.warn("Failed to close channel.", e2);
-                }
+                logger.warn("Failed to close channel.", e2);
             }
             throw new ChannelException("Failed to configure channel.", e);
         }
diff --git a/transport-udt/src/test/java/io/netty/test/udt/util/CaliperBench.java b/transport-udt/src/test/java/io/netty/test/udt/util/CaliperBench.java
index 983daa8..ca86eda 100644
--- a/transport-udt/src/test/java/io/netty/test/udt/util/CaliperBench.java
+++ b/transport-udt/src/test/java/io/netty/test/udt/util/CaliperBench.java
@@ -91,7 +91,6 @@ public abstract class CaliperBench extends SimpleBenchmark {
                 return;
             } else {
                 System.out.print("-");
-                continue;
             }
         }
     }
diff --git a/transport/pom.xml b/transport/pom.xml
index e2b4f8e..23af92b 100644
--- a/transport/pom.xml
+++ b/transport/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>io.netty</groupId>
     <artifactId>netty-parent</artifactId>
-    <version>4.1.33.Final</version>
+    <version>4.1.48.Final</version>
   </parent>
 
   <artifactId>netty-transport</artifactId>
diff --git a/transport/src/main/java/io/netty/bootstrap/AbstractBootstrap.java b/transport/src/main/java/io/netty/bootstrap/AbstractBootstrap.java
index 1030c32..cd3e741 100644
--- a/transport/src/main/java/io/netty/bootstrap/AbstractBootstrap.java
+++ b/transport/src/main/java/io/netty/bootstrap/AbstractBootstrap.java
@@ -26,10 +26,11 @@ import io.netty.channel.DefaultChannelPromise;
 import io.netty.channel.EventLoop;
 import io.netty.channel.EventLoopGroup;
 import io.netty.channel.ReflectiveChannelFactory;
-import io.netty.util.internal.SocketUtils;
 import io.netty.util.AttributeKey;
 import io.netty.util.concurrent.EventExecutor;
 import io.netty.util.concurrent.GlobalEventExecutor;
+import io.netty.util.internal.ObjectUtil;
+import io.netty.util.internal.SocketUtils;
 import io.netty.util.internal.StringUtil;
 import io.netty.util.internal.logging.InternalLogger;
 
@@ -37,8 +38,10 @@ import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.SocketAddress;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 
 /**
  * {@link AbstractBootstrap} is a helper class that makes it easy to bootstrap a {@link Channel}. It support
@@ -48,13 +51,20 @@ import java.util.Map;
  * transports such as datagram (UDP).</p>
  */
 public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C extends Channel> implements Cloneable {
+    @SuppressWarnings("unchecked")
+    static final Map.Entry<ChannelOption<?>, Object>[] EMPTY_OPTION_ARRAY = new Map.Entry[0];
+    @SuppressWarnings("unchecked")
+    static final Map.Entry<AttributeKey<?>, Object>[] EMPTY_ATTRIBUTE_ARRAY = new Map.Entry[0];
 
     volatile EventLoopGroup group;
     @SuppressWarnings("deprecation")
     private volatile ChannelFactory<? extends C> channelFactory;
     private volatile SocketAddress localAddress;
+
+    // The order in which ChannelOptions are applied is important they may depend on each other for validation
+    // purposes.
     private final Map<ChannelOption<?>, Object> options = new LinkedHashMap<ChannelOption<?>, Object>();
-    private final Map<AttributeKey<?>, Object> attrs = new LinkedHashMap<AttributeKey<?>, Object>();
+    private final Map<AttributeKey<?>, Object> attrs = new ConcurrentHashMap<AttributeKey<?>, Object>();
     private volatile ChannelHandler handler;
 
     AbstractBootstrap() {
@@ -69,9 +79,7 @@ public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C ext
         synchronized (bootstrap.options) {
             options.putAll(bootstrap.options);
         }
-        synchronized (bootstrap.attrs) {
-            attrs.putAll(bootstrap.attrs);
-        }
+        attrs.putAll(bootstrap.attrs);
     }
 
     /**
@@ -79,9 +87,7 @@ public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C ext
      * {@link Channel}
      */
     public B group(EventLoopGroup group) {
-        if (group == null) {
-            throw new NullPointerException("group");
-        }
+        ObjectUtil.checkNotNull(group, "group");
         if (this.group != null) {
             throw new IllegalStateException("group set already");
         }
@@ -100,10 +106,9 @@ public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C ext
      * {@link Channel} implementation has no no-args constructor.
      */
     public B channel(Class<? extends C> channelClass) {
-        if (channelClass == null) {
-            throw new NullPointerException("channelClass");
-        }
-        return channelFactory(new ReflectiveChannelFactory<C>(channelClass));
+        return channelFactory(new ReflectiveChannelFactory<C>(
+                ObjectUtil.checkNotNull(channelClass, "channelClass")
+        ));
     }
 
     /**
@@ -111,9 +116,7 @@ public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C ext
      */
     @Deprecated
     public B channelFactory(ChannelFactory<? extends C> channelFactory) {
-        if (channelFactory == null) {
-            throw new NullPointerException("channelFactory");
-        }
+        ObjectUtil.checkNotNull(channelFactory, "channelFactory");
         if (this.channelFactory != null) {
             throw new IllegalStateException("channelFactory set already");
         }
@@ -168,15 +171,11 @@ public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C ext
      * created. Use a value of {@code null} to remove a previous set {@link ChannelOption}.
      */
     public <T> B option(ChannelOption<T> option, T value) {
-        if (option == null) {
-            throw new NullPointerException("option");
-        }
-        if (value == null) {
-            synchronized (options) {
+        ObjectUtil.checkNotNull(option, "option");
+        synchronized (options) {
+            if (value == null) {
                 options.remove(option);
-            }
-        } else {
-            synchronized (options) {
+            } else {
                 options.put(option, value);
             }
         }
@@ -188,17 +187,11 @@ public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C ext
      * {@code null}, the attribute of the specified {@code key} is removed.
      */
     public <T> B attr(AttributeKey<T> key, T value) {
-        if (key == null) {
-            throw new NullPointerException("key");
-        }
+        ObjectUtil.checkNotNull(key, "key");
         if (value == null) {
-            synchronized (attrs) {
-                attrs.remove(key);
-            }
+            attrs.remove(key);
         } else {
-            synchronized (attrs) {
-                attrs.put(key, value);
-            }
+            attrs.put(key, value);
         }
         return self();
     }
@@ -272,10 +265,7 @@ public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C ext
      */
     public ChannelFuture bind(SocketAddress localAddress) {
         validate();
-        if (localAddress == null) {
-            throw new NullPointerException("localAddress");
-        }
-        return doBind(localAddress);
+        return doBind(ObjectUtil.checkNotNull(localAddress, "localAddress"));
     }
 
     private ChannelFuture doBind(final SocketAddress localAddress) {
@@ -375,10 +365,7 @@ public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C ext
      * the {@link ChannelHandler} to use for serving the requests.
      */
     public B handler(ChannelHandler handler) {
-        if (handler == null) {
-            throw new NullPointerException("handler");
-        }
-        this.handler = handler;
+        this.handler = ObjectUtil.checkNotNull(handler, "handler");
         return self();
     }
 
@@ -398,15 +385,10 @@ public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C ext
      */
     public abstract AbstractBootstrapConfig<B, C> config();
 
-    static <K, V> Map<K, V> copiedMap(Map<K, V> map) {
-        final Map<K, V> copied;
-        synchronized (map) {
-            if (map.isEmpty()) {
-                return Collections.emptyMap();
-            }
-            copied = new LinkedHashMap<K, V>(map);
+    final Map.Entry<ChannelOption<?>, Object>[] newOptionsArray() {
+        synchronized (options) {
+            return options.entrySet().toArray(EMPTY_OPTION_ARRAY);
         }
-        return Collections.unmodifiableMap(copied);
     }
 
     final Map<ChannelOption<?>, Object> options0() {
@@ -431,17 +413,27 @@ public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C ext
     }
 
     final Map<ChannelOption<?>, Object> options() {
-        return copiedMap(options);
+        synchronized (options) {
+            return copiedMap(options);
+        }
     }
 
     final Map<AttributeKey<?>, Object> attrs() {
         return copiedMap(attrs);
     }
 
-    static void setChannelOptions(
-            Channel channel, Map<ChannelOption<?>, Object> options, InternalLogger logger) {
-        for (Map.Entry<ChannelOption<?>, Object> e: options.entrySet()) {
-            setChannelOption(channel, e.getKey(), e.getValue(), logger);
+    static <K, V> Map<K, V> copiedMap(Map<K, V> map) {
+        if (map.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        return Collections.unmodifiableMap(new HashMap<K, V>(map));
+    }
+
+    static void setAttributes(Channel channel, Map.Entry<AttributeKey<?>, Object>[] attrs) {
+        for (Map.Entry<AttributeKey<?>, Object> e: attrs) {
+            @SuppressWarnings("unchecked")
+            AttributeKey<Object> key = (AttributeKey<Object>) e.getKey();
+            channel.attr(key).set(e.getValue());
         }
     }
 
diff --git a/transport/src/main/java/io/netty/bootstrap/Bootstrap.java b/transport/src/main/java/io/netty/bootstrap/Bootstrap.java
index 1e2e483..7859042 100644
--- a/transport/src/main/java/io/netty/bootstrap/Bootstrap.java
+++ b/transport/src/main/java/io/netty/bootstrap/Bootstrap.java
@@ -18,7 +18,6 @@ package io.netty.bootstrap;
 import io.netty.channel.Channel;
 import io.netty.channel.ChannelFuture;
 import io.netty.channel.ChannelFutureListener;
-import io.netty.channel.ChannelOption;
 import io.netty.channel.ChannelPipeline;
 import io.netty.channel.ChannelPromise;
 import io.netty.channel.EventLoop;
@@ -27,17 +26,15 @@ import io.netty.resolver.AddressResolver;
 import io.netty.resolver.DefaultAddressResolverGroup;
 import io.netty.resolver.NameResolver;
 import io.netty.resolver.AddressResolverGroup;
-import io.netty.util.AttributeKey;
 import io.netty.util.concurrent.Future;
 import io.netty.util.concurrent.FutureListener;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.SocketAddress;
-import java.util.Map;
-import java.util.Map.Entry;
 
 /**
  * A {@link Bootstrap} that makes it easy to bootstrap a {@link Channel} to use
@@ -137,10 +134,7 @@ public class Bootstrap extends AbstractBootstrap<Bootstrap, Channel> {
      * Connect a {@link Channel} to the remote peer.
      */
     public ChannelFuture connect(SocketAddress remoteAddress) {
-        if (remoteAddress == null) {
-            throw new NullPointerException("remoteAddress");
-        }
-
+        ObjectUtil.checkNotNull(remoteAddress, "remoteAddress");
         validate();
         return doResolveAndConnect(remoteAddress, config.localAddress());
     }
@@ -149,9 +143,7 @@ public class Bootstrap extends AbstractBootstrap<Bootstrap, Channel> {
      * Connect a {@link Channel} to the remote peer.
      */
     public ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress) {
-        if (remoteAddress == null) {
-            throw new NullPointerException("remoteAddress");
-        }
+        ObjectUtil.checkNotNull(remoteAddress, "remoteAddress");
         validate();
         return doResolveAndConnect(remoteAddress, localAddress);
     }
@@ -197,7 +189,13 @@ public class Bootstrap extends AbstractBootstrap<Bootstrap, Channel> {
                                                final SocketAddress localAddress, final ChannelPromise promise) {
         try {
             final EventLoop eventLoop = channel.eventLoop();
-            final AddressResolver<SocketAddress> resolver = this.resolver.getResolver(eventLoop);
+            AddressResolver<SocketAddress> resolver;
+            try {
+                resolver = this.resolver.getResolver(eventLoop);
+            } catch (Throwable cause) {
+                channel.close();
+                return promise.setFailure(cause);
+            }
 
             if (!resolver.isSupported(remoteAddress) || resolver.isResolved(remoteAddress)) {
                 // Resolver has no idea about what to do with the specified remote address or it's resolved already.
@@ -260,21 +258,12 @@ public class Bootstrap extends AbstractBootstrap<Bootstrap, Channel> {
 
     @Override
     @SuppressWarnings("unchecked")
-    void init(Channel channel) throws Exception {
+    void init(Channel channel) {
         ChannelPipeline p = channel.pipeline();
         p.addLast(config.handler());
 
-        final Map<ChannelOption<?>, Object> options = options0();
-        synchronized (options) {
-            setChannelOptions(channel, options, logger);
-        }
-
-        final Map<AttributeKey<?>, Object> attrs = attrs0();
-        synchronized (attrs) {
-            for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
-                channel.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
-            }
-        }
+        setChannelOptions(channel, newOptionsArray(), logger);
+        setAttributes(channel, attrs0().entrySet().toArray(EMPTY_ATTRIBUTE_ARRAY));
     }
 
     @Override
diff --git a/transport/src/main/java/io/netty/bootstrap/ServerBootstrap.java b/transport/src/main/java/io/netty/bootstrap/ServerBootstrap.java
index 310e9fb..37dd350 100644
--- a/transport/src/main/java/io/netty/bootstrap/ServerBootstrap.java
+++ b/transport/src/main/java/io/netty/bootstrap/ServerBootstrap.java
@@ -28,12 +28,14 @@ import io.netty.channel.ChannelPipeline;
 import io.netty.channel.EventLoopGroup;
 import io.netty.channel.ServerChannel;
 import io.netty.util.AttributeKey;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 
 import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -44,8 +46,10 @@ public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerCh
 
     private static final InternalLogger logger = InternalLoggerFactory.getInstance(ServerBootstrap.class);
 
+    // The order in which child ChannelOptions are applied is important they may depend on each other for validation
+    // purposes.
     private final Map<ChannelOption<?>, Object> childOptions = new LinkedHashMap<ChannelOption<?>, Object>();
-    private final Map<AttributeKey<?>, Object> childAttrs = new LinkedHashMap<AttributeKey<?>, Object>();
+    private final Map<AttributeKey<?>, Object> childAttrs = new ConcurrentHashMap<AttributeKey<?>, Object>();
     private final ServerBootstrapConfig config = new ServerBootstrapConfig(this);
     private volatile EventLoopGroup childGroup;
     private volatile ChannelHandler childHandler;
@@ -59,9 +63,7 @@ public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerCh
         synchronized (bootstrap.childOptions) {
             childOptions.putAll(bootstrap.childOptions);
         }
-        synchronized (bootstrap.childAttrs) {
-            childAttrs.putAll(bootstrap.childAttrs);
-        }
+        childAttrs.putAll(bootstrap.childAttrs);
     }
 
     /**
@@ -79,13 +81,10 @@ public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerCh
      */
     public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
         super.group(parentGroup);
-        if (childGroup == null) {
-            throw new NullPointerException("childGroup");
-        }
         if (this.childGroup != null) {
             throw new IllegalStateException("childGroup set already");
         }
-        this.childGroup = childGroup;
+        this.childGroup = ObjectUtil.checkNotNull(childGroup, "childGroup");
         return this;
     }
 
@@ -95,15 +94,11 @@ public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerCh
      * {@link ChannelOption}.
      */
     public <T> ServerBootstrap childOption(ChannelOption<T> childOption, T value) {
-        if (childOption == null) {
-            throw new NullPointerException("childOption");
-        }
-        if (value == null) {
-            synchronized (childOptions) {
+        ObjectUtil.checkNotNull(childOption, "childOption");
+        synchronized (childOptions) {
+            if (value == null) {
                 childOptions.remove(childOption);
-            }
-        } else {
-            synchronized (childOptions) {
+            } else {
                 childOptions.put(childOption, value);
             }
         }
@@ -115,9 +110,7 @@ public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerCh
      * {@code null} the {@link AttributeKey} is removed
      */
     public <T> ServerBootstrap childAttr(AttributeKey<T> childKey, T value) {
-        if (childKey == null) {
-            throw new NullPointerException("childKey");
-        }
+        ObjectUtil.checkNotNull(childKey, "childKey");
         if (value == null) {
             childAttrs.remove(childKey);
         } else {
@@ -130,45 +123,28 @@ public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerCh
      * Set the {@link ChannelHandler} which is used to serve the request for the {@link Channel}'s.
      */
     public ServerBootstrap childHandler(ChannelHandler childHandler) {
-        if (childHandler == null) {
-            throw new NullPointerException("childHandler");
-        }
-        this.childHandler = childHandler;
+        this.childHandler = ObjectUtil.checkNotNull(childHandler, "childHandler");
         return this;
     }
 
     @Override
-    void init(Channel channel) throws Exception {
-        final Map<ChannelOption<?>, Object> options = options0();
-        synchronized (options) {
-            setChannelOptions(channel, options, logger);
-        }
-
-        final Map<AttributeKey<?>, Object> attrs = attrs0();
-        synchronized (attrs) {
-            for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
-                @SuppressWarnings("unchecked")
-                AttributeKey<Object> key = (AttributeKey<Object>) e.getKey();
-                channel.attr(key).set(e.getValue());
-            }
-        }
+    void init(Channel channel) {
+        setChannelOptions(channel, newOptionsArray(), logger);
+        setAttributes(channel, attrs0().entrySet().toArray(EMPTY_ATTRIBUTE_ARRAY));
 
         ChannelPipeline p = channel.pipeline();
 
         final EventLoopGroup currentChildGroup = childGroup;
         final ChannelHandler currentChildHandler = childHandler;
         final Entry<ChannelOption<?>, Object>[] currentChildOptions;
-        final Entry<AttributeKey<?>, Object>[] currentChildAttrs;
         synchronized (childOptions) {
-            currentChildOptions = childOptions.entrySet().toArray(newOptionArray(0));
-        }
-        synchronized (childAttrs) {
-            currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(0));
+            currentChildOptions = childOptions.entrySet().toArray(EMPTY_OPTION_ARRAY);
         }
+        final Entry<AttributeKey<?>, Object>[] currentChildAttrs = childAttrs.entrySet().toArray(EMPTY_ATTRIBUTE_ARRAY);
 
         p.addLast(new ChannelInitializer<Channel>() {
             @Override
-            public void initChannel(final Channel ch) throws Exception {
+            public void initChannel(final Channel ch) {
                 final ChannelPipeline pipeline = ch.pipeline();
                 ChannelHandler handler = config.handler();
                 if (handler != null) {
@@ -199,16 +175,6 @@ public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerCh
         return this;
     }
 
-    @SuppressWarnings("unchecked")
-    private static Entry<AttributeKey<?>, Object>[] newAttrArray(int size) {
-        return new Entry[size];
-    }
-
-    @SuppressWarnings("unchecked")
-    private static Map.Entry<ChannelOption<?>, Object>[] newOptionArray(int size) {
-        return new Map.Entry[size];
-    }
-
     private static class ServerBootstrapAcceptor extends ChannelInboundHandlerAdapter {
 
         private final EventLoopGroup childGroup;
@@ -246,10 +212,7 @@ public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerCh
             child.pipeline().addLast(childHandler);
 
             setChannelOptions(child, childOptions, logger);
-
-            for (Entry<AttributeKey<?>, Object> e: childAttrs) {
-                child.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
-            }
+            setAttributes(child, childAttrs);
 
             try {
                 childGroup.register(child).addListener(new ChannelFutureListener() {
@@ -307,7 +270,9 @@ public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerCh
     }
 
     final Map<ChannelOption<?>, Object> childOptions() {
-        return copiedMap(childOptions);
+        synchronized (childOptions) {
+            return copiedMap(childOptions);
+        }
     }
 
     final Map<AttributeKey<?>, Object> childAttrs() {
diff --git a/transport/src/main/java/io/netty/channel/AbstractChannel.java b/transport/src/main/java/io/netty/channel/AbstractChannel.java
index 11c14a9..e701657 100644
--- a/transport/src/main/java/io/netty/channel/AbstractChannel.java
+++ b/transport/src/main/java/io/netty/channel/AbstractChannel.java
@@ -20,8 +20,8 @@ import io.netty.channel.socket.ChannelOutputShutdownEvent;
 import io.netty.channel.socket.ChannelOutputShutdownException;
 import io.netty.util.DefaultAttributeMap;
 import io.netty.util.ReferenceCountUtil;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.PlatformDependent;
-import io.netty.util.internal.ThrowableUtil;
 import io.netty.util.internal.UnstableApi;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
@@ -44,17 +44,6 @@ public abstract class AbstractChannel extends DefaultAttributeMap implements Cha
 
     private static final InternalLogger logger = InternalLoggerFactory.getInstance(AbstractChannel.class);
 
-    private static final ClosedChannelException FLUSH0_CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace(
-            new ClosedChannelException(), AbstractUnsafe.class, "flush0()");
-    private static final ClosedChannelException ENSURE_OPEN_CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace(
-            new ClosedChannelException(), AbstractUnsafe.class, "ensureOpen(...)");
-    private static final ClosedChannelException CLOSE_CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace(
-            new ClosedChannelException(), AbstractUnsafe.class, "close(...)");
-    private static final ClosedChannelException WRITE_CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace(
-            new ClosedChannelException(), AbstractUnsafe.class, "write(...)");
-    private static final NotYetConnectedException FLUSH0_NOT_YET_CONNECTED_EXCEPTION = ThrowableUtil.unknownStackTrace(
-            new NotYetConnectedException(), AbstractUnsafe.class, "flush0()");
-
     private final Channel parent;
     private final ChannelId id;
     private final Unsafe unsafe;
@@ -67,6 +56,7 @@ public abstract class AbstractChannel extends DefaultAttributeMap implements Cha
     private volatile EventLoop eventLoop;
     private volatile boolean registered;
     private boolean closeInitiated;
+    private Throwable initialCloseCause;
 
     /** Cache for the string representation of this channel */
     private boolean strValActive;
@@ -461,9 +451,7 @@ public abstract class AbstractChannel extends DefaultAttributeMap implements Cha
 
         @Override
         public final void register(EventLoop eventLoop, final ChannelPromise promise) {
-            if (eventLoop == null) {
-                throw new NullPointerException("eventLoop");
-            }
+            ObjectUtil.checkNotNull(eventLoop, "eventLoop");
             if (isRegistered()) {
                 promise.setFailure(new IllegalStateException("registered to an event loop already"));
                 return;
@@ -589,6 +577,9 @@ public abstract class AbstractChannel extends DefaultAttributeMap implements Cha
             boolean wasActive = isActive();
             try {
                 doDisconnect();
+                // Reset remoteAddress and localAddress
+                remoteAddress = null;
+                localAddress = null;
             } catch (Throwable t) {
                 safeSetFailure(promise, t);
                 closeIfClosed();
@@ -612,7 +603,8 @@ public abstract class AbstractChannel extends DefaultAttributeMap implements Cha
         public final void close(final ChannelPromise promise) {
             assertEventLoop();
 
-            close(promise, CLOSE_CLOSED_CHANNEL_EXCEPTION, CLOSE_CLOSED_CHANNEL_EXCEPTION, false);
+            ClosedChannelException closedChannelException = new ClosedChannelException();
+            close(promise, closedChannelException, closedChannelException, false);
         }
 
         /**
@@ -637,7 +629,7 @@ public abstract class AbstractChannel extends DefaultAttributeMap implements Cha
 
             final ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
             if (outboundBuffer == null) {
-                promise.setFailure(CLOSE_CLOSED_CHANNEL_EXCEPTION);
+                promise.setFailure(new ClosedChannelException());
                 return;
             }
             this.outboundBuffer = null; // Disallow adding any messages and flushes to outboundBuffer.
@@ -870,7 +862,7 @@ public abstract class AbstractChannel extends DefaultAttributeMap implements Cha
                 // need to fail the future right away. If it is not null the handling of the rest
                 // will be done in flush0()
                 // See https://github.com/netty/netty/issues/2362
-                safeSetFailure(promise, WRITE_CLOSED_CHANNEL_EXCEPTION);
+                safeSetFailure(promise, newClosedChannelException(initialCloseCause));
                 // release message now to prevent resource-leak
                 ReferenceCountUtil.release(msg);
                 return;
@@ -923,10 +915,10 @@ public abstract class AbstractChannel extends DefaultAttributeMap implements Cha
             if (!isActive()) {
                 try {
                     if (isOpen()) {
-                        outboundBuffer.failFlushed(FLUSH0_NOT_YET_CONNECTED_EXCEPTION, true);
+                        outboundBuffer.failFlushed(new NotYetConnectedException(), true);
                     } else {
                         // Do not trigger channelWritabilityChanged because the channel is closed already.
-                        outboundBuffer.failFlushed(FLUSH0_CLOSED_CHANNEL_EXCEPTION, false);
+                        outboundBuffer.failFlushed(newClosedChannelException(initialCloseCause), false);
                     }
                 } finally {
                     inFlush0 = false;
@@ -946,12 +938,14 @@ public abstract class AbstractChannel extends DefaultAttributeMap implements Cha
                      * This is needed as otherwise {@link #isActive()} , {@link #isOpen()} and {@link #isWritable()}
                      * may still return {@code true} even if the channel should be closed as result of the exception.
                      */
-                    close(voidPromise(), t, FLUSH0_CLOSED_CHANNEL_EXCEPTION, false);
+                    initialCloseCause = t;
+                    close(voidPromise(), t, newClosedChannelException(t), false);
                 } else {
                     try {
                         shutdownOutput(voidPromise(), t);
                     } catch (Throwable t2) {
-                        close(voidPromise(), t2, FLUSH0_CLOSED_CHANNEL_EXCEPTION, false);
+                        initialCloseCause = t;
+                        close(voidPromise(), t2, newClosedChannelException(t), false);
                     }
                 }
             } finally {
@@ -959,6 +953,14 @@ public abstract class AbstractChannel extends DefaultAttributeMap implements Cha
             }
         }
 
+        private ClosedChannelException newClosedChannelException(Throwable cause) {
+            ClosedChannelException exception = new ClosedChannelException();
+            if (cause != null) {
+                exception.initCause(cause);
+            }
+            return exception;
+        }
+
         @Override
         public final ChannelPromise voidPromise() {
             assertEventLoop();
@@ -971,7 +973,7 @@ public abstract class AbstractChannel extends DefaultAttributeMap implements Cha
                 return true;
             }
 
-            safeSetFailure(promise, ENSURE_OPEN_CLOSED_CHANNEL_EXCEPTION);
+            safeSetFailure(promise, newClosedChannelException(initialCloseCause));
             return false;
         }
 
@@ -1122,6 +1124,10 @@ public abstract class AbstractChannel extends DefaultAttributeMap implements Cha
         return msg;
     }
 
+    protected void validateFileRegion(DefaultFileRegion region, long position) throws IOException {
+        DefaultFileRegion.validate(region, position);
+    }
+
     static final class CloseFuture extends DefaultChannelPromise {
 
         CloseFuture(AbstractChannel ch) {
@@ -1160,7 +1166,6 @@ public abstract class AbstractChannel extends DefaultAttributeMap implements Cha
         AnnotatedConnectException(ConnectException exception, SocketAddress remoteAddress) {
             super(exception.getMessage() + ": " + remoteAddress);
             initCause(exception);
-            setStackTrace(exception.getStackTrace());
         }
 
         @Override
@@ -1176,7 +1181,6 @@ public abstract class AbstractChannel extends DefaultAttributeMap implements Cha
         AnnotatedNoRouteToHostException(NoRouteToHostException exception, SocketAddress remoteAddress) {
             super(exception.getMessage() + ": " + remoteAddress);
             initCause(exception);
-            setStackTrace(exception.getStackTrace());
         }
 
         @Override
@@ -1192,7 +1196,6 @@ public abstract class AbstractChannel extends DefaultAttributeMap implements Cha
         AnnotatedSocketException(SocketException exception, SocketAddress remoteAddress) {
             super(exception.getMessage() + ": " + remoteAddress);
             initCause(exception);
-            setStackTrace(exception.getStackTrace());
         }
 
         @Override
diff --git a/transport/src/main/java/io/netty/channel/AbstractChannelHandlerContext.java b/transport/src/main/java/io/netty/channel/AbstractChannelHandlerContext.java
index 9e599c3..92c912a 100644
--- a/transport/src/main/java/io/netty/channel/AbstractChannelHandlerContext.java
+++ b/transport/src/main/java/io/netty/channel/AbstractChannelHandlerContext.java
@@ -18,12 +18,14 @@ package io.netty.channel;
 import io.netty.buffer.ByteBufAllocator;
 import io.netty.util.Attribute;
 import io.netty.util.AttributeKey;
-import io.netty.util.DefaultAttributeMap;
-import io.netty.util.Recycler;
 import io.netty.util.ReferenceCountUtil;
 import io.netty.util.ResourceLeakHint;
+import io.netty.util.concurrent.AbstractEventExecutor;
 import io.netty.util.concurrent.EventExecutor;
 import io.netty.util.concurrent.OrderedEventExecutor;
+import io.netty.util.internal.ObjectPool;
+import io.netty.util.internal.ObjectPool.Handle;
+import io.netty.util.internal.ObjectPool.ObjectCreator;
 import io.netty.util.internal.PromiseNotificationUtil;
 import io.netty.util.internal.ThrowableUtil;
 import io.netty.util.internal.ObjectUtil;
@@ -35,8 +37,28 @@ import io.netty.util.internal.logging.InternalLoggerFactory;
 import java.net.SocketAddress;
 import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
 
-abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
-        implements ChannelHandlerContext, ResourceLeakHint {
+import static io.netty.channel.ChannelHandlerMask.MASK_BIND;
+import static io.netty.channel.ChannelHandlerMask.MASK_CHANNEL_ACTIVE;
+import static io.netty.channel.ChannelHandlerMask.MASK_CHANNEL_INACTIVE;
+import static io.netty.channel.ChannelHandlerMask.MASK_CHANNEL_READ;
+import static io.netty.channel.ChannelHandlerMask.MASK_CHANNEL_READ_COMPLETE;
+import static io.netty.channel.ChannelHandlerMask.MASK_CHANNEL_REGISTERED;
+import static io.netty.channel.ChannelHandlerMask.MASK_CHANNEL_UNREGISTERED;
+import static io.netty.channel.ChannelHandlerMask.MASK_CHANNEL_WRITABILITY_CHANGED;
+import static io.netty.channel.ChannelHandlerMask.MASK_CLOSE;
+import static io.netty.channel.ChannelHandlerMask.MASK_CONNECT;
+import static io.netty.channel.ChannelHandlerMask.MASK_DEREGISTER;
+import static io.netty.channel.ChannelHandlerMask.MASK_DISCONNECT;
+import static io.netty.channel.ChannelHandlerMask.MASK_EXCEPTION_CAUGHT;
+import static io.netty.channel.ChannelHandlerMask.MASK_FLUSH;
+import static io.netty.channel.ChannelHandlerMask.MASK_ONLY_INBOUND;
+import static io.netty.channel.ChannelHandlerMask.MASK_ONLY_OUTBOUND;
+import static io.netty.channel.ChannelHandlerMask.MASK_READ;
+import static io.netty.channel.ChannelHandlerMask.MASK_USER_EVENT_TRIGGERED;
+import static io.netty.channel.ChannelHandlerMask.MASK_WRITE;
+import static io.netty.channel.ChannelHandlerMask.mask;
+
+abstract class AbstractChannelHandlerContext implements ChannelHandlerContext, ResourceLeakHint {
 
     private static final InternalLogger logger = InternalLoggerFactory.getInstance(AbstractChannelHandlerContext.class);
     volatile AbstractChannelHandlerContext next;
@@ -63,11 +85,10 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
      */
     private static final int INIT = 0;
 
-    private final boolean inbound;
-    private final boolean outbound;
     private final DefaultChannelPipeline pipeline;
     private final String name;
     private final boolean ordered;
+    private final int executionMask;
 
     // Will be set to null if no child executor should be used, otherwise it will be set to the
     // child executor.
@@ -76,20 +97,16 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
 
     // Lazily instantiated tasks used to trigger events to a handler with different executor.
     // There is no need to make this volatile as at worse it will just create a few more instances then needed.
-    private Runnable invokeChannelReadCompleteTask;
-    private Runnable invokeReadTask;
-    private Runnable invokeChannelWritableStateChangedTask;
-    private Runnable invokeFlushTask;
+    private Tasks invokeTasks;
 
     private volatile int handlerState = INIT;
 
-    AbstractChannelHandlerContext(DefaultChannelPipeline pipeline, EventExecutor executor, String name,
-                                  boolean inbound, boolean outbound) {
+    AbstractChannelHandlerContext(DefaultChannelPipeline pipeline, EventExecutor executor,
+                                  String name, Class<? extends ChannelHandler> handlerClass) {
         this.name = ObjectUtil.checkNotNull(name, "name");
         this.pipeline = pipeline;
         this.executor = executor;
-        this.inbound = inbound;
-        this.outbound = outbound;
+        this.executionMask = mask(handlerClass);
         // Its ordered if its driven by the EventLoop or the given Executor is an instanceof OrderedEventExecutor.
         ordered = executor == null || executor instanceof OrderedEventExecutor;
     }
@@ -125,7 +142,7 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
 
     @Override
     public ChannelHandlerContext fireChannelRegistered() {
-        invokeChannelRegistered(findContextInbound());
+        invokeChannelRegistered(findContextInbound(MASK_CHANNEL_REGISTERED));
         return this;
     }
 
@@ -157,7 +174,7 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
 
     @Override
     public ChannelHandlerContext fireChannelUnregistered() {
-        invokeChannelUnregistered(findContextInbound());
+        invokeChannelUnregistered(findContextInbound(MASK_CHANNEL_UNREGISTERED));
         return this;
     }
 
@@ -189,7 +206,7 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
 
     @Override
     public ChannelHandlerContext fireChannelActive() {
-        invokeChannelActive(findContextInbound());
+        invokeChannelActive(findContextInbound(MASK_CHANNEL_ACTIVE));
         return this;
     }
 
@@ -221,7 +238,7 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
 
     @Override
     public ChannelHandlerContext fireChannelInactive() {
-        invokeChannelInactive(findContextInbound());
+        invokeChannelInactive(findContextInbound(MASK_CHANNEL_INACTIVE));
         return this;
     }
 
@@ -253,7 +270,7 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
 
     @Override
     public ChannelHandlerContext fireExceptionCaught(final Throwable cause) {
-        invokeExceptionCaught(next, cause);
+        invokeExceptionCaught(findContextInbound(MASK_EXCEPTION_CAUGHT), cause);
         return this;
     }
 
@@ -304,7 +321,7 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
 
     @Override
     public ChannelHandlerContext fireUserEventTriggered(final Object event) {
-        invokeUserEventTriggered(findContextInbound(), event);
+        invokeUserEventTriggered(findContextInbound(MASK_USER_EVENT_TRIGGERED), event);
         return this;
     }
 
@@ -337,7 +354,7 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
 
     @Override
     public ChannelHandlerContext fireChannelRead(final Object msg) {
-        invokeChannelRead(findContextInbound(), msg);
+        invokeChannelRead(findContextInbound(MASK_CHANNEL_READ), msg);
         return this;
     }
 
@@ -370,7 +387,7 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
 
     @Override
     public ChannelHandlerContext fireChannelReadComplete() {
-        invokeChannelReadComplete(findContextInbound());
+        invokeChannelReadComplete(findContextInbound(MASK_CHANNEL_READ_COMPLETE));
         return this;
     }
 
@@ -379,16 +396,11 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
         if (executor.inEventLoop()) {
             next.invokeChannelReadComplete();
         } else {
-            Runnable task = next.invokeChannelReadCompleteTask;
-            if (task == null) {
-                next.invokeChannelReadCompleteTask = task = new Runnable() {
-                    @Override
-                    public void run() {
-                        next.invokeChannelReadComplete();
-                    }
-                };
+            Tasks tasks = next.invokeTasks;
+            if (tasks == null) {
+                next.invokeTasks = tasks = new Tasks(next);
             }
-            executor.execute(task);
+            executor.execute(tasks.invokeChannelReadCompleteTask);
         }
     }
 
@@ -406,7 +418,7 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
 
     @Override
     public ChannelHandlerContext fireChannelWritabilityChanged() {
-        invokeChannelWritabilityChanged(findContextInbound());
+        invokeChannelWritabilityChanged(findContextInbound(MASK_CHANNEL_WRITABILITY_CHANGED));
         return this;
     }
 
@@ -415,16 +427,11 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
         if (executor.inEventLoop()) {
             next.invokeChannelWritabilityChanged();
         } else {
-            Runnable task = next.invokeChannelWritableStateChangedTask;
-            if (task == null) {
-                next.invokeChannelWritableStateChangedTask = task = new Runnable() {
-                    @Override
-                    public void run() {
-                        next.invokeChannelWritabilityChanged();
-                    }
-                };
+            Tasks tasks = next.invokeTasks;
+            if (tasks == null) {
+                next.invokeTasks = tasks = new Tasks(next);
             }
-            executor.execute(task);
+            executor.execute(tasks.invokeChannelWritableStateChangedTask);
         }
     }
 
@@ -472,15 +479,13 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
 
     @Override
     public ChannelFuture bind(final SocketAddress localAddress, final ChannelPromise promise) {
-        if (localAddress == null) {
-            throw new NullPointerException("localAddress");
-        }
+        ObjectUtil.checkNotNull(localAddress, "localAddress");
         if (isNotValidPromise(promise, false)) {
             // cancelled
             return promise;
         }
 
-        final AbstractChannelHandlerContext next = findContextOutbound();
+        final AbstractChannelHandlerContext next = findContextOutbound(MASK_BIND);
         EventExecutor executor = next.executor();
         if (executor.inEventLoop()) {
             next.invokeBind(localAddress, promise);
@@ -490,7 +495,7 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
                 public void run() {
                     next.invokeBind(localAddress, promise);
                 }
-            }, promise, null);
+            }, promise, null, false);
         }
         return promise;
     }
@@ -515,16 +520,14 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
     @Override
     public ChannelFuture connect(
             final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {
+        ObjectUtil.checkNotNull(remoteAddress, "remoteAddress");
 
-        if (remoteAddress == null) {
-            throw new NullPointerException("remoteAddress");
-        }
         if (isNotValidPromise(promise, false)) {
             // cancelled
             return promise;
         }
 
-        final AbstractChannelHandlerContext next = findContextOutbound();
+        final AbstractChannelHandlerContext next = findContextOutbound(MASK_CONNECT);
         EventExecutor executor = next.executor();
         if (executor.inEventLoop()) {
             next.invokeConnect(remoteAddress, localAddress, promise);
@@ -534,7 +537,7 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
                 public void run() {
                     next.invokeConnect(remoteAddress, localAddress, promise);
                 }
-            }, promise, null);
+            }, promise, null, false);
         }
         return promise;
     }
@@ -553,32 +556,27 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
 
     @Override
     public ChannelFuture disconnect(final ChannelPromise promise) {
+        if (!channel().metadata().hasDisconnect()) {
+            // Translate disconnect to close if the channel has no notion of disconnect-reconnect.
+            // So far, UDP/IP is the only transport that has such behavior.
+            return close(promise);
+        }
         if (isNotValidPromise(promise, false)) {
             // cancelled
             return promise;
         }
 
-        final AbstractChannelHandlerContext next = findContextOutbound();
+        final AbstractChannelHandlerContext next = findContextOutbound(MASK_DISCONNECT);
         EventExecutor executor = next.executor();
         if (executor.inEventLoop()) {
-            // Translate disconnect to close if the channel has no notion of disconnect-reconnect.
-            // So far, UDP/IP is the only transport that has such behavior.
-            if (!channel().metadata().hasDisconnect()) {
-                next.invokeClose(promise);
-            } else {
-                next.invokeDisconnect(promise);
-            }
+            next.invokeDisconnect(promise);
         } else {
             safeExecute(executor, new Runnable() {
                 @Override
                 public void run() {
-                    if (!channel().metadata().hasDisconnect()) {
-                        next.invokeClose(promise);
-                    } else {
-                        next.invokeDisconnect(promise);
-                    }
+                    next.invokeDisconnect(promise);
                 }
-            }, promise, null);
+            }, promise, null, false);
         }
         return promise;
     }
@@ -602,7 +600,7 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
             return promise;
         }
 
-        final AbstractChannelHandlerContext next = findContextOutbound();
+        final AbstractChannelHandlerContext next = findContextOutbound(MASK_CLOSE);
         EventExecutor executor = next.executor();
         if (executor.inEventLoop()) {
             next.invokeClose(promise);
@@ -612,7 +610,7 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
                 public void run() {
                     next.invokeClose(promise);
                 }
-            }, promise, null);
+            }, promise, null, false);
         }
 
         return promise;
@@ -637,7 +635,7 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
             return promise;
         }
 
-        final AbstractChannelHandlerContext next = findContextOutbound();
+        final AbstractChannelHandlerContext next = findContextOutbound(MASK_DEREGISTER);
         EventExecutor executor = next.executor();
         if (executor.inEventLoop()) {
             next.invokeDeregister(promise);
@@ -647,7 +645,7 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
                 public void run() {
                     next.invokeDeregister(promise);
                 }
-            }, promise, null);
+            }, promise, null, false);
         }
 
         return promise;
@@ -667,21 +665,16 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
 
     @Override
     public ChannelHandlerContext read() {
-        final AbstractChannelHandlerContext next = findContextOutbound();
+        final AbstractChannelHandlerContext next = findContextOutbound(MASK_READ);
         EventExecutor executor = next.executor();
         if (executor.inEventLoop()) {
             next.invokeRead();
         } else {
-            Runnable task = next.invokeReadTask;
-            if (task == null) {
-                next.invokeReadTask = task = new Runnable() {
-                    @Override
-                    public void run() {
-                        next.invokeRead();
-                    }
-                };
+            Tasks tasks = next.invokeTasks;
+            if (tasks == null) {
+                next.invokeTasks = tasks = new Tasks(next);
             }
-            executor.execute(task);
+            executor.execute(tasks.invokeReadTask);
         }
 
         return this;
@@ -706,26 +699,12 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
 
     @Override
     public ChannelFuture write(final Object msg, final ChannelPromise promise) {
-        if (msg == null) {
-            throw new NullPointerException("msg");
-        }
-
-        try {
-            if (isNotValidPromise(promise, true)) {
-                ReferenceCountUtil.release(msg);
-                // cancelled
-                return promise;
-            }
-        } catch (RuntimeException e) {
-            ReferenceCountUtil.release(msg);
-            throw e;
-        }
         write(msg, false, promise);
 
         return promise;
     }
 
-    private void invokeWrite(Object msg, ChannelPromise promise) {
+    void invokeWrite(Object msg, ChannelPromise promise) {
         if (invokeHandler()) {
             invokeWrite0(msg, promise);
         } else {
@@ -743,21 +722,16 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
 
     @Override
     public ChannelHandlerContext flush() {
-        final AbstractChannelHandlerContext next = findContextOutbound();
+        final AbstractChannelHandlerContext next = findContextOutbound(MASK_FLUSH);
         EventExecutor executor = next.executor();
         if (executor.inEventLoop()) {
             next.invokeFlush();
         } else {
-            Runnable task = next.invokeFlushTask;
-            if (task == null) {
-                next.invokeFlushTask = task = new Runnable() {
-                    @Override
-                    public void run() {
-                        next.invokeFlush();
-                    }
-                };
+            Tasks tasks = next.invokeTasks;
+            if (tasks == null) {
+                next.invokeTasks = tasks = new Tasks(next);
             }
-            safeExecute(executor, task, channel().voidPromise(), null);
+            safeExecute(executor, tasks.invokeFlushTask, channel().voidPromise(), null, false);
         }
 
         return this;
@@ -781,22 +755,11 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
 
     @Override
     public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) {
-        if (msg == null) {
-            throw new NullPointerException("msg");
-        }
-
-        if (isNotValidPromise(promise, true)) {
-            ReferenceCountUtil.release(msg);
-            // cancelled
-            return promise;
-        }
-
         write(msg, true, promise);
-
         return promise;
     }
 
-    private void invokeWriteAndFlush(Object msg, ChannelPromise promise) {
+    void invokeWriteAndFlush(Object msg, ChannelPromise promise) {
         if (invokeHandler()) {
             invokeWrite0(msg, promise);
             invokeFlush0();
@@ -806,7 +769,20 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
     }
 
     private void write(Object msg, boolean flush, ChannelPromise promise) {
-        AbstractChannelHandlerContext next = findContextOutbound();
+        ObjectUtil.checkNotNull(msg, "msg");
+        try {
+            if (isNotValidPromise(promise, true)) {
+                ReferenceCountUtil.release(msg);
+                // cancelled
+                return;
+            }
+        } catch (RuntimeException e) {
+            ReferenceCountUtil.release(msg);
+            throw e;
+        }
+
+        final AbstractChannelHandlerContext next = findContextOutbound(flush ?
+                (MASK_WRITE | MASK_FLUSH) : MASK_WRITE);
         final Object m = pipeline.touch(msg, next);
         EventExecutor executor = next.executor();
         if (executor.inEventLoop()) {
@@ -816,14 +792,9 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
                 next.invokeWrite(m, promise);
             }
         } else {
-            final AbstractWriteTask task;
-            if (flush) {
-                task = WriteAndFlushTask.newInstance(next, m, promise);
-            }  else {
-                task = WriteTask.newInstance(next, m, promise);
-            }
-            if (!safeExecute(executor, task, promise, m)) {
-                // We failed to submit the AbstractWriteTask. We need to cancel it so we decrement the pending bytes
+            final WriteTask task = WriteTask.newInstance(next, m, promise, flush);
+            if (!safeExecute(executor, task, promise, m, !flush)) {
+                // We failed to submit the WriteTask. We need to cancel it so we decrement the pending bytes
                 // and put it back in the Recycler for re-use later.
                 //
                 // See https://github.com/netty/netty/issues/8343.
@@ -901,9 +872,7 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
     }
 
     private boolean isNotValidPromise(ChannelPromise promise, boolean allowVoidPromise) {
-        if (promise == null) {
-            throw new NullPointerException("promise");
-        }
+        ObjectUtil.checkNotNull(promise, "promise");
 
         if (promise.isDone()) {
             // Check if the promise was cancelled and if so signal that the processing of the operation
@@ -937,22 +906,35 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
         return false;
     }
 
-    private AbstractChannelHandlerContext findContextInbound() {
+    private AbstractChannelHandlerContext findContextInbound(int mask) {
         AbstractChannelHandlerContext ctx = this;
+        EventExecutor currentExecutor = executor();
         do {
             ctx = ctx.next;
-        } while (!ctx.inbound);
+        } while (skipContext(ctx, currentExecutor, mask, MASK_ONLY_INBOUND));
         return ctx;
     }
 
-    private AbstractChannelHandlerContext findContextOutbound() {
+    private AbstractChannelHandlerContext findContextOutbound(int mask) {
         AbstractChannelHandlerContext ctx = this;
+        EventExecutor currentExecutor = executor();
         do {
             ctx = ctx.prev;
-        } while (!ctx.outbound);
+        } while (skipContext(ctx, currentExecutor, mask, MASK_ONLY_OUTBOUND));
         return ctx;
     }
 
+    private static boolean skipContext(
+            AbstractChannelHandlerContext ctx, EventExecutor currentExecutor, int mask, int onlyMask) {
+        // Ensure we correctly handle MASK_EXCEPTION_CAUGHT which is not included in the MASK_EXCEPTION_CAUGHT
+        return (ctx.executionMask & (onlyMask | mask)) == 0 ||
+                // We can only skip if the EventExecutor is the same as otherwise we need to ensure we offload
+                // everything to preserve ordering.
+                //
+                // See https://github.com/netty/netty/issues/10067
+                (ctx.executor() == currentExecutor && (ctx.executionMask & mask) == 0);
+    }
+
     @Override
     public ChannelPromise voidPromise() {
         return channel().voidPromise();
@@ -1031,9 +1013,14 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
         return channel().hasAttr(key);
     }
 
-    private static boolean safeExecute(EventExecutor executor, Runnable runnable, ChannelPromise promise, Object msg) {
+    private static boolean safeExecute(EventExecutor executor, Runnable runnable,
+            ChannelPromise promise, Object msg, boolean lazy) {
         try {
-            executor.execute(runnable);
+            if (lazy && executor instanceof AbstractEventExecutor) {
+                ((AbstractEventExecutor) executor).lazyExecute(runnable);
+            } else {
+                executor.execute(runnable);
+            }
             return true;
         } catch (Throwable cause) {
             try {
@@ -1057,28 +1044,41 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
         return StringUtil.simpleClassName(ChannelHandlerContext.class) + '(' + name + ", " + channel() + ')';
     }
 
-    abstract static class AbstractWriteTask implements Runnable {
+    static final class WriteTask implements Runnable {
+        private static final ObjectPool<WriteTask> RECYCLER = ObjectPool.newPool(new ObjectCreator<WriteTask>() {
+            @Override
+            public WriteTask newObject(Handle<WriteTask> handle) {
+                return new WriteTask(handle);
+            }
+        });
+
+        static WriteTask newInstance(AbstractChannelHandlerContext ctx,
+                Object msg, ChannelPromise promise, boolean flush) {
+            WriteTask task = RECYCLER.get();
+            init(task, ctx, msg, promise, flush);
+            return task;
+        }
 
         private static final boolean ESTIMATE_TASK_SIZE_ON_SUBMIT =
                 SystemPropertyUtil.getBoolean("io.netty.transport.estimateSizeOnSubmit", true);
 
-        // Assuming a 64-bit JVM, 16 bytes object header, 3 reference fields and one int field, plus alignment
+        // Assuming compressed oops, 12 bytes obj header, 4 ref fields and one int field
         private static final int WRITE_TASK_OVERHEAD =
-                SystemPropertyUtil.getInt("io.netty.transport.writeTaskSizeOverhead", 48);
+                SystemPropertyUtil.getInt("io.netty.transport.writeTaskSizeOverhead", 32);
 
-        private final Recycler.Handle<AbstractWriteTask> handle;
+        private final Handle<WriteTask> handle;
         private AbstractChannelHandlerContext ctx;
         private Object msg;
         private ChannelPromise promise;
-        private int size;
+        private int size; // sign bit controls flush
 
         @SuppressWarnings("unchecked")
-        private AbstractWriteTask(Recycler.Handle<? extends AbstractWriteTask> handle) {
-            this.handle = (Recycler.Handle<AbstractWriteTask>) handle;
+        private WriteTask(Handle<? extends WriteTask> handle) {
+            this.handle = (Handle<WriteTask>) handle;
         }
 
-        protected static void init(AbstractWriteTask task, AbstractChannelHandlerContext ctx,
-                                   Object msg, ChannelPromise promise) {
+        protected static void init(WriteTask task, AbstractChannelHandlerContext ctx,
+                                   Object msg, ChannelPromise promise, boolean flush) {
             task.ctx = ctx;
             task.msg = msg;
             task.promise = promise;
@@ -1089,13 +1089,20 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
             } else {
                 task.size = 0;
             }
+            if (flush) {
+                task.size |= Integer.MIN_VALUE;
+            }
         }
 
         @Override
-        public final void run() {
+        public void run() {
             try {
                 decrementPendingOutboundBytes();
-                write(ctx, msg, promise);
+                if (size >= 0) {
+                    ctx.invokeWrite(msg, promise);
+                } else {
+                    ctx.invokeWriteAndFlush(msg, promise);
+                }
             } finally {
                 recycle();
             }
@@ -1111,7 +1118,7 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
 
         private void decrementPendingOutboundBytes() {
             if (ESTIMATE_TASK_SIZE_ON_SUBMIT) {
-                ctx.pipeline.decrementPendingOutboundBytes(size);
+                ctx.pipeline.decrementPendingOutboundBytes(size & Integer.MAX_VALUE);
             }
         }
 
@@ -1122,57 +1129,37 @@ abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
             promise = null;
             handle.recycle(this);
         }
-
-        protected void write(AbstractChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
-            ctx.invokeWrite(msg, promise);
-        }
     }
 
-    static final class WriteTask extends AbstractWriteTask implements SingleThreadEventLoop.NonWakeupRunnable {
-
-        private static final Recycler<WriteTask> RECYCLER = new Recycler<WriteTask>() {
+    private static final class Tasks {
+        private final AbstractChannelHandlerContext next;
+        private final Runnable invokeChannelReadCompleteTask = new Runnable() {
             @Override
-            protected WriteTask newObject(Handle<WriteTask> handle) {
-                return new WriteTask(handle);
+            public void run() {
+                next.invokeChannelReadComplete();
             }
         };
-
-        static WriteTask newInstance(
-                AbstractChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
-            WriteTask task = RECYCLER.get();
-            init(task, ctx, msg, promise);
-            return task;
-        }
-
-        private WriteTask(Recycler.Handle<WriteTask> handle) {
-            super(handle);
-        }
-    }
-
-    static final class WriteAndFlushTask extends AbstractWriteTask {
-
-        private static final Recycler<WriteAndFlushTask> RECYCLER = new Recycler<WriteAndFlushTask>() {
+        private final Runnable invokeReadTask = new Runnable() {
             @Override
-            protected WriteAndFlushTask newObject(Handle<WriteAndFlushTask> handle) {
-                return new WriteAndFlushTask(handle);
+            public void run() {
+                next.invokeRead();
+            }
+        };
+        private final Runnable invokeChannelWritableStateChangedTask = new Runnable() {
+            @Override
+            public void run() {
+                next.invokeChannelWritabilityChanged();
+            }
+        };
+        private final Runnable invokeFlushTask = new Runnable() {
+            @Override
+            public void run() {
+                next.invokeFlush();
             }
         };
 
-        static WriteAndFlushTask newInstance(
-                AbstractChannelHandlerContext ctx, Object msg,  ChannelPromise promise) {
-            WriteAndFlushTask task = RECYCLER.get();
-            init(task, ctx, msg, promise);
-            return task;
-        }
-
-        private WriteAndFlushTask(Recycler.Handle<WriteAndFlushTask> handle) {
-            super(handle);
-        }
-
-        @Override
-        public void write(AbstractChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
-            super.write(ctx, msg, promise);
-            ctx.invokeFlush();
+        Tasks(AbstractChannelHandlerContext next) {
+            this.next = next;
         }
     }
 }
diff --git a/transport/src/main/java/io/netty/channel/AdaptiveRecvByteBufAllocator.java b/transport/src/main/java/io/netty/channel/AdaptiveRecvByteBufAllocator.java
index a2db615..d2456dd 100644
--- a/transport/src/main/java/io/netty/channel/AdaptiveRecvByteBufAllocator.java
+++ b/transport/src/main/java/io/netty/channel/AdaptiveRecvByteBufAllocator.java
@@ -18,6 +18,7 @@ package io.netty.channel;
 import java.util.ArrayList;
 import java.util.List;
 
+import static io.netty.util.internal.ObjectUtil.checkPositive;
 import static java.lang.Math.max;
 import static java.lang.Math.min;
 
@@ -95,7 +96,7 @@ public class AdaptiveRecvByteBufAllocator extends DefaultMaxMessagesRecvByteBufA
         private int nextReceiveBufferSize;
         private boolean decreaseNow;
 
-        public HandleImpl(int minIndex, int maxIndex, int initial) {
+        HandleImpl(int minIndex, int maxIndex, int initial) {
             this.minIndex = minIndex;
             this.maxIndex = maxIndex;
 
@@ -121,7 +122,7 @@ public class AdaptiveRecvByteBufAllocator extends DefaultMaxMessagesRecvByteBufA
         }
 
         private void record(int actualReadBytes) {
-            if (actualReadBytes <= SIZE_TABLE[max(0, index - INDEX_DECREMENT - 1)]) {
+            if (actualReadBytes <= SIZE_TABLE[max(0, index - INDEX_DECREMENT)]) {
                 if (decreaseNow) {
                     index = max(index - INDEX_DECREMENT, minIndex);
                     nextReceiveBufferSize = SIZE_TABLE[index];
@@ -163,9 +164,7 @@ public class AdaptiveRecvByteBufAllocator extends DefaultMaxMessagesRecvByteBufA
      * @param maximum  the inclusive upper bound of the expected buffer size
      */
     public AdaptiveRecvByteBufAllocator(int minimum, int initial, int maximum) {
-        if (minimum <= 0) {
-            throw new IllegalArgumentException("minimum: " + minimum);
-        }
+        checkPositive(minimum, "minimum");
         if (initial < minimum) {
             throw new IllegalArgumentException("initial: " + initial);
         }
diff --git a/transport/src/main/java/io/netty/channel/ChannelDuplexHandler.java b/transport/src/main/java/io/netty/channel/ChannelDuplexHandler.java
index 07c6484..eac4645 100644
--- a/transport/src/main/java/io/netty/channel/ChannelDuplexHandler.java
+++ b/transport/src/main/java/io/netty/channel/ChannelDuplexHandler.java
@@ -15,6 +15,8 @@
  */
 package io.netty.channel;
 
+import io.netty.channel.ChannelHandlerMask.Skip;
+
 import java.net.SocketAddress;
 
 /**
@@ -32,6 +34,7 @@ public class ChannelDuplexHandler extends ChannelInboundHandlerAdapter implement
      *
      * Sub-classes may override this method to change behavior.
      */
+    @Skip
     @Override
     public void bind(ChannelHandlerContext ctx, SocketAddress localAddress,
                      ChannelPromise promise) throws Exception {
@@ -44,6 +47,7 @@ public class ChannelDuplexHandler extends ChannelInboundHandlerAdapter implement
      *
      * Sub-classes may override this method to change behavior.
      */
+    @Skip
     @Override
     public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress,
                         SocketAddress localAddress, ChannelPromise promise) throws Exception {
@@ -56,6 +60,7 @@ public class ChannelDuplexHandler extends ChannelInboundHandlerAdapter implement
      *
      * Sub-classes may override this method to change behavior.
      */
+    @Skip
     @Override
     public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise)
             throws Exception {
@@ -68,6 +73,7 @@ public class ChannelDuplexHandler extends ChannelInboundHandlerAdapter implement
      *
      * Sub-classes may override this method to change behavior.
      */
+    @Skip
     @Override
     public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
         ctx.close(promise);
@@ -79,6 +85,7 @@ public class ChannelDuplexHandler extends ChannelInboundHandlerAdapter implement
      *
      * Sub-classes may override this method to change behavior.
      */
+    @Skip
     @Override
     public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
         ctx.deregister(promise);
@@ -90,6 +97,7 @@ public class ChannelDuplexHandler extends ChannelInboundHandlerAdapter implement
      *
      * Sub-classes may override this method to change behavior.
      */
+    @Skip
     @Override
     public void read(ChannelHandlerContext ctx) throws Exception {
         ctx.read();
@@ -101,6 +109,7 @@ public class ChannelDuplexHandler extends ChannelInboundHandlerAdapter implement
      *
      * Sub-classes may override this method to change behavior.
      */
+    @Skip
     @Override
     public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
         ctx.write(msg, promise);
@@ -112,6 +121,7 @@ public class ChannelDuplexHandler extends ChannelInboundHandlerAdapter implement
      *
      * Sub-classes may override this method to change behavior.
      */
+    @Skip
     @Override
     public void flush(ChannelHandlerContext ctx) throws Exception {
         ctx.flush();
diff --git a/transport/src/main/java/io/netty/channel/ChannelException.java b/transport/src/main/java/io/netty/channel/ChannelException.java
index 3aa89ff..32f4642 100644
--- a/transport/src/main/java/io/netty/channel/ChannelException.java
+++ b/transport/src/main/java/io/netty/channel/ChannelException.java
@@ -15,6 +15,10 @@
  */
 package io.netty.channel;
 
+import io.netty.util.internal.PlatformDependent;
+import io.netty.util.internal.SuppressJava6Requirement;
+import io.netty.util.internal.UnstableApi;
+
 /**
  * A {@link RuntimeException} which is thrown when an I/O operation fails.
  */
@@ -48,4 +52,19 @@ public class ChannelException extends RuntimeException {
     public ChannelException(Throwable cause) {
         super(cause);
     }
+
+    @UnstableApi
+    @SuppressJava6Requirement(reason = "uses Java 7+ RuntimeException.<init>(String, Throwable, boolean, boolean)" +
+            " but is guarded by version checks")
+    protected ChannelException(String message, Throwable cause, boolean shared) {
+        super(message, cause, false, true);
+        assert shared;
+    }
+
+    static ChannelException newStatic(String message, Throwable cause) {
+        if (PlatformDependent.javaVersion() >= 7) {
+            return new ChannelException(message, cause, true);
+        }
+        return new ChannelException(message, cause);
+    }
 }
diff --git a/transport/src/main/java/io/netty/channel/ChannelFlushPromiseNotifier.java b/transport/src/main/java/io/netty/channel/ChannelFlushPromiseNotifier.java
index 26594a3..eb8f97c 100644
--- a/transport/src/main/java/io/netty/channel/ChannelFlushPromiseNotifier.java
+++ b/transport/src/main/java/io/netty/channel/ChannelFlushPromiseNotifier.java
@@ -15,6 +15,10 @@
  */
 package io.netty.channel;
 
+import io.netty.util.internal.ObjectUtil;
+
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
+
 import java.util.ArrayDeque;
 import java.util.Queue;
 
@@ -61,12 +65,8 @@ public final class ChannelFlushPromiseNotifier {
      * {@code pendingDataSize} was reached.
      */
     public ChannelFlushPromiseNotifier add(ChannelPromise promise, long pendingDataSize) {
-        if (promise == null) {
-            throw new NullPointerException("promise");
-        }
-        if (pendingDataSize < 0) {
-            throw new IllegalArgumentException("pendingDataSize must be >= 0 but was " + pendingDataSize);
-        }
+        ObjectUtil.checkNotNull(promise, "promise");
+        checkPositiveOrZero(pendingDataSize, "pendingDataSize");
         long checkpoint = writeCounter + pendingDataSize;
         if (promise instanceof FlushCheckpoint) {
             FlushCheckpoint cp = (FlushCheckpoint) promise;
@@ -81,9 +81,7 @@ public final class ChannelFlushPromiseNotifier {
      * Increase the current write counter by the given delta
      */
     public ChannelFlushPromiseNotifier increaseWriteCounter(long delta) {
-        if (delta < 0) {
-            throw new IllegalArgumentException("delta must be >= 0 but was " + delta);
-        }
+        checkPositiveOrZero(delta, "delta");
         writeCounter += delta;
         return this;
     }
diff --git a/transport/src/main/java/io/netty/channel/ChannelHandler.java b/transport/src/main/java/io/netty/channel/ChannelHandler.java
index f940108..3879056 100644
--- a/transport/src/main/java/io/netty/channel/ChannelHandler.java
+++ b/transport/src/main/java/io/netty/channel/ChannelHandler.java
@@ -191,7 +191,8 @@ public interface ChannelHandler {
     /**
      * Gets called if a {@link Throwable} was thrown.
      *
-     * @deprecated is part of {@link ChannelInboundHandler}
+     * @deprecated if you want to handle this event you should implement {@link ChannelInboundHandler} and
+     * implement the method there.
      */
     @Deprecated
     void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception;
diff --git a/transport/src/main/java/io/netty/channel/ChannelHandlerAdapter.java b/transport/src/main/java/io/netty/channel/ChannelHandlerAdapter.java
index aadc691..2041ebd 100644
--- a/transport/src/main/java/io/netty/channel/ChannelHandlerAdapter.java
+++ b/transport/src/main/java/io/netty/channel/ChannelHandlerAdapter.java
@@ -16,6 +16,7 @@
 
 package io.netty.channel;
 
+import io.netty.channel.ChannelHandlerMask.Skip;
 import io.netty.util.internal.InternalThreadLocalMap;
 
 import java.util.Map;
@@ -81,8 +82,12 @@ public abstract class ChannelHandlerAdapter implements ChannelHandler {
      * to the next {@link ChannelHandler} in the {@link ChannelPipeline}.
      *
      * Sub-classes may override this method to change behavior.
+     *
+     * @deprecated is part of {@link ChannelInboundHandler}
      */
+    @Skip
     @Override
+    @Deprecated
     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
         ctx.fireExceptionCaught(cause);
     }
diff --git a/transport/src/main/java/io/netty/channel/ChannelHandlerMask.java b/transport/src/main/java/io/netty/channel/ChannelHandlerMask.java
new file mode 100644
index 0000000..d307508
--- /dev/null
+++ b/transport/src/main/java/io/netty/channel/ChannelHandlerMask.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2019 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.channel;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import io.netty.util.internal.PlatformDependent;
+
+import io.netty.util.internal.logging.InternalLogger;
+import io.netty.util.internal.logging.InternalLoggerFactory;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.Method;
+import java.net.SocketAddress;
+import java.security.AccessController;
+import java.security.PrivilegedExceptionAction;
+import java.util.Map;
+import java.util.WeakHashMap;
+
+final class ChannelHandlerMask {
+    private static final InternalLogger logger = InternalLoggerFactory.getInstance(ChannelHandlerMask.class);
+
+    // Using to mask which methods must be called for a ChannelHandler.
+    static final int MASK_EXCEPTION_CAUGHT = 1;
+    static final int MASK_CHANNEL_REGISTERED = 1 << 1;
+    static final int MASK_CHANNEL_UNREGISTERED = 1 << 2;
+    static final int MASK_CHANNEL_ACTIVE = 1 << 3;
+    static final int MASK_CHANNEL_INACTIVE = 1 << 4;
+    static final int MASK_CHANNEL_READ = 1 << 5;
+    static final int MASK_CHANNEL_READ_COMPLETE = 1 << 6;
+    static final int MASK_USER_EVENT_TRIGGERED = 1 << 7;
+    static final int MASK_CHANNEL_WRITABILITY_CHANGED = 1 << 8;
+    static final int MASK_BIND = 1 << 9;
+    static final int MASK_CONNECT = 1 << 10;
+    static final int MASK_DISCONNECT = 1 << 11;
+    static final int MASK_CLOSE = 1 << 12;
+    static final int MASK_DEREGISTER = 1 << 13;
+    static final int MASK_READ = 1 << 14;
+    static final int MASK_WRITE = 1 << 15;
+    static final int MASK_FLUSH = 1 << 16;
+
+    static final int MASK_ONLY_INBOUND =  MASK_CHANNEL_REGISTERED |
+            MASK_CHANNEL_UNREGISTERED | MASK_CHANNEL_ACTIVE | MASK_CHANNEL_INACTIVE | MASK_CHANNEL_READ |
+            MASK_CHANNEL_READ_COMPLETE | MASK_USER_EVENT_TRIGGERED | MASK_CHANNEL_WRITABILITY_CHANGED;
+    private static final int MASK_ALL_INBOUND = MASK_EXCEPTION_CAUGHT | MASK_ONLY_INBOUND;
+    static final int MASK_ONLY_OUTBOUND =  MASK_BIND | MASK_CONNECT | MASK_DISCONNECT |
+            MASK_CLOSE | MASK_DEREGISTER | MASK_READ | MASK_WRITE | MASK_FLUSH;
+    private static final int MASK_ALL_OUTBOUND = MASK_EXCEPTION_CAUGHT | MASK_ONLY_OUTBOUND;
+
+    private static final FastThreadLocal<Map<Class<? extends ChannelHandler>, Integer>> MASKS =
+            new FastThreadLocal<Map<Class<? extends ChannelHandler>, Integer>>() {
+                @Override
+                protected Map<Class<? extends ChannelHandler>, Integer> initialValue() {
+                    return new WeakHashMap<Class<? extends ChannelHandler>, Integer>(32);
+                }
+            };
+
+    /**
+     * Return the {@code executionMask}.
+     */
+    static int mask(Class<? extends ChannelHandler> clazz) {
+        // Try to obtain the mask from the cache first. If this fails calculate it and put it in the cache for fast
+        // lookup in the future.
+        Map<Class<? extends ChannelHandler>, Integer> cache = MASKS.get();
+        Integer mask = cache.get(clazz);
+        if (mask == null) {
+            mask = mask0(clazz);
+            cache.put(clazz, mask);
+        }
+        return mask;
+    }
+
+    /**
+     * Calculate the {@code executionMask}.
+     */
+    private static int mask0(Class<? extends ChannelHandler> handlerType) {
+        int mask = MASK_EXCEPTION_CAUGHT;
+        try {
+            if (ChannelInboundHandler.class.isAssignableFrom(handlerType)) {
+                mask |= MASK_ALL_INBOUND;
+
+                if (isSkippable(handlerType, "channelRegistered", ChannelHandlerContext.class)) {
+                    mask &= ~MASK_CHANNEL_REGISTERED;
+                }
+                if (isSkippable(handlerType, "channelUnregistered", ChannelHandlerContext.class)) {
+                    mask &= ~MASK_CHANNEL_UNREGISTERED;
+                }
+                if (isSkippable(handlerType, "channelActive", ChannelHandlerContext.class)) {
+                    mask &= ~MASK_CHANNEL_ACTIVE;
+                }
+                if (isSkippable(handlerType, "channelInactive", ChannelHandlerContext.class)) {
+                    mask &= ~MASK_CHANNEL_INACTIVE;
+                }
+                if (isSkippable(handlerType, "channelRead", ChannelHandlerContext.class, Object.class)) {
+                    mask &= ~MASK_CHANNEL_READ;
+                }
+                if (isSkippable(handlerType, "channelReadComplete", ChannelHandlerContext.class)) {
+                    mask &= ~MASK_CHANNEL_READ_COMPLETE;
+                }
+                if (isSkippable(handlerType, "channelWritabilityChanged", ChannelHandlerContext.class)) {
+                    mask &= ~MASK_CHANNEL_WRITABILITY_CHANGED;
+                }
+                if (isSkippable(handlerType, "userEventTriggered", ChannelHandlerContext.class, Object.class)) {
+                    mask &= ~MASK_USER_EVENT_TRIGGERED;
+                }
+            }
+
+            if (ChannelOutboundHandler.class.isAssignableFrom(handlerType)) {
+                mask |= MASK_ALL_OUTBOUND;
+
+                if (isSkippable(handlerType, "bind", ChannelHandlerContext.class,
+                        SocketAddress.class, ChannelPromise.class)) {
+                    mask &= ~MASK_BIND;
+                }
+                if (isSkippable(handlerType, "connect", ChannelHandlerContext.class, SocketAddress.class,
+                        SocketAddress.class, ChannelPromise.class)) {
+                    mask &= ~MASK_CONNECT;
+                }
+                if (isSkippable(handlerType, "disconnect", ChannelHandlerContext.class, ChannelPromise.class)) {
+                    mask &= ~MASK_DISCONNECT;
+                }
+                if (isSkippable(handlerType, "close", ChannelHandlerContext.class, ChannelPromise.class)) {
+                    mask &= ~MASK_CLOSE;
+                }
+                if (isSkippable(handlerType, "deregister", ChannelHandlerContext.class, ChannelPromise.class)) {
+                    mask &= ~MASK_DEREGISTER;
+                }
+                if (isSkippable(handlerType, "read", ChannelHandlerContext.class)) {
+                    mask &= ~MASK_READ;
+                }
+                if (isSkippable(handlerType, "write", ChannelHandlerContext.class,
+                        Object.class, ChannelPromise.class)) {
+                    mask &= ~MASK_WRITE;
+                }
+                if (isSkippable(handlerType, "flush", ChannelHandlerContext.class)) {
+                    mask &= ~MASK_FLUSH;
+                }
+            }
+
+            if (isSkippable(handlerType, "exceptionCaught", ChannelHandlerContext.class, Throwable.class)) {
+                mask &= ~MASK_EXCEPTION_CAUGHT;
+            }
+        } catch (Exception e) {
+            // Should never reach here.
+            PlatformDependent.throwException(e);
+        }
+
+        return mask;
+    }
+
+    @SuppressWarnings("rawtypes")
+    private static boolean isSkippable(
+            final Class<?> handlerType, final String methodName, final Class<?>... paramTypes) throws Exception {
+        return AccessController.doPrivileged(new PrivilegedExceptionAction<Boolean>() {
+            @Override
+            public Boolean run() throws Exception {
+                Method m;
+                try {
+                    m = handlerType.getMethod(methodName, paramTypes);
+                } catch (NoSuchMethodException e) {
+                    if (logger.isDebugEnabled()) {
+                        logger.debug(
+                            "Class {} missing method {}, assume we can not skip execution", handlerType, methodName, e);
+                    }
+                    return false;
+                }
+                return m != null && m.isAnnotationPresent(Skip.class);
+            }
+        });
+    }
+
+    private ChannelHandlerMask() { }
+
+    /**
+     * Indicates that the annotated event handler method in {@link ChannelHandler} will not be invoked by
+     * {@link ChannelPipeline} and so <strong>MUST</strong> only be used when the {@link ChannelHandler}
+     * method does nothing except forward to the next {@link ChannelHandler} in the pipeline.
+     * <p>
+     * Note that this annotation is not {@linkplain Inherited inherited}. If a user overrides a method annotated with
+     * {@link Skip}, it will not be skipped anymore. Similarly, the user can override a method not annotated with
+     * {@link Skip} and simply pass the event through to the next handler, which reverses the behavior of the
+     * supertype.
+     * </p>
+     */
+    @Target(ElementType.METHOD)
+    @Retention(RetentionPolicy.RUNTIME)
+    @interface Skip {
+        // no value
+    }
+}
diff --git a/transport/src/main/java/io/netty/channel/ChannelInboundHandlerAdapter.java b/transport/src/main/java/io/netty/channel/ChannelInboundHandlerAdapter.java
index d0a1d2d..f9a8884 100644
--- a/transport/src/main/java/io/netty/channel/ChannelInboundHandlerAdapter.java
+++ b/transport/src/main/java/io/netty/channel/ChannelInboundHandlerAdapter.java
@@ -15,6 +15,8 @@
  */
 package io.netty.channel;
 
+import io.netty.channel.ChannelHandlerMask.Skip;
+
 /**
  * Abstract base class for {@link ChannelInboundHandler} implementations which provide
  * implementations of all of their methods.
@@ -37,6 +39,7 @@ public class ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implemen
      *
      * Sub-classes may override this method to change behavior.
      */
+    @Skip
     @Override
     public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
         ctx.fireChannelRegistered();
@@ -48,6 +51,7 @@ public class ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implemen
      *
      * Sub-classes may override this method to change behavior.
      */
+    @Skip
     @Override
     public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
         ctx.fireChannelUnregistered();
@@ -59,6 +63,7 @@ public class ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implemen
      *
      * Sub-classes may override this method to change behavior.
      */
+    @Skip
     @Override
     public void channelActive(ChannelHandlerContext ctx) throws Exception {
         ctx.fireChannelActive();
@@ -70,6 +75,7 @@ public class ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implemen
      *
      * Sub-classes may override this method to change behavior.
      */
+    @Skip
     @Override
     public void channelInactive(ChannelHandlerContext ctx) throws Exception {
         ctx.fireChannelInactive();
@@ -81,6 +87,7 @@ public class ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implemen
      *
      * Sub-classes may override this method to change behavior.
      */
+    @Skip
     @Override
     public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
         ctx.fireChannelRead(msg);
@@ -92,6 +99,7 @@ public class ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implemen
      *
      * Sub-classes may override this method to change behavior.
      */
+    @Skip
     @Override
     public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
         ctx.fireChannelReadComplete();
@@ -103,6 +111,7 @@ public class ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implemen
      *
      * Sub-classes may override this method to change behavior.
      */
+    @Skip
     @Override
     public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
         ctx.fireUserEventTriggered(evt);
@@ -114,6 +123,7 @@ public class ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implemen
      *
      * Sub-classes may override this method to change behavior.
      */
+    @Skip
     @Override
     public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
         ctx.fireChannelWritabilityChanged();
@@ -125,7 +135,9 @@ public class ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implemen
      *
      * Sub-classes may override this method to change behavior.
      */
+    @Skip
     @Override
+    @SuppressWarnings("deprecation")
     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
             throws Exception {
         ctx.fireExceptionCaught(cause);
diff --git a/transport/src/main/java/io/netty/channel/ChannelMetadata.java b/transport/src/main/java/io/netty/channel/ChannelMetadata.java
index c77f530..e3b116b 100644
--- a/transport/src/main/java/io/netty/channel/ChannelMetadata.java
+++ b/transport/src/main/java/io/netty/channel/ChannelMetadata.java
@@ -15,6 +15,8 @@
  */
 package io.netty.channel;
 
+import static io.netty.util.internal.ObjectUtil.checkPositive;
+
 import java.net.SocketAddress;
 
 /**
@@ -46,10 +48,7 @@ public final class ChannelMetadata {
      * set for {@link MaxMessagesRecvByteBufAllocator#maxMessagesPerRead()}. Must be {@code > 0}.
      */
     public ChannelMetadata(boolean hasDisconnect, int defaultMaxMessagesPerRead) {
-        if (defaultMaxMessagesPerRead <= 0) {
-            throw new IllegalArgumentException("defaultMaxMessagesPerRead: " + defaultMaxMessagesPerRead +
-                                               " (expected > 0)");
-        }
+        checkPositive(defaultMaxMessagesPerRead, "defaultMaxMessagesPerRead");
         this.hasDisconnect = hasDisconnect;
         this.defaultMaxMessagesPerRead = defaultMaxMessagesPerRead;
     }
diff --git a/transport/src/main/java/io/netty/channel/ChannelOption.java b/transport/src/main/java/io/netty/channel/ChannelOption.java
index 97bf315..4da4295 100644
--- a/transport/src/main/java/io/netty/channel/ChannelOption.java
+++ b/transport/src/main/java/io/netty/channel/ChannelOption.java
@@ -18,6 +18,7 @@ package io.netty.channel;
 import io.netty.buffer.ByteBufAllocator;
 import io.netty.util.AbstractConstant;
 import io.netty.util.ConstantPool;
+import io.netty.util.internal.ObjectUtil;
 
 import java.net.InetAddress;
 import java.net.NetworkInterface;
@@ -65,7 +66,10 @@ public class ChannelOption<T> extends AbstractConstant<ChannelOption<T>> {
     /**
      * Creates a new {@link ChannelOption} for the given {@code name} or fail with an
      * {@link IllegalArgumentException} if a {@link ChannelOption} for the given {@code name} exists.
+     *
+     * @deprecated use {@link #valueOf(String)}.
      */
+    @Deprecated
     @SuppressWarnings("unchecked")
     public static <T> ChannelOption<T> newInstance(String name) {
         return (ChannelOption<T>) pool.newInstance(name);
@@ -146,8 +150,6 @@ public class ChannelOption<T> extends AbstractConstant<ChannelOption<T>> {
      * may override this for special checks.
      */
     public void validate(T value) {
-        if (value == null) {
-            throw new NullPointerException("value");
-        }
+        ObjectUtil.checkNotNull(value, "value");
     }
 }
diff --git a/transport/src/main/java/io/netty/channel/ChannelOutboundBuffer.java b/transport/src/main/java/io/netty/channel/ChannelOutboundBuffer.java
index d3a934a..b3202c6 100644
--- a/transport/src/main/java/io/netty/channel/ChannelOutboundBuffer.java
+++ b/transport/src/main/java/io/netty/channel/ChannelOutboundBuffer.java
@@ -19,11 +19,13 @@ import io.netty.buffer.ByteBuf;
 import io.netty.buffer.ByteBufHolder;
 import io.netty.buffer.Unpooled;
 import io.netty.channel.socket.nio.NioSocketChannel;
-import io.netty.util.Recycler;
-import io.netty.util.Recycler.Handle;
 import io.netty.util.ReferenceCountUtil;
 import io.netty.util.concurrent.FastThreadLocal;
 import io.netty.util.internal.InternalThreadLocalMap;
+import io.netty.util.internal.ObjectPool;
+import io.netty.util.internal.ObjectPool.Handle;
+import io.netty.util.internal.ObjectPool.ObjectCreator;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.PromiseNotificationUtil;
 import io.netty.util.internal.SystemPropertyUtil;
 import io.netty.util.internal.logging.InternalLogger;
@@ -52,7 +54,7 @@ import static java.lang.Math.min;
 public final class ChannelOutboundBuffer {
     // Assuming a 64-bit JVM:
     //  - 16 bytes object header
-    //  - 8 reference fields
+    //  - 6 reference fields
     //  - 2 long fields
     //  - 2 int fields
     //  - 1 boolean field
@@ -220,6 +222,18 @@ public final class ChannelOutboundBuffer {
         return entry.msg;
     }
 
+    /**
+     * Return the current message flush progress.
+     * @return {@code 0} if nothing was flushed before for the current message or there is no current message
+     */
+    public long currentProgress() {
+        Entry entry = flushedEntry;
+        if (entry == null) {
+            return 0;
+        }
+        return entry.progress;
+    }
+
     /**
      * Notify the {@link ChannelPromise} of the current message about writing progress.
      */
@@ -227,9 +241,9 @@ public final class ChannelOutboundBuffer {
         Entry e = flushedEntry;
         assert e != null;
         ChannelPromise p = e.promise;
+        long progress = e.progress + amount;
+        e.progress = progress;
         if (p instanceof ChannelProgressivePromise) {
-            long progress = e.progress + amount;
-            e.progress = progress;
             ((ChannelProgressivePromise) p).tryProgress(progress, e.total);
         }
     }
@@ -754,9 +768,7 @@ public final class ChannelOutboundBuffer {
      * returns {@code false} or there are no more flushed messages to process.
      */
     public void forEachFlushedMessage(MessageProcessor processor) throws Exception {
-        if (processor == null) {
-            throw new NullPointerException("processor");
-        }
+        ObjectUtil.checkNotNull(processor, "processor");
 
         Entry entry = flushedEntry;
         if (entry == null) {
@@ -786,12 +798,12 @@ public final class ChannelOutboundBuffer {
     }
 
     static final class Entry {
-        private static final Recycler<Entry> RECYCLER = new Recycler<Entry>() {
+        private static final ObjectPool<Entry> RECYCLER = ObjectPool.newPool(new ObjectCreator<Entry>() {
             @Override
-            protected Entry newObject(Handle<Entry> handle) {
+            public Entry newObject(Handle<Entry> handle) {
                 return new Entry(handle);
             }
-        };
+        });
 
         private final Handle<Entry> handle;
         Entry next;
diff --git a/transport/src/main/java/io/netty/channel/ChannelOutboundHandlerAdapter.java b/transport/src/main/java/io/netty/channel/ChannelOutboundHandlerAdapter.java
index fa96892..c68bfdb 100644
--- a/transport/src/main/java/io/netty/channel/ChannelOutboundHandlerAdapter.java
+++ b/transport/src/main/java/io/netty/channel/ChannelOutboundHandlerAdapter.java
@@ -15,6 +15,8 @@
  */
 package io.netty.channel;
 
+import io.netty.channel.ChannelHandlerMask.Skip;
+
 import java.net.SocketAddress;
 
 /**
@@ -29,6 +31,7 @@ public class ChannelOutboundHandlerAdapter extends ChannelHandlerAdapter impleme
      *
      * Sub-classes may override this method to change behavior.
      */
+    @Skip
     @Override
     public void bind(ChannelHandlerContext ctx, SocketAddress localAddress,
             ChannelPromise promise) throws Exception {
@@ -41,6 +44,7 @@ public class ChannelOutboundHandlerAdapter extends ChannelHandlerAdapter impleme
      *
      * Sub-classes may override this method to change behavior.
      */
+    @Skip
     @Override
     public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress,
             SocketAddress localAddress, ChannelPromise promise) throws Exception {
@@ -53,6 +57,7 @@ public class ChannelOutboundHandlerAdapter extends ChannelHandlerAdapter impleme
      *
      * Sub-classes may override this method to change behavior.
      */
+    @Skip
     @Override
     public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise)
             throws Exception {
@@ -65,6 +70,7 @@ public class ChannelOutboundHandlerAdapter extends ChannelHandlerAdapter impleme
      *
      * Sub-classes may override this method to change behavior.
      */
+    @Skip
     @Override
     public void close(ChannelHandlerContext ctx, ChannelPromise promise)
             throws Exception {
@@ -77,6 +83,7 @@ public class ChannelOutboundHandlerAdapter extends ChannelHandlerAdapter impleme
      *
      * Sub-classes may override this method to change behavior.
      */
+    @Skip
     @Override
     public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
         ctx.deregister(promise);
@@ -88,6 +95,7 @@ public class ChannelOutboundHandlerAdapter extends ChannelHandlerAdapter impleme
      *
      * Sub-classes may override this method to change behavior.
      */
+    @Skip
     @Override
     public void read(ChannelHandlerContext ctx) throws Exception {
         ctx.read();
@@ -99,6 +107,7 @@ public class ChannelOutboundHandlerAdapter extends ChannelHandlerAdapter impleme
      *
      * Sub-classes may override this method to change behavior.
      */
+    @Skip
     @Override
     public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
         ctx.write(msg, promise);
@@ -110,6 +119,7 @@ public class ChannelOutboundHandlerAdapter extends ChannelHandlerAdapter impleme
      *
      * Sub-classes may override this method to change behavior.
      */
+    @Skip
     @Override
     public void flush(ChannelHandlerContext ctx) throws Exception {
         ctx.flush();
diff --git a/transport/src/main/java/io/netty/channel/CoalescingBufferQueue.java b/transport/src/main/java/io/netty/channel/CoalescingBufferQueue.java
index fd58360..346e53f 100644
--- a/transport/src/main/java/io/netty/channel/CoalescingBufferQueue.java
+++ b/transport/src/main/java/io/netty/channel/CoalescingBufferQueue.java
@@ -19,7 +19,6 @@ import io.netty.buffer.ByteBufAllocator;
 import io.netty.buffer.CompositeByteBuf;
 import io.netty.buffer.Unpooled;
 import io.netty.util.internal.ObjectUtil;
-import io.netty.util.internal.PlatformDependent;
 
 /**
  * A FIFO queue of bytes where producers add bytes by repeatedly adding {@link ByteBuf} and consumers take bytes in
diff --git a/transport/src/main/java/io/netty/channel/CombinedChannelDuplexHandler.java b/transport/src/main/java/io/netty/channel/CombinedChannelDuplexHandler.java
index b24ccab..bd0e569 100644
--- a/transport/src/main/java/io/netty/channel/CombinedChannelDuplexHandler.java
+++ b/transport/src/main/java/io/netty/channel/CombinedChannelDuplexHandler.java
@@ -19,6 +19,7 @@ import io.netty.buffer.ByteBufAllocator;
 import io.netty.util.Attribute;
 import io.netty.util.AttributeKey;
 import io.netty.util.concurrent.EventExecutor;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.ThrowableUtil;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
@@ -78,12 +79,9 @@ public class CombinedChannelDuplexHandler<I extends ChannelInboundHandler, O ext
                             " was constructed with non-default constructor.");
         }
 
-        if (inboundHandler == null) {
-            throw new NullPointerException("inboundHandler");
-        }
-        if (outboundHandler == null) {
-            throw new NullPointerException("outboundHandler");
-        }
+        ObjectUtil.checkNotNull(inboundHandler, "inboundHandler");
+        ObjectUtil.checkNotNull(outboundHandler, "outboundHandler");
+
         if (inboundHandler instanceof ChannelOutboundHandler) {
             throw new IllegalArgumentException(
                     "inboundHandler must not implement " +
diff --git a/transport/src/main/java/io/netty/channel/CompleteChannelFuture.java b/transport/src/main/java/io/netty/channel/CompleteChannelFuture.java
index 67a86e5..083029e 100644
--- a/transport/src/main/java/io/netty/channel/CompleteChannelFuture.java
+++ b/transport/src/main/java/io/netty/channel/CompleteChannelFuture.java
@@ -19,6 +19,7 @@ import io.netty.util.concurrent.CompleteFuture;
 import io.netty.util.concurrent.EventExecutor;
 import io.netty.util.concurrent.Future;
 import io.netty.util.concurrent.GenericFutureListener;
+import io.netty.util.internal.ObjectUtil;
 
 /**
  * A skeletal {@link ChannelFuture} implementation which represents a
@@ -35,10 +36,7 @@ abstract class CompleteChannelFuture extends CompleteFuture<Void> implements Cha
      */
     protected CompleteChannelFuture(Channel channel, EventExecutor executor) {
         super(executor);
-        if (channel == null) {
-            throw new NullPointerException("channel");
-        }
-        this.channel = channel;
+        this.channel = ObjectUtil.checkNotNull(channel, "channel");
     }
 
     @Override
diff --git a/transport/src/main/java/io/netty/channel/DefaultAddressedEnvelope.java b/transport/src/main/java/io/netty/channel/DefaultAddressedEnvelope.java
index 12cdabb..8fb46da 100644
--- a/transport/src/main/java/io/netty/channel/DefaultAddressedEnvelope.java
+++ b/transport/src/main/java/io/netty/channel/DefaultAddressedEnvelope.java
@@ -18,6 +18,7 @@ package io.netty.channel;
 
 import io.netty.util.ReferenceCountUtil;
 import io.netty.util.ReferenceCounted;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.StringUtil;
 
 import java.net.SocketAddress;
@@ -39,10 +40,7 @@ public class DefaultAddressedEnvelope<M, A extends SocketAddress> implements Add
      * {@code sender} address.
      */
     public DefaultAddressedEnvelope(M message, A recipient, A sender) {
-        if (message == null) {
-            throw new NullPointerException("message");
-        }
-
+        ObjectUtil.checkNotNull(message, "message");
         if (recipient == null && sender == null) {
             throw new NullPointerException("recipient and sender");
         }
diff --git a/transport/src/main/java/io/netty/channel/DefaultChannelConfig.java b/transport/src/main/java/io/netty/channel/DefaultChannelConfig.java
index 4118708..9966b06 100644
--- a/transport/src/main/java/io/netty/channel/DefaultChannelConfig.java
+++ b/transport/src/main/java/io/netty/channel/DefaultChannelConfig.java
@@ -16,6 +16,7 @@
 package io.netty.channel;
 
 import io.netty.buffer.ByteBufAllocator;
+import io.netty.util.internal.ObjectUtil;
 
 import java.util.IdentityHashMap;
 import java.util.Map;
@@ -36,6 +37,8 @@ import static io.netty.channel.ChannelOption.WRITE_BUFFER_LOW_WATER_MARK;
 import static io.netty.channel.ChannelOption.WRITE_BUFFER_WATER_MARK;
 import static io.netty.channel.ChannelOption.WRITE_SPIN_COUNT;
 import static io.netty.util.internal.ObjectUtil.checkNotNull;
+import static io.netty.util.internal.ObjectUtil.checkPositive;
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
 
 /**
  * The default {@link ChannelConfig} implementation.
@@ -99,9 +102,7 @@ public class DefaultChannelConfig implements ChannelConfig {
     @SuppressWarnings("unchecked")
     @Override
     public boolean setOptions(Map<ChannelOption<?>, ?> options) {
-        if (options == null) {
-            throw new NullPointerException("options");
-        }
+        ObjectUtil.checkNotNull(options, "options");
 
         boolean setAllOptions = true;
         for (Entry<ChannelOption<?>, ?> e: options.entrySet()) {
@@ -116,9 +117,7 @@ public class DefaultChannelConfig implements ChannelConfig {
     @Override
     @SuppressWarnings({ "unchecked", "deprecation" })
     public <T> T getOption(ChannelOption<T> option) {
-        if (option == null) {
-            throw new NullPointerException("option");
-        }
+        ObjectUtil.checkNotNull(option, "option");
 
         if (option == CONNECT_TIMEOUT_MILLIS) {
             return (T) Integer.valueOf(getConnectTimeoutMillis());
@@ -196,10 +195,7 @@ public class DefaultChannelConfig implements ChannelConfig {
     }
 
     protected <T> void validate(ChannelOption<T> option, T value) {
-        if (option == null) {
-            throw new NullPointerException("option");
-        }
-        option.validate(value);
+        ObjectUtil.checkNotNull(option, "option").validate(value);
     }
 
     @Override
@@ -209,10 +205,7 @@ public class DefaultChannelConfig implements ChannelConfig {
 
     @Override
     public ChannelConfig setConnectTimeoutMillis(int connectTimeoutMillis) {
-        if (connectTimeoutMillis < 0) {
-            throw new IllegalArgumentException(String.format(
-                    "connectTimeoutMillis: %d (expected: >= 0)", connectTimeoutMillis));
-        }
+        checkPositiveOrZero(connectTimeoutMillis, "connectTimeoutMillis");
         this.connectTimeoutMillis = connectTimeoutMillis;
         return this;
     }
@@ -261,10 +254,7 @@ public class DefaultChannelConfig implements ChannelConfig {
 
     @Override
     public ChannelConfig setWriteSpinCount(int writeSpinCount) {
-        if (writeSpinCount <= 0) {
-            throw new IllegalArgumentException(
-                    "writeSpinCount must be a positive integer.");
-        }
+        checkPositive(writeSpinCount, "writeSpinCount");
         // Integer.MAX_VALUE is used as a special value in the channel implementations to indicate the channel cannot
         // accept any more data, and results in the writeOp being set on the selector (or execute a runnable which tries
         // to flush later because the writeSpinCount quantum has been exhausted). This strategy prevents additional
@@ -283,10 +273,7 @@ public class DefaultChannelConfig implements ChannelConfig {
 
     @Override
     public ChannelConfig setAllocator(ByteBufAllocator allocator) {
-        if (allocator == null) {
-            throw new NullPointerException("allocator");
-        }
-        this.allocator = allocator;
+        this.allocator = ObjectUtil.checkNotNull(allocator, "allocator");
         return this;
     }
 
@@ -357,10 +344,7 @@ public class DefaultChannelConfig implements ChannelConfig {
 
     @Override
     public ChannelConfig setWriteBufferHighWaterMark(int writeBufferHighWaterMark) {
-        if (writeBufferHighWaterMark < 0) {
-            throw new IllegalArgumentException(
-                    "writeBufferHighWaterMark must be >= 0");
-        }
+        checkPositiveOrZero(writeBufferHighWaterMark, "writeBufferHighWaterMark");
         for (;;) {
             WriteBufferWaterMark waterMark = writeBufferWaterMark;
             if (writeBufferHighWaterMark < waterMark.low()) {
@@ -383,10 +367,7 @@ public class DefaultChannelConfig implements ChannelConfig {
 
     @Override
     public ChannelConfig setWriteBufferLowWaterMark(int writeBufferLowWaterMark) {
-        if (writeBufferLowWaterMark < 0) {
-            throw new IllegalArgumentException(
-                    "writeBufferLowWaterMark must be >= 0");
-        }
+        checkPositiveOrZero(writeBufferLowWaterMark, "writeBufferLowWaterMark");
         for (;;) {
             WriteBufferWaterMark waterMark = writeBufferWaterMark;
             if (writeBufferLowWaterMark > waterMark.high()) {
@@ -420,10 +401,7 @@ public class DefaultChannelConfig implements ChannelConfig {
 
     @Override
     public ChannelConfig setMessageSizeEstimator(MessageSizeEstimator estimator) {
-        if (estimator == null) {
-            throw new NullPointerException("estimator");
-        }
-        msgSizeEstimator = estimator;
+        this.msgSizeEstimator = ObjectUtil.checkNotNull(estimator, "estimator");
         return this;
     }
 
diff --git a/transport/src/main/java/io/netty/channel/DefaultChannelHandlerContext.java b/transport/src/main/java/io/netty/channel/DefaultChannelHandlerContext.java
index 58454b8..26f11d5 100644
--- a/transport/src/main/java/io/netty/channel/DefaultChannelHandlerContext.java
+++ b/transport/src/main/java/io/netty/channel/DefaultChannelHandlerContext.java
@@ -23,10 +23,7 @@ final class DefaultChannelHandlerContext extends AbstractChannelHandlerContext {
 
     DefaultChannelHandlerContext(
             DefaultChannelPipeline pipeline, EventExecutor executor, String name, ChannelHandler handler) {
-        super(pipeline, executor, name, isInbound(handler), isOutbound(handler));
-        if (handler == null) {
-            throw new NullPointerException("handler");
-        }
+        super(pipeline, executor, name, handler.getClass());
         this.handler = handler;
     }
 
@@ -34,12 +31,4 @@ final class DefaultChannelHandlerContext extends AbstractChannelHandlerContext {
     public ChannelHandler handler() {
         return handler;
     }
-
-    private static boolean isInbound(ChannelHandler handler) {
-        return handler instanceof ChannelInboundHandler;
-    }
-
-    private static boolean isOutbound(ChannelHandler handler) {
-        return handler instanceof ChannelOutboundHandler;
-    }
 }
diff --git a/transport/src/main/java/io/netty/channel/DefaultChannelPipeline.java b/transport/src/main/java/io/netty/channel/DefaultChannelPipeline.java
index 2b307cc..dd8531f 100644
--- a/transport/src/main/java/io/netty/channel/DefaultChannelPipeline.java
+++ b/transport/src/main/java/io/netty/channel/DefaultChannelPipeline.java
@@ -341,9 +341,7 @@ public class DefaultChannelPipeline implements ChannelPipeline {
 
     @Override
     public final ChannelPipeline addFirst(EventExecutorGroup executor, ChannelHandler... handlers) {
-        if (handlers == null) {
-            throw new NullPointerException("handlers");
-        }
+        ObjectUtil.checkNotNull(handlers, "handlers");
         if (handlers.length == 0 || handlers[0] == null) {
             return this;
         }
@@ -374,9 +372,7 @@ public class DefaultChannelPipeline implements ChannelPipeline {
 
     @Override
     public final ChannelPipeline addLast(EventExecutorGroup executor, ChannelHandler... handlers) {
-        if (handlers == null) {
-            throw new NullPointerException("handlers");
-        }
+        ObjectUtil.checkNotNull(handlers, "handlers");
 
         for (ChannelHandler h: handlers) {
             if (h == null) {
@@ -457,7 +453,7 @@ public class DefaultChannelPipeline implements ChannelPipeline {
         assert ctx != head && ctx != tail;
 
         synchronized (this) {
-            remove0(ctx);
+            atomicRemoveFromHandlerList(ctx);
 
             // If the registered is false it means that the channel was not registered on an eventloop yet.
             // In this case we remove the context from the pipeline and add a task that will call
@@ -482,7 +478,10 @@ public class DefaultChannelPipeline implements ChannelPipeline {
         return ctx;
     }
 
-    private static void remove0(AbstractChannelHandlerContext ctx) {
+    /**
+     * Method is synchronized to make the handler removal from the double linked list atomic.
+     */
+    private synchronized void atomicRemoveFromHandlerList(AbstractChannelHandlerContext ctx) {
         AbstractChannelHandlerContext prev = ctx.prev;
         AbstractChannelHandlerContext next = ctx.next;
         prev.next = next;
@@ -611,7 +610,7 @@ public class DefaultChannelPipeline implements ChannelPipeline {
         } catch (Throwable t) {
             boolean removed = false;
             try {
-                remove0(ctx);
+                atomicRemoveFromHandlerList(ctx);
                 ctx.callHandlerRemoved();
                 removed = true;
             } catch (Throwable t2) {
@@ -711,18 +710,12 @@ public class DefaultChannelPipeline implements ChannelPipeline {
 
     @Override
     public final ChannelHandlerContext context(String name) {
-        if (name == null) {
-            throw new NullPointerException("name");
-        }
-
-        return context0(name);
+        return context0(ObjectUtil.checkNotNull(name, "name"));
     }
 
     @Override
     public final ChannelHandlerContext context(ChannelHandler handler) {
-        if (handler == null) {
-            throw new NullPointerException("handler");
-        }
+        ObjectUtil.checkNotNull(handler, "handler");
 
         AbstractChannelHandlerContext ctx = head.next;
         for (;;) {
@@ -741,9 +734,7 @@ public class DefaultChannelPipeline implements ChannelPipeline {
 
     @Override
     public final ChannelHandlerContext context(Class<? extends ChannelHandler> handlerType) {
-        if (handlerType == null) {
-            throw new NullPointerException("handlerType");
-        }
+        ObjectUtil.checkNotNull(handlerType, "handlerType");
 
         AbstractChannelHandlerContext ctx = head.next;
         for (;;) {
@@ -881,9 +872,7 @@ public class DefaultChannelPipeline implements ChannelPipeline {
 
             final EventExecutor executor = ctx.executor();
             if (inEventLoop || executor.inEventLoop(currentThread)) {
-                synchronized (this) {
-                    remove0(ctx);
-                }
+                atomicRemoveFromHandlerList(ctx);
                 callHandlerRemoved0(ctx);
             } else {
                 final AbstractChannelHandlerContext finalCtx = ctx;
@@ -1198,6 +1187,19 @@ public class DefaultChannelPipeline implements ChannelPipeline {
         }
     }
 
+    /**
+     * Called once a message hit the end of the {@link ChannelPipeline} without been handled by the user
+     * in {@link ChannelInboundHandler#channelRead(ChannelHandlerContext, Object)}. This method is responsible
+     * to call {@link ReferenceCountUtil#release(Object)} on the given msg at some point.
+     */
+    protected void onUnhandledInboundMessage(ChannelHandlerContext ctx, Object msg) {
+        onUnhandledInboundMessage(msg);
+        if (logger.isDebugEnabled()) {
+            logger.debug("Discarded message pipeline : {}. Channel : {}.",
+                         ctx.pipeline().names(), ctx.channel());
+        }
+    }
+
     /**
      * Called once the {@link ChannelInboundHandler#channelReadComplete(ChannelHandlerContext)} event hit
      * the end of the {@link ChannelPipeline}.
@@ -1243,7 +1245,7 @@ public class DefaultChannelPipeline implements ChannelPipeline {
     final class TailContext extends AbstractChannelHandlerContext implements ChannelInboundHandler {
 
         TailContext(DefaultChannelPipeline pipeline) {
-            super(pipeline, null, TAIL_NAME, true, false);
+            super(pipeline, null, TAIL_NAME, TailContext.class);
             setAddComplete();
         }
 
@@ -1291,7 +1293,7 @@ public class DefaultChannelPipeline implements ChannelPipeline {
 
         @Override
         public void channelRead(ChannelHandlerContext ctx, Object msg) {
-            onUnhandledInboundMessage(msg);
+            onUnhandledInboundMessage(ctx, msg);
         }
 
         @Override
@@ -1306,7 +1308,7 @@ public class DefaultChannelPipeline implements ChannelPipeline {
         private final Unsafe unsafe;
 
         HeadContext(DefaultChannelPipeline pipeline) {
-            super(pipeline, null, HEAD_NAME, true, true);
+            super(pipeline, null, HEAD_NAME, HeadContext.class);
             unsafe = pipeline.channel().unsafe();
             setAddComplete();
         }
@@ -1468,7 +1470,7 @@ public class DefaultChannelPipeline implements ChannelPipeline {
                                 "Can't invoke handlerAdded() as the EventExecutor {} rejected it, removing handler {}.",
                                 executor, ctx.name(), e);
                     }
-                    remove0(ctx);
+                    atomicRemoveFromHandlerList(ctx);
                     ctx.setRemoved();
                 }
             }
diff --git a/transport/src/main/java/io/netty/channel/DefaultEventLoopGroup.java b/transport/src/main/java/io/netty/channel/DefaultEventLoopGroup.java
index ee5eeea..bbbec3b 100644
--- a/transport/src/main/java/io/netty/channel/DefaultEventLoopGroup.java
+++ b/transport/src/main/java/io/netty/channel/DefaultEventLoopGroup.java
@@ -39,6 +39,15 @@ public class DefaultEventLoopGroup extends MultithreadEventLoopGroup {
         this(nThreads, (ThreadFactory) null);
     }
 
+    /**
+     * Create a new instance with the default number of threads and the given {@link ThreadFactory}.
+     *
+     * @param threadFactory     the {@link ThreadFactory} or {@code null} to use the default
+     */
+    public DefaultEventLoopGroup(ThreadFactory threadFactory) {
+        this(0, threadFactory);
+    }
+
     /**
      * Create a new instance
      *
diff --git a/transport/src/main/java/io/netty/channel/DefaultFileRegion.java b/transport/src/main/java/io/netty/channel/DefaultFileRegion.java
index 0e8d486..867f3f5 100644
--- a/transport/src/main/java/io/netty/channel/DefaultFileRegion.java
+++ b/transport/src/main/java/io/netty/channel/DefaultFileRegion.java
@@ -17,6 +17,7 @@ package io.netty.channel;
 
 import io.netty.util.AbstractReferenceCounted;
 import io.netty.util.IllegalReferenceCountException;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 
@@ -26,6 +27,8 @@ import java.io.RandomAccessFile;
 import java.nio.channels.FileChannel;
 import java.nio.channels.WritableByteChannel;
 
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
+
 /**
  * Default {@link FileRegion} implementation which transfer data from a {@link FileChannel} or {@link File}.
  *
@@ -49,19 +52,10 @@ public class DefaultFileRegion extends AbstractReferenceCounted implements FileR
      * @param count     the number of bytes to transfer
      */
     public DefaultFileRegion(FileChannel file, long position, long count) {
-        if (file == null) {
-            throw new NullPointerException("file");
-        }
-        if (position < 0) {
-            throw new IllegalArgumentException("position must be >= 0 but was " + position);
-        }
-        if (count < 0) {
-            throw new IllegalArgumentException("count must be >= 0 but was " + count);
-        }
-        this.file = file;
-        this.position = position;
-        this.count = count;
-        f = null;
+        this.file = ObjectUtil.checkNotNull(file, "file");
+        this.position = checkPositiveOrZero(position, "position");
+        this.count = checkPositiveOrZero(count, "count");
+        this.f = null;
     }
 
     /**
@@ -73,18 +67,9 @@ public class DefaultFileRegion extends AbstractReferenceCounted implements FileR
      * @param count     the number of bytes to transfer
      */
     public DefaultFileRegion(File f, long position, long count) {
-        if (f == null) {
-            throw new NullPointerException("f");
-        }
-        if (position < 0) {
-            throw new IllegalArgumentException("position must be >= 0 but was " + position);
-        }
-        if (count < 0) {
-            throw new IllegalArgumentException("count must be >= 0 but was " + count);
-        }
-        this.position = position;
-        this.count = count;
-        this.f = f;
+        this.f = ObjectUtil.checkNotNull(f, "f");
+        this.position = checkPositiveOrZero(position, "position");
+        this.count = checkPositiveOrZero(count, "count");
     }
 
     /**
@@ -145,6 +130,12 @@ public class DefaultFileRegion extends AbstractReferenceCounted implements FileR
         long written = file.transferTo(this.position + position, count, target);
         if (written > 0) {
             transferred += written;
+        } else if (written == 0) {
+            // If the amount of written data is 0 we need to check if the requested count is bigger then the
+            // actual file itself as it may have been truncated on disk.
+            //
+            // See https://github.com/netty/netty/issues/8868
+            validate(this, position);
         }
         return written;
     }
@@ -161,9 +152,7 @@ public class DefaultFileRegion extends AbstractReferenceCounted implements FileR
         try {
             file.close();
         } catch (IOException e) {
-            if (logger.isWarnEnabled()) {
-                logger.warn("Failed to close a file.", e);
-            }
+            logger.warn("Failed to close a file.", e);
         }
     }
 
@@ -188,4 +177,16 @@ public class DefaultFileRegion extends AbstractReferenceCounted implements FileR
     public FileRegion touch(Object hint) {
         return this;
     }
+
+    static void validate(DefaultFileRegion region, long position) throws IOException {
+        // If the amount of written data is 0 we need to check if the requested count is bigger then the
+        // actual file itself as it may have been truncated on disk.
+        //
+        // See https://github.com/netty/netty/issues/8868
+        long size = region.file.size();
+        long count = region.count - position;
+        if (region.position + count + position > size) {
+            throw new IOException("Underlying file size " + size + " smaller then requested count " + region.count);
+        }
+    }
 }
diff --git a/transport/src/main/java/io/netty/channel/DefaultMaxBytesRecvByteBufAllocator.java b/transport/src/main/java/io/netty/channel/DefaultMaxBytesRecvByteBufAllocator.java
index 9077551..24f40b5 100644
--- a/transport/src/main/java/io/netty/channel/DefaultMaxBytesRecvByteBufAllocator.java
+++ b/transport/src/main/java/io/netty/channel/DefaultMaxBytesRecvByteBufAllocator.java
@@ -15,6 +15,8 @@
  */
 package io.netty.channel;
 
+import static io.netty.util.internal.ObjectUtil.checkPositive;
+
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.ByteBufAllocator;
 import io.netty.util.UncheckedBooleanSupplier;
@@ -124,9 +126,7 @@ public class DefaultMaxBytesRecvByteBufAllocator implements MaxBytesRecvByteBufA
 
     @Override
     public DefaultMaxBytesRecvByteBufAllocator maxBytesPerRead(int maxBytesPerRead) {
-        if (maxBytesPerRead <= 0) {
-            throw new IllegalArgumentException("maxBytesPerRead: " + maxBytesPerRead + " (expected: > 0)");
-        }
+        checkPositive(maxBytesPerRead, "maxBytesPerRead");
         // There is a dependency between this.maxBytesPerRead and this.maxBytesPerIndividualRead (a < b).
         // Write operations must be synchronized, but independent read operations can just be volatile.
         synchronized (this) {
@@ -149,10 +149,7 @@ public class DefaultMaxBytesRecvByteBufAllocator implements MaxBytesRecvByteBufA
 
     @Override
     public DefaultMaxBytesRecvByteBufAllocator maxBytesPerIndividualRead(int maxBytesPerIndividualRead) {
-        if (maxBytesPerIndividualRead <= 0) {
-            throw new IllegalArgumentException(
-                    "maxBytesPerIndividualRead: " + maxBytesPerIndividualRead + " (expected: > 0)");
-        }
+        checkPositive(maxBytesPerIndividualRead, "maxBytesPerIndividualRead");
         // There is a dependency between this.maxBytesPerRead and this.maxBytesPerIndividualRead (a < b).
         // Write operations must be synchronized, but independent read operations can just be volatile.
         synchronized (this) {
@@ -174,13 +171,8 @@ public class DefaultMaxBytesRecvByteBufAllocator implements MaxBytesRecvByteBufA
     }
 
     private static void checkMaxBytesPerReadPair(int maxBytesPerRead, int maxBytesPerIndividualRead) {
-        if (maxBytesPerRead <= 0) {
-            throw new IllegalArgumentException("maxBytesPerRead: " + maxBytesPerRead + " (expected: > 0)");
-        }
-        if (maxBytesPerIndividualRead <= 0) {
-            throw new IllegalArgumentException(
-                    "maxBytesPerIndividualRead: " + maxBytesPerIndividualRead + " (expected: > 0)");
-        }
+        checkPositive(maxBytesPerRead, "maxBytesPerRead");
+        checkPositive(maxBytesPerIndividualRead, "maxBytesPerIndividualRead");
         if (maxBytesPerRead < maxBytesPerIndividualRead) {
             throw new IllegalArgumentException(
                     "maxBytesPerRead cannot be less than " +
diff --git a/transport/src/main/java/io/netty/channel/DefaultMaxMessagesRecvByteBufAllocator.java b/transport/src/main/java/io/netty/channel/DefaultMaxMessagesRecvByteBufAllocator.java
index 0a44822..2e13606 100644
--- a/transport/src/main/java/io/netty/channel/DefaultMaxMessagesRecvByteBufAllocator.java
+++ b/transport/src/main/java/io/netty/channel/DefaultMaxMessagesRecvByteBufAllocator.java
@@ -15,6 +15,8 @@
  */
 package io.netty.channel;
 
+import static io.netty.util.internal.ObjectUtil.checkPositive;
+
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.ByteBufAllocator;
 import io.netty.util.UncheckedBooleanSupplier;
@@ -42,9 +44,7 @@ public abstract class DefaultMaxMessagesRecvByteBufAllocator implements MaxMessa
 
     @Override
     public MaxMessagesRecvByteBufAllocator maxMessagesPerRead(int maxMessagesPerRead) {
-        if (maxMessagesPerRead <= 0) {
-            throw new IllegalArgumentException("maxMessagesPerRead: " + maxMessagesPerRead + " (expected: > 0)");
-        }
+        checkPositive(maxMessagesPerRead, "maxMessagesPerRead");
         this.maxMessagesPerRead = maxMessagesPerRead;
         return this;
     }
diff --git a/transport/src/main/java/io/netty/channel/DefaultMessageSizeEstimator.java b/transport/src/main/java/io/netty/channel/DefaultMessageSizeEstimator.java
index 1459743..cedbb2a 100644
--- a/transport/src/main/java/io/netty/channel/DefaultMessageSizeEstimator.java
+++ b/transport/src/main/java/io/netty/channel/DefaultMessageSizeEstimator.java
@@ -15,6 +15,8 @@
  */
 package io.netty.channel;
 
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
+
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.ByteBufHolder;
 
@@ -59,9 +61,7 @@ public final class DefaultMessageSizeEstimator implements MessageSizeEstimator {
      * @param unknownSize       The size which is returned for unknown messages.
      */
     public DefaultMessageSizeEstimator(int unknownSize) {
-        if (unknownSize < 0) {
-            throw new IllegalArgumentException("unknownSize: " + unknownSize + " (expected: >= 0)");
-        }
+        checkPositiveOrZero(unknownSize, "unknownSize");
         handle = new HandleImpl(unknownSize);
     }
 
diff --git a/transport/src/main/java/io/netty/channel/EventLoopTaskQueueFactory.java b/transport/src/main/java/io/netty/channel/EventLoopTaskQueueFactory.java
new file mode 100644
index 0000000..c2788da
--- /dev/null
+++ b/transport/src/main/java/io/netty/channel/EventLoopTaskQueueFactory.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2019 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.channel;
+
+import java.util.Queue;
+
+/**
+ * Factory used to create {@link Queue} instances that will be used to store tasks for an {@link EventLoop}.
+ *
+ * Generally speaking the returned {@link Queue} MUST be thread-safe and depending on the {@link EventLoop}
+ * implementation must be of type {@link java.util.concurrent.BlockingQueue}.
+ */
+public interface EventLoopTaskQueueFactory {
+
+    /**
+     * Returns a new {@link Queue} to use.
+     * @param maxCapacity the maximum amount of elements that can be stored in the {@link Queue} at a given point
+     *                    in time.
+     * @return the new queue.
+     */
+    Queue<Runnable> newTaskQueue(int maxCapacity);
+}
diff --git a/transport/src/main/java/io/netty/channel/ExtendedClosedChannelException.java b/transport/src/main/java/io/netty/channel/ExtendedClosedChannelException.java
new file mode 100644
index 0000000..3b908cd
--- /dev/null
+++ b/transport/src/main/java/io/netty/channel/ExtendedClosedChannelException.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2019 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.channel;
+
+import java.nio.channels.ClosedChannelException;
+
+final class ExtendedClosedChannelException extends ClosedChannelException {
+
+    ExtendedClosedChannelException(Throwable cause) {
+        if (cause != null) {
+            initCause(cause);
+        }
+    }
+
+    @Override
+    public Throwable fillInStackTrace() {
+        return this;
+    }
+}
diff --git a/transport/src/main/java/io/netty/channel/FailedChannelFuture.java b/transport/src/main/java/io/netty/channel/FailedChannelFuture.java
index 92f83cd..f820905 100644
--- a/transport/src/main/java/io/netty/channel/FailedChannelFuture.java
+++ b/transport/src/main/java/io/netty/channel/FailedChannelFuture.java
@@ -16,6 +16,7 @@
 package io.netty.channel;
 
 import io.netty.util.concurrent.EventExecutor;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.PlatformDependent;
 
 /**
@@ -35,10 +36,7 @@ final class FailedChannelFuture extends CompleteChannelFuture {
      */
     FailedChannelFuture(Channel channel, EventExecutor executor, Throwable cause) {
         super(channel, executor);
-        if (cause == null) {
-            throw new NullPointerException("cause");
-        }
-        this.cause = cause;
+        this.cause = ObjectUtil.checkNotNull(cause, "cause");
     }
 
     @Override
diff --git a/transport/src/main/java/io/netty/channel/FixedRecvByteBufAllocator.java b/transport/src/main/java/io/netty/channel/FixedRecvByteBufAllocator.java
index 8dab77d..8bdd43d 100644
--- a/transport/src/main/java/io/netty/channel/FixedRecvByteBufAllocator.java
+++ b/transport/src/main/java/io/netty/channel/FixedRecvByteBufAllocator.java
@@ -15,6 +15,8 @@
  */
 package io.netty.channel;
 
+import static io.netty.util.internal.ObjectUtil.checkPositive;
+
 /**
  * The {@link RecvByteBufAllocator} that always yields the same buffer
  * size prediction.  This predictor ignores the feed back from the I/O thread.
@@ -26,7 +28,7 @@ public class FixedRecvByteBufAllocator extends DefaultMaxMessagesRecvByteBufAllo
     private final class HandleImpl extends MaxMessageHandle {
         private final int bufferSize;
 
-        public HandleImpl(int bufferSize) {
+        HandleImpl(int bufferSize) {
             this.bufferSize = bufferSize;
         }
 
@@ -41,10 +43,7 @@ public class FixedRecvByteBufAllocator extends DefaultMaxMessagesRecvByteBufAllo
      * the specified buffer size.
      */
     public FixedRecvByteBufAllocator(int bufferSize) {
-        if (bufferSize <= 0) {
-            throw new IllegalArgumentException(
-                    "bufferSize must greater than 0: " + bufferSize);
-        }
+        checkPositive(bufferSize, "bufferSize");
         this.bufferSize = bufferSize;
     }
 
diff --git a/transport/src/main/java/io/netty/channel/MessageSizeEstimator.java b/transport/src/main/java/io/netty/channel/MessageSizeEstimator.java
index 5c84927..92f164d 100644
--- a/transport/src/main/java/io/netty/channel/MessageSizeEstimator.java
+++ b/transport/src/main/java/io/netty/channel/MessageSizeEstimator.java
@@ -16,8 +16,8 @@
 package io.netty.channel;
 
 /**
- * Responsible to estimate size of a message. The size represent how much memory the message will ca. reserve in
- * memory.
+ * Responsible to estimate the size of a message. The size represents approximately how much memory the message will
+ * reserve in memory.
  */
 public interface MessageSizeEstimator {
 
diff --git a/transport/src/main/java/io/netty/channel/MultithreadEventLoopGroup.java b/transport/src/main/java/io/netty/channel/MultithreadEventLoopGroup.java
index a9bc23d..ba05062 100644
--- a/transport/src/main/java/io/netty/channel/MultithreadEventLoopGroup.java
+++ b/transport/src/main/java/io/netty/channel/MultithreadEventLoopGroup.java
@@ -96,4 +96,5 @@ public abstract class MultithreadEventLoopGroup extends MultithreadEventExecutor
     public ChannelFuture register(Channel channel, ChannelPromise promise) {
         return next().register(channel, promise);
     }
+
 }
diff --git a/transport/src/main/java/io/netty/channel/PendingWriteQueue.java b/transport/src/main/java/io/netty/channel/PendingWriteQueue.java
index 16ae47b..b945130 100644
--- a/transport/src/main/java/io/netty/channel/PendingWriteQueue.java
+++ b/transport/src/main/java/io/netty/channel/PendingWriteQueue.java
@@ -15,9 +15,10 @@
  */
 package io.netty.channel;
 
-import io.netty.util.Recycler;
 import io.netty.util.ReferenceCountUtil;
 import io.netty.util.concurrent.PromiseCombiner;
+import io.netty.util.internal.ObjectPool;
+import io.netty.util.internal.ObjectPool.ObjectCreator;
 import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.SystemPropertyUtil;
 import io.netty.util.internal.logging.InternalLogger;
@@ -92,12 +93,8 @@ public final class PendingWriteQueue {
      */
     public void add(Object msg, ChannelPromise promise) {
         assert ctx.executor().inEventLoop();
-        if (msg == null) {
-            throw new NullPointerException("msg");
-        }
-        if (promise == null) {
-            throw new NullPointerException("promise");
-        }
+        ObjectUtil.checkNotNull(msg, "msg");
+        ObjectUtil.checkNotNull(promise, "promise");
         // It is possible for writes to be triggered from removeAndFailAll(). To preserve ordering,
         // we should add them to the queue and let removeAndFailAll() fail them later.
         int messageSize = size(msg);
@@ -130,7 +127,7 @@ public final class PendingWriteQueue {
         }
 
         ChannelPromise p = ctx.newPromise();
-        PromiseCombiner combiner = new PromiseCombiner();
+        PromiseCombiner combiner = new PromiseCombiner(ctx.executor());
         try {
             // It is possible for some of the written promises to trigger more writes. The new writes
             // will "revive" the queue, so we need to write them up until the queue is empty.
@@ -165,9 +162,7 @@ public final class PendingWriteQueue {
      */
     public void removeAndFailAll(Throwable cause) {
         assert ctx.executor().inEventLoop();
-        if (cause == null) {
-            throw new NullPointerException("cause");
-        }
+        ObjectUtil.checkNotNull(cause, "cause");
         // It is possible for some of the failed promises to trigger more writes. The new writes
         // will "revive" the queue, so we need to clean them up until the queue is empty.
         for (PendingWrite write = head; write != null; write = head) {
@@ -192,11 +187,9 @@ public final class PendingWriteQueue {
      */
     public void removeAndFail(Throwable cause) {
         assert ctx.executor().inEventLoop();
-        if (cause == null) {
-            throw new NullPointerException("cause");
-        }
-        PendingWrite write = head;
+        ObjectUtil.checkNotNull(cause, "cause");
 
+        PendingWrite write = head;
         if (write == null) {
             return;
         }
@@ -292,20 +285,20 @@ public final class PendingWriteQueue {
      * Holds all meta-data and construct the linked-list structure.
      */
     static final class PendingWrite {
-        private static final Recycler<PendingWrite> RECYCLER = new Recycler<PendingWrite>() {
+        private static final ObjectPool<PendingWrite> RECYCLER = ObjectPool.newPool(new ObjectCreator<PendingWrite>() {
             @Override
-            protected PendingWrite newObject(Handle<PendingWrite> handle) {
+            public PendingWrite newObject(ObjectPool.Handle<PendingWrite> handle) {
                 return new PendingWrite(handle);
             }
-        };
+        });
 
-        private final Recycler.Handle<PendingWrite> handle;
+        private final ObjectPool.Handle<PendingWrite> handle;
         private PendingWrite next;
         private long size;
         private ChannelPromise promise;
         private Object msg;
 
-        private PendingWrite(Recycler.Handle<PendingWrite> handle) {
+        private PendingWrite(ObjectPool.Handle<PendingWrite> handle) {
             this.handle = handle;
         }
 
diff --git a/transport/src/main/java/io/netty/channel/SimpleChannelInboundHandler.java b/transport/src/main/java/io/netty/channel/SimpleChannelInboundHandler.java
index ddc516e..6fbdbde 100644
--- a/transport/src/main/java/io/netty/channel/SimpleChannelInboundHandler.java
+++ b/transport/src/main/java/io/netty/channel/SimpleChannelInboundHandler.java
@@ -38,12 +38,6 @@ import io.netty.util.internal.TypeParameterMatcher;
  * Be aware that depending of the constructor parameters it will release all handled messages by passing them to
  * {@link ReferenceCountUtil#release(Object)}. In this case you may need to use
  * {@link ReferenceCountUtil#retain(Object)} if you pass the object to the next handler in the {@link ChannelPipeline}.
- *
- * <h3>Forward compatibility notice</h3>
- * <p>
- * Please keep in mind that {@link #channelRead0(ChannelHandlerContext, I)} will be renamed to
- * {@code messageReceived(ChannelHandlerContext, I)} in 5.0.
- * </p>
  */
 public abstract class SimpleChannelInboundHandler<I> extends ChannelInboundHandlerAdapter {
 
@@ -115,9 +109,6 @@ public abstract class SimpleChannelInboundHandler<I> extends ChannelInboundHandl
     }
 
     /**
-     * <strong>Please keep in mind that this method will be renamed to
-     * {@code messageReceived(ChannelHandlerContext, I)} in 5.0.</strong>
-     *
      * Is called for each message of type {@link I}.
      *
      * @param ctx           the {@link ChannelHandlerContext} which this {@link SimpleChannelInboundHandler}
diff --git a/transport/src/main/java/io/netty/channel/SingleThreadEventLoop.java b/transport/src/main/java/io/netty/channel/SingleThreadEventLoop.java
index c547b34..7a08ba8 100644
--- a/transport/src/main/java/io/netty/channel/SingleThreadEventLoop.java
+++ b/transport/src/main/java/io/netty/channel/SingleThreadEventLoop.java
@@ -59,6 +59,13 @@ public abstract class SingleThreadEventLoop extends SingleThreadEventExecutor im
         tailTasks = newTaskQueue(maxPendingTasks);
     }
 
+    protected SingleThreadEventLoop(EventLoopGroup parent, Executor executor,
+                                    boolean addTaskWakesUp, Queue<Runnable> taskQueue, Queue<Runnable> tailTaskQueue,
+                                    RejectedExecutionHandler rejectedExecutionHandler) {
+        super(parent, executor, addTaskWakesUp, taskQueue, rejectedExecutionHandler);
+        tailTasks = ObjectUtil.checkNotNull(tailTaskQueue, "tailTaskQueue");
+    }
+
     @Override
     public EventLoopGroup parent() {
         return (EventLoopGroup) super.parent();
@@ -84,13 +91,8 @@ public abstract class SingleThreadEventLoop extends SingleThreadEventExecutor im
     @Deprecated
     @Override
     public ChannelFuture register(final Channel channel, final ChannelPromise promise) {
-        if (channel == null) {
-            throw new NullPointerException("channel");
-        }
-        if (promise == null) {
-            throw new NullPointerException("promise");
-        }
-
+        ObjectUtil.checkNotNull(promise, "promise");
+        ObjectUtil.checkNotNull(channel, "channel");
         channel.unsafe().register(this, promise);
         return promise;
     }
@@ -111,7 +113,7 @@ public abstract class SingleThreadEventLoop extends SingleThreadEventExecutor im
             reject(task);
         }
 
-        if (wakesUpForTask(task)) {
+        if (!(task instanceof LazyRunnable) && wakesUpForTask(task)) {
             wakeup(inEventLoop());
         }
     }
@@ -128,11 +130,6 @@ public abstract class SingleThreadEventLoop extends SingleThreadEventExecutor im
         return tailTasks.remove(ObjectUtil.checkNotNull(task, "task"));
     }
 
-    @Override
-    protected boolean wakesUpForTask(Runnable task) {
-        return !(task instanceof NonWakeupRunnable);
-    }
-
     @Override
     protected void afterRunningAllTasks() {
         runAllTasksFrom(tailTasks);
@@ -149,7 +146,12 @@ public abstract class SingleThreadEventLoop extends SingleThreadEventExecutor im
     }
 
     /**
-     * Marker interface for {@link Runnable} that will not trigger an {@link #wakeup(boolean)} in all cases.
+     * Returns the number of {@link Channel}s registered with this {@link EventLoop} or {@code -1}
+     * if operation is not supported. The returned value is not guaranteed to be exact accurate and
+     * should be viewed as a best effort.
      */
-    interface NonWakeupRunnable extends Runnable { }
+    @UnstableApi
+    public int registeredChannels() {
+        return -1;
+    }
 }
diff --git a/transport/src/main/java/io/netty/channel/ThreadPerChannelEventLoop.java b/transport/src/main/java/io/netty/channel/ThreadPerChannelEventLoop.java
index 1d4b958..497e796 100644
--- a/transport/src/main/java/io/netty/channel/ThreadPerChannelEventLoop.java
+++ b/transport/src/main/java/io/netty/channel/ThreadPerChannelEventLoop.java
@@ -95,4 +95,9 @@ public class ThreadPerChannelEventLoop extends SingleThreadEventLoop {
         parent.activeChildren.remove(this);
         parent.idleChildren.add(this);
     }
+
+    @Override
+    public int registeredChannels() {
+        return 1;
+    }
 }
diff --git a/transport/src/main/java/io/netty/channel/ThreadPerChannelEventLoopGroup.java b/transport/src/main/java/io/netty/channel/ThreadPerChannelEventLoopGroup.java
index 7ee89d0..a3a95a6 100644
--- a/transport/src/main/java/io/netty/channel/ThreadPerChannelEventLoopGroup.java
+++ b/transport/src/main/java/io/netty/channel/ThreadPerChannelEventLoopGroup.java
@@ -18,6 +18,7 @@ package io.netty.channel;
 
 import io.netty.util.concurrent.AbstractEventExecutorGroup;
 import io.netty.util.concurrent.DefaultPromise;
+import io.netty.util.concurrent.DefaultThreadFactory;
 import io.netty.util.concurrent.EventExecutor;
 import io.netty.util.concurrent.Future;
 import io.netty.util.concurrent.FutureListener;
@@ -25,6 +26,7 @@ import io.netty.util.concurrent.GlobalEventExecutor;
 import io.netty.util.concurrent.Promise;
 import io.netty.util.concurrent.ThreadPerTaskExecutor;
 import io.netty.util.internal.EmptyArrays;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.PlatformDependent;
 import io.netty.util.internal.ReadOnlyIterator;
 import io.netty.util.internal.ThrowableUtil;
@@ -35,7 +37,6 @@ import java.util.Queue;
 import java.util.Set;
 import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
 import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.TimeUnit;
@@ -85,7 +86,7 @@ public class ThreadPerChannelEventLoopGroup extends AbstractEventExecutorGroup i
      *                          Use {@code 0} to use no limit
      */
     protected ThreadPerChannelEventLoopGroup(int maxChannels) {
-        this(maxChannels, Executors.defaultThreadFactory());
+        this(maxChannels, (ThreadFactory) null);
     }
 
     /**
@@ -101,7 +102,7 @@ public class ThreadPerChannelEventLoopGroup extends AbstractEventExecutorGroup i
      * @param args              arguments which will passed to each {@link #newChild(Object...)} call.
      */
     protected ThreadPerChannelEventLoopGroup(int maxChannels, ThreadFactory threadFactory, Object... args) {
-        this(maxChannels, new ThreadPerTaskExecutor(threadFactory), args);
+        this(maxChannels, threadFactory == null ? null : new ThreadPerTaskExecutor(threadFactory), args);
     }
 
     /**
@@ -117,12 +118,9 @@ public class ThreadPerChannelEventLoopGroup extends AbstractEventExecutorGroup i
      * @param args              arguments which will passed to each {@link #newChild(Object...)} call.
      */
     protected ThreadPerChannelEventLoopGroup(int maxChannels, Executor executor, Object... args) {
-        if (maxChannels < 0) {
-            throw new IllegalArgumentException(String.format(
-                    "maxChannels: %d (expected: >= 0)", maxChannels));
-        }
+        ObjectUtil.checkPositiveOrZero(maxChannels, "maxChannels");
         if (executor == null) {
-            throw new NullPointerException("executor");
+            executor = new ThreadPerTaskExecutor(new DefaultThreadFactory(getClass()));
         }
 
         if (args == null) {
@@ -135,7 +133,7 @@ public class ThreadPerChannelEventLoopGroup extends AbstractEventExecutorGroup i
         this.executor = executor;
 
         tooManyChannels = ThrowableUtil.unknownStackTrace(
-                new ChannelException("too many channels (max: " + maxChannels + ')'),
+                ChannelException.newStatic("too many channels (max: " + maxChannels + ')', null),
                 ThreadPerChannelEventLoopGroup.class, "nextChild()");
     }
 
@@ -274,9 +272,7 @@ public class ThreadPerChannelEventLoopGroup extends AbstractEventExecutorGroup i
 
     @Override
     public ChannelFuture register(Channel channel) {
-        if (channel == null) {
-            throw new NullPointerException("channel");
-        }
+        ObjectUtil.checkNotNull(channel, "channel");
         try {
             EventLoop l = nextChild();
             return l.register(new DefaultChannelPromise(channel, l));
@@ -298,9 +294,7 @@ public class ThreadPerChannelEventLoopGroup extends AbstractEventExecutorGroup i
     @Deprecated
     @Override
     public ChannelFuture register(Channel channel, ChannelPromise promise) {
-        if (channel == null) {
-            throw new NullPointerException("channel");
-        }
+        ObjectUtil.checkNotNull(channel, "channel");
         try {
             return nextChild().register(channel, promise);
         } catch (Throwable t) {
diff --git a/transport/src/main/java/io/netty/channel/VoidChannelPromise.java b/transport/src/main/java/io/netty/channel/VoidChannelPromise.java
index c684843..eeffb6b 100644
--- a/transport/src/main/java/io/netty/channel/VoidChannelPromise.java
+++ b/transport/src/main/java/io/netty/channel/VoidChannelPromise.java
@@ -18,6 +18,7 @@ package io.netty.channel;
 import io.netty.util.concurrent.AbstractFuture;
 import io.netty.util.concurrent.Future;
 import io.netty.util.concurrent.GenericFutureListener;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.UnstableApi;
 
 import java.util.concurrent.TimeUnit;
@@ -35,9 +36,7 @@ public final class VoidChannelPromise extends AbstractFuture<Void> implements Ch
      * @param channel the {@link Channel} associated with this future
      */
     public VoidChannelPromise(final Channel channel, boolean fireException) {
-        if (channel == null) {
-            throw new NullPointerException("channel");
-        }
+        ObjectUtil.checkNotNull(channel, "channel");
         this.channel = channel;
         if (fireException) {
             fireExceptionListener = new ChannelFutureListener() {
@@ -162,6 +161,7 @@ public final class VoidChannelPromise extends AbstractFuture<Void> implements Ch
         fail();
         return this;
     }
+
     @Override
     public VoidChannelPromise setFailure(Throwable cause) {
         fireException0(cause);
diff --git a/transport/src/main/java/io/netty/channel/WriteBufferWaterMark.java b/transport/src/main/java/io/netty/channel/WriteBufferWaterMark.java
index ee3d466..3deb74f 100644
--- a/transport/src/main/java/io/netty/channel/WriteBufferWaterMark.java
+++ b/transport/src/main/java/io/netty/channel/WriteBufferWaterMark.java
@@ -15,6 +15,8 @@
  */
 package io.netty.channel;
 
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
+
 /**
  * WriteBufferWaterMark is used to set low water mark and high water mark for the write buffer.
  * <p>
@@ -54,9 +56,7 @@ public final class WriteBufferWaterMark {
      */
     WriteBufferWaterMark(int low, int high, boolean validate) {
         if (validate) {
-            if (low < 0) {
-                throw new IllegalArgumentException("write buffer's low water mark must be >= 0");
-            }
+            checkPositiveOrZero(low, "low");
             if (high < low) {
                 throw new IllegalArgumentException(
                         "write buffer's high water mark cannot be less than " +
diff --git a/transport/src/main/java/io/netty/channel/embedded/EmbeddedChannel.java b/transport/src/main/java/io/netty/channel/embedded/EmbeddedChannel.java
index cf4e298..976d0b0 100644
--- a/transport/src/main/java/io/netty/channel/embedded/EmbeddedChannel.java
+++ b/transport/src/main/java/io/netty/channel/embedded/EmbeddedChannel.java
@@ -26,6 +26,7 @@ import io.netty.channel.ChannelConfig;
 import io.netty.channel.ChannelFuture;
 import io.netty.channel.ChannelFutureListener;
 import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelId;
 import io.netty.channel.ChannelInitializer;
 import io.netty.channel.ChannelMetadata;
@@ -160,8 +161,25 @@ public class EmbeddedChannel extends AbstractChannel {
      * @param handlers the {@link ChannelHandler}s which will be add in the {@link ChannelPipeline}
      */
     public EmbeddedChannel(ChannelId channelId, boolean register, boolean hasDisconnect,
+                           ChannelHandler... handlers) {
+        this(null, channelId, register, hasDisconnect, handlers);
+    }
+
+    /**
+     * Create a new instance with the channel ID set to the given ID and the pipeline
+     * initialized with the specified handlers.
+     *
+     * @param parent    the parent {@link Channel} of this {@link EmbeddedChannel}.
+     * @param channelId the {@link ChannelId} that will be used to identify this channel
+     * @param register {@code true} if this {@link Channel} is registered to the {@link EventLoop} in the
+     *                 constructor. If {@code false} the user will need to call {@link #register()}.
+     * @param hasDisconnect {@code false} if this {@link Channel} will delegate {@link #disconnect()}
+     *                      to {@link #close()}, {@link false} otherwise.
+     * @param handlers the {@link ChannelHandler}s which will be add in the {@link ChannelPipeline}
+     */
+    public EmbeddedChannel(Channel parent, ChannelId channelId, boolean register, boolean hasDisconnect,
                            final ChannelHandler... handlers) {
-        super(null, channelId);
+        super(parent, channelId);
         metadata = metadata(hasDisconnect);
         config = new DefaultChannelConfig(this);
         setup(register, handlers);
@@ -856,8 +874,8 @@ public class EmbeddedChannel extends AbstractChannel {
         }
 
         @Override
-        protected void onUnhandledInboundMessage(Object msg) {
-          handleInboundMessage(msg);
+        protected void onUnhandledInboundMessage(ChannelHandlerContext ctx, Object msg) {
+            handleInboundMessage(msg);
         }
     }
 }
diff --git a/transport/src/main/java/io/netty/channel/embedded/EmbeddedEventLoop.java b/transport/src/main/java/io/netty/channel/embedded/EmbeddedEventLoop.java
index bd95fee..7a43875 100644
--- a/transport/src/main/java/io/netty/channel/embedded/EmbeddedEventLoop.java
+++ b/transport/src/main/java/io/netty/channel/embedded/EmbeddedEventLoop.java
@@ -45,10 +45,7 @@ final class EmbeddedEventLoop extends AbstractScheduledEventExecutor implements
 
     @Override
     public void execute(Runnable command) {
-        if (command == null) {
-            throw new NullPointerException("command");
-        }
-        tasks.add(command);
+        tasks.add(ObjectUtil.checkNotNull(command, "command"));
     }
 
     void runTasks() {
diff --git a/transport/src/main/java/io/netty/channel/group/ChannelGroupException.java b/transport/src/main/java/io/netty/channel/group/ChannelGroupException.java
index aeabd64..aeca9bc 100644
--- a/transport/src/main/java/io/netty/channel/group/ChannelGroupException.java
+++ b/transport/src/main/java/io/netty/channel/group/ChannelGroupException.java
@@ -18,6 +18,7 @@ package io.netty.channel.group;
 import io.netty.channel.Channel;
 import io.netty.channel.ChannelException;
 import io.netty.channel.ChannelFuture;
+import io.netty.util.internal.ObjectUtil;
 
 import java.util.Collection;
 import java.util.Collections;
@@ -32,12 +33,8 @@ public class ChannelGroupException extends ChannelException implements Iterable<
     private final Collection<Map.Entry<Channel, Throwable>> failed;
 
     public ChannelGroupException(Collection<Map.Entry<Channel, Throwable>> causes) {
-        if (causes == null) {
-            throw new NullPointerException("causes");
-        }
-        if (causes.isEmpty()) {
-            throw new IllegalArgumentException("causes must be non empty");
-        }
+        ObjectUtil.checkNonEmpty(causes, "causes");
+
         failed = Collections.unmodifiableCollection(causes);
     }
 
diff --git a/transport/src/main/java/io/netty/channel/group/CombinedIterator.java b/transport/src/main/java/io/netty/channel/group/CombinedIterator.java
index 1c42eb0..6684488 100644
--- a/transport/src/main/java/io/netty/channel/group/CombinedIterator.java
+++ b/transport/src/main/java/io/netty/channel/group/CombinedIterator.java
@@ -15,6 +15,8 @@
  */
 package io.netty.channel.group;
 
+import io.netty.util.internal.ObjectUtil;
+
 import java.util.Iterator;
 import java.util.NoSuchElementException;
 
@@ -27,15 +29,9 @@ final class CombinedIterator<E> implements Iterator<E> {
     private Iterator<E> currentIterator;
 
     CombinedIterator(Iterator<E> i1, Iterator<E> i2) {
-        if (i1 == null) {
-            throw new NullPointerException("i1");
-        }
-        if (i2 == null) {
-            throw new NullPointerException("i2");
-        }
-        this.i1 = i1;
-        this.i2 = i2;
-        currentIterator = i1;
+        this.i1 = ObjectUtil.checkNotNull(i1, "i1");
+        this.i2 = ObjectUtil.checkNotNull(i2, "i2");
+        this.currentIterator = i1;
     }
 
     @Override
diff --git a/transport/src/main/java/io/netty/channel/group/DefaultChannelGroup.java b/transport/src/main/java/io/netty/channel/group/DefaultChannelGroup.java
index 71f217a..05e0dff 100644
--- a/transport/src/main/java/io/netty/channel/group/DefaultChannelGroup.java
+++ b/transport/src/main/java/io/netty/channel/group/DefaultChannelGroup.java
@@ -24,6 +24,7 @@ import io.netty.channel.ChannelId;
 import io.netty.channel.ServerChannel;
 import io.netty.util.ReferenceCountUtil;
 import io.netty.util.concurrent.EventExecutor;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.PlatformDependent;
 import io.netty.util.internal.StringUtil;
 
@@ -91,9 +92,7 @@ public class DefaultChannelGroup extends AbstractSet<Channel> implements Channel
      * the same name, which means no duplicate check is done against group names.
      */
     public DefaultChannelGroup(String name, EventExecutor executor, boolean stayClosed) {
-        if (name == null) {
-            throw new NullPointerException("name");
-        }
+        ObjectUtil.checkNotNull(name, "name");
         this.name = name;
         this.executor = executor;
         this.stayClosed = stayClosed;
@@ -256,12 +255,8 @@ public class DefaultChannelGroup extends AbstractSet<Channel> implements Channel
 
     @Override
     public ChannelGroupFuture write(Object message, ChannelMatcher matcher, boolean voidPromise) {
-        if (message == null) {
-            throw new NullPointerException("message");
-        }
-        if (matcher == null) {
-            throw new NullPointerException("matcher");
-        }
+        ObjectUtil.checkNotNull(message, "message");
+        ObjectUtil.checkNotNull(matcher, "matcher");
 
         final ChannelGroupFuture future;
         if (voidPromise) {
@@ -272,7 +267,7 @@ public class DefaultChannelGroup extends AbstractSet<Channel> implements Channel
             }
             future = voidFuture;
         } else {
-            Map<Channel, ChannelFuture> futures = new LinkedHashMap<Channel, ChannelFuture>(size());
+            Map<Channel, ChannelFuture> futures = new LinkedHashMap<Channel, ChannelFuture>(nonServerChannels.size());
             for (Channel c: nonServerChannels.values()) {
                 if (matcher.matches(c)) {
                     futures.put(c, c.write(safeDuplicate(message)));
@@ -301,9 +296,7 @@ public class DefaultChannelGroup extends AbstractSet<Channel> implements Channel
 
     @Override
     public ChannelGroupFuture disconnect(ChannelMatcher matcher) {
-        if (matcher == null) {
-            throw new NullPointerException("matcher");
-        }
+        ObjectUtil.checkNotNull(matcher, "matcher");
 
         Map<Channel, ChannelFuture> futures =
                 new LinkedHashMap<Channel, ChannelFuture>(size());
@@ -324,9 +317,7 @@ public class DefaultChannelGroup extends AbstractSet<Channel> implements Channel
 
     @Override
     public ChannelGroupFuture close(ChannelMatcher matcher) {
-        if (matcher == null) {
-            throw new NullPointerException("matcher");
-        }
+        ObjectUtil.checkNotNull(matcher, "matcher");
 
         Map<Channel, ChannelFuture> futures =
                 new LinkedHashMap<Channel, ChannelFuture>(size());
@@ -357,9 +348,7 @@ public class DefaultChannelGroup extends AbstractSet<Channel> implements Channel
 
     @Override
     public ChannelGroupFuture deregister(ChannelMatcher matcher) {
-        if (matcher == null) {
-            throw new NullPointerException("matcher");
-        }
+        ObjectUtil.checkNotNull(matcher, "matcher");
 
         Map<Channel, ChannelFuture> futures =
                 new LinkedHashMap<Channel, ChannelFuture>(size());
@@ -400,9 +389,7 @@ public class DefaultChannelGroup extends AbstractSet<Channel> implements Channel
 
     @Override
     public ChannelGroupFuture writeAndFlush(Object message, ChannelMatcher matcher, boolean voidPromise) {
-        if (message == null) {
-            throw new NullPointerException("message");
-        }
+        ObjectUtil.checkNotNull(message, "message");
 
         final ChannelGroupFuture future;
         if (voidPromise) {
@@ -413,7 +400,7 @@ public class DefaultChannelGroup extends AbstractSet<Channel> implements Channel
             }
             future = voidFuture;
         } else {
-            Map<Channel, ChannelFuture> futures = new LinkedHashMap<Channel, ChannelFuture>(size());
+            Map<Channel, ChannelFuture> futures = new LinkedHashMap<Channel, ChannelFuture>(nonServerChannels.size());
             for (Channel c: nonServerChannels.values()) {
                 if (matcher.matches(c)) {
                     futures.put(c, c.writeAndFlush(safeDuplicate(message)));
diff --git a/transport/src/main/java/io/netty/channel/group/DefaultChannelGroupFuture.java b/transport/src/main/java/io/netty/channel/group/DefaultChannelGroupFuture.java
index e4afe23..350a690 100644
--- a/transport/src/main/java/io/netty/channel/group/DefaultChannelGroupFuture.java
+++ b/transport/src/main/java/io/netty/channel/group/DefaultChannelGroupFuture.java
@@ -24,6 +24,7 @@ import io.netty.util.concurrent.EventExecutor;
 import io.netty.util.concurrent.Future;
 import io.netty.util.concurrent.GenericFutureListener;
 import io.netty.util.concurrent.ImmediateEventExecutor;
+import io.netty.util.internal.ObjectUtil;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -82,14 +83,8 @@ final class DefaultChannelGroupFuture extends DefaultPromise<Void> implements Ch
      */
     DefaultChannelGroupFuture(ChannelGroup group, Collection<ChannelFuture> futures,  EventExecutor executor) {
         super(executor);
-        if (group == null) {
-            throw new NullPointerException("group");
-        }
-        if (futures == null) {
-            throw new NullPointerException("futures");
-        }
-
-        this.group = group;
+        this.group = ObjectUtil.checkNotNull(group, "group");
+        ObjectUtil.checkNotNull(futures, "futures");
 
         Map<Channel, ChannelFuture> futureMap = new LinkedHashMap<Channel, ChannelFuture>();
         for (ChannelFuture f: futures) {
diff --git a/transport/src/main/java/io/netty/channel/local/LocalAddress.java b/transport/src/main/java/io/netty/channel/local/LocalAddress.java
index 6098cca..bba011c 100644
--- a/transport/src/main/java/io/netty/channel/local/LocalAddress.java
+++ b/transport/src/main/java/io/netty/channel/local/LocalAddress.java
@@ -16,6 +16,7 @@
 package io.netty.channel.local;
 
 import io.netty.channel.Channel;
+import io.netty.util.internal.ObjectUtil;
 
 import java.net.SocketAddress;
 
@@ -50,9 +51,7 @@ public final class LocalAddress extends SocketAddress implements Comparable<Loca
      * Creates a new instance with the specified ID.
      */
     public LocalAddress(String id) {
-        if (id == null) {
-            throw new NullPointerException("id");
-        }
+        ObjectUtil.checkNotNull(id, "id");
         id = id.trim().toLowerCase();
         if (id.isEmpty()) {
             throw new IllegalArgumentException("empty id");
diff --git a/transport/src/main/java/io/netty/channel/local/LocalChannel.java b/transport/src/main/java/io/netty/channel/local/LocalChannel.java
index 62bd4d6..50c8e8d 100644
--- a/transport/src/main/java/io/netty/channel/local/LocalChannel.java
+++ b/transport/src/main/java/io/netty/channel/local/LocalChannel.java
@@ -32,7 +32,6 @@ import io.netty.util.concurrent.Future;
 import io.netty.util.concurrent.SingleThreadEventExecutor;
 import io.netty.util.internal.InternalThreadLocalMap;
 import io.netty.util.internal.PlatformDependent;
-import io.netty.util.internal.ThrowableUtil;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 
@@ -55,10 +54,6 @@ public class LocalChannel extends AbstractChannel {
             AtomicReferenceFieldUpdater.newUpdater(LocalChannel.class, Future.class, "finishReadFuture");
     private static final ChannelMetadata METADATA = new ChannelMetadata(false);
     private static final int MAX_READER_STACK_DEPTH = 8;
-    private static final ClosedChannelException DO_WRITE_CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace(
-            new ClosedChannelException(), LocalChannel.class, "doWrite(...)");
-    private static final ClosedChannelException DO_CLOSE_CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace(
-            new ClosedChannelException(), LocalChannel.class, "doClose()");
 
     private enum State { OPEN, BOUND, CONNECTED, CLOSED }
 
@@ -234,7 +229,7 @@ public class LocalChannel extends AbstractChannel {
                 ChannelPromise promise = connectPromise;
                 if (promise != null) {
                     // Use tryFailure() instead of setFailure() to avoid the race against cancel().
-                    promise.tryFailure(DO_CLOSE_CLOSED_CHANNEL_EXCEPTION);
+                    promise.tryFailure(new ClosedChannelException());
                     connectPromise = null;
                 }
             }
@@ -347,7 +342,7 @@ public class LocalChannel extends AbstractChannel {
         case BOUND:
             throw new NotYetConnectedException();
         case CLOSED:
-            throw DO_WRITE_CLOSED_CHANNEL_EXCEPTION;
+            throw new ClosedChannelException();
         case CONNECTED:
             break;
         }
@@ -356,6 +351,7 @@ public class LocalChannel extends AbstractChannel {
 
         writeInProgress = true;
         try {
+            ClosedChannelException exception = null;
             for (;;) {
                 Object msg = in.current();
                 if (msg == null) {
@@ -368,7 +364,10 @@ public class LocalChannel extends AbstractChannel {
                         peer.inboundBuffer.add(ReferenceCountUtil.retain(msg));
                         in.remove();
                     } else {
-                        in.remove(DO_WRITE_CLOSED_CHANNEL_EXCEPTION);
+                        if (exception == null) {
+                            exception = new ClosedChannelException();
+                        }
+                        in.remove(exception);
                     }
                 } catch (Throwable cause) {
                     in.remove(cause);
diff --git a/transport/src/main/java/io/netty/channel/local/LocalEventLoopGroup.java b/transport/src/main/java/io/netty/channel/local/LocalEventLoopGroup.java
index 2bd3ff6..c2315a8 100644
--- a/transport/src/main/java/io/netty/channel/local/LocalEventLoopGroup.java
+++ b/transport/src/main/java/io/netty/channel/local/LocalEventLoopGroup.java
@@ -39,6 +39,15 @@ public class LocalEventLoopGroup extends DefaultEventLoopGroup {
         super(nThreads);
     }
 
+    /**
+     * Create a new instance with the default number of threads and the given {@link ThreadFactory}.
+     *
+     * @param threadFactory     the {@link ThreadFactory} or {@code null} to use the default
+     */
+    public LocalEventLoopGroup(ThreadFactory threadFactory) {
+        super(0, threadFactory);
+    }
+
     /**
      * Create a new instance
      *
diff --git a/transport/src/main/java/io/netty/channel/nio/AbstractNioChannel.java b/transport/src/main/java/io/netty/channel/nio/AbstractNioChannel.java
index 01467b9..a1c4520 100644
--- a/transport/src/main/java/io/netty/channel/nio/AbstractNioChannel.java
+++ b/transport/src/main/java/io/netty/channel/nio/AbstractNioChannel.java
@@ -29,7 +29,6 @@ import io.netty.channel.ConnectTimeoutException;
 import io.netty.channel.EventLoop;
 import io.netty.util.ReferenceCountUtil;
 import io.netty.util.ReferenceCounted;
-import io.netty.util.internal.ThrowableUtil;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 
@@ -51,9 +50,6 @@ public abstract class AbstractNioChannel extends AbstractChannel {
     private static final InternalLogger logger =
             InternalLoggerFactory.getInstance(AbstractNioChannel.class);
 
-    private static final ClosedChannelException DO_CLOSE_CLOSED_CHANNEL_EXCEPTION = ThrowableUtil.unknownStackTrace(
-            new ClosedChannelException(), AbstractNioChannel.class, "doClose()");
-
     private final SelectableChannel ch;
     protected final int readInterestOp;
     volatile SelectionKey selectionKey;
@@ -90,10 +86,8 @@ public abstract class AbstractNioChannel extends AbstractChannel {
             try {
                 ch.close();
             } catch (IOException e2) {
-                if (logger.isWarnEnabled()) {
-                    logger.warn(
+                logger.warn(
                             "Failed to close a partially initialized socket.", e2);
-                }
             }
 
             throw new ChannelException("Failed to enter non-blocking mode.", e);
@@ -505,7 +499,7 @@ public abstract class AbstractNioChannel extends AbstractChannel {
         ChannelPromise promise = connectPromise;
         if (promise != null) {
             // Use tryFailure() instead of setFailure() to avoid the race against cancel().
-            promise.tryFailure(DO_CLOSE_CLOSED_CHANNEL_EXCEPTION);
+            promise.tryFailure(new ClosedChannelException());
             connectPromise = null;
         }
 
diff --git a/transport/src/main/java/io/netty/channel/nio/NioEventLoop.java b/transport/src/main/java/io/netty/channel/nio/NioEventLoop.java
index 100065f..9962778 100644
--- a/transport/src/main/java/io/netty/channel/nio/NioEventLoop.java
+++ b/transport/src/main/java/io/netty/channel/nio/NioEventLoop.java
@@ -19,10 +19,12 @@ import io.netty.channel.Channel;
 import io.netty.channel.ChannelException;
 import io.netty.channel.EventLoop;
 import io.netty.channel.EventLoopException;
+import io.netty.channel.EventLoopTaskQueueFactory;
 import io.netty.channel.SelectStrategy;
 import io.netty.channel.SingleThreadEventLoop;
 import io.netty.util.IntSupplier;
 import io.netty.util.concurrent.RejectedExecutionHandler;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.PlatformDependent;
 import io.netty.util.internal.ReflectionUtil;
 import io.netty.util.internal.SystemPropertyUtil;
@@ -45,8 +47,7 @@ import java.util.Iterator;
 import java.util.Queue;
 import java.util.Set;
 import java.util.concurrent.Executor;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
 
 /**
  * {@link SingleThreadEventLoop} implementation which register the {@link Channel}'s to a
@@ -116,13 +117,14 @@ public final class NioEventLoop extends SingleThreadEventLoop {
 
     private final SelectorProvider provider;
 
-    /**
-     * Boolean that controls determines if a blocked Selector.select should
-     * break out of its selection process. In our case we use a timeout for
-     * the select method and the select method will block for that time unless
-     * waken up.
-     */
-    private final AtomicBoolean wakenUp = new AtomicBoolean();
+    private static final long AWAKE = -1L;
+    private static final long NONE = Long.MAX_VALUE;
+
+    // nextWakeupNanos is:
+    //    AWAKE            when EL is awake
+    //    NONE             when EL is waiting with no wakeup scheduled
+    //    other value T    when EL is waiting with wakeup scheduled at time T
+    private final AtomicLong nextWakeupNanos = new AtomicLong(AWAKE);
 
     private final SelectStrategy selectStrategy;
 
@@ -131,19 +133,23 @@ public final class NioEventLoop extends SingleThreadEventLoop {
     private boolean needsToSelectAgain;
 
     NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,
-                 SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) {
-        super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler);
-        if (selectorProvider == null) {
-            throw new NullPointerException("selectorProvider");
-        }
-        if (strategy == null) {
-            throw new NullPointerException("selectStrategy");
-        }
-        provider = selectorProvider;
+                 SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler,
+                 EventLoopTaskQueueFactory queueFactory) {
+        super(parent, executor, false, newTaskQueue(queueFactory), newTaskQueue(queueFactory),
+                rejectedExecutionHandler);
+        this.provider = ObjectUtil.checkNotNull(selectorProvider, "selectorProvider");
+        this.selectStrategy = ObjectUtil.checkNotNull(strategy, "selectStrategy");
         final SelectorTuple selectorTuple = openSelector();
-        selector = selectorTuple.selector;
-        unwrappedSelector = selectorTuple.unwrappedSelector;
-        selectStrategy = strategy;
+        this.selector = selectorTuple.selector;
+        this.unwrappedSelector = selectorTuple.unwrappedSelector;
+    }
+
+    private static Queue<Runnable> newTaskQueue(
+            EventLoopTaskQueueFactory queueFactory) {
+        if (queueFactory == null) {
+            return newTaskQueue0(DEFAULT_MAX_PENDING_TASKS);
+        }
+        return queueFactory.newTaskQueue(DEFAULT_MAX_PENDING_TASKS);
     }
 
     private static final class SelectorTuple {
@@ -265,9 +271,13 @@ public final class NioEventLoop extends SingleThreadEventLoop {
 
     @Override
     protected Queue<Runnable> newTaskQueue(int maxPendingTasks) {
+        return newTaskQueue0(maxPendingTasks);
+    }
+
+    private static Queue<Runnable> newTaskQueue0(int maxPendingTasks) {
         // This event loop never calls takeTask()
         return maxPendingTasks == Integer.MAX_VALUE ? PlatformDependent.<Runnable>newMpscQueue()
-                                                    : PlatformDependent.<Runnable>newMpscQueue(maxPendingTasks);
+                : PlatformDependent.<Runnable>newMpscQueue(maxPendingTasks);
     }
 
     /**
@@ -276,9 +286,7 @@ public final class NioEventLoop extends SingleThreadEventLoop {
      * be executed by this event loop when the {@link SelectableChannel} is ready.
      */
     public void register(final SelectableChannel ch, final int interestOps, final NioTask<?> task) {
-        if (ch == null) {
-            throw new NullPointerException("ch");
-        }
+        ObjectUtil.checkNotNull(ch, "ch");
         if (interestOps == 0) {
             throw new IllegalArgumentException("interestOps must be non-zero.");
         }
@@ -286,9 +294,7 @@ public final class NioEventLoop extends SingleThreadEventLoop {
             throw new IllegalArgumentException(
                     "invalid interestOps: " + interestOps + "(validOps: " + ch.validOps() + ')');
         }
-        if (task == null) {
-            throw new NullPointerException("task");
-        }
+        ObjectUtil.checkNotNull(task, "task");
 
         if (isShutdown()) {
             throw new IllegalStateException("event loop shut down");
@@ -329,8 +335,10 @@ public final class NioEventLoop extends SingleThreadEventLoop {
     }
 
     /**
-     * Sets the percentage of the desired amount of time spent for I/O in the event loop.  The default value is
-     * {@code 50}, which means the event loop will try to spend the same amount of time for I/O as for non-I/O tasks.
+     * Sets the percentage of the desired amount of time spent for I/O in the event loop. Value range from 1-100.
+     * The default value is {@code 50}, which means the event loop will try to spend the same amount of time for I/O
+     * as for non-I/O tasks. The lower the number the more time can be spent on non-I/O tasks. If value set to
+     * {@code 100}, this feature will be disabled and event loop will not attempt to balance I/O and non-I/O tasks.
      */
     public void setIoRatio(int ioRatio) {
         if (ioRatio <= 0 || ioRatio > 100) {
@@ -356,6 +364,11 @@ public final class NioEventLoop extends SingleThreadEventLoop {
         rebuildSelector0();
     }
 
+    @Override
+    public int registeredChannels() {
+        return selector.keys().size() - cancelledKeys;
+    }
+
     private void rebuildSelector0() {
         final Selector oldSelector = selector;
         final SelectorTuple newSelectorTuple;
@@ -420,10 +433,13 @@ public final class NioEventLoop extends SingleThreadEventLoop {
 
     @Override
     protected void run() {
+        int selectCnt = 0;
         for (;;) {
             try {
+                int strategy;
                 try {
-                    switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
+                    strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
+                    switch (strategy) {
                     case SelectStrategy.CONTINUE:
                         continue;
 
@@ -431,38 +447,19 @@ public final class NioEventLoop extends SingleThreadEventLoop {
                         // fall-through to SELECT since the busy-wait is not supported with NIO
 
                     case SelectStrategy.SELECT:
-                        select(wakenUp.getAndSet(false));
-
-                        // 'wakenUp.compareAndSet(false, true)' is always evaluated
-                        // before calling 'selector.wakeup()' to reduce the wake-up
-                        // overhead. (Selector.wakeup() is an expensive operation.)
-                        //
-                        // However, there is a race condition in this approach.
-                        // The race condition is triggered when 'wakenUp' is set to
-                        // true too early.
-                        //
-                        // 'wakenUp' is set to true too early if:
-                        // 1) Selector is waken up between 'wakenUp.set(false)' and
-                        //    'selector.select(...)'. (BAD)
-                        // 2) Selector is waken up between 'selector.select(...)' and
-                        //    'if (wakenUp.get()) { ... }'. (OK)
-                        //
-                        // In the first case, 'wakenUp' is set to true and the
-                        // following 'selector.select(...)' will wake up immediately.
-                        // Until 'wakenUp' is set to false again in the next round,
-                        // 'wakenUp.compareAndSet(false, true)' will fail, and therefore
-                        // any attempt to wake up the Selector will fail, too, causing
-                        // the following 'selector.select(...)' call to block
-                        // unnecessarily.
-                        //
-                        // To fix this problem, we wake up the selector again if wakenUp
-                        // is true immediately after selector.select(...).
-                        // It is inefficient in that it wakes up the selector for both
-                        // the first case (BAD - wake-up required) and the second case
-                        // (OK - no wake-up required).
-
-                        if (wakenUp.get()) {
-                            selector.wakeup();
+                        long curDeadlineNanos = nextScheduledTaskDeadlineNanos();
+                        if (curDeadlineNanos == -1L) {
+                            curDeadlineNanos = NONE; // nothing on the calendar
+                        }
+                        nextWakeupNanos.set(curDeadlineNanos);
+                        try {
+                            if (!hasTasks()) {
+                                strategy = select(curDeadlineNanos);
+                            }
+                        } finally {
+                            // This update is just to help block unnecessary selector wakeups
+                            // so use of lazySet is ok (no race condition)
+                            nextWakeupNanos.lazySet(AWAKE);
                         }
                         // fall through
                     default:
@@ -471,29 +468,52 @@ public final class NioEventLoop extends SingleThreadEventLoop {
                     // If we receive an IOException here its because the Selector is messed up. Let's rebuild
                     // the selector and retry. https://github.com/netty/netty/issues/8566
                     rebuildSelector0();
+                    selectCnt = 0;
                     handleLoopException(e);
                     continue;
                 }
 
+                selectCnt++;
                 cancelledKeys = 0;
                 needsToSelectAgain = false;
                 final int ioRatio = this.ioRatio;
+                boolean ranTasks;
                 if (ioRatio == 100) {
                     try {
-                        processSelectedKeys();
+                        if (strategy > 0) {
+                            processSelectedKeys();
+                        }
                     } finally {
                         // Ensure we always run tasks.
-                        runAllTasks();
+                        ranTasks = runAllTasks();
                     }
-                } else {
+                } else if (strategy > 0) {
                     final long ioStartTime = System.nanoTime();
                     try {
                         processSelectedKeys();
                     } finally {
                         // Ensure we always run tasks.
                         final long ioTime = System.nanoTime() - ioStartTime;
-                        runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
+                        ranTasks = runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
                     }
+                } else {
+                    ranTasks = runAllTasks(0); // This will run the minimum number of tasks
+                }
+
+                if (ranTasks || strategy > 0) {
+                    if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS && logger.isDebugEnabled()) {
+                        logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.",
+                                selectCnt - 1, selector);
+                    }
+                    selectCnt = 0;
+                } else if (unexpectedSelectorWakeup(selectCnt)) { // Unexpected wakeup (unusual case)
+                    selectCnt = 0;
+                }
+            } catch (CancelledKeyException e) {
+                // Harmless exception - log anyway
+                if (logger.isDebugEnabled()) {
+                    logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector {} - JDK bug?",
+                            selector, e);
                 }
             } catch (Throwable t) {
                 handleLoopException(t);
@@ -512,6 +532,33 @@ public final class NioEventLoop extends SingleThreadEventLoop {
         }
     }
 
+    // returns true if selectCnt should be reset
+    private boolean unexpectedSelectorWakeup(int selectCnt) {
+        if (Thread.interrupted()) {
+            // Thread was interrupted so reset selected keys and break so we not run into a busy loop.
+            // As this is most likely a bug in the handler of the user or it's client library we will
+            // also log it.
+            //
+            // See https://github.com/netty/netty/issues/2426
+            if (logger.isDebugEnabled()) {
+                logger.debug("Selector.select() returned prematurely because " +
+                        "Thread.currentThread().interrupt() was called. Use " +
+                        "NioEventLoop.shutdownGracefully() to shutdown the NioEventLoop.");
+            }
+            return true;
+        }
+        if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
+                selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
+            // The selector returned prematurely many times in a row.
+            // Rebuild the selector to work around the problem.
+            logger.warn("Selector.select() returned prematurely {} times in a row; rebuilding Selector {}.",
+                    selectCnt, selector);
+            rebuildSelector();
+            return true;
+        }
+        return false;
+    }
+
     private static void handleLoopException(Throwable t) {
         logger.warn("Unexpected exception in the selector loop.", t);
 
@@ -550,15 +597,6 @@ public final class NioEventLoop extends SingleThreadEventLoop {
         }
     }
 
-    @Override
-    protected Runnable pollTask() {
-        Runnable task = super.pollTask();
-        if (needsToSelectAgain) {
-            selectAgain();
-        }
-        return task;
-    }
-
     private void processSelectedKeysPlain(Set<SelectionKey> selectedKeys) {
         // check if the set is empty and if so just return to not create garbage by
         // creating a new Iterator every time even if there is nothing to process.
@@ -643,11 +681,10 @@ public final class NioEventLoop extends SingleThreadEventLoop {
             // and thus the SelectionKey could be cancelled as part of the deregistration process, but the channel is
             // still healthy and should not be closed.
             // See https://github.com/netty/netty/issues/5125
-            if (eventLoop != this || eventLoop == null) {
-                return;
+            if (eventLoop == this) {
+                // close the channel if the key is not valid anymore
+                unsafe.close(unsafe.voidPromise());
             }
-            // close the channel if the key is not valid anymore
-            unsafe.close(unsafe.voidPromise());
             return;
         }
 
@@ -736,122 +773,38 @@ public final class NioEventLoop extends SingleThreadEventLoop {
 
     @Override
     protected void wakeup(boolean inEventLoop) {
-        if (!inEventLoop && wakenUp.compareAndSet(false, true)) {
+        if (!inEventLoop && nextWakeupNanos.getAndSet(AWAKE) != AWAKE) {
             selector.wakeup();
         }
     }
 
+    @Override
+    protected boolean beforeScheduledTaskSubmitted(long deadlineNanos) {
+        // Note this is also correct for the nextWakeupNanos == -1 (AWAKE) case
+        return deadlineNanos < nextWakeupNanos.get();
+    }
+
+    @Override
+    protected boolean afterScheduledTaskSubmitted(long deadlineNanos) {
+        // Note this is also correct for the nextWakeupNanos == -1 (AWAKE) case
+        return deadlineNanos < nextWakeupNanos.get();
+    }
+
     Selector unwrappedSelector() {
         return unwrappedSelector;
     }
 
     int selectNow() throws IOException {
-        try {
-            return selector.selectNow();
-        } finally {
-            // restore wakeup state if needed
-            if (wakenUp.get()) {
-                selector.wakeup();
-            }
-        }
+        return selector.selectNow();
     }
 
-    private void select(boolean oldWakenUp) throws IOException {
-        Selector selector = this.selector;
-        try {
-            int selectCnt = 0;
-            long currentTimeNanos = System.nanoTime();
-            long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
-
-            for (;;) {
-                long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
-                if (timeoutMillis <= 0) {
-                    if (selectCnt == 0) {
-                        selector.selectNow();
-                        selectCnt = 1;
-                    }
-                    break;
-                }
-
-                // If a task was submitted when wakenUp value was true, the task didn't get a chance to call
-                // Selector#wakeup. So we need to check task queue again before executing select operation.
-                // If we don't, the task might be pended until select operation was timed out.
-                // It might be pended until idle timeout if IdleStateHandler existed in pipeline.
-                if (hasTasks() && wakenUp.compareAndSet(false, true)) {
-                    selector.selectNow();
-                    selectCnt = 1;
-                    break;
-                }
-
-                int selectedKeys = selector.select(timeoutMillis);
-                selectCnt ++;
-
-                if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
-                    // - Selected something,
-                    // - waken up by user, or
-                    // - the task queue has a pending task.
-                    // - a scheduled task is ready for processing
-                    break;
-                }
-                if (Thread.interrupted()) {
-                    // Thread was interrupted so reset selected keys and break so we not run into a busy loop.
-                    // As this is most likely a bug in the handler of the user or it's client library we will
-                    // also log it.
-                    //
-                    // See https://github.com/netty/netty/issues/2426
-                    if (logger.isDebugEnabled()) {
-                        logger.debug("Selector.select() returned prematurely because " +
-                                "Thread.currentThread().interrupt() was called. Use " +
-                                "NioEventLoop.shutdownGracefully() to shutdown the NioEventLoop.");
-                    }
-                    selectCnt = 1;
-                    break;
-                }
-
-                long time = System.nanoTime();
-                if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
-                    // timeoutMillis elapsed without anything selected.
-                    selectCnt = 1;
-                } else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
-                        selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
-                    // The code exists in an extra method to ensure the method is not too big to inline as this
-                    // branch is not very likely to get hit very frequently.
-                    selector = selectRebuildSelector(selectCnt);
-                    selectCnt = 1;
-                    break;
-                }
-
-                currentTimeNanos = time;
-            }
-
-            if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS) {
-                if (logger.isDebugEnabled()) {
-                    logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.",
-                            selectCnt - 1, selector);
-                }
-            }
-        } catch (CancelledKeyException e) {
-            if (logger.isDebugEnabled()) {
-                logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector {} - JDK bug?",
-                        selector, e);
-            }
-            // Harmless exception - log anyway
+    private int select(long deadlineNanos) throws IOException {
+        if (deadlineNanos == NONE) {
+            return selector.select();
         }
-    }
-
-    private Selector selectRebuildSelector(int selectCnt) throws IOException {
-        // The selector returned prematurely many times in a row.
-        // Rebuild the selector to work around the problem.
-        logger.warn(
-                "Selector.select() returned prematurely {} times in a row; rebuilding Selector {}.",
-                selectCnt, selector);
-
-        rebuildSelector();
-        Selector selector = this.selector;
-
-        // Select again to populate selectedKeys.
-        selector.selectNow();
-        return selector;
+        // Timeout will only be 0 if deadline is within 5 microsecs
+        long timeoutMillis = deadlineToDelayNanos(deadlineNanos + 995000L) / 1000000L;
+        return timeoutMillis <= 0 ? selector.selectNow() : selector.select(timeoutMillis);
     }
 
     private void selectAgain() {
diff --git a/transport/src/main/java/io/netty/channel/nio/NioEventLoopGroup.java b/transport/src/main/java/io/netty/channel/nio/NioEventLoopGroup.java
index 833b754..68278cc 100644
--- a/transport/src/main/java/io/netty/channel/nio/NioEventLoopGroup.java
+++ b/transport/src/main/java/io/netty/channel/nio/NioEventLoopGroup.java
@@ -18,6 +18,7 @@ package io.netty.channel.nio;
 import io.netty.channel.Channel;
 import io.netty.channel.EventLoop;
 import io.netty.channel.DefaultSelectStrategyFactory;
+import io.netty.channel.EventLoopTaskQueueFactory;
 import io.netty.channel.MultithreadEventLoopGroup;
 import io.netty.channel.SelectStrategyFactory;
 import io.netty.util.concurrent.EventExecutor;
@@ -51,6 +52,14 @@ public class NioEventLoopGroup extends MultithreadEventLoopGroup {
         this(nThreads, (Executor) null);
     }
 
+    /**
+     * Create a new instance using the default number of threads, the given {@link ThreadFactory} and the
+     * {@link SelectorProvider} which is returned by {@link SelectorProvider#provider()}.
+     */
+    public NioEventLoopGroup(ThreadFactory threadFactory) {
+        this(0, threadFactory, SelectorProvider.provider());
+    }
+
     /**
      * Create a new instance using the specified number of threads, the given {@link ThreadFactory} and the
      * {@link SelectorProvider} which is returned by {@link SelectorProvider#provider()}.
@@ -101,6 +110,15 @@ public class NioEventLoopGroup extends MultithreadEventLoopGroup {
         super(nThreads, executor, chooserFactory, selectorProvider, selectStrategyFactory, rejectedExecutionHandler);
     }
 
+    public NioEventLoopGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory,
+                             final SelectorProvider selectorProvider,
+                             final SelectStrategyFactory selectStrategyFactory,
+                             final RejectedExecutionHandler rejectedExecutionHandler,
+                             final EventLoopTaskQueueFactory taskQueueFactory) {
+        super(nThreads, executor, chooserFactory, selectorProvider, selectStrategyFactory,
+                rejectedExecutionHandler, taskQueueFactory);
+    }
+
     /**
      * Sets the percentage of the desired amount of time spent for I/O in the child event loops.  The default value is
      * {@code 50}, which means the event loop will try to spend the same amount of time for I/O as for non-I/O tasks.
@@ -123,7 +141,8 @@ public class NioEventLoopGroup extends MultithreadEventLoopGroup {
 
     @Override
     protected EventLoop newChild(Executor executor, Object... args) throws Exception {
+        EventLoopTaskQueueFactory queueFactory = args.length == 4 ? (EventLoopTaskQueueFactory) args[3] : null;
         return new NioEventLoop(this, executor, (SelectorProvider) args[0],
-            ((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]);
+            ((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2], queueFactory);
     }
 }
diff --git a/transport/src/main/java/io/netty/channel/oio/OioByteStreamChannel.java b/transport/src/main/java/io/netty/channel/oio/OioByteStreamChannel.java
index d352823..d21a8b5 100644
--- a/transport/src/main/java/io/netty/channel/oio/OioByteStreamChannel.java
+++ b/transport/src/main/java/io/netty/channel/oio/OioByteStreamChannel.java
@@ -19,6 +19,7 @@ import io.netty.buffer.ByteBuf;
 import io.netty.channel.Channel;
 import io.netty.channel.FileRegion;
 import io.netty.channel.RecvByteBufAllocator;
+import io.netty.util.internal.ObjectUtil;
 
 import java.io.EOFException;
 import java.io.IOException;
@@ -75,14 +76,8 @@ public abstract class OioByteStreamChannel extends AbstractOioByteChannel {
         if (this.os != null) {
             throw new IllegalStateException("output was set already");
         }
-        if (is == null) {
-            throw new NullPointerException("is");
-        }
-        if (os == null) {
-            throw new NullPointerException("os");
-        }
-        this.is = is;
-        this.os = os;
+        this.is = ObjectUtil.checkNotNull(is, "is");
+        this.os = ObjectUtil.checkNotNull(os, "os");
     }
 
     @Override
diff --git a/transport/src/main/java/io/netty/channel/oio/OioEventLoopGroup.java b/transport/src/main/java/io/netty/channel/oio/OioEventLoopGroup.java
index 91a2c4b..8c9bff4 100644
--- a/transport/src/main/java/io/netty/channel/oio/OioEventLoopGroup.java
+++ b/transport/src/main/java/io/netty/channel/oio/OioEventLoopGroup.java
@@ -24,7 +24,6 @@ import io.netty.channel.EventLoopGroup;
 import io.netty.channel.ThreadPerChannelEventLoopGroup;
 
 import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
 import java.util.concurrent.ThreadFactory;
 
 /**
@@ -53,7 +52,7 @@ public class OioEventLoopGroup extends ThreadPerChannelEventLoopGroup {
      *                          Use {@code 0} to use no limit
      */
     public OioEventLoopGroup(int maxChannels) {
-        this(maxChannels, Executors.defaultThreadFactory());
+        this(maxChannels, (ThreadFactory) null);
     }
 
     /**
diff --git a/transport/src/main/java/io/netty/channel/pool/AbstractChannelPoolMap.java b/transport/src/main/java/io/netty/channel/pool/AbstractChannelPoolMap.java
index 4b7213e..a58c93f 100644
--- a/transport/src/main/java/io/netty/channel/pool/AbstractChannelPoolMap.java
+++ b/transport/src/main/java/io/netty/channel/pool/AbstractChannelPoolMap.java
@@ -15,6 +15,10 @@
  */
 package io.netty.channel.pool;
 
+import io.netty.util.concurrent.Future;
+import io.netty.util.concurrent.GenericFutureListener;
+import io.netty.util.concurrent.GlobalEventExecutor;
+import io.netty.util.concurrent.Promise;
 import io.netty.util.internal.PlatformDependent;
 import io.netty.util.internal.ReadOnlyIterator;
 
@@ -41,7 +45,7 @@ public abstract class AbstractChannelPoolMap<K, P extends ChannelPool>
             P old = map.putIfAbsent(key, pool);
             if (old != null) {
                 // We need to destroy the newly created pool as we not use it.
-                pool.close();
+                poolCloseAsyncIfSupported(pool);
                 pool = old;
             }
         }
@@ -51,17 +55,65 @@ public abstract class AbstractChannelPoolMap<K, P extends ChannelPool>
      * Remove the {@link ChannelPool} from this {@link AbstractChannelPoolMap}. Returns {@code true} if removed,
      * {@code false} otherwise.
      *
+     * If the removed pool extends {@link SimpleChannelPool} it will be closed asynchronously to avoid blocking in
+     * this method.
+     *
      * Please note that {@code null} keys are not allowed.
      */
     public final boolean remove(K key) {
         P pool =  map.remove(checkNotNull(key, "key"));
         if (pool != null) {
-            pool.close();
+            poolCloseAsyncIfSupported(pool);
             return true;
         }
         return false;
     }
 
+    /**
+     * Remove the {@link ChannelPool} from this {@link AbstractChannelPoolMap}. Returns a future that comletes with a
+     * {@code true} result if the pool has been removed by this call, otherwise the result is {@code false}.
+     *
+     * If the removed pool extends {@link SimpleChannelPool} it will be closed asynchronously to avoid blocking in
+     * this method. The returned future will be completed once this asynchronous pool close operation completes.
+     */
+    private Future<Boolean> removeAsyncIfSupported(K key) {
+        P pool =  map.remove(checkNotNull(key, "key"));
+        if (pool != null) {
+            final Promise<Boolean> removePromise = GlobalEventExecutor.INSTANCE.newPromise();
+            poolCloseAsyncIfSupported(pool).addListener(new GenericFutureListener<Future<? super Void>>() {
+                @Override
+                public void operationComplete(Future<? super Void> future) throws Exception {
+                    if (future.isSuccess()) {
+                        removePromise.setSuccess(Boolean.TRUE);
+                    } else {
+                        removePromise.setFailure(future.cause());
+                    }
+                }
+            });
+            return removePromise;
+        }
+        return GlobalEventExecutor.INSTANCE.newSucceededFuture(Boolean.FALSE);
+    }
+
+    /**
+     * If the pool implementation supports asynchronous close, then use it to avoid a blocking close call in case
+     * the ChannelPoolMap operations are called from an EventLoop.
+     *
+     * @param pool the ChannelPool to be closed
+     */
+    private static Future<Void> poolCloseAsyncIfSupported(ChannelPool pool) {
+        if (pool instanceof SimpleChannelPool) {
+            return ((SimpleChannelPool) pool).closeAsync();
+        } else {
+            try {
+                pool.close();
+                return GlobalEventExecutor.INSTANCE.newSucceededFuture(null);
+            } catch (Exception e) {
+                return GlobalEventExecutor.INSTANCE.newFailedFuture(e);
+            }
+        }
+    }
+
     @Override
     public final Iterator<Entry<K, P>> iterator() {
         return new ReadOnlyIterator<Entry<K, P>>(map.entrySet().iterator());
@@ -94,7 +146,8 @@ public abstract class AbstractChannelPoolMap<K, P extends ChannelPool>
     @Override
     public final void close() {
         for (K key: map.keySet()) {
-            remove(key);
+            // Wait for remove to finish to ensure that resources are released before returning from close
+            removeAsyncIfSupported(key).syncUninterruptibly();
         }
     }
 }
diff --git a/transport/src/main/java/io/netty/channel/pool/FixedChannelPool.java b/transport/src/main/java/io/netty/channel/pool/FixedChannelPool.java
index 5ca376f..7b62213 100644
--- a/transport/src/main/java/io/netty/channel/pool/FixedChannelPool.java
+++ b/transport/src/main/java/io/netty/channel/pool/FixedChannelPool.java
@@ -23,12 +23,12 @@ import io.netty.util.concurrent.FutureListener;
 import io.netty.util.concurrent.GlobalEventExecutor;
 import io.netty.util.concurrent.Promise;
 import io.netty.util.internal.ObjectUtil;
-import io.netty.util.internal.ThrowableUtil;
 
 import java.nio.channels.ClosedChannelException;
 import java.util.ArrayDeque;
 import java.util.Queue;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.Callable;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
@@ -38,18 +38,7 @@ import java.util.concurrent.TimeoutException;
  * number of concurrent connections.
  */
 public class FixedChannelPool extends SimpleChannelPool {
-    private static final IllegalStateException FULL_EXCEPTION = ThrowableUtil.unknownStackTrace(
-            new IllegalStateException("Too many outstanding acquire operations"),
-            FixedChannelPool.class, "acquire0(...)");
-    private static final TimeoutException TIMEOUT_EXCEPTION = ThrowableUtil.unknownStackTrace(
-            new TimeoutException("Acquire operation took longer then configured maximum time"),
-            FixedChannelPool.class, "<init>(...)");
-    static final IllegalStateException POOL_CLOSED_ON_RELEASE_EXCEPTION = ThrowableUtil.unknownStackTrace(
-            new IllegalStateException("FixedChannelPool was closed"),
-            FixedChannelPool.class, "release(...)");
-    static final IllegalStateException POOL_CLOSED_ON_ACQUIRE_EXCEPTION = ThrowableUtil.unknownStackTrace(
-            new IllegalStateException("FixedChannelPool was closed"),
-            FixedChannelPool.class, "acquire0(...)");
+
     public enum AcquireTimeoutAction {
         /**
          * Create a new connection when the timeout is detected.
@@ -205,7 +194,13 @@ public class FixedChannelPool extends SimpleChannelPool {
                     @Override
                     public void onTimeout(AcquireTask task) {
                         // Fail the promise as we timed out.
-                        task.promise.setFailure(TIMEOUT_EXCEPTION);
+                        task.promise.setFailure(new TimeoutException(
+                                "Acquire operation took longer then configured maximum time") {
+                            @Override
+                            public Throwable fillInStackTrace() {
+                                return this;
+                            }
+                        });
                     }
                 };
                 break;
@@ -258,7 +253,7 @@ public class FixedChannelPool extends SimpleChannelPool {
         assert executor.inEventLoop();
 
         if (closed) {
-            promise.setFailure(POOL_CLOSED_ON_ACQUIRE_EXCEPTION);
+            promise.setFailure(new IllegalStateException("FixedChannelPool was closed"));
             return;
         }
         if (acquiredChannelCount.get() < maxConnections) {
@@ -273,7 +268,7 @@ public class FixedChannelPool extends SimpleChannelPool {
             super.acquire(p);
         } else {
             if (pendingAcquireCount >= maxPendingAcquires) {
-                promise.setFailure(FULL_EXCEPTION);
+                tooManyOutstanding(promise);
             } else {
                 AcquireTask task = new AcquireTask(promise);
                 if (pendingAcquireQueue.offer(task)) {
@@ -283,7 +278,7 @@ public class FixedChannelPool extends SimpleChannelPool {
                         task.timeoutFuture = executor.schedule(timeoutTask, acquireTimeoutNanos, TimeUnit.NANOSECONDS);
                     }
                 } else {
-                    promise.setFailure(FULL_EXCEPTION);
+                    tooManyOutstanding(promise);
                 }
             }
 
@@ -291,6 +286,10 @@ public class FixedChannelPool extends SimpleChannelPool {
         }
     }
 
+    private void tooManyOutstanding(Promise<?> promise) {
+        promise.setFailure(new IllegalStateException("Too many outstanding acquire operations"));
+    }
+
     @Override
     public Future<Void> release(final Channel channel, final Promise<Void> promise) {
         ObjectUtil.checkNotNull(promise, "promise");
@@ -304,7 +303,7 @@ public class FixedChannelPool extends SimpleChannelPool {
                 if (closed) {
                     // Since the pool is closed, we have no choice but to close the channel
                     channel.close();
-                    promise.setFailure(POOL_CLOSED_ON_RELEASE_EXCEPTION);
+                    promise.setFailure(new IllegalStateException("FixedChannelPool was closed"));
                     return;
                 }
 
@@ -366,7 +365,7 @@ public class FixedChannelPool extends SimpleChannelPool {
         final long expireNanoTime = System.nanoTime() + acquireTimeoutNanos;
         ScheduledFuture<?> timeoutFuture;
 
-        public AcquireTask(Promise<Channel> promise) {
+        AcquireTask(Promise<Channel> promise) {
             super(promise);
             // We need to create a new promise as we need to ensure the AcquireListener runs in the correct
             // EventLoop.
@@ -415,7 +414,7 @@ public class FixedChannelPool extends SimpleChannelPool {
                     // Since the pool is closed, we have no choice but to close the channel
                     future.getNow().close();
                 }
-                originalPromise.setFailure(POOL_CLOSED_ON_ACQUIRE_EXCEPTION);
+                originalPromise.setFailure(new IllegalStateException("FixedChannelPool was closed"));
                 return;
             }
 
@@ -443,19 +442,47 @@ public class FixedChannelPool extends SimpleChannelPool {
 
     @Override
     public void close() {
+        try {
+            closeAsync().await();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Closes the pool in an async manner.
+     *
+     * @return Future which represents completion of the close task
+     */
+    @Override
+    public Future<Void> closeAsync() {
         if (executor.inEventLoop()) {
-            close0();
+            return close0();
         } else {
-            executor.submit(new Runnable() {
+            final Promise<Void> closeComplete = executor.newPromise();
+            executor.execute(new Runnable() {
                 @Override
                 public void run() {
-                    close0();
+                    close0().addListener(new FutureListener<Void>() {
+                        @Override
+                        public void operationComplete(Future<Void> f) throws Exception {
+                            if (f.isSuccess()) {
+                                closeComplete.setSuccess(null);
+                            } else {
+                                closeComplete.setFailure(f.cause());
+                            }
+                        }
+                    });
                 }
-            }).awaitUninterruptibly();
+            });
+            return closeComplete;
         }
     }
 
-    private void close0() {
+    private Future<Void> close0() {
+        assert executor.inEventLoop();
+
         if (!closed) {
             closed = true;
             for (;;) {
@@ -474,12 +501,15 @@ public class FixedChannelPool extends SimpleChannelPool {
 
             // Ensure we dispatch this on another Thread as close0 will be called from the EventExecutor and we need
             // to ensure we will not block in a EventExecutor.
-            GlobalEventExecutor.INSTANCE.execute(new Runnable() {
+            return GlobalEventExecutor.INSTANCE.submit(new Callable<Void>() {
                 @Override
-                public void run() {
+                public Void call() throws Exception {
                     FixedChannelPool.super.close();
+                    return null;
                 }
             });
         }
+
+        return GlobalEventExecutor.INSTANCE.newSucceededFuture(null);
     }
 }
diff --git a/transport/src/main/java/io/netty/channel/pool/SimpleChannelPool.java b/transport/src/main/java/io/netty/channel/pool/SimpleChannelPool.java
index 6fcfd44..050de39 100644
--- a/transport/src/main/java/io/netty/channel/pool/SimpleChannelPool.java
+++ b/transport/src/main/java/io/netty/channel/pool/SimpleChannelPool.java
@@ -24,11 +24,12 @@ import io.netty.channel.EventLoop;
 import io.netty.util.AttributeKey;
 import io.netty.util.concurrent.Future;
 import io.netty.util.concurrent.FutureListener;
+import io.netty.util.concurrent.GlobalEventExecutor;
 import io.netty.util.concurrent.Promise;
 import io.netty.util.internal.PlatformDependent;
-import io.netty.util.internal.ThrowableUtil;
 
 import java.util.Deque;
+import java.util.concurrent.Callable;
 
 import static io.netty.util.internal.ObjectUtil.*;
 
@@ -40,10 +41,8 @@ import static io.netty.util.internal.ObjectUtil.*;
  *
  */
 public class SimpleChannelPool implements ChannelPool {
-    private static final AttributeKey<SimpleChannelPool> POOL_KEY = AttributeKey.newInstance("channelPool");
-    private static final IllegalStateException FULL_EXCEPTION = ThrowableUtil.unknownStackTrace(
-            new IllegalStateException("ChannelPool full"), SimpleChannelPool.class, "releaseAndOffer(...)");
-
+    private static final AttributeKey<SimpleChannelPool> POOL_KEY =
+        AttributeKey.newInstance("io.netty.channel.pool.SimpleChannelPool");
     private final Deque<Channel> deque = PlatformDependent.newConcurrentDeque();
     private final ChannelPoolHandler handler;
     private final ChannelHealthChecker healthCheck;
@@ -160,8 +159,7 @@ public class SimpleChannelPool implements ChannelPool {
 
     @Override
     public Future<Channel> acquire(final Promise<Channel> promise) {
-        checkNotNull(promise, "promise");
-        return acquireHealthyFromPoolOrNew(promise);
+        return acquireHealthyFromPoolOrNew(checkNotNull(promise, "promise"));
     }
 
     /**
@@ -206,9 +204,10 @@ public class SimpleChannelPool implements ChannelPool {
         return promise;
     }
 
-    private void notifyConnect(ChannelFuture future, Promise<Channel> promise) {
+    private void notifyConnect(ChannelFuture future, Promise<Channel> promise) throws Exception {
         if (future.isSuccess()) {
             Channel channel = future.channel();
+            handler.channelAcquired(channel);
             if (!promise.trySuccess(channel)) {
                 // Promise was completed in the meantime (like cancelled), just release the channel again
                 release(channel);
@@ -351,16 +350,21 @@ public class SimpleChannelPool implements ChannelPool {
             handler.channelReleased(channel);
             promise.setSuccess(null);
         } else {
-            closeAndFail(channel, FULL_EXCEPTION, promise);
+            closeAndFail(channel, new IllegalStateException("ChannelPool full") {
+                @Override
+                public Throwable fillInStackTrace() {
+                    return this;
+                }
+            }, promise);
         }
     }
 
-    private static void closeChannel(Channel channel) {
+    private void closeChannel(Channel channel) {
         channel.attr(POOL_KEY).getAndSet(null);
         channel.close();
     }
 
-    private static void closeAndFail(Channel channel, Throwable cause, Promise<?> promise) {
+    private void closeAndFail(Channel channel, Throwable cause, Promise<?> promise) {
         closeChannel(channel);
         promise.tryFailure(cause);
     }
@@ -398,4 +402,20 @@ public class SimpleChannelPool implements ChannelPool {
             channel.close().awaitUninterruptibly();
         }
     }
+
+    /**
+     * Closes the pool in an async manner.
+     *
+     * @return Future which represents completion of the close task
+     */
+    public Future<Void> closeAsync() {
+        // Execute close asynchronously in case this is being invoked on an eventloop to avoid blocking
+        return GlobalEventExecutor.INSTANCE.submit(new Callable<Void>() {
+            @Override
+            public Void call() throws Exception {
+                close();
+                return null;
+            }
+        });
+    }
 }
diff --git a/transport/src/main/java/io/netty/channel/socket/DefaultDatagramChannelConfig.java b/transport/src/main/java/io/netty/channel/socket/DefaultDatagramChannelConfig.java
index 5d3418c..515a187 100644
--- a/transport/src/main/java/io/netty/channel/socket/DefaultDatagramChannelConfig.java
+++ b/transport/src/main/java/io/netty/channel/socket/DefaultDatagramChannelConfig.java
@@ -23,6 +23,7 @@ import io.netty.channel.FixedRecvByteBufAllocator;
 import io.netty.channel.MessageSizeEstimator;
 import io.netty.channel.RecvByteBufAllocator;
 import io.netty.channel.WriteBufferWaterMark;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.PlatformDependent;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
@@ -52,10 +53,7 @@ public class DefaultDatagramChannelConfig extends DefaultChannelConfig implement
      */
     public DefaultDatagramChannelConfig(DatagramChannel channel, DatagramSocket javaSocket) {
         super(channel, new FixedRecvByteBufAllocator(2048));
-        if (javaSocket == null) {
-            throw new NullPointerException("javaSocket");
-        }
-        this.javaSocket = javaSocket;
+        this.javaSocket = ObjectUtil.checkNotNull(javaSocket, "javaSocket");
     }
 
     protected final DatagramSocket javaSocket() {
diff --git a/transport/src/main/java/io/netty/channel/socket/DefaultServerSocketChannelConfig.java b/transport/src/main/java/io/netty/channel/socket/DefaultServerSocketChannelConfig.java
index 57ac53d..eb89644 100644
--- a/transport/src/main/java/io/netty/channel/socket/DefaultServerSocketChannelConfig.java
+++ b/transport/src/main/java/io/netty/channel/socket/DefaultServerSocketChannelConfig.java
@@ -23,6 +23,7 @@ import io.netty.channel.MessageSizeEstimator;
 import io.netty.channel.RecvByteBufAllocator;
 import io.netty.channel.WriteBufferWaterMark;
 import io.netty.util.NetUtil;
+import io.netty.util.internal.ObjectUtil;
 
 import java.net.ServerSocket;
 import java.net.SocketException;
@@ -31,6 +32,7 @@ import java.util.Map;
 import static io.netty.channel.ChannelOption.SO_BACKLOG;
 import static io.netty.channel.ChannelOption.SO_RCVBUF;
 import static io.netty.channel.ChannelOption.SO_REUSEADDR;
+import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
 
 /**
  * The default {@link ServerSocketChannelConfig} implementation.
@@ -46,10 +48,7 @@ public class DefaultServerSocketChannelConfig extends DefaultChannelConfig
      */
     public DefaultServerSocketChannelConfig(ServerSocketChannel channel, ServerSocket javaSocket) {
         super(channel);
-        if (javaSocket == null) {
-            throw new NullPointerException("javaSocket");
-        }
-        this.javaSocket = javaSocket;
+        this.javaSocket = ObjectUtil.checkNotNull(javaSocket, "javaSocket");
     }
 
     @Override
@@ -141,9 +140,7 @@ public class DefaultServerSocketChannelConfig extends DefaultChannelConfig
 
     @Override
     public ServerSocketChannelConfig setBacklog(int backlog) {
-        if (backlog < 0) {
-            throw new IllegalArgumentException("backlog: " + backlog);
-        }
+        checkPositiveOrZero(backlog, "backlog");
         this.backlog = backlog;
         return this;
     }
diff --git a/transport/src/main/java/io/netty/channel/socket/DefaultSocketChannelConfig.java b/transport/src/main/java/io/netty/channel/socket/DefaultSocketChannelConfig.java
index c154582..8ab4685 100644
--- a/transport/src/main/java/io/netty/channel/socket/DefaultSocketChannelConfig.java
+++ b/transport/src/main/java/io/netty/channel/socket/DefaultSocketChannelConfig.java
@@ -22,6 +22,7 @@ import io.netty.channel.DefaultChannelConfig;
 import io.netty.channel.MessageSizeEstimator;
 import io.netty.channel.RecvByteBufAllocator;
 import io.netty.channel.WriteBufferWaterMark;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.PlatformDependent;
 
 import java.net.Socket;
@@ -44,10 +45,7 @@ public class DefaultSocketChannelConfig extends DefaultChannelConfig
      */
     public DefaultSocketChannelConfig(SocketChannel channel, Socket javaSocket) {
         super(channel);
-        if (javaSocket == null) {
-            throw new NullPointerException("javaSocket");
-        }
-        this.javaSocket = javaSocket;
+        this.javaSocket = ObjectUtil.checkNotNull(javaSocket, "javaSocket");
 
         // Enable TCP_NODELAY by default if possible.
         if (PlatformDependent.canEnableTcpNoDelayByDefault()) {
diff --git a/transport/src/main/java/io/netty/channel/socket/nio/NioChannelOption.java b/transport/src/main/java/io/netty/channel/socket/nio/NioChannelOption.java
index 3f9550f..e2e7561 100644
--- a/transport/src/main/java/io/netty/channel/socket/nio/NioChannelOption.java
+++ b/transport/src/main/java/io/netty/channel/socket/nio/NioChannelOption.java
@@ -17,6 +17,7 @@ package io.netty.channel.socket.nio;
 
 import io.netty.channel.ChannelException;
 import io.netty.channel.ChannelOption;
+import io.netty.util.internal.SuppressJava6Requirement;
 
 import java.io.IOException;
 import java.nio.channels.Channel;
@@ -29,6 +30,7 @@ import java.util.Set;
  * Provides {@link ChannelOption} over a given {@link java.net.SocketOption} which is then passed through the underlying
  * {@link java.nio.channels.NetworkChannel}.
  */
+@SuppressJava6Requirement(reason = "Usage explicit by the user")
 public final class NioChannelOption<T> extends ChannelOption<T> {
 
     private final java.net.SocketOption<T> option;
@@ -53,6 +55,7 @@ public final class NioChannelOption<T> extends ChannelOption<T> {
     // See https://github.com/netty/netty/issues/8166
 
     // Internal helper methods to remove code duplication between Nio*Channel implementations.
+    @SuppressJava6Requirement(reason = "Usage guarded by java version check")
     static <T> boolean setOption(Channel jdkChannel, NioChannelOption<T> option, T value) {
         java.nio.channels.NetworkChannel channel = (java.nio.channels.NetworkChannel) jdkChannel;
         if (!channel.supportedOptions().contains(option.option)) {
@@ -71,6 +74,7 @@ public final class NioChannelOption<T> extends ChannelOption<T> {
         }
     }
 
+    @SuppressJava6Requirement(reason = "Usage guarded by java version check")
     static <T> T getOption(Channel jdkChannel, NioChannelOption<T> option) {
         java.nio.channels.NetworkChannel channel = (java.nio.channels.NetworkChannel) jdkChannel;
 
@@ -89,6 +93,7 @@ public final class NioChannelOption<T> extends ChannelOption<T> {
         }
     }
 
+    @SuppressJava6Requirement(reason = "Usage guarded by java version check")
     @SuppressWarnings("unchecked")
     static ChannelOption[] getOptions(Channel jdkChannel) {
         java.nio.channels.NetworkChannel channel = (java.nio.channels.NetworkChannel) jdkChannel;
diff --git a/transport/src/main/java/io/netty/channel/socket/nio/NioDatagramChannel.java b/transport/src/main/java/io/netty/channel/socket/nio/NioDatagramChannel.java
index 2fc314d..f706db3 100644
--- a/transport/src/main/java/io/netty/channel/socket/nio/NioDatagramChannel.java
+++ b/transport/src/main/java/io/netty/channel/socket/nio/NioDatagramChannel.java
@@ -30,10 +30,11 @@ import io.netty.channel.nio.AbstractNioMessageChannel;
 import io.netty.channel.socket.DatagramChannelConfig;
 import io.netty.channel.socket.DatagramPacket;
 import io.netty.channel.socket.InternetProtocolFamily;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.SocketUtils;
 import io.netty.util.internal.PlatformDependent;
 import io.netty.util.internal.StringUtil;
-import io.netty.util.internal.UnstableApi;
+import io.netty.util.internal.SuppressJava6Requirement;
 
 import java.io.IOException;
 import java.net.InetAddress;
@@ -89,6 +90,7 @@ public final class NioDatagramChannel
         }
     }
 
+    @SuppressJava6Requirement(reason = "Usage guarded by java version check")
     private static DatagramChannel newSocket(SelectorProvider provider, InternetProtocolFamily ipFamily) {
         if (ipFamily == null) {
             return newSocket(provider);
@@ -395,6 +397,7 @@ public final class NioDatagramChannel
         return joinGroup(multicastAddress, networkInterface, source, newPromise());
     }
 
+    @SuppressJava6Requirement(reason = "Usage guarded by java version check")
     @Override
     public ChannelFuture joinGroup(
             InetAddress multicastAddress, NetworkInterface networkInterface,
@@ -402,13 +405,8 @@ public final class NioDatagramChannel
 
         checkJavaVersion();
 
-        if (multicastAddress == null) {
-            throw new NullPointerException("multicastAddress");
-        }
-
-        if (networkInterface == null) {
-            throw new NullPointerException("networkInterface");
-        }
+        ObjectUtil.checkNotNull(multicastAddress, "multicastAddress");
+        ObjectUtil.checkNotNull(networkInterface, "networkInterface");
 
         try {
             MembershipKey key;
@@ -475,18 +473,15 @@ public final class NioDatagramChannel
         return leaveGroup(multicastAddress, networkInterface, source, newPromise());
     }
 
+    @SuppressJava6Requirement(reason = "Usage guarded by java version check")
     @Override
     public ChannelFuture leaveGroup(
             InetAddress multicastAddress, NetworkInterface networkInterface, InetAddress source,
             ChannelPromise promise) {
         checkJavaVersion();
 
-        if (multicastAddress == null) {
-            throw new NullPointerException("multicastAddress");
-        }
-        if (networkInterface == null) {
-            throw new NullPointerException("networkInterface");
-        }
+        ObjectUtil.checkNotNull(multicastAddress, "multicastAddress");
+        ObjectUtil.checkNotNull(networkInterface, "networkInterface");
 
         synchronized (this) {
             if (memberships != null) {
@@ -528,22 +523,17 @@ public final class NioDatagramChannel
     /**
      * Block the given sourceToBlock address for the given multicastAddress on the given networkInterface
      */
+    @SuppressJava6Requirement(reason = "Usage guarded by java version check")
     @Override
     public ChannelFuture block(
             InetAddress multicastAddress, NetworkInterface networkInterface,
             InetAddress sourceToBlock, ChannelPromise promise) {
         checkJavaVersion();
 
-        if (multicastAddress == null) {
-            throw new NullPointerException("multicastAddress");
-        }
-        if (sourceToBlock == null) {
-            throw new NullPointerException("sourceToBlock");
-        }
+        ObjectUtil.checkNotNull(multicastAddress, "multicastAddress");
+        ObjectUtil.checkNotNull(sourceToBlock, "sourceToBlock");
+        ObjectUtil.checkNotNull(networkInterface, "networkInterface");
 
-        if (networkInterface == null) {
-            throw new NullPointerException("networkInterface");
-        }
         synchronized (this) {
             if (memberships != null) {
                 List<MembershipKey> keys = memberships.get(multicastAddress);
diff --git a/transport/src/main/java/io/netty/channel/socket/nio/NioServerSocketChannel.java b/transport/src/main/java/io/netty/channel/socket/nio/NioServerSocketChannel.java
index 128e531..565cb04 100644
--- a/transport/src/main/java/io/netty/channel/socket/nio/NioServerSocketChannel.java
+++ b/transport/src/main/java/io/netty/channel/socket/nio/NioServerSocketChannel.java
@@ -24,6 +24,7 @@ import io.netty.channel.nio.AbstractNioMessageChannel;
 import io.netty.channel.socket.DefaultServerSocketChannelConfig;
 import io.netty.channel.socket.ServerSocketChannelConfig;
 import io.netty.util.internal.PlatformDependent;
+import io.netty.util.internal.SuppressJava6Requirement;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
 
@@ -106,7 +107,9 @@ public class NioServerSocketChannel extends AbstractNioMessageChannel
 
     @Override
     public boolean isActive() {
-        return javaChannel().socket().isBound();
+        // As java.nio.ServerSocketChannel.isBound() will continue to return true even after the channel was closed
+        // we will also need to check if it is open.
+        return isOpen() && javaChannel().socket().isBound();
     }
 
     @Override
@@ -124,6 +127,7 @@ public class NioServerSocketChannel extends AbstractNioMessageChannel
         return SocketUtils.localSocketAddress(javaChannel().socket());
     }
 
+    @SuppressJava6Requirement(reason = "Usage guarded by java version check")
     @Override
     protected void doBind(SocketAddress localAddress) throws Exception {
         if (PlatformDependent.javaVersion() >= 7) {
@@ -218,7 +222,6 @@ public class NioServerSocketChannel extends AbstractNioMessageChannel
             return super.getOption(option);
         }
 
-        @SuppressWarnings("unchecked")
         @Override
         public Map<ChannelOption<?>, Object> getOptions() {
             if (PlatformDependent.javaVersion() >= 7) {
diff --git a/transport/src/main/java/io/netty/channel/socket/nio/NioSocketChannel.java b/transport/src/main/java/io/netty/channel/socket/nio/NioSocketChannel.java
index 7443179..6295958 100644
--- a/transport/src/main/java/io/netty/channel/socket/nio/NioSocketChannel.java
+++ b/transport/src/main/java/io/netty/channel/socket/nio/NioSocketChannel.java
@@ -33,6 +33,7 @@ import io.netty.channel.socket.SocketChannelConfig;
 import io.netty.util.concurrent.GlobalEventExecutor;
 import io.netty.util.internal.PlatformDependent;
 import io.netty.util.internal.SocketUtils;
+import io.netty.util.internal.SuppressJava6Requirement;
 import io.netty.util.internal.UnstableApi;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
@@ -152,6 +153,7 @@ public class NioSocketChannel extends AbstractNioByteChannel implements io.netty
         return (InetSocketAddress) super.remoteAddress();
     }
 
+    @SuppressJava6Requirement(reason = "Usage guarded by java version check")
     @UnstableApi
     @Override
     protected final void doShutdownOutput() throws Exception {
@@ -270,6 +272,7 @@ public class NioSocketChannel extends AbstractNioByteChannel implements io.netty
         }
     }
 
+    @SuppressJava6Requirement(reason = "Usage guarded by java version check")
     private void shutdownInput0() throws Exception {
         if (PlatformDependent.javaVersion() >= 7) {
             javaChannel().shutdownInput();
@@ -496,7 +499,6 @@ public class NioSocketChannel extends AbstractNioByteChannel implements io.netty
             return super.getOption(option);
         }
 
-        @SuppressWarnings("unchecked")
         @Override
         public Map<ChannelOption<?>, Object> getOptions() {
             if (PlatformDependent.javaVersion() >= 7) {
@@ -517,7 +519,7 @@ public class NioSocketChannel extends AbstractNioByteChannel implements io.netty
             // Multiply by 2 to give some extra space in case the OS can process write data faster than we can provide.
             int newSendBufferSize = getSendBufferSize() << 1;
             if (newSendBufferSize > 0) {
-                setMaxBytesPerGatheringWrite(getSendBufferSize() << 1);
+                setMaxBytesPerGatheringWrite(newSendBufferSize);
             }
         }
 
diff --git a/transport/src/main/java/io/netty/channel/socket/nio/ProtocolFamilyConverter.java b/transport/src/main/java/io/netty/channel/socket/nio/ProtocolFamilyConverter.java
index e4f4dc2..7c2f141 100644
--- a/transport/src/main/java/io/netty/channel/socket/nio/ProtocolFamilyConverter.java
+++ b/transport/src/main/java/io/netty/channel/socket/nio/ProtocolFamilyConverter.java
@@ -16,6 +16,7 @@
 package io.netty.channel.socket.nio;
 
 import io.netty.channel.socket.InternetProtocolFamily;
+import io.netty.util.internal.SuppressJava6Requirement;
 
 import java.net.ProtocolFamily;
 import java.net.StandardProtocolFamily;
@@ -32,6 +33,7 @@ final class ProtocolFamilyConverter {
     /**
      * Convert the {@link InternetProtocolFamily}. This MUST only be called on jdk version >= 7.
      */
+    @SuppressJava6Requirement(reason = "Usage guarded by java version check")
     public static ProtocolFamily convert(InternetProtocolFamily family) {
         switch (family) {
         case IPv4:
diff --git a/transport/src/main/java/io/netty/channel/socket/oio/OioServerSocketChannel.java b/transport/src/main/java/io/netty/channel/socket/oio/OioServerSocketChannel.java
index bf91829..dfa6b02 100644
--- a/transport/src/main/java/io/netty/channel/socket/oio/OioServerSocketChannel.java
+++ b/transport/src/main/java/io/netty/channel/socket/oio/OioServerSocketChannel.java
@@ -20,6 +20,7 @@ import io.netty.channel.ChannelMetadata;
 import io.netty.channel.ChannelOutboundBuffer;
 import io.netty.channel.oio.AbstractOioMessageChannel;
 import io.netty.channel.socket.ServerSocketChannel;
+import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.SocketUtils;
 import io.netty.util.internal.logging.InternalLogger;
 import io.netty.util.internal.logging.InternalLoggerFactory;
@@ -32,7 +33,6 @@ import java.net.SocketAddress;
 import java.net.SocketTimeoutException;
 import java.util.List;
 import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
 
 /**
  * {@link ServerSocketChannel} which accepts new connections and create the {@link OioSocketChannel}'s for them.
@@ -59,7 +59,6 @@ public class OioServerSocketChannel extends AbstractOioMessageChannel
     }
 
     final ServerSocket socket;
-    final Lock shutdownLock = new ReentrantLock();
     private final OioServerSocketChannelConfig config;
 
     /**
@@ -76,9 +75,7 @@ public class OioServerSocketChannel extends AbstractOioMessageChannel
      */
     public OioServerSocketChannel(ServerSocket socket) {
         super(null);
-        if (socket == null) {
-            throw new NullPointerException("socket");
-        }
+        ObjectUtil.checkNotNull(socket, "socket");
 
         boolean success = false;
         try {
diff --git a/transport/src/main/resources/META-INF/native-image/io.netty/transport/native-image.properties b/transport/src/main/resources/META-INF/native-image/io.netty/transport/native-image.properties
new file mode 100644
index 0000000..dc33561
--- /dev/null
+++ b/transport/src/main/resources/META-INF/native-image/io.netty/transport/native-image.properties
@@ -0,0 +1,15 @@
+# Copyright 2019 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.
+
+Args = -H:ReflectionConfigurationResources=${.}/reflection-config.json
diff --git a/transport/src/main/resources/META-INF/native-image/io.netty/transport/reflection-config.json b/transport/src/main/resources/META-INF/native-image/io.netty/transport/reflection-config.json
new file mode 100644
index 0000000..1ebbb43
--- /dev/null
+++ b/transport/src/main/resources/META-INF/native-image/io.netty/transport/reflection-config.json
@@ -0,0 +1,15 @@
+[
+    {
+      "name": "io.netty.channel.socket.nio.NioServerSocketChannel",
+      "methods": [
+        { "name": "<init>", "parameterTypes": [] }
+      ]
+    },
+    {
+      "name": "sun.nio.ch.SelectorImpl",
+      "fields": [
+        { "name": "selectedKeys",  "allowUnsafeAccess" : true},
+        { "name": "publicSelectedKeys",  "allowUnsafeAccess" : true}
+      ]
+    }
+]
diff --git a/transport/src/test/java/io/netty/bootstrap/BootstrapTest.java b/transport/src/test/java/io/netty/bootstrap/BootstrapTest.java
index cc93a91..03b2f09 100644
--- a/transport/src/test/java/io/netty/bootstrap/BootstrapTest.java
+++ b/transport/src/test/java/io/netty/bootstrap/BootstrapTest.java
@@ -17,13 +17,17 @@
 package io.netty.bootstrap;
 
 import io.netty.channel.Channel;
+import io.netty.channel.ChannelConfig;
 import io.netty.channel.ChannelFactory;
 import io.netty.channel.ChannelFuture;
 import io.netty.channel.ChannelFutureListener;
 import io.netty.channel.ChannelHandler.Sharable;
 import io.netty.channel.ChannelInboundHandler;
 import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelOption;
 import io.netty.channel.ChannelPromise;
+import io.netty.channel.DefaultChannelConfig;
 import io.netty.channel.DefaultEventLoop;
 import io.netty.channel.DefaultEventLoopGroup;
 import io.netty.channel.EventLoopGroup;
@@ -48,6 +52,7 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.LinkedBlockingQueue;
 
 import static org.hamcrest.Matchers.*;
@@ -271,6 +276,38 @@ public class BootstrapTest {
         assertThat(connectFuture.channel().isOpen(), is(false));
     }
 
+    @Test
+    public void testGetResolverFailed() throws Exception {
+        class TestException extends RuntimeException { }
+
+        final Bootstrap bootstrapA = new Bootstrap();
+        bootstrapA.group(groupA);
+        bootstrapA.channel(LocalChannel.class);
+
+        bootstrapA.resolver(new AddressResolverGroup<SocketAddress>() {
+            @Override
+            protected AddressResolver<SocketAddress> newResolver(EventExecutor executor) {
+                throw new TestException();
+            }
+        });
+        bootstrapA.handler(dummyHandler);
+
+        final ServerBootstrap bootstrapB = new ServerBootstrap();
+        bootstrapB.group(groupB);
+        bootstrapB.channel(LocalServerChannel.class);
+        bootstrapB.childHandler(dummyHandler);
+        SocketAddress localAddress = bootstrapB.bind(LocalAddress.ANY).sync().channel().localAddress();
+
+        // Connect to the server using the asynchronous resolver.
+        ChannelFuture connectFuture = bootstrapA.connect(localAddress);
+
+        // Should fail with the IllegalStateException.
+        assertThat(connectFuture.await(10000), is(true));
+        assertThat(connectFuture.cause(), instanceOf(IllegalStateException.class));
+        assertThat(connectFuture.cause().getCause(), instanceOf(TestException.class));
+        assertThat(connectFuture.channel().isOpen(), is(false));
+    }
+
     @Test
     public void testChannelFactoryFailureNotifiesPromise() throws Exception {
         final RuntimeException exception = new RuntimeException("newChannel crash");
@@ -293,6 +330,56 @@ public class BootstrapTest {
         assertThat(connectFuture.channel(), is(not(nullValue())));
     }
 
+    @Test
+    public void testChannelOptionOrderPreserve() throws InterruptedException {
+        final BlockingQueue<ChannelOption<?>> options = new LinkedBlockingQueue<ChannelOption<?>>();
+        class ChannelConfigValidator extends DefaultChannelConfig {
+            ChannelConfigValidator(Channel channel) {
+                super(channel);
+            }
+
+            @Override
+            public <T> boolean setOption(ChannelOption<T> option, T value) {
+                options.add(option);
+                return super.setOption(option, value);
+            }
+        }
+        final CountDownLatch latch = new CountDownLatch(1);
+        final Bootstrap bootstrap = new Bootstrap()
+                .handler(new ChannelInitializer<Channel>() {
+                    @Override
+                    protected void initChannel(Channel ch) {
+                        latch.countDown();
+                    }
+                })
+                .group(groupA)
+                .channelFactory(new ChannelFactory<Channel>() {
+                    @Override
+                    public Channel newChannel() {
+                        return new LocalChannel() {
+                            private ChannelConfigValidator config;
+                            @Override
+                            public synchronized ChannelConfig config() {
+                                if (config == null) {
+                                    config = new ChannelConfigValidator(this);
+                                }
+                                return config;
+                            }
+                        };
+                    }
+                })
+                .option(ChannelOption.WRITE_BUFFER_LOW_WATER_MARK, 1)
+                .option(ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK, 2);
+
+        bootstrap.register().syncUninterruptibly();
+
+        latch.await();
+
+        // Check the order is the same as what we defined before.
+        assertSame(ChannelOption.WRITE_BUFFER_LOW_WATER_MARK, options.take());
+        assertSame(ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK, options.take());
+    }
+
     private static final class DelayedEventLoopGroup extends DefaultEventLoop {
         @Override
         public ChannelFuture register(final Channel channel, final ChannelPromise promise) {
diff --git a/transport/src/test/java/io/netty/channel/AbstractChannelTest.java b/transport/src/test/java/io/netty/channel/AbstractChannelTest.java
index afbe27c..9d5110e 100644
--- a/transport/src/test/java/io/netty/channel/AbstractChannelTest.java
+++ b/transport/src/test/java/io/netty/channel/AbstractChannelTest.java
@@ -15,8 +15,12 @@
  */
 package io.netty.channel;
 
+import java.io.IOException;
+import java.net.InetSocketAddress;
 import java.net.SocketAddress;
+import java.nio.channels.ClosedChannelException;
 
+import io.netty.util.NetUtil;
 import org.junit.Test;
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.stubbing.Answer;
@@ -82,6 +86,69 @@ public class AbstractChannelTest {
         assertTrue(channelId instanceof DefaultChannelId);
     }
 
+    @Test
+    public void testClosedChannelExceptionCarryIOException() throws Exception {
+        final IOException ioException = new IOException();
+        final Channel channel = new TestChannel() {
+            private boolean open = true;
+            private boolean active;
+
+            @Override
+            protected AbstractUnsafe newUnsafe() {
+                return new AbstractUnsafe() {
+                    @Override
+                    public void connect(
+                            SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) {
+                        active = true;
+                        promise.setSuccess();
+                    }
+                };
+            }
+
+            @Override
+            protected void doClose()  {
+                active = false;
+                open = false;
+            }
+
+            @Override
+            protected void doWrite(ChannelOutboundBuffer in) throws Exception {
+                throw ioException;
+            }
+
+            @Override
+            public boolean isOpen() {
+                return open;
+            }
+
+            @Override
+            public boolean isActive() {
+                return active;
+            }
+        };
+
+        EventLoop loop = new DefaultEventLoop();
+        try {
+            registerChannel(loop, channel);
+            channel.connect(new InetSocketAddress(NetUtil.LOCALHOST, 8888)).sync();
+            assertSame(ioException, channel.writeAndFlush("").await().cause());
+
+            assertClosedChannelException(channel.writeAndFlush(""), ioException);
+            assertClosedChannelException(channel.write(""), ioException);
+            assertClosedChannelException(channel.bind(new InetSocketAddress(NetUtil.LOCALHOST, 8888)), ioException);
+        } finally {
+            channel.close();
+            loop.shutdownGracefully();
+        }
+    }
+
+    private static void assertClosedChannelException(ChannelFuture future, IOException expected)
+            throws InterruptedException {
+        Throwable cause = future.await().cause();
+        assertTrue(cause instanceof ClosedChannelException);
+        assertSame(expected, cause.getCause());
+    }
+
     private static void registerChannel(EventLoop eventLoop, Channel channel) throws Exception {
         DefaultChannelPromise future = new DefaultChannelPromise(channel);
         channel.unsafe().register(eventLoop, future);
@@ -90,19 +157,16 @@ public class AbstractChannelTest {
 
     private static class TestChannel extends AbstractChannel {
         private static final ChannelMetadata TEST_METADATA = new ChannelMetadata(false);
-        private class TestUnsafe extends AbstractUnsafe {
 
-            @Override
-            public void connect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) { }
-        }
+        private final ChannelConfig config = new DefaultChannelConfig(this);
 
-        public TestChannel() {
+        TestChannel() {
             super(null);
         }
 
         @Override
         public ChannelConfig config() {
-            return new DefaultChannelConfig(this);
+            return config;
         }
 
         @Override
@@ -122,7 +186,12 @@ public class AbstractChannelTest {
 
         @Override
         protected AbstractUnsafe newUnsafe() {
-            return new TestUnsafe();
+            return new AbstractUnsafe() {
+                @Override
+                public void connect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) {
+                    promise.setFailure(new UnsupportedOperationException());
+                }
+            };
         }
 
         @Override
@@ -141,16 +210,16 @@ public class AbstractChannelTest {
         }
 
         @Override
-        protected void doBind(SocketAddress localAddress) throws Exception { }
+        protected void doBind(SocketAddress localAddress) { }
 
         @Override
-        protected void doDisconnect() throws Exception { }
+        protected void doDisconnect() { }
 
         @Override
-        protected void doClose() throws Exception { }
+        protected void doClose() { }
 
         @Override
-        protected void doBeginRead() throws Exception { }
+        protected void doBeginRead() { }
 
         @Override
         protected void doWrite(ChannelOutboundBuffer in) throws Exception { }
diff --git a/transport/src/test/java/io/netty/channel/AbstractEventLoopTest.java b/transport/src/test/java/io/netty/channel/AbstractEventLoopTest.java
index b4d29a6..df7fe13 100644
--- a/transport/src/test/java/io/netty/channel/AbstractEventLoopTest.java
+++ b/transport/src/test/java/io/netty/channel/AbstractEventLoopTest.java
@@ -75,7 +75,7 @@ public abstract class AbstractEventLoopTest {
         b.bind(0).sync().channel();
 
         Future<?> f = loop.shutdownGracefully(0, 1, TimeUnit.MINUTES);
-        assertTrue(loop.awaitTermination(2, TimeUnit.SECONDS));
+        assertTrue(loop.awaitTermination(600, TimeUnit.MILLISECONDS));
         assertTrue(f.syncUninterruptibly().isSuccess());
         assertTrue(loop.isShutdown());
         assertTrue(loop.isTerminated());
diff --git a/transport/src/test/java/io/netty/channel/AdaptiveRecvByteBufAllocatorTest.java b/transport/src/test/java/io/netty/channel/AdaptiveRecvByteBufAllocatorTest.java
index 762db69..8f95b2b 100644
--- a/transport/src/test/java/io/netty/channel/AdaptiveRecvByteBufAllocatorTest.java
+++ b/transport/src/test/java/io/netty/channel/AdaptiveRecvByteBufAllocatorTest.java
@@ -54,6 +54,26 @@ public class AdaptiveRecvByteBufAllocatorTest {
         allocReadExpected(handle, alloc, 8388608);
     }
 
+    @Test
+    public void memoryAllocationIntervalsTest() {
+        computingNext(512, 512);
+        computingNext(8192, 1110);
+        computingNext(8192, 1200);
+        computingNext(4096, 1300);
+        computingNext(4096, 1500);
+        computingNext(2048, 1700);
+        computingNext(2048, 1550);
+        computingNext(2048, 2000);
+        computingNext(2048, 1900);
+    }
+
+    private void computingNext(long expectedSize, int actualReadBytes) {
+        assertEquals(expectedSize, handle.guess());
+        handle.reset(config);
+        handle.lastBytesRead(actualReadBytes);
+        handle.readComplete();
+    }
+
     @Test
     public void lastPartialReadDoesNotRampDown() {
         allocReadExpected(handle, alloc, 512);
diff --git a/transport/src/test/java/io/netty/channel/DefaultChannelPipelineTailTest.java b/transport/src/test/java/io/netty/channel/DefaultChannelPipelineTailTest.java
index 697db84..7eb624b 100644
--- a/transport/src/test/java/io/netty/channel/DefaultChannelPipelineTailTest.java
+++ b/transport/src/test/java/io/netty/channel/DefaultChannelPipelineTailTest.java
@@ -237,7 +237,7 @@ public class DefaultChannelPipelineTailTest {
     private static class MyChannelFactory implements ChannelFactory<MyChannel> {
         private final MyChannel channel;
 
-        public MyChannelFactory(MyChannel channel) {
+        MyChannelFactory(MyChannel channel) {
             this.channel = channel;
         }
 
@@ -365,7 +365,7 @@ public class DefaultChannelPipelineTailTest {
 
         private class MyChannelPipeline extends DefaultChannelPipeline {
 
-            public MyChannelPipeline(Channel channel) {
+            MyChannelPipeline(Channel channel) {
                 super(channel);
             }
 
diff --git a/transport/src/test/java/io/netty/channel/DefaultChannelPipelineTest.java b/transport/src/test/java/io/netty/channel/DefaultChannelPipelineTest.java
index 31de253..4d49fdc 100644
--- a/transport/src/test/java/io/netty/channel/DefaultChannelPipelineTest.java
+++ b/transport/src/test/java/io/netty/channel/DefaultChannelPipelineTest.java
@@ -21,10 +21,10 @@ import io.netty.bootstrap.ServerBootstrap;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
 import io.netty.channel.ChannelHandler.Sharable;
+import io.netty.channel.ChannelHandlerMask.Skip;
 import io.netty.channel.embedded.EmbeddedChannel;
 import io.netty.channel.local.LocalAddress;
 import io.netty.channel.local.LocalChannel;
-import io.netty.channel.local.LocalEventLoopGroup;
 import io.netty.channel.local.LocalServerChannel;
 import io.netty.channel.nio.NioEventLoopGroup;
 import io.netty.channel.oio.OioEventLoopGroup;
@@ -43,8 +43,10 @@ import io.netty.util.concurrent.Promise;
 import io.netty.util.concurrent.UnorderedThreadPoolEventExecutor;
 import org.junit.After;
 import org.junit.AfterClass;
+import org.junit.BeforeClass;
 import org.junit.Test;
 
+import java.net.SocketAddress;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -72,11 +74,16 @@ import static org.junit.Assert.fail;
 
 public class DefaultChannelPipelineTest {
 
-    private static final EventLoopGroup group = new DefaultEventLoopGroup(1);
+    private static EventLoopGroup group;
 
     private Channel self;
     private Channel peer;
 
+    @BeforeClass
+    public static void beforeClass() throws Exception {
+        group = new DefaultEventLoopGroup(1);
+    }
+
     @AfterClass
     public static void afterClass() throws Exception {
         group.shutdownGracefully().sync();
@@ -257,6 +264,61 @@ public class DefaultChannelPipelineTest {
         assertSame(pipeline.get("handler2"), newHandler2);
     }
 
+    @Test(expected = IllegalArgumentException.class)
+    public void testReplaceHandlerChecksDuplicateNames() {
+        ChannelPipeline pipeline = new LocalChannel().pipeline();
+
+        ChannelHandler handler1 = newHandler();
+        ChannelHandler handler2 = newHandler();
+        pipeline.addLast("handler1", handler1);
+        pipeline.addLast("handler2", handler2);
+
+        ChannelHandler newHandler1 = newHandler();
+        pipeline.replace("handler1", "handler2", newHandler1);
+    }
+
+    @Test
+    public void testReplaceNameWithGenerated() {
+        ChannelPipeline pipeline = new LocalChannel().pipeline();
+
+        ChannelHandler handler1 = newHandler();
+        pipeline.addLast("handler1", handler1);
+        assertSame(pipeline.get("handler1"), handler1);
+
+        ChannelHandler newHandler1 = newHandler();
+        pipeline.replace("handler1", null, newHandler1);
+        assertSame(pipeline.get("DefaultChannelPipelineTest$TestHandler#0"), newHandler1);
+        assertNull(pipeline.get("handler1"));
+    }
+
+    @Test
+    public void testRenameChannelHandler() {
+        ChannelPipeline pipeline = new LocalChannel().pipeline();
+
+        ChannelHandler handler1 = newHandler();
+        pipeline.addLast("handler1", handler1);
+        pipeline.addLast("handler2", handler1);
+        pipeline.addLast("handler3", handler1);
+        assertSame(pipeline.get("handler1"), handler1);
+        assertSame(pipeline.get("handler2"), handler1);
+        assertSame(pipeline.get("handler3"), handler1);
+
+        ChannelHandler newHandler1 = newHandler();
+        pipeline.replace("handler1", "newHandler1", newHandler1);
+        assertSame(pipeline.get("newHandler1"), newHandler1);
+        assertNull(pipeline.get("handler1"));
+
+        ChannelHandler newHandler3 = newHandler();
+        pipeline.replace("handler3", "newHandler3", newHandler3);
+        assertSame(pipeline.get("newHandler3"), newHandler3);
+        assertNull(pipeline.get("handler3"));
+
+        ChannelHandler newHandler2 = newHandler();
+        pipeline.replace("handler2", "newHandler2", newHandler2);
+        assertSame(pipeline.get("newHandler2"), newHandler2);
+        assertNull(pipeline.get("handler2"));
+    }
+
     @Test
     public void testChannelHandlerContextNavigation() {
         ChannelPipeline pipeline = new LocalChannel().pipeline();
@@ -1223,6 +1285,480 @@ public class DefaultChannelPipelineTest {
         }
     }
 
+    @Test
+    public void testSkipHandlerMethodsIfAnnotated() {
+        EmbeddedChannel channel = new EmbeddedChannel(true);
+        ChannelPipeline pipeline = channel.pipeline();
+
+        final class SkipHandler implements ChannelInboundHandler, ChannelOutboundHandler {
+            private int state = 2;
+            private Error errorRef;
+
+            private void fail() {
+                errorRef = new AssertionError("Method should never been called");
+            }
+
+            @Skip
+            @Override
+            public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) {
+                fail();
+                ctx.bind(localAddress, promise);
+            }
+
+            @Skip
+            @Override
+            public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress,
+                                SocketAddress localAddress, ChannelPromise promise) {
+                fail();
+                ctx.connect(remoteAddress, localAddress, promise);
+            }
+
+            @Skip
+            @Override
+            public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) {
+                fail();
+                ctx.disconnect(promise);
+            }
+
+            @Skip
+            @Override
+            public void close(ChannelHandlerContext ctx, ChannelPromise promise) {
+                fail();
+                ctx.close(promise);
+            }
+
+            @Skip
+            @Override
+            public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) {
+                fail();
+                ctx.deregister(promise);
+            }
+
+            @Skip
+            @Override
+            public void read(ChannelHandlerContext ctx) {
+                fail();
+                ctx.read();
+            }
+
+            @Skip
+            @Override
+            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
+                fail();
+                ctx.write(msg, promise);
+            }
+
+            @Skip
+            @Override
+            public void flush(ChannelHandlerContext ctx) {
+                fail();
+                ctx.flush();
+            }
+
+            @Skip
+            @Override
+            public void channelRegistered(ChannelHandlerContext ctx) {
+                fail();
+                ctx.fireChannelRegistered();
+            }
+
+            @Skip
+            @Override
+            public void channelUnregistered(ChannelHandlerContext ctx) {
+                fail();
+                ctx.fireChannelUnregistered();
+            }
+
+            @Skip
+            @Override
+            public void channelActive(ChannelHandlerContext ctx) {
+                fail();
+                ctx.fireChannelActive();
+            }
+
+            @Skip
+            @Override
+            public void channelInactive(ChannelHandlerContext ctx) {
+                fail();
+                ctx.fireChannelInactive();
+            }
+
+            @Skip
+            @Override
+            public void channelRead(ChannelHandlerContext ctx, Object msg) {
+                fail();
+                ctx.fireChannelRead(msg);
+            }
+
+            @Skip
+            @Override
+            public void channelReadComplete(ChannelHandlerContext ctx) {
+                fail();
+                ctx.fireChannelReadComplete();
+            }
+
+            @Skip
+            @Override
+            public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
+                fail();
+                ctx.fireUserEventTriggered(evt);
+            }
+
+            @Skip
+            @Override
+            public void channelWritabilityChanged(ChannelHandlerContext ctx) {
+                fail();
+                ctx.fireChannelWritabilityChanged();
+            }
+
+            @Skip
+            @Override
+            public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+                fail();
+                ctx.fireExceptionCaught(cause);
+            }
+
+            @Override
+            public void handlerAdded(ChannelHandlerContext ctx) {
+                state--;
+            }
+
+            @Override
+            public void handlerRemoved(ChannelHandlerContext ctx) {
+                state--;
+            }
+
+            void assertSkipped() {
+                assertEquals(0, state);
+                Error error = errorRef;
+                if (error != null) {
+                    throw error;
+                }
+            }
+        }
+
+        final class OutboundCalledHandler extends ChannelOutboundHandlerAdapter {
+            private static final int MASK_BIND = 1;
+            private static final int MASK_CONNECT = 1 << 1;
+            private static final int MASK_DISCONNECT = 1 << 2;
+            private static final int MASK_CLOSE = 1 << 3;
+            private static final int MASK_DEREGISTER = 1 << 4;
+            private static final int MASK_READ = 1 << 5;
+            private static final int MASK_WRITE = 1 << 6;
+            private static final int MASK_FLUSH = 1 << 7;
+            private static final int MASK_ADDED = 1 << 8;
+            private static final int MASK_REMOVED = 1 << 9;
+
+            private int executionMask;
+
+            @Override
+            public void handlerAdded(ChannelHandlerContext ctx) {
+                executionMask |= MASK_ADDED;
+            }
+
+            @Override
+            public void handlerRemoved(ChannelHandlerContext ctx) {
+                executionMask |= MASK_REMOVED;
+            }
+
+            @Override
+            public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) {
+                executionMask |= MASK_BIND;
+                promise.setSuccess();
+            }
+
+            @Override
+            public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress,
+                                SocketAddress localAddress, ChannelPromise promise) {
+                executionMask |= MASK_CONNECT;
+                promise.setSuccess();
+            }
+
+            @Override
+            public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) {
+                executionMask |= MASK_DISCONNECT;
+                promise.setSuccess();
+            }
+
+            @Override
+            public void close(ChannelHandlerContext ctx, ChannelPromise promise) {
+                executionMask |= MASK_CLOSE;
+                promise.setSuccess();
+            }
+
+            @Override
+            public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) {
+                executionMask |= MASK_DEREGISTER;
+                promise.setSuccess();
+            }
+
+            @Override
+            public void read(ChannelHandlerContext ctx) {
+                executionMask |= MASK_READ;
+            }
+
+            @Override
+            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
+                executionMask |= MASK_WRITE;
+                promise.setSuccess();
+            }
+
+            @Override
+            public void flush(ChannelHandlerContext ctx) {
+                executionMask |= MASK_FLUSH;
+            }
+
+            void assertCalled() {
+                assertCalled("handlerAdded", MASK_ADDED);
+                assertCalled("handlerRemoved", MASK_REMOVED);
+                assertCalled("bind", MASK_BIND);
+                assertCalled("connect", MASK_CONNECT);
+                assertCalled("disconnect", MASK_DISCONNECT);
+                assertCalled("close", MASK_CLOSE);
+                assertCalled("deregister", MASK_DEREGISTER);
+                assertCalled("read", MASK_READ);
+                assertCalled("write", MASK_WRITE);
+                assertCalled("flush", MASK_FLUSH);
+            }
+
+            private void assertCalled(String methodName, int mask) {
+                assertTrue(methodName + " was not called", (executionMask & mask) != 0);
+            }
+        }
+
+        final class InboundCalledHandler extends ChannelInboundHandlerAdapter {
+
+            private static final int MASK_CHANNEL_REGISTER = 1;
+            private static final int MASK_CHANNEL_UNREGISTER = 1 << 1;
+            private static final int MASK_CHANNEL_ACTIVE = 1 << 2;
+            private static final int MASK_CHANNEL_INACTIVE = 1 << 3;
+            private static final int MASK_CHANNEL_READ = 1 << 4;
+            private static final int MASK_CHANNEL_READ_COMPLETE = 1 << 5;
+            private static final int MASK_USER_EVENT_TRIGGERED = 1 << 6;
+            private static final int MASK_CHANNEL_WRITABILITY_CHANGED = 1 << 7;
+            private static final int MASK_EXCEPTION_CAUGHT = 1 << 8;
+            private static final int MASK_ADDED = 1 << 9;
+            private static final int MASK_REMOVED = 1 << 10;
+
+            private int executionMask;
+
+            @Override
+            public void handlerAdded(ChannelHandlerContext ctx) {
+                executionMask |= MASK_ADDED;
+            }
+
+            @Override
+            public void handlerRemoved(ChannelHandlerContext ctx) {
+                executionMask |= MASK_REMOVED;
+            }
+
+            @Override
+            public void channelRegistered(ChannelHandlerContext ctx) {
+                executionMask |= MASK_CHANNEL_REGISTER;
+            }
+
+            @Override
+            public void channelUnregistered(ChannelHandlerContext ctx) {
+                executionMask |= MASK_CHANNEL_UNREGISTER;
+            }
+
+            @Override
+            public void channelActive(ChannelHandlerContext ctx) {
+                executionMask |= MASK_CHANNEL_ACTIVE;
+            }
+
+            @Override
+            public void channelInactive(ChannelHandlerContext ctx) {
+                executionMask |= MASK_CHANNEL_INACTIVE;
+            }
+
+            @Override
+            public void channelRead(ChannelHandlerContext ctx, Object msg) {
+                executionMask |= MASK_CHANNEL_READ;
+            }
+
+            @Override
+            public void channelReadComplete(ChannelHandlerContext ctx) {
+                executionMask |= MASK_CHANNEL_READ_COMPLETE;
+            }
+
+            @Override
+            public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
+                executionMask |= MASK_USER_EVENT_TRIGGERED;
+            }
+
+            @Override
+            public void channelWritabilityChanged(ChannelHandlerContext ctx) {
+                executionMask |= MASK_CHANNEL_WRITABILITY_CHANGED;
+            }
+
+            @Override
+            public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+                executionMask |= MASK_EXCEPTION_CAUGHT;
+            }
+
+            void assertCalled() {
+                assertCalled("handlerAdded", MASK_ADDED);
+                assertCalled("handlerRemoved", MASK_REMOVED);
+                assertCalled("channelRegistered", MASK_CHANNEL_REGISTER);
+                assertCalled("channelUnregistered", MASK_CHANNEL_UNREGISTER);
+                assertCalled("channelActive", MASK_CHANNEL_ACTIVE);
+                assertCalled("channelInactive", MASK_CHANNEL_INACTIVE);
+                assertCalled("channelRead", MASK_CHANNEL_READ);
+                assertCalled("channelReadComplete", MASK_CHANNEL_READ_COMPLETE);
+                assertCalled("userEventTriggered", MASK_USER_EVENT_TRIGGERED);
+                assertCalled("channelWritabilityChanged", MASK_CHANNEL_WRITABILITY_CHANGED);
+                assertCalled("exceptionCaught", MASK_EXCEPTION_CAUGHT);
+            }
+
+            private void assertCalled(String methodName, int mask) {
+                assertTrue(methodName + " was not called", (executionMask & mask) != 0);
+            }
+        }
+
+        OutboundCalledHandler outboundCalledHandler = new OutboundCalledHandler();
+        SkipHandler skipHandler = new SkipHandler();
+        InboundCalledHandler inboundCalledHandler = new InboundCalledHandler();
+        pipeline.addLast(outboundCalledHandler, skipHandler, inboundCalledHandler);
+
+        pipeline.fireChannelRegistered();
+        pipeline.fireChannelUnregistered();
+        pipeline.fireChannelActive();
+        pipeline.fireChannelInactive();
+        pipeline.fireChannelRead("");
+        pipeline.fireChannelReadComplete();
+        pipeline.fireChannelWritabilityChanged();
+        pipeline.fireUserEventTriggered("");
+        pipeline.fireExceptionCaught(new Exception());
+
+        pipeline.deregister().syncUninterruptibly();
+        pipeline.bind(new SocketAddress() {
+        }).syncUninterruptibly();
+        pipeline.connect(new SocketAddress() {
+        }).syncUninterruptibly();
+        pipeline.disconnect().syncUninterruptibly();
+        pipeline.close().syncUninterruptibly();
+        pipeline.write("");
+        pipeline.flush();
+        pipeline.read();
+
+        pipeline.remove(outboundCalledHandler);
+        pipeline.remove(inboundCalledHandler);
+        pipeline.remove(skipHandler);
+
+        assertFalse(channel.finish());
+
+        outboundCalledHandler.assertCalled();
+        inboundCalledHandler.assertCalled();
+        skipHandler.assertSkipped();
+    }
+
+    @Test
+    public void testWriteThrowsReleaseMessage() {
+        testWriteThrowsReleaseMessage0(false);
+    }
+
+    @Test
+    public void testWriteAndFlushThrowsReleaseMessage() {
+        testWriteThrowsReleaseMessage0(true);
+    }
+
+    private void testWriteThrowsReleaseMessage0(boolean flush) {
+        ReferenceCounted referenceCounted = new AbstractReferenceCounted() {
+            @Override
+            protected void deallocate() {
+                // NOOP
+            }
+
+            @Override
+            public ReferenceCounted touch(Object hint) {
+                return this;
+            }
+        };
+        assertEquals(1, referenceCounted.refCnt());
+
+        Channel channel = new LocalChannel();
+        Channel channel2 = new LocalChannel();
+        group.register(channel).syncUninterruptibly();
+        group.register(channel2).syncUninterruptibly();
+
+        try {
+            if (flush) {
+                channel.writeAndFlush(referenceCounted, channel2.newPromise());
+            } else {
+                channel.write(referenceCounted, channel2.newPromise());
+            }
+            fail();
+        } catch (IllegalArgumentException expected) {
+            // expected
+        }
+        assertEquals(0, referenceCounted.refCnt());
+
+        channel.close().syncUninterruptibly();
+        channel2.close().syncUninterruptibly();
+    }
+
+    @Test(timeout = 5000)
+    public void testHandlerAddedFailedButHandlerStillRemoved() throws InterruptedException {
+        testHandlerAddedFailedButHandlerStillRemoved0(false);
+    }
+
+    @Test(timeout = 5000)
+    public void testHandlerAddedFailedButHandlerStillRemovedWithLaterRegister() throws InterruptedException {
+        testHandlerAddedFailedButHandlerStillRemoved0(true);
+    }
+
+    private static void testHandlerAddedFailedButHandlerStillRemoved0(boolean lateRegister)
+            throws InterruptedException {
+        EventExecutorGroup executorGroup = new DefaultEventExecutorGroup(16);
+        final int numHandlers = 32;
+        try {
+            Channel channel = new LocalChannel();
+            channel.config().setOption(ChannelOption.SINGLE_EVENTEXECUTOR_PER_GROUP, false);
+            if (!lateRegister) {
+                group.register(channel).sync();
+            }
+            channel.pipeline().addFirst(newHandler());
+
+            List<CountDownLatch> latchList = new ArrayList<CountDownLatch>(numHandlers);
+            for (int i = 0; i < numHandlers; i++) {
+                CountDownLatch latch = new CountDownLatch(1);
+                channel.pipeline().addFirst(executorGroup, "h" + i, new BadChannelHandler(latch));
+                latchList.add(latch);
+            }
+            if (lateRegister) {
+                group.register(channel).sync();
+            }
+
+            for (int i = 0; i < numHandlers; i++) {
+                // Wait until the latch was countDown which means handlerRemoved(...) was called.
+                latchList.get(i).await();
+                assertNull(channel.pipeline().get("h" + i));
+            }
+        } finally {
+            executorGroup.shutdownGracefully();
+        }
+    }
+
+    private static final class BadChannelHandler extends ChannelHandlerAdapter {
+        private final CountDownLatch latch;
+
+        BadChannelHandler(CountDownLatch latch) {
+            this.latch = latch;
+        }
+
+        @Override
+        public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
+            TimeUnit.MILLISECONDS.sleep(10);
+            throw new RuntimeException();
+        }
+
+        @Override
+        public void handlerRemoved(ChannelHandlerContext ctx) {
+            latch.countDown();
+        }
+    }
+
     @Test(timeout = 5000)
     public void handlerAddedStateUpdatedBeforeHandlerAddedDoneForceEventLoop() throws InterruptedException {
         handlerAddedStateUpdatedBeforeHandlerAddedDone(true);
diff --git a/transport/src/test/java/io/netty/channel/DefaultFileRegionTest.java b/transport/src/test/java/io/netty/channel/DefaultFileRegionTest.java
new file mode 100644
index 0000000..e416bcc
--- /dev/null
+++ b/transport/src/test/java/io/netty/channel/DefaultFileRegionTest.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2019 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.channel;
+
+import io.netty.util.internal.PlatformDependent;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.channels.Channels;
+import java.nio.channels.WritableByteChannel;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public class DefaultFileRegionTest {
+
+    private static final byte[] data = new byte[1048576 * 10];
+
+    static {
+        PlatformDependent.threadLocalRandom().nextBytes(data);
+    }
+
+    private static File newFile() throws IOException {
+        File file = File.createTempFile("netty-", ".tmp");
+        file.deleteOnExit();
+
+        final FileOutputStream out = new FileOutputStream(file);
+        out.write(data);
+        out.close();
+        return file;
+    }
+
+    @Test
+    public void testCreateFromFile() throws IOException  {
+        File file = newFile();
+        try {
+            testFileRegion(new DefaultFileRegion(file, 0, data.length));
+        } finally {
+            file.delete();
+        }
+    }
+
+    @Test
+    public void testCreateFromFileChannel() throws IOException  {
+        File file = newFile();
+        RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
+        try {
+            testFileRegion(new DefaultFileRegion(randomAccessFile.getChannel(), 0, data.length));
+        } finally {
+            randomAccessFile.close();
+            file.delete();
+        }
+    }
+
+    private static void testFileRegion(FileRegion region) throws IOException  {
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        WritableByteChannel channel = Channels.newChannel(outputStream);
+
+        try {
+            assertEquals(data.length, region.count());
+            assertEquals(0, region.transferred());
+            assertEquals(data.length, region.transferTo(channel, 0));
+            assertEquals(data.length, region.count());
+            assertEquals(data.length, region.transferred());
+            assertArrayEquals(data, outputStream.toByteArray());
+        } finally {
+            channel.close();
+        }
+    }
+
+    @Test
+    public void testTruncated() throws IOException  {
+        File file = newFile();
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        WritableByteChannel channel = Channels.newChannel(outputStream);
+        RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
+
+        try {
+            FileRegion region = new DefaultFileRegion(randomAccessFile.getChannel(), 0, data.length);
+
+            randomAccessFile.getChannel().truncate(data.length - 1024);
+
+            assertEquals(data.length, region.count());
+            assertEquals(0, region.transferred());
+
+            assertEquals(data.length - 1024, region.transferTo(channel, 0));
+            assertEquals(data.length, region.count());
+            assertEquals(data.length - 1024, region.transferred());
+            try {
+                region.transferTo(channel, data.length - 1024);
+                fail();
+            } catch (IOException expected) {
+                // expected
+            }
+        } finally {
+            channel.close();
+
+            randomAccessFile.close();
+            file.delete();
+        }
+    }
+}
diff --git a/transport/src/test/java/io/netty/channel/DelegatingChannelPromiseNotifierTest.java b/transport/src/test/java/io/netty/channel/DelegatingChannelPromiseNotifierTest.java
index 2f4b0aa..d187773 100644
--- a/transport/src/test/java/io/netty/channel/DelegatingChannelPromiseNotifierTest.java
+++ b/transport/src/test/java/io/netty/channel/DelegatingChannelPromiseNotifierTest.java
@@ -20,8 +20,6 @@ import io.netty.util.concurrent.GenericFutureListener;
 import org.junit.Test;
 import org.mockito.Mockito;
 
-import static org.junit.Assert.*;
-
 public class DelegatingChannelPromiseNotifierTest {
     @Test
     public void varargsNotifiersAllowed() {
diff --git a/transport/src/test/java/io/netty/channel/embedded/EmbeddedChannelTest.java b/transport/src/test/java/io/netty/channel/embedded/EmbeddedChannelTest.java
index a5dae45..ff0f8a7 100644
--- a/transport/src/test/java/io/netty/channel/embedded/EmbeddedChannelTest.java
+++ b/transport/src/test/java/io/netty/channel/embedded/EmbeddedChannelTest.java
@@ -54,6 +54,17 @@ import io.netty.util.concurrent.ScheduledFuture;
 
 public class EmbeddedChannelTest {
 
+    @Test
+    public void testParent() {
+        EmbeddedChannel parent = new EmbeddedChannel();
+        EmbeddedChannel channel = new EmbeddedChannel(parent, EmbeddedChannelId.INSTANCE, true, false);
+        assertSame(parent, channel.parent());
+        assertNull(parent.parent());
+
+        assertFalse(channel.finish());
+        assertFalse(parent.finish());
+    }
+
     @Test
     public void testNotRegistered() throws Exception {
         EmbeddedChannel channel = new EmbeddedChannel(false, false);
@@ -281,6 +292,17 @@ public class EmbeddedChannelTest {
         assertNull(handler.pollEvent());
     }
 
+    @Test
+    public void testHasNoDisconnectSkipDisconnect() throws InterruptedException {
+        EmbeddedChannel channel = new EmbeddedChannel(false, new ChannelOutboundHandlerAdapter() {
+            @Override
+            public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
+                promise.tryFailure(new Throwable());
+            }
+        });
+        assertFalse(channel.disconnect().isSuccess());
+    }
+
     @Test
     public void testFinishAndReleaseAll() {
         ByteBuf in = Unpooled.buffer();
diff --git a/transport/src/test/java/io/netty/channel/nio/NioEventLoopTest.java b/transport/src/test/java/io/netty/channel/nio/NioEventLoopTest.java
index 8b176bc..eb73cb6 100644
--- a/transport/src/test/java/io/netty/channel/nio/NioEventLoopTest.java
+++ b/transport/src/test/java/io/netty/channel/nio/NioEventLoopTest.java
@@ -17,15 +17,21 @@ package io.netty.channel.nio;
 
 import io.netty.channel.AbstractEventLoopTest;
 import io.netty.channel.Channel;
+import io.netty.channel.DefaultSelectStrategyFactory;
 import io.netty.channel.EventLoop;
 import io.netty.channel.EventLoopGroup;
+import io.netty.channel.EventLoopTaskQueueFactory;
 import io.netty.channel.SelectStrategy;
 import io.netty.channel.SelectStrategyFactory;
+import io.netty.channel.SingleThreadEventLoop;
 import io.netty.channel.socket.ServerSocketChannel;
 import io.netty.channel.socket.nio.NioServerSocketChannel;
 import io.netty.util.IntSupplier;
+import io.netty.util.concurrent.DefaultEventExecutorChooserFactory;
 import io.netty.util.concurrent.DefaultThreadFactory;
 import io.netty.util.concurrent.Future;
+import io.netty.util.concurrent.RejectedExecutionHandlers;
+import io.netty.util.concurrent.ThreadPerTaskExecutor;
 import org.hamcrest.core.IsInstanceOf;
 import org.junit.Test;
 
@@ -35,9 +41,13 @@ import java.nio.channels.SelectionKey;
 import java.nio.channels.Selector;
 import java.nio.channels.SocketChannel;
 import java.nio.channels.spi.SelectorProvider;
+import java.util.Queue;
+import java.util.concurrent.Callable;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicReference;
 
 import static org.junit.Assert.*;
@@ -258,4 +268,73 @@ public class NioEventLoopTest extends AbstractEventLoopTest {
         }
     }
 
+    @Test(timeout = 3000L)
+    public void testChannelsRegistered() throws Exception {
+        NioEventLoopGroup group = new NioEventLoopGroup(1);
+        final NioEventLoop loop = (NioEventLoop) group.next();
+
+        try {
+            final Channel ch1 = new NioServerSocketChannel();
+            final Channel ch2 = new NioServerSocketChannel();
+
+            assertEquals(0, registeredChannels(loop));
+
+            assertTrue(loop.register(ch1).syncUninterruptibly().isSuccess());
+            assertTrue(loop.register(ch2).syncUninterruptibly().isSuccess());
+            assertEquals(2, registeredChannels(loop));
+
+            assertTrue(ch1.deregister().syncUninterruptibly().isSuccess());
+
+            int registered;
+            // As SelectionKeys are removed in a lazy fashion in the JDK implementation we may need to query a few
+            // times before we see the right number of registered chanels.
+            while ((registered = registeredChannels(loop)) == 2) {
+                Thread.sleep(50);
+            }
+            assertEquals(1, registered);
+        } finally {
+            group.shutdownGracefully();
+        }
+    }
+
+    // Only reliable if run from event loop
+    private static int registeredChannels(final SingleThreadEventLoop loop) throws Exception {
+        return loop.submit(new Callable<Integer>() {
+            @Override
+            public Integer call() {
+                return loop.registeredChannels();
+            }
+        }).get(1, TimeUnit.SECONDS);
+    }
+
+    @Test
+    public void testCustomQueue()  {
+        final AtomicBoolean called = new AtomicBoolean();
+        NioEventLoopGroup group = new NioEventLoopGroup(1,
+                new ThreadPerTaskExecutor(new DefaultThreadFactory(NioEventLoopGroup.class)),
+                DefaultEventExecutorChooserFactory.INSTANCE, SelectorProvider.provider(),
+                DefaultSelectStrategyFactory.INSTANCE, RejectedExecutionHandlers.reject(),
+                new EventLoopTaskQueueFactory() {
+                    @Override
+                    public Queue<Runnable> newTaskQueue(int maxCapacity) {
+                        called.set(true);
+                        return new LinkedBlockingQueue<Runnable>(maxCapacity);
+                    }
+        });
+
+        final NioEventLoop loop = (NioEventLoop) group.next();
+
+        try {
+            loop.submit(new Runnable() {
+                @Override
+                public void run() {
+                    // NOOP.
+                }
+            }).syncUninterruptibly();
+            assertTrue(called.get());
+        } finally {
+            group.shutdownGracefully();
+        }
+    }
+
 }
diff --git a/transport/src/test/java/io/netty/channel/pool/AbstractChannelPoolMapTest.java b/transport/src/test/java/io/netty/channel/pool/AbstractChannelPoolMapTest.java
index 8d03cd7..3946d22 100644
--- a/transport/src/test/java/io/netty/channel/pool/AbstractChannelPoolMapTest.java
+++ b/transport/src/test/java/io/netty/channel/pool/AbstractChannelPoolMapTest.java
@@ -22,19 +22,26 @@ import io.netty.channel.EventLoopGroup;
 import io.netty.channel.local.LocalAddress;
 import io.netty.channel.local.LocalChannel;
 import io.netty.channel.local.LocalEventLoopGroup;
+import io.netty.util.concurrent.EventExecutor;
+import io.netty.util.concurrent.Future;
+import io.netty.util.concurrent.GenericFutureListener;
+import io.netty.util.concurrent.Promise;
 import org.junit.Test;
 
 import java.net.ConnectException;
+import java.util.concurrent.TimeUnit;
 
-import static org.junit.Assert.*;
+import static io.netty.channel.pool.ChannelPoolTestUtils.getLocalAddrId;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
 
 public class AbstractChannelPoolMapTest {
-    private static final String LOCAL_ADDR_ID = "test.id";
-
     @Test(expected = ConnectException.class)
     public void testMap() throws Exception {
         EventLoopGroup group = new LocalEventLoopGroup();
-        LocalAddress addr = new LocalAddress(LOCAL_ADDR_ID);
+        LocalAddress addr = new LocalAddress(getLocalAddrId());
         final Bootstrap cb = new Bootstrap();
         cb.remoteAddress(addr);
         cb.group(group)
@@ -65,6 +72,62 @@ public class AbstractChannelPoolMapTest {
         assertEquals(0, poolMap.size());
 
         pool.acquire().syncUninterruptibly();
+        poolMap.close();
+    }
+
+    @Test
+    public void testRemoveClosesChannelPool() {
+        EventLoopGroup group = new LocalEventLoopGroup();
+        LocalAddress addr = new LocalAddress(getLocalAddrId());
+        final Bootstrap cb = new Bootstrap();
+        cb.remoteAddress(addr);
+        cb.group(group)
+          .channel(LocalChannel.class);
+
+        AbstractChannelPoolMap<EventLoop, TestPool> poolMap =
+                new AbstractChannelPoolMap<EventLoop, TestPool>() {
+                    @Override
+                    protected TestPool newPool(EventLoop key) {
+                        return new TestPool(cb.clone(key), new TestChannelPoolHandler());
+                    }
+                };
+
+        EventLoop loop = group.next();
+
+        TestPool pool = poolMap.get(loop);
+        assertTrue(poolMap.remove(loop));
+
+        // the pool should be closed eventually after remove
+        pool.closeFuture.awaitUninterruptibly(1, TimeUnit.SECONDS);
+        assertTrue(pool.closeFuture.isDone());
+        poolMap.close();
+    }
+
+    @Test
+    public void testCloseClosesPoolsImmediately() {
+        EventLoopGroup group = new LocalEventLoopGroup();
+        LocalAddress addr = new LocalAddress(getLocalAddrId());
+        final Bootstrap cb = new Bootstrap();
+        cb.remoteAddress(addr);
+        cb.group(group)
+          .channel(LocalChannel.class);
+
+        AbstractChannelPoolMap<EventLoop, TestPool> poolMap =
+                new AbstractChannelPoolMap<EventLoop, TestPool>() {
+                    @Override
+                    protected TestPool newPool(EventLoop key) {
+                        return new TestPool(cb.clone(key), new TestChannelPoolHandler());
+                    }
+                };
+
+        EventLoop loop = group.next();
+
+        TestPool pool = poolMap.get(loop);
+        assertFalse(pool.closeFuture.isDone());
+
+        // the pool should be closed immediately after remove
+        poolMap.close();
+        assertTrue(pool.closeFuture.isDone());
     }
 
     private static final class TestChannelPoolHandler extends AbstractChannelPoolHandler {
@@ -73,4 +136,30 @@ public class AbstractChannelPoolMapTest {
             // NOOP
         }
     }
+
+    private static final class TestPool extends SimpleChannelPool {
+        private final Promise<Void> closeFuture;
+
+        TestPool(Bootstrap bootstrap, ChannelPoolHandler handler) {
+            super(bootstrap, handler);
+            EventExecutor executor = bootstrap.config().group().next();
+            closeFuture = executor.newPromise();
+        }
+
+        @Override
+        public Future<Void> closeAsync() {
+            Future<Void> poolClose = super.closeAsync();
+            poolClose.addListener(new GenericFutureListener<Future<? super Void>>() {
+                @Override
+                public void operationComplete(Future<? super Void> future) throws Exception {
+                    if (future.isSuccess()) {
+                        closeFuture.setSuccess(null);
+                    } else {
+                        closeFuture.setFailure(future.cause());
+                    }
+                }
+            });
+            return poolClose;
+        }
+    }
 }
diff --git a/transport/src/test/java/io/netty/channel/pool/ChannelPoolTestUtils.java b/transport/src/test/java/io/netty/channel/pool/ChannelPoolTestUtils.java
new file mode 100644
index 0000000..5c7486d
--- /dev/null
+++ b/transport/src/test/java/io/netty/channel/pool/ChannelPoolTestUtils.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2012 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.channel.pool;
+
+import io.netty.util.internal.ThreadLocalRandom;
+
+final class ChannelPoolTestUtils {
+    private static final String LOCAL_ADDR_ID = "test.id";
+
+    private ChannelPoolTestUtils() {
+    }
+
+    static String getLocalAddrId() {
+        return LOCAL_ADDR_ID + ThreadLocalRandom.current().nextInt();
+    }
+}
diff --git a/transport/src/test/java/io/netty/channel/pool/FixedChannelPoolMapDeadlockTest.java b/transport/src/test/java/io/netty/channel/pool/FixedChannelPoolMapDeadlockTest.java
new file mode 100644
index 0000000..399383a
--- /dev/null
+++ b/transport/src/test/java/io/netty/channel/pool/FixedChannelPoolMapDeadlockTest.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright 2019 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.channel.pool;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.DefaultEventLoop;
+import io.netty.channel.EventLoop;
+import io.netty.channel.local.LocalAddress;
+import io.netty.channel.local.LocalChannel;
+import io.netty.util.concurrent.Future;
+import org.junit.Test;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import static org.junit.Assert.*;
+
+/**
+ * This is a test case for the deadlock scenario described in https://github.com/netty/netty/issues/8238.
+ */
+public class FixedChannelPoolMapDeadlockTest {
+
+    private static final NoopHandler NOOP_HANDLER = new NoopHandler();
+
+    @Test
+    public void testDeadlockOnAcquire() throws Exception {
+
+        final EventLoop threadA1 = new DefaultEventLoop();
+        final Bootstrap bootstrapA1 = new Bootstrap()
+                .channel(LocalChannel.class).group(threadA1).localAddress(new LocalAddress("A1"));
+        final EventLoop threadA2 = new DefaultEventLoop();
+        final Bootstrap bootstrapA2 = new Bootstrap()
+                .channel(LocalChannel.class).group(threadA2).localAddress(new LocalAddress("A2"));
+        final EventLoop threadB1 = new DefaultEventLoop();
+        final Bootstrap bootstrapB1 = new Bootstrap()
+                .channel(LocalChannel.class).group(threadB1).localAddress(new LocalAddress("B1"));
+        final EventLoop threadB2 = new DefaultEventLoop();
+        final Bootstrap bootstrapB2 = new Bootstrap()
+                .channel(LocalChannel.class).group(threadB2).localAddress(new LocalAddress("B2"));
+
+        final FixedChannelPool poolA1 = new FixedChannelPool(bootstrapA1, NOOP_HANDLER, 1);
+        final FixedChannelPool poolA2 = new FixedChannelPool(bootstrapB2, NOOP_HANDLER, 1);
+        final FixedChannelPool poolB1 = new FixedChannelPool(bootstrapB1, NOOP_HANDLER, 1);
+        final FixedChannelPool poolB2 = new FixedChannelPool(bootstrapA2, NOOP_HANDLER, 1);
+
+        // Synchronize threads on these barriers to ensure order of execution, first wait until each thread is inside
+        // the newPool callbak, then hold the two threads that should lose the match until the first two returns, then
+        // release them to test if they deadlock when trying to release their pools on each other's threads.
+        final CyclicBarrier arrivalBarrier = new CyclicBarrier(4);
+        final CyclicBarrier releaseBarrier = new CyclicBarrier(3);
+
+        final AbstractChannelPoolMap<String, FixedChannelPool> channelPoolMap =
+                new AbstractChannelPoolMap<String, FixedChannelPool>() {
+
+            @Override
+            protected FixedChannelPool newPool(String key) {
+
+                // Thread A1 gets a new pool on eventexecutor thread A1 (anywhere but A2 or B2)
+                // Thread B1 gets a new pool on eventexecutor thread B1 (anywhere but A2 or B2)
+                // Thread A2 gets a new pool on eventexecutor thread B2
+                // Thread B2 gets a new pool on eventexecutor thread A2
+
+                if ("A".equals(key)) {
+                    if (threadA1.inEventLoop()) {
+                        // Thread A1 gets pool A with thread A1
+                        await(arrivalBarrier);
+                        return poolA1;
+                    } else if (threadA2.inEventLoop()) {
+                        // Thread A2 gets pool A with thread B2, but only after A1 won
+                        await(arrivalBarrier);
+                        await(releaseBarrier);
+                        return poolA2;
+                    }
+                } else if ("B".equals(key)) {
+                    if (threadB1.inEventLoop()) {
+                        // Thread B1 gets pool with thread B1
+                        await(arrivalBarrier);
+                        return poolB1;
+                    } else if (threadB2.inEventLoop()) {
+                        // Thread B2 gets pool with thread A2
+                        await(arrivalBarrier);
+                        await(releaseBarrier);
+                        return poolB2;
+                    }
+                }
+                throw new AssertionError("Unexpected key=" + key + " or thread="
+                                         + Thread.currentThread().getName());
+            }
+        };
+
+        // Thread A1 calls ChannelPoolMap.get(A)
+        // Thread A2 calls ChannelPoolMap.get(A)
+        // Thread B1 calls ChannelPoolMap.get(B)
+        // Thread B2 calls ChannelPoolMap.get(B)
+
+        Future<FixedChannelPool> futureA1 = threadA1.submit(new Callable<FixedChannelPool>() {
+            @Override
+            public FixedChannelPool call() throws Exception {
+                return channelPoolMap.get("A");
+            }
+        });
+
+        Future<FixedChannelPool> futureA2 = threadA2.submit(new Callable<FixedChannelPool>() {
+            @Override
+            public FixedChannelPool call() throws Exception {
+                return channelPoolMap.get("A");
+            }
+        });
+
+        Future<FixedChannelPool> futureB1 = threadB1.submit(new Callable<FixedChannelPool>() {
+            @Override
+            public FixedChannelPool call() throws Exception {
+                return channelPoolMap.get("B");
+            }
+        });
+
+        Future<FixedChannelPool> futureB2 = threadB2.submit(new Callable<FixedChannelPool>() {
+            @Override
+            public FixedChannelPool call() throws Exception {
+                return channelPoolMap.get("B");
+            }
+        });
+
+        // Thread A1 succeeds on updating the map and moves on
+        // Thread B1 succeeds on updating the map and moves on
+        // These should always succeed and return with new pools
+        try {
+            assertSame(poolA1, futureA1.get(1, TimeUnit.SECONDS));
+            assertSame(poolB1, futureB1.get(1, TimeUnit.SECONDS));
+        } catch (Exception e) {
+            shutdown(threadA1, threadA2, threadB1, threadB2);
+            throw e;
+        }
+
+        // Now release the other two threads which at this point lost the race and will try to clean up the acquired
+        // pools. The expected scenario is that both pools close, in case of a deadlock they will hang.
+        await(releaseBarrier);
+
+        // Thread A2 fails to update the map and submits close to thread B2
+        // Thread B2 fails to update the map and submits close to thread A2
+        // If the close is blocking, then these calls will time out as the threads are waiting for each other
+        // If the close is not blocking, then the previously created pools will be returned
+        try {
+            assertSame(poolA1, futureA2.get(1, TimeUnit.SECONDS));
+            assertSame(poolB1, futureB2.get(1, TimeUnit.SECONDS));
+        } catch (TimeoutException e) {
+            // Fail the test on timeout to distinguish from other errors
+            throw new AssertionError(e);
+        } finally {
+            poolA1.close();
+            poolA2.close();
+            poolB1.close();
+            poolB2.close();
+            channelPoolMap.close();
+            shutdown(threadA1, threadA2, threadB1, threadB2);
+        }
+    }
+
+    @Test
+    public void testDeadlockOnRemove() throws Exception {
+
+        final EventLoop thread1 = new DefaultEventLoop();
+        final Bootstrap bootstrap1 = new Bootstrap()
+                .channel(LocalChannel.class).group(thread1).localAddress(new LocalAddress("#1"));
+        final EventLoop thread2 = new DefaultEventLoop();
+        final Bootstrap bootstrap2 = new Bootstrap()
+                .channel(LocalChannel.class).group(thread2).localAddress(new LocalAddress("#2"));
+
+        // pool1 runs on thread2, pool2 runs on thread1
+        final FixedChannelPool pool1 = new FixedChannelPool(bootstrap2, NOOP_HANDLER, 1);
+        final FixedChannelPool pool2 = new FixedChannelPool(bootstrap1, NOOP_HANDLER, 1);
+
+        final AbstractChannelPoolMap<String, FixedChannelPool> channelPoolMap =
+                new AbstractChannelPoolMap<String, FixedChannelPool>() {
+
+            @Override
+            protected FixedChannelPool newPool(String key) {
+                if ("#1".equals(key)) {
+                    return pool1;
+                } else if ("#2".equals(key)) {
+                    return pool2;
+                } else {
+                    throw new AssertionError("Unexpected key=" + key);
+                }
+            }
+        };
+
+        assertSame(pool1, channelPoolMap.get("#1"));
+        assertSame(pool2, channelPoolMap.get("#2"));
+
+        // thread1 tries to remove pool1 which is running on thread2
+        // thread2 tries to remove pool2 which is running on thread1
+
+        final CyclicBarrier barrier = new CyclicBarrier(2);
+
+        Future<?> future1 = thread1.submit(new Runnable() {
+            @Override
+            public void run() {
+                await(barrier);
+                channelPoolMap.remove("#1");
+            }
+        });
+
+        Future<?> future2 = thread2.submit(new Runnable() {
+            @Override
+            public void run() {
+                await(barrier);
+                channelPoolMap.remove("#2");
+            }
+        });
+
+        // A blocking close on remove will cause a deadlock here and the test will time out
+        try {
+            future1.get(1, TimeUnit.SECONDS);
+            future2.get(1, TimeUnit.SECONDS);
+        } catch (TimeoutException e) {
+            // Fail the test on timeout to distinguish from other errors
+            throw new AssertionError(e);
+        } finally {
+            pool1.close();
+            pool2.close();
+            channelPoolMap.close();
+            shutdown(thread1, thread2);
+        }
+    }
+
+    private static void await(CyclicBarrier barrier) {
+        try {
+            barrier.await(1, TimeUnit.SECONDS);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static void shutdown(EventLoop... eventLoops) {
+        for (EventLoop eventLoop : eventLoops) {
+            eventLoop.shutdownGracefully(0, 0, TimeUnit.SECONDS);
+        }
+    }
+
+    private static class NoopHandler extends AbstractChannelPoolHandler {
+        @Override
+        public void channelCreated(Channel ch) throws Exception {
+            // noop
+        }
+    };
+}
diff --git a/transport/src/test/java/io/netty/channel/pool/FixedChannelPoolTest.java b/transport/src/test/java/io/netty/channel/pool/FixedChannelPoolTest.java
index bbf1deb..0ee08fa 100644
--- a/transport/src/test/java/io/netty/channel/pool/FixedChannelPoolTest.java
+++ b/transport/src/test/java/io/netty/channel/pool/FixedChannelPoolTest.java
@@ -20,30 +20,38 @@ import io.netty.bootstrap.ServerBootstrap;
 import io.netty.channel.Channel;
 import io.netty.channel.ChannelInboundHandlerAdapter;
 import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelPromise;
+import io.netty.channel.DefaultEventLoopGroup;
 import io.netty.channel.EventLoopGroup;
 import io.netty.channel.local.LocalAddress;
 import io.netty.channel.local.LocalChannel;
-import io.netty.channel.local.LocalEventLoopGroup;
 import io.netty.channel.local.LocalServerChannel;
 import io.netty.channel.pool.FixedChannelPool.AcquireTimeoutAction;
 import io.netty.util.concurrent.Future;
+import io.netty.util.concurrent.GenericFutureListener;
 import org.junit.AfterClass;
+import org.junit.Assert;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 
-import static org.junit.Assert.*;
+import static io.netty.channel.pool.ChannelPoolTestUtils.getLocalAddrId;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 public class FixedChannelPoolTest {
-    private static final String LOCAL_ADDR_ID = "test.id";
-
     private static EventLoopGroup group;
 
     @BeforeClass
     public static void createEventLoop() {
-        group = new LocalEventLoopGroup();
+        group = new DefaultEventLoopGroup();
     }
 
     @AfterClass
@@ -55,7 +63,7 @@ public class FixedChannelPoolTest {
 
     @Test
     public void testAcquire() throws Exception {
-        LocalAddress addr = new LocalAddress(LOCAL_ADDR_ID);
+        LocalAddress addr = new LocalAddress(getLocalAddrId());
         Bootstrap cb = new Bootstrap();
         cb.remoteAddress(addr);
         cb.group(group)
@@ -88,11 +96,12 @@ public class FixedChannelPoolTest {
         assertSame(channel, channel2);
         assertEquals(1, handler.channelCount());
 
-        assertEquals(1, handler.acquiredCount());
+        assertEquals(2, handler.acquiredCount());
         assertEquals(1, handler.releasedCount());
 
         sc.close().syncUninterruptibly();
         channel2.close().syncUninterruptibly();
+        pool.close();
     }
 
     @Test(expected = TimeoutException.class)
@@ -106,7 +115,7 @@ public class FixedChannelPoolTest {
     }
 
     private static void testAcquireTimeout(long timeoutMillis) throws Exception {
-        LocalAddress addr = new LocalAddress(LOCAL_ADDR_ID);
+        LocalAddress addr = new LocalAddress(getLocalAddrId());
         Bootstrap cb = new Bootstrap();
         cb.remoteAddress(addr);
         cb.group(group)
@@ -135,12 +144,13 @@ public class FixedChannelPoolTest {
         } finally {
             sc.close().syncUninterruptibly();
             channel.close().syncUninterruptibly();
+            pool.close();
         }
     }
 
     @Test
     public void testAcquireNewConnection() throws Exception {
-        LocalAddress addr = new LocalAddress(LOCAL_ADDR_ID);
+        LocalAddress addr = new LocalAddress(getLocalAddrId());
         Bootstrap cb = new Bootstrap();
         cb.remoteAddress(addr);
         cb.group(group)
@@ -168,6 +178,7 @@ public class FixedChannelPoolTest {
         sc.close().syncUninterruptibly();
         channel.close().syncUninterruptibly();
         channel2.close().syncUninterruptibly();
+        pool.close();
     }
 
     /**
@@ -176,7 +187,7 @@ public class FixedChannelPoolTest {
      */
     @Test
     public void testAcquireNewConnectionWhen() throws Exception {
-        LocalAddress addr = new LocalAddress(LOCAL_ADDR_ID);
+        LocalAddress addr = new LocalAddress(getLocalAddrId());
         Bootstrap cb = new Bootstrap();
         cb.remoteAddress(addr);
         cb.group(group)
@@ -205,11 +216,12 @@ public class FixedChannelPoolTest {
         assertNotSame(channel1, channel2);
         sc.close().syncUninterruptibly();
         channel2.close().syncUninterruptibly();
+        pool.close();
     }
 
     @Test(expected = IllegalStateException.class)
     public void testAcquireBoundQueue() throws Exception {
-        LocalAddress addr = new LocalAddress(LOCAL_ADDR_ID);
+        LocalAddress addr = new LocalAddress(getLocalAddrId());
         Bootstrap cb = new Bootstrap();
         cb.remoteAddress(addr);
         cb.group(group)
@@ -239,12 +251,13 @@ public class FixedChannelPoolTest {
         } finally {
             sc.close().syncUninterruptibly();
             channel.close().syncUninterruptibly();
+            pool.close();
         }
     }
 
     @Test(expected = IllegalArgumentException.class)
     public void testReleaseDifferentPool() throws Exception {
-        LocalAddress addr = new LocalAddress(LOCAL_ADDR_ID);
+        LocalAddress addr = new LocalAddress(getLocalAddrId());
         Bootstrap cb = new Bootstrap();
         cb.remoteAddress(addr);
         cb.group(group)
@@ -273,12 +286,14 @@ public class FixedChannelPoolTest {
         } finally {
             sc.close().syncUninterruptibly();
             channel.close().syncUninterruptibly();
+            pool.close();
+            pool2.close();
         }
     }
 
     @Test
     public void testReleaseAfterClosePool() throws Exception {
-        LocalAddress addr = new LocalAddress(LOCAL_ADDR_ID);
+        LocalAddress addr = new LocalAddress(getLocalAddrId());
         Bootstrap cb = new Bootstrap();
         cb.remoteAddress(addr);
         cb.group(group).channel(LocalChannel.class);
@@ -310,17 +325,18 @@ public class FixedChannelPoolTest {
             pool.release(channel).syncUninterruptibly();
             fail();
         } catch (IllegalStateException e) {
-            assertSame(FixedChannelPool.POOL_CLOSED_ON_RELEASE_EXCEPTION, e);
+            // expected
         }
         // Since the pool is closed, the Channel should have been closed as well.
         channel.closeFuture().syncUninterruptibly();
         assertFalse("Unexpected open channel", channel.isOpen());
         sc.close().syncUninterruptibly();
+        pool.close();
     }
 
     @Test
     public void testReleaseClosed() {
-        LocalAddress addr = new LocalAddress(LOCAL_ADDR_ID);
+        LocalAddress addr = new LocalAddress(getLocalAddrId());
         Bootstrap cb = new Bootstrap();
         cb.remoteAddress(addr);
         cb.group(group).channel(LocalChannel.class);
@@ -344,6 +360,43 @@ public class FixedChannelPoolTest {
         pool.release(channel).syncUninterruptibly();
 
         sc.close().syncUninterruptibly();
+        pool.close();
+    }
+
+    @Test
+    public void testCloseAsync() throws ExecutionException, InterruptedException {
+        LocalAddress addr = new LocalAddress(getLocalAddrId());
+        Bootstrap cb = new Bootstrap();
+        cb.remoteAddress(addr);
+        cb.group(group).channel(LocalChannel.class);
+
+        ServerBootstrap sb = new ServerBootstrap();
+        sb.group(group)
+                .channel(LocalServerChannel.class)
+                .childHandler(new ChannelInitializer<LocalChannel>() {
+                    @Override
+                    public void initChannel(LocalChannel ch) throws Exception {
+                        ch.pipeline().addLast(new ChannelInboundHandlerAdapter());
+                    }
+                });
+
+        // Start server
+        final Channel sc = sb.bind(addr).syncUninterruptibly().channel();
+
+        final FixedChannelPool pool = new FixedChannelPool(cb, new TestChannelPoolHandler(), 2);
+
+        pool.acquire().get();
+        pool.acquire().get();
+
+        final ChannelPromise closePromise = sc.newPromise();
+        pool.closeAsync().addListener(new GenericFutureListener<Future<? super Void>>() {
+            @Override
+            public void operationComplete(Future<? super Void> future) throws Exception {
+                Assert.assertEquals(0, pool.acquiredChannelCount());
+                sc.close(closePromise).syncUninterruptibly();
+            }
+        }).awaitUninterruptibly();
+        closePromise.awaitUninterruptibly();
     }
 
     private static final class TestChannelPoolHandler extends AbstractChannelPoolHandler {
diff --git a/transport/src/test/java/io/netty/channel/pool/SimpleChannelPoolTest.java b/transport/src/test/java/io/netty/channel/pool/SimpleChannelPoolTest.java
index a91790c..debb9df 100644
--- a/transport/src/test/java/io/netty/channel/pool/SimpleChannelPoolTest.java
+++ b/transport/src/test/java/io/netty/channel/pool/SimpleChannelPoolTest.java
@@ -20,29 +20,34 @@ import io.netty.bootstrap.ServerBootstrap;
 import io.netty.channel.Channel;
 import io.netty.channel.ChannelInboundHandlerAdapter;
 import io.netty.channel.ChannelInitializer;
+import io.netty.channel.DefaultEventLoopGroup;
 import io.netty.channel.EventLoopGroup;
 import io.netty.channel.local.LocalAddress;
 import io.netty.channel.local.LocalChannel;
-import io.netty.channel.local.LocalEventLoopGroup;
 import io.netty.channel.local.LocalServerChannel;
 import io.netty.util.concurrent.Future;
 import org.hamcrest.CoreMatchers;
-import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 
 import java.util.Queue;
 import java.util.concurrent.LinkedBlockingQueue;
-
-import static org.junit.Assert.*;
+import java.util.concurrent.TimeUnit;
+
+import static io.netty.channel.pool.ChannelPoolTestUtils.getLocalAddrId;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 public class SimpleChannelPoolTest {
-    private static final String LOCAL_ADDR_ID = "test.id";
-
     @Test
     public void testAcquire() throws Exception {
-        EventLoopGroup group = new LocalEventLoopGroup();
-        LocalAddress addr = new LocalAddress(LOCAL_ADDR_ID);
+        EventLoopGroup group = new DefaultEventLoopGroup();
+        LocalAddress addr = new LocalAddress(getLocalAddrId());
         Bootstrap cb = new Bootstrap();
         cb.remoteAddress(addr);
         cb.group(group)
@@ -82,17 +87,18 @@ public class SimpleChannelPoolTest {
             assertFalse(channel.isActive());
         }
 
-        assertEquals(1, handler.acquiredCount());
+        assertEquals(2, handler.acquiredCount());
         assertEquals(2, handler.releasedCount());
 
         sc.close().sync();
+        pool.close();
         group.shutdownGracefully();
     }
 
     @Test
     public void testBoundedChannelPoolSegment() throws Exception {
-        EventLoopGroup group = new LocalEventLoopGroup();
-        LocalAddress addr = new LocalAddress(LOCAL_ADDR_ID);
+        EventLoopGroup group = new DefaultEventLoopGroup();
+        LocalAddress addr = new LocalAddress(getLocalAddrId());
         Bootstrap cb = new Bootstrap();
         cb.remoteAddress(addr);
         cb.group(group)
@@ -139,11 +145,12 @@ public class SimpleChannelPoolTest {
         channel2.close().sync();
 
         assertEquals(2, handler.channelCount());
-        assertEquals(0, handler.acquiredCount());
+        assertEquals(2, handler.acquiredCount());
         assertEquals(1, handler.releasedCount());
         sc.close().sync();
         channel.close().sync();
         channel2.close().sync();
+        pool.close();
         group.shutdownGracefully();
     }
 
@@ -154,8 +161,8 @@ public class SimpleChannelPoolTest {
      */
     @Test
     public void testUnhealthyChannelIsNotOffered() throws Exception {
-        EventLoopGroup group = new LocalEventLoopGroup();
-        LocalAddress addr = new LocalAddress(LOCAL_ADDR_ID);
+        EventLoopGroup group = new DefaultEventLoopGroup();
+        LocalAddress addr = new LocalAddress(getLocalAddrId());
         Bootstrap cb = new Bootstrap();
         cb.remoteAddress(addr);
         cb.group(group)
@@ -189,6 +196,7 @@ public class SimpleChannelPoolTest {
         assertNotSame(channel1, channel3);
         sc.close().syncUninterruptibly();
         channel3.close().syncUninterruptibly();
+        pool.close();
         group.shutdownGracefully();
     }
 
@@ -200,8 +208,8 @@ public class SimpleChannelPoolTest {
      */
     @Test
     public void testUnhealthyChannelIsOfferedWhenNoHealthCheckRequested() throws Exception {
-        EventLoopGroup group = new LocalEventLoopGroup();
-        LocalAddress addr = new LocalAddress(LOCAL_ADDR_ID);
+        EventLoopGroup group = new DefaultEventLoopGroup();
+        LocalAddress addr = new LocalAddress(getLocalAddrId());
         Bootstrap cb = new Bootstrap();
         cb.remoteAddress(addr);
         cb.group(group)
@@ -232,6 +240,7 @@ public class SimpleChannelPoolTest {
         assertNotSame(channel1, channel2);
         sc.close().syncUninterruptibly();
         channel2.close().syncUninterruptibly();
+        pool.close();
         group.shutdownGracefully();
     }
 
@@ -301,4 +310,46 @@ public class SimpleChannelPoolTest {
             noHealthCheckOnReleasePool.close();
         }
     }
+
+    @Test
+    public void testCloseAsync() throws Exception {
+        final LocalAddress addr = new LocalAddress(getLocalAddrId());
+        final EventLoopGroup group = new DefaultEventLoopGroup();
+
+        // Start server
+        final ServerBootstrap sb = new ServerBootstrap()
+                .group(group)
+                .channel(LocalServerChannel.class)
+                .childHandler(new ChannelInitializer<LocalChannel>() {
+                    @Override
+                    protected void initChannel(LocalChannel ch) throws Exception {
+                        ch.pipeline().addLast(new ChannelInboundHandlerAdapter());
+                    }
+                });
+        final Channel sc = sb.bind(addr).syncUninterruptibly().channel();
+
+        // Create pool, acquire and return channels
+        final Bootstrap bootstrap = new Bootstrap()
+                .channel(LocalChannel.class).group(group).remoteAddress(addr);
+        final SimpleChannelPool pool = new SimpleChannelPool(bootstrap, new CountingChannelPoolHandler());
+        Channel ch1 = pool.acquire().syncUninterruptibly().getNow();
+        Channel ch2 = pool.acquire().syncUninterruptibly().getNow();
+        pool.release(ch1).get(1, TimeUnit.SECONDS);
+        pool.release(ch2).get(1, TimeUnit.SECONDS);
+
+        // Assert that returned channels are open before close
+        assertTrue(ch1.isOpen());
+        assertTrue(ch2.isOpen());
+
+        // Close asynchronously with timeout
+        pool.closeAsync().get(1, TimeUnit.SECONDS);
+
+        // Assert channels were indeed closed
+        assertFalse(ch1.isOpen());
+        assertFalse(ch2.isOpen());
+
+        sc.close().sync();
+        pool.close();
+        group.shutdownGracefully();
+    }
 }
diff --git a/transport/src/test/java/io/netty/channel/socket/nio/NioServerSocketChannelTest.java b/transport/src/test/java/io/netty/channel/socket/nio/NioServerSocketChannelTest.java
index ec50223..f9c68b6 100644
--- a/transport/src/test/java/io/netty/channel/socket/nio/NioServerSocketChannelTest.java
+++ b/transport/src/test/java/io/netty/channel/socket/nio/NioServerSocketChannelTest.java
@@ -15,6 +15,7 @@
  */
 package io.netty.channel.socket.nio;
 
+import io.netty.channel.Channel;
 import io.netty.channel.EventLoopGroup;
 import io.netty.channel.nio.NioEventLoopGroup;
 import org.junit.Assert;
@@ -45,6 +46,23 @@ public class NioServerSocketChannelTest extends AbstractNioChannelTest<NioServer
         }
     }
 
+    @Test
+    public void testIsActiveFalseAfterClose()  {
+        NioServerSocketChannel serverSocketChannel = new NioServerSocketChannel();
+        EventLoopGroup group = new NioEventLoopGroup(1);
+        try {
+            group.register(serverSocketChannel).syncUninterruptibly();
+            Channel channel = serverSocketChannel.bind(new InetSocketAddress(0)).syncUninterruptibly().channel();
+            Assert.assertTrue(channel.isActive());
+            Assert.assertTrue(channel.isOpen());
+            channel.close().syncUninterruptibly();
+            Assert.assertFalse(channel.isOpen());
+            Assert.assertFalse(channel.isActive());
+        } finally {
+            group.shutdownGracefully();
+        }
+    }
+
     @Override
     protected NioServerSocketChannel newNioChannel() {
         return new NioServerSocketChannel();
diff --git a/transport/test.log b/transport/test.log
deleted file mode 100644
index e69de29..0000000
-- 
GitLab