+hco various smaller cleanups and helper additions
This commit is contained in:
parent
2ccf028a94
commit
5fce301c28
6 changed files with 48 additions and 204 deletions
|
|
@ -61,6 +61,11 @@ sealed trait HttpEntity extends japi.HttpEntity {
|
||||||
})
|
})
|
||||||
.toFuture(materializer)
|
.toFuture(materializer)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a copy of this HttpEntity with the `contentType` overridden with the given one.
|
||||||
|
*/
|
||||||
|
def withContentType(contentType: ContentType): HttpEntity
|
||||||
|
|
||||||
/** Java API */
|
/** Java API */
|
||||||
def getDataBytes(materializer: FlowMaterializer): Publisher[ByteString] = dataBytes(materializer)
|
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.
|
* 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 {
|
sealed trait Regular extends japi.HttpEntityRegular with HttpEntity {
|
||||||
|
def withContentType(contentType: ContentType): HttpEntity.Regular
|
||||||
override def isRegular: Boolean = true
|
override def isRegular: Boolean = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -120,6 +126,8 @@ object HttpEntity {
|
||||||
|
|
||||||
override def toStrict(timeout: FiniteDuration, materializer: FlowMaterializer)(implicit ec: ExecutionContext): Future[Strict] =
|
override def toStrict(timeout: FiniteDuration, materializer: FlowMaterializer)(implicit ec: ExecutionContext): Future[Strict] =
|
||||||
Future.successful(this)
|
Future.successful(this)
|
||||||
|
|
||||||
|
def withContentType(contentType: ContentType): Strict = copy(contentType = contentType)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -133,6 +141,8 @@ object HttpEntity {
|
||||||
override def isDefault: Boolean = true
|
override def isDefault: Boolean = true
|
||||||
|
|
||||||
def dataBytes(materializer: FlowMaterializer): Publisher[ByteString] = data
|
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
|
override def isCloseDelimited: Boolean = true
|
||||||
|
|
||||||
def dataBytes(materializer: FlowMaterializer): Publisher[ByteString] = data
|
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] =
|
def dataBytes(materializer: FlowMaterializer): Publisher[ByteString] =
|
||||||
Flow(chunks).map(_.data).filter(_.nonEmpty).toPublisher(materializer)
|
Flow(chunks).map(_.data).filter(_.nonEmpty).toPublisher(materializer)
|
||||||
|
|
||||||
|
def withContentType(contentType: ContentType): Chunked = copy(contentType = contentType)
|
||||||
|
|
||||||
/** Java API */
|
/** Java API */
|
||||||
def getChunks: Publisher[japi.ChunkStreamPart] = chunks.asInstanceOf[Publisher[japi.ChunkStreamPart]]
|
def getChunks: Publisher[japi.ChunkStreamPart] = chunks.asInstanceOf[Publisher[japi.ChunkStreamPart]]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
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.
|
* Determines whether this request can be safely retried, which is the case only of the request method is idempotent.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ package akka.http.rendering
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import org.reactivestreams.Publisher
|
import org.reactivestreams.Publisher
|
||||||
import scala.annotation.tailrec
|
import scala.annotation.tailrec
|
||||||
import scala.collection.immutable
|
|
||||||
import akka.event.LoggingAdapter
|
import akka.event.LoggingAdapter
|
||||||
import akka.util.ByteString
|
import akka.util.ByteString
|
||||||
import akka.stream.scaladsl.Flow
|
import akka.stream.scaladsl.Flow
|
||||||
|
|
@ -30,7 +29,7 @@ private[http] class HttpRequestRendererFactory(userAgentHeader: Option[headers.`
|
||||||
|
|
||||||
final class HttpRequestRenderer extends Transformer[RequestRenderingContext, Publisher[ByteString]] {
|
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)
|
val r = new ByteStringRendering(requestHeaderSizeHint)
|
||||||
import ctx.request._
|
import ctx.request._
|
||||||
|
|
||||||
|
|
@ -74,11 +73,8 @@ private[http] class HttpRequestRendererFactory(userAgentHeader: Option[headers.`
|
||||||
case x: `Raw-Request-URI` ⇒ // we never render this header
|
case x: `Raw-Request-URI` ⇒ // we never render this header
|
||||||
renderHeaders(tail, hostHeaderSeen, userAgentSeen)
|
renderHeaders(tail, hostHeaderSeen, userAgentSeen)
|
||||||
|
|
||||||
case x: RawHeader if x.lowercaseName == "content-type" ||
|
case x: RawHeader if (x is "content-type") || (x is "content-length") || (x is "transfer-encoding") ||
|
||||||
x.lowercaseName == "content-length" ||
|
(x is "host") || (x is "user-agent") ⇒
|
||||||
x.lowercaseName == "transfer-encoding" ||
|
|
||||||
x.lowercaseName == "host" ||
|
|
||||||
x.lowercaseName == "user-agent" ⇒
|
|
||||||
suppressionWarning(log, x, "illegal RawHeader")
|
suppressionWarning(log, x, "illegal RawHeader")
|
||||||
renderHeaders(tail, hostHeaderSeen, userAgentSeen)
|
renderHeaders(tail, hostHeaderSeen, userAgentSeen)
|
||||||
|
|
||||||
|
|
@ -97,30 +93,31 @@ private[http] class HttpRequestRendererFactory(userAgentHeader: Option[headers.`
|
||||||
r ~~ CrLf
|
r ~~ CrLf
|
||||||
}
|
}
|
||||||
|
|
||||||
def completeRequestRendering(): immutable.Seq[Publisher[ByteString]] =
|
def completeRequestRendering(): List[Publisher[ByteString]] =
|
||||||
entity match {
|
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)
|
renderContentLength(data.length)
|
||||||
SynchronousPublisherFromIterable(r.get :: data :: Nil) :: Nil
|
SynchronousPublisherFromIterable(r.get :: data :: Nil) :: Nil
|
||||||
|
|
||||||
case HttpEntity.Default(contentType, contentLength, data) ⇒
|
case HttpEntity.Default(_, contentLength, data) ⇒
|
||||||
renderContentLength(contentLength)
|
renderContentLength(contentLength)
|
||||||
renderByteStrings(r,
|
renderByteStrings(r,
|
||||||
Flow(data).transform(new CheckContentLengthTransformer(contentLength)).toPublisher(materializer),
|
Flow(data).transform(new CheckContentLengthTransformer(contentLength)).toPublisher(materializer),
|
||||||
materializer)
|
materializer)
|
||||||
|
|
||||||
case HttpEntity.Chunked(contentType, chunks) ⇒
|
case HttpEntity.Chunked(_, chunks) ⇒
|
||||||
r ~~ `Transfer-Encoding` ~~ Chunked ~~ CrLf ~~ CrLf
|
r ~~ `Transfer-Encoding` ~~ ChunkedBytes ~~ CrLf ~~ CrLf
|
||||||
renderByteStrings(r, Flow(chunks).transform(new ChunkTransformer).toPublisher(materializer), materializer)
|
renderByteStrings(r, Flow(chunks).transform(new ChunkTransformer).toPublisher(materializer), materializer)
|
||||||
}
|
}
|
||||||
|
|
||||||
renderRequestLine()
|
renderRequestLine()
|
||||||
renderHeaders(headers.toList)
|
renderHeaders(headers.toList)
|
||||||
renderEntityContentType(r, entity)
|
renderEntityContentType(r, entity)
|
||||||
if (entity.isKnownEmpty) {
|
completeRequestRendering()
|
||||||
renderContentLength(0)
|
|
||||||
SynchronousPublisherFromIterable(r.get :: Nil) :: Nil
|
|
||||||
} else completeRequestRendering()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ package akka.http.rendering
|
||||||
|
|
||||||
import org.reactivestreams.Publisher
|
import org.reactivestreams.Publisher
|
||||||
import scala.annotation.tailrec
|
import scala.annotation.tailrec
|
||||||
import scala.collection.immutable
|
|
||||||
import akka.event.LoggingAdapter
|
import akka.event.LoggingAdapter
|
||||||
import akka.util.ByteString
|
import akka.util.ByteString
|
||||||
import akka.stream.scaladsl.Flow
|
import akka.stream.scaladsl.Flow
|
||||||
|
|
@ -59,14 +58,14 @@ private[http] class HttpResponseRendererFactory(serverHeader: Option[headers.Ser
|
||||||
|
|
||||||
override def isComplete = close
|
override def isComplete = close
|
||||||
|
|
||||||
def onNext(ctx: ResponseRenderingContext): immutable.Seq[Publisher[ByteString]] = {
|
def onNext(ctx: ResponseRenderingContext): List[Publisher[ByteString]] = {
|
||||||
val r = new ByteStringRendering(responseHeaderSizeHint)
|
val r = new ByteStringRendering(responseHeaderSizeHint)
|
||||||
|
|
||||||
import ctx.response._
|
import ctx.response._
|
||||||
val noEntity = entity.isKnownEmpty || ctx.requestMethod == HttpMethods.HEAD
|
val noEntity = entity.isKnownEmpty || ctx.requestMethod == HttpMethods.HEAD
|
||||||
|
|
||||||
def renderStatusLine(): Unit =
|
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
|
def render(h: HttpHeader) = r ~~ h ~~ CrLf
|
||||||
|
|
||||||
|
|
@ -94,12 +93,8 @@ private[http] class HttpResponseRendererFactory(serverHeader: Option[headers.Ser
|
||||||
render(x)
|
render(x)
|
||||||
renderHeaders(tail, alwaysClose, connHeader, serverHeaderSeen = true)
|
renderHeaders(tail, alwaysClose, connHeader, serverHeaderSeen = true)
|
||||||
|
|
||||||
case x: RawHeader if x.lowercaseName == "content-type" ||
|
case x: RawHeader if (x is "content-type") || (x is "content-length") || (x is "transfer-encoding") ||
|
||||||
x.lowercaseName == "content-length" ||
|
(x is "date") || (x is "server") || (x is "connection") ⇒
|
||||||
x.lowercaseName == "transfer-encoding" ||
|
|
||||||
x.lowercaseName == "date" ||
|
|
||||||
x.lowercaseName == "server" ||
|
|
||||||
x.lowercaseName == "connection" ⇒
|
|
||||||
suppressionWarning(log, x, "illegal RawHeader")
|
suppressionWarning(log, x, "illegal RawHeader")
|
||||||
renderHeaders(tail, alwaysClose, connHeader, serverHeaderSeen)
|
renderHeaders(tail, alwaysClose, connHeader, serverHeaderSeen)
|
||||||
|
|
||||||
|
|
@ -115,31 +110,31 @@ private[http] class HttpResponseRendererFactory(serverHeader: Option[headers.Ser
|
||||||
ctx.closeAfterResponseCompletion || // request wants to close
|
ctx.closeAfterResponseCompletion || // request wants to close
|
||||||
(connHeader != null && connHeader.hasClose) // application wants to close
|
(connHeader != null && connHeader.hasClose) // application wants to close
|
||||||
ctx.requestProtocol match {
|
ctx.requestProtocol match {
|
||||||
case `HTTP/1.0` if !close ⇒ r ~~ Connection ~~ KeepAlive ~~ CrLf
|
case `HTTP/1.0` if !close ⇒ r ~~ Connection ~~ KeepAliveBytes ~~ CrLf
|
||||||
case `HTTP/1.1` if close ⇒ r ~~ Connection ~~ Close ~~ CrLf
|
case `HTTP/1.1` if close ⇒ r ~~ Connection ~~ CloseBytes ~~ CrLf
|
||||||
case _ ⇒ // no need for rendering
|
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)
|
renderByteStrings(r, entityBytes, materializer, skipEntity = noEntity)
|
||||||
|
|
||||||
def completeResponseRendering(entity: HttpEntity): immutable.Seq[Publisher[ByteString]] =
|
def completeResponseRendering(entity: HttpEntity): List[Publisher[ByteString]] =
|
||||||
entity match {
|
entity match {
|
||||||
case HttpEntity.Strict(contentType, data) ⇒
|
case HttpEntity.Strict(_, data) ⇒
|
||||||
renderHeaders(headers.toList)
|
renderHeaders(headers.toList)
|
||||||
renderEntityContentType(r, entity)
|
renderEntityContentType(r, entity)
|
||||||
r ~~ `Content-Length` ~~ data.length ~~ CrLf ~~ CrLf
|
r ~~ `Content-Length` ~~ data.length ~~ CrLf ~~ CrLf
|
||||||
val entityBytes = if (noEntity) Nil else data :: Nil
|
val entityBytes = if (noEntity) Nil else data :: Nil
|
||||||
SynchronousPublisherFromIterable(r.get :: entityBytes) :: Nil
|
SynchronousPublisherFromIterable(r.get :: entityBytes) :: Nil
|
||||||
|
|
||||||
case HttpEntity.Default(contentType, contentLength, data) ⇒
|
case HttpEntity.Default(_, contentLength, data) ⇒
|
||||||
renderHeaders(headers.toList)
|
renderHeaders(headers.toList)
|
||||||
renderEntityContentType(r, entity)
|
renderEntityContentType(r, entity)
|
||||||
r ~~ `Content-Length` ~~ contentLength ~~ CrLf ~~ CrLf
|
r ~~ `Content-Length` ~~ contentLength ~~ CrLf ~~ CrLf
|
||||||
byteStrings(Flow(data).transform(new CheckContentLengthTransformer(contentLength)).toPublisher(materializer))
|
byteStrings(Flow(data).transform(new CheckContentLengthTransformer(contentLength)).toPublisher(materializer))
|
||||||
|
|
||||||
case HttpEntity.CloseDelimited(contentType, data) ⇒
|
case HttpEntity.CloseDelimited(_, data) ⇒
|
||||||
renderHeaders(headers.toList, alwaysClose = true)
|
renderHeaders(headers.toList, alwaysClose = true)
|
||||||
renderEntityContentType(r, entity)
|
renderEntityContentType(r, entity)
|
||||||
r ~~ CrLf
|
r ~~ CrLf
|
||||||
|
|
@ -152,7 +147,7 @@ private[http] class HttpResponseRendererFactory(serverHeader: Option[headers.Ser
|
||||||
renderHeaders(headers.toList)
|
renderHeaders(headers.toList)
|
||||||
renderEntityContentType(r, entity)
|
renderEntityContentType(r, entity)
|
||||||
if (!entity.isKnownEmpty || ctx.requestMethod == HttpMethods.HEAD)
|
if (!entity.isKnownEmpty || ctx.requestMethod == HttpMethods.HEAD)
|
||||||
r ~~ `Transfer-Encoding` ~~ Chunked ~~ CrLf
|
r ~~ `Transfer-Encoding` ~~ ChunkedBytes ~~ CrLf
|
||||||
r ~~ CrLf
|
r ~~ CrLf
|
||||||
byteStrings(Flow(chunks).transform(new ChunkTransformer).toPublisher(materializer))
|
byteStrings(Flow(chunks).transform(new ChunkTransformer).toPublisher(materializer))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@
|
||||||
package akka.http.rendering
|
package akka.http.rendering
|
||||||
|
|
||||||
import org.reactivestreams.Publisher
|
import org.reactivestreams.Publisher
|
||||||
import scala.collection.immutable
|
|
||||||
import akka.parboiled2.CharUtils
|
import akka.parboiled2.CharUtils
|
||||||
import akka.util.ByteString
|
import akka.util.ByteString
|
||||||
import akka.event.LoggingAdapter
|
import akka.event.LoggingAdapter
|
||||||
|
|
@ -19,11 +18,11 @@ import akka.http.util._
|
||||||
* INTERNAL API
|
* INTERNAL API
|
||||||
*/
|
*/
|
||||||
private object RenderSupport {
|
private object RenderSupport {
|
||||||
val DefaultStatusLine = "HTTP/1.1 200 OK\r\n".getAsciiBytes
|
val DefaultStatusLineBytes = "HTTP/1.1 200 OK\r\n".getAsciiBytes
|
||||||
val StatusLineStart = "HTTP/1.1 ".getAsciiBytes
|
val StatusLineStartBytes = "HTTP/1.1 ".getAsciiBytes
|
||||||
val Chunked = "chunked".getAsciiBytes
|
val ChunkedBytes = "chunked".getAsciiBytes
|
||||||
val KeepAlive = "Keep-Alive".getAsciiBytes
|
val KeepAliveBytes = "Keep-Alive".getAsciiBytes
|
||||||
val Close = "close".getAsciiBytes
|
val CloseBytes = "close".getAsciiBytes
|
||||||
|
|
||||||
def CrLf = Rendering.CrLf
|
def CrLf = Rendering.CrLf
|
||||||
|
|
||||||
|
|
@ -36,7 +35,7 @@ private object RenderSupport {
|
||||||
r ~~ headers.`Content-Type` ~~ entity.contentType ~~ CrLf
|
r ~~ headers.`Content-Type` ~~ entity.contentType ~~ CrLf
|
||||||
|
|
||||||
def renderByteStrings(r: ByteStringRendering, entityBytes: ⇒ Publisher[ByteString], materializer: FlowMaterializer,
|
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 messageStart = SynchronousPublisherFromIterable(r.get :: Nil)
|
||||||
val messageBytes =
|
val messageBytes =
|
||||||
if (!skipEntity) Flow(messageStart).concat(entityBytes).toPublisher(materializer)
|
if (!skipEntity) Flow(messageStart).concat(entityBytes).toPublisher(materializer)
|
||||||
|
|
@ -46,7 +45,7 @@ private object RenderSupport {
|
||||||
|
|
||||||
class ChunkTransformer extends Transformer[HttpEntity.ChunkStreamPart, ByteString] {
|
class ChunkTransformer extends Transformer[HttpEntity.ChunkStreamPart, ByteString] {
|
||||||
var lastChunkSeen = false
|
var lastChunkSeen = false
|
||||||
def onNext(chunk: HttpEntity.ChunkStreamPart): immutable.Seq[ByteString] = {
|
def onNext(chunk: HttpEntity.ChunkStreamPart): List[ByteString] = {
|
||||||
if (chunk.isLastChunk) lastChunkSeen = true
|
if (chunk.isLastChunk) lastChunkSeen = true
|
||||||
renderChunk(chunk) :: Nil
|
renderChunk(chunk) :: Nil
|
||||||
}
|
}
|
||||||
|
|
@ -56,14 +55,14 @@ private object RenderSupport {
|
||||||
|
|
||||||
class CheckContentLengthTransformer(length: Long) extends Transformer[ByteString, ByteString] {
|
class CheckContentLengthTransformer(length: Long) extends Transformer[ByteString, ByteString] {
|
||||||
var sent = 0L
|
var sent = 0L
|
||||||
def onNext(elem: ByteString): immutable.Seq[ByteString] = {
|
def onNext(elem: ByteString): List[ByteString] = {
|
||||||
sent += elem.length
|
sent += elem.length
|
||||||
if (sent > length)
|
if (sent > length)
|
||||||
throw new InvalidContentLengthException(s"HTTP message had declared Content-Length $length but entity chunk stream amounts to more bytes")
|
throw new InvalidContentLengthException(s"HTTP message had declared Content-Length $length but entity chunk stream amounts to more bytes")
|
||||||
elem :: Nil
|
elem :: Nil
|
||||||
}
|
}
|
||||||
|
|
||||||
override def onTermination(e: Option[Throwable]): immutable.Seq[ByteString] = {
|
override def onTermination(e: Option[Throwable]): List[ByteString] = {
|
||||||
if (sent < length)
|
if (sent < length)
|
||||||
throw new InvalidContentLengthException(s"HTTP message had declared Content-Length $length but entity chunk stream amounts to ${length - sent} bytes less")
|
throw new InvalidContentLengthException(s"HTTP message had declared Content-Length $length but entity chunk stream amounts to ${length - sent} bytes less")
|
||||||
Nil
|
Nil
|
||||||
|
|
|
||||||
|
|
@ -1,127 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
|
||||||
*/
|
|
||||||
|
|
||||||
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)))
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue