CachedServiceClient and last-modified

I’ve been working on some caching strategies and some unit tests to go with them. I was getting some unexpected results and not sure if my expectations are wrong or if it’s code.

Specifically, I have an endpoint decorated with

[CacheResponse(VaryByUser = true)]

and returns

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

When I use the CachedServiceClient to make a call to the endpoint I expect to see a cached entry based on the Last-Modified I return. However, if I follow through the code in CachedServiceClient.OnResultsFilterResponse I notice that

var lastModifiedStr = webRes.Headers[HttpHeaders.LastModified];

Doesn’t return a value. There is no header. Instead, the WebResponse object has a property LastModified that does have the correct value - but it’s not looked at.

So, what should i be expecting here? Am i missing something or looking at this incorrectly?

This isn’t defined behavior, the [CacheResponse] attribute is independent from the HTTP Client Features which doesn’t compose together.

Sure, I understand that - but why is the client ignoring the fact that the last-modified is being returned?

Because it short circuits the request with its own implementation it has to implement its server caching feature.

Ok, so I’m definitely confused here. Perhaps I’m missing the obvious here, but what’s the difference between the serviceclients and httpclients ?

I’m now attempting to use the CachedHttpClient as I think that’s what I should be using here.

However, in your code for the CachedHttpClient you ignore the possibility of Last-Modified being set with No-Cache headers. My understanding as per here (goto cache validation) and here is that no-cache “forces submit to the server before releasing a cached copy” - as expected when you get a 304 response. The 304 would then allow the cached copy from the previous call to be used. If it’s not a 304 that entry would be replaced.

The [CacheResponse] attribute enables server caching where the Response is cached on the Server and requests to the same cached resource returns the cached response automatically.

The HTTP Client Caching features enables fine-grain control over HTTP Response Headers to enable caching on the client.

They are independent features which are not compatible.

I’ve been going down this rabbit hole and have a few more questions.

You state that the CacheResponse attribute is independent from the HTTP Client Features, however, in order to use the attribute you have to enable the feature. So how does that make it independent?

Also, when i follow the code for CacheResponse I’m curious as to why the CacheInfo object doesn’t include LastModified being set? or the ETag for that matter.

So, are you saying you can’t mix server and client caching with the same endpoint? That appears to be the case with my testing. If that’s the case there should be some way of notifying or throwing an exception on that endpoint. It’s very easy to mix up and try to use both.

Right the [CacheResponse] enables server caching whose purpose is to cache the response and return it when a similar request matches the cached response pertaining to the configuration of the [CacheResponse] attribute.

It shares internal constructs to facilitate its implementation but it’s purpose is independent from the fine-grained HTTP client caching features by returning a custom HttpResult. The [CacheResponse] attribute still caches the response that the HttpResult wraps, but its cached responses doesn’t have access to custom Headers returned in previous requests when the cache was created.

Ok, so now that I’m clear with that - I go back to a previous question found here.

Consider I’m using only client caching features with HttpResult. My endpoint is simply list/All , but varies per user. If, on the client, the user logs out, and a new one logs in, it’s still auto-magically passing the Last-Modified of the previous call. I can’t seem to find a way to clear those values in the browser/angular5 on logout of a user. I also can’t see how it would be possible to do any sort of check on the server.

Please provide the Request/Response HTTP Headers and the Service Implementation that created it.

Here’s the service. Note that I use a custom authentication attribute. The first line is getting the specific user information. The user would have been authenticated already at this point, and you can see that we make a specific call based on that user information.

    [RequiredApiPermission("Read Orders")]
    public IHttpResult Get(OrderPagedListRequest request)
    {
        var userInfo = GetUserInfo().ThrowOnBadUser();
        OrderList orderList = orderManager.GetPagedList(userInfo.CompanyId, request.Status, request.SearchText, request.SortField, request.SortDirection, request.PageNumber, request.PageSize);
        var converstion = ConvertOrderListResponse(orderList, userInfo);
        var response = new OrderPagedListResponse
        {
            Orders = converstion,
            TotalOrders = orderList?.TotalOrders ?? 0
        };

        var maxDate = converstion != null && converstion.Any()
            ? converstion.Max(x => x.ModifiedDate)
            : (DateTime?) null;
        
        return new HttpResult(response)
        {
            LastModified = maxDate,
            CacheControl = CacheControl.NoCache | CacheControl.Private
        };
    }

The following are headers of the requests. User A makes the first call, we logout, login to User B, who then makes the second call. The same Last-Modified is passed automatically instead of either a- passing no date or b-server somehow knowing it’s a different user (but since this is client caching i don’t see how the server would know).


If the issue is because the client is caching the response you can use the Vary HTTP Header to tell instruct the client on how to differentiate caches. For maintaining different caches for each user you can use Vary: Cookie, e.g:

