szab.kel
szab.kel

Reputation: 2536

Spring Boot Exception during download results in empty file

Reproduction of the error here. Please remove the comments before the exception throwing to see the problem. Path is: http://localhost:8080/api/download

Original Question: I am allowing users to download reports using JasperReports in an Angular7 App, but the problem is only connected to Spring Boot. The report generation works, but I can't get the file download to behave as I expected.

Currently: The download link is used with an href, link with target="_blank". The user clicks on it and the browser opens a new tab (in the background) and pops-up the File Save As window. If everything is okay, the file is saved without a problem. However if there is an exception during the PDF generation somewhere, the browser still pops-up the File Save As window and allows the user to save the file, it will complete, but the file will be 0 bytes.

Should be: When there is an exception, the browser should open a new tab with an error message of some sort, but If If there were no errors, it should display the File Save As window.

Code:

@GetMapping("/salary-report/{id}")
public void generateSalaryReport(@PathVariable("id") long salaryReportId, HttpServletResponse response) throws IOException, JRException, SQLException {
    JasperPrint jasperPrint;
    var salaryReport = salaryReportRepositoryEx.findById(salaryReportId).orElseThrow(ResourceNotFoundException::new);

    try (OutputStream out = response.getOutputStream()) {
        HashMap<String, Object> parameters = new HashMap<>();

        parameters.put("ReportId", salaryReport.getId());

        // Set meta data
        response.setContentType("application/x-download");
        response.setHeader(
            "Content-Disposition",
            String.format("attachment; filename=\"%s%s-report%s-%s-%s.pdf\"",
                .... parameters
            )
        );

        // Set report
        jasperPrint = salaryReportJasperReport.render(parameters); // exception usually here
        JasperExportManager.exportReportToPdfStream(jasperPrint, out);
    } catch (Exception e) {
        // I tried changing the content type on Exception, but the same
        response.setContentType("text/plain");
        response.setHeader("Content-Disposition", null);
        throw e;
    }
}

HTML Code with the link (Angular7):

<td>
    <a [href]="serverApiUrl+'/jasper/salary-report/'+salaryReport.id"
       target="_blank"
    >
        PDF Download
    </a>
</td>

Edit: If I just manually navigate to a full download URL, same thing happens.

Edit2: Tried it in postman too. Using an invalid report id returns the expected 404 Json (ResourceNotFoundException), but moving forward with the code always returns 200 OK (even when I manually set the HTTP Code to 500 in the catch block) and the body is empty.

Edit3: I tried using an exception handler:

@ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
public class JasperReportGenerationFailedException extends Exception {
    public JasperReportGenerationFailedException() {
        super();
    }

    public JasperReportGenerationFailedException(String message) {
        super(message);
    }

    public JasperReportGenerationFailedException(String message, Throwable cause) {
        super(message, cause);
    }

    public JasperReportGenerationFailedException(Throwable cause) {
        super(cause);
    }

    protected JasperReportGenerationFailedException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

 @ExceptionHandler(JasperReportGenerationFailedException.class )
    public ResponseEntity<String> handleJasperException(JasperReportGenerationFailedException ex) {
        log.error("Salary Report generation failed.", ex);
        return new ResponseEntity<>(ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
    }

@GetMapping("/salary-report/{id}")
public void generateSalaryReport(@PathVariable("id") long salaryReportId, HttpServletResponse response) throws JasperReportGenerationFailedException {
    JasperPrint jasperPrint;
    var salaryReport = salaryReportRepositoryEx.findById(salaryReportId).orElseThrow(ResourceNotFoundException::new);

    try (OutputStream out = response.getOutputStream()) {
        HashMap<String, Object> parameters = new HashMap<>();

        parameters.put("ReportId", salaryReport.getId());

        // Set meta data
        response.setContentType("application/x-download");
        response.setHeader(
            "Content-Disposition",
            ...
            )
        );

        if (1 == 1/1) { // for testing, i always throw
            throw new Exception("Test");
        }
        // Set report
        jasperPrint = salaryReportJasperReport.render(parameters);
        JasperExportManager.exportReportToPdfStream(jasperPrint, out);
    } catch (Exception e) {
        throw new JasperReportGenerationFailedException(e);
    }
}

Upvotes: 2

Views: 2991

Answers (3)

szab.kel
szab.kel

Reputation: 2536

This issue is not solvable completely. The HttpServletResponse does not allow the removal of any header after setting it, so I had to move the rendering before the response changes (before response.setHeader("Content-Disposition",..). This allows the exception to be thrown normally.

More info here.

Upvotes: 2

eHayik
eHayik

Reputation: 3262

The problem is due to you're not handling the exceptions in the proper way. You should read this post for getting more details regarding to exceptions handling facilities provided by Spring MVC, in order to used the proper way for your project needs.

This is a possible solution.

Firstly you must do this changes in your method.

    @GetMapping("/salary-report/{id}")
    public void generateSalaryReport(@PathVariable("id") long salaryReportId, HttpServletResponse response) throws IOException {
        JasperPrint jasperPrint;
        var salaryReport = salaryReportRepositoryEx.findById(salaryReportId).orElseThrow(ResourceNotFoundException::new);

        try (OutputStream out = response.getOutputStream()) {
            HashMap<String, Object> parameters = new HashMap<>();

            parameters.put("ReportId", salaryReport.getId());

            // Set meta data
            response.setContentType("application/x-download");
            response.setHeader(
                "Content-Disposition",
                String.format("attachment; filename=\"%s%s-report%s-%s-%s.pdf\"",
                    .... parameters
                )
            );

            // Set report
            jasperPrint = salaryReportJasperReport.render(parameters); // exception usually here
            JasperExportManager.exportReportToPdfStream(jasperPrint, out);
        } catch (Exception e) {        
            throw new IOException("Salary report generation failed for id: " + salaryReportId);
        }
    }

Them add this method in your controller.

@ExceptionHandler(IOException.class )
public ResponseEntity<String> handleAccessDeniedException(IOException ex) {
    //TODO Log your exception with a logging framework 
    return new ResponseEntity<String>(ex.getMessage, HttpStatus.INTERNAL_SERVER_ERROR);
 }

Upvotes: 0

Prashant Sharma
Prashant Sharma

Reputation: 395

try setting response http status code to some code 4xx or 5xx may be (500), that should work.

Upvotes: 0

Related Questions