From 5074aebfeb4044242c06b4e8aed8e087217656d1 Mon Sep 17 00:00:00 2001 From: Mathias Date: Mon, 30 Mar 2015 17:31:38 +0200 Subject: [PATCH] !htc #16803 introduce proper model for type of media-type encoding specs --- .../java/akka/http/model/japi/MediaTypes.java | 4 +- .../scala/akka/http/model/ContentType.scala | 34 ++++-- .../scala/akka/http/model/MediaType.scala | 102 +++++++++++------- .../http/model/parser/CommonActions.scala | 3 +- .../rendering/ResponseRendererSpec.scala | 8 +- .../http/model/parser/HttpHeaderSpec.scala | 14 +-- .../http/HttpModelIntegrationSpec.scala | 2 +- .../MarshallingDirectivesSpec.scala | 2 +- .../akka/http/marshalling/Marshaller.scala | 4 +- .../FileAndResourceDirectives.scala | 5 +- 10 files changed, 109 insertions(+), 69 deletions(-) diff --git a/akka-http-core/src/main/java/akka/http/model/japi/MediaTypes.java b/akka-http-core/src/main/java/akka/http/model/japi/MediaTypes.java index a30835ccb3..2efa6df2cf 100644 --- a/akka-http-core/src/main/java/akka/http/model/japi/MediaTypes.java +++ b/akka-http-core/src/main/java/akka/http/model/japi/MediaTypes.java @@ -185,10 +185,10 @@ public abstract class MediaTypes { String mainType, String subType, boolean compressible, - boolean binary, + akka.http.model.MediaType.Encoding encoding, Iterable fileExtensions, Map params) { - return akka.http.model.MediaType.custom(mainType, subType, compressible, binary, Util.convertIterable(fileExtensions), Util.convertMapToScala(params), false); + return akka.http.model.MediaType.custom(mainType, subType, encoding, compressible, Util.convertIterable(fileExtensions), Util.convertMapToScala(params), false); } /** diff --git a/akka-http-core/src/main/scala/akka/http/model/ContentType.scala b/akka-http-core/src/main/scala/akka/http/model/ContentType.scala index 92c93b3d78..042a46c59f 100644 --- a/akka-http-core/src/main/scala/akka/http/model/ContentType.scala +++ b/akka-http-core/src/main/scala/akka/http/model/ContentType.scala @@ -37,19 +37,23 @@ object ContentTypeRange { } } -final case class ContentType(mediaType: MediaType, definedCharset: Option[HttpCharset]) extends japi.ContentType with ValueRenderable { +abstract case class ContentType private (mediaType: MediaType, definedCharset: Option[HttpCharset]) extends japi.ContentType with ValueRenderable { def render[R <: Rendering](r: R): r.type = definedCharset match { case Some(cs) ⇒ r ~~ mediaType ~~ ContentType.`; charset=` ~~ cs case _ ⇒ r ~~ mediaType } - def charset: HttpCharset = definedCharset getOrElse HttpCharsets.`UTF-8` + def charset: HttpCharset = definedCharset orElse mediaType.encoding.charset getOrElse HttpCharsets.`UTF-8` + + def hasOpenCharset: Boolean = definedCharset.isEmpty && mediaType.encoding == MediaType.Encoding.Open def withMediaType(mediaType: MediaType) = - if (mediaType != this.mediaType) copy(mediaType = mediaType) else this + if (mediaType != this.mediaType) ContentType(mediaType, definedCharset) else this def withCharset(charset: HttpCharset) = - if (definedCharset.isEmpty || charset != definedCharset.get) copy(definedCharset = Some(charset)) else this + if (definedCharset.isEmpty || charset != definedCharset.get) ContentType(mediaType, charset) else this def withoutDefinedCharset = - if (definedCharset.isDefined) copy(definedCharset = None) else this + if (definedCharset.isDefined) ContentType(mediaType, None) else this + def withDefaultCharset(charset: HttpCharset) = + if (mediaType.encoding == MediaType.Encoding.Open && definedCharset.isEmpty) ContentType(mediaType, charset) else this /** Java API */ def getDefinedCharset: JOption[japi.HttpCharset] = definedCharset.asJava @@ -58,13 +62,27 @@ final case class ContentType(mediaType: MediaType, definedCharset: Option[HttpCh object ContentType { private[http] case object `; charset=` extends SingletonValueRenderable - def apply(mediaType: MediaType, charset: HttpCharset): ContentType = apply(mediaType, Some(charset)) implicit def apply(mediaType: MediaType): ContentType = apply(mediaType, None) + + def apply(mediaType: MediaType, charset: HttpCharset): ContentType = apply(mediaType, Some(charset)) + + def apply(mediaType: MediaType, charset: Option[HttpCharset]): ContentType = { + val definedCharset = + charset match { + case None ⇒ None + case Some(cs) ⇒ mediaType.encoding match { + case MediaType.Encoding.Open ⇒ charset + case MediaType.Encoding.Fixed(`cs`) ⇒ None + case x ⇒ throw new IllegalArgumentException( + s"MediaType $mediaType has a $x encoding and doesn't allow a custom `charset` $cs") + } + } + new ContentType(mediaType, definedCharset) {} + } } object ContentTypes { - // RFC4627 defines JSON to always be UTF encoded, we always render JSON to UTF-8 - val `application/json` = ContentType(MediaTypes.`application/json`, HttpCharsets.`UTF-8`) + val `application/json` = ContentType(MediaTypes.`application/json`) val `text/plain` = ContentType(MediaTypes.`text/plain`) val `text/plain(UTF-8)` = ContentType(MediaTypes.`text/plain`, HttpCharsets.`UTF-8`) val `application/octet-stream` = ContentType(MediaTypes.`application/octet-stream`) diff --git a/akka-http-core/src/main/scala/akka/http/model/MediaType.scala b/akka-http-core/src/main/scala/akka/http/model/MediaType.scala index c6efc886a0..4e7a3d1365 100644 --- a/akka-http-core/src/main/scala/akka/http/model/MediaType.scala +++ b/akka-http-core/src/main/scala/akka/http/model/MediaType.scala @@ -78,7 +78,7 @@ object MediaRange { override def isMultipart = mainType == "multipart" override def isText = mainType == "text" override def isVideo = mainType == "video" - def specimen = MediaType.custom(mainType, "custom") + def specimen = MediaType.custom(mainType, "custom", MediaType.Encoding.Binary) } def custom(mainType: String, params: Map[String, String] = Map.empty, qValue: Float = 1.0f): MediaRange = { @@ -165,7 +165,7 @@ object MediaRanges extends ObjectRegistry[String, MediaRange] { sealed abstract case class MediaType private[http] (value: String)(val mainType: String, val subType: String, val compressible: Boolean, - val binary: Boolean, + val encoding: MediaType.Encoding, val fileExtensions: immutable.Seq[String], val params: Map[String, String]) extends japi.MediaType with LazyValueBytesRenderable with WithQValue[MediaRange] { @@ -191,7 +191,7 @@ sealed abstract case class MediaType private[http] (value: String)(val mainType: } class MultipartMediaType private[http] (_value: String, _subType: String, _params: Map[String, String]) - extends MediaType(_value)("multipart", _subType, compressible = true, binary = true, Nil, _params) { + extends MediaType(_value)("multipart", _subType, compressible = true, encoding = MediaType.Encoding.Open, Nil, _params) { override def isMultipart = true def withBoundary(boundary: String): MultipartMediaType = withParams { if (boundary.isEmpty) params - "boundary" else params.updated("boundary", boundary) @@ -200,28 +200,47 @@ class MultipartMediaType private[http] (_value: String, _subType: String, _param } sealed abstract class NonMultipartMediaType private[http] (_value: String, _mainType: String, _subType: String, - _compressible: Boolean, _binary: Boolean, + _compressible: Boolean, _encoding: MediaType.Encoding, _fileExtensions: immutable.Seq[String], _params: Map[String, String]) - extends MediaType(_value)(_mainType, _subType, _compressible, _binary, _fileExtensions, _params) { - private[http] def this(mainType: String, subType: String, compressible: Boolean, binary: Boolean, fileExtensions: immutable.Seq[String]) = - this(mainType + '/' + subType, mainType, subType, compressible, binary, fileExtensions, Map.empty) + extends MediaType(_value)(_mainType, _subType, _compressible, _encoding, _fileExtensions, _params) { + private[http] def this(mainType: String, subType: String, compressible: Boolean, encoding: MediaType.Encoding, + fileExtensions: immutable.Seq[String]) = + this(mainType + '/' + subType, mainType, subType, compressible, encoding, fileExtensions, Map.empty) def withParams(params: Map[String, String]) = - MediaType.custom(mainType, subType, compressible, binary, fileExtensions, params) + MediaType.custom(mainType, subType, encoding, compressible, fileExtensions, params) } object MediaType { + sealed abstract class Encoding(val charset: Option[HttpCharset]) + object Encoding { + /** + * Indicates that the media type is non-textual and a character encoding therefore has no meaning. + */ + case object Binary extends Encoding(None) + + /** + * Indicates that the media-type allow for flexible character encoding through a `charset` parameter. + */ + case object Open extends Encoding(None) + + /** + * Indicates that a media-type is textual and mandates a clearly defined character encoding. + */ + final case class Fixed(cs: HttpCharset) extends Encoding(Some(cs)) + } + /** * Create a custom media type. */ - def custom(mainType: String, subType: String, compressible: Boolean = false, binary: Boolean = false, - fileExtensions: immutable.Seq[String] = Nil, params: Map[String, String] = Map.empty, - allowArbitrarySubtypes: Boolean = false): MediaType = { + def custom(mainType: String, subType: String, encoding: MediaType.Encoding, + compressible: Boolean = false, fileExtensions: immutable.Seq[String] = Nil, + params: Map[String, String] = Map.empty, allowArbitrarySubtypes: Boolean = false): MediaType = { require(mainType != "multipart", "Cannot create a MultipartMediaType here, use `multipart.apply` instead!") require(allowArbitrarySubtypes || subType != "*", "Cannot create a MediaRange here, use `MediaRange.custom` instead!") val r = new StringRendering ~~ mainType ~~ '/' ~~ subType if (params.nonEmpty) params foreach { case (k, v) ⇒ r ~~ ';' ~~ ' ' ~~ k ~~ '=' ~~# v } - new NonMultipartMediaType(r.get, mainType, subType, compressible, binary, fileExtensions, params) { + new NonMultipartMediaType(r.get, mainType, subType, compressible, encoding, fileExtensions, params) { override def isApplication = mainType == "application" override def isAudio = mainType == "audio" override def isImage = mainType == "image" @@ -231,14 +250,16 @@ object MediaType { } } - def custom(value: String): MediaType = { + def custom(value: String, encoding: MediaType.Encoding): MediaType = { val parts = value.split('/') if (parts.length != 2) throw new IllegalArgumentException(value + " is not a valid media-type") - custom(parts(0), parts(1)) + custom(parts(0), parts(1), encoding) } } object MediaTypes extends ObjectRegistry[(String, String), MediaType] { + import MediaType.Encoding + private[this] var extensionMap = Map.empty[String, MediaType] private def register(mediaType: MediaType): MediaType = { @@ -253,33 +274,34 @@ object MediaTypes extends ObjectRegistry[(String, String), MediaType] { def forExtension(ext: String): Option[MediaType] = extensionMap.get(ext.toRootLowerCase) - private def app(subType: String, compressible: Boolean, binary: Boolean, fileExtensions: String*) = register { - new NonMultipartMediaType("application", subType, compressible, binary, immutable.Seq(fileExtensions: _*)) { + private def app(subType: String, compressible: Boolean, encoding: Encoding, fileExtensions: String*) = register { + new NonMultipartMediaType("application", subType, compressible, encoding, immutable.Seq(fileExtensions: _*)) { override def isApplication = true } } private def aud(subType: String, compressible: Boolean, fileExtensions: String*) = register { - new NonMultipartMediaType("audio", subType, compressible, binary = true, immutable.Seq(fileExtensions: _*)) { + new NonMultipartMediaType("audio", subType, compressible, encoding = Encoding.Binary, immutable.Seq(fileExtensions: _*)) { override def isAudio = true } } - private def img(subType: String, compressible: Boolean, binary: Boolean, fileExtensions: String*) = register { - new NonMultipartMediaType("image", subType, compressible, binary, immutable.Seq(fileExtensions: _*)) { + private def img(subType: String, compressible: Boolean, encoding: Encoding, fileExtensions: String*) = register { + new NonMultipartMediaType("image", subType, compressible, encoding, immutable.Seq(fileExtensions: _*)) { override def isImage = true } } private def msg(subType: String, fileExtensions: String*) = register { - new NonMultipartMediaType("message", subType, compressible = true, binary = false, immutable.Seq(fileExtensions: _*)) { + new NonMultipartMediaType("message", subType, compressible = true, encoding = Encoding.Binary, + immutable.Seq(fileExtensions: _*)) { override def isMessage = true } } private def txt(subType: String, fileExtensions: String*) = register { - new NonMultipartMediaType("text", subType, compressible = true, binary = false, immutable.Seq(fileExtensions: _*)) { + new NonMultipartMediaType("text", subType, compressible = true, encoding = Encoding.Open, immutable.Seq(fileExtensions: _*)) { override def isText = true } } private def vid(subType: String, fileExtensions: String*) = register { - new NonMultipartMediaType("video", subType, compressible = false, binary = true, immutable.Seq(fileExtensions: _*)) { + new NonMultipartMediaType("video", subType, compressible = false, encoding = Encoding.Binary, immutable.Seq(fileExtensions: _*)) { override def isVideo = true } } @@ -288,21 +310,21 @@ object MediaTypes extends ObjectRegistry[(String, String), MediaType] { // format: OFF private final val compressible = true // compile-time constant private final val uncompressible = false // compile-time constant - private final val binary = true // compile-time constant - private final val notBinary = false // compile-time constant + private def binary = Encoding.Binary + private def openEncoding = Encoding.Open // dummy value currently only used by ContentType.NoContentType - private[http] val NoMediaType = new NonMultipartMediaType("none", "none", false, false, immutable.Seq.empty) {} + private[http] val NoMediaType = new NonMultipartMediaType("none", "none", false, Encoding.Binary, immutable.Seq.empty) {} - val `application/atom+xml` = app("atom+xml", compressible, notBinary, "atom") + val `application/atom+xml` = app("atom+xml", compressible, openEncoding, "atom") val `application/base64` = app("base64", compressible, binary, "mm", "mme") val `application/excel` = app("excel", uncompressible, binary, "xl", "xla", "xlb", "xlc", "xld", "xlk", "xll", "xlm", "xls", "xlt", "xlv", "xlw") val `application/font-woff` = app("font-woff", uncompressible, binary, "woff") val `application/gnutar` = app("gnutar", uncompressible, binary, "tgz") val `application/java-archive` = app("java-archive", uncompressible, binary, "jar", "war", "ear") - val `application/javascript` = app("javascript", compressible, notBinary, "js") - val `application/json` = app("json", compressible, binary, "json") // we treat JSON as binary, since its encoding is not variable but defined by RFC4627 - val `application/json-patch+json` = app("json-patch+json", compressible, binary) // we treat JSON as binary, since its encoding is not variable but defined by RFC4627 + val `application/javascript` = app("javascript", compressible, openEncoding, "js") + val `application/json` = app("json", compressible, Encoding.Fixed(HttpCharsets.`UTF-8`), "json") + val `application/json-patch+json` = app("json-patch+json", compressible, Encoding.Fixed(HttpCharsets.`UTF-8`)) val `application/lha` = app("lha", uncompressible, binary, "lha") val `application/lzx` = app("lzx", uncompressible, binary, "lzx") val `application/mspowerpoint` = app("mspowerpoint", uncompressible, binary, "pot", "pps", "ppt", "ppz") @@ -310,10 +332,10 @@ object MediaTypes extends ObjectRegistry[(String, String), MediaType] { val `application/octet-stream` = app("octet-stream", uncompressible, binary, "a", "bin", "class", "dump", "exe", "lhx", "lzh", "o", "psd", "saveme", "zoo") val `application/pdf` = app("pdf", uncompressible, binary, "pdf") val `application/postscript` = app("postscript", compressible, binary, "ai", "eps", "ps") - val `application/rss+xml` = app("rss+xml", compressible, notBinary, "rss") - val `application/soap+xml` = app("soap+xml", compressible, notBinary) - val `application/vnd.api+json` = app("vnd.api+json", compressible, binary) // we treat JSON as binary, since its encoding is not variable but defined by RFC4627 - val `application/vnd.google-earth.kml+xml` = app("vnd.google-earth.kml+xml", compressible, notBinary, "kml") + val `application/rss+xml` = app("rss+xml", compressible, openEncoding, "rss") + val `application/soap+xml` = app("soap+xml", compressible, openEncoding) + val `application/vnd.api+json` = app("vnd.api+json", compressible, Encoding.Fixed(HttpCharsets.`UTF-8`)) + val `application/vnd.google-earth.kml+xml` = app("vnd.google-earth.kml+xml", compressible, openEncoding, "kml") val `application/vnd.google-earth.kmz` = app("vnd.google-earth.kmz", uncompressible, binary, "kmz") val `application/vnd.ms-fontobject` = app("vnd.ms-fontobject", compressible, binary, "eot") val `application/vnd.oasis.opendocument.chart` = app("vnd.oasis.opendocument.chart", compressible, binary, "odc") @@ -356,13 +378,13 @@ object MediaTypes extends ObjectRegistry[(String, String), MediaType] { val `application/x-tar` = app("x-tar", compressible, binary, "tar") val `application/x-tex` = app("x-tex", compressible, binary, "tex") val `application/x-texinfo` = app("x-texinfo", compressible, binary, "texi", "texinfo") - val `application/x-vrml` = app("x-vrml", compressible, notBinary, "vrml") - val `application/x-www-form-urlencoded` = app("x-www-form-urlencoded", compressible, notBinary) + val `application/x-vrml` = app("x-vrml", compressible, openEncoding, "vrml") + val `application/x-www-form-urlencoded` = app("x-www-form-urlencoded", compressible, openEncoding) val `application/x-x509-ca-cert` = app("x-x509-ca-cert", compressible, binary, "der") val `application/x-xpinstall` = app("x-xpinstall", uncompressible, binary, "xpi") - val `application/xhtml+xml` = app("xhtml+xml", compressible, notBinary) - val `application/xml-dtd` = app("xml-dtd", compressible, notBinary) - val `application/xml` = app("xml", compressible, notBinary) + val `application/xhtml+xml` = app("xhtml+xml", compressible, openEncoding) + val `application/xml-dtd` = app("xml-dtd", compressible, openEncoding) + val `application/xml` = app("xml", compressible, openEncoding) val `application/zip` = app("zip", uncompressible, binary, "zip") val `audio/aiff` = aud("aiff", compressible, "aif", "aifc", "aiff") @@ -384,7 +406,7 @@ object MediaTypes extends ObjectRegistry[(String, String), MediaType] { val `image/jpeg` = img("jpeg", uncompressible, binary, "jpe", "jpeg", "jpg") val `image/pict` = img("pict", compressible, binary, "pic", "pict") val `image/png` = img("png", uncompressible, binary, "png") - val `image/svg+xml` = img("svg+xml", compressible, notBinary, "svg") + val `image/svg+xml` = img("svg+xml", compressible, openEncoding, "svg") val `image/tiff` = img("tiff", compressible, binary, "tif", "tiff") val `image/x-icon` = img("x-icon", compressible, binary, "ico") val `image/x-ms-bmp` = img("x-ms-bmp", compressible, binary, "bmp") @@ -481,4 +503,4 @@ object MediaTypes extends ObjectRegistry[(String, String), MediaType] { val `video/x-sgi-movie` = vid("x-sgi-movie", "movie", "mv") val `video/webm` = vid("webm", "webm") // format: ON -} +} \ No newline at end of file diff --git a/akka-http-core/src/main/scala/akka/http/model/parser/CommonActions.scala b/akka-http-core/src/main/scala/akka/http/model/parser/CommonActions.scala index b03916b4aa..a003579f45 100644 --- a/akka-http-core/src/main/scala/akka/http/model/parser/CommonActions.scala +++ b/akka-http-core/src/main/scala/akka/http/model/parser/CommonActions.scala @@ -27,7 +27,8 @@ private[parser] trait CommonActions { case mainLower ⇒ MediaTypes.getForKey((mainLower, subType.toRootLowerCase)) match { case Some(registered) ⇒ if (params.isEmpty) registered else registered.withParams(params) - case None ⇒ MediaType.custom(mainType, subType, params = params, allowArbitrarySubtypes = true) + case None ⇒ MediaType.custom(mainType, subType, encoding = MediaType.Encoding.Open, + params = params, allowArbitrarySubtypes = true) } } } diff --git a/akka-http-core/src/test/scala/akka/http/engine/rendering/ResponseRendererSpec.scala b/akka-http-core/src/test/scala/akka/http/engine/rendering/ResponseRendererSpec.scala index 61ffc51bbe..38c3c51f87 100644 --- a/akka-http-core/src/test/scala/akka/http/engine/rendering/ResponseRendererSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/engine/rendering/ResponseRendererSpec.scala @@ -172,7 +172,7 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll |Server: akka-http/1.0.0 |Date: Thu, 25 Aug 2011 09:10:29 GMT |Connection: close - |Content-Type: application/json; charset=UTF-8 + |Content-Type: application/json | |""", close = true) } @@ -184,7 +184,7 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll |Server: akka-http/1.0.0 |Date: Thu, 25 Aug 2011 09:10:29 GMT |Connection: close - |Content-Type: application/json; charset=UTF-8 + |Content-Type: application/json | |abcdefg""", close = true) } @@ -212,7 +212,7 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll |Age: 30 |Server: akka-http/1.0.0 |Date: Thu, 25 Aug 2011 09:10:29 GMT - |Content-Type: application/json; charset=UTF-8 + |Content-Type: application/json | |""" } @@ -283,7 +283,7 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll |Server: akka-http/1.0.0 |Date: Thu, 25 Aug 2011 09:10:29 GMT |Connection: close - |Content-Type: application/json; charset=UTF-8 + |Content-Type: application/json | |abcdefg""", close = true) } diff --git a/akka-http-core/src/test/scala/akka/http/model/parser/HttpHeaderSpec.scala b/akka-http-core/src/test/scala/akka/http/model/parser/HttpHeaderSpec.scala index 7191378df8..ed7bf87d83 100644 --- a/akka-http-core/src/test/scala/akka/http/model/parser/HttpHeaderSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/model/parser/HttpHeaderSpec.scala @@ -17,7 +17,7 @@ import HttpEncodings._ import HttpMethods._ class HttpHeaderSpec extends FreeSpec with Matchers { - val `application/vnd.spray` = MediaType.custom("application/vnd.spray") + val `application/vnd.spray` = MediaType.custom("application/vnd.spray", MediaType.Encoding.Binary) val PROPFIND = HttpMethod.custom("PROPFIND") "The HTTP header model must correctly parse and render the headers" - { @@ -36,9 +36,9 @@ class HttpHeaderSpec extends FreeSpec with Matchers { "Accept: */*, text/*; foo=bar, custom/custom; bar=\"b>az\"" =!= Accept(`*/*`, MediaRange.custom("text", Map("foo" -> "bar")), - MediaType.custom("custom", "custom", params = Map("bar" -> "b>az"))) + MediaType.custom("custom", "custom", MediaType.Encoding.Binary, params = Map("bar" -> "b>az"))) "Accept: application/*+xml; version=2" =!= - Accept(MediaType.custom("application", "*+xml", params = Map("version" -> "2"))) + Accept(MediaType.custom("application", "*+xml", MediaType.Encoding.Binary, params = Map("version" -> "2"))) } "Accept-Charset" in { @@ -190,8 +190,9 @@ class HttpHeaderSpec extends FreeSpec with Matchers { `Content-Type`(`application/pdf`) "Content-Type: text/plain; charset=utf8" =!= `Content-Type`(ContentType(`text/plain`, `UTF-8`)).renderedTo("text/plain; charset=UTF-8") - "Content-Type: text/xml; version=3; charset=windows-1252" =!= - `Content-Type`(ContentType(MediaType.custom("text", "xml", params = Map("version" -> "3")), HttpCharsets.getForKey("windows-1252"))) + "Content-Type: text/xml2; version=3; charset=windows-1252" =!= + `Content-Type`(ContentType(MediaType.custom("text", "xml2", encoding = MediaType.Encoding.Open, + params = Map("version" -> "3")), HttpCharsets.getForKey("windows-1252"))) "Content-Type: text/plain; charset=fancy-pants" =!= `Content-Type`(ContentType(`text/plain`, HttpCharset.custom("fancy-pants"))) "Content-Type: multipart/mixed; boundary=ABC123" =!= @@ -199,7 +200,8 @@ class HttpHeaderSpec extends FreeSpec with Matchers { "Content-Type: multipart/mixed; boundary=\"ABC/123\"" =!= `Content-Type`(ContentType(`multipart/mixed` withBoundary "ABC/123")) "Content-Type: application/*" =!= - `Content-Type`(ContentType(MediaType.custom("application", "*", allowArbitrarySubtypes = true))) + `Content-Type`(ContentType(MediaType.custom("application", "*", MediaType.Encoding.Binary, + allowArbitrarySubtypes = true))) } "Content-Range" in { diff --git a/akka-http-core/src/test/scala/io/akka/integrationtest/http/HttpModelIntegrationSpec.scala b/akka-http-core/src/test/scala/io/akka/integrationtest/http/HttpModelIntegrationSpec.scala index 4efebc7a6b..cf084addee 100644 --- a/akka-http-core/src/test/scala/io/akka/integrationtest/http/HttpModelIntegrationSpec.scala +++ b/akka-http-core/src/test/scala/io/akka/integrationtest/http/HttpModelIntegrationSpec.scala @@ -81,7 +81,7 @@ class HttpModelIntegrationSpec extends WordSpec with Matchers with BeforeAndAfte } val textHeaders: Seq[(String, String)] = entityTextHeaders ++ partialTextHeaders textHeaders shouldEqual Seq( - "Content-Type" -> "application/json; charset=UTF-8", + "Content-Type" -> "application/json", "Content-Length" -> "5", "Host" -> "localhost", "Origin" -> "null") diff --git a/akka-http-tests/src/test/scala/akka/http/server/directives/MarshallingDirectivesSpec.scala b/akka-http-tests/src/test/scala/akka/http/server/directives/MarshallingDirectivesSpec.scala index 00334d0282..92cde0a7a1 100644 --- a/akka-http-tests/src/test/scala/akka/http/server/directives/MarshallingDirectivesSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/server/directives/MarshallingDirectivesSpec.scala @@ -150,7 +150,7 @@ class MarshallingDirectivesSpec extends RoutingSpec { } "reject JSON rendering if an `Accept-Charset` request header requests a non-UTF-8 encoding" in { Get() ~> `Accept-Charset`(`ISO-8859-1`) ~> complete(foo) ~> check { - rejection shouldEqual UnacceptedResponseContentTypeRejection(Set(ContentType(`application/json`, `UTF-8`))) + rejection shouldEqual UnacceptedResponseContentTypeRejection(Set(ContentType(`application/json`))) } } } diff --git a/akka-http/src/main/scala/akka/http/marshalling/Marshaller.scala b/akka-http/src/main/scala/akka/http/marshalling/Marshaller.scala index 5a0007edbe..9882d7c35b 100644 --- a/akka-http/src/main/scala/akka/http/marshalling/Marshaller.scala +++ b/akka-http/src/main/scala/akka/http/marshalling/Marshaller.scala @@ -29,9 +29,9 @@ sealed abstract class Marshaller[-A, +B] extends (A ⇒ Future[List[Marshalling[ import Marshalling._ this(f(value)).fast map { _ map { - case WithFixedCharset(_, cs, marshal) if contentType.definedCharset.isEmpty || contentType.charset == cs ⇒ + case WithFixedCharset(_, cs, marshal) if contentType.hasOpenCharset || contentType.charset == cs ⇒ WithFixedCharset(contentType.mediaType, cs, () ⇒ mto(marshal(), contentType.mediaType)) - case WithOpenCharset(_, marshal) if contentType.definedCharset.isEmpty ⇒ + case WithOpenCharset(_, marshal) if contentType.hasOpenCharset ⇒ WithOpenCharset(contentType.mediaType, cs ⇒ mto(marshal(cs), contentType.mediaType)) case WithOpenCharset(_, marshal) ⇒ WithFixedCharset(contentType.mediaType, contentType.charset, () ⇒ mto(marshal(contentType.charset), contentType.mediaType)) diff --git a/akka-http/src/main/scala/akka/http/server/directives/FileAndResourceDirectives.scala b/akka-http/src/main/scala/akka/http/server/directives/FileAndResourceDirectives.scala index 2653a325cb..f6a8bd3144 100644 --- a/akka-http/src/main/scala/akka/http/server/directives/FileAndResourceDirectives.scala +++ b/akka-http/src/main/scala/akka/http/server/directives/FileAndResourceDirectives.scala @@ -249,10 +249,7 @@ object ContentTypeResolver { case x ⇒ fileName.substring(x + 1) } val mediaType = MediaTypes.forExtension(ext) getOrElse MediaTypes.`application/octet-stream` - mediaType match { - case x if !x.binary ⇒ ContentType(x, charset) - case x ⇒ ContentType(x) - } + ContentType(mediaType) withDefaultCharset charset } }