GZIP Compression causing web page expiration - asp.net

I have implemented GZIP compression on a few of my ASP.NET pages, using a class that inherits from System.Web.UI.Page, and implementing the OnLoad method to do the compression, like so:
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
if (Internet.Browser.IsGZIPSupported())
{
base.Response.Filter = new GZipStream(base.Response.Filter, CompressionMode.Compress, true);
base.Response.AppendHeader("Content-encoding", "gzip");
base.Response.AppendHeader("Vary", "Content-encoding");
}
else if (Internet.Browser.IsDeflateSupported())
{
base.Response.Filter = new DeflateStream(base.Response.Filter, CompressionMode.Compress, true);
base.Response.AppendHeader("Content-encoding", "deflate");
base.Response.AppendHeader("Vary", "Content-encoding");
}
}
The IsGZIPSupported method just determines whether the browser supports GZIP, looking at the Accept-encoding request header, and the browser's user agent (IE5-6 are excluded from GZIP compression). However, with this code, I am getting the web page has expired message in IE, when I postback from the page and try to use the back button. Setting the cache control to private seems to fix the problem:
base.Response.Cache.SetCacheability(HttpCacheability.Private);
But I am not sure why, or whether this will cause other problems. I haven't set any caching for any other pages in the site, and the site is running on an intranet with only a dozen concurrent users, so performance isn't a big issue at the moment.

See this article on Vary header and WinInet/MSIE
It seems you should be sending Vary: Accept-Encoding instead of Vary: Content-Encoding, as the response will vary depending on the request header.

Related

How do I get IIS to return a Custom-Cache Control Header from WebApi 2, IIS 10 returns private every time

** UPDATE Jan 27, 2021 **
I have been working with Microsoft on this issue and we did a request trace and reviewed the FREB log and confirmed that the code is outputting the correct headers but in the END_REQUEST handler they are replaced with cache-control private. After building several virtual machines from scratch we learned that out of the box, this problem doesn't happen. However when we install the latest version of STACKIFY AGENT (4.29.29) once we put the agent on this behavior happens. Older versions of Stackify agent (RETRACE profiler and APM) don't do this, but so far, this is irreversible: once we install 4.29.29 of Stackify, uninstalling or installing older versions doesn't undo this issue, the environment is somehow permanently modified.
STACKIFY responded with a solution which works (but suggests something is left behind after uninstall): Set the environment variable STACKIFY_ENABLERUM = false .. we tried this and IIS returned our correct header without replacing it with cache-control: private.
I want to use CloudFront CDN to cache traffic to my API, so that requests are offloaded to the content delivery network.
I'm trying to set the Cache-Control header to return: Cache-Control: "public, max-age=10". When I run my code in Visual Studio 2019 to debug it, I get the correct header. However, when I deploy it to IIS, I always get back:
Cache-Control: private
I am running Windows Server 2019 with IIS version 10.0.17763.1. I am using ASP.NET MVC with Web API 2 to operate a REST API.
In my code I created this attribute:
Code:
public class CacheControlAttribute : System.Web.Http.Filters.ActionFilterAttribute
{
public int MaxAge { get; set; }
public CacheControlAttribute()
{
MaxAge = 0;
}
public override void OnActionExecuted(HttpActionExecutedContext context)
{
// Don't set the cache header if there was an error or if there was no content in the response
if (context.Response != null && context.Response.IsSuccessStatusCode)
{
context.Response.Headers.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue
{
Private = false,
Public = true,
MaxAge = TimeSpan.FromSeconds(MaxAge)
};
}
base.OnActionExecuted(context);
}
}
I then decorate my method with this attribute:
[CacheControl(MaxAge = 10)]
I read several articles on StackOverflow but was not able to solve this issue. I also tried adding this to web.config:
It did not help.
I have this working successfully on one IIS server, same version of IIS but older version of my code. I'm having the problem setting it up on a new IIS server and I think its an IIS configuration issue but I am not certain. Maybe its something in my web.config.
Thank you for any help with this.
Cache-control is divided into request and response directives. I guess you are talking about the cache-control in the response, because the request cache-control cannot be set in IIS.
For security reasons, IIS will set cache-control to private by default, so you will encounter this problem. This is default behaviour for .NET when there's no output cache used for a request (and you have output cache enabled). If you set the sendCacheControlHeader to false in web.config, you will not get the Cache-Control: private header.
So an easy way is set sendCacheControlHeader false and add cache-control will remove private. You also don't need to custom cache-control filter in MVC.
<system.web>
<httpRuntime sendCacheControlHeader="false" />
</system.web>
<httpProtocol>
<customHeaders>
<remove name="X-Powered-By" />
<add name="Cache-Control" value="max-age=30,public" />
</customHeaders>
</httpProtocol>
Another idea is to add cached output for each action or controller. .NET has a defined filter to control maxage, you don't need to define it yourself. You can use [OutputCache(Duration = 0)]
OutputCache
You need to implement your own on top of the existing output cache like so
public static class MyCustomCacheConfig
{
public static int Duration = 600; //whatever time you want
}
public class CdnCacheCacheAttribute : OutputCacheAttribute
{
public CdnCacheCacheAttribute()
{
this.Duration = MyCustomCacheConfig.Duration;
}
}
[CdnCacheCacheAttribute(Duration = 100, VaryByParam = "none")]
public ActionResult YourActions()
{
return View();
}
VaryByParm in output cache from Microsoft.. which controls the HttpCaching
// Get the current VaryByParam.
String varyByParamValue = outputCacheProfile.VaryByParam;
// Set the VaryByParam.outputCacheProfile.VaryByParam = string.Empty;

