seanmonstar / warp

A super-easy, composable, web server framework for warp speeds.
https://seanmonstar.com/post/176530511587/warp
MIT License
9.57k stars 719 forks source link

More examples of control flow -- and, or, etc #487

Open m-doughty opened 4 years ago

m-doughty commented 4 years ago

One of the things I'm finding confusing is how to do branching filters. Let's say I have:

step1
  .and(step2)
  .and(step3a)
  .or(step3b)
  .unify()
  .and(step4)
  .and(step5a)
  .or(step5b)
  .unify()
  .and(step6)
  .and_then(step7)

I would expect this to:

  1. Run step1
  2. Run step2
  3. Run step3a, if it produces a rejection, run step3b
  4. Run step4
  5. Run step5a, if it produces a rejection, run step5b
  6. Run step6
  7. Run step7

It doesn't seem to do that, instead it seems to try to run step5b if anything up to and including step5a produces a rejection, and I can't work out how to achieve the desired behavior from the documentation.

louy2 commented 4 years ago

Maybe try something similar to:

step1
  .and(step2)
  .and(step3a.or(step3b).unify())
  .and(step4)
  .and(step5a.or(step5b).unify())
  .and(step6)
  .and_then(step7)

I don't know whether that actually works, but at least the types should match.

arlyon commented 4 years ago

Would be nice to turn this thread into a general collection of patterns. Here's one that has me stumped: 'if some header exists (say 'Authorization'), try to parse it into a Some(Token). if parsing fails, throw an error. if the header doesn't exist, return None. Here's my attempt to achieve that:

let user_extractor = warp::header::<String>("authorization")
    .and_then(|token: String| async {
        decode_jwt(token).map_err(|e| {
            warp::reject::custom(TokenError {
                message: e.to_string(),
            })
        })
    })
    .or(warp::any().map(|| None))
    .unify();

The problem is that or() currently swallows the error essentially representing 'try and parse auth header: if it fails or the header doesn't exist, return None, else Some(Token)

m-doughty commented 4 years ago

Would be nice to turn this thread into a general collection of patterns. Here's one that has me stumped: 'if some header exists (say 'Authorization'), try to parse it into a Some(Token). if parsing fails, throw an error. if the header doesn't exist, return None. Here's my attempt to achieve that:

let user_extractor = warp::header::<String>("authorization")
    .and_then(|token: String| async {
        decode_jwt(token).map_err(|e| {
            warp::reject::custom(TokenError {
                message: e.to_string(),
            })
        })
    })
    .or(warp::any().map(|| None))
    .unify();

The problem is that or() currently swallows the error essentially representing 'try and parse auth header: if it fails or the header doesn't exist, return None, else Some(Token)

This is very similar to an issue we're having -- we want to check if the user requested XML with Accept: application/xml or Accept: text/xml, if not, we want to default to JSON, but if so, we want the XML path to error as usual and not continue to try the JSON path.

louy2 commented 4 years ago

@arlyon

The problem is that or() currently swallows the error

or() is designed to swallow the error. If you need access to the error, use or_else()


Edit 1

Also, the way you model your domain is not fitting in the type signature.

You want to model your data with a Option<Result<String>>, while header() gives you Filter<Extract = String, Error = Rejection>.

A much better way is to model your data as Filter<Extract = Token, Error = Rejection> directly, and write:

impl FromStr for Token {
    type Err = TokenError;
    fn from_str(token: &str) -> Result<Self, Self::Err> {
        decode_jwt(token)
    }
}

let user_extractor = warp::header::<Token>("authorization").recover(|r: Rejection| async {
    let rep = if let Some(TokenError { message }) = r.find() {
        reply::html(format!("{}", message))
    } else if let Some(MissingHeader) = r.find() {
        reply::html(format!("Missing authorization header"))
    } else {
        reply::html("Unknown error".into())
    };
    Ok(rep)
});
arlyon commented 4 years ago

@louy2 Thanks for your insight. The final version looks like this:

let user_extractor = warp::header("authorization")
    .map(|t: Token<Claims>| Some(t.claims().user.clone()))
    .or_else(|e: Rejection| async {
        e.find::<MissingHeader>().map(|_| (None,)).ok_or_else(|| e)
    });