fastapi-cache icon indicating copy to clipboard operation
fastapi-cache copied to clipboard

Response headers are sent only on the second request.

Open aimfeld opened this issue 2 years ago • 1 comments

I noticed that the Cache-Control and ETag headers are not included for the first response. It takes a second request to generate and include these headers in the response. From looking at decorator.py, it seems that the reason is that the ETag header is a hash of the retrieve cache entry. The cache entry is created on the first request, and not available yet for the first response.

However, it's possible to include the response headers already in the first response. I created a modified version of the @cache decorator, which seems to work fine (unless I'm overlooking something). I can create a PR if you like:

def cache(
    expire: int = None,
    coder: Type[Coder] = None,
    key_builder: Callable = None,
    namespace: Optional[str] = "",
):
    """
    cache all function
    :param namespace:
    :param expire:
    :param coder:
    :param key_builder:
    :return:
    """

    def wrapper(func):
        @wraps(func)
        async def inner(*args, **kwargs):
            nonlocal coder
            nonlocal expire
            nonlocal key_builder
            copy_kwargs = kwargs.copy()
            request = copy_kwargs.pop("request", None)
            response = copy_kwargs.pop("response", None)
            if (
                request and request.headers.get("Cache-Control") == "no-store"
            ) or not FastAPICache.get_enable():
                return await func(*args, **kwargs)

            coder = coder or FastAPICache.get_coder()
            expire = expire or FastAPICache.get_expire()
            key_builder = key_builder or FastAPICache.get_key_builder()
            backend = FastAPICache.get_backend()

            cache_key = key_builder(
                func, namespace, request=request, response=response, args=args, kwargs=copy_kwargs
            )
            ttl, ret_encoded = await backend.get_with_ttl(cache_key)
            if not request:
                if ret_encoded is not None:
                    return coder.decode(ret_encoded)
                ret = await func(*args, **kwargs)
                await backend.set(cache_key, coder.encode(ret), expire or FastAPICache.get_expire())
                return ret

            if request.method != "GET":
                return await func(request, *args, **kwargs)
            if_none_match = request.headers.get("if-none-match")

            if ret_encoded is None:
                ret = await func(*args, **kwargs)
                ret_encoded = coder.encode(ret)
                ttl = expire or FastAPICache.get_expire()
                await backend.set(cache_key, ret_encoded, ttl)
            else:
                ret = coder.decode(ret_encoded)

            if response:
                response.headers["Cache-Control"] = f"max-age={ttl}"
                etag = f"W/{hash(ret_encoded)}"
                if if_none_match == etag:
                    response.status_code = 304
                    return response
                response.headers["ETag"] = etag

            return ret

        return inner

    return wrapper

aimfeld avatar Jan 13 '22 14:01 aimfeld

Looks like https://github.com/long2ice/fastapi-cache/pull/52 is intended to resolve this

mustyoshi avatar Feb 25 '22 17:02 mustyoshi

Fixed in https://github.com/long2ice/fastapi-cache/pull/112?

iqfareez avatar Mar 16 '23 14:03 iqfareez

This generates also errors when the response is not set on the first request:

AttributeError: 'NoneType' object has no attribute 'headers'

as the response can be None

bpereto avatar Apr 21 '23 10:04 bpereto

Pretty sure this is no longer an issue.

mjpieters avatar May 14 '23 21:05 mjpieters