This is an automated email from the ASF dual-hosted git repository.

pjfanning pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko-http.git


The following commit(s) were added to refs/heads/main by this push:
     new 07bd95574 Add server-side WebSocket compression (#1114)
07bd95574 is described below

commit 07bd95574aed641bc7d3c326f3072185060c6c94
Author: Matthias Kurz <[email protected]>
AuthorDate: Thu Jul 2 01:45:59 2026 +0200

    Add server-side WebSocket compression (#1114)
    
    * Add server WebSocket compression #61
    
    * Negotiate RFC 7692 permessage-deflate for server-side WebSockets.
    
    * Add compression settings, documentation, release notes, and MiMa filters.
    
    * Cover negotiation, compression, fragmentation, context takeover, max 
allocation, and low-level frame handling.
    
    * Allow disabling WebSocket compression per upgrade #61
    
    * Add WebSocketUpgrade overloads that accept a compressionEnabled flag.
    
    * Wire accepted WebSockets to decline negotiated compression per request.
    
    * Document and test per-upgrade compression disabling.
---
 .../main/paradox/client-side/websocket-support.md  |    5 +
 .../main/paradox/server-side/websocket-support.md  |   34 +
 .../websocket-compression.excludes                 |   20 +
 http-core/src/main/resources/reference.conf        |   38 +
 .../impl/engine/parsing/HttpHeaderParser.scala     |    1 +
 .../pekko/http/impl/engine/ws/Handshake.scala      |   45 +-
 .../http/impl/engine/ws/PerMessageDeflate.scala    |  345 ++++++
 .../engine/ws/UpgradeToWebSocketLowLevel.scala     |    7 +
 .../pekko/http/impl/engine/ws/WebSocket.scala      |    2 +
 .../http/impl/settings/WebSocketSettingsImpl.scala |   52 +-
 .../http/javadsl/model/ws/WebSocketUpgrade.scala   |   48 +
 .../http/scaladsl/model/ws/WebSocketUpgrade.scala  |   86 ++
 .../impl/engine/parsing/HttpHeaderParserSpec.scala |    3 +
 .../http/impl/engine/ws/WebSocketServerSpec.scala  | 1238 +++++++++++++++++++-
 14 files changed, 1897 insertions(+), 27 deletions(-)

diff --git a/docs/src/main/paradox/client-side/websocket-support.md 
b/docs/src/main/paradox/client-side/websocket-support.md
index ad6badff3..133cf1280 100644
--- a/docs/src/main/paradox/client-side/websocket-support.md
+++ b/docs/src/main/paradox/client-side/websocket-support.md
@@ -9,6 +9,11 @@ the lifetime of the connection. Therefore a WebSocket 
connection is modelled as
 @apidoc[Flow[Message, Message, Mat]] to or a @apidoc[Flow[Message, Message, 
Mat]] that you connect a @apidoc[Source[Message, Mat]] and
 a @apidoc[Sink[Message, Mat]] to.
 
+@@@ note
+Client-side WebSocket compression is not currently implemented. Apache Pekko 
HTTP can negotiate
+`permessage-deflate` compression for server-side WebSocket connections when a 
client requests it.
+@@@
+
 A WebSocket request starts with a regular HTTP request which contains an 
`Upgrade` header (and possibly
 other regular HTTP request properties), so in addition to the flow of messages 
there also is an initial response
 from the server, this is modelled with @apidoc[WebSocketUpgradeResponse].
diff --git a/docs/src/main/paradox/server-side/websocket-support.md 
b/docs/src/main/paradox/server-side/websocket-support.md
index 181de6320..f0f8326a6 100644
--- a/docs/src/main/paradox/server-side/websocket-support.md
+++ b/docs/src/main/paradox/server-side/websocket-support.md
@@ -151,6 +151,40 @@ In case you need to keep inactive connections alive, you 
can either tweak your i
 
 <a id="keep-alive-ping"></a>
 
+## WebSocket compression
+
+Apache Pekko HTTP can negotiate the RFC 7692 `permessage-deflate` WebSocket 
extension for server-side WebSocket
+connections. Compression is enabled by default, but is only used when the 
client requests it with the
+`Sec-WebSocket-Extensions: permessage-deflate` header during the WebSocket 
handshake.
+
+You can disable WebSocket compression globally for the server:
+
+```
+pekko.http.server.websocket.compression.enabled = false
+```
+
+The server exposes additional settings for the negotiated extension under
+`pekko.http.server.websocket.compression.permessage-deflate`, including 
`compression-level`,
+`preferred-client-window-size`, `allow-server-no-context`, and 
`preferred-client-no-context`. See the
+@ref[configuration reference](../configuration.md) for the complete list of 
settings and defaults.
+
+If compression is enabled globally, a route can still decline compression for 
a single accepted WebSocket by using the
+`handleMessages` or `handleMessagesWith` overload with `compressionEnabled = 
false`.
+
+@@@ note
+The `server_no_context_takeover` and `client_no_context_takeover` extension 
parameters affect whether compression
+dictionaries are retained across messages. Retaining context generally 
improves compression ratio, while disabling
+context takeover can reduce cross-message information retention and memory 
lifetime. The defaults keep context takeover
+enabled unless explicitly configured otherwise.
+
+Pekko HTTP uses the JDK `Deflater` and `Inflater` implementation for 
`permessage-deflate`. The JDK API
+does not expose zlib `windowBits` or `memLevel`, so Pekko HTTP does not accept 
`server_max_window_bits`
+values below `15` and does not provide server window-size or memory-level 
settings. A client may still
+request `client_max_window_bits`; when it does, Pekko HTTP can include the 
configured
+`preferred-client-window-size` in the handshake response to ask the client to 
use that window size for
+client-to-server messages.
+@@@
+
 ## Automatic keep-alive Ping support
 
 For long running websocket connections it may be beneficial to enable 
automatic heartbeat using `Ping` frames.
diff --git 
a/http-core/src/main/mima-filters/2.0.x.backwards.excludes/websocket-compression.excludes
 
b/http-core/src/main/mima-filters/2.0.x.backwards.excludes/websocket-compression.excludes
new file mode 100644
index 000000000..e1742d6fa
--- /dev/null
+++ 
b/http-core/src/main/mima-filters/2.0.x.backwards.excludes/websocket-compression.excludes
@@ -0,0 +1,20 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF 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.
+
+# Add server-side WebSocket compression support and per-upgrade compression 
control.
+ProblemFilters.exclude[DirectMissingMethodProblem]("org.apache.pekko.http.impl.engine.ws.Handshake#Server.buildResponse")
+ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.http.javadsl.model.ws.WebSocketUpgrade.handleMessagesWith")
diff --git a/http-core/src/main/resources/reference.conf 
b/http-core/src/main/resources/reference.conf
index 81f1aa903..1784bfd93 100644
--- a/http-core/src/main/resources/reference.conf
+++ b/http-core/src/main/resources/reference.conf
@@ -348,6 +348,44 @@ pekko.http {
 
       # Enable verbose debug logging for all ingoing and outgoing frames
       log-frames = false
+
+      compression {
+        # Whether the server should support WebSocket compression using the 
RFC 7692
+        # permessage-deflate extension. Compression is negotiated during the
+        # WebSocket handshake and is only used when the client requests it.
+        enabled = true
+
+        # Maximum size of a decompressed WebSocket message. If this value is
+        # exceeded while inflating a compressed message, the connection is 
closed
+        # with a WebSocket protocol error.
+        # Set to 0 to disable this limit.
+        max-allocation = 64k
+
+        permessage-deflate {
+          # Pekko HTTP uses the JDK Deflater/Inflater implementation for
+          # permessage-deflate. The JDK API does not expose zlib windowBits or
+          # memLevel, so clients cannot negotiate a server_max_window_bits 
value
+          # below 15. client_max_window_bits can still be negotiated through
+          # preferred-client-window-size.
+
+          # DEFLATE compression level used for server-to-client messages. Valid
+          # values are 0-9, where 0 uses no compression and 9 favors 
compression
+          # ratio over CPU usage.
+          compression-level = 6
+
+          # The client_max_window_bits value Pekko HTTP should request for
+          # client-to-server messages when the client sends 
client_max_window_bits
+          # without an explicit value. Valid values are 8-15.
+          preferred-client-window-size = 15
+
+          # Whether a client may request server_no_context_takeover.
+          allow-server-no-context = false
+
+          # Whether Pekko HTTP should request client_no_context_takeover when 
the
+          # client indicates that it supports this parameter.
+          preferred-client-no-context = false
+        }
+      }
     }
   }
 
diff --git 
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/HttpHeaderParser.scala
 
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/HttpHeaderParser.scala
index 63010f6e4..9b887c743 100644
--- 
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/HttpHeaderParser.scala
+++ 
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/HttpHeaderParser.scala
@@ -487,6 +487,7 @@ private[http] object HttpHeaderParser {
     "content-type",
     "expect",
     "host",
+    "sec-websocket-extensions",
     "sec-websocket-key",
     "sec-websocket-protocol",
     "sec-websocket-version",
diff --git 
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/ws/Handshake.scala 
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/ws/Handshake.scala
index 9e0865cc7..e98a900c5 100644
--- 
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/ws/Handshake.scala
+++ 
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/ws/Handshake.scala
@@ -23,6 +23,7 @@ import scala.collection.immutable.Seq
 import pekko.event.LoggingAdapter
 import pekko.http.impl.util._
 import pekko.http.impl.engine.server.UpgradeToOtherProtocolResponseHeader
+import pekko.http.impl.settings.WebSocketSettingsImpl
 import pekko.http.scaladsl.model.headers._
 import pekko.http.scaladsl.model.ws.Message
 import pekko.http.scaladsl.model._
@@ -122,26 +123,47 @@ private[http] object Handshake {
             case OptionVal.Some(p) => p.protocols
             case _                 => Nil
           }
+          val clientRequestedExtensions = headers.collect {
+            case extensions: `Sec-WebSocket-Extensions` => 
extensions.extensions
+          }.flatten
+          val perMessageDeflate =
+            PerMessageDeflate.negotiate(
+              clientRequestedExtensions,
+              settings.asInstanceOf[WebSocketSettingsImpl].compression)
 
           val header = new UpgradeToWebSocketLowLevel {
             def requestedProtocols: Seq[String] = clientSupportedSubprotocols
 
             def handle(
                 handler: Either[Graph[FlowShape[FrameEvent, FrameEvent], Any], 
Graph[FlowShape[Message, Message], Any]],
-                subprotocol: Option[String]): HttpResponse = {
+                subprotocol: Option[String],
+                compressionEnabled: Boolean): HttpResponse = {
               require(
                 subprotocol.forall(chosen => 
clientSupportedSubprotocols.contains(chosen)),
                 s"Tried to choose invalid subprotocol '$subprotocol' which 
wasn't offered by the client: [${requestedProtocols.mkString(", ")}]")
-              buildResponse(key.get, handler, subprotocol, settings, log)
+              val acceptedPerMessageDeflate = if (compressionEnabled) 
perMessageDeflate else None
+              buildResponse(key.get, handler, subprotocol, 
acceptedPerMessageDeflate, settings, log)
             }
 
             def handleFrames(
                 handlerFlow: Graph[FlowShape[FrameEvent, FrameEvent], Any], 
subprotocol: Option[String]): HttpResponse =
-              handle(Left(handlerFlow), subprotocol)
+              handle(Left(handlerFlow), subprotocol, compressionEnabled = true)
+
+            override private[http] def handleFrames(
+                handlerFlow: Graph[FlowShape[FrameEvent, FrameEvent], Any],
+                subprotocol: Option[String],
+                compressionEnabled: Boolean): HttpResponse =
+              handle(Left(handlerFlow), subprotocol, compressionEnabled)
 
             override def handleMessages(handlerFlow: Graph[FlowShape[Message, 
Message], Any],
                 subprotocol: Option[String] = None): HttpResponse =
-              handle(Right(handlerFlow), subprotocol)
+              handle(Right(handlerFlow), subprotocol, compressionEnabled = 
true)
+
+            override def handleMessages(
+                handlerFlow: Graph[FlowShape[Message, Message], Any],
+                subprotocol: Option[String],
+                compressionEnabled: Boolean): HttpResponse =
+              handle(Right(handlerFlow), subprotocol, compressionEnabled)
           }
           OptionVal.Some(header)
         } else OptionVal.None
@@ -169,11 +191,16 @@ private[http] object Handshake {
      */
     def buildResponse(key: `Sec-WebSocket-Key`,
         handler: Either[Graph[FlowShape[FrameEvent, FrameEvent], Any], 
Graph[FlowShape[Message, Message], Any]],
-        subprotocol: Option[String], settings: WebSocketSettings, log: 
LoggingAdapter): HttpResponse = {
+        subprotocol: Option[String],
+        perMessageDeflate: Option[PerMessageDeflate.Negotiated],
+        settings: WebSocketSettings,
+        log: LoggingAdapter): HttpResponse = {
       val frameHandler = handler match {
-        case Left(frameHandler)    => frameHandler
+        case Left(frameHandler) =>
+          
perMessageDeflate.map(_.frameEventBidiFlow(settings.randomFactory).join(frameHandler)).getOrElse(frameHandler)
         case Right(messageHandler) =>
-          WebSocket.stack(serverSide = true, settings, log = 
log).join(messageHandler)
+          WebSocket.stack(serverSide = true, settings, perMessageDeflate = 
perMessageDeflate, log = log)
+            .join(messageHandler)
       }
 
       HttpResponse(
@@ -182,7 +209,9 @@ private[http] object Handshake {
         List(
           UpgradeHeader,
           ConnectionUpgradeHeader,
-          `Sec-WebSocket-Accept`.forKey(key),
+          `Sec-WebSocket-Accept`.forKey(key)) :::
+        perMessageDeflate.map(p => 
`Sec-WebSocket-Extensions`(Seq(p.responseExtension))).toList :::
+        List(
           
UpgradeToOtherProtocolResponseHeader(WebSocket.framing.join(frameHandler))))
     }
   }
diff --git 
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/ws/PerMessageDeflate.scala
 
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/ws/PerMessageDeflate.scala
new file mode 100644
index 000000000..eb71b21d0
--- /dev/null
+++ 
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/ws/PerMessageDeflate.scala
@@ -0,0 +1,345 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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
+ *
+ *    https://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 org.apache.pekko.http.impl.engine.ws
+
+import java.io.ByteArrayOutputStream
+import java.util.Random
+import java.util.zip.Deflater
+import java.util.zip.Inflater
+import java.util.zip.DataFormatException
+
+import org.apache.pekko
+import pekko.NotUsed
+import pekko.annotation.InternalApi
+import pekko.http.impl.settings.WebSocketCompressionSettingsImpl
+import pekko.http.scaladsl.model.headers.WebSocketExtension
+import pekko.stream.scaladsl.BidiFlow
+import pekko.stream.scaladsl.Flow
+import pekko.stream.stage.GraphStage
+import pekko.stream.stage.GraphStageLogic
+import pekko.stream.stage.InHandler
+import pekko.stream.stage.OutHandler
+import pekko.stream.{ Attributes, FlowShape, Inlet, Outlet }
+import pekko.util.ByteString
+
+import scala.collection.immutable
+import scala.collection.immutable.ListMap
+
+/**
+ * INTERNAL API
+ */
+@InternalApi
+private[http] object PerMessageDeflate {
+  private val ExtensionName = "permessage-deflate"
+  private val ClientMaxWindowBits = "client_max_window_bits"
+  private val ServerMaxWindowBits = "server_max_window_bits"
+  private val ClientNoContextTakeover = "client_no_context_takeover"
+  private val ServerNoContextTakeover = "server_no_context_takeover"
+  private val EmptyStoredBlock = ByteString(0x00, 0x00, 0xFF.toByte, 
0xFF.toByte)
+
+  final case class Negotiated(
+      responseExtension: WebSocketExtension,
+      serverNoContextTakeover: Boolean,
+      clientNoContextTakeover: Boolean,
+      settings: WebSocketCompressionSettingsImpl) {
+    def bidiFlow: BidiFlow[FrameEventOrError, FrameEventOrError, FrameEvent, 
FrameEvent, NotUsed] =
+      BidiFlow.fromFlows(inflaterFlow, deflaterFlow)
+
+    def frameEventBidiFlow(
+        maskRandom: () => Random): BidiFlow[FrameEvent, FrameEvent, 
FrameEvent, FrameEvent, NotUsed] =
+      BidiFlow.fromFlows(
+        Flow[FrameEvent]
+          .via(Masking.unmaskIf(condition = true))
+          .via(inflaterFlow)
+          .map {
+            case frame: FrameEvent => frame
+            case FrameError(ex)    => throw ex
+          }
+          .via(Masking.maskIf(condition = true, maskRandom)),
+        deflaterFlow)
+
+    private def inflaterFlow: Flow[FrameEventOrError, FrameEventOrError, 
NotUsed] =
+      Flow.fromGraph(new LifecycleMapConcatStage(
+        "PerMessageDeflate.inflater",
+        () => new InflaterFlow(clientNoContextTakeover, settings)))
+
+    private def deflaterFlow: Flow[FrameEvent, FrameEvent, NotUsed] =
+      Flow.fromGraph(new LifecycleMapConcatStage(
+        "PerMessageDeflate.deflater",
+        () => new DeflaterFlow(serverNoContextTakeover, settings)))
+  }
+
+  def negotiate(
+      requested: immutable.Seq[WebSocketExtension],
+      settings: WebSocketCompressionSettingsImpl): Option[Negotiated] = {
+    if (!settings.enabled) None
+    else {
+      requested.collectFirst(Function.unlift { extension =>
+        if (extension.name.equalsIgnoreCase(ExtensionName)) 
negotiate(extension, settings) else None
+      })
+    }
+  }
+
+  private def negotiate(
+      extension: WebSocketExtension,
+      settings: WebSocketCompressionSettingsImpl): Option[Negotiated] = {
+    var responseParams = ListMap.empty[String, String]
+    var clientNoContext = false
+    var serverNoContext = false
+    var accepted = true
+
+    extension.params.foreach {
+      case (ClientMaxWindowBits, value) =>
+        if (value.isEmpty) responseParams += ClientMaxWindowBits -> 
settings.preferredClientWindowSize.toString
+        else if (validWindowBits(value)) responseParams += ClientMaxWindowBits 
-> value
+        else accepted = false
+      case (ServerMaxWindowBits, value) =>
+        if (value == "15") responseParams += ServerMaxWindowBits -> value
+        else accepted = false
+      case (ClientNoContextTakeover, "") =>
+        clientNoContext = settings.preferredClientNoContext
+        if (clientNoContext) responseParams += ClientNoContextTakeover -> ""
+      case (ServerNoContextTakeover, "") =>
+        if (settings.allowServerNoContext) {
+          serverNoContext = true
+          responseParams += ServerNoContextTakeover -> ""
+        } else accepted = false
+      case _ =>
+        accepted = false
+    }
+
+    if (accepted) {
+      Some(Negotiated(WebSocketExtension(ExtensionName, responseParams), 
serverNoContext, clientNoContext, settings))
+    } else None
+  }
+
+  private def validWindowBits(value: String): Boolean =
+    value.length <= 2 && value.forall(_.isDigit) && {
+      val parsed = value.toInt
+      parsed >= 8 && parsed <= 15
+    }
+
+  private final class InflaterFlow(
+      noContextTakeover: Boolean,
+      settings: WebSocketCompressionSettingsImpl)
+      extends LifecycleMapConcat[FrameEventOrError, FrameEventOrError] {
+    private var inflater = new Inflater(true)
+    private var compressedFrame: Option[CompressedFrame] = None
+    private var compressedMessageInProgress = false
+    private var decompressedMessageBytes = 0L
+    private var bypassFrameInProgress = false
+
+    override def apply(event: FrameEventOrError): 
immutable.Iterable[FrameEventOrError] = event match {
+      case start @ FrameStart(header, data)
+          if header.rsv1 &&
+          (header.opcode == Protocol.Opcode.Text ||
+          header.opcode == Protocol.Opcode.Binary) =>
+        if (compressedMessageInProgress || compressedFrame.isDefined)
+          throw new ProtocolException("Unexpected data frame while fragmented 
message is open")
+        if (header.rsv2 || header.rsv3) throw new 
ProtocolException("Unexpected reserved bit for compressed message")
+        compressedMessageInProgress = !header.fin
+        compressedFrame = Some(CompressedFrame(header.copy(rsv1 = false, 
length = 0), data, appendTail = header.fin))
+        if (start.lastPart) finishFrame() else Nil
+      case start @ FrameStart(header, _) if bypassFrameInProgress =>
+        throw new ProtocolException(s"Unexpected frame ${header.opcode} while 
frame data is open")
+      case start @ FrameStart(header, _)
+          if (compressedFrame.isDefined || compressedMessageInProgress) && 
header.opcode.isControl =>
+        bypassFrameInProgress = !start.lastPart
+        start :: Nil
+      case start @ FrameStart(header, data)
+          if compressedMessageInProgress && header.opcode == 
Protocol.Opcode.Continuation =>
+        if (header.rsv1 || header.rsv2 || header.rsv3)
+          throw new ProtocolException("Unexpected reserved bit for 
continuation frame")
+        compressedMessageInProgress = !header.fin
+        compressedFrame = Some(CompressedFrame(header.copy(length = 0), data, 
appendTail = header.fin))
+        if (start.lastPart) finishFrame() else Nil
+      case start @ FrameStart(header, _) if compressedFrame.isDefined || 
compressedMessageInProgress =>
+        throw new ProtocolException(s"Unexpected frame ${header.opcode} while 
fragmented message is open")
+      case data: FrameData if bypassFrameInProgress =>
+        bypassFrameInProgress = !data.lastPart
+        data :: Nil
+      case data: FrameData if compressedFrame.isDefined =>
+        compressedFrame = compressedFrame.map(_.append(data.data))
+        if (data.lastPart) finishFrame() else Nil
+      case other => other :: Nil
+    }
+
+    private def finishFrame(): immutable.Iterable[FrameEventOrError] = {
+      val frame = compressedFrame.get
+      compressedFrame = None
+      val inflated = inflate(frame.data, frame.appendTail)
+      if (frame.appendTail) decompressedMessageBytes = 0L
+      if (frame.appendTail && noContextTakeover) {
+        inflater.end()
+        inflater = new Inflater(true)
+      }
+      FrameStart(frame.header.copy(length = inflated.length), inflated) :: Nil
+    }
+
+    private def inflate(data: ByteString, appendTail: Boolean): ByteString = {
+      try {
+        val input = if (appendTail) data ++ EmptyStoredBlock else data
+        inflater.setInput(input.toArray)
+        val output = new ByteArrayOutputStream()
+        val buffer = new Array[Byte](1024)
+        var count = inflater.inflate(buffer)
+        while (count > 0) {
+          decompressedMessageBytes += count
+          if (settings.maxAllocation > 0 && decompressedMessageBytes > 
settings.maxAllocation)
+            throw new ProtocolException("WebSocket decompressed message 
exceeds configured maximum allocation")
+          output.write(buffer, 0, count)
+          count = inflater.inflate(buffer)
+        }
+        ByteString.fromArray(output.toByteArray)
+      } catch {
+        case ex: DataFormatException =>
+          throw new ProtocolException(s"Invalid WebSocket compressed message: 
${ex.getMessage}")
+      }
+    }
+
+    override def close(): Unit =
+      inflater.end()
+  }
+
+  private final class DeflaterFlow(
+      noContextTakeover: Boolean,
+      settings: WebSocketCompressionSettingsImpl)
+      extends LifecycleMapConcat[FrameEvent, FrameEvent] {
+    private var deflater = new Deflater(settings.compressionLevel, true)
+    private var frame: Option[UncompressedFrame] = None
+    private var messageInProgress = false
+    private var bypassFrameInProgress = false
+
+    override def apply(event: FrameEvent): immutable.Iterable[FrameEvent] = 
event match {
+      case FrameStart(header, _)
+          if (header.opcode == Protocol.Opcode.Text ||
+          header.opcode == Protocol.Opcode.Binary) &&
+          (header.rsv1 || header.rsv2 || header.rsv3) =>
+        throw new ProtocolException("Unexpected reserved bit for outbound 
WebSocket message")
+      case FrameStart(header, _)
+          if header.opcode == Protocol.Opcode.Continuation &&
+          (header.rsv1 || header.rsv2 || header.rsv3) =>
+        throw new ProtocolException("Unexpected reserved bit for outbound 
WebSocket continuation frame")
+      case start @ FrameStart(header, data)
+          if header.opcode == Protocol.Opcode.Text ||
+          header.opcode == Protocol.Opcode.Binary =>
+        if (messageInProgress || frame.isDefined)
+          throw new ProtocolException("Unexpected data frame while fragmented 
message is open")
+        messageInProgress = !header.fin
+        frame = Some(UncompressedFrame(header.copy(length = 0, rsv1 = true), 
data, removeTail = header.fin))
+        if (start.lastPart) finishFrame() else Nil
+      case start @ FrameStart(header, _) if bypassFrameInProgress =>
+        throw new ProtocolException(s"Unexpected frame ${header.opcode} while 
frame data is open")
+      case start @ FrameStart(header, _) if (frame.isDefined || 
messageInProgress) && header.opcode.isControl =>
+        bypassFrameInProgress = !start.lastPart
+        start :: Nil
+      case start @ FrameStart(header, data) if messageInProgress && 
header.opcode == Protocol.Opcode.Continuation =>
+        messageInProgress = !header.fin
+        frame = Some(UncompressedFrame(header.copy(length = 0), data, 
removeTail = header.fin))
+        if (start.lastPart) finishFrame() else Nil
+      case start @ FrameStart(header, _) if frame.isDefined || 
messageInProgress =>
+        throw new ProtocolException(s"Unexpected frame ${header.opcode} while 
fragmented message is open")
+      case data: FrameData if bypassFrameInProgress =>
+        bypassFrameInProgress = !data.lastPart
+        data :: Nil
+      case data: FrameData if frame.isDefined =>
+        frame = frame.map(_.append(data.data))
+        if (data.lastPart) finishFrame() else Nil
+      case other => other :: Nil
+    }
+
+    private def finishFrame(): immutable.Iterable[FrameEvent] = {
+      val current = frame.get
+      frame = None
+      val compressed = deflate(current.data, current.removeTail)
+      if (current.removeTail && noContextTakeover) {
+        deflater.end()
+        deflater = new Deflater(settings.compressionLevel, true)
+      }
+      FrameStart(current.header.copy(length = compressed.length), compressed) 
:: Nil
+    }
+
+    private def deflate(data: ByteString, removeTail: Boolean): ByteString = {
+      deflater.setInput(data.toArray)
+      val output = new ByteArrayOutputStream()
+      val buffer = new Array[Byte](1024)
+      var count = deflater.deflate(buffer, 0, buffer.length, 
Deflater.SYNC_FLUSH)
+      while (count > 0) {
+        output.write(buffer, 0, count)
+        count = deflater.deflate(buffer, 0, buffer.length, Deflater.SYNC_FLUSH)
+      }
+      val bytes = ByteString.fromArray(output.toByteArray)
+      if (removeTail && bytes.endsWith(EmptyStoredBlock)) 
bytes.dropRight(EmptyStoredBlock.length) else bytes
+    }
+
+    override def close(): Unit =
+      deflater.end()
+  }
+
+  private trait LifecycleMapConcat[-In, +Out] extends (In => 
immutable.Iterable[Out]) {
+    def close(): Unit
+  }
+
+  private final class LifecycleMapConcatStage[In, Out](
+      name: String,
+      create: () => LifecycleMapConcat[In, Out])
+      extends GraphStage[FlowShape[In, Out]] {
+    private val in = Inlet[In](s"$name.in")
+    private val out = Outlet[Out](s"$name.out")
+    override val shape: FlowShape[In, Out] = FlowShape(in, out)
+
+    override def createLogic(inheritedAttributes: Attributes): GraphStageLogic 
=
+      new GraphStageLogic(shape) with InHandler with OutHandler {
+        private val handler = create()
+        private var pending = Iterator.empty[Out]
+        private var upstreamFinished = false
+
+        override def onPush(): Unit = {
+          pending = handler(grab(in)).iterator
+          pushOrPull()
+        }
+
+        override def onPull(): Unit =
+          pushOrPull()
+
+        override def onUpstreamFinish(): Unit = {
+          upstreamFinished = true
+          if (!pending.hasNext) completeStage()
+        }
+
+        override def postStop(): Unit =
+          handler.close()
+
+        private def pushOrPull(): Unit =
+          if (pending.hasNext) push(out, pending.next())
+          else if (upstreamFinished) completeStage()
+          else if (!hasBeenPulled(in)) pull(in)
+
+        setHandler(in, this)
+        setHandler(out, this)
+      }
+  }
+
+  private final case class CompressedFrame(header: FrameHeader, data: 
ByteString, appendTail: Boolean) {
+    def append(next: ByteString): CompressedFrame = copy(data = data ++ next)
+  }
+
+  private final case class UncompressedFrame(header: FrameHeader, data: 
ByteString, removeTail: Boolean) {
+    def append(next: ByteString): UncompressedFrame = copy(data = data ++ next)
+  }
+}
diff --git 
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/ws/UpgradeToWebSocketLowLevel.scala
 
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/ws/UpgradeToWebSocketLowLevel.scala
index 805dfeb56..ca5fbd165 100644
--- 
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/ws/UpgradeToWebSocketLowLevel.scala
+++ 
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/ws/UpgradeToWebSocketLowLevel.scala
@@ -42,4 +42,11 @@ private[http] abstract class UpgradeToWebSocketLowLevel 
extends InternalCustomHe
   @InternalApi
   private[http] def handleFrames(handlerFlow: Graph[FlowShape[FrameEvent, 
FrameEvent], Any],
       subprotocol: Option[String] = None): HttpResponse
+
+  @InternalApi
+  private[http] def handleFrames(
+      handlerFlow: Graph[FlowShape[FrameEvent, FrameEvent], Any],
+      subprotocol: Option[String],
+      compressionEnabled: Boolean): HttpResponse =
+    handleFrames(handlerFlow, subprotocol)
 }
diff --git 
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/ws/WebSocket.scala 
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/ws/WebSocket.scala
index 0ee01d0c4..5d1d37656 100644
--- 
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/ws/WebSocket.scala
+++ 
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/ws/WebSocket.scala
@@ -47,10 +47,12 @@ private[http] object WebSocket {
   def stack(
       serverSide: Boolean,
       websocketSettings: WebSocketSettings,
+      perMessageDeflate: Option[PerMessageDeflate.Negotiated] = None,
       closeTimeout: FiniteDuration = 3.seconds, // TODO put close timeout into 
the settings?
       log: LoggingAdapter): BidiFlow[FrameEvent, Message, Message, FrameEvent, 
NotUsed] =
     masking(serverSide, websocketSettings.randomFactory).atop(
       FrameLogger.logFramesIfEnabled(websocketSettings.logFrames)).atop(
+      perMessageDeflate.map(_.bidiFlow).getOrElse(BidiFlow.identity)).atop(
       frameHandling(serverSide, closeTimeout, log)).atop(
       periodicKeepAlive(websocketSettings)).atop(
       messageAPI(serverSide, closeTimeout))
diff --git 
a/http-core/src/main/scala/org/apache/pekko/http/impl/settings/WebSocketSettingsImpl.scala
 
b/http-core/src/main/scala/org/apache/pekko/http/impl/settings/WebSocketSettingsImpl.scala
index 7410699bd..831ca8c97 100644
--- 
a/http-core/src/main/scala/org/apache/pekko/http/impl/settings/WebSocketSettingsImpl.scala
+++ 
b/http-core/src/main/scala/org/apache/pekko/http/impl/settings/WebSocketSettingsImpl.scala
@@ -31,7 +31,8 @@ private[pekko] final case class WebSocketSettingsImpl(
     periodicKeepAliveMode: String,
     periodicKeepAliveMaxIdle: Duration,
     periodicKeepAliveData: () => ByteString,
-    logFrames: Boolean)
+    logFrames: Boolean,
+    compression: WebSocketCompressionSettingsImpl)
     extends pekko.http.scaladsl.settings.WebSocketSettings {
 
   require(
@@ -58,21 +59,62 @@ private[pekko] object WebSocketSettingsImpl { // on purpose 
not extending Settin
   def serverFromRoot(root: Config): WebSocketSettingsImpl =
     server(root.getConfig("pekko.http.server.websocket"))
   def server(config: Config): WebSocketSettingsImpl =
-    fromConfig(config)
+    fromConfig(config, 
WebSocketCompressionSettingsImpl.fromConfig(config.getConfig("compression")))
 
   def clientFromRoot(root: Config): WebSocketSettingsImpl =
     client(root.getConfig("pekko.http.client.websocket"))
   def client(config: Config): WebSocketSettingsImpl =
-    fromConfig(config)
+    fromConfig(config, WebSocketCompressionSettingsImpl.Disabled)
 
-  private def fromConfig(inner: Config): WebSocketSettingsImpl = {
+  private def fromConfig(inner: Config, compression: 
WebSocketCompressionSettingsImpl): WebSocketSettingsImpl = {
     val c = inner
     WebSocketSettingsImpl(
       Randoms.SecureRandomInstances,
       c.getString("periodic-keep-alive-mode"), // mode could be extended to be 
a factory of pings, if we'd need control over the data field
       c.getPotentiallyInfiniteDuration("periodic-keep-alive-max-idle"),
       NoPeriodicKeepAliveData,
-      c.getBoolean("log-frames"))
+      c.getBoolean("log-frames"),
+      compression)
   }
 
 }
+
+/** INTERNAL API */
+@InternalApi
+private[pekko] final case class WebSocketCompressionSettingsImpl(
+    enabled: Boolean,
+    maxAllocation: Long,
+    compressionLevel: Int,
+    preferredClientWindowSize: Int,
+    allowServerNoContext: Boolean,
+    preferredClientNoContext: Boolean) {
+  require(maxAllocation >= 0, "websocket compression max-allocation must be >= 
0")
+  require(compressionLevel >= 0 && compressionLevel <= 9, "websocket 
compression level must be between 0 and 9")
+  require(
+    preferredClientWindowSize >= 8 && preferredClientWindowSize <= 15,
+    "websocket compression preferred-client-window-size must be between 8 and 
15")
+}
+
+/** INTERNAL API */
+@InternalApi
+private[pekko] object WebSocketCompressionSettingsImpl {
+  val Disabled: WebSocketCompressionSettingsImpl =
+    WebSocketCompressionSettingsImpl(
+      enabled = false,
+      maxAllocation = 0,
+      compressionLevel = 6,
+      preferredClientWindowSize = 15,
+      allowServerNoContext = false,
+      preferredClientNoContext = false)
+
+  def fromConfig(c: Config): WebSocketCompressionSettingsImpl = {
+    val perMessageDeflate = c.getConfig("permessage-deflate")
+    WebSocketCompressionSettingsImpl(
+      c.getBoolean("enabled"),
+      c.getBytes("max-allocation"),
+      perMessageDeflate.getInt("compression-level"),
+      perMessageDeflate.getInt("preferred-client-window-size"),
+      perMessageDeflate.getBoolean("allow-server-no-context"),
+      perMessageDeflate.getBoolean("preferred-client-no-context"))
+  }
+}
diff --git 
a/http-core/src/main/scala/org/apache/pekko/http/javadsl/model/ws/WebSocketUpgrade.scala
 
b/http-core/src/main/scala/org/apache/pekko/http/javadsl/model/ws/WebSocketUpgrade.scala
index 93ba7eeae..ddb24aef4 100644
--- 
a/http-core/src/main/scala/org/apache/pekko/http/javadsl/model/ws/WebSocketUpgrade.scala
+++ 
b/http-core/src/main/scala/org/apache/pekko/http/javadsl/model/ws/WebSocketUpgrade.scala
@@ -41,6 +41,17 @@ trait WebSocketUpgrade {
    */
   def handleMessagesWith(handlerFlow: Graph[FlowShape[Message, Message], ? <: 
Any]): HttpResponse
 
+  /**
+   * Returns a response that can be used to answer a WebSocket handshake 
request. The connection will afterwards
+   * use the given handlerFlow to handle WebSocket messages from the client.
+   *
+   * The compressionEnabled flag allows declining negotiated WebSocket 
compression for this accepted WebSocket.
+   *
+   * @since 2.0.0
+   */
+  def handleMessagesWith(
+      handlerFlow: Graph[FlowShape[Message, Message], ? <: Any], 
compressionEnabled: Boolean): HttpResponse
+
   /**
    * Returns a response that can be used to answer a WebSocket handshake 
request. The connection will afterwards
    * use the given handlerFlow to handle WebSocket messages from the client. 
The given subprotocol must be one
@@ -48,6 +59,20 @@ trait WebSocketUpgrade {
    */
   def handleMessagesWith(handlerFlow: Graph[FlowShape[Message, Message], ? <: 
Any], subprotocol: String): HttpResponse
 
+  /**
+   * Returns a response that can be used to answer a WebSocket handshake 
request. The connection will afterwards
+   * use the given handlerFlow to handle WebSocket messages from the client. 
The given subprotocol must be one
+   * of the ones offered by the client.
+   *
+   * The compressionEnabled flag allows declining negotiated WebSocket 
compression for this accepted WebSocket.
+   *
+   * @since 2.0.0
+   */
+  def handleMessagesWith(
+      handlerFlow: Graph[FlowShape[Message, Message], ? <: Any],
+      subprotocol: String,
+      compressionEnabled: Boolean): HttpResponse
+
   /**
    * Returns a response that can be used to answer a WebSocket handshake 
request. The connection will afterwards
    * use the given inSink to handle WebSocket messages from the client and the 
given outSource to send messages to the client.
@@ -55,6 +80,17 @@ trait WebSocketUpgrade {
   def handleMessagesWith(
       inSink: Graph[SinkShape[Message], ? <: Any], outSource: 
Graph[SourceShape[Message], ? <: Any]): HttpResponse
 
+  /**
+   * Returns a response that can be used to answer a WebSocket handshake 
request. The connection will afterwards
+   * use the given inSink to handle WebSocket messages from the client and the 
given outSource to send messages to the client.
+   *
+   * The compressionEnabled flag allows declining negotiated WebSocket 
compression for this accepted WebSocket.
+   *
+   * @since 2.0.0
+   */
+  def handleMessagesWith(inSink: Graph[SinkShape[Message], ? <: Any], 
outSource: Graph[SourceShape[Message], ? <: Any],
+      compressionEnabled: Boolean): HttpResponse
+
   /**
    * Returns a response that can be used to answer a WebSocket handshake 
request. The connection will afterwards
    * use the given inSink to handle WebSocket messages from the client and the 
given outSource to send messages to the client.
@@ -63,4 +99,16 @@ trait WebSocketUpgrade {
    */
   def handleMessagesWith(inSink: Graph[SinkShape[Message], ? <: Any], 
outSource: Graph[SourceShape[Message], ? <: Any],
       subprotocol: String): HttpResponse
+
+  /**
+   * Returns a response that can be used to answer a WebSocket handshake 
request. The connection will afterwards
+   * use the given inSink to handle WebSocket messages from the client and the 
given outSource to send messages to the client.
+   *
+   * The given subprotocol must be one of the ones offered by the client.
+   * The compressionEnabled flag allows declining negotiated WebSocket 
compression for this accepted WebSocket.
+   *
+   * @since 2.0.0
+   */
+  def handleMessagesWith(inSink: Graph[SinkShape[Message], ? <: Any], 
outSource: Graph[SourceShape[Message], ? <: Any],
+      subprotocol: String, compressionEnabled: Boolean): HttpResponse
 }
diff --git 
a/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/ws/WebSocketUpgrade.scala
 
b/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/ws/WebSocketUpgrade.scala
index 4a26a9b71..34de701c2 100644
--- 
a/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/ws/WebSocketUpgrade.scala
+++ 
b/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/ws/WebSocketUpgrade.scala
@@ -52,6 +52,25 @@ trait WebSocketUpgrade extends jm.ws.WebSocketUpgrade {
       handlerFlow: Graph[FlowShape[Message, Message], Any],
       subprotocol: Option[String] = None): HttpResponse
 
+  /**
+   * The high-level interface to create a WebSocket server based on "messages".
+   *
+   * Returns a response to return in a request handler that will signal the
+   * low-level HTTP implementation to upgrade the connection to WebSocket and
+   * use the supplied handler to handle incoming WebSocket messages.
+   *
+   * Optionally, a subprotocol out of the ones requested by the client can be 
chosen.
+   * The `compressionEnabled` flag allows declining negotiated WebSocket 
compression
+   * for this accepted WebSocket.
+   *
+   * @since 2.0.0
+   */
+  def handleMessages(
+      handlerFlow: Graph[FlowShape[Message, Message], Any],
+      subprotocol: Option[String],
+      compressionEnabled: Boolean): HttpResponse =
+    handleMessages(handlerFlow, subprotocol)
+
   /**
    * The high-level interface to create a WebSocket server based on "messages".
    *
@@ -68,6 +87,27 @@ trait WebSocketUpgrade extends jm.ws.WebSocketUpgrade {
       subprotocol: Option[String] = None): HttpResponse =
     handleMessages(scaladsl.Flow.fromSinkAndSource(inSink, outSource), 
subprotocol)
 
+  /**
+   * The high-level interface to create a WebSocket server based on "messages".
+   *
+   * Returns a response to return in a request handler that will signal the
+   * low-level HTTP implementation to upgrade the connection to WebSocket and
+   * use the supplied inSink to consume messages received from the client and
+   * the supplied outSource to produce message to sent to the client.
+   *
+   * Optionally, a subprotocol out of the ones requested by the client can be 
chosen.
+   * The `compressionEnabled` flag allows declining negotiated WebSocket 
compression
+   * for this accepted WebSocket.
+   *
+   * @since 2.0.0
+   */
+  def handleMessagesWithSinkSource(
+      inSink: Graph[SinkShape[Message], Any],
+      outSource: Graph[SourceShape[Message], Any],
+      subprotocol: Option[String],
+      compressionEnabled: Boolean): HttpResponse =
+    handleMessages(scaladsl.Flow.fromSinkAndSource(inSink, outSource), 
subprotocol, compressionEnabled)
+
   import scala.jdk.CollectionConverters._
 
   /**
@@ -81,6 +121,16 @@ trait WebSocketUpgrade extends jm.ws.WebSocketUpgrade {
   def handleMessagesWith(handlerFlow: Graph[FlowShape[jm.ws.Message, 
jm.ws.Message], ? <: Any]): HttpResponse =
     handleMessages(JavaMapping.toScala(handlerFlow))
 
+  /**
+   * Java API
+   *
+   * @since 2.0.0
+   */
+  def handleMessagesWith(
+      handlerFlow: Graph[FlowShape[jm.ws.Message, jm.ws.Message], ? <: Any],
+      compressionEnabled: Boolean): HttpResponse =
+    handleMessages(JavaMapping.toScala(handlerFlow), None, compressionEnabled)
+
   /**
    * Java API
    */
@@ -88,6 +138,17 @@ trait WebSocketUpgrade extends jm.ws.WebSocketUpgrade {
       handlerFlow: Graph[FlowShape[jm.ws.Message, jm.ws.Message], ? <: Any], 
subprotocol: String): HttpResponse =
     handleMessages(JavaMapping.toScala(handlerFlow), subprotocol = 
Some(subprotocol))
 
+  /**
+   * Java API
+   *
+   * @since 2.0.0
+   */
+  def handleMessagesWith(
+      handlerFlow: Graph[FlowShape[jm.ws.Message, jm.ws.Message], ? <: Any],
+      subprotocol: String,
+      compressionEnabled: Boolean): HttpResponse =
+    handleMessages(JavaMapping.toScala(handlerFlow), subprotocol = 
Some(subprotocol), compressionEnabled)
+
   /**
    * Java API
    */
@@ -97,6 +158,19 @@ trait WebSocketUpgrade extends jm.ws.WebSocketUpgrade {
 
   /**
    * Java API
+   *
+   * @since 2.0.0
+   */
+  def handleMessagesWith(
+      inSink: Graph[SinkShape[jm.ws.Message], ? <: Any],
+      outSource: Graph[SourceShape[jm.ws.Message], ? <: Any],
+      compressionEnabled: Boolean): HttpResponse =
+    handleMessages(createScalaFlow(inSink, outSource), None, 
compressionEnabled)
+
+  /**
+   * Java API
+   *
+   * @since 2.0.0
    */
   def handleMessagesWith(
       inSink: Graph[SinkShape[jm.ws.Message], ? <: Any],
@@ -104,6 +178,18 @@ trait WebSocketUpgrade extends jm.ws.WebSocketUpgrade {
       subprotocol: String): HttpResponse =
     handleMessages(createScalaFlow(inSink, outSource), subprotocol = 
Some(subprotocol))
 
+  /**
+   * Java API
+   *
+   * @since 2.0.0
+   */
+  def handleMessagesWith(
+      inSink: Graph[SinkShape[jm.ws.Message], ? <: Any],
+      outSource: Graph[SourceShape[jm.ws.Message], ? <: Any],
+      subprotocol: String,
+      compressionEnabled: Boolean): HttpResponse =
+    handleMessages(createScalaFlow(inSink, outSource), subprotocol = 
Some(subprotocol), compressionEnabled)
+
   private def createScalaFlow(inSink: Graph[SinkShape[jm.ws.Message], ? <: 
Any],
       outSource: Graph[SourceShape[jm.ws.Message], ? <: Any]): 
Graph[FlowShape[Message, Message], NotUsed] =
     JavaMapping.toScala(scaladsl.Flow.fromSinkAndSourceMat(inSink, 
outSource)(scaladsl.Keep.none): Graph[FlowShape[
diff --git 
a/http-core/src/test/scala/org/apache/pekko/http/impl/engine/parsing/HttpHeaderParserSpec.scala
 
b/http-core/src/test/scala/org/apache/pekko/http/impl/engine/parsing/HttpHeaderParserSpec.scala
index 09092a28e..32b18dba7 100644
--- 
a/http-core/src/test/scala/org/apache/pekko/http/impl/engine/parsing/HttpHeaderParserSpec.scala
+++ 
b/http-core/src/test/scala/org/apache/pekko/http/impl/engine/parsing/HttpHeaderParserSpec.scala
@@ -312,6 +312,9 @@ abstract class HttpHeaderParserSpec(mode: String, newLine: 
String) extends Pekko
       parseAndCache(s"Connection: close${newLine}x")(s"CONNECTION: 
close${newLine}x") shouldEqual Connection("close")
       parseAndCache(s"Host: spray.io:123${newLine}x")(s"HOST: 
spray.io:123${newLine}x") shouldEqual Host("spray.io",
         123)
+      parseAndCache(s"Sec-WebSocket-Extensions: 
permessage-deflate${newLine}x")(
+        s"SEC-WEBSOCKET-EXTENSIONS: permessage-deflate${newLine}x") 
shouldEqual `Sec-WebSocket-Extensions`(
+        Seq(WebSocketExtension("permessage-deflate")))
 
       // don't parse other headers
       parseAndCache(s"User-Agent: hmpf${newLine}x")(s"USER-AGENT: 
hmpf${newLine}x") shouldEqual RawHeader("User-Agent",
diff --git 
a/http-core/src/test/scala/org/apache/pekko/http/impl/engine/ws/WebSocketServerSpec.scala
 
b/http-core/src/test/scala/org/apache/pekko/http/impl/engine/ws/WebSocketServerSpec.scala
index 82a9c4627..0146cc948 100644
--- 
a/http-core/src/test/scala/org/apache/pekko/http/impl/engine/ws/WebSocketServerSpec.scala
+++ 
b/http-core/src/test/scala/org/apache/pekko/http/impl/engine/ws/WebSocketServerSpec.scala
@@ -13,6 +13,10 @@
 
 package org.apache.pekko.http.impl.engine.ws
 
+import java.io.ByteArrayOutputStream
+import java.util.zip.Deflater
+import java.util.zip.Inflater
+
 import org.apache.pekko
 import pekko.actor.ActorSystem
 import pekko.http.scaladsl.model.ws._
@@ -22,12 +26,15 @@ import pekko.stream.scaladsl.{ Flow, Keep, Sink, Source }
 import pekko.stream.testkit.Utils
 import pekko.util.ByteString
 import pekko.http.impl.engine.server.HttpServerTestSetupBase
+import pekko.http.impl.settings.WebSocketSettingsImpl
 import pekko.http.impl.util.PekkoSpecWithMaterializer
 
 import scala.concurrent.duration._
 
 class WebSocketServerSpec extends 
PekkoSpecWithMaterializer("pekko.http.server.websocket.log-frames = on") { spec 
=>
 
+  private val EmptyDeflateBlock = ByteString(0x00)
+
   "The server-side WebSocket integration should" should {
     "establish a websocket connection when the user requests it" should {
       "when user handler instantly tries to send messages" in 
Utils.assertAllStagesStopped {
@@ -177,21 +184,1224 @@ class WebSocketServerSpec extends 
PekkoSpecWithMaterializer("pekko.http.server.w
       }
     }
     "prevent the selection of an unavailable subprotocol" in pending
-    "reject invalid WebSocket handshakes" should {
-      "missing `Upgrade: websocket` header" in pending
-      "missing `Connection: upgrade` header" in pending
-      "missing `Sec-WebSocket-Key header" in pending
-      "`Sec-WebSocket-Key` with wrong amount of base64 encoded data" in pending
-      "missing `Sec-WebSocket-Version` header" in pending
-      "unsupported `Sec-WebSocket-Version`" in pending
-    }
-  }
+    "support permessage-deflate compression" should {
+      "negotiate permessage-deflate when requested by the client" in 
Utils.assertAllStagesStopped {
+        new TestSetup {
+          sendWebSocketRequest("Sec-WebSocket-Extensions: 
permessage-deflate\r\n")
 
-  class TestSetup extends HttpServerTestSetupBase with WSTestSetupBase {
-    implicit def system: ActorSystem = spec.system
-    implicit def materializer: Materializer = spec.materializer
+          val request = expectRequest()
+          val upgrade = request.attribute(webSocketUpgrade)
+          val response = upgrade.get.handleMessages(Flow[Message])
+          responses.sendNext(response)
 
-    def expectBytes(length: Int): ByteString = netOut.expectBytes(length)
-    def expectBytes(bytes: ByteString): Unit = netOut.expectBytes(bytes)
+          expectResponseWithWipedDate(
+            """HTTP/1.1 101 Switching Protocols
+              |Upgrade: websocket
+              |Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
+              |Sec-WebSocket-Extensions: permessage-deflate
+              |Server: pekko-http/test
+              |Date: XXXX
+              |Connection: upgrade
+              |
+              |""")
+
+          sendWSCloseFrame(Protocol.CloseCodes.Regular, mask = true)
+          expectWSCloseFrame(Protocol.CloseCodes.Regular)
+
+          closeNetworkInput()
+          expectNetworkClose()
+        }
+      }
+
+      "negotiate permessage-deflate from repeated extension headers" in 
Utils.assertAllStagesStopped {
+        new TestSetup {
+          sendWebSocketRequest(
+            "Sec-WebSocket-Extensions: unknown-extension\r\n" +
+            "Sec-WebSocket-Extensions: permessage-deflate\r\n")
+
+          val request = expectRequest()
+          val upgrade = request.attribute(webSocketUpgrade)
+          val response = upgrade.get.handleMessages(Flow[Message])
+          responses.sendNext(response)
+
+          expectResponseWithWipedDate(
+            """HTTP/1.1 101 Switching Protocols
+              |Upgrade: websocket
+              |Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
+              |Sec-WebSocket-Extensions: permessage-deflate
+              |Server: pekko-http/test
+              |Date: XXXX
+              |Connection: upgrade
+              |
+              |""")
+
+          sendWSCloseFrame(Protocol.CloseCodes.Regular, mask = true)
+          expectWSCloseFrame(Protocol.CloseCodes.Regular)
+
+          closeNetworkInput()
+          expectNetworkClose()
+        }
+      }
+
+      "negotiate permessage-deflate from a fallback offer in the same 
extension header" in
+      Utils.assertAllStagesStopped {
+        new TestSetup {
+          sendWebSocketRequest(
+            "Sec-WebSocket-Extensions: permessage-deflate; 
server_max_window_bits=14, permessage-deflate\r\n")
+
+          val request = expectRequest()
+          val upgrade = request.attribute(webSocketUpgrade)
+          val response = upgrade.get.handleMessages(Flow[Message])
+          responses.sendNext(response)
+
+          expectResponseWithWipedDate(
+            """HTTP/1.1 101 Switching Protocols
+              |Upgrade: websocket
+              |Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
+              |Sec-WebSocket-Extensions: permessage-deflate
+              |Server: pekko-http/test
+              |Date: XXXX
+              |Connection: upgrade
+              |
+              |""")
+
+          sendWSCloseFrame(Protocol.CloseCodes.Regular, mask = true)
+          expectWSCloseFrame(Protocol.CloseCodes.Regular)
+
+          closeNetworkInput()
+          expectNetworkClose()
+        }
+      }
+
+      "inflate inbound permessage-deflate messages before passing them to the 
application" in
+      Utils.assertAllStagesStopped {
+        new TestSetup {
+          sendWebSocketRequest("Sec-WebSocket-Extensions: 
permessage-deflate\r\n")
+
+          val request = expectRequest()
+          val upgrade = request.attribute(webSocketUpgrade)
+          val response = upgrade.get.handleMessages(Flow[Message])
+          responses.sendNext(response)
+
+          expectResponseWithWipedDate(
+            """HTTP/1.1 101 Switching Protocols
+              |Upgrade: websocket
+              |Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
+              |Sec-WebSocket-Extensions: permessage-deflate
+              |Server: pekko-http/test
+              |Date: XXXX
+              |Connection: upgrade
+              |
+              |""")
+
+          sendWSFrame(Protocol.Opcode.Text, 
deflatePerMessage(ByteString("compressed client message")), fin = true,
+            mask = true, rsv1 = true)
+          expectCompressedTextFrame("compressed client message")
+
+          sendWSCloseFrame(Protocol.CloseCodes.Regular, mask = true)
+          expectWSCloseFrame(Protocol.CloseCodes.Regular)
+
+          closeNetworkInput()
+          expectNetworkClose()
+        }
+      }
+
+      "inflate empty inbound permessage-deflate messages before passing them 
to the application" in
+      Utils.assertAllStagesStopped {
+        new TestSetup {
+          sendWebSocketRequest("Sec-WebSocket-Extensions: 
permessage-deflate\r\n")
+
+          val request = expectRequest()
+          val upgrade = request.attribute(webSocketUpgrade)
+          val response = upgrade.get.handleMessages(Flow[Message])
+          responses.sendNext(response)
+
+          expectResponseWithWipedDate(
+            """HTTP/1.1 101 Switching Protocols
+              |Upgrade: websocket
+              |Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
+              |Sec-WebSocket-Extensions: permessage-deflate
+              |Server: pekko-http/test
+              |Date: XXXX
+              |Connection: upgrade
+              |
+              |""")
+
+          sendWSFrame(Protocol.Opcode.Text, EmptyDeflateBlock, fin = true, 
mask = true, rsv1 = true)
+          expectCompressedTextFrame("")
+
+          sendWSCloseFrame(Protocol.CloseCodes.Regular, mask = true)
+          expectWSCloseFrame(Protocol.CloseCodes.Regular)
+
+          closeNetworkInput()
+          expectNetworkClose()
+        }
+      }
+
+      "pass uncompressed inbound messages through when permessage-deflate is 
negotiated" in
+      Utils.assertAllStagesStopped {
+        new TestSetup {
+          sendWebSocketRequest("Sec-WebSocket-Extensions: 
permessage-deflate\r\n")
+
+          val request = expectRequest()
+          val upgrade = request.attribute(webSocketUpgrade)
+          val response = upgrade.get.handleMessages(Flow[Message])
+          responses.sendNext(response)
+
+          expectResponseWithWipedDate(
+            """HTTP/1.1 101 Switching Protocols
+              |Upgrade: websocket
+              |Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
+              |Sec-WebSocket-Extensions: permessage-deflate
+              |Server: pekko-http/test
+              |Date: XXXX
+              |Connection: upgrade
+              |
+              |""")
+
+          sendWSFrame(Protocol.Opcode.Text, ByteString("plain client 
message"), fin = true, mask = true)
+          expectCompressedTextFrame("plain client message")
+
+          sendWSCloseFrame(Protocol.CloseCodes.Regular, mask = true)
+          expectWSCloseFrame(Protocol.CloseCodes.Regular)
+
+          closeNetworkInput()
+          expectNetworkClose()
+        }
+      }
+
+      "handle fragmented compressed messages with interleaved control frames" 
in Utils.assertAllStagesStopped {
+        new TestSetup {
+          sendWebSocketRequest("Sec-WebSocket-Extensions: 
permessage-deflate\r\n")
+
+          val request = expectRequest()
+          val upgrade = request.attribute(webSocketUpgrade)
+          val response = upgrade.get.handleMessages(Flow[Message])
+          responses.sendNext(response)
+
+          expectResponseWithWipedDate(
+            """HTTP/1.1 101 Switching Protocols
+              |Upgrade: websocket
+              |Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
+              |Sec-WebSocket-Extensions: permessage-deflate
+              |Server: pekko-http/test
+              |Date: XXXX
+              |Connection: upgrade
+              |
+              |""")
+
+          val (firstPart, secondPart) =
+            deflatePerMessageFrames(ByteString("fragmented compressed client 
message"), splitAt = 16)
+
+          sendWSFrame(Protocol.Opcode.Text, firstPart, fin = false, mask = 
true, rsv1 = true)
+          sendWSFrame(Protocol.Opcode.Ping, ByteString("ping"), fin = true, 
mask = true)
+          val firstResponse = expectCompressedFrame(Protocol.Opcode.Text, fin 
= false, rsv1 = true)
+          expectWSFrame(Protocol.Opcode.Pong, ByteString("ping"), fin = true)
+          sendWSFrame(Protocol.Opcode.Continuation, secondPart, fin = true, 
mask = true)
+
+          val secondResponse = 
expectCompressedFrame(Protocol.Opcode.Continuation, fin = false, rsv1 = false)
+          val finalResponse = 
expectCompressedFrame(Protocol.Opcode.Continuation, fin = true, rsv1 = false)
+          inflatePerMessageFrames(Seq(firstResponse, secondResponse, 
finalResponse)).utf8String shouldEqual
+          "fragmented compressed client message"
+
+          sendWSCloseFrame(Protocol.CloseCodes.Regular, mask = true)
+          expectWSCloseFrame(Protocol.CloseCodes.Regular)
+
+          closeNetworkInput()
+          expectNetworkClose()
+        }
+      }
+
+      "not finish fragmented compressed messages at split frame data 
boundaries" in Utils.assertAllStagesStopped {
+        new TestSetup {
+          sendWebSocketRequest("Sec-WebSocket-Extensions: 
permessage-deflate\r\n")
+
+          val request = expectRequest()
+          val upgrade = request.attribute(webSocketUpgrade)
+          val response = upgrade.get.handleMessages(Flow[Message])
+          responses.sendNext(response)
+
+          expectResponseWithWipedDate(
+            """HTTP/1.1 101 Switching Protocols
+              |Upgrade: websocket
+              |Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
+              |Sec-WebSocket-Extensions: permessage-deflate
+              |Server: pekko-http/test
+              |Date: XXXX
+              |Connection: upgrade
+              |
+              |""")
+
+          val (firstFrame, secondFrame) =
+            deflatePerMessageFrames(ByteString("split frame data compressed 
client message"), splitAt = 22)
+
+          sendWSFrameInTwoNetworkChunks(Protocol.Opcode.Text, firstFrame, fin 
= false, mask = true, rsv1 = true)
+          sendWSFrame(Protocol.Opcode.Continuation, secondFrame, fin = true, 
mask = true)
+
+          val firstResponse = expectCompressedFrame(Protocol.Opcode.Text, fin 
= false, rsv1 = true)
+          val secondResponse = 
expectCompressedFrame(Protocol.Opcode.Continuation, fin = false, rsv1 = false)
+          val finalResponse = 
expectCompressedFrame(Protocol.Opcode.Continuation, fin = true, rsv1 = false)
+          inflatePerMessageFrames(Seq(firstResponse, secondResponse, 
finalResponse)).utf8String shouldEqual
+          "split frame data compressed client message"
+
+          sendWSCloseFrame(Protocol.CloseCodes.Regular, mask = true)
+          expectWSCloseFrame(Protocol.CloseCodes.Regular)
+
+          closeNetworkInput()
+          expectNetworkClose()
+        }
+      }
+
+      "finish fragmented compressed messages when the last fragment only 
completes the inflater" in
+      Utils.assertAllStagesStopped {
+        new TestSetup {
+          sendWebSocketRequest("Sec-WebSocket-Extensions: 
permessage-deflate\r\n")
+
+          val request = expectRequest()
+          val upgrade = request.attribute(webSocketUpgrade)
+          val response = upgrade.get.handleMessages(Flow[Message])
+          responses.sendNext(response)
+
+          expectResponseWithWipedDate(
+            """HTTP/1.1 101 Switching Protocols
+              |Upgrade: websocket
+              |Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
+              |Sec-WebSocket-Extensions: permessage-deflate
+              |Server: pekko-http/test
+              |Date: XXXX
+              |Connection: upgrade
+              |
+              |""")
+
+          val payload =
+            ByteString.fromArrayUnsafe(hexToBytes(
+              
"677170647a777a737574656b707a787a6f6a7561756578756f6b7868616371716c657a6d64697479766d726f6"
 +
+              
"269746c6376777464776f6f72767a726f64667278676764687775786f6762766d776d706b76697773777a7072"
 +
+              
"6a6a737279707a7078697a6c69616d7461656d646278626d786f66666e686e776a7a7461746d7a776668776b6"
 +
+              
"f6f736e73746575637a6d727a7175707a6e74627578687871767771697a71766c64626d78726d6d7675756877"
 +
+              
"62667963626b687a726d676e646263776e67797264706d6c6863626577616967706a78636a72697464756e627"
 +
+              
"977616f79736475676f76736f7178746a7a7479626c64636b6b6778637768746c62"))
+          val frames = deflatePerMessageFrames(payload, compressionLevel = 9, 
fragmentCount = 4)
+          frames.last.length shouldEqual 1
+          frames.last.head shouldEqual 1
+
+          sendWSFrame(Protocol.Opcode.Text, frames(0), fin = false, mask = 
true, rsv1 = true)
+          sendWSFrame(Protocol.Opcode.Continuation, frames(1), fin = false, 
mask = true)
+          sendWSFrame(Protocol.Opcode.Continuation, frames(2), fin = false, 
mask = true)
+          sendWSFrame(Protocol.Opcode.Continuation, frames(3), fin = true, 
mask = true)
+
+          val firstResponse = expectCompressedFrame(Protocol.Opcode.Text, fin 
= false, rsv1 = true)
+          inflatePerMessageFrames(firstResponse +: 
expectCompressedContinuationFramesUntilFinal()) shouldEqual payload
+
+          sendWSCloseFrame(Protocol.CloseCodes.Regular, mask = true)
+          expectWSCloseFrame(Protocol.CloseCodes.Regular)
+
+          closeNetworkInput()
+          expectNetworkClose()
+        }
+      }
+
+      "deflate outbound messages sent by the application" in 
Utils.assertAllStagesStopped {
+        new TestSetup {
+          sendWebSocketRequest("Sec-WebSocket-Extensions: 
permessage-deflate\r\n")
+
+          val request = expectRequest()
+          val upgrade = request.attribute(webSocketUpgrade)
+          val response = upgrade.get.handleMessages(
+            Flow.fromSinkAndSource(Sink.ignore, 
Source.single(TextMessage.Strict("compressed server message"))))
+          responses.sendNext(response)
+
+          expectResponseWithWipedDate(
+            """HTTP/1.1 101 Switching Protocols
+              |Upgrade: websocket
+              |Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
+              |Sec-WebSocket-Extensions: permessage-deflate
+              |Server: pekko-http/test
+              |Date: XXXX
+              |Connection: upgrade
+              |
+              |""")
+
+          expectCompressedTextFrame("compressed server message")
+          expectWSCloseFrame(Protocol.CloseCodes.Regular)
+
+          sendWSCloseFrame(Protocol.CloseCodes.Regular, mask = true)
+          closeNetworkInput()
+          expectNetworkClose()
+        }
+      }
+
+      "deflate empty outbound messages sent by the application" in 
Utils.assertAllStagesStopped {
+        new TestSetup {
+          sendWebSocketRequest("Sec-WebSocket-Extensions: 
permessage-deflate\r\n")
+
+          val request = expectRequest()
+          val upgrade = request.attribute(webSocketUpgrade)
+          val response = upgrade.get.handleMessages(
+            Flow.fromSinkAndSource(Sink.ignore, 
Source.single(TextMessage.Strict(""))))
+          responses.sendNext(response)
+
+          expectResponseWithWipedDate(
+            """HTTP/1.1 101 Switching Protocols
+              |Upgrade: websocket
+              |Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
+              |Sec-WebSocket-Extensions: permessage-deflate
+              |Server: pekko-http/test
+              |Date: XXXX
+              |Connection: upgrade
+              |
+              |""")
+
+          val payload = expectCompressedFrame(Protocol.Opcode.Text, fin = 
true, rsv1 = true)
+          payload shouldEqual EmptyDeflateBlock
+          inflatePerMessage(payload) shouldEqual ByteString.empty
+          expectWSCloseFrame(Protocol.CloseCodes.Regular)
+
+          sendWSCloseFrame(Protocol.CloseCodes.Regular, mask = true)
+          closeNetworkInput()
+          expectNetworkClose()
+        }
+      }
+
+      "deflate streamed outbound messages without aggregating the full 
message" in Utils.assertAllStagesStopped {
+        new TestSetup {
+          sendWebSocketRequest("Sec-WebSocket-Extensions: 
permessage-deflate\r\n")
+
+          val request = expectRequest()
+          val upgrade = request.attribute(webSocketUpgrade)
+          val response = upgrade.get.handleMessages(
+            Flow.fromSinkAndSource(
+              Sink.ignore,
+              Source.single(TextMessage(Source(List("streamed ", "server ", 
"message"))))))
+          responses.sendNext(response)
+
+          expectResponseWithWipedDate(
+            """HTTP/1.1 101 Switching Protocols
+              |Upgrade: websocket
+              |Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
+              |Sec-WebSocket-Extensions: permessage-deflate
+              |Server: pekko-http/test
+              |Date: XXXX
+              |Connection: upgrade
+              |
+              |""")
+
+          val firstResponse = expectCompressedFrame(Protocol.Opcode.Text, fin 
= false, rsv1 = true)
+          val secondResponse = 
expectCompressedFrame(Protocol.Opcode.Continuation, fin = false, rsv1 = false)
+          val thirdResponse = 
expectCompressedFrame(Protocol.Opcode.Continuation, fin = false, rsv1 = false)
+          val finalResponse = 
expectCompressedFrame(Protocol.Opcode.Continuation, fin = true, rsv1 = false)
+          inflatePerMessageFrames(Seq(firstResponse, secondResponse, 
thirdResponse,
+            finalResponse)).utf8String shouldEqual
+          "streamed server message"
+          expectWSCloseFrame(Protocol.CloseCodes.Regular)
+
+          sendWSCloseFrame(Protocol.CloseCodes.Regular, mask = true)
+          closeNetworkInput()
+          expectNetworkClose()
+        }
+      }
+
+      "fail invalid compressed messages with a protocol error" in 
Utils.assertAllStagesStopped {
+        new TestSetup {
+          sendWebSocketRequest("Sec-WebSocket-Extensions: 
permessage-deflate\r\n")
+
+          val request = expectRequest()
+          val upgrade = request.attribute(webSocketUpgrade)
+          val response = upgrade.get.handleMessages(Flow[Message])
+          responses.sendNext(response)
+
+          expectResponseWithWipedDate(
+            """HTTP/1.1 101 Switching Protocols
+              |Upgrade: websocket
+              |Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
+              |Sec-WebSocket-Extensions: permessage-deflate
+              |Server: pekko-http/test
+              |Date: XXXX
+              |Connection: upgrade
+              |
+              |""")
+
+          sendWSFrame(Protocol.Opcode.Text, ByteString(0xFF, 0xFF, 0xFF), fin 
= true, mask = true, rsv1 = true)
+          expectWSCloseFrame(Protocol.CloseCodes.ProtocolError)
+
+          closeNetworkInput()
+          expectNetworkClose()
+        }
+      }
+
+      "fail non-continuation frames during fragmented compressed messages with 
a protocol error" in
+      Utils.assertAllStagesStopped {
+        new TestSetup {
+          sendWebSocketRequest("Sec-WebSocket-Extensions: 
permessage-deflate\r\n")
+
+          val request = expectRequest()
+          val upgrade = request.attribute(webSocketUpgrade)
+          val response = upgrade.get.handleMessages(Flow[Message])
+          responses.sendNext(response)
+
+          expectResponseWithWipedDate(
+            """HTTP/1.1 101 Switching Protocols
+              |Upgrade: websocket
+              |Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
+              |Sec-WebSocket-Extensions: permessage-deflate
+              |Server: pekko-http/test
+              |Date: XXXX
+              |Connection: upgrade
+              |
+              |""")
+
+          val (firstPart, _) = deflatePerMessageFrames(ByteString("fragmented 
compressed client message"), splitAt = 16)
+
+          sendWSFrame(Protocol.Opcode.Text, firstPart, fin = false, mask = 
true, rsv1 = true)
+          sendWSFrame(Protocol.Opcode.Text, ByteString("not a continuation"), 
fin = true, mask = true)
+          expectCompressedFrame(Protocol.Opcode.Text, fin = false, rsv1 = true)
+          expectWSCloseFrame(Protocol.CloseCodes.ProtocolError)
+
+          closeNetworkInput()
+          expectNetworkClose()
+        }
+      }
+
+      "not negotiate permessage-deflate when websocket compression is 
disabled" in Utils.assertAllStagesStopped {
+        new TestSetup {
+          override def settings = {
+            val defaults = 
super.settings.websocketSettings.asInstanceOf[WebSocketSettingsImpl]
+            super.settings.withWebsocketSettings(
+              defaults.copy(compression = defaults.compression.copy(enabled = 
false)))
+          }
+
+          sendWebSocketRequest("Sec-WebSocket-Extensions: 
permessage-deflate\r\n")
+
+          val request = expectRequest()
+          val upgrade = request.attribute(webSocketUpgrade)
+          val response = upgrade.get.handleMessages(Flow[Message])
+          responses.sendNext(response)
+
+          expectResponseWithWipedDate(
+            """HTTP/1.1 101 Switching Protocols
+              |Upgrade: websocket
+              |Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
+              |Server: pekko-http/test
+              |Date: XXXX
+              |Connection: upgrade
+              |
+              |""")
+
+          sendWSCloseFrame(Protocol.CloseCodes.Regular, mask = true)
+          expectWSCloseFrame(Protocol.CloseCodes.Regular)
+
+          closeNetworkInput()
+          expectNetworkClose()
+        }
+      }
+
+      "not negotiate permessage-deflate when compression is disabled for the 
accepted WebSocket" in
+      Utils.assertAllStagesStopped {
+        new TestSetup {
+          sendWebSocketRequest("Sec-WebSocket-Extensions: 
permessage-deflate\r\n")
+
+          val request = expectRequest()
+          val upgrade = request.attribute(webSocketUpgrade)
+          val response = upgrade.get.handleMessages(
+            Flow.fromSinkAndSource(Sink.ignore, 
Source.single(TextMessage.Strict("plain server message"))),
+            None,
+            compressionEnabled = false)
+          responses.sendNext(response)
+
+          expectResponseWithWipedDate(
+            """HTTP/1.1 101 Switching Protocols
+              |Upgrade: websocket
+              |Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
+              |Server: pekko-http/test
+              |Date: XXXX
+              |Connection: upgrade
+              |
+              |""")
+
+          expectWSFrame(Protocol.Opcode.Text, ByteString("plain server 
message"), fin = true)
+          expectWSCloseFrame(Protocol.CloseCodes.Regular)
+
+          sendWSCloseFrame(Protocol.CloseCodes.Regular, mask = true)
+          closeNetworkInput()
+          expectNetworkClose()
+        }
+      }
+
+      "negotiate configured permessage-deflate parameters" in 
Utils.assertAllStagesStopped {
+        new TestSetup {
+          override def settings = {
+            val defaults = 
super.settings.websocketSettings.asInstanceOf[WebSocketSettingsImpl]
+            super.settings.withWebsocketSettings(
+              defaults.copy(compression = defaults.compression.copy(
+                preferredClientWindowSize = 11,
+                allowServerNoContext = true,
+                preferredClientNoContext = true)))
+          }
+
+          sendWebSocketRequest(
+            "Sec-WebSocket-Extensions: permessage-deflate; 
client_max_window_bits; server_max_window_bits=15; " +
+            "client_no_context_takeover; server_no_context_takeover\r\n")
+
+          val request = expectRequest()
+          val upgrade = request.attribute(webSocketUpgrade)
+          val response = upgrade.get.handleMessages(Flow[Message])
+          responses.sendNext(response)
+
+          expectResponseWithWipedDate(
+            """HTTP/1.1 101 Switching Protocols
+              |Upgrade: websocket
+              |Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
+              |Sec-WebSocket-Extensions: permessage-deflate; 
client_max_window_bits=11; server_max_window_bits=15; 
client_no_context_takeover; server_no_context_takeover
+              |Server: pekko-http/test
+              |Date: XXXX
+              |Connection: upgrade
+              |
+              |""")
+
+          sendWSCloseFrame(Protocol.CloseCodes.Regular, mask = true)
+          expectWSCloseFrame(Protocol.CloseCodes.Regular)
+
+          closeNetworkInput()
+          expectNetworkClose()
+        }
+      }
+
+      "negotiate explicit client_max_window_bits values" in 
Utils.assertAllStagesStopped {
+        new TestSetup {
+          sendWebSocketRequest("Sec-WebSocket-Extensions: permessage-deflate; 
client_max_window_bits=12\r\n")
+
+          val request = expectRequest()
+          val upgrade = request.attribute(webSocketUpgrade)
+          val response = upgrade.get.handleMessages(Flow[Message])
+          responses.sendNext(response)
+
+          expectResponseWithWipedDate(
+            """HTTP/1.1 101 Switching Protocols
+              |Upgrade: websocket
+              |Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
+              |Sec-WebSocket-Extensions: permessage-deflate; 
client_max_window_bits=12
+              |Server: pekko-http/test
+              |Date: XXXX
+              |Connection: upgrade
+              |
+              |""")
+
+          sendWSCloseFrame(Protocol.CloseCodes.Regular, mask = true)
+          expectWSCloseFrame(Protocol.CloseCodes.Regular)
+
+          closeNetworkInput()
+          expectNetworkClose()
+        }
+      }
+
+      "not negotiate invalid client_max_window_bits values" in 
Utils.assertAllStagesStopped {
+        new TestSetup {
+          sendWebSocketRequest("Sec-WebSocket-Extensions: permessage-deflate; 
client_max_window_bits=7\r\n")
+
+          val request = expectRequest()
+          val upgrade = request.attribute(webSocketUpgrade)
+          val response = upgrade.get.handleMessages(Flow[Message])
+          responses.sendNext(response)
+
+          expectResponseWithWipedDate(
+            """HTTP/1.1 101 Switching Protocols
+              |Upgrade: websocket
+              |Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
+              |Server: pekko-http/test
+              |Date: XXXX
+              |Connection: upgrade
+              |
+              |""")
+
+          sendWSCloseFrame(Protocol.CloseCodes.Regular, mask = true)
+          expectWSCloseFrame(Protocol.CloseCodes.Regular)
+
+          closeNetworkInput()
+          expectNetworkClose()
+        }
+      }
+
+      "not negotiate client_no_context_takeover when it was not requested by 
the client" in
+      Utils.assertAllStagesStopped {
+        new TestSetup {
+          override def settings = {
+            val defaults = 
super.settings.websocketSettings.asInstanceOf[WebSocketSettingsImpl]
+            super.settings.withWebsocketSettings(
+              defaults.copy(compression = 
defaults.compression.copy(preferredClientNoContext = true)))
+          }
+
+          sendWebSocketRequest("Sec-WebSocket-Extensions: 
permessage-deflate\r\n")
+
+          val request = expectRequest()
+          val upgrade = request.attribute(webSocketUpgrade)
+          val response = upgrade.get.handleMessages(Flow[Message])
+          responses.sendNext(response)
+
+          expectResponseWithWipedDate(
+            """HTTP/1.1 101 Switching Protocols
+              |Upgrade: websocket
+              |Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
+              |Sec-WebSocket-Extensions: permessage-deflate
+              |Server: pekko-http/test
+              |Date: XXXX
+              |Connection: upgrade
+              |
+              |""")
+
+          sendWSCloseFrame(Protocol.CloseCodes.Regular, mask = true)
+          expectWSCloseFrame(Protocol.CloseCodes.Regular)
+
+          closeNetworkInput()
+          expectNetworkClose()
+        }
+      }
+
+      "not negotiate permessage-deflate when server_no_context_takeover is not 
allowed" in
+      Utils.assertAllStagesStopped {
+        new TestSetup {
+          sendWebSocketRequest("Sec-WebSocket-Extensions: permessage-deflate; 
server_no_context_takeover\r\n")
+
+          val request = expectRequest()
+          val upgrade = request.attribute(webSocketUpgrade)
+          val response = upgrade.get.handleMessages(Flow[Message])
+          responses.sendNext(response)
+
+          expectResponseWithWipedDate(
+            """HTTP/1.1 101 Switching Protocols
+              |Upgrade: websocket
+              |Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
+              |Server: pekko-http/test
+              |Date: XXXX
+              |Connection: upgrade
+              |
+              |""")
+
+          sendWSCloseFrame(Protocol.CloseCodes.Regular, mask = true)
+          expectWSCloseFrame(Protocol.CloseCodes.Regular)
+
+          closeNetworkInput()
+          expectNetworkClose()
+        }
+      }
+
+      "reset the server compressor when server_no_context_takeover is 
negotiated" in Utils.assertAllStagesStopped {
+        new TestSetup {
+          override def settings = {
+            val defaults = 
super.settings.websocketSettings.asInstanceOf[WebSocketSettingsImpl]
+            super.settings.withWebsocketSettings(
+              defaults.copy(compression = 
defaults.compression.copy(allowServerNoContext = true)))
+          }
+
+          sendWebSocketRequest("Sec-WebSocket-Extensions: permessage-deflate; 
server_no_context_takeover\r\n")
+
+          val request = expectRequest()
+          val upgrade = request.attribute(webSocketUpgrade)
+          val message = "same server message same server message same server 
message"
+          val response = upgrade.get.handleMessages(
+            Flow.fromSinkAndSource(
+              Sink.ignore,
+              Source(List(TextMessage.Strict(message), 
TextMessage.Strict(message)))))
+          responses.sendNext(response)
+
+          expectResponseWithWipedDate(
+            """HTTP/1.1 101 Switching Protocols
+              |Upgrade: websocket
+              |Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
+              |Sec-WebSocket-Extensions: permessage-deflate; 
server_no_context_takeover
+              |Server: pekko-http/test
+              |Date: XXXX
+              |Connection: upgrade
+              |
+              |""")
+
+          val firstPayload = expectCompressedFrame(Protocol.Opcode.Text, fin = 
true, rsv1 = true)
+          val secondPayload = expectCompressedFrame(Protocol.Opcode.Text, fin 
= true, rsv1 = true)
+          firstPayload shouldEqual deflatePerMessage(ByteString(message))
+          secondPayload shouldEqual firstPayload
+          inflatePerMessage(firstPayload).utf8String shouldEqual message
+          inflatePerMessage(secondPayload).utf8String shouldEqual message
+          expectWSCloseFrame(Protocol.CloseCodes.Regular)
+
+          sendWSCloseFrame(Protocol.CloseCodes.Regular, mask = true)
+          closeNetworkInput()
+          expectNetworkClose()
+        }
+      }
+
+      "reset the server decompressor when client_no_context_takeover is 
negotiated" in Utils.assertAllStagesStopped {
+        new TestSetup {
+          override def settings = {
+            val defaults = 
super.settings.websocketSettings.asInstanceOf[WebSocketSettingsImpl]
+            super.settings.withWebsocketSettings(
+              defaults.copy(compression = 
defaults.compression.copy(preferredClientNoContext = true)))
+          }
+
+          sendWebSocketRequest("Sec-WebSocket-Extensions: permessage-deflate; 
client_no_context_takeover\r\n")
+
+          val request = expectRequest()
+          val upgrade = request.attribute(webSocketUpgrade)
+          val response = upgrade.get.handleMessages(Flow[Message])
+          responses.sendNext(response)
+
+          expectResponseWithWipedDate(
+            """HTTP/1.1 101 Switching Protocols
+              |Upgrade: websocket
+              |Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
+              |Sec-WebSocket-Extensions: permessage-deflate; 
client_no_context_takeover
+              |Server: pekko-http/test
+              |Date: XXXX
+              |Connection: upgrade
+              |
+              |""")
+
+          val Seq(firstPayload, secondPayload) = deflatePerMessagesWithContext(
+            ByteString("same client message same client message same client 
message"),
+            ByteString("same client message same client message same client 
message"))
+
+          sendWSFrame(Protocol.Opcode.Text, firstPayload, fin = true, mask = 
true, rsv1 = true)
+          expectCompressedTextFrame("same client message same client message 
same client message")
+          sendWSFrame(Protocol.Opcode.Text, secondPayload, fin = true, mask = 
true, rsv1 = true)
+          expectWSCloseFrame(Protocol.CloseCodes.ProtocolError)
+
+          closeNetworkInput()
+          expectNetworkClose()
+        }
+      }
+
+      "not compress server control frames" in Utils.assertAllStagesStopped {
+        new TestSetup {
+          sendWebSocketRequest("Sec-WebSocket-Extensions: 
permessage-deflate\r\n")
+
+          val request = expectRequest()
+          val upgrade = request.attribute(webSocketUpgrade)
+          val response = 
upgrade.get.asInstanceOf[UpgradeToWebSocketLowLevel].handleFrames(
+            Flow.fromSinkAndSource(
+              Sink.ignore,
+              Source.single(FrameEvent.fullFrame(Protocol.Opcode.Ping, None, 
ByteString("server ping"), fin = true))))
+          responses.sendNext(response)
+
+          expectResponseWithWipedDate(
+            """HTTP/1.1 101 Switching Protocols
+              |Upgrade: websocket
+              |Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
+              |Sec-WebSocket-Extensions: permessage-deflate
+              |Server: pekko-http/test
+              |Date: XXXX
+              |Connection: upgrade
+              |
+              |""")
+
+          expectWSFrame(Protocol.Opcode.Ping, ByteString("server ping"), fin = 
true)
+          netOut.expectComplete()
+
+          sendWSCloseFrame(Protocol.CloseCodes.Regular, mask = true)
+          closeNetworkInput()
+        }
+      }
+
+      "fail low-level outbound data frames with reserved bits when compression 
is negotiated" in
+      Utils.assertAllStagesStopped {
+        new TestSetup {
+          sendWebSocketRequest("Sec-WebSocket-Extensions: 
permessage-deflate\r\n")
+
+          val request = expectRequest()
+          val upgrade = request.attribute(webSocketUpgrade)
+          val response = 
upgrade.get.asInstanceOf[UpgradeToWebSocketLowLevel].handleFrames(
+            Flow.fromSinkAndSource(
+              Sink.ignore,
+              Source.single(FrameEvent.fullFrame(
+                Protocol.Opcode.Text,
+                None,
+                ByteString("already compressed"),
+                fin = true,
+                rsv1 = true))))
+          responses.sendNext(response)
+
+          expectResponseWithWipedDate(
+            """HTTP/1.1 101 Switching Protocols
+              |Upgrade: websocket
+              |Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
+              |Sec-WebSocket-Extensions: permessage-deflate
+              |Server: pekko-http/test
+              |Date: XXXX
+              |Connection: upgrade
+              |
+              |""")
+
+          netOut.expectError()
+
+          sendWSCloseFrame(Protocol.CloseCodes.Regular, mask = true)
+          closeNetworkInput()
+        }
+      }
+
+      "fail low-level outbound continuation frames with reserved bits when 
compression is negotiated" in
+      Utils.assertAllStagesStopped {
+        new TestSetup {
+          sendWebSocketRequest("Sec-WebSocket-Extensions: 
permessage-deflate\r\n")
+
+          val request = expectRequest()
+          val upgrade = request.attribute(webSocketUpgrade)
+          val response = 
upgrade.get.asInstanceOf[UpgradeToWebSocketLowLevel].handleFrames(
+            Flow.fromSinkAndSource(
+              Sink.ignore,
+              Source(List(
+                FrameEvent.fullFrame(Protocol.Opcode.Text, None, 
ByteString("fragmented "), fin = false),
+                FrameEvent.fullFrame(
+                  Protocol.Opcode.Continuation,
+                  None,
+                  ByteString("message"),
+                  fin = true,
+                  rsv1 = true)))))
+          responses.sendNext(response)
+
+          expectResponseWithWipedDate(
+            """HTTP/1.1 101 Switching Protocols
+              |Upgrade: websocket
+              |Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
+              |Sec-WebSocket-Extensions: permessage-deflate
+              |Server: pekko-http/test
+              |Date: XXXX
+              |Connection: upgrade
+              |
+              |""")
+
+          expectCompressedFrame(Protocol.Opcode.Text, fin = false, rsv1 = true)
+          netOut.expectError()
+
+          sendWSCloseFrame(Protocol.CloseCodes.Regular, mask = true)
+          closeNetworkInput()
+        }
+      }
+
+      "not negotiate unsupported server_max_window_bits values" in 
Utils.assertAllStagesStopped {
+        new TestSetup {
+          sendWebSocketRequest("Sec-WebSocket-Extensions: permessage-deflate; 
server_max_window_bits=14\r\n")
+
+          val request = expectRequest()
+          val upgrade = request.attribute(webSocketUpgrade)
+          val response = upgrade.get.handleMessages(Flow[Message])
+          responses.sendNext(response)
+
+          expectResponseWithWipedDate(
+            """HTTP/1.1 101 Switching Protocols
+              |Upgrade: websocket
+              |Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
+              |Server: pekko-http/test
+              |Date: XXXX
+              |Connection: upgrade
+              |
+              |""")
+
+          sendWSCloseFrame(Protocol.CloseCodes.Regular, mask = true)
+          expectWSCloseFrame(Protocol.CloseCodes.Regular)
+
+          closeNetworkInput()
+          expectNetworkClose()
+        }
+      }
+
+      "fail compressed messages exceeding max-allocation" in 
Utils.assertAllStagesStopped {
+        new TestSetup {
+          override def settings = {
+            val defaults = 
super.settings.websocketSettings.asInstanceOf[WebSocketSettingsImpl]
+            super.settings.withWebsocketSettings(
+              defaults.copy(compression = 
defaults.compression.copy(maxAllocation = 4)))
+          }
+
+          sendWebSocketRequest("Sec-WebSocket-Extensions: 
permessage-deflate\r\n")
+
+          val request = expectRequest()
+          val upgrade = request.attribute(webSocketUpgrade)
+          val response = upgrade.get.handleMessages(Flow[Message])
+          responses.sendNext(response)
+
+          expectResponseWithWipedDate(
+            """HTTP/1.1 101 Switching Protocols
+              |Upgrade: websocket
+              |Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
+              |Sec-WebSocket-Extensions: permessage-deflate
+              |Server: pekko-http/test
+              |Date: XXXX
+              |Connection: upgrade
+              |
+              |""")
+
+          sendWSFrame(Protocol.Opcode.Text, deflatePerMessage(ByteString("too 
large")), fin = true, mask = true,
+            rsv1 = true)
+          expectWSCloseFrame(Protocol.CloseCodes.ProtocolError)
+
+          closeNetworkInput()
+          expectNetworkClose()
+        }
+      }
+
+      "fail fragmented compressed messages exceeding max-allocation 
cumulatively" in Utils.assertAllStagesStopped {
+        new TestSetup {
+          override def settings = {
+            val defaults = 
super.settings.websocketSettings.asInstanceOf[WebSocketSettingsImpl]
+            super.settings.withWebsocketSettings(
+              defaults.copy(compression = 
defaults.compression.copy(maxAllocation = 4)))
+          }
+
+          sendWebSocketRequest("Sec-WebSocket-Extensions: 
permessage-deflate\r\n")
+
+          val request = expectRequest()
+          val upgrade = request.attribute(webSocketUpgrade)
+          val response = upgrade.get.handleMessages(Flow[Message].mapConcat(_ 
=> Nil: List[Message]))
+          responses.sendNext(response)
+
+          expectResponseWithWipedDate(
+            """HTTP/1.1 101 Switching Protocols
+              |Upgrade: websocket
+              |Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
+              |Sec-WebSocket-Extensions: permessage-deflate
+              |Server: pekko-http/test
+              |Date: XXXX
+              |Connection: upgrade
+              |
+              |""")
+
+          val (firstFrame, secondFrame) = 
deflatePerMessageFrames(ByteString("too large"), splitAt = 4)
+
+          sendWSFrame(Protocol.Opcode.Text, firstFrame, fin = false, mask = 
true, rsv1 = true)
+          sendWSFrame(Protocol.Opcode.Continuation, secondFrame, fin = true, 
mask = true)
+          expectWSCloseFrame(Protocol.CloseCodes.ProtocolError)
+
+          closeNetworkInput()
+          expectNetworkClose()
+        }
+      }
+
+      "apply compression to low-level frame handlers" in 
Utils.assertAllStagesStopped {
+        new TestSetup {
+          sendWebSocketRequest("Sec-WebSocket-Extensions: 
permessage-deflate\r\n")
+
+          val request = expectRequest()
+          val upgrade = request.attribute(webSocketUpgrade)
+          val response = 
upgrade.get.asInstanceOf[UpgradeToWebSocketLowLevel].handleFrames(Flow[FrameEvent].map
 {
+            case start @ FrameStart(header, data) =>
+              start.copy(header = header.copy(mask = None), data = 
FrameEventParser.mask(data, header.mask))
+            case other => other
+          })
+          responses.sendNext(response)
+
+          expectResponseWithWipedDate(
+            """HTTP/1.1 101 Switching Protocols
+              |Upgrade: websocket
+              |Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
+              |Sec-WebSocket-Extensions: permessage-deflate
+              |Server: pekko-http/test
+              |Date: XXXX
+              |Connection: upgrade
+              |
+              |""")
+
+          sendWSFrame(Protocol.Opcode.Text, 
deflatePerMessage(ByteString("compressed low-level message")), fin = true,
+            mask = true, rsv1 = true)
+          expectCompressedTextFrame("compressed low-level message")
+
+          sendWSCloseFrame(Protocol.CloseCodes.Regular, mask = true)
+          expectWSCloseFrame(Protocol.CloseCodes.Regular)
+
+          closeNetworkInput()
+          expectNetworkClose()
+        }
+      }
+    }
+
+    "reject invalid WebSocket handshakes" should {
+      "missing `Upgrade: websocket` header" in pending
+      "missing `Connection: upgrade` header" in pending
+      "missing `Sec-WebSocket-Key header" in pending
+      "`Sec-WebSocket-Key` with wrong amount of base64 encoded data" in pending
+      "missing `Sec-WebSocket-Version` header" in pending
+      "unsupported `Sec-WebSocket-Version`" in pending
+    }
+  }
+
+  class TestSetup extends HttpServerTestSetupBase with WSTestSetupBase {
+    implicit def system: ActorSystem = spec.system
+    implicit def materializer: Materializer = spec.materializer
+
+    def expectBytes(length: Int): ByteString = netOut.expectBytes(length)
+    def expectBytes(bytes: ByteString): Unit = netOut.expectBytes(bytes)
+
+    def sendWebSocketRequest(extraHeader: String): Unit =
+      send(
+        s"""GET /chat HTTP/1.1
+           |Host: server.example.com
+           |Upgrade: websocket
+           |Connection: Upgrade
+           |Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
+           |Origin: http://example.com
+           |Sec-WebSocket-Version: 13
+           |$extraHeader
+           |""".stripMargin)
+
+    def sendWSFrameInTwoNetworkChunks(
+        opcode: Protocol.Opcode,
+        data: ByteString,
+        fin: Boolean,
+        mask: Boolean = false,
+        rsv1: Boolean = false): Unit = {
+      val maskValue = if (mask) Some(0x12345678) else None
+      val payload = maskValue match {
+        case Some(value) => WSTestUtils.maskedBytes(data, value)._1
+        case None        => data
+      }
+      val header = WSTestUtils.frameHeader(opcode, data.length, fin, 
maskValue, rsv1 = rsv1)
+      val splitAt = math.max(1, payload.length / 2)
+      send(header ++ payload.take(splitAt))
+      send(payload.drop(splitAt))
+    }
+
+    def expectCompressedTextFrame(message: String): Unit = {
+      val payload = expectCompressedFrame(Protocol.Opcode.Text, fin = true, 
rsv1 = true)
+      payload should not be ByteString(message)
+      inflatePerMessage(payload).utf8String shouldEqual message
+    }
+
+    def expectCompressedFrame(opcode: Protocol.Opcode, fin: Boolean, rsv1: 
Boolean): ByteString = {
+      val header = expectBytes(2)
+      ((header(0) & Protocol.FIN_MASK) != 0) shouldEqual fin
+      ((header(0) & Protocol.RSV1_MASK) != 0) shouldEqual rsv1
+      (header(0) & Protocol.OP_MASK) shouldEqual opcode.code
+      (header(1) & Protocol.MASK_MASK) shouldEqual 0
+      val length = header(1) & 0x7F
+      expectBytes(length)
+    }
+
+    def expectCompressedContinuationFramesUntilFinal(): Seq[ByteString] = {
+      def read(acc: Vector[ByteString]): Vector[ByteString] = {
+        val header = expectBytes(2)
+        val fin = (header(0) & Protocol.FIN_MASK) != 0
+        ((header(0) & Protocol.RSV1_MASK) != 0) shouldEqual false
+        (header(0) & Protocol.OP_MASK) shouldEqual 
Protocol.Opcode.Continuation.code
+        (header(1) & Protocol.MASK_MASK) shouldEqual 0
+        val length = header(1) & 0x7F
+        val payload = expectBytes(length)
+        val next = acc :+ payload
+        if (fin) next else read(next)
+      }
+
+      read(Vector.empty)
+    }
+  }
+
+  private def deflatePerMessage(data: ByteString): ByteString = {
+    val deflater = new Deflater(6, true)
+    try {
+      deflater.setInput(data.toArray)
+      val output = new ByteArrayOutputStream()
+      val buffer = new Array[Byte](256)
+      var count = deflater.deflate(buffer, 0, buffer.length, 
Deflater.SYNC_FLUSH)
+      while (count > 0) {
+        output.write(buffer, 0, count)
+        count = deflater.deflate(buffer, 0, buffer.length, Deflater.SYNC_FLUSH)
+      }
+      val compressed = ByteString.fromArray(output.toByteArray)
+      compressed.dropRight(4)
+    } finally {
+      deflater.end()
+    }
+  }
+
+  private def deflatePerMessageFrames(data: ByteString, splitAt: Int): 
(ByteString, ByteString) = {
+    val deflater = new Deflater(6, true)
+    try {
+      val (first, second) = data.splitAt(splitAt)
+      (deflateFrame(deflater, first, removeTail = false), 
deflateFrame(deflater, second, removeTail = true))
+    } finally {
+      deflater.end()
+    }
+  }
+
+  private def deflatePerMessageFrames(
+      data: ByteString,
+      compressionLevel: Int,
+      fragmentCount: Int): Seq[ByteString] = {
+    val compressed = deflatePerMessage(data, compressionLevel)
+    val fragmentLength = compressed.length / (fragmentCount - 1)
+    (0 until fragmentCount).map { index =>
+      val offset = index * fragmentLength
+      val length = if (index == fragmentCount - 1) compressed.length - offset 
else fragmentLength
+      compressed.slice(offset, offset + length)
+    }
+  }
+
+  private def deflatePerMessagesWithContext(messages: ByteString*): 
Seq[ByteString] = {
+    val deflater = new Deflater(6, true)
+    try messages.map(message => deflateFrame(deflater, message, removeTail = 
true))
+    finally {
+      deflater.end()
+    }
+  }
+
+  private def deflatePerMessage(data: ByteString, compressionLevel: Int): 
ByteString = {
+    val deflater = new Deflater(compressionLevel, true)
+    try deflateFrame(deflater, data, removeTail = true)
+    finally {
+      deflater.end()
+    }
+  }
+
+  private def deflateFrame(deflater: Deflater, data: ByteString, removeTail: 
Boolean): ByteString = {
+    deflater.setInput(data.toArray)
+    val output = new ByteArrayOutputStream()
+    val buffer = new Array[Byte](256)
+    var count = deflater.deflate(buffer, 0, buffer.length, Deflater.SYNC_FLUSH)
+    while (count > 0) {
+      output.write(buffer, 0, count)
+      count = deflater.deflate(buffer, 0, buffer.length, Deflater.SYNC_FLUSH)
+    }
+    val compressed = ByteString.fromArray(output.toByteArray)
+    if (removeTail) compressed.dropRight(4) else compressed
+  }
+
+  private def hexToBytes(hex: String): Array[Byte] =
+    hex.grouped(2).map(Integer.parseInt(_, 16).toByte).toArray
+
+  private def inflatePerMessageFrames(frames: Seq[ByteString]): ByteString = {
+    val inflater = new Inflater(true)
+    try {
+      frames.zipWithIndex.foldLeft(ByteString.empty) {
+        case (inflated, (frame, index)) =>
+          val data =
+            if (index == frames.length - 1) frame ++ ByteString(0x00, 0x00, 
0xFF.toByte, 0xFF.toByte) else frame
+          inflated ++ inflateFrame(inflater, data)
+      }
+    } finally {
+      inflater.end()
+    }
+  }
+
+  private def inflateFrame(inflater: Inflater, data: ByteString): ByteString = 
{
+    inflater.setInput(data.toArray)
+    val output = new ByteArrayOutputStream()
+    val buffer = new Array[Byte](256)
+    var count = inflater.inflate(buffer)
+    while (count > 0) {
+      output.write(buffer, 0, count)
+      count = inflater.inflate(buffer)
+    }
+    ByteString.fromArray(output.toByteArray)
+  }
+
+  private def inflatePerMessage(data: ByteString): ByteString = {
+    val inflater = new Inflater(true)
+    try {
+      inflater.setInput((data ++ ByteString(0x00, 0x00, 0xFF.toByte, 
0xFF.toByte)).toArray)
+      val output = new ByteArrayOutputStream()
+      val buffer = new Array[Byte](256)
+      var count = inflater.inflate(buffer)
+      while (count > 0) {
+        output.write(buffer, 0, count)
+        count = inflater.inflate(buffer)
+      }
+      ByteString.fromArray(output.toByteArray)
+    } finally {
+      inflater.end()
+    }
   }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to