346

I'm working on a web service using ASP.NET MVC's new WebAPI that will serve up binary files, mostly .cab and .exe files.

The following controller method seems to work, meaning that it returns a file, but it's setting the content type to application/json:

public HttpResponseMessage<Stream> Post(string version, string environment, string filetype)
{
    var path = @"C:\Temp\test.exe";
    var stream = new FileStream(path, FileMode.Open);
    return new HttpResponseMessage<Stream>(stream, new MediaTypeHeaderValue("application/octet-stream"));
}

Is there a better way to do this?

Josh Earl
  • 18,151
  • 15
  • 62
  • 91
  • 4
    Anyone who lands wanting to return a byte array via stream via web api and IHTTPActionResult then see here: http://nodogmablog.bryanhogan.net/2017/02/downloading-an-inmemory-file-using-web-api-2/ – IbrarMumtaz Nov 07 '17 at 13:02
  • // using System.IO; // using System.Net.Http; // using System.Net.Http.Headers; public HttpResponseMessage Post(string version, string environment, string filetype) { var path = @"C:\Temp\test.exe"; HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK); var stream = new FileStream(path, FileMode.Open, FileAccess.Read); result.Content = new StreamContent(stream); result.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); return result; } –  Aug 03 '21 at 16:12

8 Answers8

543

Try using a simple HttpResponseMessage with its Content property set to a StreamContent:

// using System.IO;
// using System.Net.Http;
// using System.Net.Http.Headers;

public HttpResponseMessage Post(string version, string environment,
    string filetype)
{
    var path = @"C:\Temp\test.exe";
    HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK);
    var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
    result.Content = new StreamContent(stream);
    result.Content.Headers.ContentType = 
        new MediaTypeHeaderValue("application/octet-stream");
    return result;
}

A few things to note about the stream used:

  • You must not call stream.Dispose(), since Web API still needs to be able to access it when it processes the controller method's result to send data back to the client. Therefore, do not use a using (var stream = …) block. Web API will dispose the stream for you.

  • Make sure that the stream has its current position set to 0 (i.e. the beginning of the stream's data). In the above example, this is a given since you've only just opened the file. However, in other scenarios (such as when you first write some binary data to a MemoryStream), make sure to stream.Seek(0, SeekOrigin.Begin); or set stream.Position = 0;

  • With file streams, explicitly specifying FileAccess.Read permission can help prevent access rights issues on web servers; IIS application pool accounts are often given only read / list / execute access rights to the wwwroot.

Hakan Fıstık
  • 16,800
  • 14
  • 110
  • 131
carlosfigueira
  • 85,035
  • 14
  • 131
  • 171
  • 41
    Would you happen to know when the stream gets closed? I am assuming the framework ultimately calls HttpResponseMessage.Dispose(), which in turn calls HttpResponseMessage.Content.Dispose() effectively closing the stream. – Steve Guidi Apr 20 '12 at 18:36
  • 46
    Steve - you're correct and I verified by adding a breakpoint to FileStream.Dispose and running this code. The framework calls HttpResponseMessage.Dispose, which calls StreamContent.Dispose, which calls FileStream.Dispose. – Dan Gartner Aug 22 '12 at 20:02
  • 19
    You can't really add a `using` to either the result (`HttpResponseMessage`) or the stream itself, since they'll still be used outside the method. As @Dan mentioned, they're disposed by the framework after it's done sending the response to the client. – carlosfigueira Jun 19 '13 at 16:18
  • 1
    @Drew Noakes will the above code works for downloading zip file – GowthamanSS Jan 13 '14 at 10:27
  • 1
    @GowthamanSS, sure. You may like to use the MIME type for ZIP files though. – Drew Noakes Jan 13 '14 at 10:48
  • 1
    @GowthamanSS, I suggest you ask a new question rather than seek help in the comments on an existing question! – Drew Noakes Jan 13 '14 at 14:14
  • @DrewNoakes check this http://stackoverflow.com/questions/21094165/downloading-zip-file-from-controller-in-asp-net-web-api – GowthamanSS Jan 13 '14 at 14:50
  • Besides setting the async option in the FileStream constructor, is this approach as efficient as it could be in terms of asynchronous reading from the file stream? Does the Web API infrastructure do it properly? – Gabriel S. Feb 07 '14 at 08:31
  • How does the other end of this work? How does the client receive the returned result? Is it simply a matter of assigning what's returned to a filestream and then saving that file to disk? – B. Clay Shannon-B. Crow Raven Feb 19 '14 at 00:29
  • 2
    @B.ClayShannon yes, that's about it. As far as the client is concerned it's just a bunch of bytes in the content of the HTTP response. The client can do with those bytes whatever they choose, including saving it to a local file. – carlosfigueira Feb 20 '14 at 14:53
  • 5
    @carlosfigueira, hi, do you know how to delete the file after the bytes are all sent? – Zach Dec 12 '14 at 09:06
  • 1
    You can implement your own Stream class that wraps the file being streamed, and when it's done you can close the file stream and then delete the file. – carlosfigueira Dec 12 '14 at 19:26
  • 2
    Try also adding the line -> result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment") { FileName = "test.exe" }; – Peter Morris Aug 12 '15 at 10:38
  • 1
    To avoid content negotiation this method doesn't use `Request.CreateResponse()`: https://stackoverflow.com/questions/22060357 – Bart Verkoeijen Jun 14 '17 at 09:49
  • If something breaks during processing, what should be returned from Post method? – joym8 Apr 26 '19 at 18:56
  • This corrupts binary files by (seemingly) interpreting them as text and doing some kind of encoding conversion. See https://stackoverflow.com/questions/41200744/issue-with-returning-httpresponsemessage-as-excel-file-in-webapi – Florian Winter Oct 08 '19 at 13:10
  • 1
    CORRECTION: Actually, this returns the correct binary data, but web browsers (at least Firefox) corrupt the file when they save it to disk! – Florian Winter Oct 08 '19 at 13:26
  • This doesn't work for me. On the server-side I am returning 40 bytes, but on the client-side it picks up 300 bytes in the content. It's as though the headers etc. are being included in the content. – Christian Findlay Dec 20 '19 at 10:40
  • When I implement the code above, and point it at a random excel file (even put the file inside my project directory structure), I get the headers in JSON format in the browser but no file. I even calculate the MD5 hash and put that into a header (which comes through), but still no file. Any thoughts would be helpful. – Gineer Jan 18 '20 at 06:29
  • You can close the file stream deterministically using RegisterForDispose() - see [this post](https://www.strathweb.com/2015/08/disposing-resources-at-the-end-of-web-api-request/) for details – Paul Keister Jan 06 '21 at 16:17
  • Will the stream remain open until either it times out or is closed? i.e. can you show the start of an html page with a progress bar, generate an html report updating the progress as it is generated, then send the final report after it generates? – Brain2000 Dec 16 '21 at 20:10
145

For Web API 2, you can implement IHttpActionResult. Here's mine:

using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;

class FileResult : IHttpActionResult
{
    private readonly string _filePath;
    private readonly string _contentType;

    public FileResult(string filePath, string contentType = null)
    {
        if (filePath == null) throw new ArgumentNullException("filePath");

        _filePath = filePath;
        _contentType = contentType;
    }

    public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
    {
        var response = new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StreamContent(File.OpenRead(_filePath))
        };

        var contentType = _contentType ?? MimeMapping.GetMimeMapping(Path.GetExtension(_filePath));
        response.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType);

        return Task.FromResult(response);
    }
}

