Skip to content
Docs

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-proxy

Or add the group to pyproject.toml yourself:

pyproject.toml
[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:

vercel.json
{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "proxy": {
    "entrypoint": "proxy:proxy"
  }
}
entrypointLoads
proxy:proxyproxy from proxy.py
app.proxy:proxyproxy from app/proxy.py
middleware:handlerhandler from middleware.py
  • The name must be defined at the module's top level, by assignment, def, or from ... import.
  • A plain file path such as proxy.py isn't accepted. Use module: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:

proxy.py
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"})
ArgumentTypeDescription
fallbackResponse or handlerWhat to do with requests that match no route. See Fallback
strictboolRequire 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:

proxy.py
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:

PlaceholderMatches
{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 /

Captured values are URL-decoded. Quote them with urllib.parse.quote before you build a URL from them. Check any value that chooses a destination, such as a host or a path that contains ...

You can limit a route to certain HTTP methods or hosts:

proxy.py
@proxy.route("/admin", methods=["GET", "POST"], host="admin.example.com")
def admin(request: Request) -> Response:
    return RewriteResponse("/internal/admin")
ArgumentTypeDescription
methodslist[str]HTTP methods the route accepts. Omit it to accept every method
hoststrHostname 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.py
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:

proxy.py
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.py
@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.

ResponseWhat 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.py
@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:

proxy.py
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.py
@proxy.route("/")
def home(request: Request) -> Response:
    response = ContinueResponse()
    response.set_cookie("bucket", "b", max_age=86400)
    return response

Header 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:

vercel.json
{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "proxy": {
    "entrypoint": "app.proxy:proxy"
  },
  "functions": {
    "app/proxy.py": {
      "maxDuration": 10
    }
  }
}
ErrorCauseFix
invalid_proxy_entrypointentrypoint is a plain .py pathUse module:attribute, such as proxy:proxy
proxy_entrypoint_not_foundThe module file doesn't existCheck the module path. app.proxy loads app/proxy.py
PYTHON_HANDLER_NOT_FOUNDThe attribute isn't defined at the module's top levelDefine it at the top level, such as proxy = Proxy()
proxy_runtime_mismatchYour app doesn't use the Python runtimeWrite your Routing Middleware in the same language as your app
Every matched request failsThe vercel.proxy dependency group is missing, or lacks vercel-proxy or another package your Routing Middleware importsAdd the packages to "vercel.proxy" under [dependency-groups] in pyproject.toml

Was this helpful?

supported.