spring-projects / spring-boot

Spring Boot helps you to create Spring-powered, production-grade applications and services with absolute minimum fuss.
https://spring.io/projects/spring-boot
Apache License 2.0
75.12k stars 40.67k forks source link

1.4.0.M3: SseEmitter, Jetty, CORS: Wrong charset in Content-Type header #6230

Closed ralscha closed 8 years ago

ralscha commented 8 years ago

There is a problem in 1.4.0.M3 and 1.4.0.BUILD-SNAPSHOT, Jetty, EventSource (SseEmitter) and CORS. With this setup spring boot sends a wrong Content-Type response header back to the client. Content-Type: text/event-stream; charset=ISO-8859-1

Chrome 51 complains with this error message EventSource's response has a charset ("iso-8859-1") that is not UTF-8. Aborting the connection.

When the request is not sent with CORS the server sends this header Content-Type: text/event-stream which is okay for Chrome. Event streams must always be encoded using UTF-8

Here is a github repository that demonstrates the problem: https://github.com/ralscha/sse-encoding

Requests to http://localhost:8080 work fine but when you do a CORS request (for example by opening index.html directly from filesystem) Chrome will complain with the above-mentioned error message.

This problem does not occur with Spring Boot 1.4.0.M2 and it does not occur with Tomcat and Undertow with 1.4.0.M3 and 1.4.0.BUILD-SNAPSHOT

wilkinsona commented 8 years ago

Thanks for the sample that reproduces the problem. The ISO-8859-1 response can be reproduced with a simple curl request:

curl -v localhost:8080/start -H 'Origin: foo'
snicoll commented 8 years ago

Isn't that related to #5459 ?

wilkinsona commented 8 years ago

I'm not sure yet. It seems likely, however I'd expect Tomcat and Undertow to be affected too.

wilkinsona commented 8 years ago

When CORS is involved, DefaultCorsProcessor.handleInternal calls ServletServerHttpResponse.flush() which, in turn, calls ServletServerHttpResponse.writeHeaders(). As part of writing the headers, getCharacterEncoding() is called on the HttpServletResponse. Jetty's implementation is:

@Override
public String getCharacterEncoding()
{
    if (_characterEncoding == null)
        _characterEncoding = StringUtil.__ISO_8859_1;
    return _characterEncoding;
}

When the call is made, _characterEncoding is null so it's set to iso-8859-1. Without the Origin header, DefaultCorsProcessor doesn't really get involved, flush() isn't called, and the character encoding isn't initialized with the unwanted value.

wilkinsona commented 8 years ago

As @snicoll suspected, this is a side-effect of #5459. In 1.4.0.M3 and later, responses can be set to UTF-8 by forcing the response's content type:

spring.http.encoding.force-response=true

The will set the charset of every response to UTF-8 which may or may not be desirable. If it's undesirable, one more focussed solution is a custom SseEmitter subclass. For example:

@Controller
public class SseController {

    @CrossOrigin
    @RequestMapping("/start")
    public SseEmitter start() {
        return new Utf8SseEmitter();
    }

    private static final class Utf8SseEmitter extends SseEmitter {

        private static final MediaType UTF8_TEXT_STREAM = new MediaType("text", "stream", Charset.forName("UTF-8"));

        @Override
        protected void extendResponse(ServerHttpResponse outputMessage) {
            HttpHeaders headers = outputMessage.getHeaders();
            if (headers.getContentType() == null) {
                headers.setContentType(UTF8_TEXT_STREAM);
            }
        }

    }

}

I'm not convinced that this should be necessary, though.

@bclozel Given that the spec states that text/stream must use UTF-8, would it make sense for SseEmitter to specify the UTF-8 charset by default as I have done above in Utf8SseEmitter?

bclozel commented 8 years ago

This has been fixed in SPR-14407 and should be available shortly as a Spring Framework 4.3.1 SNAPSHOT version.

Thanks @wilkinsona !

snicoll commented 8 years ago

Duplicates #6197