-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #102 from lsst-sqre/tickets/DM-35033
[DM-35033] Add CaseInsensitiveQueryMiddleware
- Loading branch information
Showing
7 changed files
with
127 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,6 +38,7 @@ Guides | |
kubernetes | ||
logging | ||
x-forwarded | ||
ivoa | ||
|
||
API | ||
=== | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
##################### | ||
IVOA protocol support | ||
##################### | ||
|
||
The IVOA web protocols aren't entirely RESTful and have some unusual requirements that are not provided by modern web frameworks. | ||
Safir provides some FastAPI support facilities to make implementing IVOA services easier. | ||
|
||
Query parameter case insensitivity | ||
================================== | ||
|
||
Many IVOA protocols require the key of a query parameter to be case-insensitive. | ||
For example, the requests ``GET /api/foo?param=bar`` and ``GET /api/foo?PARAM=bar`` are supposed to produce identical results. | ||
Safir provides `safir.middleware.ivoa.CaseInsensitiveQueryMiddleware` to implement this protocol requirement. | ||
|
||
Add this middleware to the FastAPI application: | ||
|
||
.. code-block:: python | ||
from safir.middleware.ivoa import CaseInsensitiveQueryMiddleware | ||
app = FastAPI() | ||
app.add_middleware(CaseInsensitiveQueryMiddleware) | ||
In the route handlers, declare all query parameters in all lowercase. | ||
For instance, for the above example queries: | ||
|
||
.. code-block:: python | ||
@app.get("/api/foo") | ||
async def get_foo(param: str) -> Response: | ||
result = do_something_with_param(param) | ||
The keys of all incoming query parameters will be converted to lowercase by the middleware before the route handler is called. | ||
The value will be unchanged; so, for example, with a request of ``GET /api/foo?PARAM=BAR``, the value of ``param`` will still be ``BAR``, not ``bar``. | ||
|
||
The OpenAPI interface specification will only document the lowercase form of the parameters, but all other case combinations will be supported because the middleware will rewrite the incoming request. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
"""Middleware for IVOA services.""" | ||
|
||
from typing import Awaitable, Callable | ||
from urllib.parse import urlencode | ||
|
||
from fastapi import Request, Response | ||
from starlette.middleware.base import BaseHTTPMiddleware | ||
|
||
__all__ = ["CaseInsensitiveQueryMiddleware"] | ||
|
||
|
||
class CaseInsensitiveQueryMiddleware(BaseHTTPMiddleware): | ||
"""Make query parameter keys all lowercase. | ||
Unfortunately, several IVOA standards require that query parameters be | ||
case-insensitive, which is not supported by modern HTTP web frameworks. | ||
This middleware attempts to work around this by lowercasing the query | ||
parameter keys before the request is processed, allowing normal FastAPI | ||
query parsing to then work without regard for case. This, in turn, | ||
permits FastAPI to perform input validation on GET parameters, which would | ||
otherwise only happen if the case used in the request happened to match | ||
the case used in the function signature. | ||
This unfortunately doesn't handle POST, so routes that accept POST will | ||
need to parse the POST data case-insensitively in the handler or a | ||
dependency. | ||
Based on `fastapi#826 <https://github.com/tiangolo/fastapi/issues/826>`__. | ||
""" | ||
|
||
async def dispatch( | ||
self, | ||
request: Request, | ||
call_next: Callable[[Request], Awaitable[Response]], | ||
) -> Response: | ||
params = [(k.lower(), v) for k, v in request.query_params.items()] | ||
request.scope["query_string"] = urlencode(params).encode() | ||
return await call_next(request) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
"""Test IVOA middleware.""" | ||
|
||
from __future__ import annotations | ||
|
||
from typing import Dict | ||
|
||
import pytest | ||
from fastapi import FastAPI | ||
from httpx import AsyncClient | ||
|
||
from safir.middleware.ivoa import CaseInsensitiveQueryMiddleware | ||
|
||
|
||
def build_app() -> FastAPI: | ||
"""Construct a test FastAPI app with the middleware registered.""" | ||
app = FastAPI() | ||
app.add_middleware(CaseInsensitiveQueryMiddleware) | ||
return app | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_case_insensitive() -> None: | ||
app = build_app() | ||
|
||
@app.get("/") | ||
async def handler(param: str) -> Dict[str, str]: | ||
return {"param": param} | ||
|
||
async with AsyncClient(app=app, base_url="https://example.com") as client: | ||
r = await client.get("/", params={"param": "foo"}) | ||
assert r.status_code == 200 | ||
assert r.json() == {"param": "foo"} | ||
|
||
r = await client.get("/", params={"PARAM": "foo"}) | ||
assert r.status_code == 200 | ||
assert r.json() == {"param": "foo"} | ||
|
||
r = await client.get("/", params={"pARam": "foo"}) | ||
assert r.status_code == 200 | ||
assert r.json() == {"param": "foo"} | ||
|
||
r = await client.get("/", params={"paramX": "foo"}) | ||
assert r.status_code == 422 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters