warp: Improved rejections
I get the impression that the rejection system in this crate ought to be severely refactored. The rejections example does not show a simple way of handling functions which return Results:
async fn login(credentials: Credentials) -> Result<UserSession, LoginError> {
...
}
which is the standard way of handling Errors in Rust. The suggested way of handling this seems to be to
- implement
Reject for LoginError(which requires me to own theLoginErrortype) - call
recoverafter the handling filter with a “rejection handler” function, which converts rejections into aReplytype which can be sent by warp
This causes a lot of cognitive and syntactic overhead, and I think the crate would benefit a lot from a refactor like this.
About this issue
- Original URL
- State: closed
- Created 4 years ago
- Reactions: 27
- Comments: 20 (11 by maintainers)
Commits related to this issue
- Stop abusing warp rejections https://github.com/seanmonstar/warp/issues/712 — committed to LLFourn/olivia by LLFourn 4 years ago
- Change to error codes after reading https://github.com/seanmonstar/warp/issues/712#issuecomment-697031645 — committed to jlvandenhout/iota-streams-channels-interface by jlvandenhout 3 years ago
It has been brought up multiple times, but I that also likely means the design is missing something. It might need to be easier to use rejections and stuff, but it also is the case that people see rejections and assume that all errors should be turned into rejections. That shouldn’t be the case, though. Rejections are meant to say “this filter didn’t accept the request, maybe another can.” So things like
DbErroror similar shouldn’t be turned into rejections.I’ve seen it suggested the idea of “fatal” rejections, that don’t try other filters. But I don’t know… I don’t yet know the best answer.
For context, this is the way that I found to handle this case:
There is a lot of boilerplate here that ought to be handled by
Filter::and_then(and friends) or theRejectorReplytrait. The problem I see currently is thatFilter::and_thenrequires a function, returning aTryFuture, resolving to aResult, where theErrorimplementsCombinedRejection, which is a sealed trait of this crate, and is only implemented forRejectionandInfallible.So actually returning an error from a
Filterusingand_then(which is the only method accepting aFuture, i.e. async values btw) comes down to converting the error to the aRejectiontype, which seems completely useless by itself, and does not integrate well with other types. The only way to use it with code external towarp(which is absolutely necessary given the currentrejectmodule, basically only supportingnot_found) is to usereject::custom. This function however requires the input type to implement theRejecttrait, which is not implemented for anything by warp itself, so you can only implement it yourself, for types defined by your own crate. This however requires you to define a new type for every error type your crate is using, which seems completely unreasonable.My current proposal would be to remove the
rejectconcept altogether and use theReplyinterface instead. Errors can be denoted through the HTTP error codes, but are just replies otherwise. I will take a deeper look at how to implement this though, and might change the design later on. I would be glad to talk about any feedback.FWIW, my suggestion is:
where
handleris a function which returnsResult<impl warp::Reply, std::error::Error>.The error is automatically converted into an
InternalErrorrejection, and such a rejection will not continue trying other filters.This allows nice handler implementations, while the rejection can still be centrally recovered to convert to an error reply, together with 404’s and such.
Yes, that makes it possible to do explicit early-return of error replies, which is kind of nice. But #458 (I keep repeating that PR number) would make it possible to do that early return with the standard
?operator.Correct. I just found out that having my own type as an error type is not enough to impl a trait on Result.
There is an
impl<T> Reply for Result<T, http::Error> where T: Reply, but I don’t see a way to construct ahttp::Errorother than for specific errors that can happen in thehttpcrate. Maybe warp could provide animpl<T, E> Reply for Result<T, E> where T: Reply, E: Reply? I would like to be able to define how to render error messages in my application code, and I think beeing able to provide animpl Reply for MyErrorand then have warp provide Reply forResult<impl Reply, MyError>.Maybe warp should provide
impl<T, E> Reply for Result<T, E> where T: Reply + Send, E: Reply + Send + std::error::Error? Or should it add another marker type onEto avoid being too generic?The rejection system makes sense for built-in and custom filters, but for the final handler, e.g. some
/usersendpoint, 95% of the time you want theErrorof theResultbe a 500, not a rejection. And this should work nicely with the?operator.I hope @seanmonstar you can either take the time to re-think/extend this, or invite the community to come up with something for 0.3 and trust the kids to do the right thing 😃
The way I see it, the
recoverfunction is only for those cases when you you have tried all filters. When there is a matching route and that route actually results in an error, the route function should return animpl Replywhith the error code (and maybe a nice html or json error message for the user).And the nice way of doing that would be to return an Error that is not a rejection but instead treated as a reply, as per #458.
Also, the way I see it, having a three-state special result, that may contain an Error or a Rejection would not be really helpful, as the operation should be in one of two modes: Either we are still resolving the route, in which case we need a
Result<impl Reply, Rejection>or we are in the function handling one specific route and need to return animpl Replyand never a rejection. In that case, havingResult<MyResponse, MyError>implementReplywould be really nice.I find the documentation very hard to read, because a lot of the code is hidden and not documented. For example, the
F::Error where F: Filterassociated type is implemented by theFilterBasetrait, which is not documented, so you wonder where theErrortype comes from, when reading the documentation, and have to look at the source code. This makes the API harder to understand. But that is a separate issue: #742Implementing
ReplyforResult<T: Reply, E:Reply>is already very helpful, but the Rejections are probably still somewhat easily confused with errors, because right now (if I understand correctly), rejections are returned either through theErrorassociated type inFilterBaseor in theErrortype inTryFuture(for async filters), which makes it seem like this is where actual errors belong.Maybe it would be better if warp explicitly implemented a
FilterResultand aFilterFuture(which resolves toFilterResult) whereFilterResultcould look something like this:Then it would be clear that this is not an error.
Filter functions could then return some
T: Into<FilterResult<T>>andFilterResultcould implementFrom<Rejection>.Some notes from Discord yesterday, hopefully to continue here:
Hi, this has been addressed multiple times, see https://github.com/seanmonstar/warp/pull/527, https://github.com/seanmonstar/warp/pull/374, https://github.com/seanmonstar/warp/issues/307,