HTTP Server and Client
Spice ships a blocking HTTP/1.1 server and client in std/net/http-server and std/net/http-client, built directly
on top of the plain TCP sockets in std/net/socket. Both understand request/response framing via Content-Length
and chunked transfer encoding, so you get a working HTTP stack without pulling in any third-party dependency.
This tutorial builds a small server with a few routes, and a client that talks to it, as two separate programs - which is how you would run them in practice.
TLS
HttpClient and HttpServer both speak TLS via std/net/tls, built on the OpenSSL bindings in
std/bindings/openssl - see TLS / HTTPS below. This adds a build-time dependency on OpenSSL
(libssl-dev on Debian/Ubuntu), the same way the libcurl bindings in std/bindings/libcurl depend on libcurl.
A minimal server¶
Every server is built around the HttpServer struct. Construct it with a port, register routes on it, then start
it and let it run:
serve registers a GET route that always answers with the same, fixed piece of content - handy for static pages.
start binds the port, and run then serves connections one at a time, forever, until something calls stop or a
connection can no longer be accepted. Run it with:
The process keeps running in the foreground; leave it there and press ++ctrl+c++ once you are done with the tutorial.
A minimal client¶
HttpClient is the counterpart on the other side. Every call opens its own connection, sends one request, and
closes the connection again once the response has arrived:
With the server from the previous section still running, open a second terminal and run the client:
Adding routes¶
Besides serve, HttpServer has one method per HTTP method - get, post, put, patch and delete - that
takes a path and a handler lambda of type p(const HttpRequest&, HttpResponse&). The handler fills in the response
that is passed to it:
getQueryParam reads and percent-decodes a value straight out of the query string, so a request to
/greet?name=Spice answers with Hello, Spice!, while a plain /greet falls back to Hello, stranger!.
Routing is exact-match
A route only answers the exact path it was registered for - /a and /a/ are different routes, path
parameters such as /users/{id} are not supported, and the query string never takes part in the match. Read
dynamic parts of the path yourself out of request.getPath(), and query parameters via getQueryParam.
A path that exists for another method answers 405 Method Not Allowed automatically, and a HEAD request is
answered from the matching GET route with the body dropped - you don't need to register those cases yourself.
Request and response bodies¶
A handler reads the request body straight off the request.body field, and hands a response body to
response.setBody together with its media type. setJsonBody and setHtmlBody are shorthands for the two most
common cases:
| Spice | |
|---|---|
std/net/http defines constants for the common status codes (STATUS_OK, STATUS_CREATED, STATUS_NOT_FOUND,
STATUS_INTERNAL_SERVER_ERROR, ...) and media types (CONTENT_TYPE_TEXT, CONTENT_TYPE_HTML, CONTENT_TYPE_JSON,
...), so you rarely have to spell out a raw number or MIME string yourself.
Handling unmatched requests¶
Without any configuration, a request that matches no route gets a plain 404. Register a custom handler with
setNotFoundHandler to answer it differently:
| Spice | |
|---|---|
The status code is already set to 404 by the time your handler runs - you only need to fill in the body.
Sending more than GET¶
HttpClient mirrors the server's set of methods - get, head, post, put, patch and delete - plus a
request method that takes an arbitrary HttpMethod for anything that does not fit the named ones. post, put
and patch take a body and a content type, just like setBody does on the server side:
A handful of public fields on HttpClient let you adjust its behavior:
| Spice | |
|---|---|
defaultHeaders fields are only added to a request if it does not already carry a field with that name, so a
one-off request can always override them.
TLS / HTTPS¶
HttpClient.get (and the other request methods) transparently use TLS whenever the URL starts with https:// - no
extra code needed on the client side beyond the URL itself:
| Spice | |
|---|---|
By default, the server's certificate is verified against the operating system's trust store, and its host name is
checked against the URL. Point verification at a specific CA bundle instead - for example to talk to a server with
a self-signed certificate - with trustedCaFile:
| Spice | |
|---|---|
There is no way to switch verification off: a client that does not verify the server it talks to gets no real
confidentiality guarantee, so std/net/tls does not offer that option.
To serve https instead of plain http, call useTls with a certificate and private key (PEM files) before start:
| Spice | |
|---|---|
Everything else - routes, handlers, start/run/stop - stays the same; only the listening socket's connections
are now TLS-handshaked before their request is read.
Putting it all together¶
server.spice:
client.spice:
Run the server in one terminal and the client in another:
| Bash | |
|---|---|
Skipping error handling in a demo
The full example above calls unwrap() straight away for brevity. unwrap() aborts the program if the
Result holds an error, so in real code check isErr() first, the way the earlier sections do.