3

Following a number of tutorials and examples on line I have constructed a file download system built on jQuery on the client and MS WebAPI on the server.

Providing direct links to the files isn't possible as the API requires authentication, thus the file URL is an API endpoint not the file location.

On the server I have this:

[HttpGet]
[Route("download/{filename}")]
public HttpResponseMessage DownloadFile(string filename)
{
    try
    {
        // https://gist.github.com/joeriks/3714093
        string path = string.Format("{0}/Exports/{1}", root, filename);

        HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK);
        var stream = new FileStream(path, FileMode.Open);
        result.Content = new StreamContent(stream);
        result.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
        result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment");
        result.Content.Headers.ContentDisposition.FileName = "download.txt";
        return result;

    }
    catch (Exception ex)
    {
        throw new HttpResponseException(HttpStatusCode.InternalServerError);
    }
}

Which returns a response as expected:

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Length: 4809
Content-Type: application/octet-stream
Expires: -1
Server: Microsoft-IIS/10.0
Content-Disposition: attachment; filename=download.txt
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: POST, PUT, DELETE, GET, OPTIONS
Access-Control-Allow-Headers: content-Type, accept, origin, X-Requested-With, X-Authentication, X-Nonce, name
Date: Thu, 25 Oct 2018 13:07:25 GMT

With the text content in the response. So far all is as I would hope.

On the client I have the following for handling responses from my API:

// https://stackoverflow.com/a/23797348
let disposition = jqXHR.getResponseHeader('Content-Disposition');