Web API as a Proxy and Chunked Transfer Encoding

I have been playing around with using Web API (Web Host) as a proxy server and have run into an issue with how my Web API proxy handles responses with the "Transfer-Encoding: chunked" header.
When bypassing the proxy, the remote resource sends the following response headers:
Cache-Control:no-cache
Content-Encoding:gzip
Content-Type:text/html
Date:Fri, 24 May 2013 12:42:27 GMT
Expires:-1
Pragma:no-cache
Server:Microsoft-IIS/8.0
Transfer-Encoding:chunked
Vary:Accept-Encoding
X-AspNet-Version:4.0.30319
X-Powered-By:ASP.NET
When going through my Web API based proxy, my request hangs unless I explicitly reset the TransferEncodingChunked property on the response header to false:
response.Headers.TransferEncodingChunked = false;
I admit, I don't fully understand what impact setting the TransferEncodingChunked property has, but it seems strange to me that in order to make the proxy work as expected, I need to set this property to false when clearly the incoming response has a "Transfer-Encoding: chunked" header. I am also concerned about side effects to explicitly setting this property. Can anyone help me understand what is going on and why setting this property is required?
UPDATE: So I did a little more digging into the difference in the response when going through the proxy vs. not. Whether I explicitly set the TransferEncodingChunked property to false, the response headers when coming through the proxy are exactly the same as when not going through the proxy. However, the response content is different. Here are a few samples (I turned off gzip encoding):
// With TransferEncodingChunked = false
2d\r\n
This was sent with transfer-encoding: chunked\r\n
0\r\n
// Without explicitly setting TransferEncodingChunked
This was sent with transfer-encoding: chunked
Clearly, the content sent with TransferEncodingChunked set to false is in fact transfer encoded. This is actually the correct response as it is what was received from the requested resource behind the proxy. What continues to be strange is the second scenario in which I don't explicitly set TransferEncodingChunked on the response (but it is in the response header received from the proxied service). Clearly, in this case, the response is NOT in fact transfer encoded by IIS, in spite of the fact that the actual response is. Strange...this is starting to feel like designed behavior (in which case, I'd love to know how / why) or a bug in IIS, ASP.Net, or Web API.
Here is a simplified version of the code I am running:
Proxy Web API application:
// WebApiConfig.cs
config.Routes.MapHttpRoute(
name: "Proxy",
routeTemplate: "{*path}",
handler: HttpClientFactory.CreatePipeline(
innerHandler: new HttpClientHandler(), // Routes the request to an external resource
handlers: new DelegatingHandler[] { new ProxyHandler() }
),
defaults: new { path = RouteParameter.Optional },
constraints: null
);
// ProxyHandler.cs
public class ProxyHandler : DelegatingHandler
{
protected override async System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
{
// Route the request to my web application
var uri = new Uri("http://localhost:49591" + request.RequestUri.PathAndQuery);
request.RequestUri = uri;
// For GET requests, somewhere upstream, Web API creates an empty stream for the request.Content property
// HttpClientHandler doesn't like this for GET requests, so set it back to null before sending along the request
if (request.Method == HttpMethod.Get)
{
request.Content = null;
}
var response = await base.SendAsync(request, cancellationToken);
// If I comment this out, any response that already has the Transfer-Encoding: chunked header will hang in the browser
response.Headers.TransferEncodingChunked = false;
return response;
}
}
And my web application controller which creates a "chunked" response (also Web API):
public class ChunkedController : ApiController
{
public HttpResponseMessage Get()
{
var response = Request.CreateResponse(HttpStatusCode.OK);
var content = "This was sent with transfer-encoding: chunked";
var bytes = System.Text.Encoding.ASCII.GetBytes(content);
var stream = new MemoryStream(bytes);
response.Content = new ChunkedStreamContent(stream);
return response;
}
}
public class ChunkedStreamContent : StreamContent
{
public ChunkedStreamContent(Stream stream)
: base(stream) { }
protected override bool TryComputeLength(out long length)
{
length = 0L;
return false;
}
}
From an HttpClient standpoint, content chunking is essentially a detail of the transport. The content provided by response.Content is always de-chunked for you by HttpClient.
It looks like there's a bug in Web API that it doesn't correctly (re-)chunk content when requested by the response.Headers.TransferEncodingChunked property when running on IIS. So the problem is that the proxy is telling the client, via the headers, that the content is chunked when in fact it is not. I've filed the bug here:
https://aspnetwebstack.codeplex.com/workitem/1124
I think your workaround is the best option at the moment.
Also notice that you have multiple layers here that likely weren't designed/tested for proxying scenarios (and may not support it). On the HttpClient side, note that it will automatically decompress and follow redirects unless you turn that behavior off. At a minimum, you'll want to set these two properties:
http://msdn.microsoft.com/en-us/library/system.net.http.httpclienthandler.allowautoredirect.aspx
http://msdn.microsoft.com/en-us/library/system.net.http.httpclienthandler.automaticdecompression.aspx
On the WebApi/IIS side, you've found at least one bug, and it wouldn't be suprising to find others as well. Just be forewarned there may be bugs like this currently writing a proxy using these technologies outside their main design use cases.

