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.
Due to requirements of ResponseError
, ServiceError
is required to implement Debug
and Display
. These implementation are available from the middleware layer (which gives out a &dyn ResponseError
) and can be used to retrieve the error.
Example Debug
and Display
implementation for the type shown in the question:
impl std::fmt::Display for ServiceError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use ServiceError::*;
match self {
Internal(err) => write!(f, "{err:#}"),
Message(code, msg) => write!(f, "http error {code}: {msg}"),
}
}
}
impl std::fmt::Debug for ServiceError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self) // reuse Display impl
}
}
To use them from App
, use wrap_fn()
and debug-print the return value of resp.response().error()
which will delegate to the Debug
impl of your error type:
App::new()
.wrap_fn(|req, srv| {
srv.call(req).instrument(span.clone()).map(move |resp| {
if let Ok(resp) = &resp {
if let Some(err) = resp.response().error() {
// {:?} invokes ServiceError's Debug impl
tracing::error!("request handling error: {:?}", err.as_response_error());
}
}
resp
})
})
Equivalent functionality can be achieved through middleware provided by external crates, such as tracing_actix_web::TracingLogger
.
A more general and powerful mechanism is to make use of extensions to transmit data from ServiceError
to App
. For example:
pub struct FormattedInternalError(pub String);
impl ResponseError for ServiceError {
// ...fn status_code() unchanged...
fn error_response(&self) -> HttpResponse<BoxBody> {
let mut builder = HttpResponseBuilder::new(self.status_code());
match self {
ServiceError::Internal(_) => {
// transmit details internally
builder
.extensions_mut()
.insert(FormattedInternalError(format!("{err:?}")));
// ...but hide them externally
builder.body("internal server error")
}
ServiceError::Message(_, msg) => builder.body(msg.clone()),
}
}
}
App
would access them from wrap_fn
much like in the example above, but with the difference that it would no longer be limited to Debug
and Display
, it could transmit any information. In this case it would only log the error that corresponds to "internal" errors, not to business exceptions:
App::new()
.wrap_fn(|req, srv| {
srv.call(req).instrument(span.clone()).map(move |resp| {
if let Ok(resp) = &resp {
if let Some(FormattedInternalError(formatted)) =
resp.response().extensions().get::<FormattedInternalError>()
{
tracing::error!("request handling error: {formatted}");
}
}
resp
})
})