Skip to content

Commit

Permalink
Merge pull request #102 from lsst-sqre/tickets/DM-35033
Browse files Browse the repository at this point in the history
[DM-35033] Add CaseInsensitiveQueryMiddleware
  • Loading branch information
rra authored Jun 1, 2022
2 parents 29339c0 + 10b5f76 commit 3afe21a
Show file tree
Hide file tree
Showing 7 changed files with 127 additions and 1 deletion.
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ Change log
.. Headline template:
X.Y.Z (YYYY-MM-DD)
3.1.0 (2022-06-01)
==================

- Add new FastAPI middleware ``CaseInsensitiveQueryMiddleware`` to aid in implementing the IVOA protocol requirement that the keys of query parameters be case-isensitive.

3.0.3 (2022-05-16)
==================

Expand Down
3 changes: 3 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ API reference
.. automodapi:: safir.models
:include-all-objects:

.. automodapi:: safir.middleware.ivoa
:include-all-objects:

.. automodapi:: safir.middleware.x_forwarded
:include-all-objects:

Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Guides
kubernetes
logging
x-forwarded
ivoa

API
===
Expand Down
36 changes: 36 additions & 0 deletions docs/ivoa.rst
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.
38 changes: 38 additions & 0 deletions src/safir/middleware/ivoa.py
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)
43 changes: 43 additions & 0 deletions tests/middleware/ivoa_test.py
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
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tox]
envlist = py,coverage-report,typing,lint
envlist = py,coverage-report,typing,lint,docs
isolated_build = True

[docker:postgres]
Expand Down

0 comments on commit 3afe21a

Please sign in to comment.