I’ve hit an issue with actix-web recently, and ended up learning a bit about how it does routing (or URL dispatch as they name it).

Here’s a simplified version of my problem:

// service code
#[get("/{path:.*}")]
pub async fn basic_handler(params: web::Path<String>) -> HttpResponse {
    // ...
}

#[post("/auth/")]
pub async fn auth_handler() -> HttpResponse {
    // ...
}

// in main
let auth_service = web::scope("")
    .wrap(auth_middleware)
    .service(auth_handler);
App::new()
    .service(authenticated_scope)
    .service(basic_handler)

auth_middleware is a custom middleware I wrote which checks for the existence of an auth token in a header or cookie, then validates the token. The middleware responds early with a 401 if the token is missing, which ensures that the protected handlers can only be reached by authenticated users.

I expected Actix to realize that if a request doesn’t match /auth/, it should go to the basic_handler and the authentication middleware shouldn’t run. But the middleware did run even if the path had nothing to do with /auth/! That would cause the middleware to respond with a 401 and stops it from propagating, so it never reached basic_handler.

The solution, it turns out, is using the web::scope to scope out the authenticated routes. If the scope doesn’t match, Actix then seems to skip over that entire scope and never runs the middleware. Here’s the same code, fixed:

// service code
#[get("/{path:.*}")]
pub async fn basic_handler(params: web::Path<String>) -> HttpResponse {
    // ...
}

#[post("/")]  // <-- change here
pub async fn auth_handler() -> HttpResponse {
    // ...
}

// in main
let auth_service = web::scope("/auth") // <-- change here
    .wrap(auth_middleware)
    .service(auth_handler);
App::new()
    .service(authenticated_scope)
    .service(basic_handler)