Last straw for warp

I'm almost ready to throw in the towel on learning to use warp. I'm blown away that I can't find an example of something pretty basic. I just want to implement a GET REST service that either returns JSON or a 404 status if a matching item cannot be found. My latest attempt can be found here:

The compiler doesn't like that the get_dog route returns a Result instead of Reply. But I can't figure out how to return warp::reject::not_found() in a Reply.

Hi! Have you tried and_then instead of map? It allows returning a result:

https://docs.rs/warp/0.3.0/warp/trait.Filter.html#method.and_then

1 Like

Here it is using and_then: rust-warp-demo/main.rs at master · mvolkmann/rust-warp-demo · GitHub
This gives me different errors starting with "std::result::Result<Json, Rejection>` is not a future".

Looks like you need async in front of move:

async move {
}

and_then expects a future that resolves to a result; adding async here makes the block return a future

1 Like

Adding that gives the error "this closure implements FnOnce, not Fn".

I don't know anything about web apps but it seems like this file from the examples/ directory in the warp repo has the elements you're describing?

2 Likes

Here's a PR: appease the compiler by danbruder · Pull Request #1 · mvolkmann/rust-warp-demo · GitHub that compiles.

The FnOnce error is because we are 'moving' the dog_map into the route handler, but it is moved into the first invocation of the route, so the closure cannot be called again with a reference to dog_map.

I added a filter that can be added to any route that provides a reference to the dog_map. The dog map is moved once into the filter, but then provided to the closure each time the route is visited.

I'm off on my reasoning, someone else here plz correct me :slightly_smiling_face:.

If you keep going with warp and run into snags feel free to message me.

1 Like

Thanks so much! Can you explain why this line is needed and what it is doing?

let dog_map_state = warp::any().map(move || dog_map.clone());

Maybe it's the case that warp isn't the easiest Rust crate to use for this sort of thing.
Can anyone suggest an alternative they feel is easier?
I'm not sure yet if the issue is that warp is too complicated for me or Rust is too complicated.

The reason that you got the error about FnOnce is that when you do this:

let get_dog = warp::path!("dog" / String).and_then(move |id: String| {
    async move {
        if let Some(dog) = dog_map.read().get(&id) {
            Ok(warp::reply::json(&dog))
        } else {
            Err(warp::reject::not_found())
        }
    }
});

you have to understand that the closure move |...| { ... } is a separate entity from the async block inside it. For the async block to be able to access dog_map, it must be the async block that has ownership of dog_map, but for it to get ownership, the ownership must be given to it by the closure. Since dog_map is not Copy, the closure can only give away ownership once.

One way to fix this is to give the async block ownership of a clone of dog_map each time the closure is called. This lets you call the closure many times.

let get_dog = warp::path!("dog" / String).and_then(move |id: String| {
    let dog_map_clone = dog_map.clone();
    async move {
        if let Some(dog) = dog_map_clone.read().get(&id) {
            Ok(warp::reply::json(&dog))
        } else {
            Err(warp::reject::not_found())
        }
    }
});

Another option is to not use the dog_map inside the async block at all.

let get_dog = warp::path!("dog" / String).and_then(move |id: String| {
    let response = if let Some(dog) = dog_map.read().get(&id) {
        Ok(warp::reply::json(&dog))
    } else {
        Err(warp::reject::not_found())
    };
    async move { response }
});

Regarding the warp::any().map(move || dog_map.clone()) thing, it is just another way to clone dog_map before reaching the async block.

Warp is a very functional library, and it pushes the complexity of Rust as far as it can to embrace the functional style. If you want a web server library that doesn't use all of Rust's more advanced features in this manner, I would recommend actix-web.

4 Likes

I've read that the rub against actin-web is that it uses unsafe code, perhaps unnecessarily, and that the maintainer abandoned it because he didn't want to change that and was frustrated with people asking him to do it. If I understand correctly, others have taken over the project. Is that right?

Yes, that's fairly correct. actix-web 3.0 is the first release of the community maintained version. All uses of unsafe were examined for soundness and it should be safe to use.

1 Like

@danbruder You offered to help if I ran into more snaps in using warp. I think I'm close to getting this figured out. The last bit is being able to share access to a HashMap across my routes. Could you take a look at this and see if you can spot what I'm doing wrong?

The compiler doesn't like my use of with_state which I copied from with_db in this example: warp/todos.rs at master · seanmonstar/warp · GitHub

The first error is:

  --> src/main.rs:58:14
   |
58 |         .and(with_state(state))
   |              ^^^^^^^^^^^^^^^^^ the trait `Clone` is not implemented for `impl warp::Filter`

You can fix this by adding a Clone bound to the return type of with_state (matching the bounds on with_db):

-    fn with_state(state: State) -> impl Filter<Extract = (State,), Error = Infallible> {
+    fn with_state(state: State) -> impl Filter<Extract = (State,), Error = Infallible> + Clone {

Then you'll get an error that your closures expect the wrong type, which you can fix like this:

-        .map(|dog_map: DogMap| {
+        .map(|dog_map: State| {

Then there's a problem caused by returning a Rejection to map. You can fix that by using and_then instead of map. This will also require you to use an async block or function, which is good because you need it to access your tokio::sync::Mutex:

let get_dog = warp::path!("dog" / String)
    .and(warp::get())
    .and(with_state(state.clone()))
    .and_then(|id, dog_map: State| async move {
        println!("got get for id {}, dog_map = {:?}", id, dog_map);
        if let Some(dog) = dog_map.lock().await.get(&id) {
            Ok(warp::reply::json(&dog))
        } else {
            Err(warp::reject::not_found())
        }
    });

Then, for routes that never return an error, you'll get an error that the compiler can't infer the error type. Until we have proper async closures, the easiest way to annotate the return type is to change the closure to an async fn like in the todos.rs example:

let get_dogs = warp::path!("dog")
    .and(warp::get())
    .and(with_state(state.clone()))
    .and_then(handle_get_dogs);

async fn handle_get_dogs(dog_map: State) -> Result<Json, Rejection> {
    println!("got get: dog_map = {:?}", dog_map);
    let dogs: Vec<Dog> = dog_map.lock().await.values().cloned().collect();
    Ok(warp::reply::json(&dogs))
}
4 Likes

By the way, I highly recommend compiling your code continuously as you write, and stopping to fix compiler errors as soon as one appears.

If you keep adding on to code that already has errors, the new errors will get increasingly confusing. Plus, if you copy-paste incorrect code, you'll then need to fix the same error in many places instead of just one.

Instead, you should create a tiny program that is error-free, and then keep it error-free as you incrementally add to it. Then the compiler will be like a helper instead of an adversary.

10 Likes

That is good advice.

Also instead of a compile just a quick "cargo check" will do.

Also having rust-analyser installed in VS Code does much the same on a continuous basis.

I quite enjoy my long conversations with the compiler. It is very helpful as you say.

But beware, sometimes the compilers suggestions can lead one down an endless habit hole. A common case being when it suggests making some lifetime 'static to fix some lifetime problem or other. This is almost certainly not what you want to do.

3 Likes

This is definitely something I did not understand. Thanks so much for explaining that!
I have all the routes working now.

the other option for specifying the error type is to explicitly state the error type with the turbo fish:

Ok::<_, Rejection>(json(&dogs))
2 Likes

Agreed! Getting Rust Analyzer set up in your ide (if it isn't already) is a big help for feedback - also gives you auto-formatting on save.

3 Likes