The scenario does not work as expected for a WebSocket server with "permessage-deflate" enabled

I have confirmed that it works in the following environment.

Windows 11 Pro
AzulJDK 21.0.6
gatling 3.13.4
wscat 6.0.1

I set up a WebSocket server with “permessage-deflate” disabled.
This server is simply designed to return a response to a WebSocket message from a client.

When I run a Gatling simulation on this server, messages are sent to the server as expected and messages from the server are received.

Next, I enable “permessage-deflate” on the WebSocket server and run the same simulation,
and Gatling fails without sending WebSocket messages or receiving messages from the server.

I have confirmed that if I use wscat instead of Gatling on the client, I can exchange messages with the WebSocket server.

When creating a simulation for a WebSocket server with “permessage-deflate” disabled/enabled, do I need to modify any definitions?


Below are the scenario definitions used:

val scn = scenario(“S1”)
.exec(
ws(“WSOpen1”).wsName(“wsCon1”).connect(“/”)
.headers(Map(“Sec-WebSocket-Extensions” → “permessage-deflate; client_max_window_bits”))
)
.exec(
ws(“Message1”)
.wsName(“wsCon1”)
.sendText(“”“Message!”“”)
.await(10)(
ws.checkTextMessage(“pMessage1”)
.check(regex(“(.*)”).saveAs(“pMessage1”))
)
)
.exec(
ws(“WSClose1”).wsName(“wsCon1”)close
)

Could you please provide a sample application we can use to reproduce.

The server environment is as follows:

Windows 11 Pro
Python 3.9.13
websockets library 15.0.1

The steps to reproduce the problem are as follows:

  1. Install the websockets library
    pip install websockets==15.0.1

  2. Start a WebSocket server with message compression disabled.
    python CompressionOff.py

  3. Run the simulation sim.CompressionTest.

  4. Stop the WebSocket server with message compression disabled (CompressionOff.py) with Ctrl+C.

  5. Start a WebSocket server with message compression enabled.
    python CompressionOn.py

  6. Modify XXX.XXX.XXX.XXX in CompressionTest.scala to the IP address of the WebSocket server.

  7. Run the simulation sim.CompressionTest.


The contents of the files in the procedure are as follows.

CompressionOff.py

import asyncio
import websockets
async def echo(websocket):
    try:
        async for message in websocket:
            print(f"Res: {message}")
            await websocket.send(f"Res: {message}")
    except Exception as e:
        print(f"Error: {e}")


async def main():   
    extensions = []
    server = await websockets.serve(
        echo,        
        '0.0.0.0',     
        8765,   
        compression=None,     
    )
    print("WebSocket server started at ws://0.0.0.0:8765")
    await server.wait_closed()


loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(main())

CompressionOn.py

import asyncio
import websockets
async def echo(websocket):
    try:
        async for message in websocket:
            print(f"Res: {message}")
            await websocket.send(f"Res: {message}")
    except Exception as e:
        print(f"Error: {e}")


async def main():   
    server = await websockets.serve(
        echo,        
        '0.0.0.0',     
        8765,        
    )
    print("WebSocket server started at ws://0.0.0.0:8765")
    await server.wait_closed()


loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(main())

CompressionTest.scala

package sim

import io.gatling.core.Predef._
import io.gatling.http.Predef._

import scala.concurrent.duration._

class CompressionTest extends Simulation {

  val httpProtocol = http
    .wsBaseUrl("ws://XXX.XXX.XXX.XXX:8765")

  val scn = scenario("S1")
    .exec(
      ws("WSOpen1").wsName("wsCon1").connect("/")
      .headers(Map("Sec-WebSocket-Extensions" -> "permessage-deflate; client_max_window_bits"))
    )
    .exec(
      ws("Message1")
        .wsName("wsCon1")
        .sendText("""Message!""")
        .await(10)(
          ws.checkTextMessage("pMessage1")
            .check(regex("(.*)").saveAs("pMessage1"))
        )
    )
    .exec(
      ws("WSClose1").wsName("wsCon1")close
    )
    .exec(s=>{
      println(s("pMessage1").as[String])
      s
    })

  setUp(
    scn.inject(rampUsers(1) during (1 seconds))
  ).protocols(httpProtocol)

}

I really don’t have good things to say about the developper experience python brings on MacOS…

pip install websockets==15.0.1


error: externally-managed-environment

× This environment is externally managed
╰─> To install Python packages system-wide, try brew install
    xyz, where xyz is the package you are trying to
    install.

    If you wish to install a Python library that isn't in Homebrew,
    use a virtual environment:

    python3 -m venv path/to/venv
    source path/to/venv/bin/activate
    python3 -m pip install xyz

    If you wish to install a Python application that isn't in Homebrew,
    it may be easiest to use 'pipx install xyz', which will manage a
    virtual environment for you. You can install pipx with

    brew install pipx

    You may restore the old behavior of pip by passing
    the '--break-system-packages' flag to pip, or by adding
    'break-system-packages = true' to your pip.conf file. The latter
    will permanently disable this error.

    If you disable this error, we STRONGLY recommend that you additionally
    pass the '--user' flag to pip, or set 'user = true' in your pip.conf
    file. Failure to do this can result in a broken Homebrew installation.

    Read more about this behavior here: <https://peps.python.org/pep-0668/>

note: If you believe this is a mistake, please contact your Python installation or OS distribution provider. You can override this, at the risk of breaking your Python installation or OS, by passing --break-system-packages.
hint: See PEP 668 for the detailed specification.

Thank you for taking the time to try it.

I’m sorry.

It’s difficult to provide a sample application that can be used to reproduce the problem.

Instead, let me confirm the following two points.

  1. Am I correct in understanding that Gatling itself supports sending and receiving messages with WebSocket servers with “permessage-deflate” enabled?

  2. Am I correct in understanding that there is no need to modify the definition in the simulation file depending on whether “permessage-deflate” is enabled/disabled on the WebSocket server side?

I was able to figure out what happens.

The core issue is that your python WebSocket library is enforcing client_max_window_bits=12 on the client.

I can’t speak for other technologies, but almost no Java HTTP client would be compatible with your WebSocket server with this setting. The reason is that the standard java.util.zip.Deflater doesn’t expose a way to tune the window bits setting and uses a value of 15, which is higher than the maximum of 12 your server requires.

In Gatling’s case, you have 2 solutions:

  • configure your python library so it stops requiring a client_max_window_bits. IMO, this is a very bad default as it’s not compatible with other WebSocket implementations. And IMO, this tuning is overkill anyway.
  • add the com.jcraft:jzlib library to your classpath. Netty (Gatling’s underlying IO library) will detect that java.util.zip.Deflater doesn’t support your server’s requirements and will fallback to it. Please note that this pure Java implementation is not as performant as the former.

Side note: we’re going to investigate why we didn’t get an error properly reported.

Thank you for your investigation.

I was able to confirm that the simulation works as expected by not returning client_max_window_bits on the server side.

My goal was to try out Gatling’s WebSocket-related DSL, so my doubts on this topic have been resolved.
Thank you.

Correcting myself: the core issue is netty client code advertising the server that it supports client_max_window_bits while it actually doesn’t unless the jzlib library is in the classpath.
The python server is completely fine and properly implements the specification.

I’m discussing with the Netty project leader on how to fix this. Worst case scenario: we’ll be able to implement a fix on the Gatling side.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.