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.
No TLS
Neither the server nor the client speaks TLS - the client rejects https:// URLs outright. If you need to talk
to an HTTPS endpoint, use the libcurl bindings in std/bindings/libcurl instead, as described in the
C/C++ interoperability tutorial.
// server.spiceimport"std/net/http";import"std/net/http-server";f<int>main(){HttpServerserver=HttpServer(8080s);server.serve("/","<h1>Hello from Spice!</h1>");Result<bool>started=server.start();ifstarted.isErr(){printf("Failed to start server: %s\n",started.getErr().message);return1;}printf("Listening on http://127.0.0.1:8080\n");Result<bool>ran=server.run();ifran.isErr(){printf("Server stopped with an error: %s\n",ran.getErr().message);return1;}return0;}
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:
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:
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.
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:
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.
Without any configuration, a request that matches no route gets a plain 404. Register a custom handler with
setNotFoundHandler to answer it differently:
server.setNotFoundHandler(p(constHttpRequest&request,HttpResponse&response){constStringpath=request.getPath();Stringmessage=String("No route for ");message.append(path);response.setBody(message,CONTENT_TYPE_TEXT);});
The status code is already set to 404 by the time your handler runs - you only need to fill in the body.
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:
client.setDefaultHeader("Authorization","Bearer secret-token");// sent with every requestclient.timeoutMillis=5000l;// per read/write timeout, 0 to block indefinitelyclient.maxRedirects=0u;// hand 3xx responses back as-is instead of following them
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.
import"std/net/http";import"std/net/http-server";f<int>main(){HttpServerserver=HttpServer(8080s);server.serve("/","<h1>Hello from Spice!</h1>");server.get("/greet",p(constHttpRequest&request,HttpResponse&response){Stringname=request.getQueryParam("name");ifname.isEmpty(){name=String("stranger");}Stringgreeting=String("Hello, ");greeting.append(name);greeting.append('!');response.setBody(greeting,CONTENT_TYPE_TEXT);});server.post("/users",p(constHttpRequest&request,HttpResponse&response){response.setStatus(STATUS_CREATED);Stringbody=String("{\"received\":");body.append(request.body);body.append('}');response.setJsonBody(body);});server.setNotFoundHandler(p(constHttpRequest&request,HttpResponse&response){constStringpath=request.getPath();Stringmessage=String("No route for ");message.append(path);response.setBody(message,CONTENT_TYPE_TEXT);});Result<bool>started=server.start();ifstarted.isErr(){printf("Failed to start server: %s\n",started.getErr().message);return1;}printf("Listening on http://127.0.0.1:8080\n");Result<bool>ran=server.run();ifran.isErr(){printf("Server stopped with an error: %s\n",ran.getErr().message);return1;}return0;}
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.