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:
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:
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
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
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