Post

Middleware

Let's use middleware in FastAPI.

Middleware

When building APIs with FastAPI, there are often cases where you want to handle certain tasks across all requests or responses.
For example, you might want to log how long each request takes to process or add a common header to every response.
This is where middleware comes in.

In this post, we’ll dive into what middleware is in FastAPI and how you can create your own.

What is Middleware?

Middleware refers to code that runs before the request reaches the route handler and before the response is sent back to the client.

Middleware is ideal for handling operations such as:

  • Logging every request
  • Authentication and authorization checks
  • Modifying request/response data
  • Setting CORS policies
  • Adding common response headers
  • Measuring request processing time

In simple terms, middleware acts as a layer between the client request and the route handler, intercepting the flow.

Writing Middleware in FastAPI

In FastAPI, the easiest way to create middleware is by using the @app.middleware("http") decorator.

Here’s the basic structure:

1
2
3
4
5
6
7
8
# main.py

@app.middleware("http")
async def my_middleware(request: Request, call_next):
    # Code to run before the request is processed
    response = await call_next(request)
    # Code to run after the response is generated
    return response
  • request: The incoming HTTP request object.
  • call_next: A function that passes the request to the next stage (usually the route handler) and returns the response.
  • response: The final HTTP response after processing the request.

Important Notes:

  • Middleware runs for every HTTP request. If you want to apply it selectively, you’ll need to add extra logic inside the middleware.
  • You must call call_next(request); otherwise, the request won’t proceed to the route handler.
  • Avoid putting heavy or slow operations inside middleware, as it can negatively impact the overall API performance.

Use Cases for Middleware

Example1: User-Defined Request

If you want to log information about every request, middleware makes it easy to implement.
In this example, the check_url_time middleware logs the URL of each incoming request and the status code of the response.
This allows you to easily monitor the behavior of your application.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# main.py

logging.basicConfig(
    level=logging.INFO,
    format="[%(levelname)s] %(asctime)s %(message)s",
    handlers=[logging.StreamHandler()]
)

@app.middleware("http")
async def check_url_time(request: Request, call_next):
    start_time = time.time()
    logging.info(f"Request URL: {request.url.path}")
    
    response = await call_next(request)
    
    process_time = time.time() - start_time    
    response.headers["Request-URL"] = str(request.url.path)
    response.headers["MyProcess-Time"] = str(process_time)
    logging.info(f"Response status code: {response.status_code}")
    
    return response
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
async def log_response_info(response: aiohttp.ClientResponse):
    print("\n[Response Info]")
    print(f"Status       : {response.status} {response.reason}")
    print(f"URL          : {response.url}")
    print(f"Method       : {response.method}")
    print(f"Content-Type : {response.content_type}")
    print(f"Charset      : {response.charset}")
    print(f"Cookies      : {dict(response.cookies)}")
    print(f"Headers:")
    pprint(dict(response.headers))
    print(f"OK?          : {response.ok}")

    try:
        data = await response.json()
        print(f"\nJSON Body:")
        print(f"type(data): {type(data)}")
        pprint(data)
    except aiohttp.ContentTypeError:
        text = await response.text()
        print(f"\nText Body:\n{text}")

try:    
    url = "http://127.0.0.1:7249/ds2man/basic/request&response/case2"
    payload = {
         "key1": "value1", 
         "key2": "value2"
    }
    async with aiohttp.ClientSession(
        trust_env=True, timeout=aiohttp.ClientTimeout(total=120)
    ) as session:
        async with session.get(url) as response:
            response.raise_for_status()
            await log_response_info(response)
except ClientResponseError as e:
        print(f"[ClientResponseError] {e.status}: {e.message}")
except Exception as e:
    print(f"[Unhandled Exception] {e}")

As shown in the logs below, the custom middleware logs both the request URL (/ds2man/basic/request&response/case1) and the response status code (200) before the response is returned, offering useful insights into each HTTP request-response cycle.

1
2
3
[INFO] 2025-05-11 08:23:57,912 Request URL: /ds2man/basic/request&response/case2
[INFO] 2025-05-11 08:23:57,913 Response status code: 200
INFO:     127.0.0.1:11373 - "GET /ds2man/basic/request%26response/case2 HTTP/1.1" 200 OK

Example2: Adding CORS Header

When developing APIs with FastAPI, handling CORS (Cross-Origin Resource Sharing) is essential. Especially when the frontend (e.g., React, Vue, Svelte) and backend (FastAPI) are hosted on different domains, browsers may block requests due to CORS policies. To solve this, you need to add the appropriate CORS headers to your server responses.

In this post, we’ll walk through how to manually add CORS headers using custom middleware in FastAPI.

Setting up CORS in FastAPI is very simple. FastAPI provides fastapi.middleware.cors.CORSMiddleware for this purpose, and CORS issues can be resolved with just a few straightforward configurations.

In this example, CORS settings are configured using CORSMiddleware.

  • allow_origins: Specifies the list of permitted origins. Only the domains listed here will be allowed to access the API.
  • allow_credentials: Determines whether requests that include credentials such as cookies should be permitted by the browser.
  • allow_methods: Defines the HTTP methods allowed for cross-origin requests. Setting it to ["*"] permits all methods.
  • allow_headers: Defines the HTTP headers allowed for cross-origin requests. Setting it to ["*"] permits all headers.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# main.py

