This guide shows how to use the wolfIP HTTP server module (src/http/): how
to build it, how to bring up a server on a TCP port, how to register static
pages and dynamic handlers, and how to layer TLS on top of it to serve HTTPS.
It is a getting-started document. The authoritative API is src/http/httpd.h;
the worked examples come from src/test/test_httpd.c (a TLS server serving a
static page) and src/port/stm32h563/main.c (an HTTPS status page served by a
dynamic handler on an STM32H5).
- 1. What the HTTP module is
- 2. Building with HTTP
- 3. Architecture: event callbacks, no threads
- 4. The server lifecycle
- 5. Registering content: static pages and handlers
- 6. The handler/callback API
- 7. Reading the request: methods, query and form args
- 8. Building the response
- 9. Serving a page: a minimal example
- 10. Enabling HTTPS (TLS layering)
- 11. URL encoding/decoding helpers
- 12. Troubleshooting
src/http/ is a small, self-contained HTTP/1.1 server (httpd.c/httpd.h)
built directly on the wolfIP socket API. Like the rest of wolfIP it performs
zero dynamic allocation: the server, its client slots, and the routing table
all live inside a single caller-provided struct httpd. Request and response
buffers are fixed-size and stack-allocated per call.
The server is event-driven. It registers wolfIP socket callbacks and is
driven entirely from inside wolfIP_poll() on a single thread — there is no
accept loop or per-connection thread to manage. When data arrives on a client
socket, wolfIP calls back into the module, which parses one request and emits a
response synchronously.
It supports:
- GET and POST requests (any other method is rejected as
400 Bad Request). - Static pages registered by path, served with
Content-Type: text/html. - Dynamic handlers registered by path, which build the response themselves.
- HTTPS by handing the server a
WOLFSSL_CTX, in which case every client connection is wrapped in a TLS session via the wolfSSL-over-wolfIP I/O layer.
The module is deliberately minimal. Responses are sent immediately as they are
generated, with no buffering; if the output socket is congested the write fails
and the connection is closed (see the header comment in httpd.c).
The HTTP server is gated by the WOLFIP_ENABLE_HTTP macro. The default POSIX
config.h enables it:
/* config.h */
/* Enable HTTP server for POSIX builds */
#ifndef WOLFIP_ENABLE_HTTP
#define WOLFIP_ENABLE_HTTP
#endifUnlike the TFTP module, src/http/httpd.c is not globbed into the main
wolfIP library. It is compiled and linked per target, and it always depends on
wolfSSL (the header pulls in <wolfssl/ssl.h> unconditionally, even for plain
HTTP). The relevant flags, from the top-level Makefile, are:
| Flag | Purpose |
|---|---|
-DWOLFIP_ENABLE_HTTP |
Compiles the module in (everything in httpd.c/httpd.h is #ifdef-guarded by it). |
-DWOLFSSL_WOLFIP |
Selects the wolfSSL-over-wolfIP I/O backend used for HTTPS. |
-Isrc/http |
Lets sources #include "httpd.h". |
To build and run the bundled HTTP/TLS server test:
make build/test-httpdThat target links build/http/httpd.o, the wolfSSL I/O glue
(build/port/wolfssl_io.o), and a test certificate/key against -lwolfssl
(Makefile). The CMake build defines an equivalent test-httpd target /
add_test(NAME httpd ...).
To use the module in your own build, compile src/http/httpd.c with the flags
above, add src/http to the include path, and link wolfSSL.
The module installs two kinds of wolfIP callback and never blocks:
wolfIP_poll(s, now_ms)
│
├─ listen socket readable ─▶ http_accept_cb()
│ │
│ ├─ wolfIP_sock_accept() ─▶ new client_sd
│ ├─ claim a free clients[] slot (max HTTPD_MAX_CLIENTS)
│ ├─ if ssl_ctx: wolfSSL_new() + wolfSSL_SetIO_wolfIP()
│ └─ wolfIP_register_callback(client_sd, http_recv_cb, &client)
│
└─ client socket readable ─▶ http_recv_cb()
│
├─ read : wolfSSL_read() (HTTPS) or wolfIP_sock_recv()
├─ parse : parse_http_request() ── one request, in place
├─ route : http_find_url(path)
│ ├─ static_content ─▶ headers + body
│ └─ handler(...) ─▶ app builds the response
└─ write : wolfSSL_write() (HTTPS) or wolfIP_sock_send()
All you do at the application level is: create a wolfIP stack, call
httpd_init(), register your routes, and keep calling wolfIP_poll() from your
main loop. Everything above happens inside poll.
A server is one struct httpd plus a call to httpd_init()
(src/http/httpd.h):
int httpd_init(struct httpd *httpd, struct wolfIP *s, uint16_t port, void *ssl_ctx);httpd_init() zeroes the struct httpd, then creates a TCP socket, binds it to
port, calls listen() with a backlog of 5, and registers the accept callback
(httpd.c). Pass ssl_ctx = NULL for plain HTTP, or a configured
WOLFSSL_CTX * for HTTPS (see section 10).
It returns 0 on success or -1 if any socket step fails.
The lifecycle is:
- Bring up the wolfIP stack and configure its IP (DHCP or static).
httpd_init(&httpd, s, port, ssl_ctx).- Register one or more routes with
httpd_register_static_page()and/orhttpd_register_handler(). - Drive the stack: call
wolfIP_poll(s, now_ms)in your main loop forever.
There is no explicit teardown call; individual client connections are closed
automatically by the module on error or after a response. struct httpd holds
its own listen socket, so its lifetime must match the server's.
Routes are matched by exact path (strcmp), up to HTTPD_MAX_URLS (16)
entries. There are two registration calls (src/http/httpd.h):
int httpd_register_static_page(struct httpd *httpd, const char *path,
const char *content);
int httpd_register_handler(struct httpd *httpd, const char *path,
int (*handler)(struct httpd *httpd, struct http_client *hc,
struct http_request *req));- Static page —
contentis a NUL-terminated string the module serves verbatim with status200 OKandContent-Type: text/html. The string is referenced by pointer, not copied, so it must outlive the server. - Handler — for any request to
path, the module calls your function and lets it build the entire response.
Both copy the path into the route table (truncated to HTTP_PATH_LEN, 128) and
return 0, or -1 if all 16 slots are taken. Each successful registration
consumes one slot; there is no public deregistration API. A registered route with
neither a handler nor static content yields 503 Service Unavailable; an
unmatched path yields 404 Not Found (parse_http_request() in httpd.c).
A handler has this exact signature (src/http/httpd.h):
int my_handler(struct httpd *httpd, struct http_client *hc,
struct http_request *req);httpd— the server instance (often unused; cast to(void)).hc— the opaque per-client context. You pass it back to the response helpers; you do not write its fields directly.req— the parsed request (see below). The struct is stack-allocated for the duration of the call, so copy out anything you need to keep.
The parsed request (struct http_request, httpd.h) is fixed-size:
struct http_request {
char method[HTTP_METHOD_LEN]; /* "GET", "POST" (max 8) */
char path[HTTP_PATH_LEN]; /* URL path, percent-decoded (max 128) */
char query[HTTP_QUERY_LEN]; /* raw query string (max 256) */
char headers[HTTP_HEADERS_LEN]; /* last header line seen (max 512) */
char body[HTTP_BODY_LEN]; /* request body (max 1024) */
size_t body_len;
};The return value is propagated by the module: return 0 after you have sent a
response. A negative return from your handler causes the module to close the
client connection (http_recv_cb() treats a negative parse/handler result as a
failure and tears the connection down).
Note.
req->headersholds only the last header line parsed, not the full header block — the parser reuses one buffer. Use it for at most a single expected header; framing headers (Content-Length,Transfer-Encoding) are consumed internally and are not meant to be re-read here.
Only GET and POST reach a route; anything else is rejected before dispatch.
To pull a named argument out of a GET query string or a POST form body, use the
helper (src/http/httpd.h):
int httpd_get_request_arg(struct http_request *req, const char *name,
char *value, size_t value_len);It searches req->query for a GET and req->body for a POST, looking for
name=value pairs separated by &. On a match it copies the value (NUL
terminated) into value and returns 0; it returns -1 if the key is not
found, the method is unsupported, or value_len is too small
(httpd_get_request_arg() in httpd.c).
char user[32];
if (httpd_get_request_arg(req, "user", user, sizeof(user)) == 0) {
/* user now holds the value of ?user=... or user=... */
}The body parser derives the body length from the declared Content-Length and
rejects Transfer-Encoding (chunked) request bodies, as well as a body with no
Content-Length, to avoid request-smuggling ambiguity (see the comment in
parse_http_request()).
The response helpers all take the struct http_client *hc from your handler and
write straight to the socket (plain or TLS). From src/http/httpd.h:
| Function | Purpose |
|---|---|
http_send_response_headers(hc, code, status_text, content_type, content_length) |
Emit the status line + headers. content_length == 0 switches to Transfer-Encoding: chunked. |
http_send_response_body(hc, body, len) |
Send a response body (after fixed-length headers). |
http_send_response_chunk(hc, chunk, len) |
Send one chunk (after chunked headers). |
http_send_response_chunk_end(hc) |
Terminate a chunked response. |
http_send_200_OK(hc) |
Shorthand: 200 headers, text/plain, chunked. |
http_send_500_server_error(hc) |
Shorthand: 500 Internal Server Error. |
http_send_503_service_unavailable(hc) |
Shorthand: 503 Service Unavailable. |
http_send_418_teapot(hc) |
Shorthand: 418 I'm a teapot. |
The common fixed-length pattern is headers then body:
http_send_response_headers(hc, HTTP_STATUS_OK, "OK", "text/html", len);
http_send_response_body(hc, response, len);
return 0;For a response whose size is not known up front, send chunked headers
(content_length == 0), then any number of http_send_response_chunk() calls,
and finish with http_send_response_chunk_end(). Any write failure inside these
helpers closes the client connection automatically.
Status code constants are defined in httpd.h: HTTP_STATUS_OK (200),
HTTP_STATUS_BAD_REQUEST (400), HTTP_STATUS_NOT_FOUND (404),
HTTP_STATUS_TEAPOT (418), HTTP_STATUS_TOO_MANY_REQUESTS (429),
HTTP_STATUS_INTERNAL_SERVER_ERROR (500), HTTP_STATUS_SERVICE_UNAVAILABLE
(503).
A complete dynamic handler that builds an HTML page and returns it, condensed
from src/port/stm32h563/main.c (https_status_handler):
static int status_handler(struct httpd *httpd, struct http_client *hc,
struct http_request *req)
{
char response[512];
int len;
(void)httpd;
/* Build the page (req->method / req->path are available if needed) */
len = snprintf(response, sizeof(response),
"<!DOCTYPE html><html><head><title>wolfIP</title></head>"
"<body><h1>wolfIP Status</h1>"
"<p>You asked for: %s</p></body></html>",
req->path);
http_send_response_headers(hc, HTTP_STATUS_OK, "OK", "text/html", len);
http_send_response_body(hc, response, len);
return 0;
}Wiring it up (the static-page variant is even shorter, from
src/test/test_httpd.c):
struct httpd httpd;
const char homepage[] = "<html><body><h1>Hello, world!</h1></body></html>";
/* Plain HTTP on port 80; pass NULL for the ssl_ctx argument. */
if (httpd_init(&httpd, s, 80, NULL) < 0)
return -1;
httpd_register_static_page(&httpd, "/", homepage); /* serves homepage */
httpd_register_handler(&httpd, "/status", status_handler);
/* Main loop: the server runs entirely inside wolfIP_poll(). */
for (;;) {
uint32_t ms_next = wolfIP_poll(s, now_ms());
/* sleep up to ms_next, service other work, then loop */
}That is the whole server. httpd_register_static_page(&httpd, "/", homepage)
is exactly what test_httpd.c does (over TLS, on port 443).
HTTPS is enabled by passing a configured WOLFSSL_CTX * as the fourth argument
to httpd_init() instead of NULL. The module then:
- calls
wolfSSL_SetIO_wolfIP_CTX(ssl_ctx, ipstack)inhttpd_init()to bind the TLS context to the wolfIP I/O backend, and - for every accepted connection creates a
WOLFSSLwithwolfSSL_new()and attaches it to the client socket withwolfSSL_SetIO_wolfIP()(http_accept_cb()).
After that, every read uses wolfSSL_read() and every write
wolfSSL_write() transparently — the same handlers and response helpers work
for HTTP and HTTPS unchanged. The wolfSSL-over-wolfIP I/O glue
(wolfSSL_SetIO_wolfIP*, wolfSSL_CleanupIO_wolfIP) lives in
src/port/wolfssl_io.c and must be linked in.
Setting up the context is ordinary wolfSSL. From src/test/test_httpd.c:
WOLFSSL_CTX *server_ctx = wolfSSL_CTX_new(wolfTLSv1_2_server_method());
wolfSSL_CTX_use_certificate_buffer(server_ctx, server_der, server_der_len,
SSL_FILETYPE_ASN1);
wolfSSL_CTX_use_PrivateKey_buffer(server_ctx, server_key_der, server_key_der_len,
SSL_FILETYPE_ASN1);
httpd_init(&httpd, s, 443, server_ctx); /* HTTPS on 443 */
httpd_register_static_page(&httpd, "/", homepage);src/port/stm32h563/main.c does the same with wolfTLSv1_3_server_method() and
PEM certificate/key buffers, then registers a dynamic handler. Either TLS
version works; the choice is entirely in how you build the WOLFSSL_CTX.
On the read path, a WOLFSSL_ERROR_WANT_READ from wolfSSL_read() is handled
internally (the callback simply returns and waits for more data), so partial TLS
records do not break the connection.
The module exposes its in-place percent-codec (src/http/httpd.h); the request
parser already uses http_url_decode() on req->path, but the functions are
public if you need them:
/* Returns the decoded length, or a negative error code. */
int http_url_decode(char *buf, size_t len);
int http_url_encode(char *buf, size_t len, size_t max_len);http_url_decode() rewrites %XX escapes in place and returns the new length,
or HTTP_URL_DECODE_ERR_TRUNCATED (-1) / HTTP_URL_DECODE_ERR_BAD_ESCAPE (-2).
http_url_encode() escapes spaces as %20, growing the buffer up to max_len,
and returns the new length or -1 if there is not enough room.
- Server never accepts connections.
httpd_init()returned-1(socket, bind, or listen failed) — check the port is free and the stack has an IP configured beforehttpd_init(). Also confirm you are callingwolfIP_poll()continuously; nothing happens between polls. - All requests get
404. Routes are matched by exact path withstrcmp./statusand/status/are different, and there is no prefix or wildcard matching. Register the exact path the client requests. - A registered route returns
503. The route exists but has neither a handler nor static content (e.g. a handler registered asNULL). - POST body is empty or the request is
400. The parser requires a validContent-Lengthmatching the body length and rejects chunked (Transfer-Encoding) request bodies. The body is also capped atHTTP_BODY_LEN(1024); larger bodies are rejected. - HTTPS connection resets at handshake. Confirm
WOLFSSL_WOLFIPis defined,src/port/wolfssl_io.cis linked, the certificate/key loaded into theWOLFSSL_CTXwithout error, andwolfSSL_Init()was called at startup. - Connections drop under load / large responses truncated. Responses are written immediately with no buffering; a congested or full TX window makes the send fail and the module closes the connection. Keep responses within a TX window, or send incrementally with the chunked helpers.
- Fifth concurrent client is refused. Only
HTTPD_MAX_CLIENTS(4) client slots exist; a new accept with no free slot is dropped.