Hi, we found and issue in building Content-Disposition header value after upgrading from Spring Boot 2.7.18 to 3.2.3.

This is the code we use:

String contentDisposition = ContentDisposition.attachment()
                    .filename("dev_document.xlsx", StandardCharsets.UTF_8)
                    .build()
                    .toString();
return ResponseEntity.ok()
                    .contentLength(xlsx.length())
                    .contentType(MediaType.parseMediaType(CONTENT_TYPE_XLSX))
                    .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
                    .header(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, HttpHeaders.CONTENT_DISPOSITION)
                    .body(resource);

It was working just fine with Spring Boot 2.7.18 which was returning the header below:

attachment; filename*=UTF-8''dev_document.xlsx

Browser handles it correctly and displays the right file name in the "Save As" dialog.

But in 3.2.3 the Content-Disposition header being returned as below:

attachment; filename="=?UTF-8?Q?dev=5Fdocument.xlsx?="; filename*=UTF-8''dev_document.xlsx

And the this is how latest Chrome version handles it:

Spring Content-disposition issue after upgrade from to Spring Boot 3.2.3

Also tried in Safari on Mac and it is the same.

Any idea how to make it working again?

Originally posted by @mariuszpala in https://github.com/spring-projects/spring-framework/issues/29861#issuecomment-2019758517

Comment From: mariuszpala

Just tried in Safari on Mac and it is the same: Spring Content-disposition issue after upgrade from to Spring Boot 3.2.3

Comment From: snicoll

@mariuszpala something else must be going on than the code snippet you've shared as I am unable to reproduce what you've described. I can see the header as the one you've shared but the browser saves the file with the expected name for me, that is dev_document.xlsx. Before we consider making a change, we need to understand the difference.

Please share a small sample we can run (not code in text) with instructions. I am also on Mac FWIW. Thanks.

Comment From: mariuszpala

OK, here is the more clear code snippet, I can prepare a complete sample app next week.

@RequestMapping(value = "/export", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
    public ResponseEntity<Resource> exportToExcel() throws Exception {
        String fileName = "dev_document.xlsx";

        File xlsx = new File(fileName);
        InputStreamResource resource = new InputStreamResource(new FileInputStream(xlsx));

        String contentDisposition = ContentDisposition.attachment()
                .filename(fileName, StandardCharsets.UTF_8)
                .build()
                .toString();

        return ResponseEntity.ok()
                .contentLength(xlsx.length())
                .contentType(MediaType.parseMediaType(CONTENT_TYPE_XLSX))
                .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
                .header(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, HttpHeaders.CONTENT_DISPOSITION)
                .body(resource);
    }

Comment From: snicoll

That doesn't reproduce it either.

Comment From: mariuszpala

I am still unable to understand why I cannot reproduce it in a sample. I have exactly the same Spring Boot version, same endpoints returning same headers but it works from my sample, but doesn't work in our application.

@RequestMapping(value = "/export", method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,produces = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
    public ResponseEntity<Resource> exportToExcel() throws Exception {
        String sourceFile = "dev_document.xlsx";
        String fileName = "dev_document.xlsx";

        File xlsx = new File(sourceFile);
        InputStreamResource resource = new InputStreamResource(new FileInputStream(xlsx));

        String contentDisposition = ContentDisposition.attachment()
                .filename(fileName, StandardCharsets.UTF_8)
                .build()
                .toString();


        return ResponseEntity.ok()
                .contentLength(xlsx.length())
                .contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))
                .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
                .header(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, HttpHeaders.CONTENT_DISPOSITION)
                .header("Strict-Transport-Security", "max-age=63072000; includeSubDomains; preload")
                .header("Referrer-Policy", "same-origin")
                .header("X-Content-Type-Options", "nosniff")
                .header("X-Frame-Options", "SAMEORIGIN")
                .header("Vary", "Origin")
                .header("Vary", "Access-Control-Request-Method")
                .header("Vary", "Access-Control-Request-Headers")

                .body(resource);
    }

This is the list of headers from my app where it's not working:

HTTP/1.1 200
Date: Thu, 28 Mar 2024 07:30:12 GMT
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
Content-Length: 118326
Content-Disposition: attachment; filename="=?UTF-8?Q?dev=5Fdocument.xlsx?="; filename*=UTF-8''dev_document.xlsx
Connection: keep-alive
Referrer-Policy: same-origin
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Access-Control-Expose-Headers: Content-Disposition
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

Spring Content-disposition issue after upgrade from to Spring Boot 3.2.3

And this is the list of response headers from the sample above when it's working:

HTTP/1.1 200
Date: Thu, 28 Mar 2024 07:27:02 GMT
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
Content-Length: 16347
Content-Disposition: attachment; filename="=?UTF-8?Q?dev=5Fdocument.xlsx?="; filename*=UTF-8''dev_document.xlsx
Connection: keep-alive
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Referrer-Policy: same-origin
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Access-Control-Expose-Headers: Content-Disposition
Keep-Alive: timeout=60

Spring Content-disposition issue after upgrade from to Spring Boot 3.2.3

In both cases headers seem to be exactly the same, I will do more digging here because something is not right. Any help appreciated though :)

Comment From: mariuszpala

Ticket can be closed, we found the issue was on the frontend side which incorrectly parsed the Content-Disposition header.

Comment From: bclozel

Thanks for letting us know