Fastapi: [QUESTION] How to do case-insensitive URLs?

Created on 30 Dec 2019  路  8Comments  路  Source: tiangolo/fastapi

How can I do case-insensitive URLs in FastAPI?

question

Most helpful comment

I think the most convenient solution to your problem is the middleware, which will be convert path and params to lower case. This example works with

  • /test?t=1&t2=2&t3=3
  • /test?t=1&T2=2&T3=3
  • /TEST?t=1&t2=2&t3=3
  • /TesT?T=1&t2=2&t3=3
from fastapi import FastAPI
from starlette.requests import Request

app = FastAPI()

DECODE_FORMAT = "latin-1"

@app.get("/test")
async def test(request: Request):
    print(request.query_params)
    return request.query_params


@app.middleware("http")
async def case_sens_middleware(request: Request, call_next):
    raw_query_str = request.scope["query_string"].decode(DECODE_FORMAT).lower()
    request.scope["query_string"] = raw_query_str.encode(DECODE_FORMAT)

    path = request.scope["path"].lower()
    request.scope["path"] = path

    response = await call_next(request)
    return response

UPD: See also regex validation for Path and Query

All 8 comments

The easiest way I can think of would be to update the path regexs for the routes.

For example:

import re

from starlette.routing import Route
from starlette.testclient import TestClient
from fastapi import FastAPI

app = FastAPI()


@app.get("/endpoint")
async def endpoint() -> str:
    return "success"


for route in app.router.routes:
    if isinstance(route, Route):
        route.path_regex = re.compile(route.path_regex.pattern, re.IGNORECASE)

print(TestClient(app).get("/endpoint").json())
# success
print(TestClient(app).get("/ENDPOINT").json())
# success

Let us know if that doesn't work for you.

@dmontagu Thanks. But it only changes the path part. The query string is still case-sensitive. How can we achieved this for entire URL except scheme and domain part.

Hmm, that might be more challenging. Currently I don't think FastAPI exposes (or even has) any functionality related to transforming the query parameter keys prior to injection.

You can always add request: Request as an argument of the endpoint and read the raw query parameters from there; that would allow you to convert the parameters to lowercase before retrieving. This is obviously much less ergonomic than the usual dependency injection though.

I think it might be possible to achieve this by using a custom Request subclass (as described in these docs: https://fastapi.tiangolo.com/tutorial/custom-request-and-route/) that overrides the way that the query parameters map is populated. If you attempt this though, you'll probably need to read through some Starlette and FastAPI source code to figure out how the query parameters map is built and used.

In general, I would recommend trying not to rely on case insensitivity of the query string if possible.

I'd be interested if anyone else has a better solution here.

Well, the problem is that it comes in an immutable dict called query_params from Starlette.

I think the most convenient solution to your problem is the middleware, which will be convert path and params to lower case. This example works with

  • /test?t=1&t2=2&t3=3
  • /test?t=1&T2=2&T3=3
  • /TEST?t=1&t2=2&t3=3
  • /TesT?T=1&t2=2&t3=3
from fastapi import FastAPI
from starlette.requests import Request

app = FastAPI()

DECODE_FORMAT = "latin-1"

@app.get("/test")
async def test(request: Request):
    print(request.query_params)
    return request.query_params


@app.middleware("http")
async def case_sens_middleware(request: Request, call_next):
    raw_query_str = request.scope["query_string"].decode(DECODE_FORMAT).lower()
    request.scope["query_string"] = raw_query_str.encode(DECODE_FORMAT)

    path = request.scope["path"].lower()
    request.scope["path"] = path

    response = await call_next(request)
    return response

UPD: See also regex validation for Path and Query

Thanks @Slyfoxy. I think that this will work for me.

Thanks for the help here everyone! Clever trick @Slyfoxy :fox_face: :rocket:

I think that should solve your use case, right @shivshankardayal ? If so, you can close the issue.

For posterity, here's how to import a middleware from another module:

lower_case_middleware.py

from starlette.requests import Request


class LowerCaseMiddleware:
    def __init__(self) -> None:
        self.DECODE_FORMAT = "latin-1"

    async def __call__(self, request: Request, call_next):
        raw = request.scope["query_string"].decode(self.DECODE_FORMAT).lower()
        request.scope["query_string"] = raw.encode(self.DECODE_FORMAT)

        path = request.scope["path"].lower()
        request.scope["path"] = path

        response = await call_next(request)
        return response

main.py

...
from lower_case_middleware import LowerCaseMiddleware

app = FastAPI()

my_middleware = LowerCaseMiddleware()
app.middleware("http")(my_middleware)
Was this page helpful?
0 / 5 - 0 ratings