diff --git a/akka-http-core/src/main/java/akka/http/javadsl/TimeoutAccess.java b/akka-http-core/src/main/java/akka/http/javadsl/TimeoutAccess.java new file mode 100644 index 0000000000..fe182a4019 --- /dev/null +++ b/akka-http-core/src/main/java/akka/http/javadsl/TimeoutAccess.java @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +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 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 handler); +} diff --git a/akka-http-core/src/main/java/akka/http/javadsl/model/HttpHeader.java b/akka-http-core/src/main/java/akka/http/javadsl/model/HttpHeader.java index 666dec3c87..b22043f312 100644 --- a/akka-http-core/src/main/java/akka/http/javadsl/model/HttpHeader.java +++ b/akka-http-core/src/main/java/akka/http/javadsl/model/HttpHeader.java @@ -34,4 +34,14 @@ public abstract class HttpHeader { * Returns !is(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(); } diff --git a/akka-http-core/src/main/java/akka/http/javadsl/model/headers/CustomHeader.java b/akka-http-core/src/main/java/akka/http/javadsl/model/headers/CustomHeader.java index 7313313a2e..f693b5a7bc 100644 --- a/akka-http-core/src/main/java/akka/http/javadsl/model/headers/CustomHeader.java +++ b/akka-http-core/src/main/java/akka/http/javadsl/model/headers/CustomHeader.java @@ -7,6 +7,4 @@ package akka.http.javadsl.model.headers; public abstract class CustomHeader extends akka.http.scaladsl.model.HttpHeader { public abstract String name(); public abstract String value(); - - protected abstract boolean suppressRendering(); } diff --git a/akka-http-core/src/main/java/akka/http/javadsl/model/headers/TimeoutAccess.java b/akka-http-core/src/main/java/akka/http/javadsl/model/headers/TimeoutAccess.java new file mode 100644 index 0000000000..eb0e9de928 --- /dev/null +++ b/akka-http-core/src/main/java/akka/http/javadsl/model/headers/TimeoutAccess.java @@ -0,0 +1,16 @@ +/** + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +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); + } +} diff --git a/akka-http-core/src/main/resources/reference.conf b/akka-http-core/src/main/resources/reference.conf index 55da20ba6e..6eb274c636 100644 --- a/akka-http-core/src/main/resources/reference.conf +++ b/akka-http-core/src/main/resources/reference.conf @@ -18,6 +18,18 @@ akka.http { # Set to `infinite` to completely disable idle connection timeouts. 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. # Set to `infinite` to disable. bind-timeout = 1s @@ -273,13 +285,16 @@ akka.http { max-chunk-ext-length = 256 max-chunk-size = 1m - # Maximum content length which should not be exceeded by incoming HttpRequests. - # For file uploads which use the entityBytes Source of an incoming HttpRequest it is safe to - # set this to a very high value (or to `infinite` if feeling very adventurous) as the streaming - # upload will be back-pressured properly by Akka Streams. - # Please note however that this setting is a global property, and is applied to all incoming requests, - # not only file uploads consumed in a streaming fashion, so pick this limit wisely. - max-content-length = 8m + # Default maximum content length which should not be exceeded by incoming request entities. + # Can be changed at runtime (to a higher or lower value) via the `HttpEntity::withSizeLimit` method. + # Note that it is not necessarily a problem to set this to a high value as all stream operations + # are always properly backpressured. + # Nevertheless you might want to apply some limit in order to prevent a single client from consuming + # an excessive amount of server resources. + # + # 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. # The following values are defined: diff --git a/akka-http-core/src/main/scala/akka/http/ServerSettings.scala b/akka-http-core/src/main/scala/akka/http/ServerSettings.scala index fd2a887b4d..7bde5f2c89 100644 --- a/akka-http-core/src/main/scala/akka/http/ServerSettings.scala +++ b/akka-http-core/src/main/scala/akka/http/ServerSettings.scala @@ -46,7 +46,10 @@ final case class ServerSettings( object ServerSettings extends SettingsCompanion[ServerSettings]("akka.http.server") { final case class Timeouts(idleTimeout: Duration, + requestTimeout: Duration, 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") } 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(_)), Timeouts( c getPotentiallyInfiniteDuration "idle-timeout", + c getPotentiallyInfiniteDuration "request-timeout", c getFiniteDuration "bind-timeout"), c getInt "max-connections", c getInt "pipelining-limit", diff --git a/akka-http-core/src/main/scala/akka/http/impl/engine/client/PoolSlot.scala b/akka-http-core/src/main/scala/akka/http/impl/engine/client/PoolSlot.scala index 1e6ee2ce25..6c06b1abb0 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/engine/client/PoolSlot.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/engine/client/PoolSlot.scala @@ -163,18 +163,7 @@ private object PoolSlot { case FromConnection(OnNext(response: HttpResponse)) ⇒ val requestContext = inflightRequests.head inflightRequests = inflightRequests.tail - val (entity, whenCompleted) = response.entity match { - 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 (entity, whenCompleted) = HttpEntity.captureTermination(response.entity) val delivery = ResponseDelivery(ResponseContext(requestContext, Success(response withEntity entity))) import fm.executionContext val requestCompleted = SlotEvent.RequestCompletedFuture(whenCompleted.map(_ ⇒ SlotEvent.RequestCompleted(slotIx))) diff --git a/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/BodyPartParser.scala b/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/BodyPartParser.scala index 13b2f4fa11..06e208b54e 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/BodyPartParser.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/BodyPartParser.scala @@ -146,7 +146,7 @@ private[http] final class BodyPartParser(defaultContentType: ContentType, else if (doubleDash(input, ix)) terminate() 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` ⇒ 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 ++ "'()+_,-./:=? " private object BoundaryHeader extends HttpHeader { + def renderInRequests = false + def renderInResponses = false def name = "" def lowercaseName = "" def value = "" diff --git a/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/HttpHeaderParser.scala b/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/HttpHeaderParser.scala index 010519f582..ceb5865e38 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/HttpHeaderParser.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/HttpHeaderParser.scala @@ -11,8 +11,8 @@ import scala.annotation.tailrec import akka.parboiled2.CharUtils import akka.util.ByteString import akka.http.impl.util._ -import akka.http.scaladsl.model.{ IllegalHeaderException, StatusCodes, HttpHeader, ErrorInfo, Uri } -import akka.http.scaladsl.model.headers.RawHeader +import akka.http.scaladsl.model.{ IllegalHeaderException, StatusCodes, HttpHeader, ErrorInfo } +import akka.http.scaladsl.model.headers.{ EmptyHeader, RawHeader } import akka.http.impl.model.parser.HeaderParser import akka.http.impl.model.parser.CharacterClasses._ @@ -414,14 +414,6 @@ private[http] object HttpHeaderParser { 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( "Accept: *", "Accept: */*", diff --git a/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/HttpMessageParser.scala b/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/HttpMessageParser.scala index 227aeb5051..a160b5ddd4 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/HttpMessageParser.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/HttpMessageParser.scala @@ -136,7 +136,7 @@ private[http] abstract class HttpMessageParser[Output >: MessageOutput <: Parser resultHeader match { 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) setCompletionHandling(CompletionIsEntityStreamError) 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 } if (errorInfo eq null) { headerParser.resultHeader match { - case HttpHeaderParser.EmptyHeader ⇒ + case EmptyHeader ⇒ val lastChunk = if (extension.isEmpty && headers.isEmpty) HttpEntity.LastChunk else HttpEntity.LastChunk(extension, headers) emit(EntityChunk(lastChunk)) diff --git a/akka-http-core/src/main/scala/akka/http/impl/engine/rendering/HttpRequestRendererFactory.scala b/akka-http-core/src/main/scala/akka/http/impl/engine/rendering/HttpRequestRendererFactory.scala index e7a91775f2..5b9e3c0043 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/engine/rendering/HttpRequestRendererFactory.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/engine/rendering/HttpRequestRendererFactory.scala @@ -78,8 +78,8 @@ private[http] class HttpRequestRendererFactory(userAgentHeader: Option[headers.` case x: `Raw-Request-URI` ⇒ // we never render this header renderHeaders(tail, hostHeaderSeen, userAgentSeen, transferEncodingSeen) - case x: CustomHeader ⇒ - if (!x.suppressRendering) render(x) + case x: CustomHeader if x.renderInRequests ⇒ + render(x) renderHeaders(tail, hostHeaderSeen, userAgentSeen, transferEncodingSeen) 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) 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) } diff --git a/akka-http-core/src/main/scala/akka/http/impl/engine/rendering/HttpResponseRendererFactory.scala b/akka-http-core/src/main/scala/akka/http/impl/engine/rendering/HttpResponseRendererFactory.scala index 693bd18931..586b64b836 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/engine/rendering/HttpResponseRendererFactory.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/engine/rendering/HttpResponseRendererFactory.scala @@ -161,8 +161,8 @@ private[http] class HttpResponseRendererFactory(serverHeader: Option[headers.Ser render(x) renderHeaders(tail, alwaysClose, connHeader, serverSeen = true, transferEncodingSeen, dateSeen) - case x: CustomHeader ⇒ - if (!x.suppressRendering) render(x) + case x: CustomHeader if x.renderInResponses ⇒ + render(x) renderHeaders(tail, alwaysClose, connHeader, serverSeen, transferEncodingSeen, dateSeen) 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) 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) } diff --git a/akka-http-core/src/main/scala/akka/http/impl/engine/server/HttpServerBluePrint.scala b/akka-http-core/src/main/scala/akka/http/impl/engine/server/HttpServerBluePrint.scala index 970e0db865..5b5a3d293e 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/engine/server/HttpServerBluePrint.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/engine/server/HttpServerBluePrint.scala @@ -5,12 +5,19 @@ package akka.http.impl.engine.server import java.net.InetSocketAddress -import java.util.Random -import akka.stream.impl.fusing.GraphInterpreter +import java.util.concurrent.atomic.AtomicReference +import scala.concurrent.{ Promise, Future } +import scala.concurrent.duration.{ Deadline, FiniteDuration, Duration } import scala.collection.immutable -import org.reactivestreams.{ Publisher, Subscriber } import scala.util.control.NonFatal +import akka.actor.Cancellable +import akka.japi.Function 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.impl.engine.HttpConnectionTimeoutException 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.ws._ 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.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.stream.impl.fusing.SubSource /** * INTERNAL API @@ -54,6 +57,7 @@ private[http] object HttpServerBluePrint { def apply(settings: ServerSettings, remoteAddress: Option[InetSocketAddress], log: LoggingAdapter): Http.ServerLayer = { val theStack = userHandlerGuard(settings.pipeliningLimit) atop + requestTimeoutSupport(settings.timeouts.requestTimeout) atop requestPreparation(settings) atop controller(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] = 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]] { val in = Inlet[RequestOutput]("RequestStartThenRunIgnore.in") val out = Outlet[HttpRequest]("RequestStartThenRunIgnore.out") @@ -97,6 +107,7 @@ private[http] object HttpServerBluePrint { val entity = createEntity(entityCreator) withSizeLimit settings.parserSettings.maxContentLength push(out, HttpRequest(effectiveMethod, uri, effectiveHeaders, entity, protocol)) + case _ ⇒ throw new IllegalStateException } } setHandler(in, idle) @@ -173,6 +184,104 @@ private[http] object HttpServerBluePrint { .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) extends GraphStage[BidiShape[RequestOutput, RequestOutput, HttpResponse, ResponseRenderingContext]] { private val requestParsingIn = Inlet[RequestOutput]("requestParsingIn") diff --git a/akka-http-core/src/main/scala/akka/http/impl/engine/ws/UpgradeToWebsocketsResponseHeader.scala b/akka-http-core/src/main/scala/akka/http/impl/engine/ws/UpgradeToWebsocketsResponseHeader.scala index f1db3dcada..e736afaae2 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/engine/ws/UpgradeToWebsocketsResponseHeader.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/engine/ws/UpgradeToWebsocketsResponseHeader.scala @@ -12,7 +12,7 @@ private[http] final case class UpgradeToWebsocketResponseHeader(handler: Either[ extends InternalCustomHeader("UpgradeToWebsocketResponseHeader") private[http] abstract class InternalCustomHeader(val name: String) extends CustomHeader { - override def suppressRendering: Boolean = true - - def value(): String = "" + final def renderInRequests = false + final def renderInResponses = false + def value: String = "" } diff --git a/akka-http-core/src/main/scala/akka/http/scaladsl/TimeoutAccess.scala b/akka-http-core/src/main/scala/akka/http/scaladsl/TimeoutAccess.scala new file mode 100644 index 0000000000..776ce60260 --- /dev/null +++ b/akka-http-core/src/main/scala/akka/http/scaladsl/TimeoutAccess.scala @@ -0,0 +1,42 @@ +/** + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +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 +} diff --git a/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpEntity.scala b/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpEntity.scala index 161292239c..2c9b87913a 100755 --- a/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpEntity.scala +++ b/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpEntity.scala @@ -21,6 +21,7 @@ import akka.{ japi, stream } import akka.http.scaladsl.model.ContentType.{ NonBinary, Binary } import akka.http.scaladsl.util.FastFuture import akka.http.javadsl.{ model ⇒ jm } +import akka.http.impl.util.StreamUtils import akka.http.impl.util.JavaMapping.Implicits._ import scala.compat.java8.OptionConverters._ @@ -503,4 +504,24 @@ object HttpEntity { private object SizeLimit { 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 + } } diff --git a/akka-http-core/src/main/scala/akka/http/scaladsl/model/headers/headers.scala b/akka-http-core/src/main/scala/akka/http/scaladsl/model/headers/headers.scala index 99e2d92e2b..121e8c27ed 100644 --- a/akka-http-core/src/main/scala/akka/http/scaladsl/model/headers/headers.scala +++ b/akka-http-core/src/main/scala/akka/http/scaladsl/model/headers/headers.scala @@ -9,16 +9,12 @@ import java.net.InetSocketAddress import java.security.MessageDigest import java.util import javax.net.ssl.SSLSession - -import akka.stream.io.ScalaSessionAPI - import scala.reflect.ClassTag import scala.util.{ Failure, Success, Try } import scala.annotation.tailrec import scala.collection.immutable - import akka.parboiled2.util.Base64 - +import akka.stream.io.ScalaSessionAPI import akka.http.impl.util._ import akka.http.javadsl.{ model ⇒ jm } import akka.http.scaladsl.model._ @@ -41,6 +37,8 @@ sealed abstract class ModeledCompanion[T: ClassTag] extends Renderable { } 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 value: String = renderValue(new StringRendering).get def lowercaseName: String = companion.lowercaseName @@ -49,6 +47,11 @@ sealed trait ModeledHeader extends HttpHeader with Serializable { 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`. * @@ -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. */ 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 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] 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._ +// 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 object Connection extends ModeledCompanion[Connection] { def apply(first: String, more: String*): Connection = apply(immutable.Seq(first +: more: _*)) 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") import Connection.tokensRenderer 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 * 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 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 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 { - def renderValue[R <: Rendering](r: R): r.type = { r ~~ dispositionType; params foreach { case (k, v) ⇒ r ~~ "; " ~~ k ~~ '=' ~~# v }; r } +final case class `Content-Disposition`(dispositionType: ContentDispositionType, params: Map[String, String] = Map.empty) + 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` /** 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: _*)) 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") import `Content-Encoding`.encodingsRenderer 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`] { 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 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 * 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 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) 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") import Cookie.cookiePairsRenderer 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 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) 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 object ETag extends ModeledCompanion[ETag] { 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 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 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) 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 object `If-Match` extends ModeledCompanion[`If-Match`] { val `*` = `If-Match`(EntityTagRange.`*`) def apply(first: EntityTag, more: EntityTag*): `If-Match` = `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 protected def companion = `If-Match` } // http://tools.ietf.org/html/rfc7232#section-3.3 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) 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` = `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 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 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) protected def companion = `If-Unmodified-Since` } // http://tools.ietf.org/html/rfc7232#section-2.2 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) protected def companion = `Last-Modified` } @@ -535,7 +559,7 @@ object Link extends ModeledCompanion[Link] { def apply(values: LinkValue*): Link = apply(immutable.Seq(values: _*)) 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 def renderValue[R <: Rendering](r: R): r.type = r ~~ values 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 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 } 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] { 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 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: _*)) 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") import `Proxy-Authenticate`.challengesRenderer 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 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 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) 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") import Range.rangesRenderer 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 } +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`] -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 protected def companion = `Raw-Request-URI` } 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 protected def companion = `Remote-Address` } // http://tools.ietf.org/html/rfc7231#section-5.5.2 object Referer extends ModeledCompanion[Referer] -final case class Referer(uri: Uri) extends jm.headers.Referer with ModeledHeader { - require(uri.fragment == None, "Referer header URI must not contain a fragment") +final case class Referer(uri: Uri) extends jm.headers.Referer with RequestHeader { + 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") 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 */ -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 def companion = `Sec-WebSocket-Accept` } @@ -665,11 +702,11 @@ private[http] object `Sec-WebSocket-Extensions` extends ModeledCompanion[`Sec-We /** * 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") import `Sec-WebSocket-Extensions`.extensionsRenderer protected[http] def renderValue[R <: Rendering](r: R): r.type = r ~~ extensions - protected def companion = `Sec-WebSocket-Extensions` } @@ -686,9 +723,8 @@ private[http] object `Sec-WebSocket-Key` extends ModeledCompanion[`Sec-WebSocket /** * 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 def companion = `Sec-WebSocket-Key` /** @@ -708,11 +744,11 @@ private[http] object `Sec-WebSocket-Protocol` extends ModeledCompanion[`Sec-WebS /** * 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") import `Sec-WebSocket-Protocol`.protocolsRenderer protected[http] def renderValue[R <: Rendering](r: R): r.type = r ~~ protocols - protected def companion = `Sec-WebSocket-Protocol` } @@ -726,14 +762,13 @@ private[http] object `Sec-WebSocket-Version` extends ModeledCompanion[`Sec-WebSo /** * 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.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 protected[http] def renderValue[R <: Rendering](r: R): r.type = r ~~ versions - - def hasVersion(versionNumber: Int): Boolean = versions.exists(_ == versionNumber) - + def hasVersion(versionNumber: Int): Boolean = versions contains versionNumber 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: _*)) 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") import Server.productsRenderer 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 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 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 * 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 * ``` */ -final case class `Tls-Session-Info`(session: SSLSession) extends jm.headers.TlsSessionInfo with ScalaSessionAPI { - override def suppressRendering: Boolean = true - override def toString = s"SSL-Session-Info($session)" - def name(): String = "SSL-Session-Info" - def value(): String = "" +object `Tls-Session-Info` extends ModeledCompanion[`Tls-Session-Info`] +final case class `Tls-Session-Info`(session: SSLSession) extends jm.headers.TlsSessionInfo with SyntheticHeader + with ScalaSessionAPI { + def renderValue[R <: Rendering](r: R): r.type = r ~~ session.toString + protected def companion = `Tls-Session-Info` /** Java API */ - def getSession(): SSLSession = session - - def lowercaseName: String = name.toRootLowerCase - def render[R <: Rendering](r: R): r.type = r ~~ name ~~ ':' ~~ ' ' ~~ value + def getSession: SSLSession = session } // 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: _*)) 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") import `Transfer-Encoding`.encodingsRenderer 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] { 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 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: _*)) 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") import `User-Agent`.productsRenderer 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: _*)) 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") import `WWW-Authenticate`.challengesRenderer 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: _*)) 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") import `X-Forwarded-For`.addressesRenderer def renderValue[R <: Rendering](r: R): r.type = r ~~ addresses diff --git a/akka-http-core/src/test/scala/akka/http/impl/engine/client/ConnectionPoolSpec.scala b/akka-http-core/src/test/scala/akka/http/impl/engine/client/ConnectionPoolSpec.scala index ff2a9b3961..da73e5b07a 100644 --- a/akka-http-core/src/test/scala/akka/http/impl/engine/client/ConnectionPoolSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/impl/engine/client/ConnectionPoolSpec.scala @@ -356,6 +356,8 @@ class ConnectionPoolSpec extends AkkaSpec(""" } case class ConnNrHeader(nr: Int) extends CustomHeader { + def renderInRequests = false + def renderInResponses = true def name = "Conn-Nr" def value = nr.toString } diff --git a/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/HttpHeaderParserSpec.scala b/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/HttpHeaderParserSpec.scala index e0e631c5c7..6e11c02852 100644 --- a/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/HttpHeaderParserSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/HttpHeaderParserSpec.scala @@ -108,7 +108,7 @@ class HttpHeaderParserSpec extends WordSpec with Matchers with BeforeAndAfterAll } "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() { diff --git a/akka-http-core/src/test/scala/akka/http/impl/engine/rendering/RequestRendererSpec.scala b/akka-http-core/src/test/scala/akka/http/impl/engine/rendering/RequestRendererSpec.scala index e3391378ae..06cc0f6c66 100644 --- a/akka-http-core/src/test/scala/akka/http/impl/engine/rendering/RequestRendererSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/impl/engine/rendering/RequestRendererSpec.scala @@ -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() { HttpRequest(POST, "/abc/xyz", List( RawHeader("X-Fancy", "naa"), - Age(0), + Link(Uri("http://akka.io"), LinkParams.first), Host("spray.io", 9999))) should renderTo { """POST /abc/xyz HTTP/1.1 |X-Fancy: naa - |Age: 0 + |Link: ; rel=first |Host: spray.io:9999 |User-Agent: akka-http/1.0.0 |Content-Length: 0 @@ -262,8 +262,10 @@ class RequestRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll } } "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 { + def renderInRequests = true + def renderInResponses = false def name: String = "X-My-Header" 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 { - override def suppressRendering: Boolean = true - + def renderInRequests = false + def renderInResponses = false def name: String = "X-My-Internal-Header" def value: String = s"No$number" } diff --git a/akka-http-core/src/test/scala/akka/http/impl/engine/rendering/ResponseRendererSpec.scala b/akka-http-core/src/test/scala/akka/http/impl/engine/rendering/ResponseRendererSpec.scala index 9ba309c58e..41c6aac340 100644 --- a/akka-http-core/src/test/scala/akka/http/impl/engine/rendering/ResponseRendererSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/impl/engine/rendering/ResponseRendererSpec.scala @@ -414,8 +414,10 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll } "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 { + def renderInRequests = false + def renderInResponses = true def name: String = "X-My-Header" 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 { - override def suppressRendering: Boolean = true - + def renderInRequests = false + def renderInResponses = false def name: String = "X-My-Internal-Header" def value: String = s"No$number" } diff --git a/akka-http-core/src/test/scala/akka/http/impl/engine/server/HttpServerSpec.scala b/akka-http-core/src/test/scala/akka/http/impl/engine/server/HttpServerSpec.scala index 007a5469a0..b580a5b000 100644 --- a/akka-http-core/src/test/scala/akka/http/impl/engine/server/HttpServerSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/impl/engine/server/HttpServerSpec.scala @@ -22,7 +22,10 @@ import HttpEntity._ import MediaTypes._ 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() "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)))) } + "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 { send("""POST / HTTP/1.1 |Host: example.com @@ -723,8 +802,7 @@ class HttpServerSpec extends AkkaSpec("akka.loggers = []\n akka.loglevel = OFF") netOut.expectComplete() } - def isDefinedVia = afterWord("is defined via") - "support request length verification" which isDefinedVia { + "support request length verification" which afterWord("is defined via") { class LengthVerificationTest(maxContentLength: Int) extends TestSetup(maxContentLength) { 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)) } } + class RequestTimeoutTestSetup(requestTimeout: Duration) extends TestSetup { + override def settings = { + val s = super.settings + s.copy(timeouts = s.timeouts.copy(requestTimeout = requestTimeout)) + } + } } diff --git a/akka-http-core/src/test/scala/akka/http/scaladsl/ClientServerSpec.scala b/akka-http-core/src/test/scala/akka/http/scaladsl/ClientServerSpec.scala index 3d45b3ac21..c7b412c4e7 100644 --- a/akka-http-core/src/test/scala/akka/http/scaladsl/ClientServerSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/scaladsl/ClientServerSpec.scala @@ -35,7 +35,7 @@ class ClientServerSpec extends WordSpec with Matchers with BeforeAndAfterAll wit akka.stdout-loglevel = ERROR windows-connection-abort-workaround-enabled = auto akka.log-dead-letters = OFF - """) + akka.http.server.request-timeout = infinite""") implicit val system = ActorSystem(getClass.getSimpleName, testConf) import system.dispatcher implicit val materializer = ActorMaterializer() diff --git a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/ModeledCustomHeaderSpec.scala b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/ModeledCustomHeaderSpec.scala index addf17f5e5..b0e09aa644 100644 --- a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/ModeledCustomHeaderSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/ModeledCustomHeaderSpec.scala @@ -14,22 +14,30 @@ object ModeledCustomHeaderSpec { //#modeled-api-key-custom-header object ApiTokenHeader extends ModeledCustomHeaderCompanion[ApiTokenHeader] { + def renderInRequests = false + def renderInResponses = false override val name = "apiKey" override def parse(value: String) = Try(new ApiTokenHeader(value)) } final class ApiTokenHeader(token: String) extends ModeledCustomHeader[ApiTokenHeader] { + def renderInRequests = false + def renderInResponses = false override val companion = ApiTokenHeader override def value: String = token } //#modeled-api-key-custom-header object DifferentHeader extends ModeledCustomHeaderCompanion[DifferentHeader] { + def renderInRequests = false + def renderInResponses = false override val name = "different" override def parse(value: String) = if (value contains " ") Failure(new Exception("Contains illegal whitespace!")) else Success(new DifferentHeader(value)) } final class DifferentHeader(token: String) extends ModeledCustomHeader[DifferentHeader] { + def renderInRequests = false + def renderInResponses = false override val companion = DifferentHeader override def value = token } diff --git a/akka-http/src/main/scala/akka/http/impl/server/RouteImplementation.scala b/akka-http/src/main/scala/akka/http/impl/server/RouteImplementation.scala index 68fe36a603..8ecbc91bbd 100644 --- a/akka-http/src/main/scala/akka/http/impl/server/RouteImplementation.scala +++ b/akka-http/src/main/scala/akka/http/impl/server/RouteImplementation.scala @@ -47,8 +47,8 @@ private[http] object ExtractionMap { def addAll(values: Map[RequestVal[_], Any]): ExtractionMap = ExtractionMap(map ++ values) - // CustomHeader methods - override def suppressRendering: Boolean = true + def renderInRequests = false + def renderInResponses = false def name(): String = "ExtractedValues" def value(): String = "" } diff --git a/akka-stream/src/main/scala/akka/stream/impl/fusing/Ops.scala b/akka-stream/src/main/scala/akka/stream/impl/fusing/Ops.scala index ab19696c11..02424ff379 100644 --- a/akka-stream/src/main/scala/akka/stream/impl/fusing/Ops.scala +++ b/akka-stream/src/main/scala/akka/stream/impl/fusing/Ops.scala @@ -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]] { ReactiveStreamsCompliance.requireNonNullElement(inject) - if(start.isDefined) ReactiveStreamsCompliance.requireNonNullElement(start.get) - if(end.isDefined) ReactiveStreamsCompliance.requireNonNullElement(end.get) + if (start.isDefined) ReactiveStreamsCompliance.requireNonNullElement(start.get) + if (end.isDefined) ReactiveStreamsCompliance.requireNonNullElement(end.get) private val in = Inlet[T]("in") private val out = Outlet[T]("out") diff --git a/akka-stream/src/main/scala/akka/stream/javadsl/BidiFlow.scala b/akka-stream/src/main/scala/akka/stream/javadsl/BidiFlow.scala index 490932f5f0..f5face3b31 100644 --- a/akka-stream/src/main/scala/akka/stream/javadsl/BidiFlow.scala +++ b/akka-stream/src/main/scala/akka/stream/javadsl/BidiFlow.scala @@ -5,10 +5,17 @@ package akka.stream.javadsl import akka.japi.function import akka.stream._ +import akka.stream.scaladsl.Flow import scala.concurrent.duration.FiniteDuration 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 * it so also in type. diff --git a/akka-stream/src/main/scala/akka/stream/scaladsl/BidiFlow.scala b/akka-stream/src/main/scala/akka/stream/scaladsl/BidiFlow.scala index 9b652997e8..8e35c49844 100644 --- a/akka-stream/src/main/scala/akka/stream/scaladsl/BidiFlow.scala +++ b/akka-stream/src/main/scala/akka/stream/scaladsl/BidiFlow.scala @@ -155,6 +155,11 @@ final class BidiFlow[-I1, +O1, -I2, +O2, +Mat](private[stream] override val modu } 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 * it so also in type. diff --git a/akka-stream/src/main/scala/akka/stream/scaladsl/Graph.scala b/akka-stream/src/main/scala/akka/stream/scaladsl/Graph.scala index acd3f5d423..c9ba4e2847 100644 --- a/akka-stream/src/main/scala/akka/stream/scaladsl/Graph.scala +++ b/akka-stream/src/main/scala/akka/stream/scaladsl/Graph.scala @@ -7,7 +7,7 @@ import akka.stream._ import akka.stream.impl._ import akka.stream.impl.fusing.GraphStages 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.stage.{ OutHandler, InHandler, GraphStageLogic, GraphStage } import scala.annotation.unchecked.uncheckedVariance