origins = [
    "http://ds2man.github.io",
    "https://ds2man.github.io",
    "http://localhost:7249"
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

Open Chrome (or Edge, Brave, Safari) and navigate to any webpage.
Press F12 to open the Developer Tools, go to the Console tab, and paste the following code:

1
2
3
4
fetch("http://127.0.0.1:7249/ds2man/model1/async/?question=Where%20is%20the%20capital%20of%20Korea&user_name=DS2Man")
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Error:', error));

If everything is configured correctly, you should see the following output in the console: Middleware with CORS header added Middleware with CORS header

If the Server didn’t have CORS Configured, the browser would display a CORS error message like this: Middleware without CORS header Middleware without CORS header

For testing purposes or in a local development environment, there may be situations where requests from all origins need to be allowed. In such cases, allow_origins can be set to ["*"].

1
2
3
4
5
6
7
8
9
# main.py

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

Example3: Class-Based Middleware

In addition to functions, middleware can also be defined using classes.
Class-based middleware is useful when maintaining state or performing initialization tasks is required.

The following example demonstrates a redirect and return process.
For reference, this was slightly modified and tested based on the implementation used in OpenWebUI.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# main.py

class RedirectMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        # Check if the request is a GET request
        if request.method == "GET":
            path = request.url.path # path: /ds2man/basic/middleware/class_based/
            # urlparse(str(request.url)): ParseResult(scheme='http', netloc='127.0.0.1:7249', path='/ds2man/basic/middleware/class_based/', params='', query='question=Hello&user_name=DS2Man', fragment='')
            # parse_qs(urlparse(str(request.url)).query): {'q': ['Hello'], 'user': ['DS2Man']}
            query_params = dict(parse_qs(urlparse(str(request.url)).query))

            # Check for the specific watch path and the presence of 'v' parameter
            if path.endswith("/middleware/class_based/") and "question" in query_params:
                question = query_params["question"][0]
                user_name = query_params.get("user_name", ["unknown"])[0]
                encoded_params = urlencode({"q": question, "user": user_name})
                redirect_url = f"/redirected_endpoint?{encoded_params}"
                return RedirectResponse(url=redirect_url)

        # Proceed with the normal flow of other requests
        response = await call_next(request)
        return response
    
@app.get("/redirected_endpoint")
async def redirected_endpoint(q: str, user: str):
    start_time = time.time()

    response_data = {
        "message": "This request was redirected!",
        "question": q,
        "user_name": user,
    }

    response = JSONResponse(content=response_data)
    end_time = time.time()

    response.headers["Request-URL2"] = "/redirected_endpoint"
    response.headers["MyProcess-Time2"] = f"{(end_time - start_time):.10f}"

    return response
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
try:
    url = "http://127.0.0.1:7249/ds2man/basic/middleware/class_based/"
    payload = {
        "question": "Where is the captial of Korea?",
        "user_name": "DS2Man"
    }

    async with aiohttp.ClientSession(
        trust_env=True, timeout=aiohttp.ClientTimeout(total=120)
    ) as session:
        async with session.get(url, params=payload) as response: # Therefore, the payload must be passed as parameters.
            response.raise_for_status()
            await log_response_info(response)

except ClientResponseError as e:
    print(f"[ClientResponseError] {e.status}: {e.message}")
except Exception as e:
    print(f"[Unhandled Exception] {e}")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[Response Info]
Status       : 200 OK
URL          : http://127.0.0.1:7249/redirected_endpoint?q=Hello&user=DS2Man
Method       : GET
Content-Type : application/json
Charset      : None
Cookies      : {}
Headers:
{'Content-Length': '82',
 'Content-Type': 'application/json',
 'Date': 'Sat, 10 May 2025 23:54:17 GMT',
 'Server': 'uvicorn',
 'myprocess-time': '0.49500060081481934',
 'myprocess-time2': '0.0000000000',
 'request-url': '/redirected_endpoint',
 'request-url2': '/redirected_endpoint'}
OK?          : True

JSON Body:
type(data): <class 'dict'>
{'message': 'This request was redirected!',
 'question': 'Hello',
 'user_name': 'DS2Man'}

FastAPI Middleware Execution Order Rules

Execution Rules

  • Middleware added using app.add_middleware() (class-based middleware) is executed in the order it was registered when handling a request.
  • Middleware defined using the @app.middleware("http") decorator (function-based middleware) is executed after all class-based middleware.
  • During a request, middleware is executed from top to bottom (registration order).
  • During a response, middleware is executed in reverse order (bottom to top).

Example Structure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class RedirectMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        print(">>> [Request] RedirectMiddleware")
        response = await call_next(request)
        print("<<< [Response] RedirectMiddleware")
        return response

@app.middleware("http")
async def check_url_time(request: Request, call_next):
    print(">>> [Request] check_url_time")
    response = await call_next(request)
    print("<<< [Response] check_url_time")
    return response

app.add_middleware(RedirectMiddleware)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"],
)

Request Flow (Incoming)

  1. RedirectMiddleware (class-based, added first)
  2. CORSMiddleware (class-based, added second)
  3. check_url_time (function-based, defined later)
  4. Router handler (@app.get, @app.post, etc.)

Response Flow (Outgoing)

  1. Router handler returns a response.
  2. check_url_time middleware processes the response.
  3. CORSMiddleware processes the response.
  4. RedirectMiddleware processes the response.

Visual Flow Diagram

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[Client Request]
    ↓
RedirectMiddleware (request)
    ↓
CORSMiddleware (request)
    ↓
check_url_time (request)
    ↓
Router (handler)
    ↓
check_url_time (response)
    ↓
CORSMiddleware (response)
    ↓
RedirectMiddleware (response)[Client Response]
This post is licensed under CC BY 4.0 by the author.