My web app generates a CSV file on the fly, but whenever I use GZIP compression, the download fails:
HTTP/1.1 200 OK
Cache-Control: private, s-maxage=0,no-store, no-cache
Transfer-Encoding: chunked
Content-Type: text/csv;charset=utf-8
Content-Encoding: gzip
Vary: Accept-Encoding
Server: Microsoft-IIS/7.5
X-AspNetMvc-Version: 3.0
Content-Disposition: attachment;filename="filename.csv"
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
p3p: CP="CAO PSA OUR"
Date: Fri, 03 Feb 2012 11:27:27 GMT
The download appears as "Interrupted" in Google Chrome, and in Internet Explorer appears an error that says "Content decoding has failed" .
HTTP/1.1 200 OK
Cache-Control: private, s-maxage=0,no-store, no-cache
Transfer-Encoding: chunked
Content-Type: text/csv;charset=utf-8
Server: Microsoft-IIS/7.5
X-AspNetMvc-Version: 3.0
Content-Disposition: attachment;filename="filename.csv"
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
p3p: CP="CAO PSA OUR"
Date: Fri, 03 Feb 2012 11:23:30 GMT
The solution is disabling compression on that action, but... why does this happen?
Cheers.
UPDATE: The compression filter that I use:
public class EnableCompressionAttribute : ActionFilterAttribute
{
const CompressionMode compress = CompressionMode.Compress;
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (filterContext.IsChildAction)
return;
var actionAttributes = filterContext.ActionDescriptor.GetCustomAttributes(true);
if (actionAttributes != null && actionAttributes.Any(attr => attr is SkipCompressionAttribute))
return;
HttpRequestBase request = filterContext.HttpContext.Request;
HttpResponseBase response = filterContext.HttpContext.Response;
String acceptEncoding = request.Headers["Accept-Encoding"];
if (acceptEncoding == null || response.Filter == null)
return;
if (acceptEncoding.ToLower().Contains("gzip"))
{
response.Filter = new GZipStream(response.Filter, compress);
response.AppendHeader("Content-Encoding", "gzip");
response.AppendHeader("Vary", "Accept-Encoding");
}
else if (acceptEncoding.ToLower().Contains("deflate"))
{
response.Filter = new DeflateStream(response.Filter, compress);
response.AppendHeader("Content-Encoding", "deflate");
response.AppendHeader("Vary", "Accept-Encoding");
}
}
}
Try:
Response.Headers.Remove("Content-Encoding");
Response.AppendHeader("Content-Encoding", "gzip");
Another problem may be the caching: what if a client accepts compressed content, but the next client doesn't, and the server cached the compressed data? what the second client will get? A cached compressed page that won't decode!
To fix that, add another method:
public override string GetVaryByCustomString(HttpContext context, string custom)
{
if (custom == "GZIP")
{
string acceptEncoding = HttpContext.Current.Response.Headers["Content-Encoding"];
if (string.IsNullOrEmpty(acceptEncoding))
return "";
else if (acceptEncoding.Contains("gzip"))
return "GZIP";
else if (acceptEncoding.Contains("deflate"))
return "DEFLATE";
return "";
}
return base.GetVaryByCustomString(context, custom);
}
Related
Short Version
I'm adding the response header:
Connection: keep-alive
but it's not in the resposne.
Long Version
I am trying to add a header to an HttpResponse in ASP.net:
public void ProcessRequest(HttpContext context)
{
context.Response.CacheControl = "no-cache";
context.Response.AppendHeader("Connection", "keep-alive");
context.Response.AppendHeader("AreTheseWorking", "yes");
context.Response.Flush();
}
And when the response comes back to the client (e.g. Chrome, Edge, Internet Explorer, Postman), the Connection header is missing:
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Transfer-Encoding: chunked
Expires: -1
Server: Microsoft-IIS/10.0
AreTheseWorking: yes
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Sat, 26 Feb 2022 16:29:17 GMT
What am I doing wrong?
Bonus Chatter
In addition to trying AppendHeader:
context.Response.AppendHeader("Connection", "keep-alive"); //preferred
I also tried AddHeader (which exists "for compatibility with earlier versions of ASP"):
context.Response.AddHeader("Connection", "keep-alive"); // legacy
I also tried Headers.Add:
context.Response.Headers.Add("Connection", "keep-alive"); //requires IIS 7 and integrated pipeline
What am i doing wrong?
Bonus: hypothetical motivation for the question
By default keep-alive is not allowed in ASP.net.
In order to allow it, you need to add an option to your web.config:
web.config:
<configuration>
<system.webServer>
<httpProtocol allowKeepAlive="true" />
</system.webServer>
</configuration>
This is especially important for Server-Send Events:
public void ProcessRequest(HttpContext context)
{
if (context.Request.AcceptTypes.Any("text/event-stream".Contains))
{
//Startup the HTTP Server Send Event - broadcasting values every 1 second.
SendSSE(context);
return;
}
}
private void SendSSE(HttpContext context)
{
//Don't worry about it.
string sessionId = context.Session.SessionID; //https://stackoverflow.com/a/1966562/12597
//Setup the response the way SSE needs to be
context.Response.ContentType = "text/event-stream";
context.Response.CacheControl = "no-cache";
context.Response.AppendHeader("Connection", "keep-alive");
context.Response.Flush();
while (context.Response.IsClientConnected)
{
System.Threading.Thread.Sleep(1000);
String data = DateTime.Now.ToString();
context.Response.Write("data: " + data + "\n\n");
context.Response.Flush();
}
}
Please help to advise for my issue below.
I try to create an enrollment using sample code from here
private async Task<HttpResponseMessage> MakeRequest()
{
string path = #"<path_to_wav_file>";
var client = new HttpClient();
var queryString = HttpUtility.ParseQueryString(string.Empty);
// Request headers
client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", "<my_key>");
// Request parameters
queryString["shortAudio"] = "true";
queryString["identificationProfileId"] = "<my_profile_id>";
var uri = "https://westus.api.cognitive.microsoft.com/spid/v1.0/identificationProfiles/<my_profile_id>/enroll?" + queryString;
HttpResponseMessage response;
// Request body
byte[] byteData = File.ReadAllBytes(path);
using (var content = new ByteArrayContent(byteData))
{
content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
response = await client.PostAsync(uri, content);
}
return response;
}
and got the response
{StatusCode: 202, ReasonPhrase: 'Accepted', Version: 1.1, Content:
System.Net.Http.StreamContent, Headers: { Pragma: no-cache
Operation-Location:
https://westus.api.cognitive.microsoft.com/spid/v1.0/operations/af54c843-8df9-4511-8d65-4825ebec024d
apim-request-id: 37567cff-d259-4a1d-82fc-9fc884edcfe3
Strict-Transport-Security: max-age=31536000; includeSubDomains;
preload x-content-type-options: nosniff Cache-Control: no-cache
Date: Tue, 08 Jan 2019 07:12:05 GMT X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET Content-Length: 0 Expires: -1 }}
said that
{"error":{"code":"Unspecified","message":"Access denied due to invalid
subscription key. Make sure you are subscribed to an API you are
trying to call and provide the right key."}}
The error message is strange because I used the same Subscription Key that created profile successfully.
I think you should use ("Ocp-Apim-Subscription-Key", "") when make request to
https://api.projectoxford.ai/spid/v1.0/operations/af54c843-8df9-4511-8d65-4825ebec024d
I have a post controller in an MVC app returning this response:
return new HttpResponseMessage(HttpStatusCode.Accepted)
{
Content = new StringContent("test")
};
When I hit the post URL with this code:
using (WebClient client = new WebClient())
{
string result = client.UploadString(url, content);
}
result contains this response:
StatusCode: 202, ReasonPhrase: 'Accepted', Version: 1.1, Content: System.Net.Http.StringContent, Headers: { Content-Type: text/plain; charset=utf-8 }
Why isn't "test" appearing after Content:?
Thanks!
You should not return HttpResponseMessage from ASP.NET MVC action. In this case you'll get messy response like this:
HTTP/1.1 200 OK
Cache-Control: private
Content-Type: text/html; charset=utf-8
Vary: Accept-Encoding
Server: Microsoft-IIS/10.0
X-AspNetMvc-Version: 5.2
X-AspNet-Version: 4.0.30319
X-SourceFiles: =?UTF-8?B?RDpcRHJvcGJveFxwcm9nXFN0YWNrT3ZlcmZsb3dcZG90TmV0XE12Y0FwcGxpY2F0aW9u?=
X-Powered-By: ASP.NET
Date: Sun, 04 Feb 2018 10:18:38 GMT
Content-Length: 154
StatusCode: 202, ReasonPhrase: 'Accepted', Version: 1.1, Content: System.Net.Http.StringContent, Headers:
{
Content-Type: text/plain; charset=utf-8
}
As you see, you actually get 200 HTTP response with HttpResponseMessage details in response body. This messy body content is what you deserialize into result variable.
ASP.NET MVC actions should return an instance of the class derived from System.Web.Mvc.ActionResult. Unfortunately, there is no built-in action result that allows setting both return status code and body content.
There is ContentResult class that allows to set return string content with status code of 200. There is also HttpStatusCodeResult that allows setting arbitrary status code but the response body will be empty.
But you could implement your custom action result with settable status code and response body. For simplicity, you could base it on ContentResult class. Here is a sample:
public class ContentResultEx : ContentResult
{
private readonly HttpStatusCode statusCode;
public ContentResultEx(HttpStatusCode statusCode, string message)
{
this.statusCode = statusCode;
Content = message;
}
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
base.ExecuteResult(context);
HttpResponseBase response = context.HttpContext.Response;
response.StatusCode = (int)statusCode;
}
}
The action would look like:
public ActionResult SomeAction()
{
return new ContentResultEx(HttpStatusCode.Accepted, "test");
}
Another possible fix is to change your controller from MVC to WEB API controller. To make this - just change base class of controller from System.Web.Mvc.Controller to System.Web.Http.ApiController. In this case you could return HttpResponseMessage as in your answer.
In both cases you will get correct HTTP response with 202 status code and string in the body:
HTTP/1.1 202 Accepted
Cache-Control: private
Content-Type: text/html; charset=utf-8
Server: Microsoft-IIS/10.0
X-AspNetMvc-Version: 5.2
X-AspNet-Version: 4.0.30319
X-SourceFiles: =?UTF-8?B?RDpcRHJvcGJveFxwcm9nXFN0YWNrT3ZlcmZsb3dcZG90TmV0XE12Y0FwcGxpY2F0aW9u?=
X-Powered-By: ASP.NET
Date: Sun, 04 Feb 2018 10:35:24 GMT
Content-Length: 4
test
I have little problem with caching my views. The location header isn't correct when I lost my ticket and get logged out, and trying to get directly into url I was before.
Example: I'm inside /Admin/Categories, then I'm getting logged out due to being afk too long, so I'm being redirected to /Admin/Login. After log in I'm trying to go to /Admin/Categories and cache is sending me into /Admin/Login instead of /Admin/Categories.
My code:
LOGIN CONTROLLER
[OutputCache(CacheProfile = "OneDayCache", VaryByParam = "None", VaryByCustom = "url")]
public ActionResult Index()
{
return View();
}
CATEGORIES CONTROLLER
[OutputCache(CacheProfile = "OneDayCache", VaryByParam = "None", VaryByCustom = "url")]
public ActionResult Index()
{
if (validations.ValidateTicket())
{
return View();
}
else
{
return RedirectToAction("Index", "Login");
}
}
validations.ValidateTicket() is returning true or false and it's working good - it's not the problem.
GLOBAL.ASAX.CS
public override string GetVaryByCustomString(HttpContext context, string arg)
{
if (arg == "url")
{
return context.Request.RawUrl;
}
return base.GetVaryByCustomString(context, arg);
}
Web.config part inside :
<caching>
<outputCache enableOutputCache="true" omitVaryStar="true"></outputCache>
<outputCacheSettings>
<outputCacheProfiles>
<add name="OneDayCache" duration="86400" location="Client" />
</outputCacheProfiles>
</outputCacheSettings>
</caching>
Cache - Login (/Admin/Login)
HTTP/1.1 200 OK
Cache-Control: private, max-age=86400
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Expires: Fri, 10 Feb 2017 20:35:45 GMT
Last-Modified: Thu, 09 Feb 2017 20:35:45 GMT
Vary: Accept-Encoding
Server: Microsoft-IIS/10.0
X-AspNetMvc-Version: 5.2
X-Frame-Options: SAMEORIGIN
X-AspNet-Version: 4.0.30319
X-SourceFiles: =?UTF-8?B?TTpcUHJvamVrdHlcQU1CSVQtQ01TLU1WQ1xBTUJJVCBDTVMgTVZDXEFkbWluXExvZ2lu?=
X-Powered-By: ASP.NET
Date: Thu, 09 Feb 2017 20:35:45 GMT
Content-Length: 1113
Cache - Categories (/Admin/Categories) - look at location header which is wrong...
HTTP/1.1 302 Found
Cache-Control: private, max-age=86400
Content-Type: text/html; charset=utf-8
Expires: Fri, 10 Feb 2017 20:35:39 GMT
Last-Modified: Thu, 09 Feb 2017 20:35:39 GMT
Location: /Admin/Login
Server: Microsoft-IIS/10.0
X-AspNetMvc-Version: 5.2
X-AspNet-Version: 4.0.30319
X-SourceFiles: =?UTF-8?B?TTpcUHJvamVrdHlcQU1CSVQtQ01TLU1WQ1xBTUJJVCBDTVMgTVZDXEFkbWluXENhdGVnb3JpZXM=?=
X-Powered-By: ASP.NET
Date: Thu, 09 Feb 2017 20:35:39 GMT
Content-Length: 439
Ok, so the problem was that OutputCache location with VaryByCustom used as parameter needs to be set to Server or any other using Server location too.
For example:
Usage in controller:
[OutputCache(CacheProfile = "ControllerIndexCache")]
Web.config:
<caching>
<outputCache enableOutputCache="true" omitVaryStar="true"></outputCache>
<outputCacheSettings>
<outputCacheProfiles>
<add name="ControllerIndexCache" duration="10" location="Server" varyByCustom="Url" varyByParam="None" />
</outputCacheProfiles>
</outputCacheSettings>
</caching>
Global.asax.cs:
public override string GetVaryByCustomString(HttpContext context, string arg)
{
if (arg == "Url")
{
return context.Request.Url.AbsoluteUri;
}
return base.GetVaryByCustomString(context, arg);
}
This solution is working just fine.
I have an ASPX page that issues a Response.Redirect that points to an image file.
The redirect response headers look like this:
HTTP/1.1 302 Found
Cache-Control: private
Content-Type: text/html; charset=utf-8
Location: https://www.site.com/folder/file.jpg
Server: Microsoft-IIS/8.0
Date: Tue, 29 Apr 2014 08:29:58 GMT
Content-Length: 241
Is it possible to force the client and any proxy servers to cache this response for say 30 days? Should I do this with Cache-Control, ETags or both? If so, how?
I have figured this out and tested it. The following code adds the ETags and cache-control:
protected void Page_Load(object sender, EventArgs e)
{
var absoluteUrl = GetUrlFromDatabase(Request["fileId"]);
CacheResponse(absoluteUrl);
Response.Redirect(absoluteUrl);
}
private static void CacheResponse(string absoluteLocation)
{
// you need to clear the headers that ASP.NET automatically adds
HttpContext.Current.Response.ClearHeaders();
// now get the etag (hash the
var etag = GetETag(absoluteLocation);
// see if the etag matches what was sent
var requestedETag = HttpContext.Current.Request.Headers["If-None-Match"];
if (requestedETag == etag)
{
HttpContext.Current.Response.Status = "304 Not Modified";
HttpContext.Current.ApplicationInstance.CompleteRequest();
return;
}
// otherwise set cacheability and etag.
HttpContext.Current.Response.Cache.SetValidUntilExpires(true);
HttpContext.Current.Response.Cache.SetCacheability(HttpCacheability.ServerAndPrivate);
HttpContext.Current.Response.Cache.SetExpires(DateTime.Now.AddMonths(1));
HttpContext.Current.Response.Cache.SetLastModified(DateTime.UtcNow);
HttpContext.Current.Response.Cache.SetETag("\"" + etag + "\"");
}
private static string GetETag(string url)
{
var guid = StringToGuid(url);
var etag = new ShortGuid(guid); // see reference to ShortGuid below
return etag.Value.Replace("-", string.Empty);
}
private static Guid StringToGuid(string value)
{
// Create a new instance of the MD5CryptoServiceProvider object.
var md5Hasher = MD5.Create();
// Convert the input string to a byte array and compute the hash.
var data = md5Hasher.ComputeHash(Encoding.Default.GetBytes(value));
return new Guid(data);
}
Reference: ShortGuid.
The initial HTTP response headers are now:
HTTP/1.1 302 Found
Cache-Control: private
Content-Type: text/html; charset=utf-8
Expires: Thu, 29 May 2014 09:07:41 GMT
Last-Modified: Tue, 29 Apr 2014 09:07:41 GMT
ETag: "k28kbGNuxkWzP6gmLO2xQ"
Location: https://www.site.com/folder/file.jpg
Date: Tue, 29 Apr 2014 09:07:41 GMT
Content-Length: 241