Skip to content

Commit 0b9cfc9

Browse files
committed
Change NotModifiedFilter to ExceptionFilter to let cache clients return non-must-validate cached results on error
1 parent 4f46907 commit 0b9cfc9

10 files changed

Lines changed: 178 additions & 44 deletions

File tree

src/ServiceStack.Client/AsyncServiceClient.cs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ public partial class AsyncServiceClient : IHasSessionId, IHasVersion
8585
/// <summary>
8686
/// Called with requestUri, ResponseType when server returns 304 NotModified
8787
/// </summary>
88-
public NotModifiedFilterDelegate NotModifiedFilter { get; set; }
88+
public ExceptionFilterDelegate ExceptionFilter { get; set; }
8989

9090
public string BaseUri { get; set; }
9191
public bool DisableAutoCompression { get; set; }
@@ -355,16 +355,13 @@ private void ResponseCallback<T>(IAsyncResult asyncResult)
355355
return;
356356
}
357357

358-
if (webEx.IsNotModified())
358+
if (ExceptionFilter != null && webEx != null && webEx.Response != null)
359359
{
360-
if (NotModifiedFilter != null && webEx.Response != null)
360+
var cachedResponse = ExceptionFilter(webEx, webEx.Response, requestState.Url, typeof(T));
361+
if (cachedResponse is T)
361362
{
362-
var cachedResponse = NotModifiedFilter(webEx.Response, requestState.Url, typeof(T));
363-
if (cachedResponse is T)
364-
{
365-
requestState.OnSuccess((T)cachedResponse);
366-
return;
367-
}
363+
requestState.OnSuccess((T)cachedResponse);
364+
return;
368365
}
369366
}
370367

src/ServiceStack.Client/CachedServiceClient.cs

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ public long NotModifiedHits
3333
get { return notModifiedHits; }
3434
}
3535

36+
private long errorFallbackHits;
37+
public long ErrorFallbackHits
38+
{
39+
get { return errorFallbackHits; }
40+
}
41+
3642
private long cachesAdded;
3743
public long CachesAdded
3844
{
@@ -50,6 +56,7 @@ public long CachesRemoved
5056
private readonly Action<HttpWebRequest> existingRequestFilter;
5157
private readonly ResultsFilterDelegate existingResultsFilter;
5258
private readonly ResultsFilterResponseDelegate existingResultsFilterResponse;
59+
private readonly ExceptionFilterDelegate existingExceptionFilter;
5360

5461
private readonly ServiceClientBase client;
5562

@@ -69,11 +76,12 @@ public CachedServiceClient(ServiceClientBase client)
6976
existingRequestFilter = client.RequestFilter;
7077
existingResultsFilter = client.ResultsFilter;
7178
existingResultsFilterResponse = client.ResultsFilterResponse;
79+
existingExceptionFilter = client.ExceptionFilter;
7280

7381
client.RequestFilter = OnRequestFilter;
7482
client.ResultsFilter = OnResultsFilter;
7583
client.ResultsFilterResponse = OnResultsFilterResponse;
76-
client.NotModifiedFilter = OnNotModifiedFilter;
84+
client.ExceptionFilter = OnExceptionFilter;
7785
}
7886

7987
private void OnRequestFilter(HttpWebRequest webReq)
@@ -111,15 +119,29 @@ private object OnResultsFilter(Type responseType, string httpMethod, string requ
111119
return ret;
112120
}
113121

114-
public object OnNotModifiedFilter(WebResponse webRes, string requestUri, Type responseType)
122+
public object OnExceptionFilter(WebException webEx, WebResponse webRes, string requestUri, Type responseType)
115123
{
124+
if (existingExceptionFilter != null)
125+
{
126+
var response = existingExceptionFilter(webEx, webRes, requestUri, responseType);
127+
if (response != null)
128+
return response;
129+
}
130+
116131
HttpCacheEntry entry;
117132
if (cache.TryGetValue(requestUri, out entry))
118133
{
119-
Interlocked.Increment(ref notModifiedHits);
120-
return entry.Response;
134+
if (webEx.IsNotModified())
135+
{
136+
Interlocked.Increment(ref notModifiedHits);
137+
return entry.Response;
138+
}
139+
if (entry.CanUseCacheOnError())
140+
{
141+
Interlocked.Increment(ref errorFallbackHits);
142+
return entry.Response;
143+
}
121144
}
122-
123145
return null;
124146
}
125147

