Skip to content

Enforce WebSocket max message size across fragments - #1085

Open
Kludex wants to merge 3 commits into
mainfrom
fix/websocket-max-message-size
Open

Enforce WebSocket max message size across fragments#1085
Kludex wants to merge 3 commits into
mainfrom
fix/websocket-max-message-size

Conversation

@Kludex

@Kludex Kludex commented Jul 26, 2026

Copy link
Copy Markdown
Member

Summary

  • enforce max_message_size_bytes against cumulative incoming WebSocket message size
  • close with MESSAGE_TOO_BIG and raise WebSocketDisconnect when the limit is exceeded
  • add sync/async tests for oversized complete and fragmented messages

Tests

  • uv run ruff check src/httpx2/httpx2/websockets/_api.py tests/httpx2/websockets/test_api.py
  • uv run ruff format --check src/httpx2/httpx2/websockets/_api.py tests/httpx2/websockets/test_api.py
  • uv run pytest tests/httpx2/websockets/ -q

Review in cubic

@github-actions

github-actions Bot commented Jul 26, 2026

Copy link
Copy Markdown

@codspeed-hq

codspeed-hq Bot commented Jul 26, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 15 untouched benchmarks
⏩ 7 skipped benchmarks1


Comparing fix/websocket-max-message-size (dd649bf) with main (cad5353)

Open in CodSpeed

Footnotes

  1. 7 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Comment thread src/httpx2/httpx2/websockets/_api.py Outdated
@Kludex Kludex changed the title Enforce WebSocket max message size Enforce WebSocket max message size across fragments Jul 26, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3937e2ca12

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +526 to +528
if partial_message_size > max_bytes:
self.close(CloseReason.MESSAGE_TOO_BIG, "Message too big")
self._events.put(WebSocketDisconnect(CloseReason.MESSAGE_TOO_BIG, "Message too big"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update the public contract before rejecting larger messages

With the default configuration, every message over 65,536 bytes now closes the connection, but docs/websockets.md:96-101 still defines max_message_size_bytes as the network read chunk size and explicitly promises that larger messages are reassembled. Applications following that documented contract will unexpectedly receive WebSocketDisconnect; update the public documentation alongside this behavioral change so users know that this value is now a hard message-size limit.

Useful? React with 👍 / 👎.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/httpx2/httpx2/websockets/_api.py">

<violation number="1" location="src/httpx2/httpx2/websockets/_api.py:524">
P3: The new byte-limit behavior is not covered for multibyte text or binary fragments, so the advertised byte-based limit can regress silently for payloads whose character count differs from their UTF-8 byte count. Adding boundary tests for a UTF-8 text message and fragmented binary payload would make the enforcement reliable.</violation>

<violation number="2" location="src/httpx2/httpx2/websockets/_api.py:525">
P2: The `max_bytes` parameter is now used both as the network read chunk size (passed to `self._read_stream`) and as the hard maximum allowed message size (checked here). Previously, `max_message_size_bytes` only controlled the read buffer and larger messages were transparently reassembled from fragments. This dual use silently changes the public contract—existing callers relying on reassembly of messages larger than the read buffer will now receive `WebSocketDisconnect`. Consider introducing a separate parameter (e.g., `max_message_size`) to decouple the size enforcement from the read-buffer size, or at minimum update the documentation in `docs/websockets.md` which still describes `max_message_size_bytes` as the read chunk size.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

self._should_close.set()
if isinstance(event, wsproto.events.Message):
partial_message_size += len(event.data.encode() if isinstance(event.data, str) else event.data)
if partial_message_size > max_bytes:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The max_bytes parameter is now used both as the network read chunk size (passed to self._read_stream) and as the hard maximum allowed message size (checked here). Previously, max_message_size_bytes only controlled the read buffer and larger messages were transparently reassembled from fragments. This dual use silently changes the public contract—existing callers relying on reassembly of messages larger than the read buffer will now receive WebSocketDisconnect. Consider introducing a separate parameter (e.g., max_message_size) to decouple the size enforcement from the read-buffer size, or at minimum update the documentation in docs/websockets.md which still describes max_message_size_bytes as the read chunk size.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/httpx2/httpx2/websockets/_api.py, line 526:

<comment>The `max_bytes` parameter is now used both as the network read chunk size (passed to `self._read_stream`) and as the hard maximum allowed message size (checked here). Previously, `max_message_size_bytes` only controlled the read buffer and larger messages were transparently reassembled from fragments. This dual use silently changes the public contract—existing callers relying on reassembly of messages larger than the read buffer will now receive `WebSocketDisconnect`. Consider introducing a separate parameter (e.g., `max_message_size`) to decouple the size enforcement from the read-buffer size, or at minimum update the documentation in `docs/websockets.md` which still describes `max_message_size_bytes` as the read chunk size.</comment>

<file context>
@@ -520,6 +522,11 @@ def _background_receive(self, max_bytes: int) -> None:
                         self._should_close.set()
                     if isinstance(event, wsproto.events.Message):
+                        partial_message_size += len(event.data.encode() if isinstance(event.data, str) else event.data)
+                        if partial_message_size > max_bytes:
+                            self.close(CloseReason.MESSAGE_TOO_BIG, "Message too big")
+                            self._events.put(WebSocketDisconnect(CloseReason.MESSAGE_TOO_BIG, "Message too big"))
</file context>

if isinstance(event, wsproto.events.CloseConnection):
self._should_close.set()
if isinstance(event, wsproto.events.Message):
partial_message_size += len(event.data.encode() if isinstance(event.data, str) else event.data)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The new byte-limit behavior is not covered for multibyte text or binary fragments, so the advertised byte-based limit can regress silently for payloads whose character count differs from their UTF-8 byte count. Adding boundary tests for a UTF-8 text message and fragmented binary payload would make the enforcement reliable.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/httpx2/httpx2/websockets/_api.py, line 525:

<comment>The new byte-limit behavior is not covered for multibyte text or binary fragments, so the advertised byte-based limit can regress silently for payloads whose character count differs from their UTF-8 byte count. Adding boundary tests for a UTF-8 text message and fragmented binary payload would make the enforcement reliable.</comment>

<file context>
@@ -520,6 +522,11 @@ def _background_receive(self, max_bytes: int) -> None:
                     if isinstance(event, wsproto.events.CloseConnection):
                         self._should_close.set()
                     if isinstance(event, wsproto.events.Message):
+                        partial_message_size += len(event.data.encode() if isinstance(event.data, str) else event.data)
+                        if partial_message_size > max_bytes:
+                            self.close(CloseReason.MESSAGE_TOO_BIG, "Message too big")
</file context>

@Kludex

Kludex commented Jul 27, 2026

Copy link
Copy Markdown
Member Author

Addressed review feedback in dd649bfc:

  • Removed the stray blank line after JSONMode.
  • Updated docs/websockets.md so max_message_size_bytes is documented as a hard incoming message-size limit, including fragmented messages.
  • Expanded fragmented-message tests to cover both binary fragments and multibyte UTF-8 text fragments.

Checks:

  • uv run ruff check tests/httpx2/websockets/test_api.py
  • uv run ruff format --check tests/httpx2/websockets/test_api.py
  • uv run pytest tests/httpx2/websockets/ -q

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant