Download File with jQuery and Web API 2.0 IHttpActionResult
Sep 19, 2018
I came across a need to download a file through an AJAX request to a Web API 2.0 endpoint. A quick search revealed a fairly straightforward way to tackle this problem, which is to implement a custom IHttpActionResult
 (available in Web API 2.0) that sets up the HttpResponse in a way that can be digested by the client.
Create a Custom Web API 2.0 IHttpActionResult
The first step was to implement the IHttpActionResult
 as outlined below.
public class ApiFileResult : IHttpActionResult {
public ApiFileResult(string filePath, string contentType, string fileName = null) {
if (string.IsNullOrWhiteSpace(filePath)) {
throw new ArgumentNullException(nameof(filePath));
}
this.contentType = contentType ?? MimeMapping.GetMimeMapping(Path.GetExtension(filePath));
var fileStream = File.OpenRead(filePath);
var fileBytes = new byte[fileStream.Length];
fileStream.Read(fileBytes, 0, fileBytes.Length);
stream = new MemoryStream(fileBytes);
this.fileName = fileName ?? Path.GetFileName(filePath);
}
public ApiFileResult(MemoryStream stream, string contentType, string fileName) {
if (stream == null) {
throw new ArgumentNullException(nameof(stream));
}
if (contentType == null) {
throw new ArgumentNullException(nameof(contentType));
}
this.stream = stream;
this.contentType = contentType;
this.fileName = fileName;
}
private readonly string contentType;
private readonly string fileName;
private readonly MemoryStream stream;
public Task ExecuteAsync(CancellationToken cancellationToken) {
var bytes = stream.ToArray();
var memoryStream = new MemoryStream(bytes);
var response = new HttpResponseMessage(HttpStatusCode.OK) {
Content = new StreamContent(memoryStream)
};
response.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType);
response.Content.Headers.ContentLength = memoryStream.Length;
response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment") {
FileName = fileName
};
return Task.FromResult(response);
}
}
Once that's in place we can use the new IHttpActionResult
 as our API's return.
Create an ApiController Action That Returns Custom IHttpActionResult
Next up is creating an instance of this new custom Web API 2.0 IHttpActionResult class and using it as our API's return type. Below is a fairly lightweight ApiController to demonstrate just that. The injected IBillingExportService
 class is used to generate a memory stream containing the contents of the file. We then new-up an instance of our custom ApiFileResult
 object, passing the file stream, file type and file name through.
[RoutePrefix("api/v1/Billing")]
public class BillingApiController : ApiController {
public BillingApiController(IBillingExportService service) {
this.service = service;
}
private readonly IBillingExportService service;
[HttpPost, Route("Export", Name = "BillingExport")]
public async Task GetBillingExport(int id) {
var exportStream = await service.GetExportFile(id);
return new ApiFileResult(exportStream,
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
$"Billing Export {DateTime.Today:yyyy-MM-dd}{ExcelExport.WorkbookBuilder.ExcelFileExtension}");
}
}
Now that we have the API endpoint in place we can make a jQuery AJAX request to it, as follows:
$.ajax({
type: 'POST',
cache: false,
url: exportScheduledBillingUrl,
data: serializeScheduledBillingGrid(),
xhrFields: {
// make sure the response knows we're expecting a binary type in return.
// this is important, without it the excel file is marked corrupted.
responseType: 'arraybuffer'
}
})
.done(function (data, status, xmlHeaderRequest) {
var downloadLink = document.createElement('a');
var blob = new Blob([data],
{
type: xmlHeaderRequest.getResponseHeader('Content-Type')
});
var url = window.URL || window.webkitURL;
var downloadUrl = url.createObjectURL(blob);
var fileName = '';
// get the file name from the content disposition
var disposition = xmlHttpRequest.getResponseHeader('Content-Disposition');
if (disposition && disposition.indexOf('attachment') !== -1) {
var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
var matches = filenameRegex.exec(disposition);
if (matches != null && matches[1]) {
fileName = matches[1].replace(/['"]/g, '');
}
}
// Blob download logic taken from: https://stackoverflow.com/questions/16086162/handle-file-download-from-ajax-post
if (typeof window.navigator.msSaveBlob !== 'undefined') {
// IE workaround for "HTML7007" and "Access Denied" error.
window.navigator.msSaveBlob(blob, fileName);
} else {
if (fileName) {
if (typeof downloadLink.download === 'undefined') {
window.location = downloadUrl;
} else {
downloadLink.href = downloadUrl;
downloadLink.download = fileName;
document.body.appendChild(downloadLink);
downloadLink.click();
}
} else {
window.location = downloadUrl;
}
setTimeout(function () {
url.revokeObjectURL(downloadUrl);
},
100);
}
});
When the above Ajax call is triggered (i.e. a user clicks a button on your page) it'll return the new custom IHttpActionResult
 you created. We then use the information provided by that result to build a Blob object. This Blob object enables us to generate an object URL which can be used to download the contents of our file.
Comments
Leave a Comment
Terry
November 22, 2020 at 9:34:06 AM UTC
If you need to return an error/failure from the server, how do you do that?