"No reactor running" when calling Runtime::spawn?

I'm trying to test an HTTP endpoint that's created like so (simplified):

pub fn endpoints(
    config: Config,
) -> (
    Server<hyper::server::conn::AddrIncoming, IntoMakeService<Router>>,
) {
    let routes = Router::new()
        .route("/myroute", get(myhandler));
    let server = Server::bind(&config.into()).serve(routes.into_make_service());
    server
}

Note: config contains a port and IP to listen on, and config.into() converts it to a SocketAddr.

In the test I'm creating a runtime so I can spawn the server Future, and I'm using ureq to make a request once the server has started.

#[test]
fn request_successful() {
    let config = Config {
        ip: Ipv4Addr::new(127, 0, 0, 1),
        port: 3456,
    };
    let server = endpoints(config);
    let rt = tokio::runtime::Runtime::new().unwrap();
    rt.spawn(async move { server.await });
    let resp = ureq::get("http://127.0.0.1:3456/myroute")
        .call()
        .unwrap();
    assert_eq!(resp.status(), 200);
}

The test gives me this error:

thread 'tests::request_successful' panicked at 'there is no reactor running, must be called from the context of a Tokio 1.x runtime'

From my own research it looks like this error is usually in response to calling tokio::spawn without having called Runtime::enter (mostly via #[tokio::main]). Here I've created a runtime and I'm calling spawn directly on the runtime so I don't quite understand where this error is coming from.

Can you share your full Cargo.toml here?

[package]
name = "health_checks"
version = "0.0.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
axum = { version = "0.5", default-features = false, features = ["http1", "matched-path"]}
hyper = { version = "0.14.14", features = ["server", "tcp", "stream", "runtime"] }

[dev-dependencies]
tokio = { version = "1", features = ["macros", "test-util", "rt-multi-thread"] }
ureq = {version = "2.5", default-features = false}

You want block_on not spawn

You'll have to adjust how you're doing the request if you need it to happen while the async block is running.

Can you explain why that's the case?

I guess technically you could also use enter with spawn but that's going to give you race conditions.

block_on is the main entry point to a tokio runtime, it's what you want 99% of the time. It runs the future you provide and blocks the current thread until it's completed. If you don't use block_on you have to enter the runtime manually.

Either way though you have a problem. You're creating a runtime (which is multithreaded by default), spawning a setup task on it, and then immediately running code that assumes the setup task ran already. Which very well may not be true.

tokio::test sets up a test runtime and uses block_on to run an async test function, so I usually use that.

Here's the simplest way to do what you're trying to do:

#[tokio::test]
async fn test() {
    // select rather than join since the server isn't set up to shutdown at any point.
    tokio::select! {
        // We want to make sure the server is polled first so it's set up before we make a request. `biased` ensures select polls the futures in a non-random order.
        biased;
        // Poll the server future to ensure the server is ready to receive requests
        _ = endpoints(([127, 0, 0, 1], 3456).into()) => { }
        // Poll the request future after the server has been polled.
        result = reqwest::get("http://127.0.0.1:3456/myroute") => {
            result.unwrap();
        }
    }
}

That doesn't appear to be my issue. I get the no reactor error just trying to bind the address:

pub fn endpoints(
    config: Config,
) -> (
    Server<hyper::server::conn::AddrIncoming, IntoMakeService<Router>>,
) {
    let routes = Router::new()
        .route("/myroute", get(myhandler));
    let builder = Server::bind(&config.into());  // Traceback points at this line
    let server = builder.serve(routes.into_make_service());
    server
}

That doesn't make sense to me because I don't see async anywhere yet, I'm just creating a Future that I'd like to hand off to the user to spawn on their executor. Furthermore, none of the axum, hyper, or tower docs (that I could find) mention needing to be in the context of a runtime to make any of these calls.

bind requires you to be in the Runtime when you call it. You can fix the immediate problem with your code by calling rt.enter() before you call endpoints and holding on to the guard type.

#[test]
fn request_successful() {
    let rt = tokio::runtime::Runtime::new().unwrap();

    let _enter = rt.enter();
    let server = endpoints(([127, 0, 0, 1], 3456).into());
    rt.spawn(async move { server.await });
    let resp = ureq::get("http://127.0.0.1:3456/myroute").call().unwrap();
    assert_eq!(resp.status(), 200);
}

Doing it that way relies on the server not needing to be polled before the request starts though, and I'm not sure whether that's guaranteed to work or not.

You're right about the need to poll, we agree on that.

Is it mentioned in the docs anywhere that you need to be in the runtime when you call bind? At this point I'm just wondering how I was supposed to know that.

It is not, as far as I can find. Which I find somewhat odd.

Generally all that initialization code gets run inside async functions so it just doesn't matter. You're already inside the runtime if an async function is running.