HTTP Server
The spray-can HTTP server is an embedded, actor-based, fully asynchronous, low-level, low-overhead and high-performance HTTP/1.1 server implemented on top of Akka IO / spray-io.
It sports the following features:
- Low per-connection overhead for supporting many thousand concurrent connections
- Efficient message parsing and processing logic for high throughput applications
- Full support for HTTP persistent connections
- Full support for HTTP pipelining
- Full support for asynchronous HTTP streaming (i.e. “chunked” transfer encoding)
- Optional SSL/TLS encryption
- Actor-based architecture and API for easy integration into your Akka applications
Design Philosophy
The spray-can HttpServer
is scoped with a clear focus on the essential functionality of an HTTP/1.1 server:
- Connection management
- Message parsing and header separation
- 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 next-higher layer in the application stack, they are not implemented by spray-can itself. Apart from general focus this design keeps the server small and light-weight as well as easy to understand and maintain. It also makes a spray-can HTTP server a perfect “container” for a spray-routing application, since spray-can and spray-routing nicely complement and interface into each other.
Basic Architecture
The spray-can HTTP server is implemented by two types of Akka actors, which sit on top of Akka IO. When you tell
spray-can to start a new server instance on a given port an HttpListener
actor is started, which accepts incoming
connections and for each one spawns a new HttpServerConnection
actor, which then manages the connection for the
rest of its lifetime.
These connection actors process the requests coming in across their connection and dispatch them as immutable
spray-http HttpRequest
instances to a “handler” actor provided by your application.
The handler can complete a request by simply replying with an HttpResponse
instance:
def receive = {
case HttpRequest(GET, Uri.Path("/ping"), _, _, _) =>
sender() ! HttpResponse(entity = "PONG")
}
Your code never deals with the HttpListener
and HttpServerConnection
actor classes directly, in fact they are
marked private
to the spray-can package. All communication with these actors happens purely via actor messages,
the majority of which are defined in the spray.can.Http object.
Starting
A spray-can HTTP server is started by sending an Http.Bind
command to the Http
extension:
import akka.io.IO
import spray.can.Http
implicit val system = ActorSystem()
val myListener: ActorRef = // ...
IO(Http) ! Http.Bind(myListener, interface = "localhost", port = 8080)
With the Http.Bind
command you register an application-level “listener” actor and specify the interface and port to
bind to. Additionally the Http.Bind
command also allows you to define socket options as well as a larger number of
settings for configuring the server according to your needs.
The sender of the Http.Bind
command (e.g. an actor you have written) will receive an Http.Bound
reply after
the HTTP layer has successfully started the server at the respective endpoint. In case the bind fails (e.g. because
the port is already busy) an Http.CommandFailed
message is dispatched instead.
The sender of the Http.Bound
confirmation event is spray-can‘s HttpListener
instance. You will need this
ActorRef
if you want to stop the server later.
Stopping
To explicitly stop the server, send an Http.Unbind
command to the HttpListener
instance (the ActorRef
for this instance is available as the sender of the Http.Bound
confirmation event from when the server
was started).
The listener will reply with an Http.Unbound
event after successfully unbinding from the port (or with
an Http.CommandFailed
in the case of error). At that point no further requests will be accepted by the
server.
Any requests which were in progress at the time will proceed to completion. When the last request has terminated,
the HttpListener
instance will exit. You can monitor for this (e.g. so that you can shutdown the ActorSystem
)
by watching the listener actor and awaiting a Terminated
message.
Message Protocol
After having successfully bound an HttpListener
your application communicates with the spray-can-level connection
actors via a number of actor messages that are explained in this section.
Request-Response Cycle
When a new connection has been accepted the application-level listener, which was registered with the Http.Bind
command, receives an Http.Connected
event message from the connection actor. The application must reply to it with
an Http.Register
command within the configured registration-timeout
period, otherwise the connection will be
closed.
With the Http.Register
command the application tells the connection actor which actor should handle incoming
requests. The application is free to register the same actor for all connections (a “singleton handler”), a new one for
every connection (“per-connection handlers”) or anything in between. After the connection actor has received the
Http.Register
command it starts reading requests from the connection and dispatches them as
spray.http.HttpRequestPart
messages to the handler. The handler actor should then process the request according to
the application logic and respond by sending an HttpResponsePart
instance to the sender
of the request.
The ActorRef
used as the sender of an HttpRequestPart
received by the handler is unique to the request, i.e.
several requests, even when coming in across the same connection, will appear to be sent from different senders.
spray-can uses this sender ActorRef
to coalesce the response with the request, so you cannot send several
responses to the same sender. However, the different request parts of chunked requests arrive from the same sender,
and the different response parts of a chunked response need to be sent to the same sender as well.
Caution
Since the ActorRef
used as the sender of a request is an UnregisteredActorRef it is not
reachable remotely. This means that the actor designated as handler by the application needs to live in the same
JVM as the HTTP extension.
Chunked Requests
If the request-chunk-aggregation-limit
config setting is set to zero the connection actor also dispatches the
individual request parts of chunked requests to the handler actor. In these cases a full request consists of the
following messages:
- One
ChunkedRequestStart
- Zero or more
MessageChunks
- One
ChunkedMessageEnd
The timer for checking request handling timeouts (if not configured to infinite
) only starts running when the final
ChunkedMessageEnd
message was dispatched to the handler.
Chunked Responses
Alternatively to a single HttpResponse
instance the handler can choose to respond to the request sender with the
following sequence of individual messages:
- One
ChunkedResponseStart
- Zero or more
MessageChunks
- One
ChunkedMessageEnd
The timer for checking request handling timeouts (if not configured to infinite
) will stop running as soon as the
initial ChunkedResponseStart
message has been received from the handler, i.e. there is currently no timeout checking
for and in between individual response chunks.
Request Timeouts
If the handler does not respond to a request within the configured request-timeout
period a
spray.http.Timedout
message is sent to the timeout handler, which can be the “regular” handler itself or
another actor (depending on the timeout-handler
config setting). The timeout handler then has the chance to
complete the request within the time period configured as timeout-timeout
. Only if the timeout handler also misses
its deadline for completing the request will the connection actor complete the request itself with a “hard-coded” error
response.
In order to change the respective config setting for that connection only the application can send the following
messages to the sender
of a request (part) or the connection actor:
- spray.io.ConnectionTimeouts.SetIdleTimeout
- spray.http.SetRequestTimeout
- spray.http.SetTimeoutTimeout
Closed Notifications
When a connection is closed, for whatever reason, the connection actor dispatches one of five defined
Http.ConnectionClosed
event message to the application (see the Common Behavior chapter for more info).
Exactly which actor receives it depends on the current state of request processing.
The connection actor sends Http.ConnectionClosed
events coming in from the underlying IO layer
- to the handler actor
- to the request chunk handler if one is defined and no response part was yet received
- to the sender of the last received response part
- if the ACK for an ACKed response part has not yet been dispatched
- if a response chunk stream has not yet been finished (with a
ChunkedMessageEnd
)
Note
The application can always choose to actively close a connection by sending one of the three defined
Http.CloseCommand
messages to the sender of a request or the connection actor (see Common Behavior).
However, during normal operation it is encouraged to make use of the Connection
header to signal to the
connection actor whether or not the connection is to be closed after the response has been sent.
Server Statistics
If the stats-support
config setting is enabled the server will continuously count connections, requests, timeouts
and other basic statistics. You can ask the HttpListener
actor (i.e. the sender ActorRef
of the Http.Bound
event message!) to reply with an instance of the spray.can.server.Stats
class by sending it an Http.GetStats
command. This is what you will get back:
case class Stats(
uptime: FiniteDuration,
totalRequests: Long,
openRequests: Long,
maxOpenRequests: Long,
totalConnections: Long,
openConnections: Long,
maxOpenConnections: Long,
requestTimeouts: Long)
By sending the listener an Http.ClearStats
command message you can trigger a reset of the stats.
HTTP Headers
When a spray-can connection actor receives an HTTP request it tries to parse all its headers into their respective
spray-http model classes. No matter whether this succeeds or not, the connection actor will always pass on all
received headers to the application. Unknown headers as well as ones with invalid syntax (according to spray‘s header
parser) will be made available as RawHeader
instances. For the ones exhibiting parsing errors a warning message is
logged depending on the value of the illegal-header-warnings
config setting.
When sending out responses the connection actor watches for a Connection
header set by the application and acts
accordingly, i.e. you can force the connection actor to close the connection after having sent the response by including
a Connection("close")
header. To unconditionally force a connection keep-alive you can explicitly set a
Connection("Keep-Alive")
header. If you don’t set an explicit Connection
header the connection actor will keep
the connection alive if the client supports this (i.e. it either sent a Connection: Keep-Alive
header or advertised
HTTP/1.1 capabilities without sending a Connection: close
header).
The following response headers are managed by the spray-can layer itself and as such are ignored if you “manually” add them to the response (you’ll see a warning in your logs):
Content-Type
Content-Length
Transfer-Encoding
Date
Server
There are three exceptions:
- Responses to HEAD requests that have an empty entity are allowed to contain a user-specified
Content-Type
header. - Responses in
ChunkedResponseStart
messages that have an empty entity are allowed to contain a user-specifiedContent-Type
header. - Responses in
ChunkedResponseStart
messages are allowed to contain a user-specifiedContent-Length
header ifspray.can.server.chunkless-streaming
is enabled.
Note
The Content-Type
header has special status in spray since its value is part of the HttpEntity
model
class. Even though the header also remains in the headers
list of the HttpRequest
sprays higher layers
(like spray-routing) only work with the ContentType
value contained in the HttpEntity
.
HTTP Pipelining
spray-can fully supports HTTP pipelining. If the configured pipelining-limit
is greater than one a connection
actor will accept several requests in a row (coming in across a single connection) and dispatch them to the application
even before the first one has been responded to. This means that several requests will potentially be handled by the
application at the same time.
Since in many asynchronous applications request handling times can be somewhat undeterministic spray-can takes care of properly ordering all responses coming in from your application before sending them out to “the wire”. I.e. your application will “see” requests in the order they are coming in but is not required to itself uphold this order when generating responses.
SSL Support
If enabled via the ssl-encryption
config setting the spray-can connection actors pipe all IO traffic through an
SslTlsSupport
module, which can perform transparent SSL/TLS encryption. This module is configured via the implicit
ServerSSLEngineProvider
member on the Http.Bind
command message. An ServerSSLEngineProvider
is essentially
a function PipelineContext ⇒ Option[SSLEngine]
, which determines whether encryption is to be performed and, if so,
which javax.net.ssl.SSLEngine
instance is to be used.
If you’d like to apply some custom configuration to your SSLEngine
instances an easy way would be to bring a custom
engine provider into scope, e.g. like this:
import spray.io.ServerSSLEngineProvider
implicit val myEngineProvider = ServerSSLEngineProvider { engine =>
engine.setEnabledCipherSuites(Array("TLS_RSA_WITH_AES_256_CBC_SHA"))
engine.setEnabledProtocols(Array("SSLv3", "TLSv1"))
engine
}
EngineProvider creation also relies on an implicitly available SSLContextProvider
, which is defined like this:
trait SSLContextProvider extends (PipelineContext ⇒ Option[SSLContext])
The default SSLContextProvider
simply provides an implicitly available “constant” SSLContext
, by default the
SSLContext.getDefault
is used. This means that the easiest way to have the server use a custom SSLContext
is to simply bring one into scope implicitly:
import javax.net.ssl.SSLContext
implicit val mySSLContext: SSLContext = {
val context = SSLContext.getInstance("TLS")
// context.init(...)
context
}