-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fallible handlers for custom rejections #2965
Comments
If we went with this, I think we could remove the Also I don't think I've noticed mentioned how would be the This seems sketched out in a way that you maybe have a branch somewhere where you experimented with this? Can I look? |
Right. My thinking was just that we don’t want to break too much code that relies on the current behaviour.
Indeed, the suggestion is to use a
What special case does that refer to – isn’t that what this proposal is? From the user’s perspective, implementing
I don’t have that, unfortunately. So it is possible there are things I’m overlooking. I opened this for some initial feedback, I should probably make the branch. So yes, overall this proposal is made more complex by the desire to avoid breaking existing code. The idea is that larger applications can be piecemeal moved to the “fallible handler” design, but the default errors mode is still there for simpler use cases, where it would be annoying to have to define one’s own error type. Possibly, if I were designing it from scratch, then it would somewhat different:
The breakage points would be that:
But now I think about it, this probably is not as bad as I initially thought. Edit: On the other hand, it may be a footgun, because while something like |
By special casing I meant having fn not_found() -> Result<Response, Self::Error>;
fn method_not_allowed() -> Result<Response, Self::Error>; on the trait. Why call those and not directly
I think that depends on how we'd handle the fallibility of responses in the layers. And since the users could call It definitely is very intriguing and I've also be bitten by having a handler returning an unexpected response so I do think it would be great if we can make this work. |
Well, we can’t have I’ve started hacking this up on a branch, which I’ll post once I’m done. I opted against the original proposal with the |
To set some expectations here - this proposal seems like a pretty big change to me, and I don't think we should have it in 0.8, if at all. Really, I wasn't planning to do any changes of this size for a long time, if ever. (to me, all of #2475, #2468 and what we already merged for 0.8 seem like smaller changes in terms of churn) I've got a much more experimental potential axum successor project in the works - lmk if you want to be involved. I'd love to explore different error handling approaches there at some point, and maybe it could be used as a testing ground for changes to axum too, though it's pretty different in some ways already like replacing the tower traits with its own HTTP-specific ones. |
Then fair! I opened the issue with the expectation that that would very likely be the response 😄 |
I don't think this needs to be outright closed (maybe it's something we should consider for 0.9 or beyond) - unless you feel like it's no longer worth exploring given the situation? |
Oh, fair, I was just less motivated somehow. We can keep it open! |
Understandable ^^ |
Related: #2587 |
Right! I think that issue is quite similar, this is just a much more concrete proposal. |
Related: #1116
Feature Request
Motivation
axum-extra
’sWithRejection
is a great tool for handling custom rejections, but what I would really like is if I could know at compile time that there will be none of Axum’s default rejections returned by my service.Proposal
The central concept of this proposal is that of the fallible handler. We first remove the
Rejection: IntoResponse
bound, and add the following trait:This allows for specialization over whether something uses the default approach of plaintext errors: because
DefaultErrors
is?Sized
andSized
is a#[fundamental]
trait, Rust knows that it cannot overlap withE: Sized
. We then modify theHandler
trait:The old
impl
s are kept the same (other than requiringRejection: IntoResponse
), but we add new ones for handlers with custom error handlers:Functions like
routing::get
are modified to take inE: ?Sized + ErrorHandler
as a generic parameter. Then, as long as the chosenE
does not implementIntoResponse
, type inference will be able to figure out whether a given handler is supposed to use custom error handling or not.The expectation here is that users define their own error type, implement
From
for whatever rejection types they want to deal with and return-> Result<impl IntoResponse, MyError>
from handlers. They could do something likeimpl<E: Display + IntoResponse> From<E>
to automatically cover existing rejection types; this provides extra reason forErrorHandler
s to not implementIntoResponse
, as such an implementation may conflict with the blanket implementation.One option is to stop here, and convert
ErrorHandler
s into fullResponse
s at the handler level. This would involve renamingErrorHandler
toErrorHandlerTypes
, making a new traitErrorHandler
with aninto_response
method and implementing it forInfallible
, and addingtype Error: ErrorHandler
toErrorHandlerTypes
and then converting everything to aResponse
insideRoute
.But a disadvantage of this is that error handling can’t be done at the level of middleware.
axum
already has tools for dealing with this –HandleError
– and it would be nice if we could hook into that. So, I suggest we go further, replacing theE = Infallible
parameter ofMethodRouter
withE: ?Sized + ErrorHandler = DefaultErrors
, also adding the same parameter toRouter
, and forwarding that to handlers – services are similarly allowed to be fallible, with the error type asE::Error
.Router
will still only implementService
whenE = DefaultErrors
, to prevent accidentally handing the errors to Hyper.Fallbacks
Rejections aren’t the only kind of response that Axum generates automatically; default fallbacks are another, and if I’m adding ten
MethodRouter
s to my service I would like assurance that they all have custom fallbacks.One way to achieve this is to add rejection types
NotFound
andMethodNotAllowed
and modifyErrorHandler
like so:The default fallbacks of
Router
andMethodRouter
will callE::not_found()
andE::method_not_allowed()
respectively.The disadvantage of this approach is that your custom error type is required to implement
From<NotFound>
+From<MethodNotAllowed>
even in cases where you plan on adding custom fallbacks to your routers. On the other hand, with this designhandle_error
layers mostly eliminate the need for custom fallbacks in the first place. At the same time this creätes asymmetry in how custom fallbacks look for when an app is using default error handling versus custom error handling.A different option is to add a third generic parameter to
Router
andMethodRouter
which is eitherDefaultFallback
orCustomFallback
. If the routers are used with anErrorHandler
other thanDefaultErrors
, then ifDefaultFallback
is used, it requires the boundsE: From<NotFound>
andE: From<MethodNotAllowed>
respectively; otherwise,.fallback
is required to be called. If the routers are used withE = DefaultError
,.fallback
is not required, and calling it does not change the type.For now, I’m leaning toward the first approach for its simplicitly, but I’m unsure.
Other cases of default errors
There are other instances where Axum generates its own errors, for example in
Json
’sIntoResponse
implementation. It’s possible we could makeIntoResponse
into a fallible trait, but I’m not sure if it’s worth the breakage since basically onlyJson
benefits from that, and JSON serialization errors are extremely rare anyway. There might be some others I’ve missed too, I’m unsure.The text was updated successfully, but these errors were encountered: