-
Notifications
You must be signed in to change notification settings - Fork 99
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
Fix content-type of /api response #287
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,13 +4,20 @@ | |
import attr | ||
from brotli_asgi import BrotliMiddleware | ||
from fastapi import APIRouter, FastAPI | ||
from fastapi.openapi.docs import ( | ||
get_redoc_html, | ||
get_swagger_ui_html, | ||
get_swagger_ui_oauth2_redirect_html, | ||
) | ||
from fastapi.openapi.utils import get_openapi | ||
from fastapi.responses import ORJSONResponse | ||
from pydantic import BaseModel | ||
from stac_pydantic import Collection, Item, ItemCollection | ||
from stac_pydantic.api import ConformanceClasses, LandingPage, Search | ||
from stac_pydantic.api.collections import Collections | ||
from stac_pydantic.version import STAC_VERSION | ||
from starlette.responses import JSONResponse, Response | ||
from starlette.requests import Request | ||
from starlette.responses import HTMLResponse, JSONResponse, Response | ||
|
||
from stac_fastapi.api.errors import DEFAULT_STATUS_CODES, add_exception_handlers | ||
from stac_fastapi.api.models import ( | ||
|
@@ -32,6 +39,69 @@ | |
from stac_fastapi.types.search import STACSearch | ||
|
||
|
||
class VndOaiResponse(ORJSONResponse): | ||
"""JSON with custom, vendor content-type.""" | ||
|
||
media_type = "application/vnd.oai.openapi+json;version=3.0" | ||
|
||
|
||
class VndFastAPI(FastAPI): | ||
"""Custom FastAPI to support /api response with vendor content-type.""" | ||
|
||
def setup(self) -> None: | ||
"""Update vendor-specific content-type for /api.""" | ||
if self.openapi_url: | ||
urls = (server_data.get("url") for server_data in self.servers) | ||
server_urls = {url for url in urls if url} | ||
|
||
async def openapi(req: Request) -> JSONResponse: | ||
root_path = req.scope.get("root_path", "").rstrip("/") | ||
if root_path not in server_urls: | ||
if root_path and self.root_path_in_servers: | ||
self.servers.insert(0, {"url": root_path}) | ||
server_urls.add(root_path) | ||
return VndOaiResponse(self.openapi()) | ||
|
||
self.add_route(self.openapi_url, openapi, include_in_schema=False) | ||
if self.openapi_url and self.docs_url: | ||
|
||
async def swagger_ui_html(req: Request) -> HTMLResponse: | ||
root_path = req.scope.get("root_path", "").rstrip("/") | ||
openapi_url = root_path + self.openapi_url | ||
oauth2_redirect_url = self.swagger_ui_oauth2_redirect_url | ||
if oauth2_redirect_url: | ||
oauth2_redirect_url = root_path + oauth2_redirect_url | ||
return get_swagger_ui_html( | ||
openapi_url=openapi_url, | ||
title=self.title + " - Swagger UI", | ||
oauth2_redirect_url=oauth2_redirect_url, | ||
init_oauth=self.swagger_ui_init_oauth, | ||
) | ||
|
||
self.add_route(self.docs_url, swagger_ui_html, include_in_schema=False) | ||
|
||
if self.swagger_ui_oauth2_redirect_url: | ||
|
||
async def swagger_ui_redirect(req: Request) -> HTMLResponse: | ||
return get_swagger_ui_oauth2_redirect_html() | ||
|
||
self.add_route( | ||
self.swagger_ui_oauth2_redirect_url, | ||
swagger_ui_redirect, | ||
include_in_schema=False, | ||
) | ||
if self.openapi_url and self.redoc_url: | ||
|
||
async def redoc_html(req: Request) -> HTMLResponse: | ||
root_path = req.scope.get("root_path", "").rstrip("/") | ||
openapi_url = root_path + self.openapi_url | ||
return get_redoc_html( | ||
openapi_url=openapi_url, title=self.title + " - ReDoc" | ||
) | ||
|
||
self.add_route(self.redoc_url, redoc_html, include_in_schema=False) | ||
|
||
|
||
@attr.s | ||
class StacApi: | ||
"""StacApi factory. | ||
|
@@ -64,7 +134,8 @@ class StacApi: | |
) | ||
app: FastAPI = attr.ib( | ||
default=attr.Factory( | ||
lambda self: FastAPI(openapi_url=self.settings.openapi_url), takes_self=True | ||
lambda self: VndFastAPI(openapi_url=self.settings.openapi_url), | ||
takes_self=True, | ||
) | ||
) | ||
router: APIRouter = attr.ib(default=attr.Factory(APIRouter)) | ||
|
@@ -75,7 +146,7 @@ class StacApi: | |
search_request_model: Type[Search] = attr.ib(default=STACSearch) | ||
search_get_request: Type[SearchGetRequest] = attr.ib(default=SearchGetRequest) | ||
item_collection_uri: Type[ItemCollectionUri] = attr.ib(default=ItemCollectionUri) | ||
response_class: Type[Response] = attr.ib(default=JSONResponse) | ||
response_class: Type[Response] = attr.ib(default=ORJSONResponse) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This changes the default JSONResponse value to ORJSONResponse, which seems like an orthogonal change to this PR. And, orjson isn't an explicit dependency to anything but pgstac, so either orjson should be added as a dependency to the |
||
middlewares: List = attr.ib(default=attr.Factory(lambda: [BrotliMiddleware])) | ||
|
||
def get_extension(self, extension: Type[ApiExtension]) -> Optional[ApiExtension]: | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps unnecessary code duplication can be avoided by calling
super
and then removing/re-adding the route itself.self
is aStarlette
object, which has a routes property that directly returns the list of the router's routes. AFAIK adding a route appends it to the list and does no other special registration. While there's no "remove_route" method, if you find the route with theself.openapi_url
and replace it with the one you construct with the proper response, that might save some code dupe. The worry with the dupe is that if a later version of FastAPI changes say the docs_url add logic, then this override logic won't pick up on those changes unrelated to the VndOaiResponse.Another thing to note is that Route exposes the endpoint Callable directly, so you could even wrap the endpoint in one that translates the return value into a VndOaiResponse.
Maybe not worth spending too much time trying to hack the internals of FastAPI and Starlette (though everything I mentioned is public API), but I think it would be preferable to reduce the code copy in the setup override to only as much as is needed for this small change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It appears as though
attrs
has aconverter
argument, which allows you to make modifications to supplied arguments. I've implemented a function which can be used as aconverter
to alter the routes such that the appropriate vendor content-type is advertised without any inheritance shenanigans