1 //------------------------------------------------------------------------------
2 // <copyright file="OutputCacheModule.cs" company="Microsoft">
3 // Copyright (c) Microsoft Corporation. All rights reserved.
5 //------------------------------------------------------------------------------
10 namespace System.Web.Caching {
13 using System.Threading;
14 using System.Collections;
15 using System.Globalization;
16 using System.Security.Cryptography;
18 using System.Web.Caching;
19 using System.Web.Util;
20 using System.Collections.Specialized;
21 using System.Web.Configuration;
22 using System.Web.Management;
23 using System.Web.Hosting;
24 using System.Web.Security.Cryptography;
27 * Holds header and param names that this cached item varies by.
30 internal class CachedVary {
31 // _id is used by OutputCacheProviders
32 private Guid _cachedVaryId;
33 internal readonly string[] _contentEncodings;
34 internal readonly string[] _headers;
35 internal readonly string[] _params;
36 internal readonly string _varyByCustom;
37 internal readonly bool _varyByAllParams;
39 internal Guid CachedVaryId { get { return _cachedVaryId; } }
41 internal CachedVary(string[] contentEncodings, string[] headers, string[] parameters, bool varyByAllParams, string varyByCustom) {
42 _contentEncodings = contentEncodings;
45 _varyByAllParams = varyByAllParams;
46 _varyByCustom = varyByCustom;
47 _cachedVaryId = Guid.NewGuid();
50 public override bool Equals(Object obj) {
51 CachedVary cv = obj as CachedVary;
56 return _varyByAllParams == cv._varyByAllParams
57 && _varyByCustom == cv._varyByCustom
58 && StringUtil.StringArrayEquals(_contentEncodings, cv._contentEncodings)
59 && StringUtil.StringArrayEquals(_headers, cv._headers)
60 && StringUtil.StringArrayEquals(_params, cv._params);
63 public override int GetHashCode() {
64 HashCodeCombiner hashCodeCombiner = new HashCodeCombiner();
65 hashCodeCombiner.AddObject(_varyByAllParams);
67 // Cast _varyByCustom to an object, since the HashCodeCombiner.AddObject(string)
68 // overload uses StringUtil.GetStringHashCode(). We want to use String.GetHashCode()
69 // in this method, since we do not require a stable hash code across architectures.
70 hashCodeCombiner.AddObject((object)_varyByCustom);
72 hashCodeCombiner.AddArray(_contentEncodings);
73 hashCodeCombiner.AddArray(_headers);
74 hashCodeCombiner.AddArray(_params);
75 return hashCodeCombiner.CombinedHash32;
80 * Holds the cached response.
82 internal class CachedRawResponse {
84 * Fields to store an actual response.
86 internal Guid _cachedVaryId;
87 internal readonly HttpRawResponse _rawResponse;
88 internal readonly HttpCachePolicySettings _settings;
89 internal readonly String _kernelCacheUrl;
91 internal CachedRawResponse(
92 HttpRawResponse rawResponse,
93 HttpCachePolicySettings settings,
94 String kernelCacheUrl,
96 _rawResponse = rawResponse;
98 _kernelCacheUrl = kernelCacheUrl;
99 _cachedVaryId = cachedVaryId;
104 // OutputCacheModule real implementation for premium SKUs
107 sealed class OutputCacheModule : IHttpModule {
108 const int MAX_POST_KEY_LENGTH = 15000;
109 const string NULL_VARYBY_VALUE = "+n+";
110 const string ERROR_VARYBY_VALUE = "+e+";
111 internal const string TAG_OUTPUTCACHE = "OutputCache";
112 const string OUTPUTCACHE_KEYPREFIX_POST = CacheInternal.PrefixOutputCache + "1";
113 const string OUTPUTCACHE_KEYPREFIX_GET = CacheInternal.PrefixOutputCache + "2";
114 const string IDENTITY = "identity";
115 const string ASTERISK = "*";
117 static internal readonly char[] s_fieldSeparators;
120 bool _recordedCacheMiss;
122 static OutputCacheModule() {
123 s_fieldSeparators = new char[] {',', ' '};
126 internal OutputCacheModule() {
129 internal static string CreateOutputCachedItemKey(
133 CachedVary cachedVary) {
141 NameValueCollection col;
145 if (verb == HttpVerb.POST) {
146 sb = new StringBuilder(OUTPUTCACHE_KEYPREFIX_POST, path.Length + OUTPUTCACHE_KEYPREFIX_POST.Length);
149 sb = new StringBuilder(OUTPUTCACHE_KEYPREFIX_GET, path.Length + OUTPUTCACHE_KEYPREFIX_GET.Length);
152 sb.Append(CultureInfo.InvariantCulture.TextInfo.ToLower(path));
154 /* key for cached vary item has additional information */
155 if (cachedVary != null) {
156 request = context.Request;
159 for (j = 0; j <= 2; j++) {
162 getAllParams = false;
167 a = cachedVary._headers;
169 col = request.GetServerVarsWithoutDemand();
175 Debug.Assert(cachedVary._params == null || !cachedVary._varyByAllParams, "cachedVary._params == null || !cachedVary._varyByAllParams");
178 a = cachedVary._params;
179 if (request.HasQueryString && (a != null || cachedVary._varyByAllParams)) {
180 col = request.QueryString;
181 getAllParams = cachedVary._varyByAllParams;
188 Debug.Assert(cachedVary._params == null || !cachedVary._varyByAllParams, "cachedVary._params == null || !cachedVary._varyByAllParams");
191 if (verb == HttpVerb.POST) {
192 a = cachedVary._params;
193 if (request.HasForm && (a != null || cachedVary._varyByAllParams)) {
195 getAllParams = cachedVary._varyByAllParams;
202 Debug.Assert(a == null || !getAllParams, "a == null || !getAllParams");
204 /* handle all params case (VaryByParams[*] = true) */
205 if (getAllParams && col.Count > 0) {
207 for (i = a.Length - 1; i >= 0; i--) {
209 a[i] = CultureInfo.InvariantCulture.TextInfo.ToLower(a[i]);
212 Array.Sort(a, InvariantComparer.Default);
216 for (i = 0, n = a.Length; i < n; i++) {
219 value = NULL_VARYBY_VALUE;
224 value = NULL_VARYBY_VALUE;
236 /* custom string part */
238 if (cachedVary._varyByCustom != null) {
240 sb.Append(cachedVary._varyByCustom);
244 value = context.ApplicationInstance.GetVaryByCustomString(
245 context, cachedVary._varyByCustom);
247 value = NULL_VARYBY_VALUE;
250 catch (Exception e) {
251 value = ERROR_VARYBY_VALUE;
252 HttpApplicationFactory.RaiseError(e);
259 * if VaryByParms=*, and method is not a form, then
260 * use a cryptographically strong hash of the data as
264 if ( verb == HttpVerb.POST &&
265 cachedVary._varyByAllParams &&
266 request.Form.Count == 0) {
268 contentLength = request.ContentLength;
269 if (contentLength > MAX_POST_KEY_LENGTH || contentLength < 0) {
273 if (contentLength > 0) {
274 buf = ((HttpInputStream)request.InputStream).GetAsByteArray();
279 // Use SHA256 to generate a collision-free hash of the input data
280 value = Convert.ToBase64String(CryptoUtil.ComputeSHA256Hash(buf));
286 * VaryByContentEncoding
289 string[] contentEncodings = cachedVary._contentEncodings;
290 if (contentEncodings != null) {
291 string coding = context.Response.GetHttpHeaderContentEncoding();
292 if (coding != null) {
293 for (int k = 0; k < contentEncodings.Length; k++) {
294 if (contentEncodings[k] == coding) {
302 // The key must end in "E", or the VaryByContentEncoding feature will break. Unfortunately,
303 // there was no good way to encapsulate the logic within this routine. See the code in
304 // OnEnter where we append the result of GetAcceptableEncoding to the key.
307 return sb.ToString();
311 * Return a key to lookup a cached response. The key contains
312 * the path and optionally, vary parameters, vary headers, custom strings,
313 * and form posted data.
315 string CreateOutputCachedItemKey(HttpContext context, CachedVary cachedVary) {
316 return CreateOutputCachedItemKey(context.Request.Path, context.Request.HttpVerb, context, cachedVary);
320 * GetAcceptableEncoding finds an acceptable coding for the given
321 * Accept-Encoding header (see RFC 2616)
322 * returns either i) an acceptable index in contentEncodings, ii) -1 if the identity is acceptable, or iii) -2 if nothing is acceptable
324 static int GetAcceptableEncoding(string[] contentEncodings, int startIndex, string acceptEncoding) {
325 // The format of Accept-Encoding is ( 1#( codings [ ";" "q" "=" qvalue ] ) | "*" )
326 if (String.IsNullOrEmpty(acceptEncoding)) {
327 return -1; // use "identity"
330 // is there only one token?
331 int tokenEnd = acceptEncoding.IndexOf(',');
332 if (tokenEnd == -1) {
333 string acceptEncodingWithoutWeight = acceptEncoding;
334 // WOS 1984913: is there a weight?
335 tokenEnd = acceptEncoding.IndexOf(';');
338 int space = acceptEncoding.IndexOf(' ');
339 if (space > -1 && space < tokenEnd) {
342 acceptEncodingWithoutWeight = acceptEncoding.Substring(0, tokenEnd);
343 if (ParseWeight(acceptEncoding, tokenEnd) == 0) {
344 // WOS 1985352 & WOS 1985353: weight is 0, use "identity" only if it is acceptable
345 bool identityIsAcceptable = acceptEncodingWithoutWeight != IDENTITY && acceptEncodingWithoutWeight != ASTERISK;
346 return (identityIsAcceptable) ? -1 : -2;
349 // WOS 1985353: is this the special "*" symbol?
350 if (acceptEncodingWithoutWeight == ASTERISK) {
351 // just return the index of the first entry in the list, since it is acceptable
354 for (int i = startIndex; i < contentEncodings.Length; i++) {
355 if (StringUtil.EqualsIgnoreCase(contentEncodings[i], acceptEncodingWithoutWeight)) {
359 return -1; // not found, use "identity"
362 // there are multiple tokens
363 int bestCodingIndex = -1;
364 double bestCodingWeight = 0;
365 for (int i = startIndex; i < contentEncodings.Length; i++) {
366 string coding = contentEncodings[i];
367 // get weight of current coding
368 double weight = GetAcceptableEncodingHelper(coding, acceptEncoding);
369 // if it is 1, use it
373 // if it is the best so far, remember it
374 if (weight > bestCodingWeight) {
376 bestCodingWeight = weight;
379 // WOS 1985352: use "identity" only if it is acceptable
380 if (bestCodingIndex == -1 && !IsIdentityAcceptable(acceptEncoding)) {
381 bestCodingIndex = -2;
383 return bestCodingIndex; // coding index with highest weight, possibly -1 or -2
386 // Get the weight of the specified coding from the Accept-Encoding header.
387 // 1 means use this coding. 0 means don't use this coding. A number between
388 // 1 and 0 must be compared with other codings. -1 means the coding was not found
389 static double GetAcceptableEncodingHelper(string coding, string acceptEncoding) {
391 int startSearchIndex = 0;
392 int codingLength = coding.Length;
393 int acceptEncodingLength = acceptEncoding.Length;
394 int maxSearchIndex = acceptEncodingLength - codingLength;
395 while (startSearchIndex < maxSearchIndex) {
396 int indexStart = acceptEncoding.IndexOf(coding, startSearchIndex, StringComparison.OrdinalIgnoreCase);
398 if (indexStart == -1) {
402 // if index is in middle of string, previous char should be ' ' or ','
403 if (indexStart != 0) {
404 char previousChar = acceptEncoding[indexStart-1];
405 if (previousChar != ' ' && previousChar != ',') {
406 startSearchIndex = indexStart + 1;
407 continue; // move index forward and continue searching
411 // the match starts on a token boundary, but it must also end
412 // on a token boundary ...
414 int indexNextChar = indexStart + codingLength;
415 char nextChar = '\0';
416 if (indexNextChar < acceptEncodingLength) {
417 nextChar = acceptEncoding[indexNextChar];
418 while (nextChar == ' ' && ++indexNextChar < acceptEncodingLength) {
419 nextChar = acceptEncoding[indexNextChar];
421 if (nextChar != ' ' && nextChar != ',' && nextChar != ';') {
422 startSearchIndex = indexStart + 1;
423 continue; // move index forward and continue searching
426 weight = (nextChar == ';') ? ParseWeight(acceptEncoding, indexNextChar) : 1;
432 // Gets the weight of the encoding beginning at startIndex.
433 // If Accept-Encoding header is formatted incorrectly, return 1 to short-circuit search.
434 static double ParseWeight(string acceptEncoding, int startIndex) {
436 int tokenEnd = acceptEncoding.IndexOf(',', startIndex);
437 if (tokenEnd == -1) {
438 tokenEnd = acceptEncoding.Length;
440 int qIndex = acceptEncoding.IndexOf('q', startIndex);
441 if (qIndex > -1 && qIndex < tokenEnd) {
442 int equalsIndex = acceptEncoding.IndexOf('=', qIndex);
443 if (equalsIndex > -1 && equalsIndex < tokenEnd) {
444 string s = acceptEncoding.Substring(equalsIndex+1, tokenEnd - (equalsIndex + 1));
446 if (Double.TryParse(s, NumberStyles.Float & ~NumberStyles.AllowLeadingSign & ~NumberStyles.AllowExponent, CultureInfo.InvariantCulture, out d)) {
447 weight = (d >= 0 && d <= 1) ? d : 1; // if format is invalid, short-circut search by returning weight of 1
454 static bool IsIdentityAcceptable(string acceptEncoding) {
456 double identityWeight = GetAcceptableEncodingHelper(IDENTITY, acceptEncoding);
457 if (identityWeight == 0
458 || (identityWeight <= 0 && GetAcceptableEncodingHelper(ASTERISK, acceptEncoding) == 0)) {
464 static bool IsAcceptableEncoding(string contentEncoding, string acceptEncoding) {
465 if (String.IsNullOrEmpty(contentEncoding)) {
466 // if Content-Encoding is not set treat it as the identity
467 contentEncoding = IDENTITY;
469 if (String.IsNullOrEmpty(acceptEncoding)) {
470 // only the identity is acceptable if Accept-Encoding is not set
471 return (contentEncoding == IDENTITY);
473 double weight = GetAcceptableEncodingHelper(contentEncoding, acceptEncoding);
475 || (weight <= 0 && GetAcceptableEncodingHelper(ASTERISK, acceptEncoding) == 0)) {
482 * Record a cache miss to the perf counters.
484 void RecordCacheMiss() {
485 if (!_recordedCacheMiss) {
486 PerfCounters.IncrementCounter(AppPerfCounter.OUTPUT_CACHE_RATIO_BASE);
487 PerfCounters.IncrementCounter(AppPerfCounter.OUTPUT_CACHE_MISSES);
488 _recordedCacheMiss = true;
494 /// <para>Initializes the output cache for an application.</para>
496 void IHttpModule.Init(HttpApplication app) {
497 OutputCacheSection cacheConfig = RuntimeConfig.GetAppConfig().OutputCache;
498 if (cacheConfig.EnableOutputCache) {
499 app.ResolveRequestCache += new EventHandler(this.OnEnter);
500 app.UpdateRequestCache += new EventHandler(this.OnLeave);
506 /// <para>Disposes of items from the output cache.</para>
508 void IHttpModule.Dispose() {
512 * Try to find this request in the cache. If so, return it. Otherwise,
513 * store the cache key for use on Leave.
517 /// <para>Raises the <see langword='Enter'/>
518 /// event, which searches the output cache for an item to satisfy the HTTP request. </para>
520 internal void OnEnter(Object source, EventArgs eventArgs) {
521 Debug.Trace("OutputCacheModuleEnter", "Beginning OutputCacheModule::Enter");
523 _recordedCacheMiss = false;
525 if (!OutputCache.InUse) {
526 Debug.Trace("OutputCacheModuleEnter", "Miss, no entries in output Cache" +
527 "\nReturning from OutputCacheModule::Enter");
535 HttpResponse response;
537 CachedRawResponse cachedRawResponse;
538 HttpCachePolicySettings settings;
541 HttpValidationStatus validationStatus, validationStatusFinal;
542 ValidationCallbackInfo callbackInfo;
543 string ifModifiedSinceHeader;
544 DateTime utcIfModifiedSince;
549 string[] cacheDirectives = null;
551 string[] pragmaDirectives = null;
557 bool hasValidationPolicy;
558 CachedVary cachedVary;
559 HttpRawResponse rawResponse;
560 CachedPathData cachedPathData;
562 app = (HttpApplication)source;
563 context = app.Context;
564 cachedPathData = context.GetFilePathData();
565 request = context.Request;
566 response = context.Response;
569 * Check if the request can be resolved for this method.
571 switch (request.HttpVerb) {
578 Debug.Trace("OutputCacheModuleEnter", "Miss, Http method not GET, POST, or HEAD" +
579 "\nReturning from OutputCacheModule::Enter");
585 * Create a lookup key. Remember the key for use inside Leave()
587 _key = key = CreateOutputCachedItemKey(context, null);
588 Debug.Assert(_key != null, "_key != null");
591 * Lookup the cache vary for this key.
593 item = OutputCache.Get(key);
595 Debug.Trace("OutputCacheModuleEnter", "Miss, item not found.\n\tkey=" + key +
596 "\nReturning from OutputCacheModule::Enter");
600 // 'item' may be one of the following:
601 // - a CachedVary object (if the object varies by something)
602 // - a "no vary" CachedRawResponse object (i.e. it doesn't vary on anything)
604 // Let's assume it's a CacheVary and see what happens.
605 cachedVary = item as CachedVary;
607 // If we have one, create a new cache key for it (this is a must)
608 if (cachedVary != null) {
610 * This cached output has a Vary policy. Create a new key based
611 * on the vary headers in cachedRawResponse and try again.
613 * Skip this step if it's a VaryByNone vary policy.
617 key = CreateOutputCachedItemKey(context, cachedVary);
619 Debug.Trace("OutputCacheModuleEnter", "Miss, key could not be created for vary-by item." +
620 "\nReturning from OutputCacheModule::Enter");
625 if (cachedVary._contentEncodings == null) {
626 // With the new key, look up the in-memory key.
627 // At this point, we've exhausted the lookups in memory for this item.
628 item = OutputCache.Get(key);
632 Debug.Assert(key[key.Length-1] == 'E', "key[key.Length-1] == 'E'");
635 bool identityIsAcceptable = true;
636 string acceptEncoding = context.WorkerRequest.GetKnownRequestHeader(HttpWorkerRequest.HeaderAcceptEncoding);
637 if (acceptEncoding != null) {
638 string[] contentEncodings = cachedVary._contentEncodings;
643 int index = GetAcceptableEncoding(contentEncodings, startIndex, acceptEncoding);
646 Debug.Trace("OutputCacheModuleEnter", "VaryByContentEncoding key=" + key + contentEncodings[index]);
648 identityIsAcceptable = false; // the client Accept-Encoding header contains an encoding that's in the VaryByContentEncoding list
649 item = OutputCache.Get(key + contentEncodings[index]);
651 startIndex = index+1;
652 if (startIndex < contentEncodings.Length) {
657 else if (index == -2) {
658 // the identity has a weight of 0 and is not acceptable
659 identityIsAcceptable = false;
664 // the identity should not be used if the client Accept-Encoding contains an entry in the VaryByContentEncoding list or "identity" is not acceptable
665 if (item == null && identityIsAcceptable) {
667 Debug.Trace("OutputCacheModuleEnter", "VaryByContentEncoding key=" + key);
669 item = OutputCache.Get(key);
673 Debug.Assert(item == null || item is CachedRawResponse, "item == null || item is CachedRawResponse");
674 if (item == null || ((CachedRawResponse)item)._cachedVaryId != cachedVary.CachedVaryId) {
677 Debug.Trace("OutputCacheModuleEnter", "Miss, cVary found, cRawResponse not found.\n\t\tkey=" + key +
678 "\nReturning from OutputCacheModule::Enter");
681 string msg = "Miss, _cachedVaryId=" + ((CachedRawResponse)item)._cachedVaryId.ToString() + ", cVary.CachedVaryId=" + cachedVary.CachedVaryId.ToString();
682 Debug.Trace("OutputCacheModuleEnter", msg + key +
683 "\nReturning from OutputCacheModule::Enter");
687 // explicitly remove entry because _cachedVaryId does not match
688 OutputCache.Remove(key, context);
694 // From this point on, we have an entry to work with.
696 Debug.Assert(item is CachedRawResponse, "item is CachedRawResponse");
697 cachedRawResponse = (CachedRawResponse) item;
698 settings = cachedRawResponse._settings;
699 if (cachedVary == null && !settings.IgnoreParams) {
701 * This cached output has no vary policy, so make sure it doesn't have a query string or form post.
703 if (request.HttpVerb == HttpVerb.POST) {
704 Debug.Trace("OutputCacheModuleEnter", "Output cache item found but method is POST and no VaryByParam specified." +
706 "\nReturning from OutputCacheModule::Enter");
711 if (request.HasQueryString) {
712 Debug.Trace("OutputCacheModuleEnter", "Output cache item found but contains a querystring and no VaryByParam specified." +
714 "\nReturning from OutputCacheModule::Enter");
720 if (settings.IgnoreRangeRequests) {
721 string rangeHeader = request.Headers["Range"];
722 if (StringUtil.StringStartsWithIgnoreCase(rangeHeader, "bytes")) {
723 Debug.Trace("OutputCacheModuleEnter", "Output cache item found but this is a Range request and IgnoreRangeRequests is true." +
725 "\nReturning from OutputCacheModule::Enter");
726 // Don't record this as a cache miss. The response for a range request is not cached, and so
727 // we don't want to pollute the cache hit/miss ratio.
732 hasValidationPolicy = settings.HasValidationPolicy();
735 * Determine whether the client can accept a cached copy, and
736 * get values of other cache control directives.
738 * We do this after lookup so we don't have to break down the headers
739 * if the item is not found. Cracking the headers is expensive.
741 if (!hasValidationPolicy) {
742 cacheControl = request.Headers["Cache-Control"];
743 if (cacheControl != null) {
744 cacheDirectives = cacheControl.Split(s_fieldSeparators);
745 for (i = 0; i < cacheDirectives.Length; i++) {
746 directive = cacheDirectives[i];
747 if (directive == "no-cache" || directive == "no-store") {
748 Debug.Trace("OutputCacheModuleEnter",
749 "Skipping lookup because of Cache-Control: no-cache or no-store directive." +
750 "\nReturning from OutputCacheModule::Enter");
756 if (StringUtil.StringStartsWith(directive, "max-age=")) {
758 maxage = Convert.ToInt32(directive.Substring(8), CultureInfo.InvariantCulture);
765 age = (int) ((context.UtcTimestamp.Ticks - settings.UtcTimestampCreated.Ticks) / TimeSpan.TicksPerSecond);
767 Debug.Trace("OutputCacheModuleEnter",
768 "Not returning found item due to Cache-Control: max-age directive." +
769 "\nReturning from OutputCacheModule::Enter");
776 else if (StringUtil.StringStartsWith(directive, "min-fresh=")) {
778 minfresh = Convert.ToInt32(directive.Substring(10), CultureInfo.InvariantCulture);
784 if (minfresh >= 0 && settings.IsExpiresSet && !settings.SlidingExpiration) {
785 fresh = (int) ((settings.UtcExpires.Ticks - context.UtcTimestamp.Ticks) / TimeSpan.TicksPerSecond);
786 if (fresh < minfresh) {
787 Debug.Trace("OutputCacheModuleEnter",
788 "Not returning found item due to Cache-Control: min-fresh directive." +
789 "\nReturning from OutputCacheModule::Enter");
799 pragma = request.Headers["Pragma"];
800 if (pragma != null) {
801 pragmaDirectives = pragma.Split(s_fieldSeparators);
802 for (i = 0; i < pragmaDirectives.Length; i++) {
803 if (pragmaDirectives[i] == "no-cache") {
804 Debug.Trace("OutputCacheModuleEnter",
805 "Skipping lookup because of Pragma: no-cache directive." +
806 "\nReturning from OutputCacheModule::Enter");
814 else if (settings.ValidationCallbackInfo != null) {
816 * Check if the item is still valid.
818 validationStatus = HttpValidationStatus.Valid;
819 validationStatusFinal = validationStatus;
820 for (i = 0, n = settings.ValidationCallbackInfo.Length; i < n; i++) {
821 callbackInfo = settings.ValidationCallbackInfo[i];
823 callbackInfo.handler(context, callbackInfo.data, ref validationStatus);
825 catch (Exception e) {
826 validationStatus = HttpValidationStatus.Invalid;
827 HttpApplicationFactory.RaiseError(e);
830 switch (validationStatus) {
831 case HttpValidationStatus.Invalid:
832 Debug.Trace("OutputCacheModuleEnter", "Output cache item found but callback invalidated it." +
834 "\nReturning from OutputCacheModule::Enter");
836 OutputCache.Remove(key, context);
840 case HttpValidationStatus.IgnoreThisRequest:
841 validationStatusFinal = HttpValidationStatus.IgnoreThisRequest;
844 case HttpValidationStatus.Valid:
848 Debug.Trace("OutputCacheModuleEnter", "Invalid validation status, ignoring it, status=" + validationStatus +
851 validationStatus = validationStatusFinal;
857 if (validationStatusFinal == HttpValidationStatus.IgnoreThisRequest) {
858 Debug.Trace("OutputCacheModuleEnter", "Output cache item found but callback status is IgnoreThisRequest." +
860 "\nReturning from OutputCacheModule::Enter");
867 Debug.Assert(validationStatusFinal == HttpValidationStatus.Valid,
868 "validationStatusFinal == HttpValidationStatus.Valid");
871 rawResponse = cachedRawResponse._rawResponse;
873 // WOS 1985154 ensure Content-Encoding is acceptable
874 if (cachedVary == null || cachedVary._contentEncodings == null) {
875 string acceptEncoding = request.Headers["Accept-Encoding"];
876 string contentEncoding = null;
877 ArrayList headers = rawResponse.Headers;
878 if (headers != null) {
879 foreach (HttpResponseHeader h in headers) {
880 if (h.Name == "Content-Encoding") {
881 contentEncoding = h.Value;
886 if (!IsAcceptableEncoding(contentEncoding, acceptEncoding)) {
894 * Try to satisfy a conditional request. The cached response
895 * must satisfy all conditions that are present.
897 * We can only satisfy a conditional request if the response
898 * is buffered and has no substitution blocks.
900 * N.B. RFC 2616 says conditional requests only occur
901 * with the GET method, but we try to satisfy other
902 * verbs (HEAD, POST) as well.
906 if (!rawResponse.HasSubstBlocks) {
907 /* Check "If-Modified-Since" header */
908 ifModifiedSinceHeader = request.IfModifiedSince;
909 if (ifModifiedSinceHeader != null) {
912 utcIfModifiedSince = HttpDate.UtcParse(ifModifiedSinceHeader);
913 if ( settings.IsLastModifiedSet &&
914 settings.UtcLastModified <= utcIfModifiedSince &&
915 utcIfModifiedSince <= context.UtcTimestamp) {
921 Debug.Trace("OutputCacheModuleEnter", "Ignore If-Modified-Since header, invalid format: " + ifModifiedSinceHeader);
925 /* Check "If-None-Match" header */
927 etag = request.IfNoneMatch;
930 etags = etag.Split(s_fieldSeparators);
931 for (i = 0, n = etags.Length; i < n; i++) {
932 if (i == 0 && etags[i].Equals(ASTERISK)) {
937 if (etags[i].Equals(settings.ETag)) {
948 * Send 304 Not Modified
950 Debug.Trace("OutputCacheModuleEnter", "Hit, conditional request satisfied, status=304." +
952 "\nReturning from OutputCacheModule::Enter");
955 response.StatusCode = 304;
959 * Send the full response.
963 Debug.Trace("OutputCacheModuleEnter", "Hit.\n\tkey=" + key +
964 "\nReturning from OutputCacheModule::Enter");
968 Debug.Trace("OutputCacheModuleEnter", "Hit, but conditional request not satisfied.\n\tkey=" + key +
969 "\nReturning from OutputCacheModule::Enter");
973 sendBody = (request.HttpVerb != HttpVerb.HEAD);
975 // Check and see if the cachedRawResponse is from the disk
976 // If so, we must clone the HttpRawResponse before sending it
978 // UseSnapshot calls ClearAll
979 response.UseSnapshot(rawResponse, sendBody);
982 response.Cache.ResetFromHttpCachePolicySettings(settings, context.UtcTimestamp);
984 // re-insert entry in kernel cache if necessary
985 string originalCacheUrl = cachedRawResponse._kernelCacheUrl;
986 if (originalCacheUrl != null) {
987 response.SetupKernelCaching(originalCacheUrl);
990 PerfCounters.IncrementCounter(AppPerfCounter.OUTPUT_CACHE_RATIO_BASE);
991 PerfCounters.IncrementCounter(AppPerfCounter.OUTPUT_CACHE_HITS);
993 _recordedCacheMiss = false;
995 app.CompleteRequest();
1000 * If the item is cacheable, add it to the cache.
1004 /// <para>Raises the <see langword='Leave'/> event, which causes any cacheable items to
1005 /// be put into the output cache.</para>
1007 internal /*public*/ void OnLeave(Object source, EventArgs eventArgs) {
1008 HttpApplication app;
1009 HttpContext context;
1011 CachedVary cachedVary;
1012 HttpCachePolicy cache;
1013 HttpCachePolicySettings settings;
1014 string keyRawResponse;
1015 string[] varyByContentEncodings;
1016 string[] varyByHeaders;
1017 string[] varyByParams;
1018 bool varyByAllParams;
1019 HttpRequest request;
1020 HttpResponse response;
1022 bool cacheAuthorizedPage;
1024 Debug.Trace("OutputCacheModuleLeave", "Beginning OutputCacheModule::Leave");
1026 app = (HttpApplication)source;
1027 context = app.Context;
1028 request = context.Request;
1029 response = context.Response;
1033 string reason = null;
1036 * Determine whether the response is cacheable.
1040 if (!response.HasCachePolicy) {
1042 reason = "CachePolicy not created, not modified from non-caching default.";
1047 cache = response.Cache;
1048 if (!cache.IsModified()) {
1050 reason = "CachePolicy created, but not modified from non-caching default.";
1055 if (response.StatusCode != 200) {
1057 reason = "response.StatusCode != 200.";
1062 if (request.HttpVerb != HttpVerb.GET && request.HttpVerb != HttpVerb.POST) {
1064 reason = "the cache can only cache responses to GET and POST.";
1069 if (!response.IsBuffered()) {
1071 reason = "the response is not buffered.";
1077 * Change a response with HttpCacheability.Public to HttpCacheability.Private
1078 * if it requires authorization, and allow it to be cached.
1080 * Note that setting Cacheability to ServerAndPrivate would accomplish
1081 * the same thing without needing the "cacheAuthorizedPage" variable,
1082 * but in RTM we did not have ServerAndPrivate, and setting that value
1083 * would change the behavior.
1085 cacheAuthorizedPage = false;
1086 if ( cache.GetCacheability() == HttpCacheability.Public &&
1087 context.RequestRequiresAuthorization()) {
1089 cache.SetCacheability(HttpCacheability.Private);
1090 cacheAuthorizedPage = true;
1093 if ( cache.GetCacheability() != HttpCacheability.Public &&
1094 cache.GetCacheability() != HttpCacheability.ServerAndPrivate &&
1095 cache.GetCacheability() != HttpCacheability.ServerAndNoCache &&
1096 !cacheAuthorizedPage) {
1098 reason = "CachePolicy.Cacheability is not Public, ServerAndPrivate, or ServerAndNoCache.";
1103 if (cache.GetNoServerCaching()) {
1105 reason = "CachePolicy.NoServerCaching is set.";
1110 // MSRC 11855 (DevDiv 297240 / 362405) - We should suppress output caching for responses which contain non-shareable cookies.
1111 // We already disable the HTTP.SYS and IIS user mode cache when *any* response cookie is present (see IIS7WorkerRequest.SendUnknownResponseHeader)
1112 if (response.ContainsNonShareableCookies()) {
1114 reason = "Non-shareable response cookies were present.";
1119 if (!cache.HasExpirationPolicy() && !cache.HasValidationPolicy()) {
1121 reason = "CachePolicy has no expiration policy or validation policy.";
1126 if (cache.VaryByHeaders.GetVaryByUnspecifiedParameters()) {
1128 reason = "CachePolicy.Vary.VaryByUnspecifiedParameters was called.";
1133 if (!cache.VaryByParams.AcceptsParams() && (request.HttpVerb == HttpVerb.POST || request.HasQueryString)) {
1135 reason = "the cache cannot cache responses to POSTs or GETs with query strings unless Cache.VaryByParams is modified.";
1140 if (cache.VaryByContentEncodings.IsModified() && !cache.VaryByContentEncodings.IsCacheableEncoding(context.Response.GetHttpHeaderContentEncoding())) {
1142 reason = "the cache cannot cache encoded responses that are not listed in the VaryByContentEncodings collection.";
1151 * Add response to cache.
1155 Debug.Assert(reason != null, "reason != null");
1156 Debug.Trace("OutputCacheModuleLeave", "Item is not output cacheable because " + reason +
1157 "\n\tUrl=" + request.Path +
1158 "\nReturning from OutputCacheModule::Leave");
1166 settings = cache.GetCurrentSettings(response);
1168 varyByContentEncodings = settings.VaryByContentEncodings;
1170 varyByHeaders = settings.VaryByHeaders;
1171 if (settings.IgnoreParams) {
1172 varyByParams = null;
1175 varyByParams = settings.VaryByParams;
1179 /* Create the key if it was not created in OnEnter */
1181 _key = CreateOutputCachedItemKey(context, null);
1182 Debug.Assert(_key != null, "_key != null");
1186 if (varyByContentEncodings == null && varyByHeaders == null && varyByParams == null && settings.VaryByCustom == null) {
1188 * This is not a varyBy item.
1190 keyRawResponse = _key;
1195 * There is a vary in the cache policy. We handle this
1196 * by adding another item to the cache which contains
1197 * a list of the vary headers. A request for the item
1198 * without the vary headers in the key will return this
1199 * item. From the headers another key can be constructed
1200 * to lookup the item with the raw response.
1202 if (varyByHeaders != null) {
1203 for (i = 0, n = varyByHeaders.Length; i < n; i++) {
1204 varyByHeaders[i] = "HTTP_" + CultureInfo.InvariantCulture.TextInfo.ToUpper(
1205 varyByHeaders[i].Replace('-', '_'));
1209 varyByAllParams = false;
1210 if (varyByParams != null) {
1211 varyByAllParams = (varyByParams.Length == 1 && varyByParams[0] == ASTERISK);
1212 if (varyByAllParams) {
1213 varyByParams = null;
1216 for (i = 0, n = varyByParams.Length; i < n; i++) {
1217 varyByParams[i] = CultureInfo.InvariantCulture.TextInfo.ToLower(varyByParams[i]);
1222 cachedVary = new CachedVary(varyByContentEncodings, varyByHeaders, varyByParams, varyByAllParams, settings.VaryByCustom);
1223 keyRawResponse = CreateOutputCachedItemKey(context, cachedVary);
1224 if (keyRawResponse == null) {
1225 Debug.Trace("OutputCacheModuleLeave", "Couldn't add non-cacheable post.\n\tkey=" + _key);
1229 // it is possible that the user code calculating custom vary-by
1230 // string would Flush making the response non-cacheable. Check fo it here.
1231 if (!response.IsBuffered()) {
1232 Debug.Trace("OutputCacheModuleLeave", "Response.Flush() inside GetVaryByCustomString\n\tkey=" + _key);
1237 DateTime utcExpires = Cache.NoAbsoluteExpiration;
1238 TimeSpan slidingDelta = Cache.NoSlidingExpiration;
1240 if (settings.SlidingExpiration) {
1241 slidingDelta = settings.SlidingDelta;
1243 else if (settings.IsMaxAgeSet) {
1244 DateTime utcTimestamp = (settings.UtcTimestampCreated != DateTime.MinValue) ? settings.UtcTimestampCreated : context.UtcTimestamp;
1245 utcExpires = utcTimestamp + settings.MaxAge;
1247 else if (settings.IsExpiresSet) {
1248 utcExpires = settings.UtcExpires;
1251 // Check and ensure that item hasn't expired:
1252 if (utcExpires > DateTime.UtcNow) {
1254 // Create the response object to be sent on cache hits.
1255 HttpRawResponse httpRawResponse = response.GetSnapshot();
1256 string kernelCacheUrl = response.SetupKernelCaching(null);
1257 Guid cachedVaryId = (cachedVary != null) ? cachedVary.CachedVaryId : Guid.Empty;
1258 CachedRawResponse cachedRawResponse = new CachedRawResponse(httpRawResponse, settings, kernelCacheUrl, cachedVaryId);
1260 Debug.Trace("OutputCacheModuleLeave", "Adding response to cache.\n\tkey=" + keyRawResponse);
1262 CacheDependency dep = response.CreateCacheDependencyForResponse();
1264 OutputCache.InsertResponse(_key, cachedVary,
1265 keyRawResponse, cachedRawResponse,
1267 utcExpires, slidingDelta);
1279 Debug.Trace("OutputCacheModuleLeave", "Returning from OutputCacheModule::Leave");