Skip to main content
Lesson 10 of 12Beginner⏱ 10 mins

Adding Middleware

Logging and CORS.

75% of path

Learning Objectives

  • Add the Logger middleware

##Code that runs around every request

Some logic belongs to every request, not to one handler: logging, compression, authentication, CORS headers, request timing. Copying it into each handler would be tedious and error prone. Middleware wraps your handlers so that logic runs in one place.

#The onion model

Think of middleware as layers of an onion. A request travels inward through each layer to the handler, and the response travels back out through the same layers in reverse order. Each layer can inspect or change the request on the way in, and the response on the way out.

#Built-in middleware

Add middleware with .wrap(). These two ship with Actix Web:

code_blocksrust
use actix_web::{middleware, App, HttpServer};

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    std::env::set_var("RUST_LOG", "info");
    env_logger::init();

    HttpServer::new(|| {
        App::new()
            .wrap(middleware::Logger::default())
            .wrap(middleware::Compress::default())
            // .service(...)
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

Logger prints one line for each request. It needs the env_logger crate (or any other log backend) and the RUST_LOG variable. Compress automatically gzips responses when the client supports it.

#Order matters

The middleware registered last runs first on an incoming request. If you wrap A and then B, a request goes through B, then A, then your handler, and the response returns through A, then B. Put things like logging at the end of the chain so it sees every request.

#Scoping middleware

You do not have to apply a layer to the whole app. Attach it to a scope to protect only part of the API:

code_blocksrust
use actix_web::web;

App::new().service(
    web::scope("/admin")
        .wrap(middleware::Logger::default())
        // .route(...)
);

#Writing your own

The simplest custom middleware is a function with from_fn:

code_blocksrust
use actix_web::{
    body::MessageBody,
    dev::{ServiceRequest, ServiceResponse},
    middleware::{from_fn, Next},
    Error,
};

async fn timing(
    req: ServiceRequest,
    next: Next<impl MessageBody>,
) -> Result<ServiceResponse<impl MessageBody>, Error> {
    let start = std::time::Instant::now();
    let res = next.call(req).await?;
    println!("request took {:?}", start.elapsed());
    Ok(res)
}

// App::new().wrap(from_fn(timing))

Code before next.call(req) runs on the way in, and code after it runs on the way out.

Interactive Code Inspector

Select any highlighted line of code below to deeply inspect its mechanics, understand why it exists, and what happens if you change it.

src/main.rs
1use actix_web::{middleware::Logger, App, HttpServer};
2
3#[actix_web::main]
4async fn main() -> std::io::Result<()> {
5 env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
6
7 HttpServer::new(|| {
8 App::new()
9 .wrap(Logger::default())
10 })
11 .bind(("127.0.0.1", 8080))?
12 .run()
13 .await
14}
Line 9
.wrap(Logger::default())

What it does

Adds the request logging middleware.

Why use it?

To see HTTP requests being made in your console.

How it works underneath

Intercepts the request before the handler, and the response after.

If you remove it

The server will be silent.

"What If?" Experiment

Learning by doing. Modify the code to see what happens.

Goal

See middleware ordering.

Starting Code
App::new().wrap(a).wrap(b)
Suggested Modification

Add a println! at the start of each middleware and send one request.

Expected Result

b prints first and a second on the way in, then they return in the opposite order.

Explanation: The last layer registered is the outermost, so it handles the request first and the response last.

Common Mistakes

Anticipate and avoid these frequent pitfalls when working with this concept.

!

Logger prints nothing

Logger only emits log records; without a log backend and RUST_LOG nothing is displayed.

Incorrect Pattern
App::new().wrap(middleware::Logger::default()) // but no env_logger::init()
Correct Pattern
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));

Explanation: Initialize a log backend such as env_logger before starting the server so the log lines are actually printed.

!

Assuming middleware runs in registration order

Actix wraps each new layer around the previous ones, so the newest is outermost.

Incorrect Pattern
// expecting auth to run before logging .wrap(auth).wrap(logger)
Correct Pattern
.wrap(logger).wrap(auth)

Explanation: The last wrap() is the outermost layer and sees the request first. Register in the reverse of the order you want requests to flow.

Practical Exercise

Time every request

Task

Write a from_fn middleware that prints how long each request took and register it on the App.

Requirements

  • Use middleware::from_fn
  • Measure with Instant before and after next.call
  • Print the elapsed time

Need a hint?

Try solving the exercise in your local environment first. Once you have a solution, or if you get completely stuck, reveal the answer.

Real-World Application

Production services typically stack request-ID, logging, metrics, compression, CORS and authentication middleware, with the auth layer applied only to the protected scopes.

Key Takeaways

  • wrap() adds middleware; the last one registered runs first on the way in.
  • Logger and Compress are built in and cover most simple needs.
  • Middleware can be applied to the whole App or just a web::scope.
  • from_fn is the easiest way to write your own middleware.

Finished reading?

Mark this lesson as done to track your progress and earn XP.