0

I'm trying to deliver a zip archive in response to an AJAX POST request made from Axios to WebAPI.

On the client side I have

import AjaxDownload from "../../data/AjaxDownload";

AjaxDownload.post(id, pageRecords, {
            responseType: "blob"
        }).then((response) => {
            let blob = new Blob([response.data], { type: extractContentType(response) }),
                url = window.URL.createObjectURL(blob);    
            window.open(url, "_self");
        }).catch((error) => {
            // ...
        }).then(() => {
            // ...
        });

function extractContentType(response: AxiosResponse): string {
    return response.headers["content-type"] || "";
}

// AjaxDownload:
import * as axios from "axios";
import { apiUrl } from "./Ajax";

const ajax = axios.default.create({
    baseURL: new URL("download", apiUrl).toString(),
    timeout: 3600000    // 1 hour
});

export default ajax;

That posts to the following WebAPI method - and the POST part of that client-side logic works exactly as expected.

[HttpPost]
[Route("download/{id:guid}")]
public async Task<HttpResponseMessage> Download(Guid id, [FromBody] IEnumerable<PageRecord> pageRecords)
{
    var stream = await _repo.GetAsArchiveStream(id,
                                                pageRecords,
                                                true).ConfigureAwait(false);

    stream.Position = 0;

    var result = new HttpResponseMessage(HttpStatusCode.OK) {Content = new StreamContent(stream)};
    result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment") {FileName = $"{...}.zip"};
    result.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");    // "application/zip" has same result
    result.Content.Headers.ContentLength = stream.Length;

    return result;
}

However, the browser displays the result.Content as a JSON object, without the zip archive. I assume that the it's displaying as JSON because the request mentions JSON, but why does it appear to ignore the binary content - particularly as the Content-Type header details the type of content?

And as you can see, the JavaScript is also expecting to read in the content as a blob.

I don't see how my code differs meaningfully from this answer - please explain if there is a crucial difference.

On the server-side, I've also tried returning...

return new FileStreamResult(stream, "application/zip");

The problem with this approach is that there's no way to set a filename. Firefox does download the zip albeit with a random name while Chrome doesn't appear to download anything at all.

There must be a way to do this, right? To POST a request to a WebAPI method which returns a zip archive, and the client-side then presents a Save dialog? What am I missing?

awj
  • 7,482
  • 10
  • 66
  • 120
  • Quite strange to use POST for this. And what if file is big? You are now loading it into browser memory. Maybe better use regular GET request? – Evk Apr 10 '18 at 18:23
  • I have to use a POST because the client defines the files and the structure (from potentially a _very_ long list) which isn't suitable for a GET query-string. As for it taking time, that's not too much of a problem because of the way in which the UI explains this to the user and updates them. – awj Apr 11 '18 at 09:25
  • I still think returning url to real file from your POST request is better solution. Now you are using `Blob` to which you feed `response.data`, which means that 1) user can download it only when it has been completely downloaded from server and 2) the whole file is in browser memory. If you are fine with that and your file cannot be, say, larger than 500 MB - well, this should work. But if you try to download something like 1GB this way - I believe browser will just crash. – Evk Apr 11 '18 at 10:58

1 Answers1

0

I managed to solve this simply by returning the zip from the controller action using...

return File(stream,
            "application/zip",
            "FILENAME.zip");

And in the client-side code I can fetch the filename from the headers using some JavaScript found in this SO answer.

let blob = new Blob([response.data], { type: extractContentType(response) }),
                downloadUrl = window.URL.createObjectURL(blob),
                filename = "",
                disposition = response.headers["content-disposition"];

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

    if (matches != null && matches[1]) {
        filename = matches[1].replace(/['"]/g, '');
    }
}

var a = document.createElement("a");
// safari doesn't support this yet
if (typeof a.download === 'undefined') {
    window.location.href = downloadUrl;
} else {
    a.href = downloadUrl;
    a.download = filename;
    document.body.appendChild(a);
    a.click();
}
awj
  • 7,482
  • 10
  • 66
  • 120