Prompting a File download from a .NET API using Axios

ยท

3 min read

I recently needed to produce a small report and serve it as a file download via a .NET API endpoint. The endpoint would be invoked from a React app, using Axios, and trigger the file download in the browser.

As the server needed to produce the filename with a timestamp, it made sense to make use of the FileStreamResult to specify the content type and filename. What's great about FileStreamResult is that it can take streams as parameters and will automatically include the Content-Disposition header in the response.

Important: It is rarely a good idea to pass byte arrays around in a system. If you know the expected size of the bytes (like a key being used in asymmetric encryption), then that's ok. But any indeterminate length of data should be handled as a Stream.

    [HttpGet("todo/{id}/download")]
    public IActionResult Download(Guid id, CancellationToken cancellationToken)
    {
        var text = $"hello world - {id}";
        var fileName = "example.txt";
        var contentType = "text/plain";
        var fileBytes = System.Text.Encoding.UTF8.GetBytes(text);
        return File(fileBytes, contentType, fileName);        
    }

I could easily test the endpoint in my browser using Swagger; the browser saw the response as an attachment and downloaded the text file as expected.

But when I tried to implement this using Axios, I ran into problems:

  • Invoking the endpoint using Axios, would not trigger the browser's download dialogue.

  • Even though I received a response from the server using Axios, the Content-Disposition header was missing.

What's going on here? ๐Ÿค”

Triggering the download using Axios

Credit to this Medium article, but in essence: making a request via XHR (which Axios, fetch, etc. abstracts over), tells the browser that the response should be handled by JavaScript code. So we need to trigger the download using JS and a quick search reveals this rather popular gist below:

Including the Content-Disposition Header in the AxiosResponse

This part was confusing; FileStreamResult was including the header in the response, verifiable in the browser and the Network tab of my browser's DevTools. Why was it missing from the AxiosResponse?

Turns out this was a CORS issue.

When a browser makes a cross-origin request, it first sends an HTTP OPTIONS request to the server to check if the server allows the request. If the server allows the request, it sends a response with an Access-Control-Allow-Origin header to indicate which origins are allowed to access the resource.

The Access-Control-Expose-Headers header is used to specify which headers in the response can be exposed to the client, beyond the simple response headers that are automatically exposed

I needed to update the .NET WebApi's CORS settings

        services.AddCors(o => o.AddPolicy("DefaultPolicy", builder =>
        {
            builder.AllowAnyOrigin()
                   .AllowAnyMethod()
                   .AllowAnyHeader()
                   .WithExposedHeaders("Content-Disposition");
        }));

Now that the header was being exposed to the client, it appeared in the Axios response object.

And with some error checking and some filename assembly, we can now trigger the download in the browser with the filename supplied by the server.

        const response = await axios({
                method:'GET',
                url:url,
                responseType:'blob'
        })

        if (response.status === 200) {
            const fname = response.headers['content-disposition'].split('filename=')[1].split('.')[0];
            const ext = response.headers['content-disposition'].split('.')[1].split(';')[0];
            const filename = `${fname.replace(`"`,``)}.${ext.replace(`"`,``)}`;

            const href = URL.createObjectURL(response.data);
            // create "a" HTML element with href to file & click
            const link = document.createElement('a');
            try {
                link.href = href;
                link.setAttribute('download', filename);
                document.body.appendChild(link);
                link.click();
            }
            finally {
                // clean up "a" element & remove ObjectURL
                document.body.removeChild(link);
                URL.revokeObjectURL(href);
            }

        }

Happy downloading!

ย