@@ -140,6 +162,7 @@ private void OnResultsFilterResponse(WebResponse webRes, object response, string
140162
var entry = new HttpCacheEntry(response)
141163
{
142164
ETag = eTag,
165+
ContentLength = webRes.ContentLength >= 0 ? webRes.ContentLength : (long?)null,
143166
};
144167

145168
if (lastModifiedStr != null)
@@ -179,7 +202,7 @@ private void OnResultsFilterResponse(WebResponse webRes, object response, string
179202
}
180203
}
181204

182-
entry.Expires = entry.Created + entry.MaxAge;
205+
entry.SetMaxAge(entry.MaxAge);
183206
cache[requestUri] = entry;
184207
Interlocked.Increment(ref cachesAdded);
185208

src/ServiceStack.Client/HttpCacheEntry.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,30 @@ public HttpCacheEntry(object response)
1818
public TimeSpan? Age { get; set; }
1919
public TimeSpan MaxAge { get; set; }
2020
public DateTime Expires { get; set; }
21+
public long? ContentLength { get; set; }
2122
public object Response { get; set; }
2223

23-
public void InitMaxAge(TimeSpan maxAge)
24+
public void SetMaxAge(TimeSpan maxAge)
2425
{
2526
MaxAge = maxAge;
26-
Expires = Created + maxAge;
27+
Expires = maxAge > TimeSpan.Zero
28+
? Created + maxAge
29+
: Created - TimeSpan.FromSeconds(1); //auto expire
30+
}
31+
32+
public bool HasExpired()
33+
{
34+
return DateTime.UtcNow > Expires;
35+
}
36+
37+
public bool CanUseCacheOnError()
38+
{
39+
return !NoCache && !(MustRevalidate && HasExpired());
2740
}
2841

2942
public bool ShouldRevalidate()
3043
{
31-
return NoCache || DateTime.UtcNow > Expires; //always implies MustRevalidate
44+
return NoCache || HasExpired(); //always implies MustRevalidate
3245
}
3346
}
3447
}

src/ServiceStack.Client/ICachedServiceClient.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public interface ICachedServiceClient : IServiceClient
1010
int CacheCount { get; }
1111
long CacheHits { get; }
1212
long NotModifiedHits { get; }
13+
long ErrorFallbackHits { get; }
1314
long CachesAdded { get; }
1415
long CachesRemoved { get; }
1516

@@ -32,6 +33,7 @@ public static Dictionary<string, string> GetStats(this ICachedServiceClient clie
3233
{ "CacheCount", client.CacheCount + "" },
3334
{ "CacheHits", client.CacheHits + "" },
3435
{ "NotModifiedHits", client.NotModifiedHits + "" },
36+
{ "ErrorFallbackHits", client.ErrorFallbackHits + "" },
3537
{ "CachesAdded", client.CachesAdded + "" },
3638
{ "CachesRemoved", client.CachesRemoved + "" },
3739
};

src/ServiceStack.Client/ServiceClientBase.cs

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -422,17 +422,17 @@ public ResultsFilterResponseDelegate ResultsFilterResponse
422422
/// <summary>
423423
/// Called with requestUri, ResponseType when server returns 304 NotModified
424424
/// </summary>
425-
public NotModifiedFilterDelegate notModifiedFilter;
426-
public NotModifiedFilterDelegate NotModifiedFilter
425+
public ExceptionFilterDelegate exceptionFilter;
426+
public ExceptionFilterDelegate ExceptionFilter
427427
{
428428
get
429429
{
430-
return notModifiedFilter;
430+
return exceptionFilter;
431431
}
432432
set
433433
{
434-
notModifiedFilter = value;
435-
asyncClient.NotModifiedFilter = value;
434+
exceptionFilter = value;
435+
asyncClient.ExceptionFilter = value;
436436
}
437437
}
438438

@@ -646,16 +646,13 @@ protected virtual bool HandleResponseException<TResponse>(Exception ex, object r
646646
throw;
647647
}
648648

