fastapi custom response class as default response class
Question:
I am trying to use a custom response class as default response.
from fastapi.responses import Response
from bson.json_util import dumps
class MongoResponse(Response):
def __init__(self, content, *args, **kwargs):
super().__init__(
content=dumps(content),
media_type="application/json",
*args,
**kwargs,
)
This works fine when I explicitly use the response class.
@app.get("/")
async def getDoc():
foo = client.get_database('foo')
result = await foo.bar.find_one({'author': 'fool'})
return MongoResponse(result)
However, when I try to pass this as argument to the FastAPI constructor, it doesn’t seem to be used when only returning the data from the request handler.
app = FastAPI(default_response_class=MongoResponse)
@app.get("/")
async def getDoc():
foo = client.get_database('foo')
result = await foo.bar.find_one({'author': 'fool'})
return result
When I look at below stack trace, it seems like its still using the normal default response which is the json response.
ERROR: Exception in ASGI application
Traceback (most recent call last):
File "/home/blue/podman/test/.venv/lib/python3.6/site-packages/uvicorn/protocols/http/httptools_impl.py", line 390, in run_asgi
result = await app(self.scope, self.receive, self.send)
File "/home/blue/podman/test/.venv/lib/python3.6/site-packages/uvicorn/middleware/proxy_headers.py", line 45, in __call__
return await self.app(scope, receive, send)
File "/home/blue/podman/test/.venv/lib/python3.6/site-packages/fastapi/applications.py", line 181, in __call__
await super().__call__(scope, receive, send) # pragma: no cover
File "/home/blue/podman/test/.venv/lib/python3.6/site-packages/starlette/applications.py", line 111, in __call__
await self.middleware_stack(scope, receive, send)
File "/home/blue/podman/test/.venv/lib/python3.6/site-packages/starlette/middleware/errors.py", line 181, in __call__
raise exc from None
File "/home/blue/podman/test/.venv/lib/python3.6/site-packages/starlette/middleware/errors.py", line 159, in __call__
await self.app(scope, receive, _send)
File "/home/blue/podman/test/.venv/lib/python3.6/site-packages/starlette/exceptions.py", line 82, in __call__
raise exc from None
File "/home/blue/podman/test/.venv/lib/python3.6/site-packages/starlette/exceptions.py", line 71, in __call__
await self.app(scope, receive, sender)
File "/home/blue/podman/test/.venv/lib/python3.6/site-packages/starlette/routing.py", line 566, in __call__
await route.handle(scope, receive, send)
File "/home/blue/podman/test/.venv/lib/python3.6/site-packages/starlette/routing.py", line 227, in handle
await self.app(scope, receive, send)
File "/home/blue/podman/test/.venv/lib/python3.6/site-packages/starlette/routing.py", line 41, in app
response = await func(request)
File "/home/blue/podman/test/.venv/lib/python3.6/site-packages/fastapi/routing.py", line 199, in app
is_coroutine=is_coroutine,
File "/home/blue/podman/test/.venv/lib/python3.6/site-packages/fastapi/routing.py", line 122, in serialize_response
return jsonable_encoder(response_content)
File "/home/blue/podman/test/.venv/lib/python3.6/site-packages/fastapi/encoders.py", line 94, in jsonable_encoder
sqlalchemy_safe=sqlalchemy_safe,
File "/home/blue/podman/test/.venv/lib/python3.6/site-packages/fastapi/encoders.py", line 139, in jsonable_encoder
raise ValueError(errors)
ValueError: [TypeError("'ObjectId' object is not iterable",), TypeError('vars() argument must have __dict__ attribute',)]
Answers:
So it turns out that the default response class as well as the response class on the route are only there for the open API documentation. By default the documentation will document every endpoint as if they would return json.
So with the below example code every response would be marked as content type text/html.
On the second rout this is overwritten with application/json
app = FastAPI(default_response_class=HTMLResponse)
@app.get("/")
async def getDoc():
foo = client.get_database('foo')
result = await foo.bar.find_one({'author': 'Mike'})
return MongoResponse(result)
@app.get("/other", response_class=JSONResponse)
async def json():
return {"json": "true"}
In that sense, I should probably use my class explicitly and leave the default response class as JSON so that they are documented as JSON responses.
I resorted to monkeypatching
from fastapi import routing as fastapi_routing
from fastapi.responses import ORJSONResponse
def api_route(self, path, **kwargs):
def decorator(func):
if type(kwargs["response_class"]) == DefaultPlaceholder:
kwargs["response_class"] = Default(ORJSONResponse)
self.add_api_route(
path,
func,
**kwargs,
)
return func
return decorator
fastapi_routing.APIRouter.api_route = api_route
I am trying to use a custom response class as default response.
from fastapi.responses import Response
from bson.json_util import dumps
class MongoResponse(Response):
def __init__(self, content, *args, **kwargs):
super().__init__(
content=dumps(content),
media_type="application/json",
*args,
**kwargs,
)
This works fine when I explicitly use the response class.
@app.get("/")
async def getDoc():
foo = client.get_database('foo')
result = await foo.bar.find_one({'author': 'fool'})
return MongoResponse(result)
However, when I try to pass this as argument to the FastAPI constructor, it doesn’t seem to be used when only returning the data from the request handler.
app = FastAPI(default_response_class=MongoResponse)
@app.get("/")
async def getDoc():
foo = client.get_database('foo')
result = await foo.bar.find_one({'author': 'fool'})
return result
When I look at below stack trace, it seems like its still using the normal default response which is the json response.
ERROR: Exception in ASGI application
Traceback (most recent call last):
File "/home/blue/podman/test/.venv/lib/python3.6/site-packages/uvicorn/protocols/http/httptools_impl.py", line 390, in run_asgi
result = await app(self.scope, self.receive, self.send)
File "/home/blue/podman/test/.venv/lib/python3.6/site-packages/uvicorn/middleware/proxy_headers.py", line 45, in __call__
return await self.app(scope, receive, send)
File "/home/blue/podman/test/.venv/lib/python3.6/site-packages/fastapi/applications.py", line 181, in __call__
await super().__call__(scope, receive, send) # pragma: no cover
File "/home/blue/podman/test/.venv/lib/python3.6/site-packages/starlette/applications.py", line 111, in __call__
await self.middleware_stack(scope, receive, send)
File "/home/blue/podman/test/.venv/lib/python3.6/site-packages/starlette/middleware/errors.py", line 181, in __call__
raise exc from None
File "/home/blue/podman/test/.venv/lib/python3.6/site-packages/starlette/middleware/errors.py", line 159, in __call__
await self.app(scope, receive, _send)
File "/home/blue/podman/test/.venv/lib/python3.6/site-packages/starlette/exceptions.py", line 82, in __call__
raise exc from None
File "/home/blue/podman/test/.venv/lib/python3.6/site-packages/starlette/exceptions.py", line 71, in __call__
await self.app(scope, receive, sender)
File "/home/blue/podman/test/.venv/lib/python3.6/site-packages/starlette/routing.py", line 566, in __call__
await route.handle(scope, receive, send)
File "/home/blue/podman/test/.venv/lib/python3.6/site-packages/starlette/routing.py", line 227, in handle
await self.app(scope, receive, send)
File "/home/blue/podman/test/.venv/lib/python3.6/site-packages/starlette/routing.py", line 41, in app
response = await func(request)
File "/home/blue/podman/test/.venv/lib/python3.6/site-packages/fastapi/routing.py", line 199, in app
is_coroutine=is_coroutine,
File "/home/blue/podman/test/.venv/lib/python3.6/site-packages/fastapi/routing.py", line 122, in serialize_response
return jsonable_encoder(response_content)
File "/home/blue/podman/test/.venv/lib/python3.6/site-packages/fastapi/encoders.py", line 94, in jsonable_encoder
sqlalchemy_safe=sqlalchemy_safe,
File "/home/blue/podman/test/.venv/lib/python3.6/site-packages/fastapi/encoders.py", line 139, in jsonable_encoder
raise ValueError(errors)
ValueError: [TypeError("'ObjectId' object is not iterable",), TypeError('vars() argument must have __dict__ attribute',)]
So it turns out that the default response class as well as the response class on the route are only there for the open API documentation. By default the documentation will document every endpoint as if they would return json.
So with the below example code every response would be marked as content type text/html.
On the second rout this is overwritten with application/json
app = FastAPI(default_response_class=HTMLResponse)
@app.get("/")
async def getDoc():
foo = client.get_database('foo')
result = await foo.bar.find_one({'author': 'Mike'})
return MongoResponse(result)
@app.get("/other", response_class=JSONResponse)
async def json():
return {"json": "true"}
In that sense, I should probably use my class explicitly and leave the default response class as JSON so that they are documented as JSON responses.
I resorted to monkeypatching
from fastapi import routing as fastapi_routing
from fastapi.responses import ORJSONResponse
def api_route(self, path, **kwargs):
def decorator(func):
if type(kwargs["response_class"]) == DefaultPlaceholder:
kwargs["response_class"] = Default(ORJSONResponse)
self.add_api_route(
path,
func,
**kwargs,
)
return func
return decorator
fastapi_routing.APIRouter.api_route = api_route