Fix two production crashes and bump to v3.2.0
1. Fix IndexError in current_player() when player leaves mid-game - remove_player() now adjusts current_player_index after popping - current_player() has safety bounds check as defensive fallback 2. Fix AssertionError in StaticFiles catching WebSocket upgrades - Wrap static file mount to reject non-HTTP requests gracefully - Starlette's StaticFiles asserts scope["type"] == "http" Both crashes were observed in production on 2026-02-28 during a multi-player session. The IndexError cascaded into reconnection attempts that hit the StaticFiles assertion. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7f0f580631
commit
a8b521f7f7
@ -782,9 +782,17 @@ class Game:
|
|||||||
for i, player in enumerate(self.players):
|
for i, player in enumerate(self.players):
|
||||||
if player.id == player_id:
|
if player.id == player_id:
|
||||||
removed = self.players.pop(i)
|
removed = self.players.pop(i)
|
||||||
# Adjust dealer_idx if needed after removal
|
if self.players:
|
||||||
if self.players and self.dealer_idx >= len(self.players):
|
# Adjust dealer_idx if needed after removal
|
||||||
self.dealer_idx = 0
|
if self.dealer_idx >= len(self.players):
|
||||||
|
self.dealer_idx = 0
|
||||||
|
# Adjust current_player_index after removal
|
||||||
|
if i < self.current_player_index:
|
||||||
|
# Removed player was before current: shift back
|
||||||
|
self.current_player_index -= 1
|
||||||
|
elif self.current_player_index >= len(self.players):
|
||||||
|
# Removed player was at/after current and index is now OOB
|
||||||
|
self.current_player_index = 0
|
||||||
self._emit("player_left", player_id=player_id, reason=reason)
|
self._emit("player_left", player_id=player_id, reason=reason)
|
||||||
return removed
|
return removed
|
||||||
return None
|
return None
|
||||||
@ -807,6 +815,8 @@ class Game:
|
|||||||
def current_player(self) -> Optional[Player]:
|
def current_player(self) -> Optional[Player]:
|
||||||
"""Get the player whose turn it currently is."""
|
"""Get the player whose turn it currently is."""
|
||||||
if self.players:
|
if self.players:
|
||||||
|
if self.current_player_index >= len(self.players):
|
||||||
|
self.current_player_index = self.current_player_index % len(self.players)
|
||||||
return self.players[self.current_player_index]
|
return self.players[self.current_player_index]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@ -431,7 +431,7 @@ async def _close_all_websockets():
|
|||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Golf Card Game",
|
title="Golf Card Game",
|
||||||
debug=config.DEBUG,
|
debug=config.DEBUG,
|
||||||
version="3.1.6",
|
version="3.2.0",
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -943,7 +943,18 @@ if os.path.exists(client_path):
|
|||||||
return FileResponse(os.path.join(client_path, "index.html"))
|
return FileResponse(os.path.join(client_path, "index.html"))
|
||||||
|
|
||||||
# Mount static files for everything else (JS, CSS, SVG, etc.)
|
# Mount static files for everything else (JS, CSS, SVG, etc.)
|
||||||
app.mount("/", StaticFiles(directory=client_path), name="static")
|
# Wrap StaticFiles to reject WebSocket requests gracefully instead of
|
||||||
|
# crashing with AssertionError (starlette asserts scope["type"] == "http").
|
||||||
|
static_files = StaticFiles(directory=client_path)
|
||||||
|
|
||||||
|
async def safe_static_files(scope, receive, send):
|
||||||
|
if scope["type"] != "http":
|
||||||
|
if scope["type"] == "websocket":
|
||||||
|
await send({"type": "websocket.close", "code": 1000})
|
||||||
|
return
|
||||||
|
await static_files(scope, receive, send)
|
||||||
|
|
||||||
|
app.mount("/", safe_static_files, name="static")
|
||||||
|
|
||||||
|
|
||||||
def run():
|
def run():
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user