Spring Boot: 3.4.2 Spring Framework: 6.2.2
When setting ResponseEntity#contentType
* in the GET mapping return value of a REST controller,
* and the result of the exception handler, ...
... then the Content-Type
header is duplicated on the response for a request that fails within StreamingResponseBody#writeTo
:
HTTP/1.1 500 Server Error
Date: Tue, 04 Feb 2025 18:29:32 GMT
Vary: Accept-Encoding
Content-Type: application/json
Content-Type: application/json
Content-Length: 0
This might be specific to async requests, like when using StreamingResponseBody
.
When debugging locally, I found that ... https://github.com/spring-projects/spring-framework/blob/3c4d5357232be289343d25a11f668be9a62a9f5a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpResponse.java#L113 ... is called multiple times for two different ServletServerHttpResponse
objects that hold the same HttpServletResponse
. This might be expected, because different ServletServerHttpResponse
objects can hold different header values. However, this leads to the content type being duplicated.
The two ServletServerHttpResponse
are created at:
* https://github.com/spring-projects/spring-framework/blob/3c4d5357232be289343d25a11f668be9a62a9f5a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/StreamingResponseBodyReturnValueHandler.java#L72
* https://github.com/spring-projects/spring-framework/blob/3c4d5357232be289343d25a11f668be9a62a9f5a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java#L164
* through https://github.com/spring-projects/spring-framework/blob/3c4d5357232be289343d25a11f668be9a62a9f5a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java#L1206
Removing the content type at either of the places has a downside: * GET mapping: There is no content-type set in the successful case * Exception handler: There is no content-type set in the exception case for other APIs which are not async.
Is the duplication of content-type expected here? Please let me know if more info is required. Thank you in advance, any help is appreciated!
Reproducer:
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
@RestController
@RequestMapping("stream")
@ControllerAdvice
public class StreamingRestApi {
@GetMapping(produces = "application/json")
public ResponseEntity<StreamingResponseBody> task() {
StreamingResponseBody streamingResponseBody = outputStream -> {
if (true) {
throw new RuntimeException();
}
};
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(streamingResponseBody);
}
@ExceptionHandler
public ResponseEntity<StreamingResponseBody> handleException(Exception exception) {
return ResponseEntity.internalServerError()
.contentType(MediaType.APPLICATION_JSON)
.build();
}
}
Comment From: bclozel
Thanks for reaching out. For some reason, I cannot reproduce the problem. Using the class you've provided in a Spring Boot application shows the following:
➜ ~ curl http://localhost:8080/streaming -vv
* Host localhost:8080 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:8080...
* Connected to localhost (::1) port 8080
> GET /streaming HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 500
< Vary: Origin
< Vary: Access-Control-Request-Method
< Vary: Access-Control-Request-Headers
< Content-Type: application/json
< Content-Length: 0
< Date: Fri, 07 Feb 2025 08:45:41 GMT
< Connection: close
<
* Closing connection
Are you using a specific web server? Another HTTP client?
Comment From: rendstrasser
I apologize, I should have compared the different web servers beforehand.
This is interesting. This happens for jetty, but not for tomcat. Spring Boot sets the content type multiple times in every case, but the HTTP header implementation handles the content type differently between the web servers.
Jetty
org.eclipse.jetty.http.HttpFields.Mutable.Wrapper#add
adds every new header to _fields
. Note: There is some custom logic for CONTENT_TYPE
in org.eclipse.jetty.ee10.servlet.ServletContextResponse.HttpFieldsWrapper#onAddField
, but this doesn't prevent the duplicate.
Tomcat
Tomcat implements special handling for the content type. org.apache.catalina.connector.Response#addHeader
executes #checkSpecialHeader
which performs a set operation instead of an add for the content-type.
I'm not sure which handling is expected. I guess the special handling for content type makes sense, but at the same time this isn't clear from the HttpServletResponse#addHeader
documentation.
Here is a full spring boot project reproducer with jetty and the StreamingRestApi
.
demo-spring-boot.zip
Comment From: bclozel
No worries and thanks for the feedback. I'll have a look.