Then something like this in your controller:

[Route("Images/{*imagePath}")]
public IHttpActionResult GetImage(string imagePath)
{
    var serverPath = Path.Combine(_rootPath, imagePath);
    var fileInfo = new FileInfo(serverPath);

    return !fileInfo.Exists
        ? (IHttpActionResult) NotFound()
        : new FileResult(fileInfo.FullName);
}

And here's one way you can tell IIS to ignore requests with an extension so that the request will make it to the controller:

<!-- web.config -->
<system.webServer>
  <modules runAllManagedModulesForAllRequests="true"/>
Ronnie Overby
  • 45,287
  • 73
  • 267
  • 346
  • @RonnieOverby: What does this mean: "tell IIS to ignore requests with an extension so that the request will make it to the controller" What sort of request has an extension, and how would having an extension potentially cause it to not make it to the Controller? – B. Clay Shannon-B. Crow Raven Feb 19 '14 at 00:24
  • It means that the web server must be instructed to treat requests that look like static asset requests differently than it normally does. – Ronnie Overby Feb 19 '14 at 01:31
  • 1
    Nice answer, not always SO code runs just after pasting and for different cases (different files). – Krzysztof Morcinek Nov 20 '14 at 12:33
  • According to [Stephen Cleary](http://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-dont-use.html) and answers here in SO like in [this example](http://stackoverflow.com/questions/24296083/should-i-wrap-slow-calls-with-task-run) **you should avoid using Task.Run in Asp.Net applications**. Try using Task.FromResult instead. – Jony Adamit Mar 24 '15 at 13:47
  • 1
    @JonyAdamit Thanks. I think another option is to place an `async` modifier on the method signature and remove the creation of a task altogether: https://gist.github.com/ronnieoverby/ae0982c7832c531a9022 – Ronnie Overby Mar 24 '15 at 20:49
  • I think both methods are similar. They both run synchronously. Except the method you suggested generates a warning while the other doesn't. In any way, thanks for the code snippet - already adapted it in my project :) – Jony Adamit Mar 25 '15 at 08:39
  • 4
    Just a heads up for anyone coming over this running IIS7+. runAllManagedModulesForAllRequests can now be [omitted](https://www.iis.net/configreference/system.webserver/modules). – Index Oct 09 '15 at 12:41
  • Note, you should gracefully handle `FileNotFoundException` from `File.Open`. It is a race condition to merely check if a file exists before opening the file. – Cory Nelson Nov 23 '15 at 17:15
  • Couldn't someone just pass in a path like "../../../../allMyPasswords.txt" and get to a file they shouldn't using this?!?! – adam0101 Jul 14 '16 at 21:25
  • @adam0101 That's what ACL's are for. – Ronnie Overby Jul 15 '16 at 11:46
  • Excellent approach. – Julian Corrêa Jul 29 '16 at 14:51
  • @RonnieOverby does the api take care of closing the file stream? – BendEg Aug 19 '16 at 08:17
  • 1
    @BendEg Seems like at one time I checked the source and it did. And it makes sense that it should. Not being able to control the source of the framework, any answer to this question could change over time. – Ronnie Overby Aug 19 '16 at 15:29
  • 2
    There's actually already a built in FileResult (and even FileStreamResult) class. – BrainSlugs83 Apr 12 '17 at 19:30
  • @BrainSlugs83 I think those built in types are for MVC controllers (derive from ActionResult). At the time I wrote this, we were talking about Web API 2, which was a different framework from MVC 5. Unless someone has come up with a way to convert an ActionResult to an IHttpActionResult, I don't think you can use the built in FileResult implementations. – Ronnie Overby Oct 22 '18 at 14:32
  • Sorry the limit is 3.99999999999999 GB – Ronnie Overby Nov 30 '18 at 16:44
  • @BrainSlugs83 is correct, you can easily `return FileContentResult(byteArray, @"application/octet-stream");` or `return new FileStreamResult(stream, @"application/octet-stream");` depending on your case. `Assembly Microsoft.AspNetCore.Mvc.Core, Version=5.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60` possibly in earlier versions as well. – Shaun Wilson Feb 07 '21 at 08:59
34

For those using .NET Core:

You can make use of the IActionResult interface in an API controller method, like so.

[HttpGet("GetReportData/{year}")]
public async Task<IActionResult> GetReportData(int year)
{
    // Render Excel document in memory and return as Byte[]
    Byte[] file = await this._reportDao.RenderReportAsExcel(year);

    return File(file, "application/vnd.openxmlformats", "fileName.xlsx");
}

This example is simplified, but should get the point across. In .NET Core this process is so much simpler than in previous versions of .NET - i.e. no setting response type, content, headers, etc.

Also, of course the MIME type for the file and the extension will depend on individual needs.

Reference: SO Post Answer by @NKosi

Uwe Keim
  • 39,551
  • 56
  • 175
  • 291
Kurtis Jungersen
  • 2,204
  • 1
  • 26
  • 31
  • 5
    Just a note, if it's an image and you want it to be viewable in a browser with direct URL access, then don't supply a filename. – Pluto May 16 '20 at 16:11
10

While the suggested solution works fine, there is another way to return a byte array from the controller, with response stream properly formatted :

  • In the request, set header "Accept: application/octet-stream".
  • Server-side, add a media type formatter to support this mime type.

Unfortunately, WebApi does not include any formatter for "application/octet-stream". There is an implementation here on GitHub: BinaryMediaTypeFormatter (there are minor adaptations to make it work for webapi 2, method signatures changed).

You can add this formatter into your global config :

HttpConfiguration config;
// ...
config.Formatters.Add(new BinaryMediaTypeFormatter(false));

WebApi should now use BinaryMediaTypeFormatter if the request specifies the correct Accept header.

I prefer this solution because an action controller returning byte[] is more comfortable to test. Though, the other solution allows you more control if you want to return another content-type than "application/octet-stream" (for example "image/gif").

Bernard Vander Beken
  • 4,848
  • 5
  • 54
  • 76
Eric Boumendil
  • 2,318
  • 1
  • 27
  • 32
9

For anyone having the problem of the API being called more than once while downloading a fairly large file using the method in the accepted answer, please set response buffering to true System.Web.HttpContext.Current.Response.Buffer = true;

This makes sure that the entire binary content is buffered on the server side before it is sent to the client. Otherwise you will see multiple request being sent to the controller and if you do not handle it properly, the file will become corrupt.

lionello
  • 499
  • 9
  • 11
JBA
  • 220
  • 2
  • 8
  • 3
    The `Buffer` property [has been deprecated](https://msdn.microsoft.com/en-us/library/system.web.httpresponse.buffer(v=vs.110).aspx) in favour of `BufferOutput`. It defaults to `true`. – decates Oct 06 '17 at 07:42
8

The overload that you're using sets the enumeration of serialization formatters. You need to specify the content type explicitly like:

httpResponseMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
David Peden
  • 17,596
  • 6
  • 52
  • 72
  • 4
    Thanks for the reply. I tried this out, and I'm still seeing `Content Type: application/json` in Fiddler. The `Content Type` appears to be set correctly if I break before returning the `httpResponseMessage` response. Any more ideas? – Josh Earl Mar 03 '12 at 14:34
5

You could try

httpResponseMessage.Content.Headers.Add("Content-Type", "application/octet-stream");
MickySmig
  • 239
  • 1
  • 6
  • 17
0

You can try the following code snippet

httpResponseMessage.Content.Headers.Add("Content-Type", "application/octet-stream");

Hope it will work for you.

Abdul Moeez
  • 1,331
  • 2
  • 13
  • 31