godotengine / godotengine/godot
WebSocketPeer: Packets buffered before close frame are inaccessible due to STATE_CLOSING check in get_available_packet_count()
- Dominant language
- C++
- Stars
- 117k
- Forks
- 26.8k
- PR merge metrics
- PR metrics pending
Description
### Tested versions
- Reproduced in latest master ([d5edd4a](https://github.com/godotengine/godot/commit/d5edd4a59287679ae390149c0c1c3397aeb5f502)
### System information
Parallels VM, aarch64 Macbook Pro M2
### Issue description
When a WebSocket server sends a data frame followed by a close frame in the same TCP segment, the data frame is received and buffered correctly by
wslay, but becomes inaccessible to GDScript because get_available_packet_count() returns 0 for any state other than STATE_OPEN.
Steps to reproduce
1. Connect to a WebSocket server that sends a message immediately followed by a close frame
2. Call poll() on the WebSocketPeer
3. Call get_available_packet_count() - returns 0 even though data was received
4. The message is lost
Root cause
In wsl_peer.cpp, both get_available_packet_count() and get_packet() check for STATE_OPEN exclusively:
```cpp
int WSLPeer::get_available_packet_count() const {
if (ready_state != STATE_OPEN) {
return 0; // Returns 0 for STATE_CLOSING, losing buffered packets
}
return in_buffer.packets_left();
}
Error WSLPeer::get_packet(const uint8_t **r_buffer, int &r_buffer_size) {
r_buffer_size = 0;
ERR_FAIL_COND_V(ready_state != STATE_OPEN, FAILED); // Fails for STATE_CLOSING
// ...
}
```
When wslay processes a close frame in _wsl_msg_recv_callback(), the state transitions to STATE_CLOSING before poll() returns. Any data frames received
before the close frame are in in_buffer but become inaccessible.
Expected behavior
Packets should remain accessible during STATE_CLOSING (and ideally even after STATE_CLOSED until the buffer is drained). The WebSocket RFC guarantees
message ordering - data frames received before a close frame should be delivered to the application.
Suggested fix
```cpp
int WSLPeer::get_available_packet_count() const {
if (ready_state == STATE_CLOSED || ready_state == STATE_CONNECTING) {
return 0;
}
return in_buffer.packets_left();
}
```
And similarly for get_packet():
```cpp
ERR_FAIL_COND_V(ready_state == STATE_CLOSED || ready_state == STATE_CONNECTING, FAILED);
```
### Steps to reproduce
Server:
`python3 server.py`
Client:
`~/godot/bin/godot.linuxbsd.editor.arm64 --headless -s client.gd`
### Minimal reproduction project (MRP)
- client.gd:
```gdscript
extends SceneTree
## WebSocketPeer bug: Packets lost when close frame arrives with data frame
##
## Run: godot --script client.gd --headless
## (Start server.py first)
var ws: WebSocketPeer
var frames: int = 0
func _init():
print("Connecting to ws://localhost:9876...")
ws = WebSocketPeer.new()
ws.connect_to_url("ws://localhost:9876")
func _process(_delta):
frames += 1
if frames > 300:
print("Timeout")
quit(1)
return true
ws.poll()
var state = ws.get_ready_state()
var packets = ws.get_available_packet_count()
match state:
WebSocketPeer.STATE_OPEN:
if packets > 0:
print("SUCCESS: Received message: ", ws.get_packet().get_string_from_utf8())
WebSocketPeer.STATE_CLOSING:
print("STATE_CLOSING - packets available: ", packets)
# BUG: packets is 0 here even though data was buffered
WebSocketPeer.STATE_CLOSED:
print("STATE_CLOSED - packets available: ", packets)
print("Close code: ", ws.get_close_code())
if packets == 0:
print("BUG: Message was lost!")
quit(0)
return true
return false
```
- server.py
```python
#!/usr/bin/env python3
"""
WebSocket server that sends a message immediately followed by close.
This reproduces a bug where Godot's WebSocketPeer loses the message.
Usage: python3 server.py
"""
import asyncio
import websockets
async def handler(websocket):
print("Client connected")
await asyncio.sleep(0.5) # Let connection stabilize
await websocket.send("Important message before close")
print("Sent message")
# Close immediately - data frame and close frame end up in same TCP segment
await websocket.close(1000, "done")
print("Closed connection")
async def main():
print("WebSocket server on ws://localhost:9876")
async with websockets.serve(handler, "localhost", 9876):
await asyncio.Future()
if __name__ == "__main__":
asyncio.run(main())
```
Contributor guide
Research direction
Start in wsl_peer.cpp, especially get_available_packet_count(), get_packet(), and _wsl_msg_recv_callback(). Reproduce the issue with the provided server.py and client.gd, then trace the state transition when a data frame is followed by a close frame. Done means buffered packets remain readable during closing, with the reproduction receiving the message before reporting the close.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- networking
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 55/100