MOOSSin
Menu

All posts · Open Source Tools

FastAPI and uvicorn: Frameworks rarely stop me; what does stop me are health checks, prefixes, and trust boundaries.

FastAPI is built on Starlette and Pydantic, and uvicorn is the ASGI server that runs it. This article will not repeat the tutorial, but will only talk about a few things that you will encounter after going live: how health checks can be misleading, who should strip the prefix when it is attached to the path prefix, whether or not to trust the proxy header, and how to test without TestClient. Let's start with a story about the "green light".

There is a very special kind of despair where all the lights are green, but nothing moves.

Every service on the dashboard shows as healthy, there are no errors in the logs, and the monitoring graphs are perfectly normal. Then, a user messages you saying the website is inaccessible. You rework it, and it still won't open. You look at the dashboard again, and it's still green. At that moment, you start to question the meaning of the word "green light," and half of this article is about this, just using FastAPI as a backdrop.

First, introduce the main character

FastAPI is a Python web framework published by Sebastián Ramírez in 2018, licensed under the MIT license. It consists of Starlette for routing, middleware, and ASGI, and Pydantic for data validation. Type annotations also serve as documentation and validation rules. You write a function, specify the type of your parameters, and it checks the input, generates the documentation, and returns the correct error messages. First-time users often experience a sense of bewilderment, similar to using an automatic dishwasher for the first time.

uvicorn is the most commonly used ASGI server, licensed under BSD.uvicorn[standard] I'll install uvloop, httptools, websockets, and watchfiles, each to make certain parts of the code faster or more convenient. My backends for several websites use this setup. The framework itself rarely makes me stop and think; there are already plenty of tutorials. However, the following things each taught me a lesson, and I only learned them after they were deployed.

Health checks can be deceiving; they can be misleading in two ways.

Container health checks are usually performed on/health Send a request and check the status code. Sounds like it's hard to make a mistake, right? I thought so too.

The first type of scam: You used...TrustedHostMiddleware Allowing only official hostnames is good practice and blocks requests with random IP addresses. However, health checks are performed inside the container.127.0.0.1 The system was sending out requests with a default host, which resulted in each request being blocked as a 400 error, making the container perpetually "unhealthy." The service itself was fine; it was just that its health check reports kept failing because people going through the wrong doors were stopped by security. Health checks require the correct host header.

The second scam is the reverse, and it's the same story as at the beginning. There's a background thread in the application doing something important; even if that thread dies, the webpage still opens, and the health check shows all green. This is like someone with a normal heartbeat, normal blood pressure, and able to walk and jump, but who hasn't slept for three days. Let.../health Check if the thread is still running; if not, return a 503 error, and the container will be restarted. A health check should check "whether this service can perform its function," not "whether this webpage can be opened."

When attached to a prefix, who removes the prefix?

It's a common deployment to combine several applications under the same hostname, each with its own path prefix. uvicorn uses...--root-path /blog Tell the application where you are attached, and the application will use it to generate the correct links and URLs.

Here's a detail that could easily take up an afternoon. According to current ASGI specifications,scope["path"] It's the complete path including root_path. uvicorn will append the prefix back, so the prefix needs to be removed during reverse proxy forwarding; otherwise, the application will see two layers./blog/blog/posts This kind of thing... When I first saw it, I thought it was a mistake in my link generator. I spent ages fixing the links, and the more I changed them, the worse it got. The documentation wasn't very prominent, so pinning it to the test is more reliable than memorizing it, because you'll definitely forget it the next time you move up your things.

Agent Header: I don't believe it, I just don't believe it.

uvicorn by default only trusts data from the local machine.X-Forwarded-* header,--forwarded-allow-ips It can be relaxed. My choice is to completely disregard these headers: the reverse proxy retains the original Host forwarding, and the application uses the Host to determine the canonical and the address, without looking at X-Forwarded.

Less trust means less potential for problems caused by spoofed headers. The trade-off is that the client IP seen by the application is from a proxy, which is irrelevant to my purpose. It's a bit like receiving a letter with "This is from the principal" written on the envelope—would you believe it? My approach is to ignore what's written on the envelope and only look at which mailbox it came from.

Two applications, two factories

For both the public service and the backend management interface, I built two applications using two factory functions, each run by a separate uvicorn process. The backend only listens for the local loopback address. The public one doesn't connect to the backend router or parse the backend's session cookies. Even if someone gains a vulnerability in the public service, the backend code isn't in that process at all, so there's nothing to bypass.

Taking it a step further, you could maintain a route classification table, indicating for each route whether it's publicly accessible to browsers, publicly accessible to other machines, local only, or disabled; if a new route is added but its classification is forgotten, it will be rejected at startup. The first time you encounter this, it might seem annoying, like being constantly asked by your roommate, "Did you bring your keys?" Later, you'll see it as evidence that the system is functioning, because one day you really will forget them.

Mini-drama: Should I learn FastAPI or something else?

"I want to do a project; should I learn FastAPI, Django, or Flask?"

"Does your project require existing components like a backend management system, user system, and ORM?"

"It doesn't seem necessary, there are only a few APIs."

"FastAPI is a good fit; it gives you just enough and doesn't cram a bunch of stuff into your system."

"What about Flask? Everyone says it's simple."

"Flask is fine too, but FastAPI adds type checking and automatic documentation. You'll be very grateful for the automatically generated API documentation when you're writing reports."

"So it was chosen for the report."

"That's half true. The other half is that its error messages actually tell you which field is wrong, instead of giving you a 500 and making you guess."

Testing without TestClient

StarletteTestClient It depends on httpx. If httpx isn't in the project's configuration file, instead of installing an extra package for testing, you can write a very small in-process ASGI client: create a scope, send request events, and receive response events. A few dozen lines are enough, and you can even add another one.root_path The parameters are simulated when attached to a prefix, and the same test is run in two modes.

The night I wrote this little tool, I just didn't want to install another package, but it ended up becoming the program I ran the most during testing. Sometimes taking a detour leads to a better place, although most of the time a detour is just a detour.

Who are those two floors below?

FastAPI is often considered a framework, but it's more like a highly efficient integrator. Routing, the intermediary layer, and request/response functionality are all handled by Starlette. Starlette's author, Tom Christie, is also the creator of the Django REST framework and httpx; his work shares a common clean aesthetic. Document validation is handled by Pydantic. The 2023 v2 version rewrote the core in Rust, resulting in validation speeds several times faster. This is why FastAPI consistently boasts impressive performance figures.

Automatic file handling is the first reason many people love it. You write the API, open.../docs An interactive Swagger interface is already there, listing every endpoint, parameter, and response format. You can even try typing it directly on the webpage. Taking a screenshot of this page while writing a report is ten times faster than drawing the API table yourself./redoc It's a different style of version, and it looks more like a formal document.

It's worth knowing the differences between ASGI and WSGI. WSGI is the interface used in the Python web world for twenty years; it's synchronous, with one thread per request. ASGI is the asynchronous version, allowing multiple I/O operations to occur simultaneously within a single thread. FastAPI accepts both types of functions.async def The endpoint runs in the event loop, generally...def Endpoints are dropped into the thread pool, so if you accidentally...async def The code snippet mentioned a synchronous call that would freeze, causing the entire service to stop. This is a common pitfall for beginners, and the symptoms are quite strange: the service itself isn't broken, but it occasionally stops completely for a second.

Three common misunderstandings

The first misconception is that "FastAPI is fast because it uses some kind of black magic." It's fast because Starlette and uvloop are fast, and because it doesn't cram a bunch of unnecessary stuff into your endpoints. If you write slow things in your endpoints, it will still be slow.

The second misconception is that "a type label equals security." Type labels check the shape, not the intent. A field labeled with...str The user sends in a piece of SQL, and it works too.str Verifying the shape is Pydantic's job; verifying the content is your job.

The third misconception is that "uvicorn can directly interact with the outside world." It can, but usually an nginx or Caddy is placed in front to handle TLS, static files, and some unusual requests. uvicorn focuses on running the application; other tasks are handled by specialized tools. This is a division of labor, not a lack of trust.

A small exercise you can try over the weekend

Write a ten-line FastAPI application, one/health Endpoint return{"ok": true} AddTrustedHostMiddleware Only allowedexample.local Run it with uvicorn, thencurl http://127.0.0.1:8000/health You will receive 400.

Play it again, this time add-H "Host: example.local" It's 200. This five-minute segment perfectly illustrates the first deception of the "health check can be misleading" phenomenon. Afterwards, when you see 400 in the container's health check, you'll immediately think of the Host, instead of spending an afternoon checking something else. By the way, open.../docs Take a look and see what free files really mean.

Postscript

The story at the beginning about "all the lights being green" never happened again after I changed the health check to include background threads. It wasn't because threads couldn't die; it was because when a thread died, the light would turn red, the container would restart, and the thread would come back to life. The whole thing happened while I was sleeping, and I only saw one line of record in the log in the morning. This is probably my final understanding of the word "health": it's not about not getting sick, but about having someone know when you are sick.

Official website: fastapi.tiangolo.com , uvicorn.org

Reading preferences

Back to all postsAbout the author and corrections (Chinese)