0

I am running into an issue trying to implement the download of spreadsheets served by my Spring Boot-based server through my Angular (v11) web app. My app offers some 'larger' spreadsheets for download (~32 MB), and the browser is sometimes crashing while attempting to download those files.

Here's the basics of my code - first, the RESTful client in my Angular app:

export class RestService {
    
    private url: string = "http://example.com/api/dowwnload";
    constructor(private http: HttpClient) {}

    requestFile(id: number): Observable<HttpResponse<any>> { 
        this.http.get<any>(url+"?id="+id, {observe: 'response', responseType: 'blob' as 'json'});
    }
}

...and the code that is trying to do the downloading (using file-saver):

this.restService.requestFile(id).subscribe(res => {
   // requires: import {saveAs} from 'file-saver';
   saveAs(res.body, 'filename.xlxs');
});

Here is the endpoint on the server side:

@GetMapping("/download")
public ResponseEntity<Resource> downloadFile(@RequestParam("id") int id) { 

    File out = exportFileToTmpById(id); // method that writes file to /tmp here 
    try { 
        FileSystemResource resource = new FileSystemResource(out);
        
        HttpHeaders headers = new HttpHeaders();
        headers.set(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + out.getName() + "\"");
        return ResponseEntity.ok()
            .headers(httpHeaders)
            .contentLength(out.length())
            .contentType(MediaType.APPLICATION_OCTET_STREAM)
            .body(resource);
    }
    catch (Exception e) { 
        e.printStackTrace();
    }

}

My intuition is that there is either a memory issue or a race condition occurring on the Angular side, but I am not an expert on any of this stuff (#programmingbystackoverflow). The browser (Chrome & Firefox) dies with no error message, and no relevant messages are written to Chrome's log. Should I be removing the child "a" element after the click?

Please let me know if you need any more details in order to help me solve this problem.

  • 1
    Actually, without errors or reproducable example on stackblitz it will be hard to guess where the failing is. – onrails Aug 11 '22 at 13:47
  • However, I see that you are missing something exremely important when it comes to client side security. You are not Sanitizing the DOM while downloading the files read here in the docs on DOMSanitizer to prevent XSS attacks: https://angular.io/api/platform-browser/DomSanitizer – onrails Aug 11 '22 at 13:51
  • Suggestion noted, @onrails - thanks. I'm just trying to get the basics working first. :) – James A Cubeta Aug 11 '22 at 14:08
  • 1
    I guess the problem lies with the new Blob object you are constructing. Since you are already setting the responseType to blob, it is unnecessary to construct it again. It will load all the content into memory before initiating download. In addition to that you can refer following post as well - https://stackoverflow.com/questions/42543448/download-large-size-files-with-angular-file-saver – CuriousMind Aug 11 '22 at 14:09
  • @CuriousMind - thank you. I moved to using FileSaver, greatly simplifying my code (should I update my code above?) and now reference the response body directly vs creating a new Blob, but still experiencing the problem. – James A Cubeta Aug 11 '22 at 15:10
  • Yes please. Also I believe you have removed the `new Blob` statement. – CuriousMind Aug 12 '22 at 06:14
  • @CuriousMind I've updated my code example to reflect what I'm doing now. I'm still experiencing the issue. :/ – James A Cubeta Aug 12 '22 at 10:52
  • 1
    @JamesACubeta Thats bit strange. With the new code, it shouldn't load the data in client memory but directly stream it. Can you confirm what happens when you invoke this api using Postman? Also, as an alternative, can you skip the call to HttpClient and instead directly construct anchor tag with link to your Spring Rest API, invoke click event on it and then remove the anchor tag. (This approach will fail, if you expect any authorization parameter to be set). Try this and see if it is able to download the file. – CuriousMind Aug 12 '22 at 11:50
  • 1
    @CuriousMind Unfortunately, I don't have access to a tool like Postman. Your suggestion seems to be the solution! I created an anchor tag direct to the REST service endpoint, and it seems to be totally stable. I have noticed that it seems to 'pause' my app while trying to write the file, but that's not too bad vs a browser crash. Thank you very much for the suggestion! – James A Cubeta Aug 12 '22 at 14:10

1 Answers1

0

Thanks to @CuriousMind, removing my REST call via HttpClient and invoking the endpoint directly using an anchor tag works great for my purposes:

const a = document.createElement('a');
a.setAttribute('display', 'none');
a.href = downloadServiceUrl+"?id="+id";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);

Not sure why this works, but it does.

Also, I've noticed that the browser/app momentarily freezes on downloads of the larger files. Perhaps a web worker would alleviate this side effect.

  • Awesome. In my opinion, the problem seems to be something else. Web workers will not solve this problem. The possible culprits could be an antivirus, a browser extension or you are running an application in localhost which might be consuming memory. But again these are all guesses. – CuriousMind Aug 13 '22 at 08:51