Merge pull request #19526 from ktoso/spray-wip-16819-mathias

PR #19479 with identity fixup
This commit is contained in:
Konrad Malawski 2016-01-20 01:55:18 +01:00
commit ef77b56e66
29 changed files with 795 additions and 399 deletions

View file

@ -0,0 +1,44 @@
/**
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.javadsl;
import akka.http.javadsl.model.HttpRequest;
import akka.http.javadsl.model.HttpResponse;
import akka.japi.Function;
import scala.concurrent.duration.Duration;
/**
* Enables programmatic access to the server-side request timeout logic.
*/
public interface TimeoutAccess {
/**
* Tries to set a new timeout.
* The timeout period is measured as of the point in time that the end of the request has been received,
* which may be in the past or in the future!
* Use `Duration.Inf` to completely disable request timeout checking for this request.
*
* Due to the inherent raciness it is not guaranteed that the update will be applied before
* the previously set timeout has expired!
*/
void updateTimeout(Duration timeout);
/**
* Tries to set a new timeout handler, which produces the timeout response for a
* given request. Note that the handler must produce the response synchronously and shouldn't block!
*
* Due to the inherent raciness it is not guaranteed that the update will be applied before
* the previously set timeout has expired!
*/
void updateHandler(Function<HttpRequest, HttpResponse> handler);
/**
* Tries to set a new timeout and handler at the same time.
*
* Due to the inherent raciness it is not guaranteed that the update will be applied before
* the previously set timeout has expired!
*/
void update(Duration timeout, Function<HttpRequest, HttpResponse> handler);
}

View file

@ -34,4 +34,14 @@ public abstract class HttpHeader {
* Returns !is(nameInLowerCase). * Returns !is(nameInLowerCase).
*/ */
public abstract boolean isNot(String nameInLowerCase); public abstract boolean isNot(String nameInLowerCase);
/**
* Returns true iff the header is to be rendered in requests.
*/
public abstract boolean renderInRequests();
/**
* Returns true iff the header is to be rendered in responses.
*/
public abstract boolean renderInResponses();
} }

View file

@ -7,6 +7,4 @@ package akka.http.javadsl.model.headers;
public abstract class CustomHeader extends akka.http.scaladsl.model.HttpHeader { public abstract class CustomHeader extends akka.http.scaladsl.model.HttpHeader {
public abstract String name(); public abstract String name();
public abstract String value(); public abstract String value();
protected abstract boolean suppressRendering();
} }

View file

@ -0,0 +1,16 @@
/**
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.javadsl.model.headers;
/**
* Model for the synthetic `Timeout-Access` header.
*/
public abstract class TimeoutAccess extends akka.http.scaladsl.model.HttpHeader {
public abstract akka.http.javadsl.TimeoutAccess timeoutAccess();
public static TimeoutAccess create(akka.http.javadsl.TimeoutAccess timeoutAccess) {
return new akka.http.scaladsl.model.headers.Timeout$minusAccess((akka.http.scaladsl.TimeoutAccess) timeoutAccess);
}
}

View file