649-
if (webEx.IsNotModified())
649+
if (ExceptionFilter != null && webEx != null && webEx.Response != null)
650650
{
651-
if (NotModifiedFilter != null && webEx.Response != null)
651+
var cachedResponse = ExceptionFilter(webEx, webEx.Response, requestUri, typeof(TResponse));
652+
if (cachedResponse is TResponse)
652653
{
653-
var cachedResponse = NotModifiedFilter(webEx.Response, requestUri, typeof(TResponse));
654-
if (cachedResponse is TResponse)
655-
{
656-
response = (TResponse)cachedResponse;
657-
return true;
658-
}
654+
response = (TResponse)cachedResponse;
655+
return true;
659656
}
660657
}
661658

@@ -1887,5 +1884,5 @@ public interface IServiceClientMeta
18871884

18881885
public delegate void ResultsFilterResponseDelegate(WebResponse webResponse, object response, string httpMethod, string requestUri, object request);
18891886

1890-
public delegate object NotModifiedFilterDelegate(WebResponse webResponse, string requestUri, Type responseType);
1887+
public delegate object ExceptionFilterDelegate(WebException webEx, WebResponse webResponse, string requestUri, Type responseType);
18911888
}

src/ServiceStack.HttpClient/CachedHttpClient.cs

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Collections.Generic;
44
using System.IO;
55
using System.Linq;
6+
using System.Net;
67
using System.Net.Http;
78
using System.Net.Http.Headers;
89
using System.Threading;
@@ -34,6 +35,12 @@ public long NotModifiedHits
3435
get { return notModifiedHits; }
3536
}
3637

38+
private long errorFallbackHits;
39+
public long ErrorFallbackHits
40+
{
41+
get { return errorFallbackHits; }
42+
}
43+
3744
private long cachesAdded;
3845
public long CachesAdded
3946
{
@@ -51,6 +58,7 @@ public long CachesRemoved
5158
private readonly Action<HttpRequestMessage> existingRequestFilter;
5259
private readonly ResultsFilterHttpDelegate existingResultsFilter;
5360
private readonly ResultsFilterHttpResponseDelegate existingResultsFilterResponse;
61+
private ExceptionFilterHttpDelegate existingExceptionFilter;
5462

5563
private readonly JsonHttpClient client;
5664

@@ -70,11 +78,12 @@ public CachedHttpClient(JsonHttpClient client)
7078
existingRequestFilter = client.RequestFilter;
7179
existingResultsFilter = client.ResultsFilter;
7280
existingResultsFilterResponse = client.ResultsFilterResponse;
81+
existingExceptionFilter = client.ExceptionFilter;
7382

7483
client.RequestFilter = OnRequestFilter;
7584
client.ResultsFilter = OnResultsFilter;
7685
client.ResultsFilterResponse = OnResultsFilterResponse;
77-
client.NotModifiedFilter = OnNotModifiedFilter;
86+
client.ExceptionFilter = OnExceptionFilter;
7887
}
7988

8089
private void OnRequestFilter(HttpRequestMessage webReq)
@@ -114,13 +123,28 @@ private object OnResultsFilter(Type responseType, string httpMethod, string requ
114123
return ret;
115124
}
116125

