-
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
Improve error handling user experience. #2665
Conversation
Adds the trait `IntoResultResponse` to be use in place of `IntoResponse` in handlers to allow easy use of `?` on standard errors and other implementations like `anyhow` and `eyre`. The type `ErrorResponse` encapsulate any kind of error with an associated `StatusCode` and the type alias `ResultResponse` use it by default in a `Result`. `ErrorResponse` can be converted from any `Into<Box<dyn Error>>` and also implements `IntoResponse`. `IntoResultResponse` is only implemented by `ResultResponse` to force the type system to unambiguously convert any errors from handlers to `ErrorResponse` when using `?`. This commits only add new traits and types doesn't change any existing API.
I'll think about it but my initial reaction is that this just adds more noise to the public API without much of an actual benefit. Note that instead of type MyCustomResult<T> = Result<T, MyCustomError>; or type Result<T, E = MyCustomError> = std::result::Result<T, E>; and write |
The idea is that I guess basically everyone wants to handle errors in handlers so this noise needs to exist somewhere. Either:
My feeling is that reducing this kind of friction is what helps giving a good opinion when people try out and evaluate a new crate. |
Yes, it's also already possible to write And |
To avoid extending I think it's important that something easy and official exists to solve this common issue. |
But they are.. And we also have |
I guess the problem here is that I don't really understand your motivation / problem statement, or I didn't initially at least. Is it right that you're looking to make it easier for people to return errors that don't implement |
I think i went through a quite common struggle when I started using axum. Very quickly, I needed to In my case, on a custom function returning an Neither types implement I'm therefore stuck. I know I could come up with some boilerplate code to do the plumbing myself but it feels weird this use case is not taken care of already. I start searching online and find lot of other people with the same issue and the official example code for Looking deeper, I also find a few semi-maintained crates with the boilerplate code I need. I'm really not into adding single-unknown-maintainer dependencies but i have to make a choice. I check all the boilerplate code I find and evaluate them. I also need to dig deeper into axum's types to really understand what I'm doing before adding this code to my project. After about an hour (or more) I finally write my own custom types and traits, in my project, to solve the problem I encountered 5 minutes into using |
I think I went through all the links of the first page of Google result + the example folder in axum + quite many axum document pages, but i don't remember seeing or at least considering Probably because it can't be used in the use case I described as it doesn't implement conversation traits from regular errors. And, just like it's impossible to implement So the user is forced to look for boilerplate code to copy paste as soon as he wants to use |
I hope my longer explanation will help!
Yes, that's exactly the problem. I don't think forcing the user to prefix all its |
I understand your pain, but there's no single obvious solution for turning an arbitrary error type into an http response. Do you agree that the most common need is to return an internal server error? What do you think about my suggested solution, |
Why not impl<E: Into<Box<dyn Error>>> From<E> for axum::Error ? That's the central piece that makes things work natively without having to use mappings. |
I think that's exactly what this PR does.
I'm not a fan because people would need to copy paste this snippet over and over again for no real value when this could be avoided.
What the issue with making I don't understand the downside of this solution. I'll refactor this PR to remove Then this PR won't add any new types, which is much better and I hope you'll reconsider 🙂 |
I'm sorry, but making |
I absolutely understand that, and I wouldn't do this on a service exposed to the web too. That's why this PR didn't make it implicit by default (even though similar projects like In this PR, nothing changes by default, and no errors are implicitly converted. One has to opt-in with The idea is that:
If that doesn't seem sensible to you, I won't bother refactoring this PR so please tell me! |
Hm, I don't think using Hope it's okay if I ping a few people for opinions again: @mladedav @yanns @tottoto @programatik29 @SabrinaJewson @lilyball (if you don't want to be pinged let me know) |
You're right, the name should probably be more a lot more explicit, like |
In fact, when opting into this, there are three things that can be done when an error is returned:
This PR does 3. but probably it should only do 2. This is probably what most people need anyway, and this way, there are no concerns with inadvertently leaking internal errors to clients. For reference, |
Oh, I didn't even check the code yet, only your PR description. I actually think the exact opposite, that it should be returned but not logged. In my mind, this is purely a quick-prototyping thing. But that's certainly something that could be discussed more as well. I think it makes sense to create some issues from this PR soon, to have better places for the individual bits of the discussion. But I'm not completely certain yet what we need dedicates issues for :) |
For me,
Are you trying to cover this use-case of non-expected errors? |
@jplatte Ohh ahah, indeed I think this should be split into many different bits. Here's the different "subjects" I see:
|
Not in particular no, on the contrary, I liked the idea of being able to easily attach the Like: async fn get_user(Path(user_id): Path<String>) -> impl IntoResultReponse {
let u = Db::get(user_id).err_with_status(StatusCode::NOT_FOUND)?;
...
} EDIT: this works thanks to the |
And my first goal was to get the error in the logs, but I also find it convenient to get it back in the body when testing. Returning errors in the HTTP body should probably be a "Debug"-only feature or something similarly gated away from normal usage. |
I've created #2671 for the potential improvements to the existing Regarding the |
My two cents. I personally like that I have to handle errors explicitly. I wouldn't want to make a mistake by putting a question mark somewhere it doesn't belong and not having axum reject the code. I do believe any way to opt into something like that should be way more explicit like I'm not convinced that it is needed either though, if you need a quick and dirty way to prototype and get errors logged, why not just |
Actually, panics are handled as closing the tcp connection, unless you add a middleware that catches the panic and returns a response. |
@mladedav removing the The user would have to very explicitly write My goal is that |
My feeling is that panics, even for testing releases, are very difficult to deal with in async code. The "raison-d'etre" of this PR is very similar to |
Just my 0.02$. In my experience, this whole class of problem is solved by #[derive(thiserror::Error, Debug)]
enum MyError {
#[error("an internal server error occurred")]
Reqwest(#[from] reqwest::Error)
}
impl IntoResponse for MyError {
fn into_response(self) -> axum::response::Response {
match self {
MyError::Reqwest(ref e) => {
tracing::error!("Reqwest error: {:?}", e);
}
}
// just a quick example, don't return 500 for everything in real code
(StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response()
} and then look how cleanly it composes with axum handlers: async fn handler() -> Result<String, MyError> {
let content = reqwest::get("example.com").await?.text().await?;
Ok(content)
} You have the chance to log the error in the |
@Ptrskay3 yes, that's basically what my PR is. Here's the crux of the PR: /// Here, we can wrap any kind of error, and also attach a `StatusCode` to it.
/// The StatusCode will be returned when the error happens.
#[derive(Debug)]
pub struct ErrorResponse {
status: StatusCode,
error: Box<dyn Error>,
}
// can be created from any errors, using the same technique as `anyhow` and `eyre`
impl<E: Into<Box<dyn Error>>> From<E> for ErrorResponse
{
fn from(error: E) -> Self {
Self::new(StatusCode::INTERNAL_SERVER_ERROR, error) // by default we use a 500 error
}
}
// can be converted to Response
impl IntoResponse for ErrorResponse {
fn into_response(self) -> Response {
let error = format!("{:?}", self.error);
#[cfg(feature = "tracing")]
tracing::error!(error = %error); // we trace when tracing is enabled
// this part should be behind an explicit flag for testing only
(self.status, error).into_response()
// for regular releases, it should do this:
self.status.into_response()
}
} It's very similar to your code. The main difference is that my code handles all the errors, not just I know very well the debate between I personally use both kind of error handling, sometimes custom errors, sometimes Like many others, I think both have their place. dtolney, one of the most important contributor to the rust project and compiler, and who wrote I got the inspiration for this code from reading his source code in And finally, in avery similar project, |
I really got my fair share of people assuming I didn't know how to write custom errors or that I didn't know how to use I started using Rust shortly after the 0.6 release and saw the evolution of the
I wrote my own errors and used all those error handling crates too. I probably read all the blog posts about error handling from, boat/dtolney, niko matsakis, yaahc, steve klabnik, aaron turon, carol nichols and many others, and in 10 years, I can tell you, that's a lot of them! The path to finding a good error handling story for Rust has been very tumultuous in the first years after the 1.0 release. I feel that I'm opening this almost 10 years old debate again now. Many people in the rust community got burned out in those debates. I just wanted to contribute something I wrote that I found useful, but yes, this PR does too many things at once, even though it was just a Draft. I don't want to defend it anymore. I shouldn't have spent that much time here already, I'll go back to other projects. Sorry to have made everyone loose time here. |
I really am sorry if I or anyone else made you feel unwelcome or feel that your experience was unfairly dismissed. I for one don't think the time was wasted at least for me because it did give me another perspective of how people use or want to use errors in handlers. |
@mladedav Thanks for the kind words. I didn't want to force people to use implicit error. I thought my PR reflected that. My PR didn't change any behavior by default. All my code was opt-in. I know rust developers often argue with people using "weaker" languages about how great rust explicitness is. But I'm a rust developer too. Because of Rust, I stopped using C++ and forgot about it so quickly that I then couldn't find dev jobs because Rust wasn't popular yet. This PR only wanted to ADD a feature, not change or remove anything. For info, it's the same guy who wrote both |
One fun fact, in 2017, I applied to Google and got invited to their headquarters for the final 5 SWE+SRE on-site interviews. By that time, I was already so into Rust, that I started forgetting about C++ but Rust wasn't popular yet so I had to do my interviews in C++. I totally failed one of the interview because, with the stress, I totally forgot about C++ exceptions. I looked very stupid to the interviewers and because of this massive fail, it got me rejected even though my other interviews went well. |
My thanks for your contributions too. Even though we seem to disagree not just on the solution but also in some parts the problem(s) to solve here, I think something productive has already come out of this PR. In case you haven't seen it, maybe you're interested in voicing your opinion on the alternatives listed in #2671 about improving the existing |
Motivation
I started using
axum
recently (afterwarp
andpoem
) and I really like its "style", but I was really surprised to see that the project doesn't natively support returning errors from handlers.I saw the examples code in the source code and the documentation and there are also a few crates available and code snippets on Reddit, but I wasn't happy with them.
I feel that one solution can be good and generic enough to become officially part of
axum
.This would reduce friction for new
axum
users and prevent them for copy-pasting suboptimal code.Poem
handlers can returnPoem::Result<T>
and it's really nice.Solution
I wanted to keep things as
axum
y as possible without chaning any existing API or add any dependencies.Handlers currently typically return
impl IntoReponse
so my idea was to be able to change that toimpl IntoResultResponse
(I don't likeResult<impl IntoResponse, MyCustomError>
) and then be able to use?
with any kind of errors in the function.Errors from
std
,anyhow
,eyre
etc are all supported and the return type will be converted into aResult<impl IntoResponse, ErrorResponse>
whereErrorResponse
implementsIntoResponse
.ErrorResponse
just encapsulates the original error with an associatedStatusCode
(by defaultINTERNAL_SERVER_ERROR
) and is then converted to(self.status, format!("{:?}", self.error))
.To be able to easily customize the
StatusCode
I also added aResult
extension traitResultExt
that implementsResult<T>::err_with_status(self, status: StatusCode) -> ResultResponse<T>
.Resulting user experience
Status of PR
Since this PR add some new elements to the API I don't expect it to be readily accepted.
I therefor skipped many things that should be done if it were to be accepted:
Send
,Sync
and'static
)