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:
7 files changed
+162
-35
No files matched your search
+30
-13
@@ -4,7 +4,9 @@ use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use sycamore::prelude::*;
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
|
||||
use crate::api;
|
||||
use crate::components::card::{card_back, card_img};
|
||||
use crate::components::summary::{hand_summary_modal, summary_rows};
|
||||
use crate::components::toast::toast;
|
||||
@@ -135,23 +137,38 @@ fn start_connect(id: Rc<String>, ctx: ConnCtx, alive: Rc<Cell<bool>>) {
|
||||
let on_close = {
|
||||
let id = id.clone();
|
||||
let alive = alive.clone();
|
||||
move |code: Option<u16>| {
|
||||
move |_code: Option<u16>| {
|
||||
if !alive.get() {
|
||||
return;
|
||||
}
|
||||
ctx.closed.set(true);
|
||||
match code {
|
||||
Some(4401) => {
|
||||
ctx.fatal.set(true);
|
||||
ctx.error
|
||||
.set(Some("Session expired — please log in again.".to_string()));
|
||||
}
|
||||
Some(4403) | Some(4404) => {
|
||||
ctx.fatal.set(true);
|
||||
ctx.error
|
||||
.set(Some("This game is no longer available.".to_string()));
|
||||
}
|
||||
_ => schedule_retry(id.clone(), ctx, alive.clone()),
|
||||
if ctx.game.get_clone().is_none() && ctx.attempts.get() == 0 {
|
||||
// The connection ended before any snapshot arrived. A
|
||||
// refused handshake looks exactly like a network drop to
|
||||
// the browser (close code 1006, no HTTP status), so ask
|
||||
// the API whether the server deliberately rejected us —
|
||||
// in which case retrying is pointless — or the loss was
|
||||
// transient. Only on the first failure: later attempts
|
||||
// are governed by the retry backoff.
|
||||
let id = id.clone();
|
||||
let alive = alive.clone();
|
||||
spawn_local(async move {
|
||||
match api::probe_game(&id).await {
|
||||
api::GameEntry::Unauthenticated => {
|
||||
ctx.fatal.set(true);
|
||||
ctx.error
|
||||
.set(Some("Session expired — please log in again.".to_string()));
|
||||
}
|
||||
api::GameEntry::Unavailable => {
|
||||
ctx.fatal.set(true);
|
||||
ctx.error
|
||||
.set(Some("This game is no longer available.".to_string()));
|
||||
}
|
||||
api::GameEntry::Transient => schedule_retry(id, ctx, alive),
|
||||
}
|
||||
});
|
||||
} else {
|
||||
schedule_retry(id.clone(), ctx, alive.clone());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user