Not Modified is changing the cache-control headers

I’m hoping I’m just missing some setup. Currently we are using If-Modified-Since headers to try and improve some performance of some endpoints. The base setup is as follows

Plugins.Add(new CorsFeature(allowedHeaders: "Content-Type, authorization, If-Unmodified-Since, If-Modified-Since"));
GetPlugin<HttpCacheFeature>().DefaultMaxAge = TimeSpan.Zero;

Our endpoint returns a result as follows:

return new HttpResult(response)
        {
            LastModified = maxDate,
            CacheControl = CacheControl.NoCache | CacheControl.Private
        };

Now when we call the endpoint, initially we get a status 200 with the result returned, a last-modified date and cache-control max-age=0, private, no-cache which is expected.

The second time we get a 304 and no last-modified, which is expected, but the cache-control is private only. My expectation is that it’s as the first call, as we defined it.

As a result, a 3rd call to the endpoint returns 200, cache-control private and has pulled from disk cache instead of hitting the endpoint again to verify.

There’s no state being kept between requests so responses on subsequent requests are not going to be influenced by what was returned in previous requests. Can you post the raw HTTP Headers for the 304 response so I can see what you’re seeing.

First Call

Second

Third

Is your Service getting called again for the 2nd request, i.e. is this code being returned in all 3 responses?

return new HttpResult(response)
{
    LastModified = maxDate,
    CacheControl = CacheControl.NoCache | CacheControl.Private
};

If not how are the requests being short-circuited? i.e. do you have any custom logic short-circuiting the requests? If so, can you include all relevant code used in the request.

Not at my desk until later to add some more code. However, the 3rd time the server is not hit. It’s taken from browser cache of previous call (expected since the cache-headers are set private).

The server code in this case doesn’t short circuit. The same results (response object) would be passed to the HttpResult constructor each time with the same LastModified date being set as well.

Am I even understanding correctly how the 304 reply works? My understanding is that the first call should have stored in cache the result set. The second call, which returns the 304, is also empty - but should then automatically fetch results from the cache for usage? Or is this something we have to manually do?

Cache-Control:private controls whether the response can be cached by shared public caches like intermediate proxy servers. Both Cache-Control:private and Cache-Control:public directives are locally cached by the browser, if you’re not going through a proxy they should both have the same behavior with respect to browser caching.

Yes, a 304 response is empty upon which time the browser will transparently use/return it’s pre-cached version.

Right, so private/public I understand, although I’m not sure why the 304 response switched my cache headers to private only, rather than including no-cache.

Here’s my exact code

private readonly CacheControl DefaultCacheControl = CacheControl.NoCache | CacheControl.Private;

[RequiredApiPermission("Read Orders")]
public IHttpResult Get(OrderListRequest request)
    {
        var userInfo = GetUserInfo().ThrowOnBadUser();

        OrderList orderList = orderManager.GetOrderListByCompany(userInfo.CompanyId, userInfo.TimeZoneId, request.Status, true);

        if (request.ModifiedAfter.HasValue && orderList.Orders != null)
        {
            DateTime modifiedAfterDate = DateTimeOffset.FromUnixTimeSeconds(request.ModifiedAfter.Value).UtcDateTime;
            orderList.Orders = orderList.Orders.ToList().Where(o => o.ModifiedDate >= modifiedAfterDate);
        }

        var converstion = ConvertOrderListResponse(orderList, userInfo);

        return new HttpResult(converstion, converstion == null ? HttpStatusCode.NoContent : HttpStatusCode.OK)
        {
            LastModified = converstion?.Max(x => x.ModifiedDate),
            CacheControl = DefaultCacheControl
        };
    }

Another thing I can’t figure out. If i try to test locally, using localhost with full IIS for api, and our front end running local (angular), the cach-control seems to be ignored all together and is always private.

Ok the issue was that 304’s were short-circuiting the responses and bypassing writing any Cache-Control headers to the response. ServiceStack isn’t writing the Cache-Control: private header so it’s being added by the underlying Web Server. I’ve changed it so 304 responses also writes the Cache Headers when available in this commit.

This change is available from v5.03 that’s now available on MyGet.

I’ll give that a try tonight.

It worked! I am getting 304 on each request correctly now without cached response.

1 Like