1

I have an Angular 4 application which consumes an Asp.Net Web Api, and I want the Api to return a binary file. The Api route seems to be working correctly - I tested it using a rest console and the response is as expected. However, when trying to use the same route in the Angular app, the request sends but returns an error. I can see with the C# debugger that the request is executing completely and doesn't fail on the server. Here's the error in the JS console:enter image description here

This error occurs on all browsers tested (Chrome, Firefox, IE, Edge, Safari).

Here's the server side code:

[Route("api/getfile")]
public IHttpActionResult GetFile()
{
    byte[] file = <file generator code>;
    System.Net.Http.HttpResponseMessage responseMessage = new System.Net.Http.HttpResponseMessage
    {
        Content = new System.Net.Http.StreamContent(new System.IO.MemoryStream(file))
    };
    responseMessage.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream");
    responseMessage.Content.Headers.ContentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment");
    responseMessage.Content.Headers.ContentDisposition.FileName = "file.pdf";
    return this.ResponseMessage(responseMessage);
}

And here's the Angular code:

let headers = new Headers({
  "Accept": "application/octet-stream",
  "X-Requested-With": "XMLHttpRequest",
  "Authorization": `Bearer ${token}`
});
let opts = new RequestOptions({headers = headers});
opts.responseType = ResponseContentType.Blob;
// Uses old @angular/http, not HttpClient
this.http
  .get(`${apiUrl}/getfile`, opts)
  .map(res => res.blob())
  .catch(err => handleError(err));

EDIT: I tried using a plain XMLHttpRequest instead of Angular's Http service and it works. What do I need to do to get this to work with Angular?

EDIT 2: It works if I fetch an actual file on the file system that's accessible using the same host that the Angular app is running on. The Angular app is on localhost:8080, while the api is on a different port. If I expose a file on localhost:8080 (e.g., in the build folder) than I can fetch that file. This makes me wonder if it's a security issue, or maybe has to do with the headers or the way Web Api returns the binary data.

Shlomo Zalman Heigh
  • 3,968
  • 8
  • 41
  • 71
  • Have you tried ResponseContentType.ArrayBuffer ? – RandomUs1r Jul 19 '18 at 21:58
  • @RandomUs1r Yes, same error – Shlomo Zalman Heigh Jul 19 '18 at 21:59
  • try to rewrite `let opts = new RequestOptions({headers = headers});` to `let opts = new RequestOptions({headers, responseType: 'blob'});` – Andriy Jul 24 '18 at 20:15
  • 1. It looks like your request might be reporting progress and you are treating the progress events as the content. The observable looks like it's returning a progress event that you are then calling `blob()` on in your `map` function. Set [reportProgress](https://angular.io/api/common/http/HttpRequest#reportProgress) to false. 2. If you **are** trying to report progress, you need to return content-length (that's why `lengthComputable` is false) and keep an eye on those progress events. Look at [this answer](https://stackoverflow.com/a/45222419/3718246) for details. – Dean Jul 26 '18 at 01:56

3 Answers3

0

On your Api that will return your PDF

  FileContentResult result;
  if(!string.IsNullOrEmpty(fileName))
  {
     string absoluteFileName = Path.Combine(pathToFile, fileName);
     byte[] fileContents = System.IO.File.ReadAllBytes(absoluteFileName);
     result = new FileContentResult(fileContents, "application/pdf");
  }

And then on Angular:

 downloadFile(api: string) {
    window.open(this.endPoint + api);
  }

Try the old Way:

            FileInfo  fileInfo = New FileInfo(filePath)         
        Response.Clear()
        Response.ClearHeaders()
        Response.ClearContent()
        Response.AddHeader("Content-Disposition", "attachment;filename=" + fileInfo.Name)
        Response.AddHeader("Content-Type",  "application/pdf")
        Response.ContentType = "application/pdf"
        Response.AddHeader("Content-Length", fileInfo.Length.ToString())
        Response.TransmitFile(fileInfo.FullName)
        Response.Flush()
        Response.End()
CREM
  • 1,929
  • 1
  • 25
  • 35
  • I'm not using MVC. Can I use FileContentResult? – Shlomo Zalman Heigh Jul 24 '18 at 19:43
  • Are you on WebAPI 2 or 1 ?. I think from WeBAPI 2 is available IHttpActionResult – CREM Jul 24 '18 at 19:56
  • I'm using WebApi 2, with an ASP.net Ajax web application, .NET 4.5 – Shlomo Zalman Heigh Jul 24 '18 at 19:57
  • The last piece of code should produce your pdf the right way – CREM Jul 24 '18 at 20:10
  • All depends on the libraries you have available on your project – CREM Jul 24 '18 at 20:12
  • When I use the old way (accessing the Response directly), it does work. The problem is that then it isn't going through Owin like the rest of the Api, so it doesn't add the necessary CORS headers (like "Access-Control-Allow-Origin"). Any way to edit the response body directly and still have it go through the normal processing? – Shlomo Zalman Heigh Jul 24 '18 at 20:23
  • Try to add Response.AddHeader("Access-Control-Allow-Origin", "*") I think if the API allows AnyOrigine then it's not a problem. On NuGet Packages should be a library to allow CORS – CREM Jul 24 '18 at 20:36
0

This is for an image that I was keeping in a blob column in a db, but process should be similar. I ended up doing something like this back in Angular 4 (although this was 4.3+ which means HttpClient, not Http) to handle downloading files on clicking a button:

public downloadImage(id: number, imageName: string, imageType: string) {

    this.http.get(urlToApiHere, { responseType: 'blob' }).subscribe((image: Blob) => {
      if (isPlatformBrowser(this.platformId)) {
        let a = window.document.createElement("a");
        document.body.appendChild(a);
        let blobUrl = window.URL.createObjectURL(image);
        a.href = blobUrl;
        a.download = imageName;
        a.click();
        window.URL.revokeObjectURL(blobUrl);
        document.body.removeChild(a);
      }
    })
  }

This API is .Net Core, but should be similar in .Net MVC, I believe:

[HttpGet]
public FileResult DisplayLineItemImage(int lineItemID)
{
  using (var db = new Context())
  {
    var image = //retrieve blob binary, type, and filename here
    if (image.Image == null)
    {
      //throw error
    }
    return File(image.Image, image.Type, image.Name);
  }
}
yanbu
  • 183
  • 8
0

The second answer by Crying Freeman, using Response directly, does work, but it bypasses Owin's processing and would mean having to manually implement things like CORS or anything else normally handled using CORS.

I found another solution, to use a custom formatter to allow returning a byte array from the controller method. This is also nicer because I don't need to set any headers manually, not even Content-Type.

Shlomo Zalman Heigh
  • 3,968
  • 8
  • 41
  • 71