Refuse websocket upgrades with HTTP statuses, not close codes
A close() before accept() carries the HTTP status of the refused
upgrade, not a WebSocket close code: RSGI (granian) uses the value
verbatim as the response status, so 4401 panicked on the invalid status
(InvalidStatusCode) and — granian being built with panic = "abort" —
aborted the worker and took the whole app down. ASGI servers answer 403
regardless, and browsers report a refused handshake as an abnormal close
(1006), so the 4401/4403/4404 codes never reached clients anyway.
Reject 401/404/403 to mirror GET /api/games/{id}, and classify a failed
first connection in the game page through the REST API so players still
see "session expired" / "game no longer available" instead of a retry
loop. Add a regression test asserting every pre-accept rejection uses a
status in the HTTP range.
This commit is contained in:
+26
-9
@@ -87,16 +87,33 @@ pub async fn join_preview(code: &str) -> Result<Option<JoinPreview>, String> {
|
||||
resp.json().await.map(Some).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn game_state(game_id: &str) -> Result<GameView, String> {
|
||||
let resp = Request::get(&format!("/api/games/{game_id}"))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
if !resp.ok() {
|
||||
return Err(server_error(resp.status()));
|
||||
/// Why a game connection failed before the first snapshot arrived.
|
||||
///
|
||||
/// A refused WebSocket handshake is invisible to the browser: the close
|
||||
/// event reports an abnormal closure (1006) with no HTTP status, so the
|
||||
/// game page asks the REST API to tell a deliberate rejection from a
|
||||
/// transient network problem.
|
||||
pub enum GameEntry {
|
||||
/// Not logged in (`401` from `GET /api/games/{id}`).
|
||||
Unauthenticated,
|
||||
/// Unknown game (`404`) or the user is not seated (`403`).
|
||||
Unavailable,
|
||||
/// The API could not be asked (or answered unexpectedly): treat the
|
||||
/// connection loss as transient and retry.
|
||||
Transient,
|
||||
}
|
||||
|
||||
/// Classify a failed game connection via `GET /api/games/{game_id}`,
|
||||
/// mirroring the statuses used to refuse the websocket upgrade.
|
||||
pub async fn probe_game(game_id: &str) -> GameEntry {
|
||||
match Request::get(&format!("/api/games/{game_id}")).send().await {
|
||||
Ok(resp) => match resp.status() {
|
||||
401 => GameEntry::Unauthenticated,
|
||||
403 | 404 => GameEntry::Unavailable,
|
||||
_ => GameEntry::Transient,
|
||||
},
|
||||
Err(_) => GameEntry::Transient,
|
||||
}
|
||||
resp.json().await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub async fn my_matches(cursor: Option<&str>) -> Result<MatchesPage, String> {
|
||||
|
||||
Reference in New Issue
Block a user