How to get the filename from a file downloaded using JavaScript Fetch API?
Asked Answered
M

3

36

In my Javascript client, I'm using Fetch API to call the server to retrieve a server-generated file. I'm using the following client-side code:

var _url = "";    
var initParms = {  
   method: "GET",
   mode: 'cors'
}

fetch(_url, initParms)
.then(response => {
   if(response.ok){
      alert(response.headers.get("content-disposition"));
      return response.blob();
   }

   throw new Error("Network response was not OK.");
})
.then(blob => {
   var url = new URL.createObjectURL(blob);
})     

This actually works just fine. However, the server generates a filename for the file and includes it in the response as part of the content-disposition header.

I need to save this file to the user's machine using the filename generated by the server. In Postman, I can actually see that the content-disposition header of the response is set to: Content-Disposition: attachment;filename=myfilename.txt.

I made an attempt to read the content-disposition from the response (see the alert in my JS code), but I always get null (even though the same response shows the content-disposition in Postman).

Am I doing something wrong? Is there a way to retrieve the filename using the fetch response? Is there a better way to get the filename from the server along with the file?

P.S. This is my server-side code for returning the file:

Controller Action

public IHttpActionResult GetFile(){
   return new FileResult("myfilename.txt","Hello World!");
}

FileResult Class

public class FileResult : IHttpActionResult
{
   private string _fileText = "";
   private string _fileName = "";
   private string _contentType = "";

   public FileResult(string name, string text)
   {
       _fileText = text;
       _fileName = name;
       _contentType = "text/plain";
   }

   public Task<HttpResponseMessage> ExecuteActionAsync(CancellationToken token)
   {
        Stream _stream = null;
        if (_contentType == "text/plain")
        {
            var bytes = Encoding.Unicode.GetBytes(_fileText);
            _stream = new MemoryStream(bytes);
        }
        return Task.Run(() =>
        {
            var response = new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new StreamContent(_stream),
            };

            response.Content.Headers.ContentType = 
                new MediaTypeHeaderValue(_contentType);
            response.Content.Headers.ContentDisposition = 
                new ContentDispositionHeaderValue("attachment")
            {
                FileName = _fileName
            };

            return response;

        }, token);

Edit

My question was specifically about the fetch not the ajax api. Also, in my code, I showed that I was already reading the header from the response exactly like the accepted answer demonstrated on the suggested answer. However, as stated in my post, this solution was not working with fetch.

Moia answered 14/3, 2018 at 19:28 Comment(2)
Possible duplicate of How to download a file via URL then get its namePouncey
Difference is I'm asking about the fetch api not ajax api. Also note that I'm already doing what was suggested in the accepted answer but it is not working.Moia
M
48

So, shortly after posting this question, I ran across this issue on Github. It apparently has to do with using CORS.

The suggested work around was adding Access-Control-Expose-Headers:Content-Disposition to the response header on the server.

This worked!

Moia answered 14/3, 2018 at 19:41 Comment(0)
R
17

You can extract the filename from the content-disposition header like this:

let filename = '';

fetch(`/url`, { headers: { Authorization: `Bearer ${token}` }}).then((res) => {
    const header = res.headers.get('Content-Disposition');
    const parts = header!.split(';');
    filename = parts[1].split('=')[1];
    return res.blob();
}).then((blob) => {
    // Use `filename` here e.g. with file-saver:
    // saveAs(blob, filename);
});
Rondeau answered 10/6, 2021 at 8:6 Comment(6)
It worked for me. But (as a non expert in JS), I am wondering : why the two following "Then" promises, the first with the response, the seconde with the blob, when it would probably be easier to process everything in a single "Then" ? Thank youInheritor
@Inheritor The first promise sends HTTP request to the server and waits for the response. Of the received response only HTTP head is read. It lets you make up a decision (based on headers and response status) if you want to read the HHTP body. And if yes - then what specific reader you will use for it (e.g. text, json, blob). Second promise is for reading the body.Adaadabel
@Adaadabel I understand the intend, thank you. I was assuming that, as i know the API and why I called it, I don't have such wonderings, I just wanted to get my content ;)Inheritor
@Inheritor I assume that, since Fetch API is a built-in browser tool, it is intentionally more low-level. To let custom implementations adapt it to a specific use case. In project where I participate, for example, we write a simple wrapper function which does a response status check, some error handling and reads the body. And this function returns a single promise to the calling code, as you say. Most likely W3C decided that it is easier to let developers write such custom function than cover all possible cases in the API. But maybe someone from MDN could comment on it - would be great!Adaadabel
I believe that header! is not valid JS (probably TypeScript?).Attached
Just a note that if the filename contains spaces, then the value of the filename variable will have double quotes at the beginning and end of the string. If you then try to save the file with this filename, those double quotes get converted to underscores. You need to do an additional check to see whether the first and last character of the filename are double quotes, and remove them if so.Ego
M
16

Decided to post this, as the accepted answer (although helpful to many people) does not actually answer the original question as to:

"How to get the filename from a file downloaded using JavaScript Fetch API?".

One can read the filename (as shown in the example below), and download the file using a similar approach to this (the recommended downloadjs library in that post is no longer being maintained; hence, I wouldn't suggest using it). The below also takes into account scenarios where the filename includes unicode characters (i.e.,-, !, (, ), etc.) and hence, comes (utf-8 encoded) in the form of, for instance, filename*=utf-8''Na%C3%AFve%20file.txt (see here for more details). In such cases, the decodeURIComponent() function is used to decode the filename.

Working Example

const url ='http://127.0.0.1:8000/'
fetch(url)
    .then(res => {
        const disposition = res.headers.get('Content-Disposition');
        filename = disposition.split(/;(.+)/)[1].split(/=(.+)/)[1];
        if (filename.toLowerCase().startsWith("utf-8''"))
            filename = decodeURIComponent(filename.replace(/utf-8''/i, ''));
        else
            filename = filename.replace(/['"]/g, '');
        return res.blob();
    })
    .then(blob => {
        var url = window.URL.createObjectURL(blob);
        var a = document.createElement('a');
        a.href = url;
        a.download = filename;
        document.body.appendChild(a); // append the element to the dom
        a.click();
        a.remove(); // afterwards, remove the element  
    });

If you are doing a cross-origin request, make sure to set the Access-Control-Expose-Headers response header on server side, in order to expose the Content-Disposition header; otherwise, the filename won't be accessible on client side trhough JavaScript (see furhter documentation here). For instance (taken from this answer):

headers = {'Access-Control-Expose-Headers': 'Content-Disposition'}
return FileResponse("Naïve file.txt", filename="Naïve file.txt", headers=headers)
Madelon answered 20/2, 2022 at 15:31 Comment(3)
The check if "UTF-8" is present is case insensitive, but filename.replace("utf-8''", '') only works with lowercase "utf-8". I used filename.substring(7) instead.Newhall
@MoritzRingler Thanks. It is fixed now, using the ignore case flag,Madelon
how can I use this with post method? I need to transmit body and paramContinuation

© 2022 - 2024 — McMap. All rights reserved.