godotengine / godotengine/godot

WebSocketPeer: Packets buffered before close frame are inaccessible due to STATE_CLOSING check in get_available_packet_count()

Open
#115,384 1 comment 2 reactions 0 assignees View on GitHub
bug topic:network
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.