docs: clarify cross-level middleware ordering between App and Scope

The existing docs stated middleware executes in 'opposite order as
registration' without clarifying this only applies within a single
level (App, Scope, or Resource).

When middleware is registered at both the App level and a Scope level,
App middleware *always* runs first because it structurally wraps the
entire request router. Scope middleware cannot bypass or run before
App middleware.

Changes:
- Qualify the 'opposite order' statement in middleware/mod.rs to
  note it applies within a single level only
- Add a new 'Cross-Level Middleware Ordering' section with an ASCII
  diagram, explanation, and a code example showing the recommended
  per-scope pattern
- Add a '# Middleware Ordering' note to App::wrap pointing to the
  new section

Closes: https://github.com/actix/actix-web/issues/2993
This commit is contained in:
Ashish Kaushik 2026-02-23 05:24:26 +05:30
parent 15cfc03878
commit af844c6d08
2 changed files with 74 additions and 1 deletions

View File

@ -326,6 +326,20 @@ where
/// Middleware can be applied similarly to individual `Scope`s and `Resource`s. /// Middleware can be applied similarly to individual `Scope`s and `Resource`s.
/// See [`Scope::wrap`](crate::Scope::wrap) and [`Resource::wrap`]. /// See [`Scope::wrap`](crate::Scope::wrap) and [`Resource::wrap`].
/// ///
/// # Middleware Ordering
///
/// App-level middleware wraps the entire request router and **always executes before** any
/// [`Scope`](crate::Scope)-level or [`Resource`]-level middleware, regardless of the order in
/// which services are registered on the builder. Scope middleware cannot bypass or run before
/// App middleware.
///
/// If you need certain routes to skip a middleware, apply that middleware at the
/// [`Scope`](crate::Scope) level instead of the `App` level.
///
/// For more info on middleware ordering, see the
/// [Cross-Level Middleware Ordering](crate::middleware#cross-level-middleware-ordering)
/// section.
///
/// For more info on middleware take a look at the [`middleware` module][crate::middleware]. /// For more info on middleware take a look at the [`middleware` module][crate::middleware].
/// ///
/// # Examples /// # Examples

View File

@ -15,7 +15,10 @@
//! - Access external services (e.g., [sessions](https://docs.rs/actix-session), etc.) //! - Access external services (e.g., [sessions](https://docs.rs/actix-session), etc.)
//! //!
//! Middleware is registered for each [`App`], [`Scope`](crate::Scope), or //! Middleware is registered for each [`App`], [`Scope`](crate::Scope), or
//! [`Resource`](crate::Resource) and executed in opposite order as registration. //! [`Resource`](crate::Resource) and executed in opposite order as registration. Note that this
//! reverse ordering applies _within_ a single level (`App`, `Scope`, or `Resource`). When
//! middleware exists at multiple levels, App-level middleware always runs first. See
//! [Cross-Level Middleware Ordering](#cross-level-middleware-ordering) for details.
//! //!
//! # Simple Middleware //! # Simple Middleware
//! //!
@ -120,6 +123,62 @@
//! middleware are run in reverse order for [`App`] _and then_ in reverse order for the wrapped //! middleware are run in reverse order for [`App`] _and then_ in reverse order for the wrapped
//! service. //! service.
//! //!
//! # Cross-Level Middleware Ordering
//!
//! When middleware is registered on _both_ an [`App`] and a [`Scope`](crate::Scope) (or
//! [`Resource`](crate::Resource)), the App-level middleware **always** runs before the
//! Scope/Resource-level middleware. This is because App-level middleware wraps the entire request
//! router, and routing to a specific scope or resource happens _inside_ that middleware chain.
//!
//! ```plain
//! Request
//! ⭣
//! ╭──────────────────────┼──────╮
//! │ App Middleware │ │
//! │ ╭────────────────────┼─────╮│
//! │ │ Router │ ││
//! │ │ ╭─────────────┼────╮││
//! │ │ │ Scope MW │ │││
//! │ │ │ ╭───────────┼───╮│││
//! │ │ │ │ Resource │ ││││
//! │ │ │ │ MW │ ││││
//! │ │ │ │ ╭─────────┼──╮││││
//! │ │ │ │ │ Handler │ │││││
//! │ │ │ │ ╰─────────┼──╯││││
//! │ │ │ ╰───────────┼───╯│││
//! │ │ ╰─────────────┼────╯││
//! │ ╰────────────────────┼─────╯│
//! ╰──────────────────────┼──────╯
//! ⭣
//! Response
//! ```
//!
//! This means that a middleware applied to a [`Scope`](crate::Scope) **cannot** bypass or run
//! before middleware applied to the enclosing [`App`]. For example, if authentication middleware
//! is registered on `App`, a "bypass auth" middleware on a scope will not work because the App
//! auth middleware has already executed by the time routing reaches that scope.
//!
//! If you need different middleware behavior for different groups of routes, apply middleware at
//! the scope level rather than the app level:
//!
//! ```
//! use actix_web::{web, middleware, App};
//!
//! // Instead of applying auth middleware at the App level and trying
//! // to bypass it on certain scopes, apply it per-scope:
//! let app = App::new()
//! .service(
//! web::scope("/api")
//! .wrap(middleware::DefaultHeaders::new()) // e.g., auth middleware
//! // ... api routes
//! )
//! .service(
//! web::scope("/public")
//! // no auth middleware here
//! // ... public routes
//! );
//! ```
//!
//! # Middleware Traits //! # Middleware Traits
//! //!
//! ## `Transform<S, Req>` //! ## `Transform<S, Req>`