Core Server API
The core Server API is scoped with a clear focus on the essential functionality of an HTTP/1.1 server:
- Connection management
- Parsing and rendering of messages and headers
- Timeout management (for requests and connections)
- Response ordering (for transparent pipelining support)
All non-core features of typical HTTP servers (like request routing, file serving, compression, etc.) are left to the higher layers, they are not implemented by the pekko-http-core
-level server itself. Apart from general focus this design keeps the server core small and light-weight as well as easy to understand and maintain.
It is recommended to read the Implications of the streaming nature of Request/Response Entities section, as it explains the underlying full-stack streaming concepts, which may be unexpected when coming from a background with non-“streaming first” HTTP Servers.
Streams and HTTP
The Apache Pekko HTTP server is implemented on top of Streams and makes heavy use of it - in its implementation as well as on all levels of its API.
On the connection level, Apache Pekko HTTP offers basically the same kind of interface as Working with streaming IO: A socket binding is represented as a stream of incoming connections. The application pulls connections from this stream source and, for each of them, provides a Flow<HttpRequest, HttpResponse, ?>
Flow[HttpRequest, HttpResponse, _]
to “translate” requests into responses.
Apart from regarding a socket bound on the server-side as a Source<IncomingConnection, ?>
Source[IncomingConnection, _]
and each connection as a Source<HttpRequest, ?>
Source[HttpRequest, _]
with a Sink<HttpResponse, ?>
Sink[HttpResponse, _]
the stream abstraction is also present inside a single HTTP message: The entities of HTTP requests and responses are generally modeled as a Source<ByteString, ?>
Source[ByteString, _]
. See also the HTTP Model for more information on how HTTP messages are represented in Apache Pekko HTTP.
Starting and Stopping
On the most basic level an Apache Pekko HTTP server is bound by invoking the bind
method of the org.apache.pekko.http.scaladsl.Http
org.apache.pekko.http.javadsl.Http
extension:
- Scala
-
source
import org.apache.pekko import pekko.actor.ActorSystem import pekko.http.scaladsl.Http import pekko.stream.scaladsl._ implicit val system = ActorSystem() implicit val executionContext = system.dispatcher val serverSource: Source[Http.IncomingConnection, Future[Http.ServerBinding]] = Http().newServerAt("localhost", 8080).connectionSource() val bindingFuture: Future[Http.ServerBinding] = serverSource.to(Sink.foreach { connection => // foreach materializes the source println("Accepted new connection from " + connection.remoteAddress) // ... and then actually handle the connection }).run()
- Java
-
source
ActorSystem system = ActorSystem.create(); Materializer materializer = ActorMaterializer.create(system); Source<IncomingConnection, CompletionStage<ServerBinding>> serverSource = Http.get(system).bind(ConnectHttp.toHost("localhost", 8080)); CompletionStage<ServerBinding> serverBindingFuture = serverSource .to( Sink.foreach( connection -> { System.out.println( "Accepted new connection from " + connection.remoteAddress()); // ... and then actually handle the connection })) .run(materializer);
Arguments to the Http().bind
method specify the interface and port to bind to and register interest in handling incoming HTTP connections. Additionally, the method also allows for the definition of socket options as well as a larger number of settings for configuring the server according to your needs.
The result of the bind
method is a Source<Http.IncomingConnection>
Source[Http.IncomingConnection]
which must be drained by the application in order to accept incoming connections. The actual binding is not performed before this source is materialized as part of a processing pipeline. In case the bind fails (e.g. because the port is already busy) the materialized stream will immediately be terminated with a respective exception. The binding is released (i.e. the underlying socket unbound) when the subscriber of the incoming connection source has cancelled its subscription. Alternatively one can use the unbind()
method of the Http.ServerBinding
instance that is created as part of the connection source’s materialization process. The Http.ServerBinding
also provides a way to get a hold of the actual local address of the bound socket, which is useful for example when binding to port zero (and thus letting the OS pick an available port).
Request-Response Cycle
When a new connection has been accepted it will be published as an Http.IncomingConnection
which consists of the remote address and methods to provide a Flow<HttpRequest, HttpResponse, ?>
Flow[HttpRequest, HttpResponse, _]
to handle requests coming in over this connection.
Requests are handled by calling one of the handleWithXXX
methods with a handler, which can either be
- a
Flow<HttpRequest, HttpResponse, ?>
Flow[HttpRequest, HttpResponse, _]
forhandleWith
,- a function
HttpRequest => HttpResponse
Function<HttpRequest, HttpResponse>
forhandleWithSyncHandler
,- a function
HttpRequest => Future[HttpResponse]
Function<HttpRequest, CompletionStage<HttpResponse>>
forhandleWithAsyncHandler
.
Here is a complete example:
- Scala
-
source
import org.apache.pekko import pekko.actor.ActorSystem import pekko.http.scaladsl.Http import pekko.http.scaladsl.model.HttpMethods._ import pekko.http.scaladsl.model._ import pekko.stream.scaladsl.Sink implicit val system = ActorSystem() implicit val executionContext = system.dispatcher val serverSource = Http().newServerAt("localhost", 8080).connectionSource() val requestHandler: HttpRequest => HttpResponse = { case HttpRequest(GET, Uri.Path("/"), _, _, _) => HttpResponse(entity = HttpEntity( ContentTypes.`text/html(UTF-8)`, "<html><body>Hello world!</body></html>")) case HttpRequest(GET, Uri.Path("/ping"), _, _, _) => HttpResponse(entity = "PONG!") case HttpRequest(GET, Uri.Path("/crash"), _, _, _) => sys.error("BOOM!") case r: HttpRequest => r.discardEntityBytes() // important to drain incoming HTTP Entity stream HttpResponse(404, entity = "Unknown resource!") } val bindingFuture: Future[Http.ServerBinding] = serverSource.to(Sink.foreach { connection => println("Accepted new connection from " + connection.remoteAddress) connection.handleWithSyncHandler(requestHandler) // this is equivalent to // connection handleWith { Flow[HttpRequest] map requestHandler } }).run()
- Java
-
source
ActorSystem system = ActorSystem.create(); final Materializer materializer = ActorMaterializer.create(system); Source<IncomingConnection, CompletionStage<ServerBinding>> serverSource = Http.get(system).bind(ConnectHttp.toHost("localhost", 8080)); final Function<HttpRequest, HttpResponse> requestHandler = new Function<HttpRequest, HttpResponse>() { private final HttpResponse NOT_FOUND = HttpResponse.create().withStatus(404).withEntity("Unknown resource!"); @Override public HttpResponse apply(HttpRequest request) throws Exception { Uri uri = request.getUri(); if (request.method() == HttpMethods.GET) { if (uri.path().equals("/")) { return HttpResponse.create() .withEntity( ContentTypes.TEXT_HTML_UTF8, "<html><body>Hello world!</body></html>"); } else if (uri.path().equals("/hello")) { String name = uri.query().get("name").orElse("Mister X"); return HttpResponse.create().withEntity("Hello " + name + "!"); } else if (uri.path().equals("/ping")) { return HttpResponse.create().withEntity("PONG!"); } else { return NOT_FOUND; } } else { return NOT_FOUND; } } }; CompletionStage<ServerBinding> serverBindingFuture = serverSource .to( Sink.foreach( connection -> { System.out.println( "Accepted new connection from " + connection.remoteAddress()); connection.handleWithSyncHandler(requestHandler, materializer); // this is equivalent to // connection.handleWith(Flow.of(HttpRequest.class).map(requestHandler), // materializer); })) .run(materializer);
In this example, a request is handled by transforming the request stream with a function HttpRequest => HttpResponse
Function<HttpRequest, HttpResponse>
using handleWithSyncHandler
(or equivalently, Apache Pekko Stream’s map
operator). Depending on the use case many other ways of providing a request handler are conceivable using Apache Pekko Stream’s combinators. If the application provides a Flow
Flow
it is also the responsibility of the application to generate exactly one response for every request and that the ordering of responses matches the ordering of the associated requests (which is relevant if HTTP pipelining is enabled where processing of multiple incoming requests may overlap). When relying on handleWithSyncHandler
or handleWithAsyncHandler
, or the map
or mapAsync
stream operators, this requirement will be automatically fulfilled.
See Routing DSL Overview for a more convenient high-level DSL to create request handlers.
Streaming Request/Response Entities
Streaming of HTTP message entities is supported through subclasses of HttpEntity
HttpEntity
. The application needs to be able to deal with streamed entities when receiving a request as well as, in many cases, when constructing responses. See HttpEntity for a description of the alternatives.
If you rely on the Marshalling and/or Unmarshalling facilities provided by Apache Pekko HTTP then the conversion of custom types to and from streamed entities can be quite convenient.
Closing a connection
The HTTP connection will be closed when the handling Flow
Flow
cancels its upstream subscription or the peer closes the connection. An often times more convenient alternative is to explicitly add a Connection: close
header to an HttpResponse
HttpResponse
. This response will then be the last one on the connection and the server will actively close the connection when it has been sent out.
Connection will also be closed if request entity has been cancelled (e.g. by attaching it to Sink.cancelled()
or consumed only partially (e.g. by using take
combinator). In order to prevent this behaviour entity should be explicitly drained by attaching it to Sink.ignore()
.
Configuring Server-side HTTPS
For detailed documentation about configuring and using HTTPS on the server-side refer to Server-Side HTTPS Support.
Stand-Alone HTTP Layer Usage
Due to its Reactive-Streams-based nature, the Apache Pekko HTTP layer is fully detachable from the underlying TCP interface. While in most applications this “feature” will not be crucial it can be useful in certain cases to be able to “run” the HTTP layer (and, potentially, higher-layers) against data that do not come from the network but rather some other source. Potential scenarios where this might be useful include tests, debugging or low-level event-sourcing (e.g by replaying network traffic).
On the server-side the stand-alone HTTP layer forms a BidiFlow
BidiFlow
that is defined like this:
source/**
* The type of the server-side HTTP layer as a stand-alone BidiFlow
* that can be put atop the TCP layer to form an HTTP server.
*
* {{{
* +------+
* HttpResponse ~>| |~> SslTlsOutbound
* | bidi |
* HttpRequest <~| |<~ SslTlsInbound
* +------+
* }}}
*/
type ServerLayer = BidiFlow[HttpResponse, SslTlsOutbound, SslTlsInbound, HttpRequest, NotUsed]
You create an instance of Http.ServerLayer
by calling one of the two overloads of the Http().serverLayer
method, which also allows for varying degrees of configuration.
On the server-side the stand-alone HTTP layer forms a BidiFlow<HttpResponse, SslTlsOutbound, SslTlsInbound, HttpRequest, NotUsed>
BidiFlow[HttpResponse, SslTlsOutbound, SslTlsInbound, HttpRequest, NotUsed]
, that is a stage that “upgrades” a potentially encrypted raw connection to the HTTP level.
You create an instance of the layer by calling one of the two overloads of the Http.get(system).serverLayer
method, which also allows for varying degrees of configuration. Note, that the returned instance is not reusable and can only be materialized once.
Controlling server parallelism
Request handling can be parallelized on two axes, by handling several connections in parallel and by relying on HTTP pipelining to send several requests on one connection without waiting for a response first. In both cases the client controls the number of ongoing requests. To prevent being overloaded by too many requests, Apache Pekko HTTP can limit the number of requests it handles in parallel.
To limit the number of simultaneously open connections, use the pekko.http.server.max-connections
setting. This setting applies to all of Http.bindAndHandle*
methods. If you use Http.bind
, incoming connections are represented by a Source<IncomingConnection, ...>
Source[IncomingConnection, ...]
. Use Apache Pekko Stream’s combinators to apply backpressure to control the flow of incoming connections, e.g. by using throttle
or mapAsync
.
HTTP pipelining is generally discouraged (and disabled by most browsers) but is nevertheless fully supported in Apache Pekko HTTP. The limit is applied on two levels. First, there’s the pekko.http.server.pipelining-limit
config setting which prevents that more than the given number of outstanding requests is ever given to the user-supplied handler-flow. On the other hand, the handler flow itself can apply any kind of throttling itself. If you use the Http.bindAndHandleAsync
entry-point, you can specify the parallelism
argument (which defaults to 1
, which means that pipelining is disabled) to control the number of concurrent requests per connection. If you use Http.bindAndHandle
or Http.bind
, the user-supplied handler flow has full control over how many request it accepts simultaneously by applying backpressure. In this case, you can e.g. use Apache Pekko Stream’s mapAsync
combinator with a given parallelism to limit the number of concurrently handled requests. Effectively, the more constraining one of these two measures, config setting and manual flow shaping, will determine how parallel requests on one connection are handled.
Handling HTTP Server failures in the Low-Level API
There are various situations when failure may occur while initialising or running an Apache Pekko HTTP server. Apache Pekko by default will log all these failures, however sometimes one may want to react to failures in addition to them just being logged, for example by shutting down the actor system, or notifying some external monitoring end-point explicitly.
There are multiple things that can fail when creating and materializing an HTTP Server (similarly, the same applied to a plain streaming Tcp()
server). The types of failures that can happen on different layers of the stack, starting from being unable to start the server, and ending with failing to unmarshal an HttpRequest, examples of failures include (from outer-most, to inner-most):
- Failure to
bind
to the specified address/port, - Failure while accepting new
IncomingConnection
s, for example when the OS has run out of file descriptors or memory, - Failure while handling a connection, for example if the incoming
HttpRequest
HttpRequest
is malformed.
This section describes how to handle each failure situation, and in which situations these failures may occur.
Bind failures
The first type of failure is when the server is unable to bind to the given port. For example when the port is already taken by another application, or if the port is privileged (i.e. only usable by root
). In this case the “binding future” will fail immediately, and we can react to it by listening on the Future’sCompletionStage’s completion:
- Scala
-
source
import org.apache.pekko import pekko.actor.ActorSystem import pekko.http.scaladsl.Http import pekko.http.scaladsl.Http.ServerBinding import scala.concurrent.Future implicit val system = ActorSystem() // needed for the future foreach in the end implicit val executionContext = system.dispatcher // let's say the OS won't allow us to bind to 80. val (host, port) = ("localhost", 80) val serverSource = Http().newServerAt(host, port).connectionSource() val bindingFuture: Future[ServerBinding] = serverSource .to(handleConnections) // Sink[Http.IncomingConnection, _] .run() bindingFuture.failed.foreach { ex => log.error(ex, "Failed to bind to {}:{}!", host, port) }
- Java
-
source
ActorSystem system = ActorSystem.create(); Materializer materializer = ActorMaterializer.create(system); Source<IncomingConnection, CompletionStage<ServerBinding>> serverSource = Http.get(system).bind(ConnectHttp.toHost("localhost", 80)); CompletionStage<ServerBinding> serverBindingFuture = serverSource .to( Sink.foreach( connection -> { System.out.println( "Accepted new connection from " + connection.remoteAddress()); // ... and then actually handle the connection })) .run(materializer); serverBindingFuture.whenCompleteAsync( (binding, failure) -> { // possibly report the failure somewhere... }, system.dispatcher());
Once the server has successfully bound to a port, the Source<IncomingConnection, ?>
Source[IncomingConnection, _]
starts running and emitting new incoming connections. This source technically can signal a failure as well, however this should only happen in very dramatic situations such as running out of file descriptors or memory available to the system, such that it’s not able to accept a new incoming connection. Handling failures in Apache Pekko Streams is pretty straight forward, as failures are signaled through the stream starting from the stage which failed, all the way downstream to the final stages.
Connections Source failures
In the example below we add a custom GraphStage
GraphStage
in order to react to the stream’s failure. See Custom stream processing for more on custom stages. We signal a failureMonitor
actor with the cause why the stream is going down, and let the Actor handle the rest – maybe it’ll decide to restart the server or shutdown the ActorSystem, that however is not our concern anymore.
- Scala
-
source
import org.apache.pekko import pekko.actor.ActorSystem import pekko.actor.ActorRef import pekko.http.scaladsl.Http import pekko.stream.scaladsl.Flow implicit val system = ActorSystem() implicit val executionContext = system.dispatcher import Http._ val (host, port) = ("localhost", 8080) val serverSource = Http().newServerAt(host, port).connectionSource() val failureMonitor: ActorRef = system.actorOf(MyExampleMonitoringActor.props) val reactToTopLevelFailures = Flow[IncomingConnection] .watchTermination()((_, termination) => termination.failed.foreach { cause => failureMonitor ! cause }) serverSource .via(reactToTopLevelFailures) .to(handleConnections) // Sink[Http.IncomingConnection, _] .run()
- Java
-
source
ActorSystem system = ActorSystem.create(); Materializer materializer = ActorMaterializer.create(system); Source<IncomingConnection, CompletionStage<ServerBinding>> serverSource = Http.get(system).bind(ConnectHttp.toHost("localhost", 8080)); Flow<IncomingConnection, IncomingConnection, NotUsed> failureDetection = Flow.of(IncomingConnection.class) .watchTermination( (notUsed, termination) -> { termination.whenComplete( (done, cause) -> { if (cause != null) { // signal the failure to external monitoring service! } }); return NotUsed.getInstance(); }); CompletionStage<ServerBinding> serverBindingFuture = serverSource .via(failureDetection) // feed signals through our custom stage .to( Sink.foreach( connection -> { System.out.println( "Accepted new connection from " + connection.remoteAddress()); // ... and then actually handle the connection })) .run(materializer);
Connection failures
The third type of failure that can occur is when the connection has been properly established, however afterwards is terminated abruptly – for example by the client aborting the underlying TCP connection.
To handle this failure we can use the same pattern as in the previous snippet, however apply it to the connection’s Flow:
- Scala
-
source
import org.apache.pekko import pekko.actor.ActorSystem import pekko.http.scaladsl.Http import pekko.http.scaladsl.model._ import pekko.stream.scaladsl.Flow implicit val system = ActorSystem() implicit val executionContext = system.dispatcher val (host, port) = ("localhost", 8080) val serverSource = Http().newServerAt(host, port).connectionSource() val reactToConnectionFailure = Flow[HttpRequest] .recover[HttpRequest] { case ex => // handle the failure somehow throw ex } val httpEcho = Flow[HttpRequest] .via(reactToConnectionFailure) .map { request => // simple streaming (!) "echo" response: HttpResponse(entity = HttpEntity(ContentTypes.`text/plain(UTF-8)`, request.entity.dataBytes)) } serverSource .runForeach { con => con.handleWith(httpEcho) }
- Java
-
source
ActorSystem system = ActorSystem.create(); Materializer materializer = ActorMaterializer.create(system); Source<IncomingConnection, CompletionStage<ServerBinding>> serverSource = Http.get(system).bind(ConnectHttp.toHost("localhost", 8080)); Flow<HttpRequest, HttpRequest, NotUsed> failureDetection = Flow.of(HttpRequest.class) .watchTermination( (notUsed, termination) -> { termination.whenComplete( (done, cause) -> { if (cause != null) { // signal the failure to external monitoring service! } }); return NotUsed.getInstance(); }); Flow<HttpRequest, HttpResponse, NotUsed> httpEcho = Flow.of(HttpRequest.class) .via(failureDetection) .map( request -> { Source<ByteString, Object> bytes = request.entity().getDataBytes(); HttpEntity.Chunked entity = HttpEntities.create(ContentTypes.TEXT_PLAIN_UTF8, bytes); return HttpResponse.create().withEntity(entity); }); CompletionStage<ServerBinding> serverBindingFuture = serverSource .to( Sink.foreach( conn -> { System.out.println("Accepted new connection from " + conn.remoteAddress()); conn.handleWith(httpEcho, materializer); })) .run(materializer);
Note that this is when the TCP connection is closed correctly, if the client just goes away, for example because of a network failure, it will not be seen as this kind of stream failure. It will instead be detected through the idle timeout).
These failures can be described more or less infrastructure related, they are failing bindings or connections. Most of the time you won’t need to dive into those very deeply, as Apache Pekko will simply log errors of this kind anyway, which is a reasonable default for such problems.
In order to learn more about handling exceptions in the actual routing layer, which is where your application code comes into the picture, refer to Exception Handling which focuses explicitly on explaining how exceptions thrown in routes can be handled and transformed into HttpResponse
HttpResponse
s with appropriate error codes and human-readable failure descriptions.