Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to do case-insensitive URLs? #826

Closed
shivshankardayal opened this issue Dec 30, 2019 · 10 comments
Closed

How to do case-insensitive URLs? #826

shivshankardayal opened this issue Dec 30, 2019 · 10 comments
Labels
question Question or problem question-migrate

Comments

@shivshankardayal
Copy link

How can I do case-insensitive URLs in FastAPI?

@shivshankardayal shivshankardayal added the question Question or problem label Dec 30, 2019
@dmontagu
Copy link
Collaborator

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.

@shivshankardayal
Copy link
Author

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

@dmontagu
Copy link
Collaborator

dmontagu commented Jan 4, 2020

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.

@shivshankardayal
Copy link
Author

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

@nkhitrov
Copy link

nkhitrov commented Jan 20, 2020

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

@shivshankardayal
Copy link
Author

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

@tiangolo
Copy link
Member

tiangolo commented Feb 13, 2020

Thanks for the help here everyone! Clever trick @SlyFoxy 🦊 🚀

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

@pmsoltani
Copy link

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)

@lorenzori
Copy link

lorenzori commented Jun 4, 2021

any idea on how we could only change the keys of the query_string rather than the whole query_string (i.e. both keys and values) in an elegant way?

this would do the job but a bit hacky:

query_string = ""
for k in request.query_params:
    query_string += k.lower() + '=' + request.query_params[k] + "&"

query_string = query_string[:-1]
request.scope["query_string"] = query_string.encode(self.DECODE_FORMAT)

@xlg-go
Copy link

xlg-go commented Mar 14, 2022

real query_params case insensitive~

from starlette.requests import Request
from starlette.responses import Response
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint


class QueryParamsCaseInsensitiveMiddleware(BaseHTTPMiddleware):

    async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
        routes = list(filter(lambda r: r.path.lower() == request.url.path.lower(), request.app.routes))
        if len(routes) == 1 and len(request.query_params) > 0:
            query_str = ''
            query_params = routes[0].dependant.query_params
            for param in query_params:
                req_params = list(filter(lambda p: p.lower() == param.name.lower(), request.query_params))
                for req_param in req_params:
                    query_str += f"{param.name}={request.query_params[req_param]}&"

            query_str = query_str[:-1]
            request.scope["query_string"] = query_str.encode("latin-1")

        response = await call_next(request)

        return response


# reference:
app.add_middleware(middleware_class=QueryParamsCaseInsensitiveMiddleware)

path case insensitive~

from typing import Callable
from fastapi import FastAPI
import re
from starlette.routing import Route


def startup_path_ignore_case(app: FastAPI) -> Callable:
    async def _startup() -> None:
        for route in app.router.routes:
            if isinstance(route, Route):
                route.path_regex = re.compile(route.path_regex.pattern, re.IGNORECASE)

    return _startup

# reference:
app.add_event_handler("startup", request_events.startup_path_ignore_case(app))

@tiangolo tiangolo changed the title [QUESTION] How to do case-insensitive URLs? How to do case-insensitive URLs? Feb 24, 2023
@tiangolo tiangolo reopened this Feb 28, 2023
@fastapi fastapi locked and limited conversation to collaborators Feb 28, 2023
@tiangolo tiangolo converted this issue into discussion #7877 Feb 28, 2023

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
question Question or problem question-migrate
Projects
None yet
Development

No branches or pull requests

7 participants