diff --git a/README.md b/README.md
index 419eaec..29ad1d4 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,6 @@
+![Maven Central](https://img.shields.io/maven-central/v/com.jauntsdn.netty/netty-websocket-http1)
+[![Build](https://github.com/jauntsdn/netty-websocket-http1/actions/workflows/ci-build.yml/badge.svg)](https://github.com/jauntsdn/netty-websocket-http1/actions/workflows/ci-build.yml)
+
 # netty-websocket-http1
 
 Alternative Netty implementation of [RFC6455](https://tools.ietf.org/html/rfc6455) - the WebSocket protocol. 
@@ -59,9 +62,10 @@ non-masked frames with 8, 64, 125, 1000 bytes of payload over encrypted/non-encr
 
 ### websocket-http2
 
-Library may be combined with [jauntsdn/websocket-http2](https://github.com/jauntsdn/netty-websocket-http2) using [http1 codec](https://github.com/jauntsdn/netty-websocket-http2/blob/develop/netty-websocket-http2/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/Http1WebSocketCodec.java) 
+Library may be combined with [jauntsdn/websocket-http2](https://github.com/jauntsdn/netty-websocket-http2) using [http1 codec](https://github.com/jauntsdn/netty-websocket-http2/blob/develop/netty-websocket-http2-callbacks-codec/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/WebSocketCallbacksCodec.java) 
 
-for significantly improved per-core throughput [this codec perf-test](), [netty built-in codec perf-test](): 
+for significantly improved per-core throughput [this codec perf-test](https://github.com/jauntsdn/netty-websocket-http2/tree/develop/netty-websocket-http2-perftest/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/perftest/callbackscodec), 
+[netty built-in codec perf-test](https://github.com/jauntsdn/netty-websocket-http2/tree/develop/netty-websocket-http2-perftest/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/perftest/messagecodec): 
 for 8, 125, 1000 byte payload frames over encrypted connection results are as follows:  
 
 | payload size | this codec, million msgs  | netty's codec, million msgs |
@@ -162,7 +166,7 @@ repositories {
 }
 
 dependencies {
-    implementation "com.jauntsdn.netty:netty-websocket-http1:<VERSION>"
+    implementation "com.jauntsdn.netty:netty-websocket-http1:0.9.0"
 }
 ```
 
diff --git a/gradle.properties b/gradle.properties
index 5ba75a4..ec516f9 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,5 +1,5 @@
 group=com.jauntsdn.netty
-version=0.9.0
+version=0.9.1
 
 googleJavaFormatPluginVersion=0.9
 dependencyManagementPluginVersion=1.1.0
@@ -7,15 +7,15 @@ gitPluginVersion=0.13.0
 osDetectorPluginVersion=1.7.1
 versionsPluginVersion=0.44.0
 
-nettyVersion=4.1.86.Final
-nettyTcnativeVersion=2.0.54.Final
+nettyVersion=4.1.87.Final
+nettyTcnativeVersion=2.0.56.Final
 hdrHistogramVersion=2.1.12
 slf4jVersion=1.7.36
 logbackVersion=1.2.11
 jsr305Version=3.0.2
 
-junitVersion=5.9.1
-assertjVersion=3.23.1
+junitVersion=5.9.2
+assertjVersion=3.24.2
 
 org.gradle.parallel=true
 org.gradle.configureondemand=true
\ No newline at end of file
diff --git a/license.md b/license.md
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/license.md
@@ -0,0 +1,201 @@
+                                 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
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed 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.
diff --git a/netty-websocket-http1-soaktest/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/soaktest/server/Main.java b/netty-websocket-http1-soaktest/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/soaktest/server/Main.java
index bd09017..dbfe6e8 100644
--- a/netty-websocket-http1-soaktest/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/soaktest/server/Main.java
+++ b/netty-websocket-http1-soaktest/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/soaktest/server/Main.java
@@ -51,6 +51,9 @@ public static void main(String[] args) throws Exception {
 
     String host = System.getProperty("HOST", "localhost");
     int port = Integer.parseInt(System.getProperty("PORT", "8088"));
+    int frameSizeLimit = Integer.parseInt(System.getProperty("SIZE", "65535"));
+    boolean expectMasked = Boolean.parseBoolean(System.getProperty("MASKED", "false"));
+    boolean maskMismatch = !Boolean.parseBoolean(System.getProperty("STRICT", "false"));
 
     boolean isOpensslAvailable = OpenSsl.isAvailable();
     boolean isEpollAvailable = Transport.isEpollAvailable();
@@ -71,7 +74,8 @@ public static void main(String[] args) throws Exception {
         bootstrap
             .group(transport.eventLoopGroup())
             .channel(transport.serverChannel())
-            .childHandler(new ConnectionAcceptor(sslContext))
+            .childHandler(
+                new ConnectionAcceptor(sslContext, frameSizeLimit, expectMasked, maskMismatch))
             .bind(host, port)
             .sync()
             .channel();
@@ -83,13 +87,17 @@ private static class ConnectionAcceptor extends ChannelInitializer<SocketChannel
     private final SslContext sslContext;
     private final WebSocketDecoderConfig webSocketDecoderConfig;
 
-    ConnectionAcceptor(SslContext sslContext) {
+    ConnectionAcceptor(
+        SslContext sslContext,
+        int frameSizeLimit,
+        boolean expectMasked,
+        boolean allowMaskMismatch) {
       this.sslContext = sslContext;
       this.webSocketDecoderConfig =
           WebSocketDecoderConfig.newBuilder()
-              .allowMaskMismatch(true)
-              .expectMaskedFrames(false)
-              .maxFramePayloadLength(65_535)
+              .allowMaskMismatch(allowMaskMismatch)
+              .expectMaskedFrames(expectMasked)
+              .maxFramePayloadLength(frameSizeLimit)
               .withUTF8Validator(false)
               .build();
     }
diff --git a/netty-websocket-http1-test/gradle.lockfile b/netty-websocket-http1-test/gradle.lockfile
index de991d9..e4452cb 100644
--- a/netty-websocket-http1-test/gradle.lockfile
+++ b/netty-websocket-http1-test/gradle.lockfile
@@ -9,26 +9,26 @@ com.google.errorprone:javac-shaded:9+181-r4173-1=googleJavaFormat1.6
 com.google.googlejavaformat:google-java-format:1.6=googleJavaFormat1.6
 com.google.guava:guava:22.0=googleJavaFormat1.6
 com.google.j2objc:j2objc-annotations:1.1=googleJavaFormat1.6
-io.netty:netty-buffer:4.1.86.Final=compileClasspath,testCompileClasspath,testRuntimeClasspath
-io.netty:netty-codec-http:4.1.86.Final=compileClasspath,testCompileClasspath,testRuntimeClasspath
-io.netty:netty-codec:4.1.86.Final=compileClasspath,testCompileClasspath,testRuntimeClasspath
-io.netty:netty-common:4.1.86.Final=compileClasspath,testCompileClasspath,testRuntimeClasspath
-io.netty:netty-handler:4.1.86.Final=compileClasspath,testCompileClasspath,testRuntimeClasspath
-io.netty:netty-resolver:4.1.86.Final=compileClasspath,testCompileClasspath,testRuntimeClasspath
-io.netty:netty-transport-classes-epoll:4.1.86.Final=compileClasspath,testCompileClasspath,testRuntimeClasspath
-io.netty:netty-transport-classes-kqueue:4.1.86.Final=compileClasspath,testCompileClasspath,testRuntimeClasspath
-io.netty:netty-transport-native-unix-common:4.1.86.Final=compileClasspath,testCompileClasspath,testRuntimeClasspath
-io.netty:netty-transport:4.1.86.Final=compileClasspath,testCompileClasspath,testRuntimeClasspath
-net.bytebuddy:byte-buddy:1.12.10=testCompileClasspath,testRuntimeClasspath
+io.netty:netty-buffer:4.1.87.Final=compileClasspath,testCompileClasspath,testRuntimeClasspath
+io.netty:netty-codec-http:4.1.87.Final=compileClasspath,testCompileClasspath,testRuntimeClasspath
+io.netty:netty-codec:4.1.87.Final=compileClasspath,testCompileClasspath,testRuntimeClasspath
+io.netty:netty-common:4.1.87.Final=compileClasspath,testCompileClasspath,testRuntimeClasspath
+io.netty:netty-handler:4.1.87.Final=compileClasspath,testCompileClasspath,testRuntimeClasspath
+io.netty:netty-resolver:4.1.87.Final=compileClasspath,testCompileClasspath,testRuntimeClasspath
+io.netty:netty-transport-classes-epoll:4.1.87.Final=compileClasspath,testCompileClasspath,testRuntimeClasspath
+io.netty:netty-transport-classes-kqueue:4.1.87.Final=compileClasspath,testCompileClasspath,testRuntimeClasspath
+io.netty:netty-transport-native-unix-common:4.1.87.Final=compileClasspath,testCompileClasspath,testRuntimeClasspath
+io.netty:netty-transport:4.1.87.Final=compileClasspath,testCompileClasspath,testRuntimeClasspath
+net.bytebuddy:byte-buddy:1.12.21=testCompileClasspath,testRuntimeClasspath
 org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath
-org.assertj:assertj-core:3.23.1=testCompileClasspath,testRuntimeClasspath
+org.assertj:assertj-core:3.24.2=testCompileClasspath,testRuntimeClasspath
 org.codehaus.mojo:animal-sniffer-annotations:1.14=googleJavaFormat1.6
-org.junit.jupiter:junit-jupiter-api:5.9.1=testCompileClasspath,testRuntimeClasspath
-org.junit.jupiter:junit-jupiter-engine:5.9.1=testRuntimeClasspath
-org.junit.jupiter:junit-jupiter-params:5.9.1=testCompileClasspath,testRuntimeClasspath
-org.junit.platform:junit-platform-commons:1.9.1=testCompileClasspath,testRuntimeClasspath
-org.junit.platform:junit-platform-engine:1.9.1=testRuntimeClasspath
-org.junit:junit-bom:5.9.1=testCompileClasspath,testRuntimeClasspath
+org.junit.jupiter:junit-jupiter-api:5.9.2=testCompileClasspath,testRuntimeClasspath
+org.junit.jupiter:junit-jupiter-engine:5.9.2=testRuntimeClasspath
+org.junit.jupiter:junit-jupiter-params:5.9.2=testCompileClasspath,testRuntimeClasspath
+org.junit.platform:junit-platform-commons:1.9.2=testCompileClasspath,testRuntimeClasspath
+org.junit.platform:junit-platform-engine:1.9.2=testRuntimeClasspath
+org.junit:junit-bom:5.9.2=testCompileClasspath,testRuntimeClasspath
 org.opentest4j:opentest4j:1.2.0=testCompileClasspath,testRuntimeClasspath
 org.slf4j:slf4j-api:1.7.36=compileClasspath,testCompileClasspath,testRuntimeClasspath
 empty=annotationProcessor,testAnnotationProcessor
diff --git a/netty-websocket-http1-test/src/test/java/com/jauntsdn/netty/handler/codec/http/websocketx/WebSocketCodecTest.java b/netty-websocket-http1-test/src/test/java/com/jauntsdn/netty/handler/codec/http/websocketx/WebSocketCodecTest.java
index 116550d..d534e8c 100644
--- a/netty-websocket-http1-test/src/test/java/com/jauntsdn/netty/handler/codec/http/websocketx/WebSocketCodecTest.java
+++ b/netty-websocket-http1-test/src/test/java/com/jauntsdn/netty/handler/codec/http/websocketx/WebSocketCodecTest.java
@@ -21,6 +21,7 @@
 import io.netty.bootstrap.Bootstrap;
 import io.netty.bootstrap.ServerBootstrap;
 import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufAllocator;
 import io.netty.buffer.CompositeByteBuf;
 import io.netty.channel.Channel;
 import io.netty.channel.ChannelHandler;
@@ -58,6 +59,7 @@
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -77,6 +79,25 @@ void tearDown() {
     }
   }
 
+  @Timeout(300)
+  @ValueSource(booleans = {true, false})
+  @ParameterizedTest
+  void binaryFramesEncoder(boolean mask) throws Exception {
+    int maxFrameSize = DEFAULT_CODEC_MAX_FRAME_SIZE;
+    Channel s = server = nettyServer(new BinaryFramesTestServerHandler(), mask, false);
+    BinaryFramesEncoderClientHandler clientHandler =
+        new BinaryFramesEncoderClientHandler(maxFrameSize);
+    Channel client =
+        webSocketCallbacksClient(s.localAddress(), mask, true, maxFrameSize, clientHandler);
+
+    WebSocketFrameFactory.Encoder encoder = clientHandler.onHandshakeCompleted().join();
+    Assertions.assertThat(encoder).isNotNull();
+
+    CompletableFuture<Void> onComplete = clientHandler.startFramesExchange();
+    onComplete.join();
+    client.close();
+  }
+
   @Timeout(300)
   @MethodSource("maskingArgs")
   @ParameterizedTest
@@ -423,6 +444,155 @@ protected void initChannel(SocketChannel ch) {
     }
   }
 
+  static class BinaryFramesEncoderClientHandler
+      implements WebSocketCallbacksHandler, WebSocketFrameListener {
+    private final CompletableFuture<WebSocketFrameFactory.Encoder> onHandshakeComplete =
+        new CompletableFuture<>();
+    private final CompletableFuture<Void> onFrameExchangeComplete = new CompletableFuture<>();
+    private WebSocketFrameFactory.Encoder binaryFrameEncoder;
+    private final int framesCount;
+    private int receivedFrames;
+    private int sentFrames;
+    private volatile ChannelHandlerContext ctx;
+
+    BinaryFramesEncoderClientHandler(int maxFrameSize) {
+      this.framesCount = maxFrameSize;
+    }
+
+    @Override
+    public WebSocketFrameListener exchange(
+        ChannelHandlerContext ctx, WebSocketFrameFactory webSocketFrameFactory) {
+      this.binaryFrameEncoder = webSocketFrameFactory.encoder();
+      return this;
+    }
+
+    @Override
+    public void onChannelRead(
+        ChannelHandlerContext ctx, boolean finalFragment, int rsv, int opcode, ByteBuf payload) {
+      if (!finalFragment) {
+        onFrameExchangeComplete.completeExceptionally(
+            new AssertionError("received non-final frame: " + finalFragment));
+        payload.release();
+        return;
+      }
+      if (rsv != 0) {
+        onFrameExchangeComplete.completeExceptionally(
+            new AssertionError("received frame with non-zero rsv: " + rsv));
+        payload.release();
+        return;
+      }
+      if (opcode != WebSocketProtocol.OPCODE_BINARY) {
+        onFrameExchangeComplete.completeExceptionally(
+            new AssertionError("received non-binary frame: " + Long.toHexString(opcode)));
+        payload.release();
+        return;
+      }
+
+      int readableBytes = payload.readableBytes();
+
+      int expectedSize = receivedFrames;
+      if (expectedSize != readableBytes) {
+        onFrameExchangeComplete.completeExceptionally(
+            new AssertionError(
+                "received frame of unexpected size: "
+                    + expectedSize
+                    + ", actual: "
+                    + readableBytes));
+        payload.release();
+        return;
+      }
+
+      for (int i = 0; i < readableBytes; i++) {
+        byte b = payload.readByte();
+        if (b != (byte) 0xFE) {
+          onFrameExchangeComplete.completeExceptionally(
+              new AssertionError("received frame with unexpected content: " + Long.toHexString(b)));
+          payload.release();
+          return;
+        }
+      }
+      payload.release();
+      if (++receivedFrames == framesCount) {
+        onFrameExchangeComplete.complete(null);
+      }
+    }
+
+    @Override
+    public void onChannelWritabilityChanged(ChannelHandlerContext ctx) {
+      boolean writable = ctx.channel().isWritable();
+      if (sentFrames > 0 && writable) {
+        int toSend = framesCount - sentFrames;
+        if (toSend > 0) {
+          sendFrames(ctx, toSend);
+        }
+      }
+    }
+
+    @Override
+    public void onOpen(ChannelHandlerContext ctx) {
+      this.ctx = ctx;
+      onHandshakeComplete.complete(binaryFrameEncoder);
+    }
+
+    @Override
+    public void onClose(ChannelHandlerContext ctx) {
+      if (!onFrameExchangeComplete.isDone()) {
+        onFrameExchangeComplete.completeExceptionally(new ClosedChannelException());
+      }
+    }
+
+    @Override
+    public void onExceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+      if (!onFrameExchangeComplete.isDone()) {
+        onFrameExchangeComplete.completeExceptionally(cause);
+      }
+    }
+
+    CompletableFuture<WebSocketFrameFactory.Encoder> onHandshakeCompleted() {
+      return onHandshakeComplete;
+    }
+
+    CompletableFuture<Void> startFramesExchange() {
+      ChannelHandlerContext c = ctx;
+      c.executor().execute(() -> sendFrames(c, framesCount - sentFrames));
+      return onFrameExchangeComplete;
+    }
+
+    private void sendFrames(ChannelHandlerContext c, int toSend) {
+      Channel ch = c.channel();
+      WebSocketFrameFactory.Encoder frameEncoder = binaryFrameEncoder;
+      boolean pendingFlush = false;
+      ByteBufAllocator allocator = c.alloc();
+      for (int frameIdx = 0; frameIdx < toSend; frameIdx++) {
+        if (!c.channel().isOpen()) {
+          return;
+        }
+        int payloadSize = sentFrames;
+        int frameSize = frameEncoder.sizeofBinaryFrame(payloadSize);
+        ByteBuf binaryFrame = allocator.buffer(frameSize);
+        binaryFrame.writerIndex(frameSize - payloadSize);
+        for (int payloadIdx = 0; payloadIdx < payloadSize; payloadIdx++) {
+          binaryFrame.writeByte(0xFE);
+        }
+        ByteBuf maskedBinaryFrame = frameEncoder.encodeBinaryFrame(binaryFrame);
+        sentFrames++;
+        if (ch.bytesBeforeUnwritable() < binaryFrame.capacity()) {
+          c.writeAndFlush(maskedBinaryFrame, c.voidPromise());
+          pendingFlush = false;
+          if (!ch.isWritable()) {
+            return;
+          }
+        } else {
+          c.write(maskedBinaryFrame, c.voidPromise());
+          pendingFlush = true;
+        }
+      }
+      if (pendingFlush) {
+        c.flush();
+      }
+    }
+  }
+
   static class BinaryFramesTestClientHandler
       implements WebSocketCallbacksHandler, WebSocketFrameListener {
     private final CompletableFuture<WebSocketFrameFactory> onHandshakeComplete =
diff --git a/netty-websocket-http1-test/src/test/java/com/jauntsdn/netty/handler/codec/http/websocketx/WebSocketValidationTest.java b/netty-websocket-http1-test/src/test/java/com/jauntsdn/netty/handler/codec/http/websocketx/WebSocketValidationTest.java
index 29099a1..877753d 100644
--- a/netty-websocket-http1-test/src/test/java/com/jauntsdn/netty/handler/codec/http/websocketx/WebSocketValidationTest.java
+++ b/netty-websocket-http1-test/src/test/java/com/jauntsdn/netty/handler/codec/http/websocketx/WebSocketValidationTest.java
@@ -36,6 +36,8 @@
 import io.netty.util.ReferenceCountUtil;
 import java.net.InetSocketAddress;
 import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
@@ -44,6 +46,8 @@
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Timeout;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 
 public class WebSocketValidationTest {
   Channel server;
@@ -59,7 +63,7 @@ void tearDown() throws Exception {
   @Timeout(15)
   @Test
   void frameSizeLimit() throws Exception {
-    FrameSizeLimitServerHandler serverHandler = new FrameSizeLimitServerHandler();
+    ValidationTestServerHandler serverHandler = new ValidationTestServerHandler();
     Channel s = server = testServer("localhost", 0, decoderConfig(125), serverHandler);
     FrameSizeLimitClientHandler clientHandler = new FrameSizeLimitClientHandler(126);
     Channel client = testClient(s.localAddress(), 125, clientHandler);
@@ -86,20 +90,161 @@ void frameSizeLimit() throws Exception {
     }
   }
 
-  @Test
-  void controlFrameSizeLimit() {}
+  @Timeout(15)
+  @ValueSource(
+      bytes = {
+        WebSocketProtocol.OPCODE_PING,
+        WebSocketProtocol.OPCODE_PONG,
+        WebSocketProtocol.OPCODE_CLOSE
+      })
+  @ParameterizedTest
+  void controlFrameSizeLimit(byte opcode) throws Exception {
+    ValidationTestServerHandler serverHandler = new ValidationTestServerHandler();
+    Channel s = server = testServer("localhost", 0, decoderConfig(65_535), serverHandler);
+    ControlFrameSizeLimitClientHandler clientHandler =
+        new ControlFrameSizeLimitClientHandler(opcode);
+    Channel client = testClient(s.localAddress(), 65_535, clientHandler);
+    serverHandler.onClose.join();
+    clientHandler.onClose.join();
+
+    Throwable serverInboundException = serverHandler.inboundException;
+    Assertions.assertThat(serverInboundException).isNotNull();
+    Assertions.assertThat(serverInboundException)
+        .isInstanceOf(CorruptedWebSocketFrameException.class);
+    WebSocketCloseStatus closeStatus =
+        ((CorruptedWebSocketFrameException) serverInboundException).closeStatus();
+    Assertions.assertThat(closeStatus.code())
+        .isEqualTo(WebSocketCloseStatus.MESSAGE_TOO_BIG.code());
+    Assertions.assertThat(serverHandler.framesReceived).isEqualTo(0);
+    Assertions.assertThat(clientHandler.nonCloseFrames).isEqualTo(0);
+    Set<ByteBuf> closeFrames = clientHandler.closeFrames;
+    try {
+      Assertions.assertThat(closeFrames.size()).isEqualTo(1);
+      ByteBuf closeFramePayload = closeFrames.iterator().next();
+      Assertions.assertThat(WebSocketFrameListener.CloseFramePayload.statusCode(closeFramePayload))
+          .isEqualTo(WebSocketCloseStatus.MESSAGE_TOO_BIG.code());
+    } finally {
+      closeFrames.forEach(ByteBuf::release);
+    }
+  }
 
+  @Timeout(15)
   @Test
-  void frameWithExtensions() {}
+  void frameWithExtensions() throws Exception {
+    ValidationTestServerHandler serverHandler = new ValidationTestServerHandler();
+    Channel s = server = testServer("localhost", 0, decoderConfig(65_535), serverHandler);
+    ExtensionFrameClientHandler clientHandler = new ExtensionFrameClientHandler();
+    Channel client = testClient(s.localAddress(), 65_535, clientHandler);
+    serverHandler.onClose.join();
+    clientHandler.onClose.join();
 
+    Throwable serverInboundException = serverHandler.inboundException;
+    Assertions.assertThat(serverInboundException).isNotNull();
+    Assertions.assertThat(serverInboundException)
+        .isInstanceOf(CorruptedWebSocketFrameException.class);
+    WebSocketCloseStatus closeStatus =
+        ((CorruptedWebSocketFrameException) serverInboundException).closeStatus();
+    Assertions.assertThat(closeStatus.code()).isEqualTo(WebSocketCloseStatus.PROTOCOL_ERROR.code());
+    Assertions.assertThat(serverHandler.framesReceived).isEqualTo(0);
+    Assertions.assertThat(clientHandler.nonCloseFrames).isEqualTo(0);
+    Set<ByteBuf> closeFrames = clientHandler.closeFrames;
+    try {
+      Assertions.assertThat(closeFrames.size()).isEqualTo(1);
+      ByteBuf closeFramePayload = closeFrames.iterator().next();
+      Assertions.assertThat(WebSocketFrameListener.CloseFramePayload.statusCode(closeFramePayload))
+          .isEqualTo(WebSocketCloseStatus.PROTOCOL_ERROR.code());
+    } finally {
+      closeFrames.forEach(ByteBuf::release);
+    }
+  }
+
+  @Timeout(15)
   @Test
-  void invalidFragmentStart() {}
+  void invalidFragmentStart() throws Exception {
+    ValidationTestServerHandler serverHandler = new ValidationTestServerHandler();
+    Channel s = server = testServer("localhost", 0, decoderConfig(65_535), serverHandler);
+    FragmentStartClientHandler clientHandler = new FragmentStartClientHandler();
+    Channel client = testClient(s.localAddress(), 65_535, clientHandler);
+    serverHandler.onClose.join();
+    clientHandler.onClose.join();
+
+    Throwable serverInboundException = serverHandler.inboundException;
+    Assertions.assertThat(serverInboundException).isNotNull();
+    Assertions.assertThat(serverInboundException)
+        .isInstanceOf(CorruptedWebSocketFrameException.class);
+    WebSocketCloseStatus closeStatus =
+        ((CorruptedWebSocketFrameException) serverInboundException).closeStatus();
+    Assertions.assertThat(closeStatus.code()).isEqualTo(WebSocketCloseStatus.PROTOCOL_ERROR.code());
+    Assertions.assertThat(serverHandler.framesReceived).isEqualTo(0);
+    Assertions.assertThat(clientHandler.nonCloseFrames).isEqualTo(0);
+    Set<ByteBuf> closeFrames = clientHandler.closeFrames;
+    try {
+      Assertions.assertThat(closeFrames.size()).isEqualTo(1);
+      ByteBuf closeFramePayload = closeFrames.iterator().next();
+      Assertions.assertThat(WebSocketFrameListener.CloseFramePayload.statusCode(closeFramePayload))
+          .isEqualTo(WebSocketCloseStatus.PROTOCOL_ERROR.code());
+    } finally {
+      closeFrames.forEach(ByteBuf::release);
+    }
+  }
 
   @Test
-  void invalidFragmentContinuation() {}
+  void invalidFragmentContinuation() throws Exception {
+    ValidationTestServerHandler serverHandler = new ValidationTestServerHandler();
+    Channel s = server = testServer("localhost", 0, decoderConfig(65_535), serverHandler);
+    FragmentContinuationClientHandler clientHandler = new FragmentContinuationClientHandler();
+    Channel client = testClient(s.localAddress(), 65_535, clientHandler);
+    serverHandler.onClose.join();
+    clientHandler.onClose.join();
+
+    Throwable serverInboundException = serverHandler.inboundException;
+    Assertions.assertThat(serverInboundException).isNotNull();
+    Assertions.assertThat(serverInboundException)
+        .isInstanceOf(CorruptedWebSocketFrameException.class);
+    WebSocketCloseStatus closeStatus =
+        ((CorruptedWebSocketFrameException) serverInboundException).closeStatus();
+    Assertions.assertThat(closeStatus.code()).isEqualTo(WebSocketCloseStatus.PROTOCOL_ERROR.code());
+    Assertions.assertThat(serverHandler.framesReceived).isEqualTo(1);
+    Assertions.assertThat(clientHandler.nonCloseFrames).isEqualTo(0);
+    Set<ByteBuf> closeFrames = clientHandler.closeFrames;
+    try {
+      Assertions.assertThat(closeFrames.size()).isEqualTo(1);
+      ByteBuf closeFramePayload = closeFrames.iterator().next();
+      Assertions.assertThat(WebSocketFrameListener.CloseFramePayload.statusCode(closeFramePayload))
+          .isEqualTo(WebSocketCloseStatus.PROTOCOL_ERROR.code());
+    } finally {
+      closeFrames.forEach(ByteBuf::release);
+    }
+  }
 
   @Test
-  void invalidFragmentCompletion() {}
+  void invalidFragmentCompletion() throws Exception {
+    ValidationTestServerHandler serverHandler = new ValidationTestServerHandler();
+    Channel s = server = testServer("localhost", 0, decoderConfig(65_535), serverHandler);
+    FragmentCompletionClientHandler clientHandler = new FragmentCompletionClientHandler();
+    Channel client = testClient(s.localAddress(), 65_535, clientHandler);
+    serverHandler.onClose.join();
+    clientHandler.onClose.join();
+
+    Throwable serverInboundException = serverHandler.inboundException;
+    Assertions.assertThat(serverInboundException).isNotNull();
+    Assertions.assertThat(serverInboundException)
+        .isInstanceOf(CorruptedWebSocketFrameException.class);
+    WebSocketCloseStatus closeStatus =
+        ((CorruptedWebSocketFrameException) serverInboundException).closeStatus();
+    Assertions.assertThat(closeStatus.code()).isEqualTo(WebSocketCloseStatus.PROTOCOL_ERROR.code());
+    Assertions.assertThat(serverHandler.framesReceived).isEqualTo(1);
+    Assertions.assertThat(clientHandler.nonCloseFrames).isEqualTo(0);
+    Set<ByteBuf> closeFrames = clientHandler.closeFrames;
+    try {
+      Assertions.assertThat(closeFrames.size()).isEqualTo(1);
+      ByteBuf closeFramePayload = closeFrames.iterator().next();
+      Assertions.assertThat(WebSocketFrameListener.CloseFramePayload.statusCode(closeFramePayload))
+          .isEqualTo(WebSocketCloseStatus.PROTOCOL_ERROR.code());
+    } finally {
+      closeFrames.forEach(ByteBuf::release);
+    }
+  }
 
   static WebSocketDecoderConfig decoderConfig(int maxFramePayloadLength) {
     return WebSocketDecoderConfig.newBuilder()
@@ -148,6 +293,249 @@ protected void initChannel(SocketChannel ch) {
         .channel();
   }
 
+  static class ExtensionFrameClientHandler
+      implements WebSocketCallbacksHandler, WebSocketFrameListener {
+    public static final int PAYLOAD_SIZE = 42;
+    final CompletableFuture<Void> onClose = new CompletableFuture<>();
+    final Set<ByteBuf> closeFrames = ConcurrentHashMap.newKeySet();
+    volatile int nonCloseFrames;
+    WebSocketFrameFactory webSocketFrameFactory;
+
+    @Override
+    public WebSocketFrameListener exchange(
+        ChannelHandlerContext ctx, WebSocketFrameFactory webSocketFrameFactory) {
+      this.webSocketFrameFactory = webSocketFrameFactory;
+      return this;
+    }
+
+    @Override
+    public void onOpen(ChannelHandlerContext ctx) {
+      WebSocketFrameFactory factory = webSocketFrameFactory;
+      ByteBuf controlFrame = factory.createBinaryFrame(ctx.alloc(), PAYLOAD_SIZE);
+      controlFrame.setByte(0, controlFrame.getByte(0) | 0b0100_0000);
+      byte[] payloadBytes = new byte[PAYLOAD_SIZE];
+      ThreadLocalRandom.current().nextBytes(payloadBytes);
+      controlFrame.writeBytes(payloadBytes);
+      ctx.writeAndFlush(factory.mask(controlFrame));
+    }
+
+    @Override
+    public void onChannelRead(
+        ChannelHandlerContext ctx, boolean finalFragment, int rsv, int opcode, ByteBuf payload) {
+      if (opcode == WebSocketProtocol.OPCODE_CLOSE) {
+        closeFrames.add(payload);
+        return;
+      }
+      //noinspection NonAtomicOperationOnVolatileField: written from single thread
+      nonCloseFrames++;
+      ReferenceCountUtil.release(payload);
+    }
+
+    @Override
+    public void onClose(ChannelHandlerContext ctx) {
+      onClose.complete(null);
+    }
+  }
+
+  static class FragmentStartClientHandler
+      implements WebSocketCallbacksHandler, WebSocketFrameListener {
+    public static final int PAYLOAD_SIZE = 127;
+    final CompletableFuture<Void> onClose = new CompletableFuture<>();
+    final Set<ByteBuf> closeFrames = ConcurrentHashMap.newKeySet();
+    volatile int nonCloseFrames;
+    WebSocketFrameFactory webSocketFrameFactory;
+
+    @Override
+    public WebSocketFrameListener exchange(
+        ChannelHandlerContext ctx, WebSocketFrameFactory webSocketFrameFactory) {
+      this.webSocketFrameFactory = webSocketFrameFactory;
+      return this;
+    }
+
+    @Override
+    public void onOpen(ChannelHandlerContext ctx) {
+      WebSocketFrameFactory factory = webSocketFrameFactory;
+      ByteBuf fragmentFrame = factory.createBinaryFrame(ctx.alloc(), PAYLOAD_SIZE);
+      fragmentFrame.setByte(0, 0);
+      byte[] payloadBytes = new byte[PAYLOAD_SIZE];
+      ThreadLocalRandom.current().nextBytes(payloadBytes);
+      fragmentFrame.writeBytes(payloadBytes);
+      ctx.writeAndFlush(factory.mask(fragmentFrame));
+    }
+
+    @Override
+    public void onChannelRead(
+        ChannelHandlerContext ctx, boolean finalFragment, int rsv, int opcode, ByteBuf payload) {
+      if (opcode == WebSocketProtocol.OPCODE_CLOSE) {
+        closeFrames.add(payload);
+        return;
+      }
+      //noinspection NonAtomicOperationOnVolatileField: written from single thread
+      nonCloseFrames++;
+      ReferenceCountUtil.release(payload);
+    }
+
+    @Override
+    public void onClose(ChannelHandlerContext ctx) {
+      onClose.complete(null);
+    }
+  }
+
+  static class FragmentContinuationClientHandler
+      implements WebSocketCallbacksHandler, WebSocketFrameListener {
+    public static final int PAYLOAD_SIZE = 127;
+    final CompletableFuture<Void> onClose = new CompletableFuture<>();
+    final Set<ByteBuf> closeFrames = ConcurrentHashMap.newKeySet();
+    volatile int nonCloseFrames;
+    WebSocketFrameFactory webSocketFrameFactory;
+
+    @Override
+    public WebSocketFrameListener exchange(
+        ChannelHandlerContext ctx, WebSocketFrameFactory webSocketFrameFactory) {
+      this.webSocketFrameFactory = webSocketFrameFactory;
+      return this;
+    }
+
+    @Override
+    public void onOpen(ChannelHandlerContext ctx) {
+      byte[] payloadBytes = new byte[PAYLOAD_SIZE];
+      ThreadLocalRandom.current().nextBytes(payloadBytes);
+      WebSocketFrameFactory factory = webSocketFrameFactory;
+
+      ByteBuf fragmentStartFrame = factory.createBinaryFrame(ctx.alloc(), PAYLOAD_SIZE);
+      fragmentStartFrame.setByte(0, fragmentStartFrame.getByte(0) & 0b0111_1111);
+      fragmentStartFrame.writeBytes(payloadBytes);
+      ctx.write(factory.mask(fragmentStartFrame));
+
+      ByteBuf fragmentContFrame = factory.createBinaryFrame(ctx.alloc(), PAYLOAD_SIZE);
+      fragmentContFrame.setByte(0, fragmentContFrame.getByte(0) & 0b0111_1111);
+      fragmentContFrame.writeBytes(payloadBytes);
+      ctx.writeAndFlush(factory.mask(fragmentContFrame));
+    }
+
+    @Override
+    public void onChannelRead(
+        ChannelHandlerContext ctx, boolean finalFragment, int rsv, int opcode, ByteBuf payload) {
+      if (opcode == WebSocketProtocol.OPCODE_CLOSE) {
+        closeFrames.add(payload);
+        return;
+      }
+      //noinspection NonAtomicOperationOnVolatileField: written from single thread
+      nonCloseFrames++;
+      ReferenceCountUtil.release(payload);
+    }
+
+    @Override
+    public void onClose(ChannelHandlerContext ctx) {
+      onClose.complete(null);
+    }
+  }
+
+  static class FragmentCompletionClientHandler
+      implements WebSocketCallbacksHandler, WebSocketFrameListener {
+    public static final int PAYLOAD_SIZE = 127;
+    final CompletableFuture<Void> onClose = new CompletableFuture<>();
+    final Set<ByteBuf> closeFrames = ConcurrentHashMap.newKeySet();
+    volatile int nonCloseFrames;
+    WebSocketFrameFactory webSocketFrameFactory;
+
+    @Override
+    public WebSocketFrameListener exchange(
+        ChannelHandlerContext ctx, WebSocketFrameFactory webSocketFrameFactory) {
+      this.webSocketFrameFactory = webSocketFrameFactory;
+      return this;
+    }
+
+    @Override
+    public void onOpen(ChannelHandlerContext ctx) {
+      byte[] payloadBytes = new byte[PAYLOAD_SIZE];
+      ThreadLocalRandom.current().nextBytes(payloadBytes);
+      WebSocketFrameFactory factory = webSocketFrameFactory;
+
+      ByteBuf fragmentStartFrame = factory.createBinaryFrame(ctx.alloc(), PAYLOAD_SIZE);
+      fragmentStartFrame.setByte(0, fragmentStartFrame.getByte(0) & 0b0111_1111);
+      fragmentStartFrame.writeBytes(payloadBytes);
+      ctx.write(factory.mask(fragmentStartFrame));
+
+      ByteBuf fragmentContFrame = factory.createBinaryFrame(ctx.alloc(), PAYLOAD_SIZE);
+      fragmentContFrame.writeBytes(payloadBytes);
+      ctx.writeAndFlush(factory.mask(fragmentContFrame));
+    }
+
+    @Override
+    public void onChannelRead(
+        ChannelHandlerContext ctx, boolean finalFragment, int rsv, int opcode, ByteBuf payload) {
+      if (opcode == WebSocketProtocol.OPCODE_CLOSE) {
+        closeFrames.add(payload);
+        return;
+      }
+      //noinspection NonAtomicOperationOnVolatileField: written from single thread
+      nonCloseFrames++;
+      ReferenceCountUtil.release(payload);
+    }
+
+    @Override
+    public void onClose(ChannelHandlerContext ctx) {
+      onClose.complete(null);
+    }
+  }
+
+  static class ControlFrameSizeLimitClientHandler
+      implements WebSocketCallbacksHandler, WebSocketFrameListener {
+    public static final int PAYLOAD_SIZE = 127;
+    final CompletableFuture<Void> onClose = new CompletableFuture<>();
+    final Set<ByteBuf> closeFrames = ConcurrentHashMap.newKeySet();
+    final byte opcode;
+    volatile int nonCloseFrames;
+    WebSocketFrameFactory webSocketFrameFactory;
+
+    ControlFrameSizeLimitClientHandler(byte opcode) {
+      this.opcode = opcode;
+    }
+
+    @Override
+    public WebSocketFrameListener exchange(
+        ChannelHandlerContext ctx, WebSocketFrameFactory webSocketFrameFactory) {
+      this.webSocketFrameFactory = webSocketFrameFactory;
+      return this;
+    }
+
+    @Override
+    public void onOpen(ChannelHandlerContext ctx) {
+      WebSocketFrameFactory factory = webSocketFrameFactory;
+      ByteBuf controlFrame = factory.createBinaryFrame(ctx.alloc(), PAYLOAD_SIZE);
+      controlFrame.setByte(0, controlFrame.getByte(0) & 0xF0 | opcode);
+      if (opcode == WebSocketProtocol.OPCODE_CLOSE) {
+        String closeMsg = String.join("", Collections.nCopies(25, "close"));
+        controlFrame
+            .writeShort(WebSocketCloseStatus.NORMAL_CLOSURE.code())
+            .writeCharSequence(closeMsg, StandardCharsets.UTF_8);
+      } else {
+        byte[] payloadBytes = new byte[PAYLOAD_SIZE];
+        ThreadLocalRandom.current().nextBytes(payloadBytes);
+        controlFrame.writeBytes(payloadBytes);
+      }
+      ctx.writeAndFlush(factory.mask(controlFrame));
+    }
+
+    @Override
+    public void onChannelRead(
+        ChannelHandlerContext ctx, boolean finalFragment, int rsv, int opcode, ByteBuf payload) {
+      if (opcode == WebSocketProtocol.OPCODE_CLOSE) {
+        closeFrames.add(payload);
+        return;
+      }
+      //noinspection NonAtomicOperationOnVolatileField: written from single thread
+      nonCloseFrames++;
+      ReferenceCountUtil.release(payload);
+    }
+
+    @Override
+    public void onClose(ChannelHandlerContext ctx) {
+      onClose.complete(null);
+    }
+  }
+
   static class FrameSizeLimitClientHandler
       implements WebSocketCallbacksHandler, WebSocketFrameListener {
     final int payloadSize;
@@ -230,7 +618,7 @@ protected void initChannel(SocketChannel ch) {
         .channel();
   }
 
-  static class FrameSizeLimitServerHandler
+  static class ValidationTestServerHandler
       implements WebSocketFrameListener, WebSocketCallbacksHandler {
     final CompletableFuture<Void> onClose = new CompletableFuture<>();
 
@@ -246,6 +634,8 @@ public WebSocketFrameListener exchange(
     @Override
     public void onChannelRead(
         ChannelHandlerContext ctx, boolean finalFragment, int rsv, int opcode, ByteBuf payload) {
+      //noinspection NonAtomicOperationOnVolatileField written from single thread
+      framesReceived++;
       payload.release();
     }
 
diff --git a/netty-websocket-http1/gradle.lockfile b/netty-websocket-http1/gradle.lockfile
index b9d328d..0cf3816 100644
--- a/netty-websocket-http1/gradle.lockfile
+++ b/netty-websocket-http1/gradle.lockfile
@@ -7,13 +7,13 @@ com.google.errorprone:javac-shaded:9+181-r4173-1=googleJavaFormat1.6
 com.google.googlejavaformat:google-java-format:1.6=googleJavaFormat1.6
 com.google.guava:guava:22.0=googleJavaFormat1.6
 com.google.j2objc:j2objc-annotations:1.1=googleJavaFormat1.6
-io.netty:netty-buffer:4.1.86.Final=compileClasspath
-io.netty:netty-codec-http:4.1.86.Final=compileClasspath
-io.netty:netty-codec:4.1.86.Final=compileClasspath
-io.netty:netty-common:4.1.86.Final=compileClasspath
-io.netty:netty-handler:4.1.86.Final=compileClasspath
-io.netty:netty-resolver:4.1.86.Final=compileClasspath
-io.netty:netty-transport-native-unix-common:4.1.86.Final=compileClasspath
-io.netty:netty-transport:4.1.86.Final=compileClasspath
+io.netty:netty-buffer:4.1.87.Final=compileClasspath
+io.netty:netty-codec-http:4.1.87.Final=compileClasspath
+io.netty:netty-codec:4.1.87.Final=compileClasspath
+io.netty:netty-common:4.1.87.Final=compileClasspath
+io.netty:netty-handler:4.1.87.Final=compileClasspath
+io.netty:netty-resolver:4.1.87.Final=compileClasspath
+io.netty:netty-transport-native-unix-common:4.1.87.Final=compileClasspath
+io.netty:netty-transport:4.1.87.Final=compileClasspath
 org.codehaus.mojo:animal-sniffer-annotations:1.14=googleJavaFormat1.6
 empty=annotationProcessor
diff --git a/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/DefaultWebSocketDecoder.java b/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/DefaultWebSocketDecoder.java
index 29c5ba0..73212b1 100644
--- a/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/DefaultWebSocketDecoder.java
+++ b/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/DefaultWebSocketDecoder.java
@@ -22,6 +22,9 @@
 import io.netty.handler.codec.http.websocketx.WebSocketCloseStatus;
 
 final class DefaultWebSocketDecoder extends WebSocketDecoder {
+  WebSocketFrameFactory frameFactory;
+
+  final int maxFramePayloadLength;
   int state = STATE_NON_PARTIAL;
   ByteBuf partialPrefix;
   int partialMask;
@@ -38,7 +41,7 @@ final class DefaultWebSocketDecoder extends WebSocketDecoder {
   int fragmentedTotalLength = WebSocketProtocol.VALIDATION_RESULT_NON_FRAGMENTING;
 
   DefaultWebSocketDecoder(int maxFramePayloadLength) {
-    super(maxFramePayloadLength);
+    this.maxFramePayloadLength = maxFramePayloadLength;
   }
 
   @Override
@@ -57,6 +60,20 @@ public void channelInactive(ChannelHandlerContext ctx) throws Exception {
     super.channelInactive(ctx);
   }
 
+  @Override
+  public void frameListener(
+      ChannelHandlerContext ctx,
+      WebSocketFrameListener webSocketFrameListener,
+      WebSocketFrameFactory frameFactory) {
+    super.frameListener(ctx, webSocketFrameListener, frameFactory);
+    this.frameFactory = frameFactory;
+  }
+
+  @Override
+  WebSocketFrameFactory frameFactory() {
+    return frameFactory;
+  }
+
   @Override
   void decode(ChannelHandlerContext ctx, ByteBuf in) {
     int st = state;
diff --git a/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/MaskingWebSocketEncoder.java b/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/MaskingWebSocketEncoder.java
index 4dd328c..ed90dbf 100644
--- a/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/MaskingWebSocketEncoder.java
+++ b/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/MaskingWebSocketEncoder.java
@@ -48,7 +48,7 @@ public WebSocketFrameFactory frameFactory(ChannelHandlerContext ctx) {
     return FrameFactory.INSTANCE;
   }
 
-  static class FrameFactory implements WebSocketFrameFactory {
+  static class FrameFactory implements WebSocketFrameFactory, WebSocketFrameFactory.Encoder {
     static final int PREFIX_SIZE_SMALL = 6;
     static final int BINARY_FRAME_SMALL =
         OPCODE_BINARY << 8 | /*FIN*/ (byte) 1 << 15 | /*MASK*/ (byte) 1 << 7;
@@ -136,9 +136,51 @@ public ByteBuf createPongFrame(ByteBufAllocator allocator, int payloadSize) {
     public ByteBuf mask(ByteBuf frame) {
       int maskIndex = frame.readerIndex();
       int mask = frame.getInt(maskIndex);
-      int cur = maskIndex + /*mask size*/ 4;
-      int end = frame.writerIndex();
+      mask(mask, frame, maskIndex + /*mask size*/ 4, frame.writerIndex());
+      return frame.readerIndex(0);
+    }
+
+    @Override
+    public Encoder encoder() {
+      return this;
+    }
+
+    @Override
+    public ByteBuf encodeBinaryFrame(ByteBuf binaryFrame) {
+      int frameSize = binaryFrame.readableBytes();
+      int smallPrefixSize = 6;
+      if (frameSize <= 125 + smallPrefixSize) {
+        int payloadSize = frameSize - smallPrefixSize;
+        binaryFrame.setShort(0, BINARY_FRAME_SMALL | payloadSize);
+        int mask = mask();
+        binaryFrame.setInt(2, mask);
+        return mask(mask, binaryFrame, smallPrefixSize, binaryFrame.writerIndex());
+      }
+
+      int mediumPrefixSize = 8;
+      if (frameSize <= 65_535 + mediumPrefixSize) {
+        int payloadSize = frameSize - mediumPrefixSize;
+        int mask = mask();
+        binaryFrame.setLong(0, ((BINARY_FRAME_MEDIUM | (long) payloadSize) << 32) | mask);
+        return mask(mask, binaryFrame, mediumPrefixSize, binaryFrame.writerIndex());
+      }
+      int payloadSize = frameSize - 12;
+      throw new IllegalArgumentException(payloadSizeLimit(payloadSize, 65_535));
+    }
 
+    @Override
+    public int sizeofBinaryFrame(int payloadSize) {
+      if (payloadSize <= 125) {
+        return payloadSize + 6;
+      }
+      if (payloadSize < 65_535) {
+        return payloadSize + 8;
+      }
+      throw new IllegalArgumentException(payloadSizeLimit(payloadSize, 65_535));
+    }
+
+    static ByteBuf mask(int mask, ByteBuf frame, int start, int end) {
+      int cur = start;
       if (end - cur >= 8) {
         long longMask = (long) mask & 0xFFFFFFFFL;
         longMask |= longMask << 32;
@@ -155,12 +197,7 @@ public ByteBuf mask(ByteBuf frame) {
         byte bytePayload = frame.getByte(cur);
         frame.setByte(cur, bytePayload ^ byteAtIndex(mask, maskOffset++ & 3));
       }
-      return frame.readerIndex(0);
-    }
-
-    @Override
-    public Encoder encoder() {
-      throw new UnsupportedOperationException("not implemented");
+      return frame;
     }
 
     static int byteAtIndex(int mask, int index) {
diff --git a/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/NonMaskingWebSocketEncoder.java b/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/NonMaskingWebSocketEncoder.java
index 956b059..13d04d2 100644
--- a/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/NonMaskingWebSocketEncoder.java
+++ b/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/NonMaskingWebSocketEncoder.java
@@ -32,7 +32,7 @@
 final class NonMaskingWebSocketEncoder extends ChannelOutboundHandlerAdapter
     implements WebSocketCallbacksFrameEncoder {
 
-  static NonMaskingWebSocketEncoder INSTANCE = new NonMaskingWebSocketEncoder();
+  static final NonMaskingWebSocketEncoder INSTANCE = new NonMaskingWebSocketEncoder();
 
   private NonMaskingWebSocketEncoder() {}
 
@@ -129,13 +129,15 @@ public Encoder encoder() {
     @Override
     public ByteBuf encodeBinaryFrame(ByteBuf binaryFrame) {
       int frameSize = binaryFrame.readableBytes();
-      if (frameSize <= 127) {
-        int payloadSize = frameSize - 2;
+      int smallPrefixSize = 2;
+      if (frameSize <= 125 + smallPrefixSize) {
+        int payloadSize = frameSize - smallPrefixSize;
         return binaryFrame.setShort(0, BINARY_FRAME_SMALL | payloadSize);
       }
 
-      if (frameSize <= 65_539) {
-        int payloadSize = frameSize - 4;
+      int mediumPrefixSize = 4;
+      if (frameSize <= 65_535 + mediumPrefixSize) {
+        int payloadSize = frameSize - mediumPrefixSize;
         return binaryFrame.setInt(0, BINARY_FRAME_MEDIUM | payloadSize);
       }
       int payloadSize = frameSize - 8;
@@ -150,7 +152,7 @@ public int sizeofBinaryFrame(int payloadSize) {
       if (payloadSize < 65_535) {
         return payloadSize + 4;
       }
-      throw new IllegalArgumentException(payloadSizeLimit(payloadSize, 125));
+      throw new IllegalArgumentException(payloadSizeLimit(payloadSize, 65_535));
     }
 
     static String payloadSizeLimit(int payloadSize, int limit) {
diff --git a/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/SmallWebSocketDecoder.java b/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/SmallWebSocketDecoder.java
index 4893d13..f86888b 100644
--- a/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/SmallWebSocketDecoder.java
+++ b/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/SmallWebSocketDecoder.java
@@ -20,20 +20,13 @@
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.handler.codec.http.websocketx.WebSocketCloseStatus;
 
-final class SmallWebSocketDecoder extends WebSocketDecoder {
-  int state = STATE_NON_PARTIAL;
-  int partialPrefix;
+abstract class SmallWebSocketDecoder extends WebSocketDecoder {
+  static final int MAX_FRAME_PAYLOAD_LENGTH = 125;
+  /* [8bit frag total length][8bit partial prefix][1bit unused][1bit fin][4bit opcode][2bit state] */
+  int encodedState = encodeFragmentedLength(0, WebSocketProtocol.VALIDATION_RESULT_NON_FRAGMENTING);
   ByteBuf partialPayload;
-  int partialRemaining;
-  int opcode;
-  boolean fin;
 
-  /* non-negative value means fragmentation is in progress*/
-  int fragmentedTotalLength = WebSocketProtocol.VALIDATION_RESULT_NON_FRAGMENTING;
-
-  SmallWebSocketDecoder(int maxFramePayloadLength) {
-    super(maxFramePayloadLength);
-  }
+  SmallWebSocketDecoder() {}
 
   @Override
   public void channelInactive(ChannelHandlerContext ctx) throws Exception {
@@ -46,14 +39,15 @@ public void channelInactive(ChannelHandlerContext ctx) throws Exception {
 
   @Override
   void decode(ChannelHandlerContext ctx, ByteBuf in) {
-    int st = state;
+    int encodedSt = encodedState;
+    int st = decodeState(encodedSt);
     int readableBytes = in.readableBytes();
     while (readableBytes > 0) {
       switch (st) {
         case STATE_NON_PARTIAL:
           if (readableBytes == 1) {
             st = STATE_PARTIAL_PREFIX;
-            partialPrefix = (in.readByte() << 8) & 0xFFFF;
+            encodedSt = encodePartialPrefix(encodedSt, in.readByte());
             readableBytes = 0;
           } else {
             short prefix = in.readShort();
@@ -73,25 +67,28 @@ void decode(ChannelHandlerContext ctx, ByteBuf in) {
                   "frames masking is not supported");
               return;
             }
-
             boolean finFlag = prefix < 0;
             int result =
                 WebSocketProtocol.validate(
-                    ctx, this, flags, code, length, fragmentedTotalLength, maxFramePayloadLength);
+                    ctx,
+                    this,
+                    flags,
+                    code,
+                    length,
+                    decodeFragmentedLength(encodedSt),
+                    MAX_FRAME_PAYLOAD_LENGTH);
             if (result == WebSocketProtocol.VALIDATION_RESULT_INVALID) {
               return;
             }
-            fragmentedTotalLength = result;
+            encodedSt = encodeFragmentedLength(encodedSt, result);
 
             if (readableBytes >= length) {
               ByteBuf payload = in.readRetainedSlice(length);
               readableBytes -= length;
               onFrameRead(ctx, finFlag, code, payload);
             } else {
-              opcode = code;
-              fin = finFlag;
+              encodedSt = encodeFlags(encodedSt, code, finFlag);
               partialPayload = partialPayload(ctx, in, length);
-              partialRemaining = length - readableBytes;
               readableBytes = 0;
               st = STATE_PARTIAL_PAYLOAD;
             }
@@ -99,8 +96,7 @@ void decode(ChannelHandlerContext ctx, ByteBuf in) {
           break;
 
         case STATE_PARTIAL_PREFIX:
-          int prefix = partialPrefix;
-          prefix |= in.readByte();
+          int prefix = decodePartialPrefix(encodedSt) | in.readByte();
           readableBytes -= 1;
 
           int flagsAndOpcode = prefix >> 8;
@@ -115,14 +111,16 @@ void decode(ChannelHandlerContext ctx, ByteBuf in) {
                 ctx, this, WebSocketCloseStatus.NORMAL_CLOSURE, "frames masking is not supported");
             return;
           }
+          int fragmentedTotalLength = decodeFragmentedLength(encodedSt);
 
           int result =
               WebSocketProtocol.validate(
-                  ctx, this, flags, code, length, fragmentedTotalLength, maxFramePayloadLength);
+                  ctx, this, flags, code, length, fragmentedTotalLength, MAX_FRAME_PAYLOAD_LENGTH);
           if (result == WebSocketProtocol.VALIDATION_RESULT_INVALID) {
             return;
           }
           fragmentedTotalLength = result;
+          encodedSt = encodeFragmentedLength(encodedSt, fragmentedTotalLength);
 
           if (readableBytes >= length) {
             ByteBuf payload = in.readRetainedSlice(length);
@@ -130,28 +128,27 @@ void decode(ChannelHandlerContext ctx, ByteBuf in) {
             st = STATE_NON_PARTIAL;
             onFrameRead(ctx, finFlag, code, payload);
           } else {
-            opcode = code;
-            fin = finFlag;
+            encodedSt = encodeFlags(encodedSt, code, finFlag);
             partialPayload = partialPayload(ctx, in, length);
-            partialRemaining = length - readableBytes;
             readableBytes = 0;
             st = STATE_PARTIAL_PAYLOAD;
           }
           break;
 
         case STATE_PARTIAL_PAYLOAD:
-          int remaining = partialRemaining;
-          int toRead = Math.min(readableBytes, remaining);
           ByteBuf partial = partialPayload;
+          int remaining = partial.capacity() - partial.writerIndex();
+          int toRead = Math.min(readableBytes, remaining);
           partial.writeBytes(in, toRead);
           remaining -= toRead;
           readableBytes -= toRead;
           if (remaining == 0) {
             partialPayload = null;
+            int opcodeFin = decodeFlags(encodedSt);
+            int opcode = decodeFlagOpcode(opcodeFin);
+            boolean fin = decodeFlagFin(opcodeFin);
             onFrameRead(ctx, fin, opcode, partial);
             st = STATE_NON_PARTIAL;
-          } else {
-            partialRemaining = remaining;
           }
           break;
 
@@ -160,15 +157,15 @@ void decode(ChannelHandlerContext ctx, ByteBuf in) {
           break;
 
         default:
-          throw new IllegalStateException("unexpected decoding state: " + state);
+          throw new IllegalStateException("unexpected decoding state: " + st);
       }
     }
-    state = st;
+    encodedState = encodeState(encodedSt, st);
   }
 
   @Override
   void closeInbound() {
-    state = STATE_CLOSED_INBOUND;
+    encodedState = encodeState(encodedState, STATE_CLOSED_INBOUND);
   }
 
   static ByteBuf partialPayload(ChannelHandlerContext ctx, ByteBuf in, int length) {
@@ -176,4 +173,64 @@ static ByteBuf partialPayload(ChannelHandlerContext ctx, ByteBuf in, int length)
     partial.writeBytes(in);
     return partial;
   }
+
+  /* layout description is on "encodedState" field */
+
+  static int encodeState(int encodedState, int state) {
+    return encodedState & 0xFF_FF_FF_FC | state;
+  }
+
+  static int decodeState(int encodedState) {
+    return encodedState & 0x3;
+  }
+
+  static int encodeFlags(int encodedState, int opcode, boolean fin) {
+    int flags = (fin ? (byte) 1 : 0) << 6 | opcode << 2;
+    return encodedState & 0xFF_FF_FF_F3 | flags;
+  }
+
+  static int decodeFlags(int encodedState) {
+    return (encodedState & 0xFC) >> 2;
+  }
+
+  static int decodeFlagOpcode(int flags) {
+    return flags & 0xF;
+  }
+
+  static boolean decodeFlagFin(int flags) {
+    return (flags & 0x10) == 0x10;
+  }
+
+  static int encodePartialPrefix(int encodedState, byte partialPrefix) {
+    return encodedState & 0xFF_FF_00_FF | (partialPrefix << 8) & 0xFFFF;
+  }
+
+  static int decodePartialPrefix(int encodedState) {
+    return encodedState & 0xFF_00;
+  }
+
+  static int encodeFragmentedLength(int encodedState, int fragmentedTotalLength) {
+    return encodedState & 0xFF_FF | fragmentedTotalLength << 16;
+  }
+
+  /* non-negative value means fragmentation is in progress*/
+  static int decodeFragmentedLength(int encodedState) {
+    return (byte) ((encodedState & 0xFF_00_00) >> 16);
+  }
+
+  static final class WithMaskingEncoder extends SmallWebSocketDecoder {
+
+    @Override
+    WebSocketFrameFactory frameFactory() {
+      return MaskingWebSocketEncoder.FrameFactory.INSTANCE;
+    }
+  }
+
+  static final class WithNonMaskingEncoder extends SmallWebSocketDecoder {
+
+    @Override
+    WebSocketFrameFactory frameFactory() {
+      return NonMaskingWebSocketEncoder.FrameFactory.INSTANCE;
+    }
+  }
 }
diff --git a/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/WebSocketCallbacksFrameDecoder.java b/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/WebSocketCallbacksFrameDecoder.java
index de6037e..66711d4 100644
--- a/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/WebSocketCallbacksFrameDecoder.java
+++ b/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/WebSocketCallbacksFrameDecoder.java
@@ -26,6 +26,26 @@ void frameListener(
       WebSocketFrameListener webSocketFrameListener,
       WebSocketFrameFactory frameFactory);
 
+  static WebSocketCallbacksFrameDecoder frameDecoder(
+      boolean maskPayload,
+      int maxFramePayloadLength,
+      boolean expectMaskedFrames,
+      boolean allowMaskMismatch) {
+
+    /*strict*/
+    if (!allowMaskMismatch) {
+      /*small unmasked*/
+      if (maxFramePayloadLength <= 125 && !expectMaskedFrames) {
+        return maskPayload
+            ? new SmallWebSocketDecoder.WithMaskingEncoder()
+            : new SmallWebSocketDecoder.WithNonMaskingEncoder();
+      }
+      throw new IllegalArgumentException(
+          "enforcing strictly masked/unmasked frames is not supported");
+    }
+    return new DefaultWebSocketDecoder(maxFramePayloadLength);
+  }
+
   static WebSocketCallbacksFrameDecoder frameDecoder(
       int maxFramePayloadLength, boolean expectMaskedFrames, boolean allowMaskMismatch) {
 
@@ -33,7 +53,12 @@ static WebSocketCallbacksFrameDecoder frameDecoder(
     if (!allowMaskMismatch) {
       /*small unmasked*/
       if (maxFramePayloadLength <= 125 && !expectMaskedFrames) {
-        return new SmallWebSocketDecoder(maxFramePayloadLength);
+        throw new IllegalArgumentException(
+            "small decoder: use frameDecoder("
+                + "boolean maskPayload,"
+                + "int maxFramePayloadLength,"
+                + "boolean expectMaskedFrames,"
+                + "boolean allowMaskMismatch) instead of deprecated one");
       }
       throw new IllegalArgumentException(
           "enforcing strictly masked/unmasked frames is not supported");
diff --git a/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/WebSocketClientHandshaker.java b/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/WebSocketClientHandshaker.java
index aff21a7..70d8bc9 100644
--- a/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/WebSocketClientHandshaker.java
+++ b/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/WebSocketClientHandshaker.java
@@ -55,7 +55,7 @@ public WebSocketClientHandshaker(
   @Override
   protected WebSocketFrameDecoder newWebsocketDecoder() {
     return WebSocketCallbacksFrameDecoder.frameDecoder(
-        maxFramePayloadLength(), expectMaskedFrames, allowMaskMismatch);
+        performMasking, maxFramePayloadLength(), expectMaskedFrames, allowMaskMismatch);
   }
 
   @Override
diff --git a/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/WebSocketDecoder.java b/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/WebSocketDecoder.java
index 8f20257..ecdb399 100644
--- a/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/WebSocketDecoder.java
+++ b/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/WebSocketDecoder.java
@@ -28,16 +28,14 @@ abstract class WebSocketDecoder implements ChannelInboundHandler, WebSocketCallb
   static final int STATE_PARTIAL_PAYLOAD = 3;
   static final int STATE_CLOSED_INBOUND = 4;
 
-  final int maxFramePayloadLength;
   WebSocketFrameListener webSocketFrameListener;
-  WebSocketFrameFactory frameFactory;
 
-  WebSocketDecoder(int maxFramePayloadLength) {
-    this.maxFramePayloadLength = maxFramePayloadLength;
-  }
+  WebSocketDecoder() {}
 
   abstract void decode(ChannelHandlerContext ctx, ByteBuf buf);
 
+  abstract WebSocketFrameFactory frameFactory();
+
   @Override
   public void channelInactive(ChannelHandlerContext ctx) throws Exception {
     WebSocketFrameListener listener = webSocketFrameListener;
@@ -67,7 +65,6 @@ public void frameListener(
       WebSocketFrameListener webSocketFrameListener,
       WebSocketFrameFactory frameFactory) {
     this.webSocketFrameListener = webSocketFrameListener;
-    this.frameFactory = frameFactory;
   }
 
   @Override
diff --git a/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/WebSocketProtocol.java b/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/WebSocketProtocol.java
index ed5b0aa..5aac516 100644
--- a/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/WebSocketProtocol.java
+++ b/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/WebSocketProtocol.java
@@ -34,7 +34,7 @@ public final class WebSocketProtocol {
   public static final byte OPCODE_PONG = 0xA;
 
   /*validation*/
-  static final int VALIDATION_RESULT_INVALID = Integer.MIN_VALUE;
+  static final int VALIDATION_RESULT_INVALID = Byte.MIN_VALUE;
   static final int VALIDATION_RESULT_NON_FRAGMENTING = -1;
 
   static int validate(
@@ -203,7 +203,7 @@ static void close(
       WebSocketDecoder webSocketDecoder,
       WebSocketCloseStatus status,
       String msg) {
-    WebSocketFrameFactory frameFactory = webSocketDecoder.frameFactory;
+    WebSocketFrameFactory frameFactory = webSocketDecoder.frameFactory();
     WebSocketFrameListener frameListener = webSocketDecoder.webSocketFrameListener;
 
     ByteBuf closeFrame =
@@ -220,8 +220,8 @@ public static void validateDecoderConfig(
       boolean expectMaskedFrames,
       boolean allowMaskMismatch) {
 
-    if (maxFramePayloadLength < 0 || maxFramePayloadLength > 65_535) {
-      throw new IllegalArgumentException("maxFramePayloadLength must be in range [0; 65535]");
+    if (maxFramePayloadLength < 125 || maxFramePayloadLength > 65_535) {
+      throw new IllegalArgumentException("maxFramePayloadLength must be in range [125; 65535]");
     }
     if (allowExtensions) {
       throw new IllegalArgumentException("extensions are not supported");
@@ -255,12 +255,23 @@ private WebSocketProtocol() {}
 
   /*for use with external websocket handlers, e.g. websocket-http2*/
 
+  /** @deprecated use {@link #frameDecoder(boolean, int, boolean, boolean)} instead */
+  @Deprecated
   public static WebSocketFrameDecoder frameDecoder(
       int maxFramePayloadLength, boolean expectMaskedFrames, boolean allowMaskMismatch) {
     return WebSocketCallbacksFrameDecoder.frameDecoder(
         maxFramePayloadLength, expectMaskedFrames, allowMaskMismatch);
   }
 
+  public static WebSocketFrameDecoder frameDecoder(
+      boolean maskPayload,
+      int maxFramePayloadLength,
+      boolean expectMaskedFrames,
+      boolean allowMaskMismatch) {
+    return WebSocketCallbacksFrameDecoder.frameDecoder(
+        maskPayload, maxFramePayloadLength, expectMaskedFrames, allowMaskMismatch);
+  }
+
   public static WebSocketFrameEncoder frameEncoder(boolean expectMaskedFrames) {
     return WebSocketCallbacksFrameEncoder.frameEncoder(expectMaskedFrames);
   }
diff --git a/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/WebSocketServerHandshaker.java b/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/WebSocketServerHandshaker.java
index df16eb8..aa4ef2b 100644
--- a/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/WebSocketServerHandshaker.java
+++ b/netty-websocket-http1/src/main/java/com/jauntsdn/netty/handler/codec/http/websocketx/WebSocketServerHandshaker.java
@@ -41,6 +41,7 @@ protected WebSocketFrameDecoder newWebsocketDecoder() {
     WebSocketDecoderConfig decoderConfig = decoderConfig();
 
     return WebSocketCallbacksFrameDecoder.frameDecoder(
+        false,
         decoderConfig.maxFramePayloadLength(),
         decoderConfig.expectMaskedFrames(),
         decoderConfig.allowMaskMismatch());
diff --git a/netty-websocket-http1/src/main/resources/META-INF/native-image/com.jauntsdn.netty/netty-websocket-http1/generated/handlers/reflect-config.json b/netty-websocket-http1/src/main/resources/META-INF/native-image/com.jauntsdn.netty/netty-websocket-http1/generated/handlers/reflect-config.json
new file mode 100644
index 0000000..543aa30
--- /dev/null
+++ b/netty-websocket-http1/src/main/resources/META-INF/native-image/com.jauntsdn.netty/netty-websocket-http1/generated/handlers/reflect-config.json
@@ -0,0 +1,79 @@
+[
+  {
+    "name": "com.jauntsdn.netty.handler.codec.http.websocketx.DefaultWebSocketDecoder",
+    "condition": {
+      "typeReachable": "com.jauntsdn.netty.handler.codec.http.websocketx.DefaultWebSocketDecoder"
+    },
+    "queryAllPublicMethods": true
+  },
+  {
+    "name": "com.jauntsdn.netty.handler.codec.http.websocketx.WebSocketDecoder",
+    "condition": {
+      "typeReachable": "com.jauntsdn.netty.handler.codec.http.websocketx.WebSocketDecoder"
+    },
+    "queryAllPublicMethods": true
+  },
+  {
+    "name": "com.jauntsdn.netty.handler.codec.http.websocketx.MaskingWebSocketEncoder",
+    "condition": {
+      "typeReachable": "com.jauntsdn.netty.handler.codec.http.websocketx.MaskingWebSocketEncoder"
+    },
+    "queryAllPublicMethods": true
+  },
+  {
+    "name": "com.jauntsdn.netty.handler.codec.http.websocketx.SmallWebSocketDecoder",
+    "condition": {
+      "typeReachable": "com.jauntsdn.netty.handler.codec.http.websocketx.SmallWebSocketDecoder"
+    },
+    "queryAllPublicMethods": true
+  },
+  {
+    "name": "com.jauntsdn.netty.handler.codec.http.websocketx.SmallWebSocketDecoder$WithMaskingEncoder",
+    "condition": {
+      "typeReachable": "com.jauntsdn.netty.handler.codec.http.websocketx.SmallWebSocketDecoder$WithMaskingEncoder"
+    },
+    "queryAllPublicMethods": true
+  },
+  {
+    "name": "com.jauntsdn.netty.handler.codec.http.websocketx.SmallWebSocketDecoder$WithNonMaskingEncoder",
+    "condition": {
+      "typeReachable": "com.jauntsdn.netty.handler.codec.http.websocketx.SmallWebSocketDecoder$WithNonMaskingEncoder"
+    },
+    "queryAllPublicMethods": true
+  },
+  {
+    "name": "com.jauntsdn.netty.handler.codec.http.websocketx.WebSocketClientHandshaker",
+    "condition": {
+      "typeReachable": "com.jauntsdn.netty.handler.codec.http.websocketx.WebSocketClientHandshaker"
+    },
+    "queryAllPublicMethods": true
+  },
+  {
+    "name": "com.jauntsdn.netty.handler.codec.http.websocketx.WebSocketClientProtocolHandler",
+    "condition": {
+      "typeReachable": "com.jauntsdn.netty.handler.codec.http.websocketx.WebSocketClientProtocolHandler"
+    },
+    "queryAllPublicMethods": true
+  },
+  {
+    "name": "com.jauntsdn.netty.handler.codec.http.websocketx.WebSocketProtocol",
+    "condition": {
+      "typeReachable": "com.jauntsdn.netty.handler.codec.http.websocketx.WebSocketProtocol"
+    },
+    "queryAllPublicMethods": true
+  },
+  {
+    "name": "com.jauntsdn.netty.handler.codec.http.websocketx.WebSocketServerHandshaker",
+    "condition": {
+      "typeReachable": "com.jauntsdn.netty.handler.codec.http.websocketx.WebSocketServerHandshaker"
+    },
+    "queryAllPublicMethods": true
+  },
+  {
+    "name": "com.jauntsdn.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler",
+    "condition": {
+      "typeReachable": "com.jauntsdn.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler"
+    },
+    "queryAllPublicMethods": true
+  }
+]
\ No newline at end of file