Service not emitting Etag header

Hi, I’ve implemented a sample service to test the http caching using ETag header.
Using a basic implementation as shown in the docs

[CacheResponse(Duration=60, MaxAge=30)]
public object Any(GetCustomer request)
{
var response = Db.SingleById(request.Id);
return new HttpResult(response) {
ETag = “myetag”
};
}

however with this implementation, the response does not contain the Etag header.

I added the following code
Response.AddHeader(“ETag”,“pippo”); to explicitly add the etag header

however doing so when I send a second request with
If-None-Match: pippo
I’m not returned a not modified but the full response (server cached) …

What am I missing ?
thank you
enrico

I had a look at the HttpCacheFeature class :

  1. looking at this line of code :
    CacheInfo cacheInfo1 = RequestExtensions.GetItem(req, Keywords.CacheInfo) as CacheInfo;
    if (cacheInfo1 != null && cacheInfo1.CacheKey != null && this.CacheAndWriteResponse(cacheInfo1, req, res, response))
    return;

it seems that if I attach a CacheResponse attribute, it will always fall into this if … and CacheAndWriteResponse does never emit the Etag header

  1. if I remove the attribute and return a HttpResult it does emit etag since it falls into the following code

HttpResult httpResult = response as HttpResult;
if (httpResult == null)
return;
CacheInfo cacheInfo2 = CacheInfoExtensions.ToCacheInfo(httpResult);
… which does emit the Etag
However I lost server caching in this case (is it correct?)

  1. I always get a cache-control header set to private, without max age (whether I use the attriute or the HttpResult option) … looking at the HttpCacheFeature code this does not seem possibile … it appears that something is resetting the header next in the flow … what could it be ?

thank you
enrico

You should only be using one of the caching constructs, do not use both. So if you’re going to return an Etag do not use the [CacheResponse] attribute as well.

thank you, however if I don’t use CacheResponse, can I still use ToOptimizedResult to achieve server caching ?
Using ToOptimizedResult and returning an HttpResult can I mix both client and server caching ?

What about point 3 ? In my tests the cache-control header is always set to private with no max.age

thank you
enrico

No you can’t mix and match ETag with another caching construct. The purpose of the ETag is to determine if the entity has changed, you can’t determine if the underlying entity has changed if you’re also caching the response. If the entity hasn’t changed the server returns a 304 Not Modified with no response.

The HTTP Caching Tests show returning HttpResult caching properties working as intended:

var client = GetClient();
client.ResponseFilter = res =>
{
    Assert.That(res.Headers[HttpHeaders.Age], Is.EqualTo("864000"));
    Assert.That(res.Headers[HttpHeaders.ETag], Is.EqualTo("etag".Quoted()));
    Assert.That(res.Headers[HttpHeaders.CacheControl], Is.EqualTo("max-age=86400, public, must-revalidate, no-store"));
};

var request = new SetCache
{
    ETag = "etag",
    Age = TimeSpan.FromDays(10),
    MaxAge = TimeSpan.FromDays(1),
    CacheControl = CacheControl.Public | CacheControl.NoStore | CacheControl.MustRevalidate,
};
var response = client.Get(request);

If you have a valid example where this isn’t the case please open a new issue with a stand-alone failing example I can run locally to reproduce the issue.

Well, it seems that the cache-control is always set to private if the request has no session cookie
this:
GET http://localhost:7885/healthcheck HTTP/1.1
Accept: application/json
User-Agent: ServiceStack .NET Client 4.060
Accept-Encoding: gzip,deflate
Host: localhost:7885

returns this

HTTP/1.1 200 OK
Cache-Control: private
Transfer-Encoding: chunked
Content-Type: application/json; charset=utf-8
Vary: Accept
Server: Microsoft-IIS/10.0
X-Powered-By: ServiceStack/4,060 Win32NT/.NET
X-AspNet-Version: 4.0.30319
Set-Cookie: ss-id=0KfG1QNbatvPacqICMdL; path=/; HttpOnly
Set-Cookie: ss-pid=Hf3JE4A00NwZ1tUXsQix; expires=Mon, 25-Aug-2036 10:02:57 GMT; path=/; HttpOnly
X-SourceFiles: =?UTF-8?B?QzpcREVWXERpZ2l0YWwgTWVkaWFcREhTXEFQSVxESFNCdXNpbmVzc1NlcnZpY2VcbWFzdGVyXHNyY1xESFNfQlMuSHR0cFxoZWFsdGhjaGVjaw==?=
X-Powered-By: ASP.NET
Date: Thu, 25 Aug 2016 10:02:58 GMT

instead this
GET http://localhost:7885/healthcheck HTTP/1.1
Accept: application/json
User-Agent: ServiceStack .NET Client 4.060
Accept-Encoding: gzip,deflate
Host: localhost:7885
Cookie: ss-id=Rd4qGROwQOH8dTT8KmpL; ss-pid=GDhaNSrMKokLKzALru6L

returns
HTTP/1.1 200 OK
Cache-Control: max-age=20, public
Transfer-Encoding: chunked
Content-Type: application/json; charset=utf-8
Vary: Accept
Server: Microsoft-IIS/10.0
X-Powered-By: ServiceStack/4,060 Win32NT/.NET
X-AspNet-Version: 4.0.30319
X-SourceFiles: =?UTF-8?B?QzpcREVWXERpZ2l0YWwgTWVkaWFcREhTXEFQSVxESFNCdXNpbmVzc1NlcnZpY2VcbWFzdGVyXHNyY1xESFNfQlMuSHR0cFxoZWFsdGhjaGVjaw==?=
X-Powered-By: ASP.NET

with the correct max-age set on the server side

how is that ?

this

seems to explain the issue I am facing …
you might want to replace .addcookie with a generic .addheader to bypass this nasty asp.net behaviour

No it needs to use ASP.NET’s explicit SetCookie API in order to set cookies otherwise they can get suppressed.

But is the issue just the Set-Cookie HTTP Response Header and not the Cookie HTTP Request Header? I.e. What’s the behavior when you disable Auth/Sessions?

Hi,
if I do this
Plugins.RemoveAll(x => x is SessionFeature);
the problem “goes” awway … no session cookie is emitted and the cache-control is returned as set in the code
var ret2 = new HttpResult(response2)
{
MaxAge = TimeSpan.FromSeconds(20),
CacheControl = CacheControl.Private,
ETag = getETag(response2)
};
return ret2;

HTTP/1.1 200 OK
Cache-Control: max-age=20, private
Transfer-Encoding: chunked
Content-Type: application/json; charset=utf-8
ETag: “etag0”

ok so it looks like ASP.NET changes caching visibility when the server returns create cookie instructions, which is understandable.