Open SabrinaJewson opened 1 week ago
Functions like routing::get are modified to take in E: ?Sized + ErrorHandler as a generic parameter. Then, as long as the chosen E does not implement IntoResponse, type inference will be able to figure out whether a given handler is supposed to use custom error handling or not.
If we went with this, I think we could remove the impl IntoResponse for Result
and then this wouldn't be an issue, right?
Also I don't think I've noticed mentioned how would be the ErrorHandler::Error
converted into an actual HTTP response, shouldn't that be a method on the trait? Or is there an expectation to use a HandleErrors
layer to recover these? In either case, why are NotFound
and MethodNotAllowed
special-cased and aren't simply errors where we could even require ErrorHandler: From<NotFound>
?
This seems sketched out in a way that you maybe have a branch somewhere where you experimented with this? Can I look?
If we went with this, I think we could remove the
impl IntoResponse for Result
and then this wouldn't be an issue, right?
Right. My thinking was just that we don’t want to break too much code that relies on the current behaviour.
Also I don't think I've noticed mentioned how would be the
ErrorHandler::Error
converted into an actual HTTP response, shouldn't that be a method on the trait? Or is there an expectation to use aHandleErrors
layer to recover these?
Indeed, the suggestion is to use a .handle_error
layer for this.
In either case, why are
NotFound
andMethodNotAllowed
special-cased and aren't simply errors where we could even requireErrorHandler: From<NotFound>
?
What special case does that refer to – isn’t that what this proposal is? From the user’s perspective, implementing From<NotFound>
and From<MethodNotAllowed>
will cause ErrorHandler
to automatically be implemented. The only special case is when E = DefaultErrors
, which allows the error type to be Infallible
, and that’s for compatibility with existing code.
This seems sketched out in a way that you maybe have a branch somewhere where you experimented with this? Can I look?
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:
IntoResponse
for Result
would be removed;Router
, by default, uses a type PlaintextError
, and is an infallible service that makes use of PlaintextError: IntoResponse
.The breakage points would be that:
Result
now require the error type to implement From
for more rejections.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 .layer(SetResponseHeaderLayer::overriding(…))
would previously apply to all responses, it now only applies to successful responses. Hard to know what’s right, I guess.
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 E::from(NotFound)
?
something like
.layer(SetResponseHeaderLayer::overriding(…))
would previously apply to all responses, it now only applies to successful responses
I think that depends on how we'd handle the fallibility of responses in the layers. And since the users could call handle_errors
before a given layer, they could then just use the same layers as they do now and they would apply to all responses.
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.
Why call those and not directly
E::from(NotFound)?
Well, we can’t have E::from(NotFound)
because E
is not necessarily even Sized
. We also can’t have E::Error::from(NotFound)
, because Infallible
does not (and cannot) implement NotFound
. Note that for the non-infallible case, we do just use E::from(NotFound)
– those functions are basically private API and are just there to make the infallible case work.
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 ?Sized
hacks, instead goïng for a far simpler design in which Router
is a synonym for Router<(), PlaintextError>
(by default). If we really do think the breakage isn’t worth it then we can go back to the original, but I now believe it’s not nearly as bad as I thought initially.
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 :smile:
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 ^^
If we went with this, I think we could remove the
impl IntoResponse for Result
and then this wouldn't be an issue, right?I'd love to explore different error handling approaches there at some point
Related: #2587 The centralized error handler and observer design used in pavex can potentially be applied to extractor rejections as well.
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.