I'm experimenting with caching between my .NET client and server. I'm seeing a seemingly random number of hits to an endpoint before WinInet decides to cache the result.
The .NET client makes requests using HttpWebRequest
:
HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(uiTextBoxUrl.Text);
var policy = new RequestCachePolicy(RequestCacheLevel.CacheIfAvailable);
webRequest.CachePolicy = policy;
WebResponse webResponse = webRequest.GetResponse();
The server, implemented using ASP.net Web API, sets these CacheControl
headers:
response.Headers.CacheControl = new CacheControlHeaderValue
{
MaxAge =3600,
MustRevalidate = true,
Public = true,
Private = true
};
Using a test-harness with a button that sends the request to the endpoint, I can see that, even though CacheIfAvailable
is used, the response isn't immediately cached. By checking the debug output on my server, I'm seeing that a seemingly random number of hits (or more likely hit-count/elapsed time heuristics) need to be fired before the request is eventually cached. If I whack the test button quickly, it'll start caching after about 10 hits. If I click the button every 1 or 2 seconds, I've counted up to 25 hits before caching kicks in.
This is the response I'm seeing from Fiddler:
HTTP/200 responses are cacheable by default, unless Expires, Pragma, or Cache-Control headers are present and forbid caching.
HTTP/1.1 Cache-Control Header is present: public, must-revalidate, max-age=3600, private
private: This response MUST NOT be cached by a shared cache.
public: This response MAY be cached by any cache.
max-age: This resource will expire in 1 hours. [3600 sec]
must-revalidate: After expiration, the server MUST be contacted to verify the freshness of this resource.
HTTP/1.1 ETAG Header is present: "3488770a-8659-4fc0-b579-dcda9200a1c7"
I've read that HttpWebRequest
uses WinInet for caching, so I'm curious as to how WinInet determines when something needs to be cached, more specifically, why does it not cache on the first hit?
The WinINet functions have simple, yet flexible, built-in caching support. Any data retrieved from the network is cached on the hard disk and retrieved for subsequent requests. The application can control the caching on each request. For http requests from the server, most headers received are also cached.
Web caching works by caching the HTTP responses for requests according to certain rules. Subsequent requests for cached content can then be fulfilled from a cache closer to the user instead of sending the request all the way back to the web server.
I use RequestCacheLevel.Default otherwise I found Wininet will happily serve stale responses.
I also do not use must-revalidate, as I assume this is the default behaviour once a cached response is stale.
I believe if you always include a LastModifed or Etag in your requests then by default you will get the local copy until it has expired (because you set Private and MaxAge) and once it has expired then you might get a 304 back in which case it will use the local copy.
Having said all of this check your responses for the vary header. In many cases a Vary header will stop Wininet from being used because it does not support the vary header properly.
Using this controller,
public class PrivateCachingController : ApiController
{
public HttpResponseMessage Get()
{
var response = new HttpResponseMessage()
{
Content = new StringContent("This is cached content")
};
response.Headers.CacheControl = new CacheControlHeaderValue() {MaxAge = new TimeSpan(0,0,0,60)};
return response;
}
}
and this client code,
var clientHandler = new WebRequestHandler();
clientHandler.CachePolicy = new RequestCachePolicy(RequestCacheLevel.Default);
var client = new HttpClient(clientHandler) { BaseAddress = _BaseAddress };
var response = await client.GetAsync("/PrivateCaching");
var response2 = await client.GetAsync("/PrivateCaching");
When it executes, there is only one request made across the network.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With