fix(multiplayer): reject websocket caps before upgrade

This commit is contained in:
Josh Creek
2026-09-01 19:35:34 +01:00
parent ad59c5f567
commit 52e3d73678
3 changed files with 35 additions and 8 deletions
+3 -1
View File
@@ -1461,7 +1461,9 @@ Observability redaction now adds content-aware protection on top of denylisted f
The authenticated control-plane WebSocket now caps each player at two
simultaneous connections, releasing capacity on disconnect; this complements
the bounded per-player event queue and prevents connection fan-out from
becoming an unbounded account-level resource cost.
becoming an unbounded account-level resource cost. Over-limit attempts fail
before upgrade with `429 websocket_connection_limited`, rather than becoming
ambiguous post-upgrade disconnects.
Proposal explicit-decline cooldowns are now durable: the declining player is
requeued for recovery, but a subsequent queue create is rejected until the
+7 -6
View File
@@ -161,6 +161,13 @@ func (s *Service) controlPlaneEvent(w http.ResponseWriter, r *http.Request) {
if !ok {
return
}
hub := s.getEventHub()
subscriber := hub.subscribe(playerID)
if subscriber == nil {
writeError(w, http.StatusTooManyRequests, "websocket_connection_limited")
return
}
defer hub.unsubscribe(subscriber)
hijacker, ok := w.(http.Hijacker)
if !ok {
writeError(w, http.StatusNotImplemented, "websocket_unavailable")
@@ -178,12 +185,6 @@ func (s *Service) controlPlaneEvent(w http.ResponseWriter, r *http.Request) {
if err := buffered.Flush(); err != nil {
return
}
subscriber := s.getEventHub().subscribe(playerID)
if subscriber == nil {
return
}
defer s.getEventHub().unsubscribe(subscriber)
var writeMu sync.Mutex
done := make(chan struct{})
go func() {
+25 -1
View File
@@ -1,6 +1,10 @@
package api
import "testing"
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestEventHubCapsConnectionsPerPlayerAndReleasesCapacity(t *testing.T) {
hub := newEventHub()
@@ -20,3 +24,23 @@ func TestEventHubCapsConnectionsPerPlayerAndReleasesCapacity(t *testing.T) {
}
hub.unsubscribe(second)
}
func TestEventConnectionCapReturnsHTTP429BeforeUpgrade(t *testing.T) {
service := &Service{SessionBackend: &sessionBackendSpy{}}
hub := service.getEventHub()
first := hub.subscribe("player-1")
second := hub.subscribe("player-1")
defer hub.unsubscribe(first)
defer hub.unsubscribe(second)
request := httptest.NewRequest(http.MethodGet, "/v1/events", nil)
request.Header.Set("Upgrade", "websocket")
request.Header.Set("Connection", "Upgrade")
request.Header.Set("Sec-WebSocket-Version", "13")
request.Header.Set("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==")
request.Header.Set("Authorization", "Bearer session-1:token-1")
recorder := httptest.NewRecorder()
service.controlPlaneEvent(recorder, request)
if recorder.Code != http.StatusTooManyRequests {
t.Fatalf("connection-cap status = %d, want 429", recorder.Code)
}
}