117-
public object OnNotModifiedFilter(HttpResponseMessage webRes, string requestUri, Type responseType)
126+
public object OnExceptionFilter(HttpResponseMessage webRes, string requestUri, Type responseType)
118127
{
128+
if (existingExceptionFilter != null)
129+
{
130+
var response = existingExceptionFilter(webRes, requestUri, responseType);
131+
if (response != null)
132+
return response;
133+
}
134+
119135
HttpCacheEntry entry;
120136
if (cache.TryGetValue(requestUri, out entry))
121137
{
122-
Interlocked.Increment(ref notModifiedHits);
123-
return entry.Response;
138+
if (webRes.StatusCode == HttpStatusCode.NotModified)
139+
{
140+
Interlocked.Increment(ref notModifiedHits);
141+
return entry.Response;
142+
}
143+
if (entry.CanUseCacheOnError())
144+
{
145+
Interlocked.Increment(ref errorFallbackHits);
146+
return entry.Response;
147+
}
124148
}
125149

126150
return null;
@@ -142,6 +166,7 @@ private void OnResultsFilterResponse(HttpResponseMessage webRes, object response
142166
var entry = new HttpCacheEntry(response)
143167
{
144168
ETag = eTag,
169+
ContentLength = webRes.Content.Headers.ContentLength
145170
};
146171

147172
if (webRes.Content.Headers.LastModified != null)
@@ -161,7 +186,7 @@ private void OnResultsFilterResponse(HttpResponseMessage webRes, object response
161186
entry.MustRevalidate = cacheControl.MustRevalidate;
162187
entry.NoCache = cacheControl.NoCache;
163188

164-
entry.Expires = entry.Created + entry.MaxAge;
189+
entry.SetMaxAge(entry.MaxAge);
165190
cache[requestUri] = entry;
166191
Interlocked.Increment(ref cachesAdded);
167192

src/ServiceStack.HttpClient/JsonHttpClient.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public class JsonHttpClient : IServiceClient, IJsonServiceClient, IHasCookieCont
3131

3232
public ResultsFilterHttpDelegate ResultsFilter { get; set; }
3333
public ResultsFilterHttpResponseDelegate ResultsFilterResponse { get; set; }
34-
public NotModifiedFilterHttpDelegate NotModifiedFilter { get; set; }
34+
public ExceptionFilterHttpDelegate ExceptionFilter { get; set; }
3535

3636
public const string DefaultHttpMethod = HttpMethods.Post;
3737
public static string DefaultUserAgent = "ServiceStack .NET HttpClient " + Env.ServiceStackVersion;
@@ -232,9 +232,9 @@ public Task<TResponse> SendAsync<TResponse>(string httpMethod, string absoluteUr
232232
var httpRes = responseTask.Result;
233233
ApplyWebResponseFilters(httpRes);
234234

235-
if (httpRes.StatusCode == HttpStatusCode.NotModified && NotModifiedFilter != null)
235+
if (!httpRes.IsSuccessStatusCode && ExceptionFilter != null)
236236
{
237-
var cachedResponse = NotModifiedFilter(httpRes, absoluteUrl, typeof(TResponse));
237+
var cachedResponse = ExceptionFilter(httpRes, absoluteUrl, typeof(TResponse));
238238
if (cachedResponse is TResponse)
239239
return Task.FromResult((TResponse)cachedResponse);
240240
}
@@ -968,7 +968,7 @@ public void Dispose()
968968

969969
public delegate void ResultsFilterHttpResponseDelegate(HttpResponseMessage webResponse, object response, string httpMethod, string requestUri, object request);
970970

971-
public delegate object NotModifiedFilterHttpDelegate(HttpResponseMessage webResponse, string requestUri, Type responseType);
971+
public delegate object ExceptionFilterHttpDelegate(HttpResponseMessage webResponse, string requestUri, Type responseType);
972972

973973
public static class JsonHttpClientUtils
974974
{

src/ServiceStack/HttpCacheFeature.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public class HttpCacheFeature : IPlugin
1717
public HttpCacheFeature()
1818
{
1919
DefaultMaxAge = TimeSpan.FromHours(1);
20-
CacheControlForOptimizedResults = "max-age=0, must-revalidate";
20+
CacheControlForOptimizedResults = "max-age=0";
2121
}
2222

2323
public void Register(IAppHost appHost)

tests/ServiceStack.WebHost.Endpoints.Tests/CacheServerFeatureTests.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,16 @@ public bool Equals(CachedRequest other)
366366
}
367367
}
368368

369+
public class FailsAfterOnce : CacheRequestBase, IReturn<FailsAfterOnce>, IEquatable<FailsAfterOnce>
370+
{
371+
internal static int Count = 0;
372+
373+
public bool Equals(FailsAfterOnce other)
374+
{
375+
return base.Equals(other);
376+
}
377+
}
378+
369379
public class CacheEtagServices : Service
370380
{
371381
public object Any(SetCache request)
@@ -403,5 +413,21 @@ public object Any(CachedRequest request)
403413
Request.QueryString.ToString(),
404414
() => request);
405415
}
416+
417+
public object Any(FailsAfterOnce request)
418+
{
419+
if (FailsAfterOnce.Count++ > 0)
420+
throw new Exception("Can only be called once");
421+
422+
return new HttpResult(request)
423+
{
424+
Age = request.Age,
425+
ETag = request.ETag,
426+
MaxAge = request.MaxAge,
427+
Expires = request.Expires,
428+
LastModified = request.LastModified,
429+
CacheControl = request.CacheControl.GetValueOrDefault(CacheControl.None),
430+
};
431+
}
406432
}
407433
}

0 commit comments

Comments
 (0)