As an introduction to Rust I'm trying to create the backend for a card game that my family loves to play (since we cannot play in person due to quarantine). I've decided to use Warp given its well-documented support for websockets, and am trying to get the following architecture working:
Users can post to a create route to create a game. That route is responsible for creating an rx/tx in an unbounded channel, incrementing the global game_idAtomicUsize, storing the tx in mutable state of type type Games = Arc<RwLock<BTreeMap<usize, UnboundedSender<msg::ToGame>>>>;, and spawning the tokio task for the game loop.
Users can get /ws/:game_id to get the game's sender (if) it exists, upgrade the ws connection, and spawn a player loop. If the game id does not exist the route should return a 404.
Where I'm having trouble is in returning a 404 if the game id doesn't exist on the websocket route. My filter code looks like this:
pub async fn player_ws(
game_id: usize,
ws: warp::ws::Ws,
games: Games,
) -> Result<impl warp::Reply, Infallible> {
let games = games.read().await;
if let Some(tx) = games.get(&game_id) {
return Ok(ws.on_upgrade(|socket| player_loop(socket, tx.clone())));
}
Ok(StatusCode::NOT_FOUND)
}
That handler code produces the following compiler error:
expected opaque type, found struct `http::status::StatusCode`
note: expected opaque type `impl warp::reply::Reply`
found struct `http::status::StatusCode`
The Warp docs seem to suggest that StatusCode does implement Reply. What's going on here? Is there a better/more idiomatic way to achieve this in Warp? Thanks!
The issue is that you’re returning two different types. Even though that both implement Reply the type returned by on_upgrade is different from the StatusCode which isn’t allowed. One common way to unify the types is to use Either from the futures library. Where you would wrap the on_upgrade call in Either::Left and the StatusCode in Either::Right. And warp implements Reply for Either which satisfies the function signature.
That makes sense, but it doesn't appear like warp implements Reply for Either. Updated handler code:
pub async fn player_ws(
game_id: usize,
ws: warp::ws::Ws,
games: Games,
) -> Result<futures::future::Either<impl warp::Reply, impl warp::Reply>, Infallible> {
let games = games.read().await;
if let Some(tx) = games.get(&game_id) {
let tx = tx.clone();
return Ok(Left(ws.on_upgrade(move |socket| player_loop(socket, tx))));
}
Ok(Right(StatusCode::NOT_FOUND))
}
That results in the following error for the handler:
the trait bound `futures_util::future::either::Either<impl warp::reply::Reply, impl warp::reply::Reply>: warp::reply::Reply` is not satisfied
the trait `warp::reply::Reply` is not implemented for `futures_util::future::either::Either<impl warp::reply::Reply, impl warp::reply::Reply>
So it looks like warp has its own version of Either (which isn’t accessible) and doesn’t use the one from futures which is why that doesn’t work. So instead what I would do is have player_ws return not found as an error. And then in the calling function I would chain an or_else function call to map that not found error to a reply. You probably need to return a custom error in player_ws and then use find in the or_else function you make sure you’re not catching other errors with or_else. I’m on a phone right now but if you still have trouble I can create some sample code on my laptop.
Disclaimer: I don’t know if this is the idiomatic way to use warp. I mostly brute forced my way into learning how to get it to do what I want.
That did the trick, thanks! I did end up using recover instead of or_else, though. Why did you suggest or_else? Are there any advantages to using that?