return new HttpResult(response)
{
    LastModified = maxDate,
    CacheControl = CacheControl.NoCache | CacheControl.Private,
    Headers =
    {
        { HttpHeaders.Vary, "Cookie" }
    }
};

Great. I was just starting to look into that Vary Header. It’s a bit new to me.

Now that this looks to be out of the way, I digress back to another question/concern from earlier in this thread.

To verify that any of my http client cache endpoints are working correctly, I have some unit tests. I’m using the CacheHttpCient to make calls, but I don’t get expected results.

in your code for the CachedHttpClient you ignore the possibility of Last-Modified being set with No-Cache headers. My understanding as per here (goto cache validation) and here is that no-cache “forces submit to the server before releasing a cached copy” - as expected when you get a 304 response. The 304 would then allow the cached copy from the previous call to be used. If it’s not a 304 that entry would be replaced.

So, I would expect that the CachedHttpClient put an entry in it’s cache for my endpoint (example above).

If you have a stand-alone unit tests on expected behavior which fails please provide them.

Here’s a simple example. This is my basic service

 public class CachingTestService : IService
{
    private readonly CacheControl DefaultCacheControl = CacheControl.NoCache | CacheControl.Private;

    [CacheTestResponse(VaryByUser = true)]
    public IHttpResult Get(CacheGetRequest request)
    {
        return new HttpResult(new CacheResponse() { Something = "same" }, HttpStatusCode.OK)
        {
            LastModified = request.LastModifiedReturnDate,
            CacheControl = DefaultCacheControl
        };
    }

    public IHttpResult Get(CacheGetTwoRequest request)
    {
        return new HttpResult(new CacheResponse() { Something = "same" }, HttpStatusCode.OK)
        {
            LastModified = request.LastModifiedReturnDate,
            CacheControl = DefaultCacheControl,
            Headers =
            {
              { HttpHeaders.Vary, "Cookie" }
            }
        };

    }
}

public class CacheGetRequest : IGet, IReturn<CacheResponse>
{
    public DateTime? LastModifiedReturnDate { get; set; }
}

public class CacheGetTwoRequest : CacheGetRequest
{
}

public class CacheResponse
{
    public string Something { get; set; }
}

Now a simple test

[Test]
    public void NoCacheAttribute()
    {
        var modifiedDate = DateTime.UtcNow;

        using (var client = new CachedHttpClient(new JsonHttpClient(BaseUri)))
        {
            // act
            var response = client.Get(new CacheGetTwoRequest() { LastModifiedReturnDate = modifiedDate });
            client.CacheCount.Should().Be(1);
            client.NotModifiedHits.Should().Be(0);

            response = client.Get(new CacheGetTwoRequest() { LastModifiedReturnDate = modifiedDate });
            client.CacheCount.Should().Be(1);
            client.NotModifiedHits.Should().Be(1);
        }
    }

My expectation is that on the first call to the endpoint, 200 is returned with a Last-Modified date. The client then should add it to the cache.

On the second call I expect the client to add If-Modified-Since header and return a 304 with empty body , then using the clients cached version.

Are my expectations wrong? Or have I made a simple mistake in the code?

Without a stand-alone test I can run locally I wont be able to debug what’s actually happening, from here I see that you don’t want to be returning CacheControl.NoCache as it prevents the Cached Client from caching anything, i.e. only return that when you want to skip caching on the client completely.

You can run that code locally. There’s nothing special about it. That said, that’s not how the specs for NoCache say it should work. NoCache should cached on the client but only be used when 304 is returned.

No-Cache

“Forces caches to submit the request to the origin server for validation before releasing a cached copy.”

Without this implementation, how else could you possibly deal with 304 responses? It has to be cached but only used with the reply is 304. That’s how a browser treats it. And works as expected using a browser. As per a previous thread here where a chance allowed 304s to work as expected with browsers.

Additionally, I’ve also found that the CachedHttpClient isn’t using consistent url encoding. During debug, the _OnResultsFilter will have a

requestUri http://localhost:1337/json/reply/CacheGetTwoRequest?lastModifiedReturnDate=2018-02-26T15%3A42%3A02.7927173Z&AuthSecret=mocking

where the OnRequestFilter uses webReq.RequestUri.ToString() which is actually
http://localhost:1337/json/reply/CacheGetTwoRequest?lastModifiedReturnDate=2018-02-26T15:42:02.7927173Z&AuthSecret=mocking

and thus would never find the cached result in the dictionary anyway.

I looked into it and saw that NoCache was being ignored in CachedHttpClient but not in CachedServiceClient so I’ve stopped it from being ignored in this commit.

It’s not going to get a 304 response for clients who ignore caching No-Cache requests since they wont have sent out any client caching headers with the Request for which the Server can validate and return a 304 on.

I’ve also changed JsonHttpClient to use the parsed URL with Uri which should normalize the URL’s used.

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

Thanks. I will give it a try after lunch.