diff --git a/akka-http-core/src/main/scala/akka/http/engine/parsing/HttpMessageParser.scala b/akka-http-core/src/main/scala/akka/http/engine/parsing/HttpMessageParser.scala index 43856fae3d..889ca565b2 100644 --- a/akka-http-core/src/main/scala/akka/http/engine/parsing/HttpMessageParser.scala +++ b/akka-http-core/src/main/scala/akka/http/engine/parsing/HttpMessageParser.scala @@ -247,4 +247,10 @@ private[http] abstract class HttpMessageParser[Output >: ParserOutput.MessageOut val chunks = Flow(entityChunks).collect { case ParserOutput.EntityChunk(chunk) ⇒ chunk }.toPublisher() HttpEntity.Chunked(contentType(cth), chunks) } + + def addTransferEncodingWithChunkedPeeled(headers: List[HttpHeader], teh: `Transfer-Encoding`): List[HttpHeader] = + teh.withChunkedPeeled match { + case Some(x) ⇒ x :: headers + case None ⇒ headers + } } \ No newline at end of file diff --git a/akka-http-core/src/main/scala/akka/http/engine/parsing/HttpRequestParser.scala b/akka-http-core/src/main/scala/akka/http/engine/parsing/HttpRequestParser.scala index e1d46e4b78..0df5009828 100644 --- a/akka-http-core/src/main/scala/akka/http/engine/parsing/HttpRequestParser.scala +++ b/akka-http-core/src/main/scala/akka/http/engine/parsing/HttpRequestParser.scala @@ -110,7 +110,8 @@ private[http] class HttpRequestParser(_settings: ParserSettings, clh: Option[`Content-Length`], cth: Option[`Content-Type`], teh: Option[`Transfer-Encoding`], hostHeaderPresent: Boolean, closeAfterResponseCompletion: Boolean): StateResult = if (hostHeaderPresent || protocol == HttpProtocols.`HTTP/1.0`) { - def emitRequestStart(createEntity: Publisher[ParserOutput.RequestOutput] ⇒ RequestEntity) = + def emitRequestStart(createEntity: Publisher[ParserOutput.RequestOutput] ⇒ RequestEntity, + headers: List[HttpHeader] = headers) = emit(ParserOutput.RequestStart(method, uri, protocol, headers, createEntity, closeAfterResponseCompletion)) teh match { @@ -135,12 +136,14 @@ private[http] class HttpRequestParser(_settings: ParserSettings, } case Some(te) ⇒ - if (te.encodings.size == 1 && te.hasChunked) { + val completedHeaders = addTransferEncodingWithChunkedPeeled(headers, te) + if (te.isChunked) { if (clh.isEmpty) { - emitRequestStart(chunkedEntity(cth)) + emitRequestStart(chunkedEntity(cth), completedHeaders) parseChunk(input, bodyStart, closeAfterResponseCompletion) } else fail("A chunked request must not contain a Content-Length header.") - } else fail(NotImplemented, s"`$te` is not supported by this server") + } else parseEntity(completedHeaders, protocol, input, bodyStart, clh, cth, teh = None, hostHeaderPresent, + closeAfterResponseCompletion) } } else fail("Request is missing required `Host` header") } \ No newline at end of file diff --git a/akka-http-core/src/main/scala/akka/http/engine/parsing/HttpResponseParser.scala b/akka-http-core/src/main/scala/akka/http/engine/parsing/HttpResponseParser.scala index 173f86f684..5a0b85a4ae 100644 --- a/akka-http-core/src/main/scala/akka/http/engine/parsing/HttpResponseParser.scala +++ b/akka-http-core/src/main/scala/akka/http/engine/parsing/HttpResponseParser.scala @@ -75,7 +75,8 @@ private[http] class HttpResponseParser(_settings: ParserSettings, def parseEntity(headers: List[HttpHeader], protocol: HttpProtocol, input: ByteString, bodyStart: Int, clh: Option[`Content-Length`], cth: Option[`Content-Type`], teh: Option[`Transfer-Encoding`], hostHeaderPresent: Boolean, closeAfterResponseCompletion: Boolean): StateResult = { - def emitResponseStart(createEntity: Publisher[ParserOutput.ResponseOutput] ⇒ ResponseEntity) = + def emitResponseStart(createEntity: Publisher[ParserOutput.ResponseOutput] ⇒ ResponseEntity, + headers: List[HttpHeader] = headers) = emit(ParserOutput.ResponseStart(statusCode, protocol, headers, createEntity, closeAfterResponseCompletion)) def finishEmptyResponse() = { emitResponseStart(emptyEntity(cth)) @@ -106,12 +107,14 @@ private[http] class HttpResponseParser(_settings: ParserSettings, } case Some(te) ⇒ - if (te.encodings.size == 1 && te.hasChunked) { + val completedHeaders = addTransferEncodingWithChunkedPeeled(headers, te) + if (te.isChunked) { if (clh.isEmpty) { - emitResponseStart(chunkedEntity(cth)) + emitResponseStart(chunkedEntity(cth), completedHeaders) parseChunk(input, bodyStart, closeAfterResponseCompletion) } else fail("A chunked request must not contain a Content-Length header.") - } else fail(s"`$te` is not supported by this client") + } else parseEntity(completedHeaders, protocol, input, bodyStart, clh, cth, teh = None, hostHeaderPresent, + closeAfterResponseCompletion) } } else finishEmptyResponse() } diff --git a/akka-http-core/src/main/scala/akka/http/engine/rendering/HttpRequestRendererFactory.scala b/akka-http-core/src/main/scala/akka/http/engine/rendering/HttpRequestRendererFactory.scala index e2e5869783..5da5a605fa 100644 --- a/akka-http-core/src/main/scala/akka/http/engine/rendering/HttpRequestRendererFactory.scala +++ b/akka-http-core/src/main/scala/akka/http/engine/rendering/HttpRequestRendererFactory.scala @@ -46,45 +46,54 @@ private[http] class HttpRequestRendererFactory(userAgentHeader: Option[headers.` def render(h: HttpHeader) = r ~~ h ~~ CrLf @tailrec def renderHeaders(remaining: List[HttpHeader], hostHeaderSeen: Boolean = false, - userAgentSeen: Boolean = false): Unit = + userAgentSeen: Boolean = false, transferEncodingSeen: Boolean = false): Unit = remaining match { case head :: tail ⇒ head match { case x: `Content-Length` ⇒ suppressionWarning(log, x, "explicit `Content-Length` header is not allowed. Use the appropriate HttpEntity subtype.") - renderHeaders(tail, hostHeaderSeen, userAgentSeen) + renderHeaders(tail, hostHeaderSeen, userAgentSeen, transferEncodingSeen) case x: `Content-Type` ⇒ suppressionWarning(log, x, "explicit `Content-Type` header is not allowed. Set `HttpRequest.entity.contentType` instead.") - renderHeaders(tail, hostHeaderSeen, userAgentSeen) + renderHeaders(tail, hostHeaderSeen, userAgentSeen, transferEncodingSeen) - case `Transfer-Encoding`(_) ⇒ - suppressionWarning(log, head) - renderHeaders(tail, hostHeaderSeen, userAgentSeen) + case x: `Transfer-Encoding` ⇒ + x.withChunkedPeeled match { + case None ⇒ + suppressionWarning(log, head) + renderHeaders(tail, hostHeaderSeen, userAgentSeen, transferEncodingSeen) + case Some(te) ⇒ + // if the user applied some custom transfer-encoding we need to keep the header + render(if (entity.isChunked && !entity.isKnownEmpty) te.withChunked else te) + renderHeaders(tail, hostHeaderSeen, userAgentSeen, transferEncodingSeen = true) + } case x: `Host` ⇒ render(x) - renderHeaders(tail, hostHeaderSeen = true, userAgentSeen) + renderHeaders(tail, hostHeaderSeen = true, userAgentSeen, transferEncodingSeen) case x: `User-Agent` ⇒ render(x) - renderHeaders(tail, hostHeaderSeen, userAgentSeen = true) + renderHeaders(tail, hostHeaderSeen, userAgentSeen = true, transferEncodingSeen) case x: `Raw-Request-URI` ⇒ // we never render this header - renderHeaders(tail, hostHeaderSeen, userAgentSeen) + renderHeaders(tail, hostHeaderSeen, userAgentSeen, transferEncodingSeen) 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) + renderHeaders(tail, hostHeaderSeen, userAgentSeen, transferEncodingSeen) case x ⇒ render(x) - renderHeaders(tail, hostHeaderSeen, userAgentSeen) + renderHeaders(tail, hostHeaderSeen, userAgentSeen, transferEncodingSeen) } case Nil ⇒ if (!hostHeaderSeen) r ~~ Host(ctx.serverAddress) ~~ CrLf if (!userAgentSeen && userAgentHeader.isDefined) r ~~ userAgentHeader.get ~~ CrLf + if (entity.isChunked && !entity.isKnownEmpty && !transferEncodingSeen) + r ~~ `Transfer-Encoding` ~~ ChunkedBytes ~~ CrLf } def renderContentLength(contentLength: Long): Unit = { @@ -105,10 +114,10 @@ private[http] class HttpRequestRendererFactory(userAgentHeader: Option[headers.` case HttpEntity.Default(_, contentLength, data) ⇒ renderContentLength(contentLength) renderByteStrings(r, - Flow(data).transform("checkContentLenght", () ⇒ new CheckContentLengthTransformer(contentLength)).toPublisher()) + Flow(data).transform("checkContentLength", () ⇒ new CheckContentLengthTransformer(contentLength)).toPublisher()) case HttpEntity.Chunked(_, chunks) ⇒ - r ~~ `Transfer-Encoding` ~~ ChunkedBytes ~~ CrLf ~~ CrLf + r ~~ CrLf renderByteStrings(r, Flow(chunks).transform("chunkTransform", () ⇒ new ChunkTransformer).toPublisher()) } diff --git a/akka-http-core/src/main/scala/akka/http/engine/rendering/HttpResponseRendererFactory.scala b/akka-http-core/src/main/scala/akka/http/engine/rendering/HttpResponseRendererFactory.scala index 9ba51a9ceb..67df4524e8 100644 --- a/akka-http-core/src/main/scala/akka/http/engine/rendering/HttpResponseRendererFactory.scala +++ b/akka-http-core/src/main/scala/akka/http/engine/rendering/HttpResponseRendererFactory.scala @@ -68,38 +68,53 @@ private[http] class HttpResponseRendererFactory(serverHeader: Option[headers.Ser def render(h: HttpHeader) = r ~~ h ~~ CrLf + def mustRenderTransferEncodingChunkedHeader = + entity.isChunked && (!entity.isKnownEmpty || ctx.requestMethod == HttpMethods.HEAD) && (ctx.requestProtocol == `HTTP/1.1`) + @tailrec def renderHeaders(remaining: List[HttpHeader], alwaysClose: Boolean = false, - connHeader: Connection = null, serverHeaderSeen: Boolean = false): Unit = + connHeader: Connection = null, serverHeaderSeen: Boolean = false, + transferEncodingSeen: Boolean = false): Unit = remaining match { case head :: tail ⇒ head match { case x: `Content-Length` ⇒ suppressionWarning(log, x, "explicit `Content-Length` header is not allowed. Use the appropriate HttpEntity subtype.") - renderHeaders(tail, alwaysClose, connHeader, serverHeaderSeen) + renderHeaders(tail, alwaysClose, connHeader, serverHeaderSeen, transferEncodingSeen) case x: `Content-Type` ⇒ suppressionWarning(log, x, "explicit `Content-Type` header is not allowed. Set `HttpResponse.entity.contentType` instead.") - renderHeaders(tail, alwaysClose, connHeader, serverHeaderSeen) + renderHeaders(tail, alwaysClose, connHeader, serverHeaderSeen, transferEncodingSeen) - case `Transfer-Encoding`(_) | Date(_) ⇒ + case Date(_) ⇒ suppressionWarning(log, head) - renderHeaders(tail, alwaysClose, connHeader, serverHeaderSeen) + renderHeaders(tail, alwaysClose, connHeader, serverHeaderSeen, transferEncodingSeen) + + case x: `Transfer-Encoding` ⇒ + x.withChunkedPeeled match { + case None ⇒ + suppressionWarning(log, head) + renderHeaders(tail, alwaysClose, connHeader, serverHeaderSeen, transferEncodingSeen) + case Some(te) ⇒ + // if the user applied some custom transfer-encoding we need to keep the header + render(if (mustRenderTransferEncodingChunkedHeader) te.withChunked else te) + renderHeaders(tail, alwaysClose, connHeader, serverHeaderSeen, transferEncodingSeen = true) + } case x: `Connection` ⇒ val connectionHeader = if (connHeader eq null) x else Connection(x.tokens ++ connHeader.tokens) - renderHeaders(tail, alwaysClose, connectionHeader, serverHeaderSeen) + renderHeaders(tail, alwaysClose, connectionHeader, serverHeaderSeen, transferEncodingSeen) case x: `Server` ⇒ render(x) - renderHeaders(tail, alwaysClose, connHeader, serverHeaderSeen = true) + renderHeaders(tail, alwaysClose, connHeader, serverHeaderSeen = true, transferEncodingSeen) 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) + renderHeaders(tail, alwaysClose, connHeader, serverHeaderSeen, transferEncodingSeen) case x ⇒ render(x) - renderHeaders(tail, alwaysClose, connHeader, serverHeaderSeen) + renderHeaders(tail, alwaysClose, connHeader, serverHeaderSeen, transferEncodingSeen) } case Nil ⇒ @@ -113,6 +128,8 @@ private[http] class HttpResponseRendererFactory(serverHeader: Option[headers.Ser case `HTTP/1.1` if close ⇒ r ~~ Connection ~~ CloseBytes ~~ CrLf case _ ⇒ // no need for rendering } + if (mustRenderTransferEncodingChunkedHeader && !transferEncodingSeen) + r ~~ `Transfer-Encoding` ~~ ChunkedBytes ~~ CrLf } def byteStrings(entityBytes: ⇒ Publisher[ByteString]): List[Publisher[ByteString]] = @@ -145,8 +162,6 @@ private[http] class HttpResponseRendererFactory(serverHeader: Option[headers.Ser else { renderHeaders(headers.toList) renderEntityContentType(r, entity) - if (!entity.isKnownEmpty || ctx.requestMethod == HttpMethods.HEAD) - r ~~ `Transfer-Encoding` ~~ ChunkedBytes ~~ CrLf r ~~ CrLf byteStrings(Flow(chunks).transform("renderChunks", () ⇒ new ChunkTransformer).toPublisher()) } diff --git a/akka-http-core/src/main/scala/akka/http/model/headers/headers.scala b/akka-http-core/src/main/scala/akka/http/model/headers/headers.scala index 3ed42c6f63..05d20d8e17 100644 --- a/akka-http-core/src/main/scala/akka/http/model/headers/headers.scala +++ b/akka-http-core/src/main/scala/akka/http/model/headers/headers.scala @@ -536,7 +536,15 @@ object `Transfer-Encoding` extends ModeledCompanion { final case class `Transfer-Encoding`(encodings: immutable.Seq[TransferEncoding]) extends japi.headers.TransferEncoding with ModeledHeader { require(encodings.nonEmpty, "encodings must not be empty") import `Transfer-Encoding`.encodingsRenderer - def hasChunked: Boolean = encodings contains TransferEncodings.chunked + def isChunked: Boolean = encodings.last == TransferEncodings.chunked + def withChunked: `Transfer-Encoding` = if (isChunked) this else `Transfer-Encoding`(encodings :+ TransferEncodings.chunked) + def withChunkedPeeled: Option[`Transfer-Encoding`] = + if (isChunked) { + encodings.init match { + case Nil ⇒ None + case remaining ⇒ Some(`Transfer-Encoding`(remaining)) + } + } else Some(this) def renderValue[R <: Rendering](r: R): r.type = r ~~ encodings protected def companion = `Transfer-Encoding` diff --git a/akka-http-core/src/main/scala/akka/http/util/package.scala b/akka-http-core/src/main/scala/akka/http/util/package.scala index c51e9271ee..733b22e192 100644 --- a/akka-http-core/src/main/scala/akka/http/util/package.scala +++ b/akka-http-core/src/main/scala/akka/http/util/package.scala @@ -7,7 +7,7 @@ package akka.http import language.implicitConversions import java.nio.charset.Charset import com.typesafe.config.Config -import org.reactivestreams.{Subscription, Subscriber, Publisher} +import org.reactivestreams.{ Subscription, Subscriber, Publisher } import scala.util.matching.Regex import akka.event.LoggingAdapter import akka.util.ByteString diff --git a/akka-http-core/src/test/scala/akka/http/engine/parsing/RequestParserSpec.scala b/akka-http-core/src/test/scala/akka/http/engine/parsing/RequestParserSpec.scala index 4b4e0f942d..dd914e883c 100644 --- a/akka-http-core/src/test/scala/akka/http/engine/parsing/RequestParserSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/engine/parsing/RequestParserSpec.scala @@ -133,6 +133,16 @@ class RequestParserSpec extends FreeSpec with Matchers with BeforeAndAfterAll { closeAfterResponseCompletion shouldEqual Seq(true) } + "with a funky `Transfer-Encoding` header" in new Test { + """GET / HTTP/1.1 + |Transfer-Encoding: foo, chunked, bar + |Host: x + | + |""" should parseTo(HttpRequest(GET, "/", List(`Transfer-Encoding`(TransferEncodings.Extension("foo"), + TransferEncodings.chunked, TransferEncodings.Extension("bar")), Host("x")))) + closeAfterResponseCompletion shouldEqual Seq(false) + } + "with several identical `Content-Type` headers" in new Test { """GET /data HTTP/1.1 |Host: x @@ -207,6 +217,17 @@ class RequestParserSpec extends FreeSpec with Matchers with BeforeAndAfterAll { } } + "properly parse a chunked request with additional transfer encodings" in new Test { + """PATCH /data HTTP/1.1 + |Transfer-Encoding: fancy, chunked + |Content-Type: application/pdf + |Host: ping + | + |""" should parseTo(HttpRequest(PATCH, "/data", List(`Transfer-Encoding`(TransferEncodings.Extension("fancy")), + Host("ping")), HttpEntity.Chunked(`application/pdf`, publisher()))) + closeAfterResponseCompletion shouldEqual Seq(false) + } + "reject a message chunk with" - { val start = """PATCH /data HTTP/1.1 diff --git a/akka-http-core/src/test/scala/akka/http/engine/parsing/ResponseParserSpec.scala b/akka-http-core/src/test/scala/akka/http/engine/parsing/ResponseParserSpec.scala index f8953cd402..d16cfb5f0a 100644 --- a/akka-http-core/src/test/scala/akka/http/engine/parsing/ResponseParserSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/engine/parsing/ResponseParserSpec.scala @@ -66,6 +66,19 @@ class ResponseParserSpec extends FreeSpec with Matchers with BeforeAndAfterAll { closeAfterResponseCompletion shouldEqual Seq(false) } + "a response funky `Transfer-Encoding` header" in new Test { + override def parserSettings: ParserSettings = + super.parserSettings.withCustomStatusCodes(ServerOnTheMove) + + """HTTP/1.1 331 Server on the move + |Transfer-Encoding: foo, chunked, bar + |Content-Length: 0 + | + |""" should parseTo(HttpResponse(ServerOnTheMove, List(`Transfer-Encoding`(TransferEncodings.Extension("foo"), + TransferEncodings.chunked, TransferEncodings.Extension("bar"))))) + closeAfterResponseCompletion shouldEqual Seq(false) + } + "a response with one header, a body, but no Content-Length header" in new Test { """HTTP/1.0 404 Not Found |Host: api.example.com @@ -167,6 +180,16 @@ class ResponseParserSpec extends FreeSpec with Matchers with BeforeAndAfterAll { publisher(LastChunk("nice=true", List(RawHeader("Bar", "xyz"), RawHeader("Foo", "pip apo")))))))) closeAfterResponseCompletion shouldEqual Seq(false) } + + "response with additional transfer encodings" in new Test { + """HTTP/1.1 200 OK + |Transfer-Encoding: fancy, chunked + |Content-Type: application/pdf + | + |""" should parseTo(HttpResponse(headers = List(`Transfer-Encoding`(TransferEncodings.Extension("fancy"))), + entity = HttpEntity.Chunked(`application/pdf`, publisher()))) + closeAfterResponseCompletion shouldEqual Seq(false) + } } "reject a response with" - { diff --git a/akka-http-core/src/test/scala/akka/http/engine/rendering/RequestRendererSpec.scala b/akka-http-core/src/test/scala/akka/http/engine/rendering/RequestRendererSpec.scala index 484d291f95..d0ca01a784 100644 --- a/akka-http-core/src/test/scala/akka/http/engine/rendering/RequestRendererSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/engine/rendering/RequestRendererSpec.scala @@ -90,7 +90,7 @@ class RequestRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll HttpRequest(PUT, "/abc/xyz", List( RawHeader("X-Fancy", "naa"), RawHeader("Cache-Control", "public"), - Host("spray.io"))).withEntity(HttpEntity(ContentTypes.NoContentType, "The content please!")) should renderTo { + Host("spray.io")), HttpEntity(ContentTypes.NoContentType, "The content please!")) should renderTo { """PUT /abc/xyz HTTP/1.1 |X-Fancy: naa |Cache-Control: public @@ -101,12 +101,26 @@ class RequestRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll |The content please!""" } } + + "PUT request with a custom Transfer-Encoding header" in new TestSetup() { + HttpRequest(PUT, "/abc/xyz", List(`Transfer-Encoding`(TransferEncodings.Extension("fancy")))) + .withEntity("The content please!") should renderTo { + """PUT /abc/xyz HTTP/1.1 + |Transfer-Encoding: fancy + |Host: test.com:8080 + |User-Agent: spray-can/1.0.0 + |Content-Type: text/plain; charset=UTF-8 + |Content-Length: 19 + | + |The content please!""" + } + } } "proper render a chunked" - { "PUT request with empty chunk stream and custom Content-Type" in new TestSetup() { - HttpRequest(PUT, "/abc/xyz").withEntity(Chunked(ContentTypes.`text/plain`, publisher())) should renderTo { + HttpRequest(PUT, "/abc/xyz", entity = Chunked(ContentTypes.`text/plain`, publisher())) should renderTo { """PUT /abc/xyz HTTP/1.1 |Host: test.com:8080 |User-Agent: spray-can/1.0.0 @@ -118,13 +132,32 @@ class RequestRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll } "POST request with body" in new TestSetup() { - HttpRequest(POST, "/abc/xyz") - .withEntity(Chunked(ContentTypes.`text/plain`, publisher("XXXX", "ABCDEFGHIJKLMNOPQRSTUVWXYZ"))) should renderTo { + HttpRequest(POST, "/abc/xyz", entity = Chunked(ContentTypes.`text/plain`, + publisher("XXXX", "ABCDEFGHIJKLMNOPQRSTUVWXYZ"))) should renderTo { + """POST /abc/xyz HTTP/1.1 + |Host: test.com:8080 + |User-Agent: spray-can/1.0.0 + |Transfer-Encoding: chunked + |Content-Type: text/plain + | + |4 + |XXXX + |1a + |ABCDEFGHIJKLMNOPQRSTUVWXYZ + |0 + | + |""" + } + } + + "POST request with custom Transfer-Encoding header" in new TestSetup() { + HttpRequest(POST, "/abc/xyz", List(`Transfer-Encoding`(TransferEncodings.Extension("fancy"))), + entity = Chunked(ContentTypes.`text/plain`, publisher("XXXX", "ABCDEFGHIJKLMNOPQRSTUVWXYZ"))) should renderTo { """POST /abc/xyz HTTP/1.1 + |Transfer-Encoding: fancy, chunked |Host: test.com:8080 |User-Agent: spray-can/1.0.0 |Content-Type: text/plain - |Transfer-Encoding: chunked | |4 |XXXX 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 1cc7dce14b..f96594d263 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 @@ -31,8 +31,8 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll implicit val materializer = FlowMaterializer() "The response preparation logic should properly render" - { - "a response with no body" - { - "with status 200, no headers and no body" in new TestSetup() { + "a response with no body," - { + "status 200 and no headers" in new TestSetup() { HttpResponse(200) should renderTo { """HTTP/1.1 200 OK |Server: akka-http/1.0.0 @@ -43,7 +43,7 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll } } - "with status 304, a few headers and no body" in new TestSetup() { + "status 304 and a few headers" in new TestSetup() { HttpResponse(304, List(RawHeader("X-Fancy", "of course"), RawHeader("Age", "0"))) should renderTo { """HTTP/1.1 304 Not Modified |X-Fancy: of course @@ -55,7 +55,7 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll |""" } } - "with a custom status code, no headers and no body" in new TestSetup() { + "a custom status code and no headers" in new TestSetup() { HttpResponse(ServerOnTheMove) should renderTo { """HTTP/1.1 330 Server on the move |Server: akka-http/1.0.0 @@ -83,8 +83,8 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll } } - "a response with a Strict body" - { - "with status 400, a few headers and a body" in new TestSetup() { + "a response with a Strict body," - { + "status 400 and a few headers" in new TestSetup() { HttpResponse(400, List(RawHeader("Age", "30"), Connection("Keep-Alive")), "Small f*ck up overhere!") should renderTo { """HTTP/1.1 400 Bad Request |Age: 30 @@ -97,7 +97,7 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll } } - "with status 400, a few headers and a body with an explicitly suppressed Content Type header" in new TestSetup() { + "status 400, a few headers and a body with an explicitly suppressed Content Type header" in new TestSetup() { HttpResponse(400, List(RawHeader("Age", "30"), Connection("Keep-Alive")), HttpEntity(contentType = ContentTypes.NoContentType, "Small f*ck up overhere!")) should renderTo { """HTTP/1.1 400 Bad Request @@ -109,9 +109,23 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll |Small f*ck up overhere!""" } } + + "status 200 and a custom Transfer-Encoding header" in new TestSetup() { + HttpResponse(headers = List(`Transfer-Encoding`(TransferEncodings.Extension("fancy"))), + entity = "All good") should renderTo { + """HTTP/1.1 200 OK + |Transfer-Encoding: fancy + |Server: akka-http/1.0.0 + |Date: Thu, 25 Aug 2011 09:10:29 GMT + |Content-Type: text/plain; charset=UTF-8 + |Content-Length: 8 + | + |All good""" + } + } } - "a response with a Default (streamed with explicit content-length body" - { - "with status 400, a few headers and a body" in new TestSetup() { + "a response with a Default (streamed with explicit content-length body," - { + "status 400 and a few headers" in new TestSetup() { HttpResponse(400, List(RawHeader("Age", "30"), Connection("Keep-Alive")), entity = Default(contentType = ContentTypes.`text/plain(UTF-8)`, 23, publisher(ByteString("Small f*ck up overhere!")))) should renderTo { """HTTP/1.1 400 Bad Request @@ -124,14 +138,14 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll |Small f*ck up overhere!""" } } - "with one chunk and incorrect (too large) Content-Length" in new TestSetup() { + "one chunk and incorrect (too large) Content-Length" in new TestSetup() { the[RuntimeException] thrownBy { HttpResponse(200, entity = Default(ContentTypes.`application/json`, 10, publisher(ByteString("body123")))) should renderTo("") } should have message "HTTP message had declared Content-Length 10 but entity chunk stream amounts to 3 bytes less" } - "with one chunk and incorrect (too small) Content-Length" in new TestSetup() { + "one chunk and incorrect (too small) Content-Length" in new TestSetup() { the[RuntimeException] thrownBy { HttpResponse(200, entity = Default(ContentTypes.`application/json`, 5, publisher(ByteString("body123")))) should renderTo("") @@ -198,8 +212,8 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll """HTTP/1.1 200 OK |Server: akka-http/1.0.0 |Date: Thu, 25 Aug 2011 09:10:29 GMT - |Content-Type: text/plain; charset=UTF-8 |Transfer-Encoding: chunked + |Content-Type: text/plain; charset=UTF-8 | |7 |Yahoooo @@ -216,8 +230,8 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll """HTTP/1.1 200 OK |Server: akka-http/1.0.0 |Date: Thu, 25 Aug 2011 09:10:29 GMT - |Content-Type: text/plain; charset=UTF-8 |Transfer-Encoding: chunked + |Content-Type: text/plain; charset=UTF-8 | |7;key=value;another="tl;dr" |body123 @@ -228,9 +242,26 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll |""" } } + + "with a custom Transfer-Encoding header" in new TestSetup() { + HttpResponse(headers = List(`Transfer-Encoding`(TransferEncodings.Extension("fancy"))), + entity = Chunked(ContentTypes.`text/plain(UTF-8)`, publisher("Yahoooo"))) should renderTo { + """HTTP/1.1 200 OK + |Transfer-Encoding: fancy, chunked + |Server: akka-http/1.0.0 + |Date: Thu, 25 Aug 2011 09:10:29 GMT + |Content-Type: text/plain; charset=UTF-8 + | + |7 + |Yahoooo + |0 + | + |""" + } + } } - "chunked responses with HTTP/1.0 requests" - { + "chunked responses to a HTTP/1.0 request" - { "with two chunks" in new TestSetup() { ResponseRenderingContext( requestProtocol = HttpProtocols.`HTTP/1.0`, diff --git a/akka-http-core/src/test/scala/akka/http/engine/server/HttpServerPipelineSpec.scala b/akka-http-core/src/test/scala/akka/http/engine/server/HttpServerPipelineSpec.scala index 186a05d0ad..3732befa56 100644 --- a/akka-http-core/src/test/scala/akka/http/engine/server/HttpServerPipelineSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/engine/server/HttpServerPipelineSpec.scala @@ -350,7 +350,7 @@ class HttpServerPipelineSpec extends AkkaSpec with Matchers with BeforeAndAfterA expectRequest shouldEqual HttpRequest(HttpMethods.HEAD, uri = "http://example.com/", headers = List(Host("example.com"))) } - "not emit entites when responding to HEAD requests if transparent-head-requests is enabled (with Strict)" in new TestSetup { + "not emit entities when responding to HEAD requests if transparent-head-requests is enabled (with Strict)" in new TestSetup { override def settings = ServerSettings(system).copy(serverHeader = Some(Server(List(ProductVersion("akka-http", "test"))))) send("""HEAD / HTTP/1.1 |Host: example.com @@ -373,7 +373,7 @@ class HttpServerPipelineSpec extends AkkaSpec with Matchers with BeforeAndAfterA } } - "not emit entites when responding to HEAD requests if transparent-head-requests is enabled (with Default)" in new TestSetup { + "not emit entities when responding to HEAD requests if transparent-head-requests is enabled (with Default)" in new TestSetup { override def settings = ServerSettings(system).copy(serverHeader = Some(Server(List(ProductVersion("akka-http", "test"))))) send("""HEAD / HTTP/1.1 |Host: example.com @@ -402,7 +402,7 @@ class HttpServerPipelineSpec extends AkkaSpec with Matchers with BeforeAndAfterA } } - "not emit entites when responding to HEAD requests if transparent-head-requests is enabled (with CloseDelimited)" in new TestSetup { + "not emit entities when responding to HEAD requests if transparent-head-requests is enabled (with CloseDelimited)" in new TestSetup { override def settings = ServerSettings(system).copy(serverHeader = Some(Server(List(ProductVersion("akka-http", "test"))))) send("""HEAD / HTTP/1.1 |Host: example.com @@ -433,7 +433,7 @@ class HttpServerPipelineSpec extends AkkaSpec with Matchers with BeforeAndAfterA netOut.expectNoMsg(50.millis) } - "not emit entites when responding to HEAD requests if transparent-head-requests is enabled (with Chunked)" in new TestSetup { + "not emit entities when responding to HEAD requests if transparent-head-requests is enabled (with Chunked)" in new TestSetup { override def settings = ServerSettings(system).copy(serverHeader = Some(Server(List(ProductVersion("akka-http", "test"))))) send("""HEAD / HTTP/1.1 |Host: example.com @@ -455,14 +455,14 @@ class HttpServerPipelineSpec extends AkkaSpec with Matchers with BeforeAndAfterA """|HTTP/1.1 200 OK |Server: akka-http/test |Date: XXXX - |Content-Type: text/plain |Transfer-Encoding: chunked + |Content-Type: text/plain | |""".stripMarginWithNewline("\r\n") } } - "respect Connetion headers of HEAD requests if transparent-head-requests is enabled" in new TestSetup { + "respect Connection headers of HEAD requests if transparent-head-requests is enabled" in new TestSetup { override def settings = ServerSettings(system).copy(serverHeader = Some(Server(List(ProductVersion("akka-http", "test"))))) send("""HEAD / HTTP/1.1 |Host: example.com