IIS Content-Disposition file name encoded twice - asp.net

When downloading a file, the Content-Disposition is encoded twice. This leads to unreadable file names for non-ascii characters.
This double encoding happens only when the request is being processed by the IIS rewrite module 2.0 and routed to a server farm. In this situation, when the code offers the file as a download, the response header, Content-Disposition, is double encoded. The browser decodes this once, which leaves with an encoded file name where non-ascii characters are unreadable.
In a situation where the same request is being processed without IIS rewrite module and ARR. The response header, Content-Disposition is encoded, but only once. In this case the browser decodes the filename and non-ascii characters are readable.
I've been looking into this for days now. I've only found 1 other question like this, but without an awnser.
For reference, here is the code used for the download file. (ASP.NET application)
In this code, report represents an entityframework object, and the filename is an unencoded string. We don't encode the filename. So i'm not even sure what is responsible for the first encoding.
System.Web.HttpResponse response = System.Web.HttpContext.Current.Response;
response.ClearContent();
response.Clear();
response.ContentType = "application/octet-stream";
response.AddHeader("Content-Disposition",
"attachment; filename=" + report.FileName);
response.TransmitFile(filePath);
response.Flush();
response.End();
Here are 2 images to display the issue:
Example download request header with IIS rewrite and ARR:
Example download request header without IIS rewrite and ARR
Any ideas on a solution? (Not using rewrite/arr/server farm is not an option)
EDIT:
Image of content-disposition found in failed tracelog.
It does look like ASP.NET automatically URL encodes the filename.
Makes it even more confusing.
Content-disposition-failed-trace-log
Additionally the inbound rewrite rule, that redirects traffic to a webfarm. (This webfarm is hosted in the same IIS on the same server)
Inbound-rewrite-rule-web-farm

Related

Generating PDF on the fly with standard HTTP response fields

I'm developing a web page with a form which returns a PDF document based on the form data. Currently I use the HTTP response fields
Content-Type: application/pdf
Content-Disposition: attachment; filename="foo.pdf"
However, since the field Content-Disposition is non-standard and doesn't work in all browsers I'm looking for a different approach. Do I have to save the PDF document on the server? What is the modus operandi?
Edit: By "doesn't work in all browsers" I mean that with some browsers the filename is not set to foo.pdf. Dillo, for instance, just sets the default filename (in the download dialog) to the basename of the URL path (plus query string).
Do I have to save the PDF document on the server?
No. As far as the HTTP client is concerned it, the inner workings of the server are completely opaque to it. All it sees is a TCP stream of bytes from the server and how exactly that stream is produced doesn't matter as long as it matches the specified Content-Type.
Just send the PDF right after the HTTP headers and you're done with.
Update due to comment
So if you're wondering how to supply a filename without using a header field: Just augment the URL with it. I.e. something like
http://${DOMAIN}/${PDF_GENERATOR}/${DESIRED_FILENAME}
In the HTTP server add a rewrite rule to simply omit the filename part and redirect to just
http://${DOMAIN}/${PDF_GENERATOR}
The HTTP client does not see that, all it see is some URL ending with a "filename", that it can present the user as a default for saving.

Difference between downloading a web file directly & indirectly

On my web server I have a video file named 03.mp4.
I have a page (videoserver.aspx) to serve that file using below code
Response.ContentType = "application/octet-stream";
Response.AppendHeader("Content-Disposition", "attachment; filename=video.mp4");
Response.TransmitFile(Server.MapPath("03.mp4"));
Response.End();
Whats the difference between these 2 calls?
1: http://localhost/media/03.mp4
2: http://localhost/media/videoserver.aspx?q=03
When I point to those URLs directly in my browser, it prompts me a Save dialog in both the cases.
I have another web page that has a SWFObject. It consumes a video as input. Ok. When I feed it URL 1, it loads the video.
When I feed it URL 2, it doesn't load the video.
Why this difference? I prefer URL 2 as you can dynamically change the videos you are serving to consumers based on the query-string.
A lot of video players, including the new HTML5 <video> element, require support for so-called byte range requests using the HTTP Range header. This is normally already built in a bit self-respected HTTP server. Basically, to inform the client that the requested URL supports byte range requests, the server is supposed to return Accept-Ranges: bytes on the response and to be able to process all incoming Range requests by serving exactly the requested byte ranges back to the response as per the specification (see the first link on the Range header for detail).
So if you choose to take the HTTP response handling fully in your own hands instead of letting the HTTP server do the job it is designed for, you have to take this carefully into account.
Hence it proves I am a newbie to SWFObject.
The SWFObject I was referring to was dished out by Camtasia and it accepts a mp4 file thru FLashVars.
The question is "why did it not accept URL 2 while it accepted URL 1?". To which the answer is, URL 2 was not ending with .mp4.
And solution to my problem then was, create a handler that would accept */media/*.mp4 path and return the appropriate file's content, which in my case is fetched from DB.

Wrangling Control of HTTP Headers in ASP.NET

I'm working with ASP.NET MVC3, and I'm trying to get absolute control over my headers because a client application that I'm working with expects a very specific content type. What I'm finding when using Fiddler to examine the HTTP traffic is that the text encoding is being returned as part of the header.
For example, the client is expecting application/appname in the Content-Type header, but the server is returning application/appname; charset=utf-8. I think the client is using a strict comparison for checking the type, so I want to be able to specify exactly what is emitted in the headers.
Right now I have a custom ActionResult in which I clear the headers and then specify only the content type, but the encoding still seems to be added on.
How can I remove the encoding from the Content-Type header?
Charset Encoding in ASP.NET Response from Rick Strahl is an older (2007) article, but maybe give it a try.
Response.ContentType = "application/appname";
Response.Charset = null;

Why is ASP.NET replacing a Content-Length header with a Transfer-Encoding header when manually flushing a response?

Our web application (ASP.NET Web Forms) has a page that will display a recently generated PDF file to users. Because the PDF file is sometimes quite large, we've implemented a "streaming" approach to send it down to the client browser in chunks.
Despite sending the data down in chunks, we know the full size of the file prior to sending it, so we set the Content-Length header appropriately. This has been working in our production environment for awhile (and continues to work in our test environment with a virtually identical configuration) until today. The issue reported was that Chrome would attempt to open the PDF file but would hang with the "Loading" animation stuck.
Because everything was still working fine in our test environment I was able to use Firebug to take a look at the response headers that were coming back in both environments. In the test environment, I was seeing a proper 'Content-Length' header, while in production that had been replaced with a Transfer-Encoding: chunked header. Chrome doesn't like this, hence the hang-up.
I've read some articles and posts talking about how the Transfer-Encoding header can show up when no Content-Length header is provided, but we are specifying the Content-Length header and everything still appears to work while running the same code for the same PDF file on a test server.
Both test and production servers are running IIS 7.5 and both have Dynamic and Static Compression enabled.
Here is the code in question:
var fileInfo = new FileInfo(fileToSendDown);
Response.ClearHeaders();
Response.ContentType = "application/pdf";
Response.AddHeader("Content-Disposition", "filename=test.pdf");
Response.AddHeader("Content-Length", fileInfo.Length.ToString());
var buffer = new byte[1024];
using (var fs = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.Read))
{
int read;
while ((read = fs.Read(buffer, 0, 1024)) > 0)
{
if (!response.IsClientConnected) break;
Response.OutputStream.Write(buffer, 0, read);
Response.Flush();
}
}
I was fortunate to see the same behavior on my local workstation so using the debugger I have been able to see that the 'Transfer-Encoding: chunked' header is being set on the 2nd pass through the while loop during the call to 'Flush'. At that point, the response has both a Content-Length header and Transfer-Encoding header, but somehow by the time the response reaches the browser Firebug is only showing the Transfer-Encoding header.
UPDATE
I think I've tracked this down to using a combination of sending the data down in "chunks" AND attaching a 'Filter' to the HttpResponse object (we were using a filter to track the size of viewstate being sent down to each page). There's no sense in us using an HTTP filter when sending a PDF down to the browser, so clearing the filter here has resolved our issue. I decided to dig in a little deeper purely out of curiosity and have updated this question should anyone else ever stumble onto this problem in the future.
I've got a simple app up on AppHarbor that reproduces the issue: http://transferencodingtest.apphb.com/. If you check both the 'Use Filter?' and 'Send In Chunks?' boxes you should be able to see the 'transfer-encoding: chunked' header show up (using Chrome dev tools, Firebug, Fiddler, whatever). If either of the boxes are not checked, you'll get a proper content-length header. The underlying code is up on github so you can see what's going on behind the scenes:
https://github.com/appakz/TransferEncodingTest
Note that to repro locally you'd need to setup a local website in IIS 7.5 (7 may also work, I haven't tried). The ASP .NET development server that ships with Visual Studio DOES NOT repro the issue.
I've added some more details to a blog post here: 'Content-Length' Header Replaced With 'Transfer-Encoding: Chunked' in ASP .NET
From an article on MSDN it seems that you can disable chunked encoding:
appcmd set config /section:asp /enableChunkedEncoding:False
But it's mentioned under ASP settings, so it may not apply to a response generated from an ASP.NET handler.
Once Response.Flush() has been called, the response body in in the process of being sent to the client, so no additional headers can be added to the response. I find it very unlikely that a second call to Response.Flush() is adding the Transfer-Encoding header at that time.
You say you have compression enabled. That almost always requires a chunked response. So it would make sense that if the server knows the Content-Length prior to compression, it might substitute that header for the Transfer-Encoding header and chunk the response. However, even with compression enabled on the server, the client has to explicitally state support for compression in its Accept-Encoding request header or else the server cannot compress the response. Did you check for that in your tests?
On a final note, since you are calling Response.Flush() manually, try setting Response.Buffer = True and Response.BufferOutput = False. Apparently they have conflicting effects on how Response.Flush() operates. See the comment at the bottom of this page and this page.
I had a similar problem when I was writing a large CSV (The file didn't exist I write a string line by line by iterating through an in memory collection and generating the line) by calling Response.Write on the Response stream with BufferOutput set to false, but the solution was to change
Reponse.ContentType = 'text/csv' to Reponse.ContentType = 'application/octet-stream'
When the content type wasn't set to application/octet-stream a bunch of other response headers were added such as Content-Encoding - gzip

Sending compressed response in ASP.NET

I am running a website on IIS6 for ASP.NET application and enabled compression, which worked fine for the .aspx web pages.
What isn't working is downloaded binary files that are transmitted as part of a postback response: e.g. link button is 'download to excel' - user clicks and the code generates a binary file, does a Response.Clear() and then writes the file to the Response.OutputStream.
A response is seen by browsers but it is always zero bytes. I assume therefore that web browsers are expecting a compressed response, and as the raw binary isn't a valid compressed stream it fails. I'm puzzled as to why this should be if I've cleared the response - surely the response headers (specifying compression) are also cleared?
So two questions arise:
1) how do I compress the binary file to send a compressed response?
2) how can I check at runtime to see if IIS compression is enabled?
Cheers
I would disable compression and check whether this still works, just to isolate the fact that this is indeed due to IIS compression. I'm telling you that 'cos I'm running a IIS/Compression enabled site which provides PDF files (binary) without a problem.
Anyway here's the part of code which works for me.
Response.Clear();
Response.ClearHeaders();
Response.AddHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
Response.AddHeader("Content-Length", fileInfo.Length.ToString());
Response.AddHeader("Content-transfer-encoding", "8bit");
Response.AddHeader("Cache-Control", "private");
Response.AddHeader("Pragma", "cache");
Response.AddHeader("Expires", "0");
Response.ContentType = "application/pdf";
Response.WriteFile(filePath);
Response.Flush();
Response.Close();
HttpContext.Current.ApplicationInstance.CompleteRequest();

Resources