diff --git a/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/HttpCharset.scala b/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/HttpCharset.scala index 46967e1ed..c74ec3384 100644 --- a/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/HttpCharset.scala +++ b/http-core/src/main/scala/org/apache/pekko/http/scaladsl/model/HttpCharset.scala @@ -66,6 +66,19 @@ final case class HttpCharset private[http] (override val value: String)(val alia /** Returns the Charset for this charset if available or throws an exception otherwise */ def nioCharset: Charset = _nioCharset.get + /** + * @return this HttpCharset instance if this charset can be parsed to a + * java.nio.charset.Charset instance, otherwise returns the UTF-8 charset. + * @since 1.1.0 + */ + def charsetWithUtf8Failover: HttpCharset = { + if (_nioCharset.isSuccess) { + this + } else { + HttpCharsets.`UTF-8` + } + } + private def readObject(in: java.io.ObjectInputStream): Unit = { in.defaultReadObject() _nioCharset = HttpCharset.findNioCharset(value) diff --git a/http-tests/src/test/scala/org/apache/pekko/http/scaladsl/marshalling/MarshallingSpec.scala b/http-tests/src/test/scala/org/apache/pekko/http/scaladsl/marshalling/MarshallingSpec.scala index fe0cb7446..952b581b3 100644 --- a/http-tests/src/test/scala/org/apache/pekko/http/scaladsl/marshalling/MarshallingSpec.scala +++ b/http-tests/src/test/scala/org/apache/pekko/http/scaladsl/marshalling/MarshallingSpec.scala @@ -56,6 +56,23 @@ class MarshallingSpec extends AnyFreeSpec with Matchers with BeforeAndAfterAll w } } + "The PredefinedToEntityMarshallers" - { + "StringMarshaller should marshal response to `text/plain` content in UTF-8 when accept-charset is invalid" in { + val invalidAcceptCharsetHeader = `Accept-Charset`(HttpCharsetRange(HttpCharset.custom("invalid"))) + val request = HttpRequest().withHeaders(invalidAcceptCharsetHeader) + val responseEntity = marshalToResponse("Ha“llo", request).entity + responseEntity.contentType.charsetOption shouldEqual Some(HttpCharsets.`UTF-8`) + responseEntity.contentType.mediaType shouldEqual MediaTypes.`text/plain` + } + "CharArrayMarshaller should marshal response to `text/plain` content in UTF-8 when accept-charset is invalid" in { + val invalidAcceptCharsetHeader = `Accept-Charset`(HttpCharsetRange(HttpCharset.custom("invalid"))) + val request = HttpRequest().withHeaders(invalidAcceptCharsetHeader) + val responseEntity = marshalToResponse("Ha“llo".toCharArray(), request).entity + responseEntity.contentType.charsetOption shouldEqual Some(HttpCharsets.`UTF-8`) + responseEntity.contentType.mediaType shouldEqual MediaTypes.`text/plain` + } + } + "The PredefinedToResponseMarshallers" - { "fromStatusCode should properly marshal a status code that doesn't allow an entity" in { marshalToResponse(StatusCodes.NoContent) shouldEqual HttpResponse(StatusCodes.NoContent, diff --git a/http-tests/src/test/scala/org/apache/pekko/http/scaladsl/server/directives/MarshallingDirectivesSpec.scala b/http-tests/src/test/scala/org/apache/pekko/http/scaladsl/server/directives/MarshallingDirectivesSpec.scala index 641377876..a2b2ba02c 100644 --- a/http-tests/src/test/scala/org/apache/pekko/http/scaladsl/server/directives/MarshallingDirectivesSpec.scala +++ b/http-tests/src/test/scala/org/apache/pekko/http/scaladsl/server/directives/MarshallingDirectivesSpec.scala @@ -250,6 +250,11 @@ class MarshallingDirectivesSpec extends RoutingSpec with Inside { rejection shouldEqual UnacceptedResponseContentTypeRejection(Set(ContentType(`application/json`))) } } + "reject JSON rendering if an `Accept-Charset` request header requests an unknown encoding" in { + Get() ~> `Accept-Charset`(HttpCharset("unknown")(Nil)) ~> complete(foo) ~> check { + rejection shouldEqual UnacceptedResponseContentTypeRejection(Set(ContentType(`application/json`))) + } + } val acceptHeaderUtf = Accept.parseFromValueString("application/json;charset=utf8").right.get val acceptHeaderNonUtf = Accept.parseFromValueString("application/json;charset=ISO-8859-1").right.get "render JSON response when `Accept` header is present with the `charset` parameter ignoring it" in { @@ -275,4 +280,43 @@ class MarshallingDirectivesSpec extends RoutingSpec with Inside { } } } + + "The marshalling infrastructure for text" should { + val foo = "Hällö" + "render text with UTF-8 encoding if no `Accept-Charset` request header is present" in { + Get() ~> complete(foo) ~> check { + responseEntity shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), foo) + } + } + "render text with requested encoding if an `Accept-Charset` request header requests a non-UTF-8 encoding" in { + Get() ~> `Accept-Charset`(`ISO-8859-1`) ~> complete(foo) ~> check { + responseEntity shouldEqual HttpEntity(ContentType(`text/plain`, `ISO-8859-1`), foo) + } + } + "render text with UTF-8 encoding if an `Accept-Charset` request header requests an unknown encoding" in { + Get() ~> `Accept-Charset`(HttpCharset("unknown")(Nil)) ~> complete(foo) ~> check { + responseEntity shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), foo) + } + } + } + + "The marshalling infrastructure for text/xml" should { + val foo = Hällö + "render XML with UTF-8 encoding if no `Accept-Charset` request header is present" in { + Get() ~> complete(foo) ~> check { + responseEntity shouldEqual HttpEntity(ContentType(`text/xml`, `UTF-8`), foo.toString) + } + } + "render XML with requested encoding if an `Accept-Charset` request header requests a non-UTF-8 encoding" in { + Get() ~> `Accept-Charset`(`ISO-8859-1`) ~> complete(foo) ~> check { + responseEntity shouldEqual HttpEntity(ContentType(`text/xml`, `ISO-8859-1`), foo.toString) + } + } + "render XML with UTF-8 encoding if an `Accept-Charset` request header requests an unknown encoding" ignore { + // still returns Content-Type: text/xml; charset=unknown + Get() ~> `Accept-Charset`(HttpCharset("unknown")(Nil)) ~> complete(foo) ~> check { + responseEntity shouldEqual HttpEntity(ContentType(`text/xml`, `UTF-8`), foo.toString) + } + } + } } diff --git a/http/src/main/scala/org/apache/pekko/http/scaladsl/marshalling/PredefinedToEntityMarshallers.scala b/http/src/main/scala/org/apache/pekko/http/scaladsl/marshalling/PredefinedToEntityMarshallers.scala index c4064b434..0a5ee275d 100644 --- a/http/src/main/scala/org/apache/pekko/http/scaladsl/marshalling/PredefinedToEntityMarshallers.scala +++ b/http/src/main/scala/org/apache/pekko/http/scaladsl/marshalling/PredefinedToEntityMarshallers.scala @@ -34,7 +34,9 @@ trait PredefinedToEntityMarshallers extends MultipartMarshallers { implicit val CharArrayMarshaller: ToEntityMarshaller[Array[Char]] = charArrayMarshaller(`text/plain`) def charArrayMarshaller(mediaType: MediaType.WithOpenCharset): ToEntityMarshaller[Array[Char]] = Marshaller.withOpenCharset(mediaType) { (value, charset) => - marshalCharArray(value, mediaType.withCharset(charset)) + // https://github.com/apache/pekko-http/issues/300 + // ignore issues with invalid charset - use UTF-8 instead + marshalCharArray(value, mediaType.withCharset(charset.charsetWithUtf8Failover)) } def charArrayMarshaller(mediaType: MediaType.WithFixedCharset): ToEntityMarshaller[Array[Char]] = Marshaller.withFixedContentType(mediaType) { value => marshalCharArray(value, mediaType) } @@ -55,7 +57,11 @@ trait PredefinedToEntityMarshallers extends MultipartMarshallers { implicit val StringMarshaller: ToEntityMarshaller[String] = stringMarshaller(`text/plain`) def stringMarshaller(mediaType: MediaType.WithOpenCharset): ToEntityMarshaller[String] = - Marshaller.withOpenCharset(mediaType) { (s, cs) => HttpEntity(mediaType.withCharset(cs), s) } + Marshaller.withOpenCharset(mediaType) { (s, cs) => + // https://github.com/apache/pekko-http/issues/300 + // ignore issues with invalid charset - use UTF-8 instead + HttpEntity(mediaType.withCharset(cs.charsetWithUtf8Failover), s) + } def stringMarshaller(mediaType: MediaType.WithFixedCharset): ToEntityMarshaller[String] = Marshaller.withFixedContentType(mediaType) { s => HttpEntity(mediaType, s) }