Caching of Web Service not working when request has query string

I'm trying to implement the client-side caching of web service calls, and based on information from the web, I was able to do it according to the SetCachingPolicy() function as shown in code 1 below.
I was able to successfully get it working with a web method, RetrieveX, but not with method RetrieveY. I noticed that RetrieveX has no parameters and RetrieveY has one string parameter, and on inspection under Fiddler, the difference seems to be that the HTTP GET request of the web service call from RetrieveY has a query string for the parameter.
All HTTP GET web service calls so far without a query string is doing the caching properly, but not this call that has a query string in it.
Examination under Fiddler indicates that RetrieveX has the following caching information in output 1, and RetrieveY has the information in output 2.
Is this a limitation of this caching method or can I do something to get the client side caching of RetrieveY working?
Code 1: SetCachingPolicy
private void SetCachingPolicy()
{
HttpCachePolicy cache = HttpContext.Current.Response.Cache;
cache.SetCacheability(HttpCacheability.Private);
cache.SetExpires(DateTime.Now.AddSeconds((double)30));
FieldInfo maxAgeField = cache.GetType().GetField(
"_maxAge", BindingFlags.Instance | BindingFlags.NonPublic);
maxAgeField.SetValue(cache, new TimeSpan(0, 0, 30));
}
Code 2: RetrieveX
[System.Web.Services.WebMethod]
[System.Web.Script.Services.ScriptMethod(UseHttpGet = true)]
public string[] RetrieveX()
{
SetCachingPolicy();
// Implementation details here.
return array;
}
Code 3: RetrieveY
[System.Web.Services.WebMethod]
[System.Web.Script.Services.ScriptMethod(UseHttpGet = true)]
public string[] RetrieveY(string arg1)
{
SetCachingPolicy();
// Implementation details here.
return array;
}
Output 1: RetrieveX caching info
HTTP/200 responses are cacheable by default, unless Expires, Pragma, or Cache-Control headers are present and forbid caching.
HTTP/1.0 Expires Header is present: Wed, 12 Sep 2012 03:16:50 GMT
HTTP/1.1 Cache-Control Header is present: private, max-age=30
private: This response MUST NOT be cached by a shared cache.
max-age: This resource will expire in .5 minutes. [30 sec]
Output 2: RetrieveY caching info
HTTP/200 responses are cacheable by default, unless Expires, Pragma, or Cache-Control headers are present and forbid caching.
HTTP/1.0 Expires Header is present: -1
Legacy Pragma Header is present: no-cache
HTTP/1.1 Cache-Control Header is present: no-cache
no-cache: This response MUST NOT be reused without successful revalidation with the origin server.
I ran into this issue as well, I thought I'd share what worked for me. The underlying issue is that VaryByParams is not being set on the response. If you add this to your SetCachingPolicy() method RetrieveY should begin working as expected:
cache.VaryByParams["*"] = true

