Http.Server
An HTTP server for an Elm program: requests arrive as subscription messages, and the program answers each one by sending a response as a command.
listen or listenOnHost starts a Server.
onRequest subscribes to its requests, and every Request arrives
together with a fresh Response, which the program builds up and
sends from its update function:
GotRequest request response ->
( model
, response
|> Http.Server.setHeader "Content-Type" "text/html"
|> Http.Server.setBody "<html>Hello there!</html>"
|> Http.Server.send
)A response answers the one request it arrived with. It carries that request's response key,
the server's name for a request that is still waiting for its answer. The key is released when
the request is answered, when upgradeRequest takes the request over, and
when the request's connection goes away. From then on, sending the response writes nothing, so a
request is answered at most once however often its response is sent.
The server speaks HTTP/1.0 and HTTP/1.1, HTTP/2 over TLS, or both, as its protocols say (see
ListenOptions). Over HTTP/1 a connection is kept alive and serves one request
after another; requests a client sends ahead of their answers (pipelining) are handed to the
program one at a time, in order, each once the one before it has been answered. Over HTTP/2
several requests on one connection are handed over at once and answered independently.
The server checks every request against its limits and the HTTP rules before the program sees it. A request that breaks them never arrives: the server answers it itself, with a status such as 400, 413 or 431, or over HTTP/2 may reset its stream instead.
Starting a server fails with this module's own Error. On the native backend under
Windows no server can be started (listen and listenOnHost fail with
NotSupported), and an onRequest subscription stops the program with a fatal
error.
Servers
type Servertype alias ListenOptions =
{ address : Address
, port_ : Int
, tls : Maybe ServerOptions
, protocols : List HttpVersion
, maxConnections : Maybe Int
, maxConcurrentStreams : Maybe Int
, maxBodySize : Int
, maxHeaderSize : Int
, keepAliveTimeout : Int
, headersTimeout : Int
, requestTimeout : Int
}How to run a server: where it listens, whether it uses TLS and HTTP/2, and the limits it holds clients to.
port_ 0 lets the system pick a free port, which endpoint reports.
tls serves HTTPS (and so wss WebSockets) with the given certificate chain and key. Its alpn
field is not used: the server offers its protocols instead, h2 for HTTP/2 and http/1.1
for HTTP/1.
protocols are the HTTP versions the server serves, as a set: their order does
not matter, repeats are ignored, and a client that can use both HTTP/1 and HTTP/2 is served
HTTP/2. Http1_0 and Http1_1 both serve HTTP/1.0 and HTTP/1.1. Http2 serves HTTP/2 to
clients that choose it during the TLS handshake, so it requires tls, and TLS 1.2 is then limited
to the cipher suites HTTP/2 allows. With HTTP/1, a TLS client that offers neither name, or none at
all, is still served HTTP/1.1; without it, such a client is refused. protocols must not be empty.
maxConnections is the most connections open at once. Natively, further clients wait in the
operating system's queue of pending connections until one closes; on the JavaScript backend Node
drops them.
maxConcurrentStreams is the most HTTP/2 requests, WebSockets included, in progress at once on one
connection, and is advertised to the client. Natively, requests the program has not answered yet
count even when the client has cancelled them, and at the limit the server processes nothing more
from that connection until the program answers one; on the JavaScript backend Node refuses the
requests over the limit. Without this limit, natively a client that keeps cancelling its requests
is held back only by a limit on cancellations (about 1000 at once, then 33 per second), so a
public server should set one.
For both of these limits Nothing means no limit, but Just 0 or a negative value is a limit of
0, which serves nothing. A maxConnections of 0 natively accepts no connection, so clients wait
indefinitely, and on the JavaScript backend drops every one. A maxConcurrentStreams of 0 refuses
every HTTP/2 request.
maxBodySize and maxHeaderSize are in bytes. A request with a larger body is answered 413, and
one with a larger header block 431 (on the JavaScript backend over HTTP/2, a header block over
twice maxHeaderSize has its stream reset instead).
keepAliveTimeout, headersTimeout and requestTimeout are in milliseconds, and a value of 0 or
less is no timeout. keepAliveTimeout is how long an idle connection stays open between requests.
headersTimeout runs from the first byte of a request to the end of its headers and
requestTimeout to the end of its body; a request that takes longer is answered 408. Natively, a
new connection without TLS that sends nothing is closed after headersTimeout. Over HTTP/2 they
apply differently: natively headersTimeout limits only the wait for a new connection's first
bytes, and on the JavaScript backend none of the three applies. No timeout limits the program's
answer: the connection waits for it.
defaultListenOptions : Address -> Int -> ListenOptionsReturns the options for plain HTTP/1.1 on address and port_, with no connection or stream
limits, bodies up to 16 MiB, header blocks up to 64 KiB, a 5 second keep-alive timeout, a 60
second headers timeout and a 300 second request timeout.
Http.Server.defaultListenOptions (Socket.Address.loopback Socket.Address.IPv4) 8080listen : ListenOptions -> Task Error ServerStarts a server as options say.
let
options =
Http.Server.defaultListenOptions (Socket.Address.loopback Socket.Address.IPv4) 8443
in
Http.Server.listen
{ options | tls = Just { certificateChain = cert, privateKey = key, alpn = [] } }Fails with an Error: for example of kind AddressInUse when the port
is taken, CertificateUnusable when the certificate chain or key cannot be used,
InvalidPort for a port outside 0 to 65535, and InvalidInput when
protocols is empty or has Http2 without tls. A listening server keeps the program running only while an onRequest subscription to it exists or
a request it handed over has not been answered.
listenOnHost : String -> Int -> Task Error ServerStarts a server listening on host and port_, with the limits and timeouts of
defaultListenOptions, and without TLS.
Http.Server.listenOnHost "localhost" 8080An address literal such as "127.0.0.1" or "::1" is used as it is. A name is resolved, and the
server listens on one of its addresses; natively that is the first IPv4 address that can be used,
if there is one. The empty string listens on every interface: natively on 0.0.0.0, so on IPv4
only, and on the JavaScript backend on Node's default, :: where IPv6 is available. Port 0 lets
the system pick a free port, which endpoint reports.
A name that cannot be resolved fails with kind HostNotFound, or
HostLookupTryAgain when the resolver could not answer for now. A
listening server keeps the program running only while an onRequest subscription to
it exists or a request it handed over has not been answered.
endpoint : Server -> InetEndpointReturns the address and port a server listens on. For port 0 this is the port the system
picked. An address the system reports as text that does not parse is given as 0.0.0.0.
close : Server -> Task x ()Stops a server gracefully, with closeWithin and a deadline of 5 seconds.
closeWithin : Int -> Server -> Task x ()Stops a server gracefully, closing the connections still open after deadline milliseconds.
The server stops accepting connections at once (the port is free when the task completes) and
closes its idle connections. Requests held because nothing subscribed to them (see
onRequest) are answered 503. Requests already handed to the program can still be
answered until the deadline, over HTTP/1 with Connection: close; then the connections that remain
are closed. A request still being received when the server closes can still be handed to a
subscription. WebSockets made with upgradeRequest stay open, except one over
HTTP/2, which shares its connection and ends with it at the deadline.
Closing a server twice is fine. Once the deadline has passed, nothing of a closed server keeps the program running.
Requests
type alias Request =
{ headers : Dict String String
, method : Method
, body : Bytes
, url : Url
, version : HttpVersion
, upgrade : Maybe Upgrade
}An HTTP request a server has received.
headers maps each header name, spelled as the client sent it, to its value. Over HTTP/2 names
are always lower case, and over HTTP/1 they need not be, so look a header up by every spelling it
may have or compare names case-insensitively: names that differ in case are separate entries. When
a name is sent more than once with the same spelling, the last value wins. Over HTTP/2 the
pseudo-header fields (:method, :path, ...) are not included, and several cookie fields are
joined into one with "; ".
body is the complete request body.
url is the absolute URL of the request, made from the Host header (over HTTP/2 the
:authority field), or, without one, the host the server was started with (its address for
listen, localhost for listenOnHost "") and its port, followed by the request target; a target
that is already an absolute URL is used as it is. When that text is not one Url.fromString
accepts, for example when the host is a bracketed IPv6 address, url has an empty host and path,
and its protocol is Https when the connection uses TLS and Http otherwise.
method is the request's Http.Method.Method, for example GET; a method
that has no constructor of its own is UNKNOWN with its name as received, such as
UNKNOWN "PROPFIND".
upgrade is the protocol the client asks to switch to, as an Upgrade, such as
Just WebSocketUpgrade: the first protocol of the HTTP/1.1 Upgrade header when Connection
also names upgrade (natively, only for a request without a body), or HTTP/2's :protocol.
Answer a WebSocket request with upgradeRequest. Any response sent instead
refuses the upgrade, and over HTTP/1 the connection then closes.
type HttpVersion
= Http1_0
| Http1_1
| Http2The HTTP version a request was made with, or one a server offers in the protocols of its
ListenOptions. A request is made with Http2 only on a server whose protocols
include it.
type Upgrade
= WebSocketUpgrade
| UnknownUpgrade StringThe protocol a request asks to switch to, from its upgrade field.
WebSocketUpgrade is a WebSocket opening request (websocket), to be taken over with
upgradeRequest. UnknownUpgrade holds any other protocol, lower-cased, as
it was received, for example UnknownUpgrade "h2c". The server switches to no other protocol
itself, so the program answers such a request with a normal response, which refuses the upgrade.
The protocol name is compared after lower-casing, so Upgrade: WebSocket is a
WebSocketUpgrade too.
bodyAsString : Request -> Maybe StringReturns the request body as a string, or Nothing if it is not valid UTF-8.
bodyAsJson : Decoder a -> Request -> Result Error aDecodes the request body as JSON. A body that is not valid UTF-8 is decoded as the empty string, so the decoder fails.
requestToString : Request -> StringReturns a one-line description of a request for logs: its method and URL, for example
"GET http://localhost:8080/". An UNKNOWN method is written as UNKNOWN(name), so that it
cannot be mistaken for one of the methods that have a constructor.
onRequest : Server -> (Request -> Response -> msg) -> Sub msgCreates a subscription to the requests server receives, each with a fresh
Response that answers it.
subscriptions : Model -> Sub Msg
subscriptions model =
Http.Server.onRequest model.server GotRequestEvery subscription to one server receives every request, all with the same response, so only the first of them to be sent answers it.
Requests that arrive while nothing subscribes to their server are held, and handed out oldest
first once a subscription appears. A server holds about maxBodySize × 4 bytes of them, and
always at least one. It answers 503 to a request beyond that, and to a request that arrives
after the server was closed with nothing subscribed.
Responses
A fresh response has status 200, no headers and an empty body.
type alias Response =
ResponseAn HTTP response being built for one request.
A response is obtained only from onRequest, together with the request it answers,
and stays tied to that request: the functions below change its status, headers and body, but
never which request it answers.
send : Response -> Cmd msgCreates a command that sends response to the request it answers.
The response is written with its status, its headers in order, one header line per value, and its body:
- A
Content-Lengthheader is always written, except with status 204 or 304, which send no body. The response's ownContent-Length,ConnectionandTransfer-Encodingheaders are left out, and so is a header whose name is empty or whose name or value holds a carriage return, line feed or NUL character. On the JavaScript backend a header Node considers invalid, such as one whose name contains a space, is left out as well. ADateheader is added unless the response has one. - Over HTTP/2 header names are sent lower-cased, and other headers HTTP/2 forbids, such as
UpgradeandKeep-Alive, are left out too. On the JavaScript backend, if a header HTTP/2 allows only once, such asContent-Type, has several values (for example from two spellings of its name), every header of the response that has several values keeps only its last one. - The answer to a
HEADrequest has the headers only. - An HTML body (see
setBodyAsHtml) is serialized now.
Over HTTP/1 the connection then stays open for the client's next request, unless, for example,
the client asked to close it (Connection: close, or HTTP/1.0 without keep-alive), the response
has a Connection: close header, the server is closing, or the request asked to switch protocols
or was a CONNECT.
Sending a response whose request was already answered, was taken over by
upgradeRequest, or lost its connection writes nothing.
Status and headers
setStatus : Int -> Response -> ResponseSets the HTTP status code of a response. A status below 200 or above 999 is sent as 500.
response |> Http.Server.setStatus 404setHeader : String -> String -> Response -> ResponseSets a header on a response, replacing the values of the header with exactly this name and
keeping its place, or adding it last. Names are compared exactly, so "content-type" does not
replace "Content-Type": the response then has both.
appendHeader : String -> String -> Response -> ResponseAdds a value to the header with exactly this name, after the values it already has, or adds
the header last. Use this for headers that may appear more than once, such as Set-Cookie.
Body
setBody : String -> Response -> ResponseSets the body of a response to a string; the same as setBodyAsString.
setBodyAsString : String -> Response -> ResponseSets the body of a response to a string, which is sent encoded as UTF-8. The backends differ only for a lone surrogate: the JavaScript backend sends it as U+FFFD, the native one in its 3-byte form, which is not valid UTF-8.
setBodyAsBytes : Bytes -> Response -> ResponseSets the body of a response to bytes, which are sent as they are.
setBodyAsHtml : Node msg -> Response -> ResponseSets the body of a response to an HTML page or fragment built with
elm/html (or Svg).
response
|> Http.Server.setBodyAsHtml
(Html.node "html" [] [ Html.node "body" [] [ Html.text "Hello!" ] ])
|> Http.Server.sendWhen the response is sent:
- the HTML is written as the text
Html.Dom.htmlToStringgives, encoded as UTF-8, with the same lone-surrogate difference between the backends assetBodyAsString; <!DOCTYPE html>is written first when the root, seen throughHtml.map, is an HTMLhtmlelement;Content-Type: text/html; charset=utf-8is added unless the response already has aContent-Typeheader, in any case.
WebSockets
upgradeRequest : Request -> Response -> Task Error UpgradeTakes over a WebSocket opening request (a Request whose upgrade is
Just WebSocketUpgrade, over HTTP/1.1 or HTTP/2) instead of answering it with its
Response. The result is a WebSocket.Upgrade, to be answered
with WebSocket.accept or WebSocket.reject. The response
is used up: sending it later writes nothing.
GotRequest request response ->
if request.upgrade == Just Http.Server.WebSocketUpgrade then
( model
, Http.Server.upgradeRequest request response
|> Task.andThen (WebSocket.accept WebSocket.defaultAcceptOptions)
|> Task.attempt GotWebSocket
)
else
...The opening request is checked by WebSocket.accept, which answers a request that breaks the
rules with 400 or 426 itself, as for WebSocket.receiveUpgrade. Over
HTTP/1.1 the requests the client sent before this one on the same connection are answered first,
so the 101 follows their responses.
Over HTTP/2 the request is an extended CONNECT with :protocol websocket, which a server with
Http2 allows; the server itself answers any other :protocol with 501. The WebSocket then runs
on that one stream of the connection, next to the connection's other requests and WebSockets:
accept answers 200 and the stream carries the frames, the close handshake ends the stream, and
an aborted WebSocket resets it. Such a WebSocket is one of the connection's streams for
maxConcurrentStreams.
The WebSocket does not belong to the server: close leaves it open, except that over
HTTP/2 it ends at the close deadline with the connection it shares.
Fails with a WebSocket.Error: with SocketError of kind
Socket.InvalidInput when the request does not ask for a WebSocket, in which
case the response is untouched and can still be sent; of kind Socket.InvalidInput too when the
request was already answered or taken over; and of kind Socket.InvalidInput or
Socket.Cancelled when its connection has gone away.
The program keeps running until the Upgrade has been accepted or rejected with
WebSocket.accept or WebSocket.reject, or its connection has gone away.
Errors
type ErrorA failure to start a server, from listen or listenOnHost.
errorKind says what kind of failure it is, and errorToString
describes it for people to read.
This is a separate type from Socket.Error, and from the WebSocket.Error
that upgradeRequest fails with, so that a server program matches only on what
starting a server can produce.
type ErrorKind
= AddressInUse
| AddressNotAvailable
| HostNotFound
| HostLookupTryAgain
| PermissionDenied
| InvalidPort
| InvalidInput
| NotSupported
| CertificateUnusable String
| OtherServerError StringWhat kind of failure an Error is.
AddressInUse(EADDRINUSE): the port is already in use, for example by another server.AddressNotAvailable(EADDRNOTAVAIL,EAFNOSUPPORT): the address does not belong to this machine, or the system does not support its family.HostNotFound(ENOTFOUND,EAI_FAIL): the host name given tolistenOnHosthas no address.HostLookupTryAgain(EAI_AGAIN): the resolver could not answer for now; trying again later may succeed.PermissionDenied(EACCES,EPERM): the operating system refused to listen, for example on a privileged port.InvalidPort(ERR_SOCKET_BAD_PORT): the port is outside 0 to 65535.InvalidInput(EINVAL): the options cannot be used, as emptyprotocolsorHttp2withouttls.NotSupported(ENOTSUP): no server can be started here, as on the native backend under Windows.CertificateUnusable reason: the TLS certificate chain or private key cannot be used, for example because it is not PEM or the key does not belong to the certificate.reasonis the TLS library's reason, upper-cased with underscores, such as"NO_CERTIFICATES". This is notSocket.Tls.CertificateRejected, which is a peer's certificate failing verification.OtherServerError code: any other failure, with the code the system reported, such as an errno name.
errorKind : Error -> ErrorKindReturns what kind of failure the error is.
case Http.Server.errorKind error of
Http.Server.AddressInUse ->
"the port is taken"
_ ->
Http.Server.errorToString errorerrorToString : Error -> StringReturns a human readable description of the error: the code the system reported, ": ", and
the message, as in "EADDRINUSE: listen EADDRINUSE: address already in use 127.0.0.1:8080".
An HTTP server that
listenorlistenOnHoststarted.A
Serverstays usable after it has been closed:endpointstill answers, and closing it again does nothing.