Hyper doesn't report errors

Continuing the discussion from Answering HTTP requests:

I've been looking into hyper. I'm currently facing the problem that errors don't get reported.

This is my test setup:

Cargo.toml:

[dependencies]
tokio = { version = "1", features = ["full"] }
hyper = { version = "0.14", features = ["http1", "http2", "server", "runtime", "tcp", "deprecated"] }
anyhow = { version = "1", features = ["backtrace"] }

src/main.rs:

use hyper::service::{make_service_fn, service_fn};
use hyper::{Body, Response, Server};

use std::net::{Ipv6Addr, SocketAddr};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let addr = SocketAddr::from((Ipv6Addr::LOCALHOST, 3000));
    let make_svc = make_service_fn(move |_conn| async move {
        Ok::<_, anyhow::Error>(service_fn(move |_req| async move {
            anyhow::bail!("Test");
            #[allow(unreachable_code)]
            Ok::<Response<Body>, anyhow::Error>(unreachable!())
        }))
    });
    Server::bind(&addr).serve(make_svc).await?;
    Ok(())
}

If I access the server with a webbrowser, the webbrowser reports a connection reset and stdout of the server process doesn't report anything at all.

% cargo run
   Compiling http-error v0.1.0 (/usr/home/jbe/rust-experiment/http-error)
    Finished dev [unoptimized + debuginfo] target(s) in 0.40s
     Running `target/debug/http-error`

And in a separate process:

% curl -i http://localhost:3000/
curl: (52) Empty reply from server

What can I do to enable error reporting or retrieve the anyhow::Error somewhere?


I found a workaround, but I'm not really happy with it and don't believe this is the idiomatic way to handle this:

use anyhow::Context as _;

async fn handler(_req: Request<Body>) -> anyhow::Result<Response<Body>> {
    anyhow::bail!("Test");
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let addr = SocketAddr::from((Ipv6Addr::LOCALHOST, 3000));
    let make_svc = make_service_fn(move |_conn| async move {
        Ok::<_, anyhow::Error>(service_fn(move |req| async move {
            match handler(req).await.context("error in handler") {
                Ok(res) => Ok(res),
                Err(err) => {
                    eprintln!("#### ERROR #### {err:?}\n");
                    Err(err)
                }
            }
        }))
    });
    Server::bind(&addr).serve(make_svc).await?;
    Ok(())
}

Generally for recoverable errors[1] you want to return some sort of error response rather than propagate the Err variant. There's no way for hyper to do that automatically since a JSON API endpoint and an HTML endpoint need completely different responses[2].

Most web server frameworks that build on hyper intercept all recoverable errors and convert them to HTTP responses of some kind, or force you to do that via their API design.

The correct way for you to handle this will depend on what you want the handler to return in the Ok case. If it's a JSON response you can just map the error to to a JSON value containing the display string of the error. There are reasons not to do it in exactly that way when you're using type erasure though, since the Display impl for some types could in theory leak sensitive information.


  1. i.e. errors that don't have to do with the state of the connection ↩︎

  2. you could argue that an empty HTTP error response would be better than killing the connection, but in the vast majority of cases that would still be insufficient and you'd have to implement your own logic anyway ↩︎

1 Like

I think I would argue like that. I would say that's what a 500 Internal Server Error is meant for. I still see a good purpose for TCP connection aborts (resulting in TCP resets). I would trigger or send these if the server is in such a critical state that it isn't even able to send a proper HTTP error response. Setting the right socket options can also make the operating system send such a TCP reset even under worst scenarios like a segmentation fault.

But I guess I can achieve that like you described here:

So my "workaround" is somehow idiomatic?

In a productive envirionment, i would guess that it's best practice to not give any internal error details and instead log them. What I'm still puzzled about is what hyper is doing with my errors?

Is there literally a drop(error) somewhere? Are these errors really meant to be silently discarded if they fall through?

P.S.: I just noticed there is hyper-1.0.0-rc.3 out already. I will try to port my code to the new version and see if error handling changed with the upcoming 1.0 release.

The error is logged: hyper/server.rs at 0.14.x · hyperium/hyper · GitHub

1 Like

Ah, the logging is done by this tracing crate. I haven't worked with that yet, but will look into it.

In hyper-1.0.0-rc.3, it seems to be returned by the hyper::server::conn::http1::Builder::serve_connection method. Though i see tracing as a dependency as well. Anyway, I have a starting point now.

I will try to handle every error myself then (and maybe consider using tracing for my own/handled errors as well).

That lower level API exists in hyper 0.14 as well if you want to use it.

The way I normally deal with this is by wrapping the service with a tower_http::catch_panic layer so it automatically catches panics and translates them to 500 responses.

Here's a real-world example of setting up panic-catching middleware:

2 Likes

I think I'm okay with a sending a 500 response on panic (though I guess a 500 is better if possible). But what I certainly would like is to send a 500 response on Result::Err. I think I could either do that manually myself, or use some other helper types/methods/crates.

The TraceLayer in your example is imported from yet another crate, tower_http. I wonder if hyper is too low-level for me? Maybe I can also start with hyper at first, and later care about more advanced error handling, compression, authorization, etc.

I guess I should also read the README of tracing (see on crates.io).

I see both tower and tracing are listed in the section on tokio's stack (see tokio website). So I guess using tower/tower_http eventually is the right way to go.

Tower is just an abstraction for modeling middleware-like functionality in a generic way. You may be able to find some existing functionality for tower that does what you want, but it isn't exactly higher level than hyper is.

That sounds pretty abstract indeed :sweat_smile:.

From the docs:

Generic components, like timeouts, rate limiting, and load balancing, can be modeled as Services that wrap some inner service and apply additional behavior before or after the inner service is called.

That sounds a bit more concrete. So I guess I need that to go from a testing-only HTTP server to one that's exposed to the public (which requires more care).

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.