fastapi-cache
fastapi-cache copied to clipboard
Caching not convertible to JSON eg. image/png response type
Would it be possible to cache images with byte response type? If so how? with a custom encoder?
eg with image_tile -> bytes:
`@router.get("/{cog_name}/{z}/{x}/{y}") async def get_cog_tile(cog_name: str, z: int, x: int, y: int, colormap: str = 'terrain') -> Response:
...
headers = {"Content-Encoding": "identity", "content-type": 'image/png'} return Response(content=image_tile, headers=headers) `
Thanks
It is possible to cache raw binary data, though there's a little bit of support missing currently (more below). To get it basically working using your example above, though, you can just provide a custom coder:
class PassthruCoder(Coder):
@classmethod
def encode(cls, value: Any) -> Any:
return value
@classmethod
def decode(cls, value: Any) -> Any:
return value
@router.get("/{cog_name}/{z}/{x}/{y}")
@cache(coder=PassthruCoder)
async def get_cog_tile(cog_name: str, z: int, x: int, y: int, colormap: str = 'terrain') -> Response:
...
headers = {"Content-Encoding": "identity", "content-type": 'image/png'}
return Response(content=image_tile, headers=headers)
This works with the InMemoryBackend because in the fastapi guts, it will check if an endpoint returns a Response and it will just return that immediately, if so. So, in this case the entire Response gets cached in memory. You likely will need to fix this Coder if this needs to support other backends where it actually needs to get serialized.
One thing missing from this implementation, though, is the cache headers. No headers will be sent down, so the browser will re-request the same files. The content will have been cached, so it won't be recalculated by the server, but it should never have been requested in the first place.
After doing some digging, it appears that the issue is that if an endpoint returns a Response
directly, this is not handled by the @cache
decorator. The cache decorator will set the headers on the response
parameter to your endpoint, rather than to the actual Response that your endpoint returned. Those headers will just be thrown away by fastapi when it sees that you already returned a response.
I could not figure out how to return raw binary data without wrapping it in a Response, as you've done here, as the data is always handed off to jsonable_encoder
which really assumes non-binary data.
My workaround was to add add a few lines of code to the cache decorator (I created a local copy of this function):
# File: fastapi_cache/decorator.py
# Lines ~158-171
if_none_match = request.headers.get("if-none-match")
if ret is not None:
# <hack>
if isinstance(ret, Response):
response = ret
# </hack>
if response:
response.headers["Cache-Control"] = f"max-age={ttl}"
etag = f"W/{hash(ret)}"
if if_none_match == etag:
response.status_code = 304
return response
response.headers["ETag"] = etag
return coder.decode(ret)
I hope that there's a nicer solution, but maybe this helps you in the interim.
Thanks for your time @hozn it worked! I hope this can be merged to the repo, in my case I'm trying to cache with Redis a couple of different binary outputs (images from COG and vector tiles from postgis). This hack seems to work properly maybe it can be an option in the decorator.
Together with the hack mentioned by @hozn , in order to store the Response on a RedisBackend, I did the following:
defined a custom response
# This is for PNG data, thus the explicit charset
class ImageResponse(Response):
media_type = "image/png"
charset = "Windows-1252"
defined the Coder as it follows:
class ImageResponseCoder(Coder):
@classmethod
def encode(cls, value: ImageResponse) -> bytes:
return value.body
@classmethod
def decode(cls, value: bytes) -> ImageResponse:
return ImageResponse(content=value, headers={"Content-Encoding": "identity", "content-type": "image/png"})
and in the route I define the coder and response class
@router.get("/png/{cog_name}/{z}/{x}/{y}", response_class=ImageResponse)
@cache(namespace="coglayers", expire=86400, coder=ImageResponseCoder)
async def get_cog_tile(cog_name: str, z: int, x: int, y: int, colormap: str = "terrain"):
# snip
return ImageResponse(content=image_tile, headers={"Content-Encoding": "identity", "content-type": "image/png"})