@ -18,6 +18,18 @@ akka.http {
# Set to `infinite` to completely disable idle connection timeouts. # Set to `infinite` to completely disable idle connection timeouts.
idle-timeout = 60 s idle-timeout = 60 s
# Defines the default time period within which the application has to
# produce an HttpResponse for any given HttpRequest it received.
# The timeout begins to run when the *end* of the request has been
# received, so even potentially long uploads can have a short timeout.
# Set to `infinite` to completely disable request timeout checking.
#
# If this setting is not `infinite` the HTTP server layer attaches a
# `Timeout-Access` header to the request, which enables programmatic
# customization of the timeout period and timeout response for each
# request individually.
request-timeout = 20 s
# The time period within which the TCP binding process must be completed. # The time period within which the TCP binding process must be completed.
# Set to `infinite` to disable. # Set to `infinite` to disable.
bind-timeout = 1s bind-timeout = 1s
@ -273,13 +285,16 @@ akka.http {
max-chunk-ext-length = 256 max-chunk-ext-length = 256
max-chunk-size = 1m max-chunk-size = 1m
# Maximum content length which should not be exceeded by incoming HttpRequests. # Default maximum content length which should not be exceeded by incoming request entities.
# For file uploads which use the entityBytes Source of an incoming HttpRequest it is safe to # Can be changed at runtime (to a higher or lower value) via the `HttpEntity::withSizeLimit` method.
# set this to a very high value (or to `infinite` if feeling very adventurous) as the streaming # Note that it is not necessarily a problem to set this to a high value as all stream operations
# upload will be back-pressured properly by Akka Streams. # are always properly backpressured.
# Please note however that this setting is a global property, and is applied to all incoming requests, # Nevertheless you might want to apply some limit in order to prevent a single client from consuming
# not only file uploads consumed in a streaming fashion, so pick this limit wisely. # an excessive amount of server resources.
max-content-length = 8m #
# Set to `infinite` to completely disable entity length checks. (Even then you can still apply one
# programmatically via `withSizeLimit`.)
max-content-length = 8m
# Sets the strictness mode for parsing request target URIs. # Sets the strictness mode for parsing request target URIs.
# The following values are defined: # The following values are defined:

View file

@ -46,7 +46,10 @@ final case class ServerSettings(
object ServerSettings extends SettingsCompanion[ServerSettings]("akka.http.server") { object ServerSettings extends SettingsCompanion[ServerSettings]("akka.http.server") {
final case class Timeouts(idleTimeout: Duration, final case class Timeouts(idleTimeout: Duration,
requestTimeout: Duration,
bindTimeout: FiniteDuration) { bindTimeout: FiniteDuration) {
require(idleTimeout > Duration.Zero, "idleTimeout must be infinite or > 0")
require(requestTimeout > Duration.Zero, "requestTimeout must be infinite or > 0")
require(bindTimeout > Duration.Zero, "bindTimeout must be > 0") require(bindTimeout > Duration.Zero, "bindTimeout must be > 0")
} }
implicit def timeoutsShortcut(s: ServerSettings): Timeouts = s.timeouts implicit def timeoutsShortcut(s: ServerSettings): Timeouts = s.timeouts
@ -55,6 +58,7 @@ object ServerSettings extends SettingsCompanion[ServerSettings]("akka.http.serve
c.getString("server-header").toOption.map(Server(_)), c.getString("server-header").toOption.map(Server(_)),
Timeouts( Timeouts(
c getPotentiallyInfiniteDuration "idle-timeout", c getPotentiallyInfiniteDuration "idle-timeout",
c getPotentiallyInfiniteDuration "request-timeout",
c getFiniteDuration "bind-timeout"), c getFiniteDuration "bind-timeout"),
c getInt "max-connections", c getInt "max-connections",
c getInt "pipelining-limit", c getInt "pipelining-limit",

View file

@ -163,18 +163,7 @@ private object PoolSlot {
case FromConnection(OnNext(response: HttpResponse)) case FromConnection(OnNext(response: HttpResponse))
val requestContext = inflightRequests.head val requestContext = inflightRequests.head
inflightRequests = inflightRequests.tail inflightRequests = inflightRequests.tail
val (entity, whenCompleted) = response.entity match { val (entity, whenCompleted) = HttpEntity.captureTermination(response.entity)
case x: HttpEntity.Strict x -> FastFuture.successful(())
case x: HttpEntity.Default
val (newData, whenCompleted) = StreamUtils.captureTermination(x.data)
x.copy(data = newData) -> whenCompleted
case x: HttpEntity.CloseDelimited
val (newData, whenCompleted) = StreamUtils.captureTermination(x.data)
x.copy(data = newData) -> whenCompleted
case x: HttpEntity.Chunked
val (newChunks, whenCompleted) = StreamUtils.captureTermination(x.chunks)
x.copy(chunks = newChunks) -> whenCompleted
}
val delivery = ResponseDelivery(ResponseContext(requestContext, Success(response withEntity entity))) val delivery = ResponseDelivery(ResponseContext(requestContext, Success(response withEntity entity)))
import fm.executionContext import fm.executionContext
val requestCompleted = SlotEvent.RequestCompletedFuture(whenCompleted.map(_ SlotEvent.RequestCompleted(slotIx))) val requestCompleted = SlotEvent.RequestCompletedFuture(whenCompleted.map(_ SlotEvent.RequestCompleted(slotIx)))

View file

@ -146,7 +146,7 @@ private[http] final class BodyPartParser(defaultContentType: ContentType,
else if (doubleDash(input, ix)) terminate() else if (doubleDash(input, ix)) terminate()
else fail("Illegal multipart boundary in message content") else fail("Illegal multipart boundary in message content")
case HttpHeaderParser.EmptyHeader parseEntity(headers.toList, contentType)(input, lineEnd) case EmptyHeader parseEntity(headers.toList, contentType)(input, lineEnd)
case h: `Content-Type` case h: `Content-Type`
if (cth.isEmpty) parseHeaderLines(input, lineEnd, headers, headerCount + 1, Some(h)) if (cth.isEmpty) parseHeaderLines(input, lineEnd, headers, headerCount + 1, Some(h))
@ -261,6 +261,8 @@ private[http] object BodyPartParser {
val boundaryChar = CharPredicate.Digit ++ CharPredicate.Alpha ++ "'()+_,-./:=? " val boundaryChar = CharPredicate.Digit ++ CharPredicate.Alpha ++ "'()+_,-./:=? "
private object BoundaryHeader extends HttpHeader { private object BoundaryHeader extends HttpHeader {
def renderInRequests = false
def renderInResponses = false
def name = "" def name = ""
def lowercaseName = "" def lowercaseName = ""
def value = "" def value = ""

View file

@ -11,8 +11,8 @@ import scala.annotation.tailrec
import akka.parboiled2.CharUtils import akka.parboiled2.CharUtils
import akka.util.ByteString import akka.util.ByteString
import akka.http.impl.util._ import akka.http.impl.util._
import akka.http.scaladsl.model.{ IllegalHeaderException, StatusCodes, HttpHeader, ErrorInfo, Uri } import akka.http.scaladsl.model.{ IllegalHeaderException, StatusCodes, HttpHeader, ErrorInfo }
import akka.http.scaladsl.model.headers.RawHeader import akka.http.scaladsl.model.headers.{ EmptyHeader, RawHeader }
import akka.http.impl.model.parser.HeaderParser import akka.http.impl.model.parser.HeaderParser
import akka.http.impl.model.parser.CharacterClasses._ import akka.http.impl.model.parser.CharacterClasses._
@ -414,14 +414,6 @@ private[http] object HttpHeaderParser {
def headerValueCacheLimit(headerName: String): Int def headerValueCacheLimit(headerName: String): Int
} }
object EmptyHeader extends HttpHeader {
def name = ""
def lowercaseName = ""
def value = ""
def render[R <: Rendering](r: R): r.type = r
override def toString = "EmptyHeader"
}
private def predefinedHeaders = Seq( private def predefinedHeaders = Seq(
"Accept: *", "Accept: *",
"Accept: */*", "Accept: */*",

View file

@ -136,7 +136,7 @@ private[http] abstract class HttpMessageParser[Output >: MessageOutput <: Parser
resultHeader match { resultHeader match {
case null continue(input, lineStart)(parseHeaderLinesAux(headers, headerCount, ch, clh, cth, teh, e100c, hh)) case null continue(input, lineStart)(parseHeaderLinesAux(headers, headerCount, ch, clh, cth, teh, e100c, hh))
case HttpHeaderParser.EmptyHeader case EmptyHeader
val close = HttpMessage.connectionCloseExpected(protocol, ch) val close = HttpMessage.connectionCloseExpected(protocol, ch)
setCompletionHandling(CompletionIsEntityStreamError) setCompletionHandling(CompletionIsEntityStreamError)
parseEntity(headers.toList, protocol, input, lineEnd, clh, cth, teh, e100c, hh, close) parseEntity(headers.toList, protocol, input, lineEnd, clh, cth, teh, e100c, hh, close)
@ -206,7 +206,7 @@ private[http] abstract class HttpMessageParser[Output >: MessageOutput <: Parser
catch { case e: ParsingException errorInfo = e.info; 0 } catch { case e: ParsingException errorInfo = e.info; 0 }
if (errorInfo eq null) { if (errorInfo eq null) {
headerParser.resultHeader match { headerParser.resultHeader match {
case HttpHeaderParser.EmptyHeader case EmptyHeader
val lastChunk = val lastChunk =
if (extension.isEmpty && headers.isEmpty) HttpEntity.LastChunk else HttpEntity.LastChunk(extension, headers) if (extension.isEmpty && headers.isEmpty) HttpEntity.LastChunk else HttpEntity.LastChunk(extension, headers)
emit(EntityChunk(lastChunk)) emit(EntityChunk(lastChunk))

View file

@ -78,8 +78,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, transferEncodingSeen) renderHeaders(tail, hostHeaderSeen, userAgentSeen, transferEncodingSeen)
case x: CustomHeader case x: CustomHeader if x.renderInRequests
if (!x.suppressRendering) render(x) render(x)
renderHeaders(tail, hostHeaderSeen, userAgentSeen, transferEncodingSeen) renderHeaders(tail, hostHeaderSeen, userAgentSeen, transferEncodingSeen)
case x: RawHeader if (x is "content-type") || (x is "content-length") || (x is "transfer-encoding") || case x: RawHeader if (x is "content-type") || (x is "content-length") || (x is "transfer-encoding") ||
@ -88,7 +88,8 @@ private[http] class HttpRequestRendererFactory(userAgentHeader: Option[headers.`
renderHeaders(tail, hostHeaderSeen, userAgentSeen, transferEncodingSeen) renderHeaders(tail, hostHeaderSeen, userAgentSeen, transferEncodingSeen)
case x case x
render(x) if (x.renderInRequests) render(x)
else log.warning("HTTP header '{}' is not allowed in requests", x)
renderHeaders(tail, hostHeaderSeen, userAgentSeen, transferEncodingSeen) renderHeaders(tail, hostHeaderSeen, userAgentSeen, transferEncodingSeen)
} }

View file

@ -161,8 +161,8 @@ private[http] class HttpResponseRendererFactory(serverHeader: Option[headers.Ser
render(x) render(x)
renderHeaders(tail, alwaysClose, connHeader, serverSeen = true, transferEncodingSeen, dateSeen) renderHeaders(tail, alwaysClose, connHeader, serverSeen = true, transferEncodingSeen, dateSeen)
case x: CustomHeader case x: CustomHeader if x.renderInResponses
if (!x.suppressRendering) render(x) render(x)
renderHeaders(tail, alwaysClose, connHeader, serverSeen, transferEncodingSeen, dateSeen) renderHeaders(tail, alwaysClose, connHeader, serverSeen, transferEncodingSeen, dateSeen)
case x: RawHeader if (x is "content-type") || (x is "content-length") || (x is "transfer-encoding") || case x: RawHeader if (x is "content-type") || (x is "content-length") || (x is "transfer-encoding") ||
@ -171,7 +171,8 @@ private[http] class HttpResponseRendererFactory(serverHeader: Option[headers.Ser
renderHeaders(tail, alwaysClose, connHeader, serverSeen, transferEncodingSeen, dateSeen) renderHeaders(tail, alwaysClose, connHeader, serverSeen, transferEncodingSeen, dateSeen)
case x case x
render(x) if (x.renderInResponses) render(x)
else log.warning("HTTP header '{}' is not allowed in responses", x)
renderHeaders(tail, alwaysClose, connHeader, serverSeen, transferEncodingSeen, dateSeen) renderHeaders(tail, alwaysClose, connHeader, serverSeen, transferEncodingSeen, dateSeen)
} }

View file

@ -5,12 +5,19 @@
package akka.http.impl.engine.server package akka.http.impl.engine.server
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.util.Random import java.util.concurrent.atomic.AtomicReference
import akka.stream.impl.fusing.GraphInterpreter import scala.concurrent.{ Promise, Future }
import scala.concurrent.duration.{ Deadline, FiniteDuration, Duration }
import scala.collection.immutable import scala.collection.immutable
import org.reactivestreams.{ Publisher, Subscriber }
import scala.util.control.NonFatal import scala.util.control.NonFatal
import akka.actor.Cancellable
import akka.japi.Function
import akka.event.LoggingAdapter import akka.event.LoggingAdapter
import akka.util.ByteString
import akka.stream._
import akka.stream.io._
import akka.stream.scaladsl._
import akka.stream.stage._
import akka.http.ServerSettings import akka.http.ServerSettings
import akka.http.impl.engine.HttpConnectionTimeoutException import akka.http.impl.engine.HttpConnectionTimeoutException
import akka.http.impl.engine.parsing.ParserOutput._ import akka.http.impl.engine.parsing.ParserOutput._
@ -18,16 +25,12 @@ import akka.http.impl.engine.parsing._
import akka.http.impl.engine.rendering.{ HttpResponseRendererFactory, ResponseRenderingContext, ResponseRenderingOutput } import akka.http.impl.engine.rendering.{ HttpResponseRendererFactory, ResponseRenderingContext, ResponseRenderingOutput }
import akka.http.impl.engine.ws._ import akka.http.impl.engine.ws._
import akka.http.impl.util._ import akka.http.impl.util._
import akka.http.scaladsl.Http import akka.http.scaladsl.util.FastFuture.EnhancedFuture
import akka.http.scaladsl.{ TimeoutAccess, Http }
import akka.http.scaladsl.model.headers.`Timeout-Access`
import akka.http.javadsl.model
import akka.http.scaladsl.model._ import akka.http.scaladsl.model._
import akka.stream._
import akka.stream.impl.ConstantFun
import akka.stream.io._
import akka.stream.scaladsl._
import akka.stream.stage._
import akka.util.ByteString
import akka.http.scaladsl.model.ws.Message import akka.http.scaladsl.model.ws.Message
import akka.stream.impl.fusing.SubSource
/** /**
* INTERNAL API * INTERNAL API
@ -54,6 +57,7 @@ private[http] object HttpServerBluePrint {
def apply(settings: ServerSettings, remoteAddress: Option[InetSocketAddress], log: LoggingAdapter): Http.ServerLayer = { def apply(settings: ServerSettings, remoteAddress: Option[InetSocketAddress], log: LoggingAdapter): Http.ServerLayer = {
val theStack = val theStack =
userHandlerGuard(settings.pipeliningLimit) atop userHandlerGuard(settings.pipeliningLimit) atop
requestTimeoutSupport(settings.timeouts.requestTimeout) atop
requestPreparation(settings) atop requestPreparation(settings) atop
controller(settings, log) atop controller(settings, log) atop
parsingRendering(settings, log) atop parsingRendering(settings, log) atop
@ -78,6 +82,12 @@ private[http] object HttpServerBluePrint {
def requestPreparation(settings: ServerSettings): BidiFlow[HttpResponse, HttpResponse, RequestOutput, HttpRequest, Unit] = def requestPreparation(settings: ServerSettings): BidiFlow[HttpResponse, HttpResponse, RequestOutput, HttpRequest, Unit] =
BidiFlow.fromFlows(Flow[HttpResponse], new PrepareRequests(settings)) BidiFlow.fromFlows(Flow[HttpResponse], new PrepareRequests(settings))
def requestTimeoutSupport(timeout: Duration): BidiFlow[HttpResponse, HttpResponse, HttpRequest, HttpRequest, Unit] =
timeout match {
case x: FiniteDuration BidiFlow.fromGraph(new RequestTimeoutSupport(x)).reversed
case _ BidiFlow.identity
}
final class PrepareRequests(settings: ServerSettings) extends GraphStage[FlowShape[RequestOutput, HttpRequest]] { final class PrepareRequests(settings: ServerSettings) extends GraphStage[FlowShape[RequestOutput, HttpRequest]] {
val in = Inlet[RequestOutput]("RequestStartThenRunIgnore.in") val in = Inlet[RequestOutput]("RequestStartThenRunIgnore.in")
val out = Outlet[HttpRequest]("RequestStartThenRunIgnore.out") val out = Outlet[HttpRequest]("RequestStartThenRunIgnore.out")
@ -97,6 +107,7 @@ private[http] object HttpServerBluePrint {
val entity = createEntity(entityCreator) withSizeLimit settings.parserSettings.maxContentLength val entity = createEntity(entityCreator) withSizeLimit settings.parserSettings.maxContentLength
push(out, HttpRequest(effectiveMethod, uri, effectiveHeaders, entity, protocol)) push(out, HttpRequest(effectiveMethod, uri, effectiveHeaders, entity, protocol))
case _ throw new IllegalStateException
} }
} }
setHandler(in, idle) setHandler(in, idle)
@ -173,6 +184,104 @@ private[http] object HttpServerBluePrint {
.via(Flow[ResponseRenderingOutput].transform(() errorHandling(errorHandler)).named("errorLogger")) .via(Flow[ResponseRenderingOutput].transform(() errorHandling(errorHandler)).named("errorLogger"))
} }
class RequestTimeoutSupport(initialTimeout: FiniteDuration)
extends GraphStage[BidiShape[HttpRequest, HttpRequest, HttpResponse, HttpResponse]] {
private val requestIn = Inlet[HttpRequest]("requestIn")
private val requestOut = Outlet[HttpRequest]("requestOut")
private val responseIn = Inlet[HttpResponse]("responseIn")
private val responseOut = Outlet[HttpResponse]("responseOut")
override def initialAttributes = Attributes.name("RequestTimeoutSupport")
val shape = new BidiShape(requestIn, requestOut, responseIn, responseOut)
def createLogic(effectiveAttributes: Attributes) = new GraphStageLogic(shape) {
var openTimeouts = immutable.Queue[TimeoutAccessImpl]()
setHandler(requestIn, new InHandler {
def onPush(): Unit = {
val request = grab(requestIn)
val (entity, requestEnd) = HttpEntity.captureTermination(request.entity)
val access = new TimeoutAccessImpl(request, initialTimeout, requestEnd,
getAsyncCallback(emitTimeoutResponse), interpreter.materializer)
openTimeouts = openTimeouts.enqueue(access)
push(requestOut, request.copy(headers = request.headers :+ `Timeout-Access`(access), entity = entity))
}
override def onUpstreamFinish() = complete(requestOut)
override def onUpstreamFailure(ex: Throwable) = fail(requestOut, ex)
def emitTimeoutResponse(response: (TimeoutAccess, HttpResponse)) =
if (openTimeouts.head eq response._1) {
emit(responseOut, response._2, () complete(responseOut))
} // else the application response arrived after we scheduled the timeout response, which is close but ok
})
// TODO: provide and use default impl for simply connecting an input and an output port as we do here
setHandler(requestOut, new OutHandler {
def onPull(): Unit = pull(requestIn)
override def onDownstreamFinish() = cancel(requestIn)
})
setHandler(responseIn, new InHandler {
def onPush(): Unit = {
openTimeouts.head.clear()
openTimeouts = openTimeouts.tail
push(responseOut, grab(responseIn))
}
override def onUpstreamFinish() = complete(responseOut)
override def onUpstreamFailure(ex: Throwable) = fail(responseOut, ex)
})
setHandler(responseOut, new OutHandler {
def onPull(): Unit = pull(responseIn)
override def onDownstreamFinish() = cancel(responseIn)
})
}
}
private class TimeoutSetup(val timeoutBase: Deadline,
val scheduledTask: Cancellable,
val timeout: Duration,
val handler: HttpRequest HttpResponse)
private class TimeoutAccessImpl(request: HttpRequest, initialTimeout: FiniteDuration, requestEnd: Future[Unit],
trigger: AsyncCallback[(TimeoutAccess, HttpResponse)], materializer: Materializer)
extends AtomicReference[Future[TimeoutSetup]] with TimeoutAccess with (HttpRequest HttpResponse) { self
import materializer.executionContext
set {
requestEnd.fast.map(_ new TimeoutSetup(Deadline.now, schedule(initialTimeout, this), initialTimeout, this))
}
override def apply(request: HttpRequest) = HttpResponse(StatusCodes.ServiceUnavailable, entity = "The server was not able " +
"to produce a timely response to your request.\r\nPlease try again in a short while!")
def clear(): Unit = // best effort timeout cancellation
get.fast.foreach(setup if (setup.scheduledTask ne null) setup.scheduledTask.cancel())
override def updateTimeout(timeout: Duration): Unit = update(timeout, null: HttpRequest HttpResponse)
override def updateHandler(handler: HttpRequest HttpResponse): Unit = update(null, handler)
override def update(timeout: Duration, handler: HttpRequest HttpResponse): Unit = {
val promise = Promise[TimeoutSetup]()
for (old getAndSet(promise.future).fast)
promise.success {
if ((old.scheduledTask eq null) || old.scheduledTask.cancel()) {
val newHandler = if (handler eq null) old.handler else handler
val newTimeout = if (timeout eq null) old.timeout else timeout
val newScheduling = newTimeout match {
case x: FiniteDuration schedule(old.timeoutBase + x - Deadline.now, newHandler)
case _ null // don't schedule a new timeout
}
new TimeoutSetup(old.timeoutBase, newScheduling, newTimeout, newHandler)
} else old // too late, the previously set timeout cannot be cancelled anymore
}
}
private def schedule(delay: FiniteDuration, handler: HttpRequest HttpResponse): Cancellable =
materializer.scheduleOnce(delay, new Runnable { def run() = trigger.invoke(self, handler(request)) })
import akka.http.impl.util.JavaMapping.Implicits._
/** JAVA API **/
def update(timeout: Duration, handler: Function[model.HttpRequest, model.HttpResponse]): Unit =
update(timeout, handler(_: HttpRequest).asScala)
def updateHandler(handler: Function[model.HttpRequest, model.HttpResponse]): Unit =
updateHandler(handler(_: HttpRequest).asScala)
}
class ControllerStage(settings: ServerSettings, log: LoggingAdapter) class ControllerStage(settings: ServerSettings, log: LoggingAdapter)
extends GraphStage[BidiShape[RequestOutput, RequestOutput, HttpResponse, ResponseRenderingContext]] { extends GraphStage[BidiShape[RequestOutput, RequestOutput, HttpResponse, ResponseRenderingContext]] {
private val requestParsingIn = Inlet[RequestOutput]("requestParsingIn") private val requestParsingIn = Inlet[RequestOutput]("requestParsingIn")

View file

@ -12,7 +12,7 @@ private[http] final case class UpgradeToWebsocketResponseHeader(handler: Either[
extends InternalCustomHeader("UpgradeToWebsocketResponseHeader") extends InternalCustomHeader("UpgradeToWebsocketResponseHeader")
private[http] abstract class InternalCustomHeader(val name: String) extends CustomHeader { private[http] abstract class InternalCustomHeader(val name: String) extends CustomHeader {
override def suppressRendering: Boolean = true final def renderInRequests = false
final def renderInResponses = false
def value(): String = "" def value: String = ""
} }

View file

@ -0,0 +1,42 @@
/**
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.scaladsl
import scala.concurrent.duration.Duration
import akka.http.scaladsl.model.{ HttpResponse, HttpRequest }
/**
* Enables programmatic access to the server-side request timeout logic.
*/
trait TimeoutAccess extends akka.http.javadsl.TimeoutAccess {
/**
* Tries to set a new timeout.
* The timeout period is measured as of the point in time that the end of the request has been received,
* which may be in the past or in the future!
* Use `Duration.Inf` to completely disable request timeout checking for this request.
*
* Due to the inherent raciness it is not guaranteed that the update will be applied before
* the previously set timeout has expired!
*/
def updateTimeout(timeout: Duration): Unit
/**
* Tries to set a new timeout handler, which produces the timeout response for a
* given request. Note that the handler must produce the response synchronously and shouldn't block!
*
* Due to the inherent raciness it is not guaranteed that the update will be applied before
* the previously set timeout has expired!
*/
def updateHandler(handler: HttpRequest HttpResponse): Unit
/**
* Tries to set a new timeout and handler at the same time.
*
* Due to the inherent raciness it is not guaranteed that the update will be applied before
* the previously set timeout has expired!
*/
def update(timeout: Duration, handler: HttpRequest HttpResponse): Unit
}

View file

@ -21,6 +21,7 @@ import akka.{ japi, stream }
import akka.http.scaladsl.model.ContentType.{ NonBinary, Binary } import akka.http.scaladsl.model.ContentType.{ NonBinary, Binary }
import akka.http.scaladsl.util.FastFuture import akka.http.scaladsl.util.FastFuture
import akka.http.javadsl.{ model jm } import akka.http.javadsl.{ model jm }
import akka.http.impl.util.StreamUtils
import akka.http.impl.util.JavaMapping.Implicits._ import akka.http.impl.util.JavaMapping.Implicits._
import scala.compat.java8.OptionConverters._ import scala.compat.java8.OptionConverters._
@ -503,4 +504,24 @@ object HttpEntity {
private object SizeLimit { private object SizeLimit {
val Disabled = -1 // any negative value will do val Disabled = -1 // any negative value will do
} }
/**
* INTERNAL API
*/
private[http] def captureTermination[T <: HttpEntity](entity: T): (T, Future[Unit]) =
entity match {
case x: HttpEntity.Strict x.asInstanceOf[T] -> FastFuture.successful(())
case x: HttpEntity.Default
val (newData, whenCompleted) = StreamUtils.captureTermination(x.data)
x.copy(data = newData).asInstanceOf[T] -> whenCompleted
case x: HttpEntity.Chunked
val (newChunks, whenCompleted) = StreamUtils.captureTermination(x.chunks)
x.copy(chunks = newChunks).asInstanceOf[T] -> whenCompleted
case x: HttpEntity.CloseDelimited
val (newData, whenCompleted) = StreamUtils.captureTermination(x.data)
x.copy(data = newData).asInstanceOf[T] -> whenCompleted
case x: HttpEntity.IndefiniteLength
val (newData, whenCompleted) = StreamUtils.captureTermination(x.data)
x.copy(data = newData).asInstanceOf[T] -> whenCompleted
}
} }

View file

@ -9,16 +9,12 @@ import java.net.InetSocketAddress
import java.security.MessageDigest import java.security.MessageDigest
import java.util import java.util
import javax.net.ssl.SSLSession import javax.net.ssl.SSLSession
import akka.stream.io.ScalaSessionAPI
import scala.reflect.ClassTag import scala.reflect.ClassTag
import scala.util.{ Failure, Success, Try } import scala.util.{ Failure, Success, Try }
import scala.annotation.tailrec import scala.annotation.tailrec
import scala.collection.immutable import scala.collection.immutable
import akka.parboiled2.util.Base64 import akka.parboiled2.util.Base64
import akka.stream.io.ScalaSessionAPI
import akka.http.impl.util._ import akka.http.impl.util._
import akka.http.javadsl.{ model jm } import akka.http.javadsl.{ model jm }
import akka.http.scaladsl.model._ import akka.http.scaladsl.model._
@ -41,6 +37,8 @@ sealed abstract class ModeledCompanion[T: ClassTag] extends Renderable {
} }
sealed trait ModeledHeader extends HttpHeader with Serializable { sealed trait ModeledHeader extends HttpHeader with Serializable {
def renderInRequests: Boolean = false // default implementation
def renderInResponses: Boolean = false // default implementation
def name: String = companion.name def name: String = companion.name
def value: String = renderValue(new StringRendering).get def value: String = renderValue(new StringRendering).get
def lowercaseName: String = companion.lowercaseName def lowercaseName: String = companion.lowercaseName
@ -49,6 +47,11 @@ sealed trait ModeledHeader extends HttpHeader with Serializable {
protected def companion: ModeledCompanion[_] protected def companion: ModeledCompanion[_]
} }
private[headers] sealed trait RequestHeader extends ModeledHeader { override def renderInRequests = true }
private[headers] sealed trait ResponseHeader extends ModeledHeader { override def renderInResponses = true }
private[headers] sealed trait RequestResponseHeader extends RequestHeader with ResponseHeader
private[headers] sealed trait SyntheticHeader extends ModeledHeader
/** /**
* Superclass for user-defined custom headers defined by implementing `name` and `value`. * Superclass for user-defined custom headers defined by implementing `name` and `value`.
* *
@ -57,9 +60,6 @@ sealed trait ModeledHeader extends HttpHeader with Serializable {
* as they allow the custom header to be matched from [[RawHeader]] and vice-versa. * as they allow the custom header to be matched from [[RawHeader]] and vice-versa.
*/ */
abstract class CustomHeader extends jm.headers.CustomHeader { abstract class CustomHeader extends jm.headers.CustomHeader {
/** Override to return true if this header shouldn't be rendered */
def suppressRendering: Boolean = false
def lowercaseName: String = name.toRootLowerCase def lowercaseName: String = name.toRootLowerCase
final def render[R <: Rendering](r: R): r.type = r ~~ name ~~ ':' ~~ ' ' ~~ value final def render[R <: Rendering](r: R): r.type = r ~~ name ~~ ':' ~~ ' ' ~~ value
} }
@ -98,17 +98,243 @@ abstract class ModeledCustomHeader[H <: ModeledCustomHeader[H]] extends CustomHe
def companion: ModeledCustomHeaderCompanion[H] def companion: ModeledCustomHeaderCompanion[H]
final override def name = companion.name final override def name = companion.name
final override def lowercaseName: String = name.toRootLowerCase final override def lowercaseName = name.toRootLowerCase
} }
import akka.http.impl.util.JavaMapping.Implicits._ import akka.http.impl.util.JavaMapping.Implicits._
// http://tools.ietf.org/html/rfc7231#section-5.3.2
object Accept extends ModeledCompanion[Accept] {
def apply(mediaRanges: MediaRange*): Accept = apply(immutable.Seq(mediaRanges: _*))
implicit val mediaRangesRenderer = Renderer.defaultSeqRenderer[MediaRange] // cache
}
final case class Accept(mediaRanges: immutable.Seq[MediaRange]) extends jm.headers.Accept with RequestHeader {
import Accept.mediaRangesRenderer
def renderValue[R <: Rendering](r: R): r.type = r ~~ mediaRanges
protected def companion = Accept
def acceptsAll = mediaRanges.exists(mr mr.isWildcard && mr.qValue > 0f)
/** Java API */
def getMediaRanges: Iterable[jm.MediaRange] = mediaRanges.asJava
}
// http://tools.ietf.org/html/rfc7231#section-5.3.3
object `Accept-Charset` extends ModeledCompanion[`Accept-Charset`] {
def apply(first: HttpCharsetRange, more: HttpCharsetRange*): `Accept-Charset` = apply(immutable.Seq(first +: more: _*))
implicit val charsetRangesRenderer = Renderer.defaultSeqRenderer[HttpCharsetRange] // cache
}
final case class `Accept-Charset`(charsetRanges: immutable.Seq[HttpCharsetRange]) extends jm.headers.AcceptCharset
with RequestHeader {
require(charsetRanges.nonEmpty, "charsetRanges must not be empty")
import `Accept-Charset`.charsetRangesRenderer
def renderValue[R <: Rendering](r: R): r.type = r ~~ charsetRanges
protected def companion = `Accept-Charset`
/** Java API */
def getCharsetRanges: Iterable[jm.HttpCharsetRange] = charsetRanges.asJava
}
// http://tools.ietf.org/html/rfc7231#section-5.3.4
object `Accept-Encoding` extends ModeledCompanion[`Accept-Encoding`] {
def apply(encodings: HttpEncodingRange*): `Accept-Encoding` = apply(immutable.Seq(encodings: _*))
implicit val encodingsRenderer = Renderer.defaultSeqRenderer[HttpEncodingRange] // cache
}
final case class `Accept-Encoding`(encodings: immutable.Seq[HttpEncodingRange]) extends jm.headers.AcceptEncoding
with RequestHeader {
import `Accept-Encoding`.encodingsRenderer
def renderValue[R <: Rendering](r: R): r.type = r ~~ encodings
protected def companion = `Accept-Encoding`
/** Java API */
def getEncodings: Iterable[jm.headers.HttpEncodingRange] = encodings.asJava
}
// http://tools.ietf.org/html/rfc7231#section-5.3.5
object `Accept-Language` extends ModeledCompanion[`Accept-Language`] {
def apply(first: LanguageRange, more: LanguageRange*): `Accept-Language` = apply(immutable.Seq(first +: more: _*))
implicit val languagesRenderer = Renderer.defaultSeqRenderer[LanguageRange] // cache
}
final case class `Accept-Language`(languages: immutable.Seq[LanguageRange]) extends jm.headers.AcceptLanguage
with RequestHeader {
require(languages.nonEmpty, "languages must not be empty")
import `Accept-Language`.languagesRenderer
def renderValue[R <: Rendering](r: R): r.type = r ~~ languages
protected def companion = `Accept-Language`
/** Java API */
def getLanguages: Iterable[jm.headers.LanguageRange] = languages.asJava
}
// http://tools.ietf.org/html/rfc7233#section-2.3
object `Accept-Ranges` extends ModeledCompanion[`Accept-Ranges`] {
def apply(rangeUnits: RangeUnit*): `Accept-Ranges` = apply(immutable.Seq(rangeUnits: _*))
implicit val rangeUnitsRenderer = Renderer.defaultSeqRenderer[RangeUnit] // cache
}
final case class `Accept-Ranges`(rangeUnits: immutable.Seq[RangeUnit]) extends jm.headers.AcceptRanges
with RequestHeader {
import `Accept-Ranges`.rangeUnitsRenderer
def renderValue[R <: Rendering](r: R): r.type = if (rangeUnits.isEmpty) r ~~ "none" else r ~~ rangeUnits
protected def companion = `Accept-Ranges`
/** Java API */
def getRangeUnits: Iterable[jm.headers.RangeUnit] = rangeUnits.asJava
}
// http://www.w3.org/TR/cors/#access-control-allow-credentials-response-header
object `Access-Control-Allow-Credentials` extends ModeledCompanion[`Access-Control-Allow-Credentials`]
final case class `Access-Control-Allow-Credentials`(allow: Boolean)
extends jm.headers.AccessControlAllowCredentials with RequestHeader {
def renderValue[R <: Rendering](r: R): r.type = r ~~ allow.toString
protected def companion = `Access-Control-Allow-Credentials`
}
// http://www.w3.org/TR/cors/#access-control-allow-headers-response-header
object `Access-Control-Allow-Headers` extends ModeledCompanion[`Access-Control-Allow-Headers`] {
def apply(headers: String*): `Access-Control-Allow-Headers` = apply(immutable.Seq(headers: _*))
implicit val headersRenderer = Renderer.defaultSeqRenderer[String] // cache
}
final case class `Access-Control-Allow-Headers`(headers: immutable.Seq[String])
extends jm.headers.AccessControlAllowHeaders with RequestHeader {
import `Access-Control-Allow-Headers`.headersRenderer
def renderValue[R <: Rendering](r: R): r.type = r ~~ headers
protected def companion = `Access-Control-Allow-Headers`
/** Java API */
def getHeaders: Iterable[String] = headers.asJava
}
// http://www.w3.org/TR/cors/#access-control-allow-methods-response-header
object `Access-Control-Allow-Methods` extends ModeledCompanion[`Access-Control-Allow-Methods`] {
def apply(methods: HttpMethod*): `Access-Control-Allow-Methods` = apply(immutable.Seq(methods: _*))
implicit val methodsRenderer = Renderer.defaultSeqRenderer[HttpMethod] // cache
}
final case class `Access-Control-Allow-Methods`(methods: immutable.Seq[HttpMethod])
extends jm.headers.AccessControlAllowMethods with RequestHeader {
import `Access-Control-Allow-Methods`.methodsRenderer
def renderValue[R <: Rendering](r: R): r.type = r ~~ methods
protected def companion = `Access-Control-Allow-Methods`
/** Java API */
def getMethods: Iterable[jm.HttpMethod] = methods.asJava
}
// http://www.w3.org/TR/cors/#access-control-allow-origin-response-header
object `Access-Control-Allow-Origin` extends ModeledCompanion[`Access-Control-Allow-Origin`] {
val `*` = forRange(HttpOriginRange.`*`)
val `null` = forRange(HttpOriginRange())
def apply(origin: HttpOrigin) = forRange(HttpOriginRange(origin))
/**
* Creates an `Access-Control-Allow-Origin` header for the given origin range.
*
* CAUTION: Even though allowed by the spec (http://www.w3.org/TR/cors/#access-control-allow-origin-response-header)
* `Access-Control-Allow-Origin` headers with more than a single origin appear to be largely unsupported in the field.
* Make sure to thoroughly test such usages with all expected clients!
*/
def forRange(range: HttpOriginRange) = new `Access-Control-Allow-Origin`(range)
}
final case class `Access-Control-Allow-Origin` private (range: HttpOriginRange)
extends jm.headers.AccessControlAllowOrigin with RequestHeader {
def renderValue[R <: Rendering](r: R): r.type = r ~~ range
protected def companion = `Access-Control-Allow-Origin`
}
// http://www.w3.org/TR/cors/#access-control-expose-headers-response-header
object `Access-Control-Expose-Headers` extends ModeledCompanion[`Access-Control-Expose-Headers`] {
def apply(headers: String*): `Access-Control-Expose-Headers` = apply(immutable.Seq(headers: _*))
implicit val headersRenderer = Renderer.defaultSeqRenderer[String] // cache
}
final case class `Access-Control-Expose-Headers`(headers: immutable.Seq[String])
extends jm.headers.AccessControlExposeHeaders with RequestHeader {
import `Access-Control-Expose-Headers`.headersRenderer
def renderValue[R <: Rendering](r: R): r.type = r ~~ headers
protected def companion = `Access-Control-Expose-Headers`
/** Java API */
def getHeaders: Iterable[String] = headers.asJava
}
// http://www.w3.org/TR/cors/#access-control-max-age-response-header
object `Access-Control-Max-Age` extends ModeledCompanion[`Access-Control-Max-Age`]
final case class `Access-Control-Max-Age`(deltaSeconds: Long) extends jm.headers.AccessControlMaxAge
with RequestHeader {
def renderValue[R <: Rendering](r: R): r.type = r ~~ deltaSeconds
protected def companion = `Access-Control-Max-Age`
}
// http://www.w3.org/TR/cors/#access-control-request-headers-request-header
object `Access-Control-Request-Headers` extends ModeledCompanion[`Access-Control-Request-Headers`] {
def apply(headers: String*): `Access-Control-Request-Headers` = apply(immutable.Seq(headers: _*))
implicit val headersRenderer = Renderer.defaultSeqRenderer[String] // cache
}
final case class `Access-Control-Request-Headers`(headers: immutable.Seq[String])
extends jm.headers.AccessControlRequestHeaders with RequestHeader {
import `Access-Control-Request-Headers`.headersRenderer
def renderValue[R <: Rendering](r: R): r.type = r ~~ headers
protected def companion = `Access-Control-Request-Headers`
/** Java API */
def getHeaders: Iterable[String] = headers.asJava
}
// http://www.w3.org/TR/cors/#access-control-request-method-request-header
object `Access-Control-Request-Method` extends ModeledCompanion[`Access-Control-Request-Method`]
final case class `Access-Control-Request-Method`(method: HttpMethod) extends jm.headers.AccessControlRequestMethod
with RequestHeader {
def renderValue[R <: Rendering](r: R): r.type = r ~~ method
protected def companion = `Access-Control-Request-Method`
}
// http://tools.ietf.org/html/rfc7234#section-5.1
object Age extends ModeledCompanion[Age]
final case class Age(deltaSeconds: Long) extends jm.headers.Age with ResponseHeader {
def renderValue[R <: Rendering](r: R): r.type = r ~~ deltaSeconds
protected def companion = Age
}
// http://tools.ietf.org/html/rfc7231#section-7.4.1
object Allow extends ModeledCompanion[Allow] {
def apply(methods: HttpMethod*): Allow = apply(immutable.Seq(methods: _*))
implicit val methodsRenderer = Renderer.defaultSeqRenderer[HttpMethod] // cache
}
final case class Allow(methods: immutable.Seq[HttpMethod]) extends jm.headers.Allow with ResponseHeader {
import Allow.methodsRenderer
def renderValue[R <: Rendering](r: R): r.type = r ~~ methods
protected def companion = Allow
/** Java API */
def getMethods: Iterable[jm.HttpMethod] = methods.asJava
}
// http://tools.ietf.org/html/rfc7235#section-4.2
object Authorization extends ModeledCompanion[Authorization]
final case class Authorization(credentials: HttpCredentials) extends jm.headers.Authorization with RequestHeader {
def renderValue[R <: Rendering](r: R): r.type = r ~~ credentials
protected def companion = Authorization
}
// http://tools.ietf.org/html/rfc7234#section-5.2
object `Cache-Control` extends ModeledCompanion[`Cache-Control`] {
def apply(first: CacheDirective, more: CacheDirective*): `Cache-Control` = apply(immutable.Seq(first +: more: _*))
implicit val directivesRenderer = Renderer.defaultSeqRenderer[CacheDirective] // cache
}
final case class `Cache-Control`(directives: immutable.Seq[CacheDirective]) extends jm.headers.CacheControl
with RequestResponseHeader {
require(directives.nonEmpty, "directives must not be empty")
import `Cache-Control`.directivesRenderer
def renderValue[R <: Rendering](r: R): r.type = r ~~ directives
protected def companion = `Cache-Control`
/** Java API */
def getDirectives: Iterable[jm.headers.CacheDirective] = directives.asJava
}
// http://tools.ietf.org/html/rfc7230#section-6.1 // http://tools.ietf.org/html/rfc7230#section-6.1
object Connection extends ModeledCompanion[Connection] { object Connection extends ModeledCompanion[Connection] {
def apply(first: String, more: String*): Connection = apply(immutable.Seq(first +: more: _*)) def apply(first: String, more: String*): Connection = apply(immutable.Seq(first +: more: _*))
implicit val tokensRenderer = Renderer.defaultSeqRenderer[String] // cache implicit val tokensRenderer = Renderer.defaultSeqRenderer[String] // cache
} }
final case class Connection(tokens: immutable.Seq[String]) extends ModeledHeader { final case class Connection(tokens: immutable.Seq[String]) extends RequestResponseHeader {
require(tokens.nonEmpty, "tokens must not be empty") require(tokens.nonEmpty, "tokens must not be empty")
import Connection.tokensRenderer import Connection.tokensRenderer
def renderValue[R <: Rendering](r: R): r.type = r ~~ tokens def renderValue[R <: Rendering](r: R): r.type = r ~~ tokens
@ -133,277 +359,20 @@ object `Content-Length` extends ModeledCompanion[`Content-Length`]
* Instances of this class will only be created transiently during header parsing and will never appear * Instances of this class will only be created transiently during header parsing and will never appear
* in HttpMessage.header. To access the Content-Length, see subclasses of HttpEntity. * in HttpMessage.header. To access the Content-Length, see subclasses of HttpEntity.
*/ */
final case class `Content-Length` private[http] (length: Long) extends ModeledHeader { final case class `Content-Length` private[http] (length: Long) extends RequestResponseHeader {
def renderValue[R <: Rendering](r: R): r.type = r ~~ length def renderValue[R <: Rendering](r: R): r.type = r ~~ length
protected def companion = `Content-Length` protected def companion = `Content-Length`
} }
// http://tools.ietf.org/html/rfc7231#section-5.1.1
object Expect extends ModeledCompanion[Expect] {
val `100-continue` = new Expect() {}
}
sealed abstract case class Expect private () extends ModeledHeader {
final def renderValue[R <: Rendering](r: R): r.type = r ~~ "100-continue"
protected def companion = Expect
}
// http://tools.ietf.org/html/rfc7230#section-5.4
object Host extends ModeledCompanion[Host] {
def apply(authority: Uri.Authority): Host = apply(authority.host, authority.port)
def apply(address: InetSocketAddress): Host = apply(address.getHostString, address.getPort)
def apply(host: String): Host = apply(host, 0)
def apply(host: String, port: Int): Host = apply(Uri.Host(host), port)
val empty = Host("")
}
final case class Host(host: Uri.Host, port: Int = 0) extends jm.headers.Host with ModeledHeader {
import UriRendering.HostRenderer
require((port >> 16) == 0, "Illegal port: " + port)
def isEmpty = host.isEmpty
def renderValue[R <: Rendering](r: R): r.type = if (port > 0) r ~~ host ~~ ':' ~~ port else r ~~ host
protected def companion = Host
def equalsIgnoreCase(other: Host): Boolean = host.equalsIgnoreCase(other.host) && port == other.port
}
// http://tools.ietf.org/html/rfc7233#section-3.2
object `If-Range` extends ModeledCompanion[`If-Range`] {
def apply(tag: EntityTag): `If-Range` = apply(Left(tag))
def apply(timestamp: DateTime): `If-Range` = apply(Right(timestamp))
}
final case class `If-Range`(entityTagOrDateTime: Either[EntityTag, DateTime]) extends ModeledHeader {
def renderValue[R <: Rendering](r: R): r.type =
entityTagOrDateTime match {
case Left(tag) r ~~ tag
case Right(dateTime) dateTime.renderRfc1123DateTimeString(r)
}
protected def companion = `If-Range`
}
final case class RawHeader(name: String, value: String) extends jm.headers.RawHeader {
val lowercaseName = name.toRootLowerCase
def render[R <: Rendering](r: R): r.type = r ~~ name ~~ ':' ~~ ' ' ~~ value
}
object RawHeader {
def unapply[H <: HttpHeader](customHeader: H): Option[(String, String)] =
Some(customHeader.name -> customHeader.value)
}
// http://tools.ietf.org/html/rfc7231#section-5.3.2
object Accept extends ModeledCompanion[Accept] {
def apply(mediaRanges: MediaRange*): Accept = apply(immutable.Seq(mediaRanges: _*))
implicit val mediaRangesRenderer = Renderer.defaultSeqRenderer[MediaRange] // cache
}
final case class Accept(mediaRanges: immutable.Seq[MediaRange]) extends jm.headers.Accept with ModeledHeader {
import Accept.mediaRangesRenderer
def renderValue[R <: Rendering](r: R): r.type = r ~~ mediaRanges
protected def companion = Accept
def acceptsAll = mediaRanges.exists(mr mr.isWildcard && mr.qValue > 0f)
/** Java API */
def getMediaRanges: Iterable[jm.MediaRange] = mediaRanges.asJava
}
// http://tools.ietf.org/html/rfc7231#section-5.3.3
object `Accept-Charset` extends ModeledCompanion[`Accept-Charset`] {
def apply(first: HttpCharsetRange, more: HttpCharsetRange*): `Accept-Charset` = apply(immutable.Seq(first +: more: _*))
implicit val charsetRangesRenderer = Renderer.defaultSeqRenderer[HttpCharsetRange] // cache
}
final case class `Accept-Charset`(charsetRanges: immutable.Seq[HttpCharsetRange]) extends jm.headers.AcceptCharset with ModeledHeader {
require(charsetRanges.nonEmpty, "charsetRanges must not be empty")
import `Accept-Charset`.charsetRangesRenderer
def renderValue[R <: Rendering](r: R): r.type = r ~~ charsetRanges
protected def companion = `Accept-Charset`
/** Java API */
def getCharsetRanges: Iterable[jm.HttpCharsetRange] = charsetRanges.asJava
}
// http://tools.ietf.org/html/rfc7231#section-5.3.4
object `Accept-Encoding` extends ModeledCompanion[`Accept-Encoding`] {
def apply(encodings: HttpEncodingRange*): `Accept-Encoding` = apply(immutable.Seq(encodings: _*))
implicit val encodingsRenderer = Renderer.defaultSeqRenderer[HttpEncodingRange] // cache
}
final case class `Accept-Encoding`(encodings: immutable.Seq[HttpEncodingRange]) extends jm.headers.AcceptEncoding with ModeledHeader {
import `Accept-Encoding`.encodingsRenderer
def renderValue[R <: Rendering](r: R): r.type = r ~~ encodings
protected def companion = `Accept-Encoding`
/** Java API */
def getEncodings: Iterable[jm.headers.HttpEncodingRange] = encodings.asJava
}
// http://tools.ietf.org/html/rfc7231#section-5.3.5
object `Accept-Language` extends ModeledCompanion[`Accept-Language`] {
def apply(first: LanguageRange, more: LanguageRange*): `Accept-Language` = apply(immutable.Seq(first +: more: _*))
implicit val languagesRenderer = Renderer.defaultSeqRenderer[LanguageRange] // cache
}
final case class `Accept-Language`(languages: immutable.Seq[LanguageRange]) extends jm.headers.AcceptLanguage with ModeledHeader {
require(languages.nonEmpty, "languages must not be empty")
import `Accept-Language`.languagesRenderer
def renderValue[R <: Rendering](r: R): r.type = r ~~ languages
protected def companion = `Accept-Language`
/** Java API */
def getLanguages: Iterable[jm.headers.LanguageRange] = languages.asJava
}
// http://tools.ietf.org/html/rfc7233#section-2.3
object `Accept-Ranges` extends ModeledCompanion[`Accept-Ranges`] {
def apply(rangeUnits: RangeUnit*): `Accept-Ranges` = apply(immutable.Seq(rangeUnits: _*))
implicit val rangeUnitsRenderer = Renderer.defaultSeqRenderer[RangeUnit] // cache
}
final case class `Accept-Ranges`(rangeUnits: immutable.Seq[RangeUnit]) extends jm.headers.AcceptRanges with ModeledHeader {
import `Accept-Ranges`.rangeUnitsRenderer
def renderValue[R <: Rendering](r: R): r.type = if (rangeUnits.isEmpty) r ~~ "none" else r ~~ rangeUnits
protected def companion = `Accept-Ranges`
/** Java API */
def getRangeUnits: Iterable[jm.headers.RangeUnit] = rangeUnits.asJava
}
// http://www.w3.org/TR/cors/#access-control-allow-credentials-response-header
object `Access-Control-Allow-Credentials` extends ModeledCompanion[`Access-Control-Allow-Credentials`]
final case class `Access-Control-Allow-Credentials`(allow: Boolean) extends jm.headers.AccessControlAllowCredentials with ModeledHeader {
def renderValue[R <: Rendering](r: R): r.type = r ~~ allow.toString
protected def companion = `Access-Control-Allow-Credentials`
}
// http://www.w3.org/TR/cors/#access-control-allow-headers-response-header
object `Access-Control-Allow-Headers` extends ModeledCompanion[`Access-Control-Allow-Headers`] {
def apply(headers: String*): `Access-Control-Allow-Headers` = apply(immutable.Seq(headers: _*))
implicit val headersRenderer = Renderer.defaultSeqRenderer[String] // cache
}
final case class `Access-Control-Allow-Headers`(headers: immutable.Seq[String]) extends jm.headers.AccessControlAllowHeaders with ModeledHeader {
import `Access-Control-Allow-Headers`.headersRenderer
def renderValue[R <: Rendering](r: R): r.type = r ~~ headers
protected def companion = `Access-Control-Allow-Headers`
/** Java API */
def getHeaders: Iterable[String] = headers.asJava
}
// http://www.w3.org/TR/cors/#access-control-allow-methods-response-header
object `Access-Control-Allow-Methods` extends ModeledCompanion[`Access-Control-Allow-Methods`] {
def apply(methods: HttpMethod*): `Access-Control-Allow-Methods` = apply(immutable.Seq(methods: _*))
implicit val methodsRenderer = Renderer.defaultSeqRenderer[HttpMethod] // cache
}
final case class `Access-Control-Allow-Methods`(methods: immutable.Seq[HttpMethod]) extends jm.headers.AccessControlAllowMethods with ModeledHeader {
import `Access-Control-Allow-Methods`.methodsRenderer
def renderValue[R <: Rendering](r: R): r.type = r ~~ methods
protected def companion = `Access-Control-Allow-Methods`
/** Java API */
def getMethods: Iterable[jm.HttpMethod] = methods.asJava
}
// http://www.w3.org/TR/cors/#access-control-allow-origin-response-header
object `Access-Control-Allow-Origin` extends ModeledCompanion[`Access-Control-Allow-Origin`] {
val `*` = forRange(HttpOriginRange.`*`)
val `null` = forRange(HttpOriginRange())
def apply(origin: HttpOrigin) = forRange(HttpOriginRange(origin))
/**
* Creates an `Access-Control-Allow-Origin` header for the given origin range.
*
* CAUTION: Even though allowed by the spec (http://www.w3.org/TR/cors/#access-control-allow-origin-response-header)
* `Access-Control-Allow-Origin` headers with more than a single origin appear to be largely unsupported in the field.
* Make sure to thoroughly test such usages with all expected clients!
*/
def forRange(range: HttpOriginRange) = new `Access-Control-Allow-Origin`(range)
}
final case class `Access-Control-Allow-Origin` private (range: HttpOriginRange) extends jm.headers.AccessControlAllowOrigin with ModeledHeader {
def renderValue[R <: Rendering](r: R): r.type = r ~~ range
protected def companion = `Access-Control-Allow-Origin`
}
// http://www.w3.org/TR/cors/#access-control-expose-headers-response-header
object `Access-Control-Expose-Headers` extends ModeledCompanion[`Access-Control-Expose-Headers`] {
def apply(headers: String*): `Access-Control-Expose-Headers` = apply(immutable.Seq(headers: _*))
implicit val headersRenderer = Renderer.defaultSeqRenderer[String] // cache
}
final case class `Access-Control-Expose-Headers`(headers: immutable.Seq[String]) extends jm.headers.AccessControlExposeHeaders with ModeledHeader {
import `Access-Control-Expose-Headers`.headersRenderer
def renderValue[R <: Rendering](r: R): r.type = r ~~ headers
protected def companion = `Access-Control-Expose-Headers`
/** Java API */
def getHeaders: Iterable[String] = headers.asJava
}
// http://www.w3.org/TR/cors/#access-control-max-age-response-header
object `Access-Control-Max-Age` extends ModeledCompanion[`Access-Control-Max-Age`]
final case class `Access-Control-Max-Age`(deltaSeconds: Long) extends jm.headers.AccessControlMaxAge with ModeledHeader {
def renderValue[R <: Rendering](r: R): r.type = r ~~ deltaSeconds
protected def companion = `Access-Control-Max-Age`
}
// http://www.w3.org/TR/cors/#access-control-request-headers-request-header
object `Access-Control-Request-Headers` extends ModeledCompanion[`Access-Control-Request-Headers`] {
def apply(headers: String*): `Access-Control-Request-Headers` = apply(immutable.Seq(headers: _*))
implicit val headersRenderer = Renderer.defaultSeqRenderer[String] // cache
}
final case class `Access-Control-Request-Headers`(headers: immutable.Seq[String]) extends jm.headers.AccessControlRequestHeaders with ModeledHeader {
import `Access-Control-Request-Headers`.headersRenderer
def renderValue[R <: Rendering](r: R): r.type = r ~~ headers
protected def companion = `Access-Control-Request-Headers`
/** Java API */
def getHeaders: Iterable[String] = headers.asJava
}
// http://www.w3.org/TR/cors/#access-control-request-method-request-header
object `Access-Control-Request-Method` extends ModeledCompanion[`Access-Control-Request-Method`]
final case class `Access-Control-Request-Method`(method: HttpMethod) extends jm.headers.AccessControlRequestMethod with ModeledHeader {
def renderValue[R <: Rendering](r: R): r.type = r ~~ method
protected def companion = `Access-Control-Request-Method`
}
// http://tools.ietf.org/html/rfc7234#section-5.1
object Age extends ModeledCompanion[Age]
final case class Age(deltaSeconds: Long) extends jm.headers.Age with ModeledHeader {
def renderValue[R <: Rendering](r: R): r.type = r ~~ deltaSeconds
protected def companion = Age
}
// http://tools.ietf.org/html/rfc7231#section-7.4.1
object Allow extends ModeledCompanion[Allow] {
def apply(methods: HttpMethod*): Allow = apply(immutable.Seq(methods: _*))
implicit val methodsRenderer = Renderer.defaultSeqRenderer[HttpMethod] // cache
}
final case class Allow(methods: immutable.Seq[HttpMethod]) extends jm.headers.Allow with ModeledHeader {
import Allow.methodsRenderer
def renderValue[R <: Rendering](r: R): r.type = r ~~ methods
protected def companion = Allow
/** Java API */
def getMethods: Iterable[jm.HttpMethod] = methods.asJava
}
// http://tools.ietf.org/html/rfc7235#section-4.2
object Authorization extends ModeledCompanion[Authorization]
final case class Authorization(credentials: HttpCredentials) extends jm.headers.Authorization with ModeledHeader {
def renderValue[R <: Rendering](r: R): r.type = r ~~ credentials
protected def companion = Authorization
}
// http://tools.ietf.org/html/rfc7234#section-5.2
object `Cache-Control` extends ModeledCompanion[`Cache-Control`] {
def apply(first: CacheDirective, more: CacheDirective*): `Cache-Control` = apply(immutable.Seq(first +: more: _*))
implicit val directivesRenderer = Renderer.defaultSeqRenderer[CacheDirective] // cache
}
final case class `Cache-Control`(directives: immutable.Seq[CacheDirective]) extends jm.headers.CacheControl with ModeledHeader {
require(directives.nonEmpty, "directives must not be empty")
import `Cache-Control`.directivesRenderer
def renderValue[R <: Rendering](r: R): r.type = r ~~ directives
protected def companion = `Cache-Control`
/** Java API */
def getDirectives: Iterable[jm.headers.CacheDirective] = directives.asJava
}
// http://tools.ietf.org/html/rfc6266 // http://tools.ietf.org/html/rfc6266
object `Content-Disposition` extends ModeledCompanion[`Content-Disposition`] object `Content-Disposition` extends ModeledCompanion[`Content-Disposition`]
final case class `Content-Disposition`(dispositionType: ContentDispositionType, params: Map[String, String] = Map.empty) extends jm.headers.ContentDisposition with ModeledHeader { final case class `Content-Disposition`(dispositionType: ContentDispositionType, params: Map[String, String] = Map.empty)
def renderValue[R <: Rendering](r: R): r.type = { r ~~ dispositionType; params foreach { case (k, v) r ~~ "; " ~~ k ~~ '=' ~~# v }; r } extends jm.headers.ContentDisposition with RequestResponseHeader {
def renderValue[R <: Rendering](r: R): r.type = {
r ~~ dispositionType
params foreach { case (k, v) r ~~ "; " ~~ k ~~ '=' ~~# v }
r
}
protected def companion = `Content-Disposition` protected def companion = `Content-Disposition`
/** Java API */ /** Java API */
@ -415,7 +384,8 @@ object `Content-Encoding` extends ModeledCompanion[`Content-Encoding`] {
def apply(first: HttpEncoding, more: HttpEncoding*): `Content-Encoding` = apply(immutable.Seq(first +: more: _*)) def apply(first: HttpEncoding, more: HttpEncoding*): `Content-Encoding` = apply(immutable.Seq(first +: more: _*))
implicit val encodingsRenderer = Renderer.defaultSeqRenderer[HttpEncoding] // cache implicit val encodingsRenderer = Renderer.defaultSeqRenderer[HttpEncoding] // cache
} }
final case class `Content-Encoding`(encodings: immutable.Seq[HttpEncoding]) extends jm.headers.ContentEncoding with ModeledHeader { final case class `Content-Encoding`(encodings: immutable.Seq[HttpEncoding]) extends jm.headers.ContentEncoding
with RequestResponseHeader {
require(encodings.nonEmpty, "encodings must not be empty") require(encodings.nonEmpty, "encodings must not be empty")
import `Content-Encoding`.encodingsRenderer import `Content-Encoding`.encodingsRenderer
def renderValue[R <: Rendering](r: R): r.type = r ~~ encodings def renderValue[R <: Rendering](r: R): r.type = r ~~ encodings
@ -429,7 +399,8 @@ final case class `Content-Encoding`(encodings: immutable.Seq[HttpEncoding]) exte
object `Content-Range` extends ModeledCompanion[`Content-Range`] { object `Content-Range` extends ModeledCompanion[`Content-Range`] {
def apply(byteContentRange: ByteContentRange): `Content-Range` = apply(RangeUnits.Bytes, byteContentRange) def apply(byteContentRange: ByteContentRange): `Content-Range` = apply(RangeUnits.Bytes, byteContentRange)
} }
final case class `Content-Range`(rangeUnit: RangeUnit, contentRange: ContentRange) extends jm.headers.ContentRange with ModeledHeader { final case class `Content-Range`(rangeUnit: RangeUnit, contentRange: ContentRange) extends jm.headers.ContentRange
with RequestResponseHeader {
def renderValue[R <: Rendering](r: R): r.type = r ~~ rangeUnit ~~ ' ' ~~ contentRange def renderValue[R <: Rendering](r: R): r.type = r ~~ rangeUnit ~~ ' ' ~~ contentRange
protected def companion = `Content-Range` protected def companion = `Content-Range`
} }
@ -440,7 +411,8 @@ object `Content-Type` extends ModeledCompanion[`Content-Type`]
* Instances of this class will only be created transiently during header parsing and will never appear * Instances of this class will only be created transiently during header parsing and will never appear
* in HttpMessage.header. To access the Content-Type, see subclasses of HttpEntity. * in HttpMessage.header. To access the Content-Type, see subclasses of HttpEntity.
*/ */
final case class `Content-Type` private[http] (contentType: ContentType) extends jm.headers.ContentType with ModeledHeader { final case class `Content-Type` private[http] (contentType: ContentType) extends jm.headers.ContentType
with RequestResponseHeader {
def renderValue[R <: Rendering](r: R): r.type = r ~~ contentType def renderValue[R <: Rendering](r: R): r.type = r ~~ contentType
protected def companion = `Content-Type` protected def companion = `Content-Type`
} }
@ -452,7 +424,7 @@ object Cookie extends ModeledCompanion[Cookie] {
def apply(values: (String, String)*): Cookie = apply(values.map(HttpCookiePair(_)).toList) def apply(values: (String, String)*): Cookie = apply(values.map(HttpCookiePair(_)).toList)
implicit val cookiePairsRenderer = Renderer.seqRenderer[HttpCookiePair](separator = "; ") // cache implicit val cookiePairsRenderer = Renderer.seqRenderer[HttpCookiePair](separator = "; ") // cache
} }
final case class Cookie(cookies: immutable.Seq[HttpCookiePair]) extends jm.headers.Cookie with ModeledHeader { final case class Cookie(cookies: immutable.Seq[HttpCookiePair]) extends jm.headers.Cookie with RequestHeader {
require(cookies.nonEmpty, "cookies must not be empty") require(cookies.nonEmpty, "cookies must not be empty")
import Cookie.cookiePairsRenderer import Cookie.cookiePairsRenderer
def renderValue[R <: Rendering](r: R): r.type = r ~~ cookies def renderValue[R <: Rendering](r: R): r.type = r ~~ cookies
@ -464,42 +436,80 @@ final case class Cookie(cookies: immutable.Seq[HttpCookiePair]) extends jm.heade
// http://tools.ietf.org/html/rfc7231#section-7.1.1.2 // http://tools.ietf.org/html/rfc7231#section-7.1.1.2
object Date extends ModeledCompanion[Date] object Date extends ModeledCompanion[Date]
final case class Date(date: DateTime) extends jm.headers.Date with ModeledHeader { final case class Date(date: DateTime) extends jm.headers.Date with RequestResponseHeader {
def renderValue[R <: Rendering](r: R): r.type = date.renderRfc1123DateTimeString(r) def renderValue[R <: Rendering](r: R): r.type = date.renderRfc1123DateTimeString(r)
protected def companion = Date protected def companion = Date
} }
/**
* INTERNAL API
*/
private[headers] object EmptyCompanion extends ModeledCompanion[EmptyHeader.type]
/**
* INTERNAL API
*/
private[http] object EmptyHeader extends SyntheticHeader {
def renderValue[R <: Rendering](r: R): r.type = r
protected def companion: ModeledCompanion[EmptyHeader.type] = EmptyCompanion
}
// http://tools.ietf.org/html/rfc7232#section-2.3 // http://tools.ietf.org/html/rfc7232#section-2.3
object ETag extends ModeledCompanion[ETag] { object ETag extends ModeledCompanion[ETag] {
def apply(tag: String, weak: Boolean = false): ETag = ETag(EntityTag(tag, weak)) def apply(tag: String, weak: Boolean = false): ETag = ETag(EntityTag(tag, weak))
} }
final case class ETag(etag: EntityTag) extends jm.headers.ETag with ModeledHeader { final case class ETag(etag: EntityTag) extends jm.headers.ETag with ResponseHeader {
def renderValue[R <: Rendering](r: R): r.type = r ~~ etag def renderValue[R <: Rendering](r: R): r.type = r ~~ etag
protected def companion = ETag protected def companion = ETag
} }
// http://tools.ietf.org/html/rfc7231#section-5.1.1
object Expect extends ModeledCompanion[Expect] {
val `100-continue` = new Expect() {}
}
sealed abstract case class Expect private () extends RequestHeader {
final def renderValue[R <: Rendering](r: R): r.type = r ~~ "100-continue"
protected def companion = Expect
}
// http://tools.ietf.org/html/rfc7234#section-5.3 // http://tools.ietf.org/html/rfc7234#section-5.3
object Expires extends ModeledCompanion[Expires] object Expires extends ModeledCompanion[Expires]
final case class Expires(date: DateTime) extends jm.headers.Expires with ModeledHeader { final case class Expires(date: DateTime) extends jm.headers.Expires with ResponseHeader {
def renderValue[R <: Rendering](r: R): r.type = date.renderRfc1123DateTimeString(r) def renderValue[R <: Rendering](r: R): r.type = date.renderRfc1123DateTimeString(r)
protected def companion = Expires protected def companion = Expires
} }
// http://tools.ietf.org/html/rfc7230#section-5.4
object Host extends ModeledCompanion[Host] {
def apply(authority: Uri.Authority): Host = apply(authority.host, authority.port)
def apply(address: InetSocketAddress): Host = apply(address.getHostString, address.getPort)
def apply(host: String): Host = apply(host, 0)
def apply(host: String, port: Int): Host = apply(Uri.Host(host), port)
val empty = Host("")
}
final case class Host(host: Uri.Host, port: Int = 0) extends jm.headers.Host with RequestHeader {
import UriRendering.HostRenderer
require((port >> 16) == 0, "Illegal port: " + port)
def isEmpty = host.isEmpty
def renderValue[R <: Rendering](r: R): r.type = if (port > 0) r ~~ host ~~ ':' ~~ port else r ~~ host
protected def companion = Host
def equalsIgnoreCase(other: Host): Boolean = host.equalsIgnoreCase(other.host) && port == other.port
}
// http://tools.ietf.org/html/rfc7232#section-3.1 // http://tools.ietf.org/html/rfc7232#section-3.1
object `If-Match` extends ModeledCompanion[`If-Match`] { object `If-Match` extends ModeledCompanion[`If-Match`] {
val `*` = `If-Match`(EntityTagRange.`*`) val `*` = `If-Match`(EntityTagRange.`*`)
def apply(first: EntityTag, more: EntityTag*): `If-Match` = def apply(first: EntityTag, more: EntityTag*): `If-Match` =
`If-Match`(EntityTagRange(first +: more: _*)) `If-Match`(EntityTagRange(first +: more: _*))
} }
final case class `If-Match`(m: EntityTagRange) extends jm.headers.IfMatch with ModeledHeader { final case class `If-Match`(m: EntityTagRange) extends jm.headers.IfMatch with RequestHeader {
def renderValue[R <: Rendering](r: R): r.type = r ~~ m def renderValue[R <: Rendering](r: R): r.type = r ~~ m
protected def companion = `If-Match` protected def companion = `If-Match`
} }
// http://tools.ietf.org/html/rfc7232#section-3.3 // http://tools.ietf.org/html/rfc7232#section-3.3
object `If-Modified-Since` extends ModeledCompanion[`If-Modified-Since`] object `If-Modified-Since` extends ModeledCompanion[`If-Modified-Since`]
final case class `If-Modified-Since`(date: DateTime) extends jm.headers.IfModifiedSince with ModeledHeader { final case class `If-Modified-Since`(date: DateTime) extends jm.headers.IfModifiedSince with RequestHeader {
def renderValue[R <: Rendering](r: R): r.type = date.renderRfc1123DateTimeString(r) def renderValue[R <: Rendering](r: R): r.type = date.renderRfc1123DateTimeString(r)
protected def companion = `If-Modified-Since` protected def companion = `If-Modified-Since`
} }
@ -510,21 +520,35 @@ object `If-None-Match` extends ModeledCompanion[`If-None-Match`] {
def apply(first: EntityTag, more: EntityTag*): `If-None-Match` = def apply(first: EntityTag, more: EntityTag*): `If-None-Match` =
`If-None-Match`(EntityTagRange(first +: more: _*)) `If-None-Match`(EntityTagRange(first +: more: _*))
} }
final case class `If-None-Match`(m: EntityTagRange) extends jm.headers.IfNoneMatch with ModeledHeader { final case class `If-None-Match`(m: EntityTagRange) extends jm.headers.IfNoneMatch with RequestHeader {
def renderValue[R <: Rendering](r: R): r.type = r ~~ m def renderValue[R <: Rendering](r: R): r.type = r ~~ m
protected def companion = `If-None-Match` protected def companion = `If-None-Match`
} }
// http://tools.ietf.org/html/rfc7233#section-3.2
object `If-Range` extends ModeledCompanion[`If-Range`] {
def apply(tag: EntityTag): `If-Range` = apply(Left(tag))
def apply(timestamp: DateTime): `If-Range` = apply(Right(timestamp))
}
final case class `If-Range`(entityTagOrDateTime: Either[EntityTag, DateTime]) extends RequestHeader {
def renderValue[R <: Rendering](r: R): r.type =
entityTagOrDateTime match {
case Left(tag) r ~~ tag
case Right(dateTime) dateTime.renderRfc1123DateTimeString(r)
}
protected def companion = `If-Range`
}
// http://tools.ietf.org/html/rfc7232#section-3.4 // http://tools.ietf.org/html/rfc7232#section-3.4
object `If-Unmodified-Since` extends ModeledCompanion[`If-Unmodified-Since`] object `If-Unmodified-Since` extends ModeledCompanion[`If-Unmodified-Since`]
final case class `If-Unmodified-Since`(date: DateTime) extends jm.headers.IfUnmodifiedSince with ModeledHeader { final case class `If-Unmodified-Since`(date: DateTime) extends jm.headers.IfUnmodifiedSince with RequestHeader {
def renderValue[R <: Rendering](r: R): r.type = date.renderRfc1123DateTimeString(r) def renderValue[R <: Rendering](r: R): r.type = date.renderRfc1123DateTimeString(r)
protected def companion = `If-Unmodified-Since` protected def companion = `If-Unmodified-Since`
} }
// http://tools.ietf.org/html/rfc7232#section-2.2 // http://tools.ietf.org/html/rfc7232#section-2.2
object `Last-Modified` extends ModeledCompanion[`Last-Modified`] object `Last-Modified` extends ModeledCompanion[`Last-Modified`]
final case class `Last-Modified`(date: DateTime) extends jm.headers.LastModified with ModeledHeader { final case class `Last-Modified`(date: DateTime) extends jm.headers.LastModified with ResponseHeader {
def renderValue[R <: Rendering](r: R): r.type = date.renderRfc1123DateTimeString(r) def renderValue[R <: Rendering](r: R): r.type = date.renderRfc1123DateTimeString(r)
protected def companion = `Last-Modified` protected def companion = `Last-Modified`
} }
@ -535,7 +559,7 @@ object Link extends ModeledCompanion[Link] {
def apply(values: LinkValue*): Link = apply(immutable.Seq(values: _*)) def apply(values: LinkValue*): Link = apply(immutable.Seq(values: _*))
implicit val valuesRenderer = Renderer.defaultSeqRenderer[LinkValue] // cache implicit val valuesRenderer = Renderer.defaultSeqRenderer[LinkValue] // cache
} }
final case class Link(values: immutable.Seq[LinkValue]) extends jm.headers.Link with ModeledHeader { final case class Link(values: immutable.Seq[LinkValue]) extends jm.headers.Link with RequestResponseHeader {
import Link.valuesRenderer import Link.valuesRenderer
def renderValue[R <: Rendering](r: R): r.type = r ~~ values def renderValue[R <: Rendering](r: R): r.type = r ~~ values
protected def companion = Link protected def companion = Link
@ -546,7 +570,7 @@ final case class Link(values: immutable.Seq[LinkValue]) extends jm.headers.Link
// http://tools.ietf.org/html/rfc7231#section-7.1.2 // http://tools.ietf.org/html/rfc7231#section-7.1.2
object Location extends ModeledCompanion[Location] object Location extends ModeledCompanion[Location]
final case class Location(uri: Uri) extends jm.headers.Location with ModeledHeader { final case class Location(uri: Uri) extends jm.headers.Location with ResponseHeader {
def renderValue[R <: Rendering](r: R): r.type = { import UriRendering.UriRenderer; r ~~ uri } def renderValue[R <: Rendering](r: R): r.type = { import UriRendering.UriRenderer; r ~~ uri }
protected def companion = Location protected def companion = Location
@ -558,7 +582,7 @@ final case class Location(uri: Uri) extends jm.headers.Location with ModeledHead
object Origin extends ModeledCompanion[Origin] { object Origin extends ModeledCompanion[Origin] {
def apply(origins: HttpOrigin*): Origin = apply(immutable.Seq(origins: _*)) def apply(origins: HttpOrigin*): Origin = apply(immutable.Seq(origins: _*))
} }
final case class Origin(origins: immutable.Seq[HttpOrigin]) extends jm.headers.Origin with ModeledHeader { final case class Origin(origins: immutable.Seq[HttpOrigin]) extends jm.headers.Origin with RequestHeader {
def renderValue[R <: Rendering](r: R): r.type = if (origins.isEmpty) r ~~ "null" else r ~~ origins def renderValue[R <: Rendering](r: R): r.type = if (origins.isEmpty) r ~~ "null" else r ~~ origins
protected def companion = Origin protected def companion = Origin
@ -571,7 +595,8 @@ object `Proxy-Authenticate` extends ModeledCompanion[`Proxy-Authenticate`] {
def apply(first: HttpChallenge, more: HttpChallenge*): `Proxy-Authenticate` = apply(immutable.Seq(first +: more: _*)) def apply(first: HttpChallenge, more: HttpChallenge*): `Proxy-Authenticate` = apply(immutable.Seq(first +: more: _*))
implicit val challengesRenderer = Renderer.defaultSeqRenderer[HttpChallenge] // cache implicit val challengesRenderer = Renderer.defaultSeqRenderer[HttpChallenge] // cache
} }
final case class `Proxy-Authenticate`(challenges: immutable.Seq[HttpChallenge]) extends jm.headers.ProxyAuthenticate with ModeledHeader { final case class `Proxy-Authenticate`(challenges: immutable.Seq[HttpChallenge]) extends jm.headers.ProxyAuthenticate
with ResponseHeader {
require(challenges.nonEmpty, "challenges must not be empty") require(challenges.nonEmpty, "challenges must not be empty")
import `Proxy-Authenticate`.challengesRenderer import `Proxy-Authenticate`.challengesRenderer
def renderValue[R <: Rendering](r: R): r.type = r ~~ challenges def renderValue[R <: Rendering](r: R): r.type = r ~~ challenges
@ -583,7 +608,8 @@ final case class `Proxy-Authenticate`(challenges: immutable.Seq[HttpChallenge])
// http://tools.ietf.org/html/rfc7235#section-4.4 // http://tools.ietf.org/html/rfc7235#section-4.4
object `Proxy-Authorization` extends ModeledCompanion[`Proxy-Authorization`] object `Proxy-Authorization` extends ModeledCompanion[`Proxy-Authorization`]
final case class `Proxy-Authorization`(credentials: HttpCredentials) extends jm.headers.ProxyAuthorization with ModeledHeader { final case class `Proxy-Authorization`(credentials: HttpCredentials) extends jm.headers.ProxyAuthorization
with RequestHeader {
def renderValue[R <: Rendering](r: R): r.type = r ~~ credentials def renderValue[R <: Rendering](r: R): r.type = r ~~ credentials
protected def companion = `Proxy-Authorization` protected def companion = `Proxy-Authorization`
} }
@ -594,7 +620,8 @@ object Range extends ModeledCompanion[Range] {
def apply(ranges: immutable.Seq[ByteRange]): Range = Range(RangeUnits.Bytes, ranges) def apply(ranges: immutable.Seq[ByteRange]): Range = Range(RangeUnits.Bytes, ranges)
implicit val rangesRenderer = Renderer.defaultSeqRenderer[ByteRange] // cache implicit val rangesRenderer = Renderer.defaultSeqRenderer[ByteRange] // cache
} }
final case class Range(rangeUnit: RangeUnit, ranges: immutable.Seq[ByteRange]) extends jm.headers.Range with ModeledHeader { final case class Range(rangeUnit: RangeUnit, ranges: immutable.Seq[ByteRange]) extends jm.headers.Range
with RequestHeader {
require(ranges.nonEmpty, "ranges must not be empty") require(ranges.nonEmpty, "ranges must not be empty")
import Range.rangesRenderer import Range.rangesRenderer
def renderValue[R <: Rendering](r: R): r.type = r ~~ rangeUnit ~~ '=' ~~ ranges def renderValue[R <: Rendering](r: R): r.type = r ~~ rangeUnit ~~ '=' ~~ ranges
@ -604,22 +631,33 @@ final case class Range(rangeUnit: RangeUnit, ranges: immutable.Seq[ByteRange]) e
def getRanges: Iterable[jm.headers.ByteRange] = ranges.asJava def getRanges: Iterable[jm.headers.ByteRange] = ranges.asJava
} }
final case class RawHeader(name: String, value: String) extends jm.headers.RawHeader {
def renderInRequests = true
def renderInResponses = true
val lowercaseName = name.toRootLowerCase
def render[R <: Rendering](r: R): r.type = r ~~ name ~~ ':' ~~ ' ' ~~ value
}
object RawHeader {
def unapply[H <: HttpHeader](customHeader: H): Option[(String, String)] =
Some(customHeader.name -> customHeader.value)
}
object `Raw-Request-URI` extends ModeledCompanion[`Raw-Request-URI`] object `Raw-Request-URI` extends ModeledCompanion[`Raw-Request-URI`]
final case class `Raw-Request-URI`(uri: String) extends jm.headers.RawRequestURI with ModeledHeader { final case class `Raw-Request-URI`(uri: String) extends jm.headers.RawRequestURI with SyntheticHeader {
def renderValue[R <: Rendering](r: R): r.type = r ~~ uri def renderValue[R <: Rendering](r: R): r.type = r ~~ uri
protected def companion = `Raw-Request-URI` protected def companion = `Raw-Request-URI`
} }
object `Remote-Address` extends ModeledCompanion[`Remote-Address`] object `Remote-Address` extends ModeledCompanion[`Remote-Address`]
final case class `Remote-Address`(address: RemoteAddress) extends jm.headers.RemoteAddress with ModeledHeader { final case class `Remote-Address`(address: RemoteAddress) extends jm.headers.RemoteAddress with SyntheticHeader {
def renderValue[R <: Rendering](r: R): r.type = r ~~ address def renderValue[R <: Rendering](r: R): r.type = r ~~ address
protected def companion = `Remote-Address` protected def companion = `Remote-Address`
} }
// http://tools.ietf.org/html/rfc7231#section-5.5.2 // http://tools.ietf.org/html/rfc7231#section-5.5.2
object Referer extends ModeledCompanion[Referer] object Referer extends ModeledCompanion[Referer]
final case class Referer(uri: Uri) extends jm.headers.Referer with ModeledHeader { final case class Referer(uri: Uri) extends jm.headers.Referer with RequestHeader {
require(uri.fragment == None, "Referer header URI must not contain a fragment") require(uri.fragment.isEmpty, "Referer header URI must not contain a fragment")
require(uri.authority.userinfo.isEmpty, "Referer header URI must not contain a userinfo component") require(uri.authority.userinfo.isEmpty, "Referer header URI must not contain a userinfo component")
def renderValue[R <: Rendering](r: R): r.type = { import UriRendering.UriRenderer; r ~~ uri } def renderValue[R <: Rendering](r: R): r.type = { import UriRendering.UriRenderer; r ~~ uri }
@ -649,9 +687,8 @@ private[http] object `Sec-WebSocket-Accept` extends ModeledCompanion[`Sec-WebSoc
/** /**
* INTERNAL API * INTERNAL API
*/ */
private[http] final case class `Sec-WebSocket-Accept`(key: String) extends ModeledHeader { private[http] final case class `Sec-WebSocket-Accept`(key: String) extends ResponseHeader {
protected[http] def renderValue[R <: Rendering](r: R): r.type = r ~~ key protected[http] def renderValue[R <: Rendering](r: R): r.type = r ~~ key
protected def companion = `Sec-WebSocket-Accept` protected def companion = `Sec-WebSocket-Accept`
} }
@ -665,11 +702,11 @@ private[http] object `Sec-WebSocket-Extensions` extends ModeledCompanion[`Sec-We
/** /**
* INTERNAL API * INTERNAL API
*/ */
private[http] final case class `Sec-WebSocket-Extensions`(extensions: immutable.Seq[WebsocketExtension]) extends ModeledHeader { private[http] final case class `Sec-WebSocket-Extensions`(extensions: immutable.Seq[WebsocketExtension])
extends ResponseHeader {
require(extensions.nonEmpty, "Sec-WebSocket-Extensions.extensions must not be empty") require(extensions.nonEmpty, "Sec-WebSocket-Extensions.extensions must not be empty")
import `Sec-WebSocket-Extensions`.extensionsRenderer import `Sec-WebSocket-Extensions`.extensionsRenderer
protected[http] def renderValue[R <: Rendering](r: R): r.type = r ~~ extensions protected[http] def renderValue[R <: Rendering](r: R): r.type = r ~~ extensions
protected def companion = `Sec-WebSocket-Extensions` protected def companion = `Sec-WebSocket-Extensions`
} }
@ -686,9 +723,8 @@ private[http] object `Sec-WebSocket-Key` extends ModeledCompanion[`Sec-WebSocket
/** /**
* INTERNAL API * INTERNAL API
*/ */
private[http] final case class `Sec-WebSocket-Key`(key: String) extends ModeledHeader { private[http] final case class `Sec-WebSocket-Key`(key: String) extends RequestHeader {
protected[http] def renderValue[R <: Rendering](r: R): r.type = r ~~ key protected[http] def renderValue[R <: Rendering](r: R): r.type = r ~~ key
protected def companion = `Sec-WebSocket-Key` protected def companion = `Sec-WebSocket-Key`
/** /**
@ -708,11 +744,11 @@ private[http] object `Sec-WebSocket-Protocol` extends ModeledCompanion[`Sec-WebS
/** /**
* INTERNAL API * INTERNAL API
*/ */
private[http] final case class `Sec-WebSocket-Protocol`(protocols: immutable.Seq[String]) extends ModeledHeader { private[http] final case class `Sec-WebSocket-Protocol`(protocols: immutable.Seq[String])
extends RequestResponseHeader {
require(protocols.nonEmpty, "Sec-WebSocket-Protocol.protocols must not be empty") require(protocols.nonEmpty, "Sec-WebSocket-Protocol.protocols must not be empty")
import `Sec-WebSocket-Protocol`.protocolsRenderer import `Sec-WebSocket-Protocol`.protocolsRenderer
protected[http] def renderValue[R <: Rendering](r: R): r.type = r ~~ protocols protected[http] def renderValue[R <: Rendering](r: R): r.type = r ~~ protocols
protected def companion = `Sec-WebSocket-Protocol` protected def companion = `Sec-WebSocket-Protocol`
} }
@ -726,14 +762,13 @@ private[http] object `Sec-WebSocket-Version` extends ModeledCompanion[`Sec-WebSo
/** /**
* INTERNAL API * INTERNAL API
*/ */
private[http] final case class `Sec-WebSocket-Version`(versions: immutable.Seq[Int]) extends ModeledHeader { private[http] final case class `Sec-WebSocket-Version`(versions: immutable.Seq[Int])
extends RequestResponseHeader {
require(versions.nonEmpty, "Sec-WebSocket-Version.versions must not be empty") require(versions.nonEmpty, "Sec-WebSocket-Version.versions must not be empty")
require(versions.forall(v v >= 0 && v <= 255), s"Sec-WebSocket-Version.versions must be in the range 0 <= version <= 255 but were $versions") require(versions.forall(v v >= 0 && v <= 255), s"Sec-WebSocket-Version.versions must be in the range 0 <= version <= 255 but were $versions")
import `Sec-WebSocket-Version`.versionsRenderer import `Sec-WebSocket-Version`.versionsRenderer
protected[http] def renderValue[R <: Rendering](r: R): r.type = r ~~ versions protected[http] def renderValue[R <: Rendering](r: R): r.type = r ~~ versions
def hasVersion(versionNumber: Int): Boolean = versions contains versionNumber
def hasVersion(versionNumber: Int): Boolean = versions.exists(_ == versionNumber)
protected def companion = `Sec-WebSocket-Version` protected def companion = `Sec-WebSocket-Version`
} }
@ -743,7 +778,7 @@ object Server extends ModeledCompanion[Server] {
def apply(first: ProductVersion, more: ProductVersion*): Server = apply(immutable.Seq(first +: more: _*)) def apply(first: ProductVersion, more: ProductVersion*): Server = apply(immutable.Seq(first +: more: _*))
implicit val productsRenderer = Renderer.seqRenderer[ProductVersion](separator = " ") // cache implicit val productsRenderer = Renderer.seqRenderer[ProductVersion](separator = " ") // cache
} }
final case class Server(products: immutable.Seq[ProductVersion]) extends jm.headers.Server with ModeledHeader { final case class Server(products: immutable.Seq[ProductVersion]) extends jm.headers.Server with ResponseHeader {
require(products.nonEmpty, "products must not be empty") require(products.nonEmpty, "products must not be empty")
import Server.productsRenderer import Server.productsRenderer
def renderValue[R <: Rendering](r: R): r.type = r ~~ products def renderValue[R <: Rendering](r: R): r.type = r ~~ products
@ -755,11 +790,18 @@ final case class Server(products: immutable.Seq[ProductVersion]) extends jm.head
// https://tools.ietf.org/html/rfc6265 // https://tools.ietf.org/html/rfc6265
object `Set-Cookie` extends ModeledCompanion[`Set-Cookie`] object `Set-Cookie` extends ModeledCompanion[`Set-Cookie`]
final case class `Set-Cookie`(cookie: HttpCookie) extends jm.headers.SetCookie with ModeledHeader { final case class `Set-Cookie`(cookie: HttpCookie) extends jm.headers.SetCookie with ResponseHeader {
def renderValue[R <: Rendering](r: R): r.type = r ~~ cookie def renderValue[R <: Rendering](r: R): r.type = r ~~ cookie
protected def companion = `Set-Cookie` protected def companion = `Set-Cookie`
} }
object `Timeout-Access` extends ModeledCompanion[`Timeout-Access`]
final case class `Timeout-Access`(timeoutAccess: akka.http.scaladsl.TimeoutAccess)
extends jm.headers.TimeoutAccess with SyntheticHeader {
def renderValue[R <: Rendering](r: R): r.type = r ~~ timeoutAccess.toString
protected def companion = `Timeout-Access`
}
/** /**
* Model for the synthetic `Tls-Session-Info` header which carries the SSLSession of the connection * Model for the synthetic `Tls-Session-Info` header which carries the SSLSession of the connection
* the message carrying this header was received with. * the message carrying this header was received with.
@ -770,17 +812,14 @@ final case class `Set-Cookie`(cookie: HttpCookie) extends jm.headers.SetCookie w
* akka.http.[client|server].parsing.tls-session-info-header = on * akka.http.[client|server].parsing.tls-session-info-header = on
* ``` * ```
*/ */
final case class `Tls-Session-Info`(session: SSLSession) extends jm.headers.TlsSessionInfo with ScalaSessionAPI { object `Tls-Session-Info` extends ModeledCompanion[`Tls-Session-Info`]
override def suppressRendering: Boolean = true final case class `Tls-Session-Info`(session: SSLSession) extends jm.headers.TlsSessionInfo with SyntheticHeader
override def toString = s"SSL-Session-Info($session)" with ScalaSessionAPI {
def name(): String = "SSL-Session-Info" def renderValue[R <: Rendering](r: R): r.type = r ~~ session.toString
def value(): String = "" protected def companion = `Tls-Session-Info`
/** Java API */ /** Java API */
def getSession(): SSLSession = session def getSession: SSLSession = session
def lowercaseName: String = name.toRootLowerCase
def render[R <: Rendering](r: R): r.type = r ~~ name ~~ ':' ~~ ' ' ~~ value
} }
// http://tools.ietf.org/html/rfc7230#section-3.3.1 // http://tools.ietf.org/html/rfc7230#section-3.3.1
@ -788,7 +827,8 @@ object `Transfer-Encoding` extends ModeledCompanion[`Transfer-Encoding`] {
def apply(first: TransferEncoding, more: TransferEncoding*): `Transfer-Encoding` = apply(immutable.Seq(first +: more: _*)) def apply(first: TransferEncoding, more: TransferEncoding*): `Transfer-Encoding` = apply(immutable.Seq(first +: more: _*))
implicit val encodingsRenderer = Renderer.defaultSeqRenderer[TransferEncoding] // cache implicit val encodingsRenderer = Renderer.defaultSeqRenderer[TransferEncoding] // cache
} }
final case class `Transfer-Encoding`(encodings: immutable.Seq[TransferEncoding]) extends jm.headers.TransferEncoding with ModeledHeader { final case class `Transfer-Encoding`(encodings: immutable.Seq[TransferEncoding]) extends jm.headers.TransferEncoding
with RequestResponseHeader {
require(encodings.nonEmpty, "encodings must not be empty") require(encodings.nonEmpty, "encodings must not be empty")
import `Transfer-Encoding`.encodingsRenderer import `Transfer-Encoding`.encodingsRenderer
def isChunked: Boolean = encodings.last == TransferEncodings.chunked def isChunked: Boolean = encodings.last == TransferEncodings.chunked
@ -812,7 +852,7 @@ final case class `Transfer-Encoding`(encodings: immutable.Seq[TransferEncoding])
object Upgrade extends ModeledCompanion[Upgrade] { object Upgrade extends ModeledCompanion[Upgrade] {
implicit val protocolsRenderer = Renderer.defaultSeqRenderer[UpgradeProtocol] implicit val protocolsRenderer = Renderer.defaultSeqRenderer[UpgradeProtocol]
} }
final case class Upgrade(protocols: immutable.Seq[UpgradeProtocol]) extends ModeledHeader { final case class Upgrade(protocols: immutable.Seq[UpgradeProtocol]) extends RequestResponseHeader {
import Upgrade.protocolsRenderer import Upgrade.protocolsRenderer
protected[http] def renderValue[R <: Rendering](r: R): r.type = r ~~ protocols protected[http] def renderValue[R <: Rendering](r: R): r.type = r ~~ protocols
@ -827,7 +867,7 @@ object `User-Agent` extends ModeledCompanion[`User-Agent`] {
def apply(first: ProductVersion, more: ProductVersion*): `User-Agent` = apply(immutable.Seq(first +: more: _*)) def apply(first: ProductVersion, more: ProductVersion*): `User-Agent` = apply(immutable.Seq(first +: more: _*))
implicit val productsRenderer = Renderer.seqRenderer[ProductVersion](separator = " ") // cache implicit val productsRenderer = Renderer.seqRenderer[ProductVersion](separator = " ") // cache
} }
final case class `User-Agent`(products: immutable.Seq[ProductVersion]) extends jm.headers.UserAgent with ModeledHeader { final case class `User-Agent`(products: immutable.Seq[ProductVersion]) extends jm.headers.UserAgent with RequestHeader {
require(products.nonEmpty, "products must not be empty") require(products.nonEmpty, "products must not be empty")
import `User-Agent`.productsRenderer import `User-Agent`.productsRenderer
def renderValue[R <: Rendering](r: R): r.type = r ~~ products def renderValue[R <: Rendering](r: R): r.type = r ~~ products
@ -842,7 +882,8 @@ object `WWW-Authenticate` extends ModeledCompanion[`WWW-Authenticate`] {
def apply(first: HttpChallenge, more: HttpChallenge*): `WWW-Authenticate` = apply(immutable.Seq(first +: more: _*)) def apply(first: HttpChallenge, more: HttpChallenge*): `WWW-Authenticate` = apply(immutable.Seq(first +: more: _*))
implicit val challengesRenderer = Renderer.defaultSeqRenderer[HttpChallenge] // cache implicit val challengesRenderer = Renderer.defaultSeqRenderer[HttpChallenge] // cache
} }
final case class `WWW-Authenticate`(challenges: immutable.Seq[HttpChallenge]) extends jm.headers.WWWAuthenticate with ModeledHeader { final case class `WWW-Authenticate`(challenges: immutable.Seq[HttpChallenge]) extends jm.headers.WWWAuthenticate
with ResponseHeader {
require(challenges.nonEmpty, "challenges must not be empty") require(challenges.nonEmpty, "challenges must not be empty")
import `WWW-Authenticate`.challengesRenderer import `WWW-Authenticate`.challengesRenderer
def renderValue[R <: Rendering](r: R): r.type = r ~~ challenges def renderValue[R <: Rendering](r: R): r.type = r ~~ challenges
@ -858,7 +899,8 @@ object `X-Forwarded-For` extends ModeledCompanion[`X-Forwarded-For`] {
def apply(first: RemoteAddress, more: RemoteAddress*): `X-Forwarded-For` = apply(immutable.Seq(first +: more: _*)) def apply(first: RemoteAddress, more: RemoteAddress*): `X-Forwarded-For` = apply(immutable.Seq(first +: more: _*))
implicit val addressesRenderer = Renderer.defaultSeqRenderer[RemoteAddress] // cache implicit val addressesRenderer = Renderer.defaultSeqRenderer[RemoteAddress] // cache
} }
final case class `X-Forwarded-For`(addresses: immutable.Seq[RemoteAddress]) extends jm.headers.XForwardedFor with ModeledHeader { final case class `X-Forwarded-For`(addresses: immutable.Seq[RemoteAddress]) extends jm.headers.XForwardedFor
with RequestHeader {
require(addresses.nonEmpty, "addresses must not be empty") require(addresses.nonEmpty, "addresses must not be empty")
import `X-Forwarded-For`.addressesRenderer import `X-Forwarded-For`.addressesRenderer
def renderValue[R <: Rendering](r: R): r.type = r ~~ addresses def renderValue[R <: Rendering](r: R): r.type = r ~~ addresses

View file

@ -356,6 +356,8 @@ class ConnectionPoolSpec extends AkkaSpec("""
} }
case class ConnNrHeader(nr: Int) extends CustomHeader { case class ConnNrHeader(nr: Int) extends CustomHeader {
def renderInRequests = false
def renderInResponses = true
def name = "Conn-Nr" def name = "Conn-Nr"
def value = nr.toString def value = nr.toString
} }

View file

@ -108,7 +108,7 @@ class HttpHeaderParserSpec extends WordSpec with Matchers with BeforeAndAfterAll
} }
"retrieve the EmptyHeader" in new TestSetup() { "retrieve the EmptyHeader" in new TestSetup() {
parseAndCache("\r\n")() shouldEqual HttpHeaderParser.EmptyHeader parseAndCache("\r\n")() shouldEqual EmptyHeader
} }
"retrieve a cached header with an exact header name match" in new TestSetup() { "retrieve a cached header with an exact header name match" in new TestSetup() {

View file

@ -66,11 +66,11 @@ class RequestRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll
"POST request, a few headers (incl. a custom Host header) and no body" in new TestSetup() { "POST request, a few headers (incl. a custom Host header) and no body" in new TestSetup() {
HttpRequest(POST, "/abc/xyz", List( HttpRequest(POST, "/abc/xyz", List(
RawHeader("X-Fancy", "naa"), RawHeader("X-Fancy", "naa"),
Age(0), Link(Uri("http://akka.io"), LinkParams.first),
Host("spray.io", 9999))) should renderTo { Host("spray.io", 9999))) should renderTo {
"""POST /abc/xyz HTTP/1.1 """POST /abc/xyz HTTP/1.1
|X-Fancy: naa |X-Fancy: naa
|Age: 0 |Link: <http://akka.io>; rel=first
|Host: spray.io:9999 |Host: spray.io:9999
|User-Agent: akka-http/1.0.0 |User-Agent: akka-http/1.0.0
|Content-Length: 0 |Content-Length: 0
@ -262,8 +262,10 @@ class RequestRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll
} }
} }
"render a CustomHeader header" - { "render a CustomHeader header" - {
"if suppressRendering = false" in new TestSetup(None) { "if renderInRequests = true" in new TestSetup(None) {
case class MyHeader(number: Int) extends CustomHeader { case class MyHeader(number: Int) extends CustomHeader {
def renderInRequests = true
def renderInResponses = false
def name: String = "X-My-Header" def name: String = "X-My-Header"
def value: String = s"No$number" def value: String = s"No$number"
} }
@ -275,10 +277,10 @@ class RequestRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll
|""" |"""
} }
} }
"not if suppressRendering = true" in new TestSetup(None) { "not if renderInRequests = false" in new TestSetup(None) {
case class MyInternalHeader(number: Int) extends CustomHeader { case class MyInternalHeader(number: Int) extends CustomHeader {
override def suppressRendering: Boolean = true def renderInRequests = false
def renderInResponses = false
def name: String = "X-My-Internal-Header" def name: String = "X-My-Internal-Header"
def value: String = s"No$number" def value: String = s"No$number"
} }

View file

@ -414,8 +414,10 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll
} }
"render a CustomHeader header" - { "render a CustomHeader header" - {
"if suppressRendering = false" in new TestSetup(None) { "if renderInResponses = true" in new TestSetup(None) {
case class MyHeader(number: Int) extends CustomHeader { case class MyHeader(number: Int) extends CustomHeader {
def renderInRequests = false
def renderInResponses = true
def name: String = "X-My-Header" def name: String = "X-My-Header"
def value: String = s"No$number" def value: String = s"No$number"
} }
@ -428,10 +430,10 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll
|""" |"""
} }
} }
"not if suppressRendering = true" in new TestSetup(None) { "not if renderInResponses = false" in new TestSetup(None) {
case class MyInternalHeader(number: Int) extends CustomHeader { case class MyInternalHeader(number: Int) extends CustomHeader {
override def suppressRendering: Boolean = true def renderInRequests = false
def renderInResponses = false
def name: String = "X-My-Internal-Header" def name: String = "X-My-Internal-Header"
def value: String = s"No$number" def value: String = s"No$number"
} }

View file

@ -22,7 +22,10 @@ import HttpEntity._
import MediaTypes._ import MediaTypes._
import HttpMethods._ import HttpMethods._
class HttpServerSpec extends AkkaSpec("akka.loggers = []\n akka.loglevel = OFF") with Inside { spec class HttpServerSpec extends AkkaSpec(
"""akka.loggers = []
akka.loglevel = OFF
akka.http.server.request-timeout = infinite""") with Inside { spec
implicit val materializer = ActorMaterializer() implicit val materializer = ActorMaterializer()
"The server implementation" should { "The server implementation" should {
@ -698,6 +701,82 @@ class HttpServerSpec extends AkkaSpec("akka.loggers = []\n akka.loglevel = OFF")
request.headers should contain(`Remote-Address`(RemoteAddress(theAddress, Some(8080)))) request.headers should contain(`Remote-Address`(RemoteAddress(theAddress, Some(8080))))
} }
"support request timeouts" which {
"are defined via the config" in new RequestTimeoutTestSetup(10.millis) {
send("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
expectRequest().header[`Timeout-Access`] shouldBe defined
expectResponseWithWipedDate(
"""HTTP/1.1 503 Service Unavailable
|Server: akka-http/test
|Date: XXXX
|Content-Type: text/plain; charset=UTF-8
|Content-Length: 105
|
|The server was not able to produce a timely response to your request.
|Please try again in a short while!""")
}
"are programmatically increased (not expiring)" in new RequestTimeoutTestSetup(10.millis) {
send("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
expectRequest().header[`Timeout-Access`].foreach(_.timeoutAccess.updateTimeout(50.millis))
netOut.expectNoBytes(30.millis)
responses.sendNext(HttpResponse())
expectResponseWithWipedDate(
"""HTTP/1.1 200 OK
|Server: akka-http/test
|Date: XXXX
|Content-Length: 0
|
|""")
}
"are programmatically increased (expiring)" in new RequestTimeoutTestSetup(10.millis) {
send("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
expectRequest().header[`Timeout-Access`].foreach(_.timeoutAccess.updateTimeout(50.millis))
netOut.expectNoBytes(30.millis)
expectResponseWithWipedDate(
"""HTTP/1.1 503 Service Unavailable
|Server: akka-http/test
|Date: XXXX
|Content-Type: text/plain; charset=UTF-8
|Content-Length: 105
|
|The server was not able to produce a timely response to your request.
|Please try again in a short while!""")
}
"are programmatically decreased" in new RequestTimeoutTestSetup(50.millis) {
send("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
expectRequest().header[`Timeout-Access`].foreach(_.timeoutAccess.updateTimeout(10.millis))
val mark = System.nanoTime()
expectResponseWithWipedDate(
"""HTTP/1.1 503 Service Unavailable
|Server: akka-http/test
|Date: XXXX
|Content-Type: text/plain; charset=UTF-8
|Content-Length: 105
|
|The server was not able to produce a timely response to your request.
|Please try again in a short while!""")
(System.nanoTime() - mark) should be < (40 * 1000000L)
}
"have a programmatically set timeout handler" in new RequestTimeoutTestSetup(10.millis) {
send("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
val timeoutResponse = HttpResponse(StatusCodes.InternalServerError, entity = "OOPS!")
expectRequest().header[`Timeout-Access`].foreach(_.timeoutAccess.updateHandler(_ timeoutResponse))
expectResponseWithWipedDate(
"""HTTP/1.1 500 Internal Server Error
|Server: akka-http/test
|Date: XXXX
|Content-Type: text/plain; charset=UTF-8
|Content-Length: 5
|
|OOPS!""")
}
}
"add `Connection: close` to early responses" in new TestSetup { "add `Connection: close` to early responses" in new TestSetup {
send("""POST / HTTP/1.1 send("""POST / HTTP/1.1
|Host: example.com |Host: example.com
@ -723,8 +802,7 @@ class HttpServerSpec extends AkkaSpec("akka.loggers = []\n akka.loglevel = OFF")
netOut.expectComplete() netOut.expectComplete()
} }
def isDefinedVia = afterWord("is defined via") "support request length verification" which afterWord("is defined via") {
"support request length verification" which isDefinedVia {
class LengthVerificationTest(maxContentLength: Int) extends TestSetup(maxContentLength) { class LengthVerificationTest(maxContentLength: Int) extends TestSetup(maxContentLength) {
val entityBase = "0123456789ABCD" val entityBase = "0123456789ABCD"
@ -912,4 +990,10 @@ class HttpServerSpec extends AkkaSpec("akka.loggers = []\n akka.loglevel = OFF")
else s.copy(parserSettings = s.parserSettings.copy(maxContentLength = maxContentLength)) else s.copy(parserSettings = s.parserSettings.copy(maxContentLength = maxContentLength))
} }
} }
class RequestTimeoutTestSetup(requestTimeout: Duration) extends TestSetup {
override def settings = {
val s = super.settings
s.copy(timeouts = s.timeouts.copy(requestTimeout = requestTimeout))
}
}
} }

View file

@ -35,7 +35,7 @@ class ClientServerSpec extends WordSpec with Matchers with BeforeAndAfterAll wit
akka.stdout-loglevel = ERROR akka.stdout-loglevel = ERROR
windows-connection-abort-workaround-enabled = auto windows-connection-abort-workaround-enabled = auto
akka.log-dead-letters = OFF akka.log-dead-letters = OFF
""") akka.http.server.request-timeout = infinite""")
implicit val system = ActorSystem(getClass.getSimpleName, testConf) implicit val system = ActorSystem(getClass.getSimpleName, testConf)
import system.dispatcher import system.dispatcher
implicit val materializer = ActorMaterializer() implicit val materializer = ActorMaterializer()

View file

@ -14,22 +14,30 @@ object ModeledCustomHeaderSpec {
//#modeled-api-key-custom-header //#modeled-api-key-custom-header
object ApiTokenHeader extends ModeledCustomHeaderCompanion[ApiTokenHeader] { object ApiTokenHeader extends ModeledCustomHeaderCompanion[ApiTokenHeader] {
def renderInRequests = false
def renderInResponses = false
override val name = "apiKey" override val name = "apiKey"
override def parse(value: String) = Try(new ApiTokenHeader(value)) override def parse(value: String) = Try(new ApiTokenHeader(value))
} }
final class ApiTokenHeader(token: String) extends ModeledCustomHeader[ApiTokenHeader] { final class ApiTokenHeader(token: String) extends ModeledCustomHeader[ApiTokenHeader] {
def renderInRequests = false
def renderInResponses = false
override val companion = ApiTokenHeader override val companion = ApiTokenHeader
override def value: String = token override def value: String = token
} }
//#modeled-api-key-custom-header //#modeled-api-key-custom-header
object DifferentHeader extends ModeledCustomHeaderCompanion[DifferentHeader] { object DifferentHeader extends ModeledCustomHeaderCompanion[DifferentHeader] {
def renderInRequests = false
def renderInResponses = false
override val name = "different" override val name = "different"
override def parse(value: String) = override def parse(value: String) =
if (value contains " ") Failure(new Exception("Contains illegal whitespace!")) if (value contains " ") Failure(new Exception("Contains illegal whitespace!"))
else Success(new DifferentHeader(value)) else Success(new DifferentHeader(value))
} }
final class DifferentHeader(token: String) extends ModeledCustomHeader[DifferentHeader] { final class DifferentHeader(token: String) extends ModeledCustomHeader[DifferentHeader] {
def renderInRequests = false
def renderInResponses = false
override val companion = DifferentHeader override val companion = DifferentHeader
override def value = token override def value = token
} }

View file

@ -47,8 +47,8 @@ private[http] object ExtractionMap {
def addAll(values: Map[RequestVal[_], Any]): ExtractionMap = def addAll(values: Map[RequestVal[_], Any]): ExtractionMap =
ExtractionMap(map ++ values) ExtractionMap(map ++ values)
// CustomHeader methods def renderInRequests = false
override def suppressRendering: Boolean = true def renderInResponses = false
def name(): String = "ExtractedValues" def name(): String = "ExtractedValues"
def value(): String = "<empty>" def value(): String = "<empty>"
} }

View file

@ -243,8 +243,8 @@ private[akka] final case class Fold[In, Out](zero: Out, f: (Out, In) ⇒ Out, de
*/ */
final case class Intersperse[T](start: Option[T], inject: T, end: Option[T]) extends GraphStage[FlowShape[T, T]] { final case class Intersperse[T](start: Option[T], inject: T, end: Option[T]) extends GraphStage[FlowShape[T, T]] {
ReactiveStreamsCompliance.requireNonNullElement(inject) ReactiveStreamsCompliance.requireNonNullElement(inject)
if(start.isDefined) ReactiveStreamsCompliance.requireNonNullElement(start.get) if (start.isDefined) ReactiveStreamsCompliance.requireNonNullElement(start.get)
if(end.isDefined) ReactiveStreamsCompliance.requireNonNullElement(end.get) if (end.isDefined) ReactiveStreamsCompliance.requireNonNullElement(end.get)
private val in = Inlet[T]("in") private val in = Inlet[T]("in")
private val out = Outlet[T]("out") private val out = Outlet[T]("out")

View file

@ -5,10 +5,17 @@ package akka.stream.javadsl
import akka.japi.function import akka.japi.function
import akka.stream._ import akka.stream._
import akka.stream.scaladsl.Flow
import scala.concurrent.duration.FiniteDuration import scala.concurrent.duration.FiniteDuration
object BidiFlow { object BidiFlow {
private[this] val _identity: BidiFlow[Object, Object, Object, Object, Unit] =
BidiFlow.fromFlows(Flow.of(classOf[Object]), Flow.of(classOf[Object]))
def identity[A, B]: BidiFlow[A, A, B, B, Unit] = _identity.asInstanceOf[BidiFlow[A, A, B, B, Unit]]
/** /**
* A graph with the shape of a BidiFlow logically is a BidiFlow, this method makes * A graph with the shape of a BidiFlow logically is a BidiFlow, this method makes
* it so also in type. * it so also in type.

View file

@ -155,6 +155,11 @@ final class BidiFlow[-I1, +O1, -I2, +O2, +Mat](private[stream] override val modu
} }
object BidiFlow { object BidiFlow {
private[this] val _identity: BidiFlow[Any, Any, Any, Any, Unit] =
BidiFlow.fromFlows(Flow[Any], Flow[Any])
def identity[A, B]: BidiFlow[A, A, B, B, Unit] = _identity.asInstanceOf[BidiFlow[A, A, B, B, Unit]]
/** /**
* A graph with the shape of a flow logically is a flow, this method makes * A graph with the shape of a flow logically is a flow, this method makes
* it so also in type. * it so also in type.

View file

@ -7,7 +7,7 @@ import akka.stream._
import akka.stream.impl._ import akka.stream.impl._
import akka.stream.impl.fusing.GraphStages import akka.stream.impl.fusing.GraphStages
import akka.stream.impl.fusing.GraphStages.MaterializedValueSource import akka.stream.impl.fusing.GraphStages.MaterializedValueSource
import akka.stream.impl.Stages.{DefaultAttributes, StageModule, SymbolicStage} import akka.stream.impl.Stages.{ DefaultAttributes, StageModule, SymbolicStage }
import akka.stream.impl.StreamLayout._ import akka.stream.impl.StreamLayout._
import akka.stream.stage.{ OutHandler, InHandler, GraphStageLogic, GraphStage } import akka.stream.stage.{ OutHandler, InHandler, GraphStageLogic, GraphStage }
import scala.annotation.unchecked.uncheckedVariance import scala.annotation.unchecked.uncheckedVariance