if (disposition && disposition.indexOf('attachment') !== -1) {
    let filename = "scada-download.txt";
    let matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(disposition);
    if (matches != null && matches[1]) filename = matches[1].replace(/['"]/g, '');

    let type = jqXHR.getResponseHeader('Content-Type');
    let blob = new Blob([data], { type: "text/csv" });

    var downloadUrl = URL.createObjectURL(blob);
    let $a = $("<a id='temp_download_link' style='display: none;' />").attr("href", downloadUrl).attr("download", filename);
    $("body").append($a);
    $a.trigger("click");
}

This does as advertised, and adds an anchor to the page, and clicks it triggering the download.

The download works, and saves a file with the correct contents.

The only bit that doesn't work is that in both browsers I've tested it in (Chrome 69, FF: 62) the default file name provided is simply a GUID.

The api and the client code are both running from my local dev machine at present, http://127.0.0.1:9000/[client | api] so cross origin shouldn't be playing any part.

The call to the api is made via ajax. Ultimately the jQuery $.ajax() method

For clarity sake, the anchor inserted into the DOM is:

<a id="temp_download_link" style="display: none;" href="blob:http://127.0.0.1:9000/c2c5ffb5-3f22-4a57-8775-4e0bbfbfef9e" download="download.txt"></a>

The default filename provided by Chrome is the GUID in the URL, FF generates a seemingly unconnected 6 character random string.

Specifically, Why are the browsers ignoring both the download="download.txt" attribute of the anchor and the Content-Disposition: attachment; filename=download.txt?

UPDATE:

I forked this fiddle:

http://jsfiddle.net/Qjvb3/

And added some other values for the href attribute, it seems that the whole setting the filename is flaky at best:

http://jsfiddle.net/yubjqwvs/

UPDATE 2

I've copied the link from a working example into my fiddle, it works from the original site, but not from fiddle.

http://jsfiddle.net/yubjqwvs/2/

I have a feeling that the answer will boil down to "Why does it work on David Walsh Blog, but not on fiddle?"

Morvael
  • 3,478
  • 3
  • 36
  • 53
  • maybe a stupid question, but have you tried `filename="download.txt"`? also, cleared the cache (really, completely cleared the whole cache)? – Alex Nov 12 '18 at 14:20
  • @Alex, I hadn't tried it - but I have now. Makes no difference – Morvael Nov 14 '18 at 08:24
  • I think this fiddle will help us debug more. There has to be a reason, no matter how oddly it looks – noobed Nov 14 '18 at 09:55
  • In the example you gave there are a couple of things to note. Firstly the button "It doesn't save" actually works as expected, because it tries to open the link instead of downloading it. (it's described here in the specification: https://html.spec.whatwg.org/multipage/links.html#as-a-download). Second the .woff file is not downloaded because of the download attribute. Actually what happens is that the browser is trying to redirect your page to the link, but doesn't understand how to render .woff thus downloading it. You can this by changing .woff with css/font-awesome.css instead. :) – noobed Nov 14 '18 at 12:12
  • @Morvael I've played and tweaked a bit with your demo - http://jsfiddle.net/kos3rjfq/19/. I could edit my initial answer as it is truly not relevant for the case. :) – noobed Nov 14 '18 at 12:13
  • @noobed I've looked at your enhanced fiddle and I can see what you are gettign at with the browser knowing how to handle certain file types. But the browser also knows how to render the .png and yet doesn't. I've updated my fiddle with an example copied straight from DWB, it works there but not on fiddle. – Morvael Nov 15 '18 at 15:29
  • @Morvael it doesn't render your pgn because it's in base64 embeded string. That's obvious a "local" resource, regarding image content and origin. However you try to download a remote/cross origin png/jpg it will simply display it, just like you stated that the browser knows how to render those type of visual files. :) http://jsfiddle.net/yubjqwvs/4/ . In the aforementioned example both the site and the file have same origin domain. – noobed Nov 16 '18 at 07:20
  • @noobed, thanks for the help here I really do appreciate it but I think I must be missing something. I found this `This attribute only works for same-origin URLs.` src: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#Attributes which explains why my DWB example isn't working and probably other stuff in the fiddle. But theres nothing there that explains the initial problem of not using the supplied name. – Morvael Nov 16 '18 at 10:05

3 Answers3

0

I personally used downloadjs to avoid similar problems in my case:

  • An AJAX request is made to download the file; that's simply a PHP script with appropriate header (Content-Disposition: attachment; filename="...")
  • The user click on the link, which use a hashchange event to trigger the download upon which I pass the AJAX response to downloadjs. I suppose the same could be done using a Blob URL.

The code of downloadjs is pretty similar to jsfiddle loosely based on your example. It works fine with Firefox 63, Opera 56 (I guess it should work on Chrome) and it fails on IE 11, but well, that's IE.

  • downloadjs creates an hidden link with a timeout set to 66ms (I suppose it is to wait for the DOM to be ready or something like that).
  • The timeout invoke click() on the link which probably call the native handler.

With your code, and using click() as well as document.getElementById() rather than jQuery, given us this jsfiddle. Contrary to downloadjs, I don't use a window.setTimeout and it works fine, at least in Fx 63. This might simply be a way to bypass a browser bug.

In your case, my guess is that jQuery is not firing the default handler when you invoke trigger('click') which I fails to understand since trigger documentation seems to tell us it does:

As of jQuery 1.3, .trigger()ed events bubble up the DOM tree; an event handler can stop the bubbling by returning false from the handler or calling the .stopPropagation() method on the event object passed into the event. Although .trigger() simulates an event activation, complete with a synthesized event object, it does not perfectly replicate a naturally-occurring event.

To trigger handlers bound via jQuery without also triggering the native event, use .triggerHandler() instead. (quoted from jquery documentation)

Perhaps that the default click on links is not considered a handler (?) for jquery and does not get fired.

NoDataFound
  • 11,381
  • 33
  • 59
0

The reason I see is that you are using jQuery .attr() function instead of .prop() function. There is a difference between HTML5 properties and attributes. The first one gives markup to elements in cases for event binding and etc, while the other gives access to/sets the value of the DOM element itself.

I'll link an explanation that could be useful here.

noobed
  • 1,329
  • 11
  • 24
  • This didn't sound likely to me, asI'm not messing with the node once I have inserted it into the DOM, so it shouldn't make any difference (the initial value is never changed so even if it wasn't a pure reflected property I should get the same result). I tried it anyway, but got the same result. – Morvael Nov 14 '18 at 08:28
0

I eventually got back to this issue when it became critical to fix it. It turned out that my (homegrown) SPA framework was the issue as it overrides the browser default click events of <a />

I fixed this by adding in a catch for links that are blobs, and then firing the default click event:

// convert all a/href to a#href
$("body").delegate("a", "click", function () {
    let href: string = $(this).attr("href"); 

    // check its not an external / absolute URL
    let regex: RegExp = new RegExp("^(blob:)?(http|https)(:\/\/)", "ig");
    let match = regex.exec(href);
    if (match) {
        // match[0] is the full match, match[1] is lookign for "blob:"
        // it will either be undefined or blob:
        if (match[1]) { // its a blob url, call the default
            return true; // !! this line is the core of the fix !!
        }
        else { // load a normal link
            // see if its got a target.
            let target: string = $(this).attr("target");

            switch (target) {
                default: document.location.href = href; break;
                case "_blank": window.open(href); break;
            }
        }   
    }
    else
        // SPA stuff
    return false;
});

So essentially a problem that no-one else would ever have had, unless they like me are foolish enough to role their own SPA framework.

Its here https://github.com/JohnRayson/JSPA

Morvael
  • 3,478
  • 3
  • 36
  • 53