fix: transmit in single frame when compression enabled#552
fix: transmit in single frame when compression enabled#552DanielleMaywood merged 6 commits intomasterfrom
Conversation
Closes #435 ### Problem When compression was enabled, `Conn.Write` sent messages across many small frames due to the flate library's internal `bufferFlushSize` (240 bytes). Each flush triggered a `writeFrame` call, producing alternating ~236 and 4 byte frames. This broke clients like Unreal Engine that only process one frame per tick. ### Solution `Conn.Write` now compresses the entire message into a buffer first, then transmits it as a single frame. Messages below `flateThreshold` bypass compression and are sent uncompressed in a single frame. For `CompressionContextTakeover` mode, the flateWriter destination is restored after buffered compression to ensure subsequent `Writer()` streaming calls work correctly. ### Changes - **write.go**: Buffer compressed output before transmission - **compress_test.go**: Added regression tests for single-frame behavior and Write/Writer interop
There was a problem hiding this comment.
Pull request overview
This PR fixes a critical bug where compressed WebSocket messages were sent across many small frames instead of a single frame, breaking clients like Unreal Engine that process only one frame per tick. The fix buffers the entire compressed message before transmission.
- Modifies
Conn.Writeto compress messages into a temporary buffer first, then transmit as a single frame - Adds logic to send small messages (below threshold) uncompressed in a single frame
- Restores flateWriter destination after buffered compression to maintain compatibility with subsequent
Writer()streaming calls
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| write.go | Implements single-frame compression by buffering compressed data before transmission; adds flateWriter destination restoration for context takeover mode |
| compress_test.go | Adds regression tests verifying single-frame transmission for both compression modes and tests Write/Writer API interoperability |
The implementation is well-designed and handles the key edge cases correctly:
- Mutex handling: The mutex is properly locked once at the start and unlocked via defer in all code paths
- Buffer management: Uses the buffer pool pattern consistently with proper cleanup via defer
- Context takeover: Correctly restores the flateWriter destination to ensure subsequent
Writer()calls work - Error handling: All error paths return properly wrapped errors
- Test coverage: The tests cover both compression modes, threshold boundaries, and the critical Write→Writer sequence
The code follows the existing conventions in the codebase and the logic is sound. No issues were identified that require changes.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
mafredri
left a comment
There was a problem hiding this comment.
Thanks for the PR!
I've left some comments inline. Main concerns: loss of compression dictionary via Reset, error path safety, and encapsulation.
(PS. Small nit: I'd appreciate it if you also went over comments and evaluated relevance/humanized them.)
- humanize some comments/remove lots of unneeded ones - refactor write - ensure we close?
- compression_test.go: add a `Writer` -> `Write` transition - compression_test.go: Claude sucks at variable names apparently - write.go: make sure to hold a lock 🤦 - write.go: combine two identical if branches - write.go: rename `writeFull` because the name was bad
mafredri
left a comment
There was a problem hiding this comment.
One last issue, and some minor vanity from my side. 😄
Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
Closes #435
Problem
When compression was enabled,
Conn.Writesent messages across many small frames due to the flate library's internalbufferFlushSize(240 bytes). Each flush triggered awriteFramecall, producing alternating ~236 and 4 byte frames. This broke clients like Unreal Engine that only process one frame per tick.Solution
Conn.Writenow compresses the entire message into a buffer first, then transmits it as a single frame. Messages belowflateThresholdbypass compression and are sent uncompressed in a single frame.For
CompressionContextTakeovermode, the flateWriter destination is restored after buffered compression to ensure subsequentWriter()streaming calls work correctly.🤖 PR was written by Claude Code using Opus 4.5 then reviewed by Opus 4.5 and a human 👩🚀