I have an actix_web service whose handlers consistently return Result<..., ServiceError>, where ServiceError distinguishes between internal errors and business errors. Internal errors are "unexpected" errors whose details shouldn't be exposed to the outside, and business errors come with a useful payload. ServiceError is defined like this:
enum ServiceError {
// internal error caused by being unable to open a file, etc.
Internal(anyhow::Error),
// HTTP error explicitly returned by the handler
Message(StatusCode, String),
// ...additional variants to return JSON-formatted errors and such...
}
impl From<anyhow::Error> for ServiceError { // provided so ? just works
fn from(err: anyhow::Error) -> Self {
ServiceError::Internal(err)
}
}
impl ResponseError for ServiceError {
fn status_code(&self) -> StatusCode {
match self {
ServiceError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
&ServiceError::Message(code, _) => code,
}
}
fn error_response(&self) -> HttpResponse<BoxBody> {
let mut builder = HttpResponseBuilder::new(self.status_code());
match self {
// details of the error are hidden from end user of the API
ServiceError::Internal(_) => builder.body("internal server error")
ServiceError::Message(_, msg) => builder.body(msg.clone()),
}
}
}
In the App setup I use wrap_fn() to consistently log both successful and error responses, so individual handlers don't need to do that. I'd like to provide more details in the log (by dumping the error value) in case it was an internal error. The issue is that wrap_fn() doesn't have access to the ServiceError returned by the handler, but only to actix's ServiceResponse crafted out of it. This makes it impossible to match ServiceError::Internal and log the details of the anyhow::Error that caused the internal error.
Does actix_web provide a mechanism for observing the actual error type returned by a handler for the purpose of loggging? Or at least for attaching some data that my middleware could observe and log?
As a workaround I'm now logging the error in <ServiceError as ResponseError>::error_response(), but that feels like a hack. Such code would seem to belong either in middleware or (if that's impossible) in the place where the routes are set up.
I discovered two ways to achieve this. Posting them as an answer to help others who might be looking for the same questions in the future.
Use Debug
Due to requirements of
ResponseError,ServiceErroris required to implementDebugandDisplay. These implementation are available from the middleware layer (which gives out a&dyn ResponseError) and can be used to retrieve the error.Example
DebugandDisplayimplementation for the type shown in the question:To use them from
App, usewrap_fn()and debug-print the return value ofresp.response().error()which will delegate to theDebugimpl of your error type:Equivalent functionality can be achieved through middleware provided by external crates, such as
tracing_actix_web::TracingLogger.Use extensions to transmit data to middleware
A more general and powerful mechanism is to make use of extensions to transmit data from
ServiceErrortoApp. For example:Appwould access them fromwrap_fnmuch like in the example above, but with the difference that it would no longer be limited toDebugandDisplay, it could transmit any information. In this case it would only log the error that corresponds to "internal" errors, not to business exceptions: