Routing Middleware: Python Reference
Use the vercel-proxy package to write Routing Middleware in Python. It routes requests by path and provides responses that continue, rewrite, or answer a request. For path matching and limits, see the Routing Middleware overview.
Python Routing Middleware works with apps on the Python runtime, such as FastAPI, Flask, and Django.
Your Routing Middleware runs separately from your app, with only the packages in the vercel.proxy dependency group of your pyproject.toml. Add vercel-proxy to that group:
uv add --group vercel.proxy vercel-proxyOr add the group to pyproject.toml yourself:
[dependency-groups]
"vercel.proxy" = ["vercel-proxy"]Add any other packages your Routing Middleware imports to the same group. Requests to your Routing Middleware fail if one is missing. Projects that only use requirements.txt should switch to pyproject.toml.
Set proxy.entrypoint in vercel.json to the module that defines your Proxy and the name of the Proxy, separated by a colon:
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"proxy": {
"entrypoint": "proxy:proxy"
}
}entrypoint | Loads |
|---|---|
proxy:proxy | proxy from proxy.py |
app.proxy:proxy | proxy from app/proxy.py |
middleware:handler | handler from middleware.py |
- The name must be defined at the module's top level, by assignment,
def, orfrom ... import. - A plain file path such as
proxy.pyisn't accepted. Usemodule:attribute. - The entrypoint can also name any other ASGI or WSGI app instead of a
Proxy.
Proxy routes each request to a handler. Create one at the top level of your entrypoint module:
from urllib.parse import quote
from vercel.proxy import ContinueResponse, Proxy, RedirectResponse, Request, Response
proxy = Proxy()
@proxy.route("/old-blog/{slug}")
def old_blog(request: Request) -> Response:
slug = quote(request.path_params["slug"])
return RedirectResponse(f"/blog/{slug}", status_code=308)
@proxy.route("/api/{path:path}")
def api(request: Request) -> Response:
return ContinueResponse(request_headers={"x-api-version": "2"})| Argument | Type | Description |
|---|---|---|
fallback | Response or handler | What to do with requests that match no route. See Fallback |
strict | bool | Require exact path matches, including trailing slashes. See Trailing slashes |
Proxy only receives requests that match proxy.matcher. See path matching.
@proxy.route(path) registers a handler for requests whose path matches. Each request goes to the first matching route, in the order you registered the routes. Handlers can be sync or async.
A path can capture parts of the URL with placeholders. Your handler reads the captured values from request.path_params:
from vercel.proxy import Proxy, Request, Response, RewriteResponse
proxy = Proxy()
@proxy.route("/users/{user_id:int}")
def user(request: Request) -> Response:
return RewriteResponse(f"/profiles/{request.path_params['user_id']}")The following placeholders are available:
| Placeholder | Matches |
|---|---|
{name} | One path segment, as a string |
{name:type} | One path segment, converted to str, int, float, or uuid |
{name:path} | Any part of the path, including / |
You can limit a route to certain HTTP methods or hosts:
@proxy.route("/admin", methods=["GET", "POST"], host="admin.example.com")
def admin(request: Request) -> Response:
return RewriteResponse("/internal/admin")| Argument | Type | Description |
|---|---|---|
methods | list[str] | HTTP methods the route accepts. Omit it to accept every method |
host | str | Hostname the route accepts. It can capture values like the path, such as "{tenant}.example.com". Omit it to accept every host |
Trailing slashes are optional by default. Proxy first matches each request against every route as sent, then with the trailing slash added or removed. A route that matches the path exactly wins.
To require exact matches, pass strict=True:
proxy = Proxy(strict=True)Requests that match no route go to the fallback. By default, they continue to your app unchanged.
To change this, pass a response or a handler as fallback:
from vercel.proxy import JSONResponse, Proxy
proxy = Proxy(fallback=JSONResponse({"error": "not found"}, status_code=404))You can also set the fallback with the @proxy.fallback decorator. It replaces any fallback you passed to Proxy():
@proxy.fallback
def fallback(request: Request) -> Response:
return RewriteResponse("/404")Request is a Starlette Request. Use method, url, headers, query_params, cookies, path_params, and client to decide how to route it.
To read a request's location, see geolocation headers.
Every handler returns a response. The response type decides what happens to the request. Responses are based on Starlette's responses.
| Response | What happens |
|---|---|
ContinueResponse() | The request continues to the original URL |
RewriteResponse(url) | Edgenivo serves the request from url. The URL the client sees doesn't change |
A continue or rewrite always has status 200 and no body. To change the status code, answer the request instead.
ContinueResponse and RewriteResponse take request_headers, a dictionary of headers to set on the request your app receives. A None value removes the header:
@proxy.route("/{path:path}")
def add_region(request: Request) -> Response:
return ContinueResponse(request_headers={"x-region": "eu", "cookie": None})Response, JSONResponse, PlainTextResponse, HTMLResponse, and RedirectResponse answer the request directly. The request doesn't reach your app. They take the same arguments as the Starlette classes of the same name:
from vercel.proxy import JSONResponse, Proxy, Request, Response
proxy = Proxy()
@proxy.route("/api/health")
def health(request: Request) -> Response:
return JSONResponse({"status": "ok"})RedirectResponse defaults to status 307. Pass status_code to use a different status.
headers, set_cookie, and delete_cookie work on every response, as in Starlette:
@proxy.route("/")
def home(request: Request) -> Response:
response = ContinueResponse()
response.set_cookie("bucket", "b", max_age=86400)
return responseHeader names starting with x-middleware- are reserved for Edgenivo. Starlette background tasks aren't supported.
Set duration, memory, and other options with the functions property. Use the module's file path as the key, not the module:attribute value:
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"proxy": {
"entrypoint": "app.proxy:proxy"
},
"functions": {
"app/proxy.py": {
"maxDuration": 10
}
}
}| Error | Cause | Fix |
|---|---|---|
invalid_proxy_entrypoint | entrypoint is a plain .py path | Use module:attribute, such as proxy:proxy |
proxy_entrypoint_not_found | The module file doesn't exist | Check the module path. app.proxy loads app/proxy.py |
PYTHON_HANDLER_NOT_FOUND | The attribute isn't defined at the module's top level | Define it at the top level, such as proxy = Proxy() |
proxy_runtime_mismatch | Your app doesn't use the Python runtime | Write your Routing Middleware in the same language as your app |
| Every matched request fails | The vercel.proxy dependency group is missing, or lacks vercel-proxy or another package your Routing Middleware imports | Add the packages to "vercel.proxy" under [dependency-groups] in pyproject.toml |
Was this helpful?