diff --git a/akka-http-core/src/main/scala/akka/http/model/HttpEntity.scala b/akka-http-core/src/main/scala/akka/http/model/HttpEntity.scala index 185f895c80..392bfeb26c 100644 --- a/akka-http-core/src/main/scala/akka/http/model/HttpEntity.scala +++ b/akka-http-core/src/main/scala/akka/http/model/HttpEntity.scala @@ -61,6 +61,11 @@ sealed trait HttpEntity extends japi.HttpEntity { }) .toFuture(materializer) + /** + * Creates a copy of this HttpEntity with the `contentType` overridden with the given one. + */ + def withContentType(contentType: ContentType): HttpEntity + /** Java API */ def getDataBytes(materializer: FlowMaterializer): Publisher[ByteString] = dataBytes(materializer) @@ -102,6 +107,7 @@ object HttpEntity { * Close-delimited entities are not `Regular` as they exists primarily for backwards compatibility with HTTP/1.0. */ sealed trait Regular extends japi.HttpEntityRegular with HttpEntity { + def withContentType(contentType: ContentType): HttpEntity.Regular override def isRegular: Boolean = true } @@ -120,6 +126,8 @@ object HttpEntity { override def toStrict(timeout: FiniteDuration, materializer: FlowMaterializer)(implicit ec: ExecutionContext): Future[Strict] = Future.successful(this) + + def withContentType(contentType: ContentType): Strict = copy(contentType = contentType) } /** @@ -133,6 +141,8 @@ object HttpEntity { override def isDefault: Boolean = true def dataBytes(materializer: FlowMaterializer): Publisher[ByteString] = data + + def withContentType(contentType: ContentType): Default = copy(contentType = contentType) } /** @@ -145,6 +155,8 @@ object HttpEntity { override def isCloseDelimited: Boolean = true def dataBytes(materializer: FlowMaterializer): Publisher[ByteString] = data + + def withContentType(contentType: ContentType): CloseDelimited = copy(contentType = contentType) } /** @@ -157,6 +169,8 @@ object HttpEntity { def dataBytes(materializer: FlowMaterializer): Publisher[ByteString] = Flow(chunks).map(_.data).filter(_.nonEmpty).toPublisher(materializer) + def withContentType(contentType: ContentType): Chunked = copy(contentType = contentType) + /** Java API */ def getChunks: Publisher[japi.ChunkStreamPart] = chunks.asInstanceOf[Publisher[japi.ChunkStreamPart]] } diff --git a/akka-http-core/src/main/scala/akka/http/model/HttpMessage.scala b/akka-http-core/src/main/scala/akka/http/model/HttpMessage.scala index 8ba6c51d6a..6521f4a42b 100644 --- a/akka-http-core/src/main/scala/akka/http/model/HttpMessage.scala +++ b/akka-http-core/src/main/scala/akka/http/model/HttpMessage.scala @@ -221,40 +221,6 @@ final case class HttpRequest(method: HttpMethod = HttpMethods.GET, case x ⇒ x collectFirst { case r if r matches encoding ⇒ r.qValue } getOrElse 0f } - /** - * Determines whether one of the given content-types is accepted by the client. - * If a given ContentType does not define a charset an accepted charset is selected, i.e. the method guarantees - * that, if a ContentType instance is returned within the option, it will contain a defined charset. - */ - def acceptableContentType(contentTypes: IndexedSeq[ContentType]): Option[ContentType] = { - val mediaRanges = acceptedMediaRanges // cache for performance - val charsetRanges = acceptedCharsetRanges // cache for performance - - @tailrec def findBest(ix: Int = 0, result: ContentType = null, maxQ: Float = 0f): Option[ContentType] = - if (ix < contentTypes.size) { - val ct = contentTypes(ix) - val q = qValueForMediaType(ct.mediaType, mediaRanges) - if (q > maxQ && (ct.noCharsetDefined || isCharsetAccepted(ct.charset, charsetRanges))) findBest(ix + 1, ct, q) - else findBest(ix + 1, result, maxQ) - } else Option(result) - - findBest() flatMap { ct ⇒ - def withCharset(cs: HttpCharset) = Some(ContentType(ct.mediaType, cs)) - - // logic for choosing the charset adapted from http://tools.ietf.org/html/rfc7231#section-5.3.3 - if (ct.isCharsetDefined) Some(ct) // if there is already an acceptable charset chosen we are done - else if (qValueForCharset(`UTF-8`, charsetRanges) == 1f) withCharset(`UTF-8`) // prefer UTF-8 if fully accepted - else charsetRanges match { // ranges are sorted by descending q-value, - case (HttpCharsetRange.One(cs, qValue)) :: _ ⇒ // so we only need to look at the first one - if (qValue == 1f) withCharset(cs) // if the client has high preference for this charset, pick it - else if (qValueForCharset(`ISO-8859-1`, charsetRanges) == 1f) withCharset(`ISO-8859-1`) // give some more preference to `ISO-8859-1` - else if (qValue > 0f) withCharset(cs) // ok, simply choose the first one if the client doesn't reject it - else None - case _ ⇒ None - } - } - } - /** * Determines whether this request can be safely retried, which is the case only of the request method is idempotent. */ diff --git a/akka-http-core/src/main/scala/akka/http/rendering/HttpRequestRendererFactory.scala b/akka-http-core/src/main/scala/akka/http/rendering/HttpRequestRendererFactory.scala index bfa5c38d8b..c4f6f8398b 100644 --- a/akka-http-core/src/main/scala/akka/http/rendering/HttpRequestRendererFactory.scala +++ b/akka-http-core/src/main/scala/akka/http/rendering/HttpRequestRendererFactory.scala @@ -7,7 +7,6 @@ package akka.http.rendering import java.net.InetSocketAddress import org.reactivestreams.Publisher import scala.annotation.tailrec -import scala.collection.immutable import akka.event.LoggingAdapter import akka.util.ByteString import akka.stream.scaladsl.Flow @@ -30,7 +29,7 @@ private[http] class HttpRequestRendererFactory(userAgentHeader: Option[headers.` final class HttpRequestRenderer extends Transformer[RequestRenderingContext, Publisher[ByteString]] { - def onNext(ctx: RequestRenderingContext): immutable.Seq[Publisher[ByteString]] = { + def onNext(ctx: RequestRenderingContext): List[Publisher[ByteString]] = { val r = new ByteStringRendering(requestHeaderSizeHint) import ctx.request._ @@ -74,11 +73,8 @@ private[http] class HttpRequestRendererFactory(userAgentHeader: Option[headers.` case x: `Raw-Request-URI` ⇒ // we never render this header renderHeaders(tail, hostHeaderSeen, userAgentSeen) - case x: RawHeader if x.lowercaseName == "content-type" || - x.lowercaseName == "content-length" || - x.lowercaseName == "transfer-encoding" || - x.lowercaseName == "host" || - x.lowercaseName == "user-agent" ⇒ + case x: RawHeader if (x is "content-type") || (x is "content-length") || (x is "transfer-encoding") || + (x is "host") || (x is "user-agent") ⇒ suppressionWarning(log, x, "illegal RawHeader") renderHeaders(tail, hostHeaderSeen, userAgentSeen) @@ -97,30 +93,31 @@ private[http] class HttpRequestRendererFactory(userAgentHeader: Option[headers.` r ~~ CrLf } - def completeRequestRendering(): immutable.Seq[Publisher[ByteString]] = + def completeRequestRendering(): List[Publisher[ByteString]] = entity match { - case HttpEntity.Strict(contentType, data) ⇒ + case x if x.isKnownEmpty ⇒ + renderContentLength(0) + SynchronousPublisherFromIterable(r.get :: Nil) :: Nil + + case HttpEntity.Strict(_, data) ⇒ renderContentLength(data.length) SynchronousPublisherFromIterable(r.get :: data :: Nil) :: Nil - case HttpEntity.Default(contentType, contentLength, data) ⇒ + case HttpEntity.Default(_, contentLength, data) ⇒ renderContentLength(contentLength) renderByteStrings(r, Flow(data).transform(new CheckContentLengthTransformer(contentLength)).toPublisher(materializer), materializer) - case HttpEntity.Chunked(contentType, chunks) ⇒ - r ~~ `Transfer-Encoding` ~~ Chunked ~~ CrLf ~~ CrLf + case HttpEntity.Chunked(_, chunks) ⇒ + r ~~ `Transfer-Encoding` ~~ ChunkedBytes ~~ CrLf ~~ CrLf renderByteStrings(r, Flow(chunks).transform(new ChunkTransformer).toPublisher(materializer), materializer) } renderRequestLine() renderHeaders(headers.toList) renderEntityContentType(r, entity) - if (entity.isKnownEmpty) { - renderContentLength(0) - SynchronousPublisherFromIterable(r.get :: Nil) :: Nil - } else completeRequestRendering() + completeRequestRendering() } } } diff --git a/akka-http-core/src/main/scala/akka/http/rendering/HttpResponseRendererFactory.scala b/akka-http-core/src/main/scala/akka/http/rendering/HttpResponseRendererFactory.scala index aed0134520..cf49ba1424 100644 --- a/akka-http-core/src/main/scala/akka/http/rendering/HttpResponseRendererFactory.scala +++ b/akka-http-core/src/main/scala/akka/http/rendering/HttpResponseRendererFactory.scala @@ -6,7 +6,6 @@ package akka.http.rendering import org.reactivestreams.Publisher import scala.annotation.tailrec -import scala.collection.immutable import akka.event.LoggingAdapter import akka.util.ByteString import akka.stream.scaladsl.Flow @@ -59,14 +58,14 @@ private[http] class HttpResponseRendererFactory(serverHeader: Option[headers.Ser override def isComplete = close - def onNext(ctx: ResponseRenderingContext): immutable.Seq[Publisher[ByteString]] = { + def onNext(ctx: ResponseRenderingContext): List[Publisher[ByteString]] = { val r = new ByteStringRendering(responseHeaderSizeHint) import ctx.response._ val noEntity = entity.isKnownEmpty || ctx.requestMethod == HttpMethods.HEAD def renderStatusLine(): Unit = - if (status eq StatusCodes.OK) r ~~ DefaultStatusLine else r ~~ StatusLineStart ~~ status ~~ CrLf + if (status eq StatusCodes.OK) r ~~ DefaultStatusLineBytes else r ~~ StatusLineStartBytes ~~ status ~~ CrLf def render(h: HttpHeader) = r ~~ h ~~ CrLf @@ -94,12 +93,8 @@ private[http] class HttpResponseRendererFactory(serverHeader: Option[headers.Ser render(x) renderHeaders(tail, alwaysClose, connHeader, serverHeaderSeen = true) - case x: RawHeader if x.lowercaseName == "content-type" || - x.lowercaseName == "content-length" || - x.lowercaseName == "transfer-encoding" || - x.lowercaseName == "date" || - x.lowercaseName == "server" || - x.lowercaseName == "connection" ⇒ + case x: RawHeader if (x is "content-type") || (x is "content-length") || (x is "transfer-encoding") || + (x is "date") || (x is "server") || (x is "connection") ⇒ suppressionWarning(log, x, "illegal RawHeader") renderHeaders(tail, alwaysClose, connHeader, serverHeaderSeen) @@ -115,31 +110,31 @@ private[http] class HttpResponseRendererFactory(serverHeader: Option[headers.Ser ctx.closeAfterResponseCompletion || // request wants to close (connHeader != null && connHeader.hasClose) // application wants to close ctx.requestProtocol match { - case `HTTP/1.0` if !close ⇒ r ~~ Connection ~~ KeepAlive ~~ CrLf - case `HTTP/1.1` if close ⇒ r ~~ Connection ~~ Close ~~ CrLf + case `HTTP/1.0` if !close ⇒ r ~~ Connection ~~ KeepAliveBytes ~~ CrLf + case `HTTP/1.1` if close ⇒ r ~~ Connection ~~ CloseBytes ~~ CrLf case _ ⇒ // no need for rendering } } - def byteStrings(entityBytes: ⇒ Publisher[ByteString]): immutable.Seq[Publisher[ByteString]] = + def byteStrings(entityBytes: ⇒ Publisher[ByteString]): List[Publisher[ByteString]] = renderByteStrings(r, entityBytes, materializer, skipEntity = noEntity) - def completeResponseRendering(entity: HttpEntity): immutable.Seq[Publisher[ByteString]] = + def completeResponseRendering(entity: HttpEntity): List[Publisher[ByteString]] = entity match { - case HttpEntity.Strict(contentType, data) ⇒ + case HttpEntity.Strict(_, data) ⇒ renderHeaders(headers.toList) renderEntityContentType(r, entity) r ~~ `Content-Length` ~~ data.length ~~ CrLf ~~ CrLf val entityBytes = if (noEntity) Nil else data :: Nil SynchronousPublisherFromIterable(r.get :: entityBytes) :: Nil - case HttpEntity.Default(contentType, contentLength, data) ⇒ + case HttpEntity.Default(_, contentLength, data) ⇒ renderHeaders(headers.toList) renderEntityContentType(r, entity) r ~~ `Content-Length` ~~ contentLength ~~ CrLf ~~ CrLf byteStrings(Flow(data).transform(new CheckContentLengthTransformer(contentLength)).toPublisher(materializer)) - case HttpEntity.CloseDelimited(contentType, data) ⇒ + case HttpEntity.CloseDelimited(_, data) ⇒ renderHeaders(headers.toList, alwaysClose = true) renderEntityContentType(r, entity) r ~~ CrLf @@ -152,7 +147,7 @@ private[http] class HttpResponseRendererFactory(serverHeader: Option[headers.Ser renderHeaders(headers.toList) renderEntityContentType(r, entity) if (!entity.isKnownEmpty || ctx.requestMethod == HttpMethods.HEAD) - r ~~ `Transfer-Encoding` ~~ Chunked ~~ CrLf + r ~~ `Transfer-Encoding` ~~ ChunkedBytes ~~ CrLf r ~~ CrLf byteStrings(Flow(chunks).transform(new ChunkTransformer).toPublisher(materializer)) } diff --git a/akka-http-core/src/main/scala/akka/http/rendering/RenderSupport.scala b/akka-http-core/src/main/scala/akka/http/rendering/RenderSupport.scala index ff851f073d..58c60402ac 100644 --- a/akka-http-core/src/main/scala/akka/http/rendering/RenderSupport.scala +++ b/akka-http-core/src/main/scala/akka/http/rendering/RenderSupport.scala @@ -5,7 +5,6 @@ package akka.http.rendering import org.reactivestreams.Publisher -import scala.collection.immutable import akka.parboiled2.CharUtils import akka.util.ByteString import akka.event.LoggingAdapter @@ -19,11 +18,11 @@ import akka.http.util._ * INTERNAL API */ private object RenderSupport { - val DefaultStatusLine = "HTTP/1.1 200 OK\r\n".getAsciiBytes - val StatusLineStart = "HTTP/1.1 ".getAsciiBytes - val Chunked = "chunked".getAsciiBytes - val KeepAlive = "Keep-Alive".getAsciiBytes - val Close = "close".getAsciiBytes + val DefaultStatusLineBytes = "HTTP/1.1 200 OK\r\n".getAsciiBytes + val StatusLineStartBytes = "HTTP/1.1 ".getAsciiBytes + val ChunkedBytes = "chunked".getAsciiBytes + val KeepAliveBytes = "Keep-Alive".getAsciiBytes + val CloseBytes = "close".getAsciiBytes def CrLf = Rendering.CrLf @@ -36,7 +35,7 @@ private object RenderSupport { r ~~ headers.`Content-Type` ~~ entity.contentType ~~ CrLf def renderByteStrings(r: ByteStringRendering, entityBytes: ⇒ Publisher[ByteString], materializer: FlowMaterializer, - skipEntity: Boolean = false): immutable.Seq[Publisher[ByteString]] = { + skipEntity: Boolean = false): List[Publisher[ByteString]] = { val messageStart = SynchronousPublisherFromIterable(r.get :: Nil) val messageBytes = if (!skipEntity) Flow(messageStart).concat(entityBytes).toPublisher(materializer) @@ -46,7 +45,7 @@ private object RenderSupport { class ChunkTransformer extends Transformer[HttpEntity.ChunkStreamPart, ByteString] { var lastChunkSeen = false - def onNext(chunk: HttpEntity.ChunkStreamPart): immutable.Seq[ByteString] = { + def onNext(chunk: HttpEntity.ChunkStreamPart): List[ByteString] = { if (chunk.isLastChunk) lastChunkSeen = true renderChunk(chunk) :: Nil } @@ -56,14 +55,14 @@ private object RenderSupport { class CheckContentLengthTransformer(length: Long) extends Transformer[ByteString, ByteString] { var sent = 0L - def onNext(elem: ByteString): immutable.Seq[ByteString] = { + def onNext(elem: ByteString): List[ByteString] = { sent += elem.length if (sent > length) throw new InvalidContentLengthException(s"HTTP message had declared Content-Length $length but entity chunk stream amounts to more bytes") elem :: Nil } - override def onTermination(e: Option[Throwable]): immutable.Seq[ByteString] = { + override def onTermination(e: Option[Throwable]): List[ByteString] = { if (sent < length) throw new InvalidContentLengthException(s"HTTP message had declared Content-Length $length but entity chunk stream amounts to ${length - sent} bytes less") Nil diff --git a/akka-http-core/src/test/scala/akka/http/model/ContentNegotiationSpec.scala b/akka-http-core/src/test/scala/akka/http/model/ContentNegotiationSpec.scala deleted file mode 100644 index e8597fc847..0000000000 --- a/akka-http-core/src/test/scala/akka/http/model/ContentNegotiationSpec.scala +++ /dev/null @@ -1,127 +0,0 @@ -/** - * Copyright (C) 2009-2014 Typesafe Inc. - */ - -package akka.http.model - -import org.scalatest.{ Matchers, FreeSpec } -import akka.http.model.parser.HeaderParser -import headers._ -import MediaTypes._ -import HttpCharsets._ - -class ContentNegotiationSpec extends FreeSpec with Matchers { - - "Content Negotiation should work properly for requests with header(s)" - { - - "(without headers)" in test { accept ⇒ - accept(`text/plain`) should select(`text/plain`, `UTF-8`) - accept(`text/plain` withCharset `UTF-16`) should select(`text/plain`, `UTF-16`) - } - - "Accept: */*" in test { accept ⇒ - accept(`text/plain`) should select(`text/plain`, `UTF-8`) - accept(`text/plain` withCharset `UTF-16`) should select(`text/plain`, `UTF-16`) - } - - "Accept: */*;q=.8" in test { accept ⇒ - accept(`text/plain`) should select(`text/plain`, `UTF-8`) - accept(`text/plain` withCharset `UTF-16`) should select(`text/plain`, `UTF-16`) - } - - "Accept: text/*" in test { accept ⇒ - accept(`text/plain`) should select(`text/plain`, `UTF-8`) - accept(`text/xml` withCharset `UTF-16`) should select(`text/xml`, `UTF-16`) - accept(`audio/ogg`) should reject - } - - "Accept: text/*;q=.8" in test { accept ⇒ - accept(`text/plain`) should select(`text/plain`, `UTF-8`) - accept(`text/xml` withCharset `UTF-16`) should select(`text/xml`, `UTF-16`) - accept(`audio/ogg`) should reject - } - - "Accept: text/*;q=0" in test { accept ⇒ - accept(`text/plain`) should reject - accept(`text/xml` withCharset `UTF-16`) should reject - accept(`audio/ogg`) should reject - } - - "Accept-Charset: UTF-16" in test { accept ⇒ - accept(`text/plain`) should select(`text/plain`, `UTF-16`) - accept(`text/plain` withCharset `UTF-8`) should reject - } - - "Accept-Charset: UTF-16, UTF-8" in test { accept ⇒ - accept(`text/plain`) should select(`text/plain`, `UTF-8`) - accept(`text/plain` withCharset `UTF-16`) should select(`text/plain`, `UTF-16`) - } - - "Accept-Charset: UTF-8;q=.2, UTF-16" in test { accept ⇒ - accept(`text/plain`) should select(`text/plain`, `UTF-16`) - accept(`text/plain` withCharset `UTF-8`) should select(`text/plain`, `UTF-8`) - } - - "Accept-Charset: UTF-8;q=.2" in test { accept ⇒ - accept(`text/plain`) should select(`text/plain`, `ISO-8859-1`) - accept(`text/plain` withCharset `UTF-8`) should select(`text/plain`, `UTF-8`) - } - - "Accept-Charset: latin1;q=.1, UTF-8;q=.2" in test { accept ⇒ - accept(`text/plain`) should select(`text/plain`, `UTF-8`) - accept(`text/plain` withCharset `UTF-8`) should select(`text/plain`, `UTF-8`) - } - - "Accept-Charset: *" in test { accept ⇒ - accept(`text/plain`) should select(`text/plain`, `UTF-8`) - accept(`text/plain` withCharset `UTF-16`) should select(`text/plain`, `UTF-16`) - } - - "Accept-Charset: *;q=0" in test { accept ⇒ - accept(`text/plain`) should reject - accept(`text/plain` withCharset `UTF-16`) should reject - } - - "Accept-Charset: us;q=0.1,*;q=0" in test { accept ⇒ - accept(`text/plain`) should select(`text/plain`, `US-ASCII`) - accept(`text/plain` withCharset `UTF-8`) should reject - } - - "Accept: text/xml, text/html;q=.5" in test { accept ⇒ - accept(`text/plain`) should reject - accept(`text/xml`) should select(`text/xml`, `UTF-8`) - accept(`text/html`) should select(`text/html`, `UTF-8`) - accept(`text/html`, `text/xml`) should select(`text/xml`, `UTF-8`) - accept(`text/xml`, `text/html`) should select(`text/xml`, `UTF-8`) - accept(`text/plain`, `text/xml`) should select(`text/xml`, `UTF-8`) - accept(`text/plain`, `text/html`) should select(`text/html`, `UTF-8`) - } - - """Accept: text/html, text/plain;q=0.8, application/*;q=.5, *;q= .2 - Accept-Charset: UTF-16""" in test { accept ⇒ - accept(`text/plain`, `text/html`, `audio/ogg`) should select(`text/html`, `UTF-16`) - accept(`text/plain`, `text/html` withCharset `UTF-8`, `audio/ogg`) should select(`text/plain`, `UTF-16`) - accept(`audio/ogg`, `application/javascript`, `text/plain` withCharset `UTF-8`) should select(`application/javascript`, `UTF-16`) - accept(`image/gif`, `application/javascript`) should select(`application/javascript`, `UTF-16`) - accept(`image/gif`, `audio/ogg`) should select(`image/gif`, `UTF-16`) - } - } - - def test[U](body: ((ContentType*) ⇒ Option[ContentType]) ⇒ U): String ⇒ U = { example ⇒ - val headers = - if (example != "(without headers)") { - example.split('\n').toList map { rawHeader ⇒ - val Array(name, value) = rawHeader.split(':') - HeaderParser.parseHeader(RawHeader(name.trim, value.trim)) match { - case Right(header) ⇒ header - case Left(err) ⇒ fail(err.formatPretty) - } - } - } else Nil - val request = HttpRequest(headers = headers) - body(contentTypes ⇒ request.acceptableContentType(Vector(contentTypes: _*))) - } - - def reject = equal(None) - def select(mediaType: MediaType, charset: HttpCharset) = equal(Some(ContentType(mediaType, charset))) -} \ No newline at end of file