Compression is not working

I need to compress all dynamic content of my data export site.
I've tried numerous ways, nothing works. Chrome shows that content is not compressed and "Content-Encoding" header is not present.
Trying to do it like this as the last resort method (before writing any response):
context.Response.Filter = new DeflateStream(context.Response.Filter, CompressionMode.Compress);
context.Response.AppendHeader("Content-Encoding", "deflate");
Logging shows that this code is executed correctly.
However, Chrome shows that content is not compressed, again.
UPD when using IIS built-in compression, it seems to work and request tracing shows "DYNAMIC_COMPRESSION_SUCCESS". However, IE still shows that response is not compressed. The same when I'm requesting the page from the server itself using localhost name.
Any ideas?
If you want to do this manually first check the compression is supported,
public static bool IsGZipSupported()
{
string AcceptEncoding = HttpContext.Current.Request.Headers["Accept-Encoding"];
if (!string.IsNullOrEmpty(AcceptEncoding) &&
(AcceptEncoding.Contains("gzip") || AcceptEncoding.Contains("deflate")))
return true;
return false;
}
And compress your response,
public static void GZipEncodePage()
{
if (IsGZipSupported()) {
HttpResponse Response = HttpContext.Current.Response;
string AcceptEncoding = HttpContext.Current.Request.Headers("Accept-Encoding");
if (AcceptEncoding.Contains("gzip")) {
Response.Filter = new System.IO.Compression.GZipStream(Response.Filter, System.IO.Compression.CompressionMode.Compress);
Response.AppendHeader("Content-Encoding", "gzip");
} else {
Response.Filter = new System.IO.Compression.DeflateStream(Response.Filter, System.IO.Compression.CompressionMode.Compress);
Response.AppendHeader("Content-Encoding", "deflate");
}
You can check filter is attached just before the headers are sent to the client
protected void Application_PreSendRequestHeaders()
{
HttpResponse response = HttpContext.Current.Response;
if (response.Filter is GZipStream && response.Headers["Content-encoding"] != "gzip")
response.AppendHeader("Content-encoding", "gzip");
else if (response.Filter is DeflateStream && response.Headers["Content-encoding"] != "deflate")
response.AppendHeader("Content-encoding", "deflate");
}
For more information check this posts;
ASP.NET GZip Encoding Caveats
Built-in GZip/Deflate Compression on IIS 7.x
Benefits and Drawbacks of IIS 7 Compression
Instead of trying to do this manually I would rely on the pre-written (and tested) Microsoft code built into IIS that will do this for you:
Install Dynamic Content Compression on the machine (bullet 5 in the link) and enable it in IIS. IIS will now handle compression for on both static and dynamic content. Less code to maintain (and invariably have bugs) is always a good thing!
If you are using IIS7+, there's an Compression option. Navigate to your site, in the right main window, click "Compression", and check all 2 checkboxes:
Enable dynamic content compression
Enable static content compression

HTTP Compression: Some external scripts/CSS not decompressing properly some of the time

I am implementing page/resource compression to improve website performance.
I have tried to implement both blowery and wicked HttpCompress but end up getting the same result. This only seems to affect Firefox, I have tested on Chrome and IE.
What happens is the first time I request the page all the external resources decompress ok. The 2nd or 3rd time the page has errors because the resource doesn't seem to be decompressed. I get unicode characters like:
������í½`I%&/mÊ{JõJ×àt¡`$Ø#ìÁÍæìiG#)«*ÊeVe]f
(actually they can't be displayed properly here)
Inspecting the page via firebug displays the response header as:
Cache-Control private
Content-Type text/html; charset=utf-8
Content-Encoding gzip
Server Microsoft-IIS/7.5
X-AspNetMvc-Version 2.0
X-AspNet-Version 2.0.50727
X-Compressed-By HttpCompress
X-Powered-By ASP.NET Date Fri, 09 Jul
2010 06:51:40 GMT Content-Length 2622
This clearly states that the resource is compressed by gzip. So something seems to be going wrong on the deflate side on the client?
I have added the following sections (in the appropriate locations) in the web.config:
<sectionGroup name="blowery.web">
<section name="httpCompress" type="blowery.Web.HttpCompress.SectionHandler, blowery.Web.HttpCompress"/>
</sectionGroup>
<blowery.web>
<httpCompress preferredAlgorithm="gzip" compressionLevel="high">
<excludedMimeTypes>
<add type="image/jpeg"/>
<add type="image/png"/>
<add type="image/gif"/>
</excludedMimeTypes>
<excludedPaths>
<add path="NoCompress.aspx"/>
</excludedPaths>
</httpCompress>
</blowery.web>
<add name="CompressionModule" type="blowery.Web.HttpCompress.HttpModule, blowery.web.HttpCompress"/>
Any help?
This is an issue that I have face before and the problem is that the Content-Length is not correct. Why is not correct ? because its probably calculate before the compression.
If you set Content-Lenght by hand, just remove it and let the module set it if he can.
I note that you use the Blowery compression. Probably this is a bug/issue inside Blowery. If you can not locate it and fix it, why not use the Ms compression ?
#ptutt if you are on shared iis, then maybe there have all ready set compression, so there is one compression over the other, and you only need to remove yours. If this is the issue then for sure the content-lenght is false because after the first compression, the second is break it.
Check it out using this site https://www.giftofspeed.com/gzip-test/ if your pages is all ready compressed by default by iis.
If not compressed by default then you can do it very easy. On Global.asax
protected void Application_BeginRequest(Object sender, EventArgs e)
{
string cTheFile = HttpContext.Current.Request.Path;
string sExtentionOfThisFile = System.IO.Path.GetExtension(cTheFile);
if (sExtentionOfThisFile.Equals(".aspx", StringComparison.InvariantCultureIgnoreCase))
{
string acceptEncoding = MyCurrentContent.Request.Headers["Accept-Encoding"].ToLower();;
if (acceptEncoding.Contains("deflate") || acceptEncoding == "*")
{
// defalte
HttpContext.Current.Response.Filter = new DeflateStream(prevUncompressedStream,
CompressionMode.Compress);
HttpContext.Current.Response.AppendHeader("Content-Encoding", "deflate");
} else if (acceptEncoding.Contains("gzip"))
{
// gzip
HttpContext.Current.Response.Filter = new GZipStream(prevUncompressedStream,
CompressionMode.Compress);
HttpContext.Current.Response.AppendHeader("Content-Encoding", "gzip");
}
}
}
Please note, I just write this code and have not tested. My code is a little more complicate, so I just create a simple verion of it.
Find more examples:
http://www.google.com/search?q=Response.Filter+GZipStream
Reference:
ASP.NET site sometimes freezing up and/or showing odd text at top of the page while loading, on load balanced servers

Resources