| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
libhttpserver is a C++20 library for building high-performance RESTful HTTP servers on top of GNU libmicrohttpd. v2.0 is a lambda-first redesign: a working server is ten lines, handlers are std::functions, responses are value-typed, and every public method is thread-safe by contract.
Features:
This README is a guided reference: it walks the v2.0 API surface section by section. It is comprehensive but not exhaustive — the headers under src/httpserver/ are the source of truth, and the examples/ tree contains a runnable demonstration of every feature.
For a visual mental model, see docs/architecture/: a class, relationship & filesystem map and a request lifecycle & routing flow diagram (Mermaid inline on GitHub, plus richer self-contained HTML pages).
The shortest possible server looks like this:
// Copyright 2026 Sebastiano Merlino
// libhttpserver hello-world example — the lambda form.
// Compiles in ten lines including main(), with no http_resource subclass
// and no raw-pointer ownership. Production code typically qualifies names
// explicitly; the `using namespace` here is a one-off so this file can
// document the shortest possible end-to-end demo. See shared_state.cpp
// for the class-based pattern that is appropriate when handlers must
// share mutable state.
#include <httpserver.hpp>
using namespace httpserver; // NOLINT(build/namespaces) - keep the demo at <=10 LOC
int main() {
webserver ws{create_webserver(8080)};
ws.on_get("/hello", [](const http_request&) {
return http_response::string("Hello, World!");
});
ws.start(true);
}The block above is reproduced byte-for-byte from examples/hello_world.cpp; a CI gate (scripts/check-readme.sh) enforces the byte-for-byte equality.
libhttpserver is meant to constitute an easy system to build HTTP servers in the REST fashion. It is built on top of libmicrohttpd and, like its substrate, it is a daemon library. The mission is to expose every useful HTTP capability through a simple, modern C++ API so that application code can focus on business logic rather than on the mechanics of HTTP request handling.
Design. libhttpserver is lambda-first. The shortest server is ten lines and uses no inheritance; handlers are std::function<http_response(const http_request&)>; responses are value-typed with seven factories and fluent with_* chaining; ownership of resource-form handlers is expressed in std::unique_ptr and std::shared_ptr. The class form (http_resource) is the right shape when several methods on one path share mutable state.
Two contracts are load-bearing — and both are stated explicitly in this README rather than left implicit:
libhttpserver decodes form bodies automatically: application/x-www-form-urlencoded and multipart/form-data are parsed into the request's argument map. Files attached to multipart uploads are exposed through http_request::get_files().
All functions are reentrant and thread-safe unless explicitly stated otherwise. Clients can also specify resource limits on overall connection count, per-IP connection count, and per-connection memory to avoid resource exhaustion.
libhttpserver can be used without any dependencies aside from libmicrohttpd.
The minimum versions required are:
On RHEL 9 (and derivatives), the stock GCC 11 is too old for some C++20 library features the build relies on; install the gcc-toolset-14 package and source /opt/rh/gcc-toolset-14/enable before configuring.
Additionally, for MinGW on Windows you will need:
For versions before 0.18.0, on MinGW you will need:
The test cases use libcurl but you don't need it to compile the library itself.
Please refer to the readme file for your particular distribution if there is one for important notes.
| Platform | Toolchain | Notes |
|---|---|---|
| Debian 13 (trixie) | GCC 14.2 | Out-of-the-box |
| RHEL 9 | gcc-toolset-14 | Stock GCC 11 is too old; install the Red Hat toolset overlay |
| RHEL 10 | GCC 14 | Out-of-the-box |
| FreeBSD 14.x | base Clang 18+ | Out-of-the-box |
| macOS | Homebrew GCC 15+ or current Apple Clang | Out-of-the-box |
| vcpkg / Conan | GCC 13+ / Clang 16+ | Out-of-the-box |
libhttpserver uses the standard autotools workflow. The usual build process is:
./bootstrap
mkdir build
cd build
../configure
make
sudo make installmake check runs the test suite (unit + integration). make examples builds every program under examples/. make dist produces a portable source tarball.
ABI / packaging. SOVERSION is 2. Distributions package libhttpserver2, parallel-installable with prior major versions so applications can link the major they need side by side. There is no inline namespace and no symbol-versioning script.
A complete list of parameters can be obtained by running ./configure --help. The libhttpserver-specific options are listed below (the canonical configure options are also supported):
Build-time feature flags (auto-detected, can be forced):
See Feature availability for how each of these flags affects the runtime API.
MSYS2 provides multiple shell environments with different purposes. Understanding which shell to use is important:
| Shell | Host triplet | Runtime dependency | Use case |
|---|---|---|---|
| MinGW64 | x86_64-w64-mingw32 | Native Windows | Recommended for native Windows apps |
| MSYS | x86_64-pc-msys | msys-2.0.dll | POSIX-style apps, build tools |
Recommended: use the MinGW64 shell for building libhttpserver to produce native Windows binaries without additional runtime dependencies.
pacman -S --needed mingw-w64-x86_64-{gcc,libtool,make,pkg-config,doxygen,gnutls,curl} autotools./bootstrap
mkdir build && cd build
../configure --disable-fastopen
make
make check # run testsImportant: the --disable-fastopen flag is required on Windows as TCP_FASTOPEN is not supported there.
Building from the MSYS shell also works, but the resulting binaries will depend on msys-2.0.dll. The configure script will emit a warning when building in this environment:
configure: WARNING: Building from MSYS environment. Binaries will depend on msys-2.0.dll.
Consider switching to the MinGW64 shell for native Windows binaries.
When building with GCC-based toolchains (MSYS2/MinGW, Cygwin), the following library files are generated:
| File | Purpose |
|---|---|
| libhttpserver.a | Static library archive |
| libhttpserver.dll | Shared library (DLL) |
| libhttpserver.dll.a | Import library for linking against the DLL |
| libhttpserver.la | Libtool archive (used by libtool during linking) |
Note about .lib files: the .dll.a format is the import library format used by GCC toolchains. If you're looking for .lib files, those are the MSVC (Microsoft Visual C++) import library format and are only generated when building with the MSVC toolchain. The .dll.a file serves the same purpose as .lib but for GCC-based compilers.
Linking against libhttpserver:
Using pkg-config (recommended):
g++ myapp.cpp $(pkg-config --cflags --libs libhttpserver) -o myappManual linking:
g++ myapp.cpp -I/mingw64/include -L/mingw64/lib -lhttpserver -lmicrohttpd -o myappThe Tl;dr at the top of this README contains the entire program. Walking through it:
To test the example, you can run the following from a terminal:
curl -XGET -v http://localhost:8080/helloSee examples/hello_world.cpp and examples/hello_with_get_arg.cpp for the complete sources.
Lambdas suffice when each HTTP method is independent. When several methods on one path share state — a counter, a cache, a mutex — derive from http_resource and register the subclass once:
class counter : public httpserver::http_resource {
public:
httpserver::http_response render_get(const httpserver::http_request&) override {
std::lock_guard lock{m_};
return httpserver::http_response::string(std::to_string(n_));
}
httpserver::http_response render_post(const httpserver::http_request&) override {
std::lock_guard lock{m_};
++n_;
return httpserver::http_response::string(std::to_string(n_));
}
private:
std::mutex m_;
int n_ = 0;
};
// ...
ws.register_path("/count", std::make_unique<counter>());The virtual hooks are render_get, render_post, render_put, render_delete, render_head, render_options, render_patch, render_connect, and render_trace — all lowercase, all returning http_response by value. The webserver takes ownership of the resource via std::unique_ptr (or a std::shared_ptr overload — see Routing).
See examples/shared_state.cpp for the canonical example.
This is the cast of types you will work with. Each is detailed further on in its own section.
Creating a webserver with a standard configuration is the one-liner shown in the Tl;dr:
webserver ws{create_webserver(8080)};create_webserver is a fluent builder: every setter returns *this so calls chain. The webserver constructor is explicit: callers must direct-initialise (webserver ws{cw};) rather than rely on implicit conversion. The builder validates eagerly where the input domain is well-defined — port rejects values outside [0, 65535], every int setter rejects negatives, etc. — and raises std::invalid_argument at the setter. Feature-gated settings (use_ssl, basic_auth, digest_auth, WebSocket registration) are validated by the webserver constructor, not the setter; so a builder configured for an unsupported feature throws feature_unavailable from webserver(create_webserver) rather than from the chained call.
A complete chained example:
webserver ws{create_webserver(8080)
.max_threads(4)
.max_connections(1024)
.connection_timeout(30)
.start_method(http::http_utils::INTERNAL_SELECT)
.turbo()
.suppress_date_header()};The rest of this section is a reference of every group of options.
The feature toggles all use positive polarity: pass false to disable (basic_auth(false), use_ssl(false), etc.).
These are no-ops unless the build has GnuTLS support. On a GnuTLS-off build, webserver(create_webserver{}.use_ssl(true)) throws feature_unavailable.
A worked example:
auto cfg = httpserver::create_webserver(8080)
.not_found_handler([](const httpserver::http_request&) {
return httpserver::http_response::string("nope").with_status(404);
})
.method_not_allowed_handler([](const httpserver::http_request&) {
return httpserver::http_response::empty().with_status(405);
})
.internal_error_handler([](const httpserver::http_request&, std::string_view what) {
// CWE-209: 'what' may contain file paths, SQL fragments, or
// attacker-influenced input. Log it internally; do NOT echo
// it to the HTTP client.
(void)what; // e.g. logger->error(what);
return httpserver::http_response::string("Internal Server Error").with_status(500);
});
httpserver::webserver ws{cfg};These dovetail with the runtime methods covered under Daemon introspection and external event loops. The default_policy selects which list is the exception list — i.e. what happens to an address on neither list:
Once constructed, the webserver instance exposes:
Registration verbs (covered in detail under Routing): on_get, on_post, on_put, on_delete, on_patch, on_options, on_head, route(...), register_path(...), register_prefix(...), register_ws_resource(...), and the matching unregister_* forms.
The http_resource class represents a logical collection of HTTP methods that will be associated with a URL when registered on the webserver. The class is designed for extension. When the webserver matches a request against a resource, the method corresponding to the request's HTTP verb is called on the resource.
The http_resource class contains the following extensible methods (also called handlers or render methods) — every one returns http_response by value:
These methods are all virtual; override only the verbs your resource supports. Unhandled verbs fall through to render(); if the resource also does not override render(), the dispatch returns the configured 405 response.
By default, every HTTP verb falls through to render(). To narrow a resource to a specific subset of methods, use the set_allowing / disallow_all API:
class read_only : public httpserver::http_resource {
public:
read_only() {
disallow_all();
set_allowing("GET", true);
set_allowing("HEAD", true);
}
httpserver::http_response render_get(const httpserver::http_request&) override {
return httpserver::http_response::string("ok");
}
};Requests for disallowed verbs are short-circuited by the dispatcher and land in the method_not_allowed_handler (HTTP 405) without calling render_*. See examples/allowing_disallowing_methods.cpp for a worked example.
Resources are passed to the webserver via std::unique_ptr (the webserver takes exclusive ownership) or std::shared_ptr (caller and webserver share ownership). Ownership is always expressed through a smart pointer.
The webserver exposes three families of registration entry points. They are all interoperable: within one server, some paths can be lambdas and others can be http_resource subclasses.
on_get, on_post, on_put, on_delete, on_patch, on_options, on_head — each takes an exact path and a std::function<http_response(const http_request&)>:
ws.on_get("/hello", [](const http_request&) {
return http_response::string("Hello, World!");
});
ws.on_post("/items", [](const http_request& req) {
return http_response::string("created: " + std::string{req.get_arg("name")})
.with_status(201);
});Re-registering the same (method, path) pair throws. The seven shortcuts cover the seven verbs with first-class handler functions; CONNECT and TRACE go through route() (below).
route() is the primary escape hatch when the HTTP method is known only at runtime (e.g. loaded from config, selected from a dispatch table). The single-method form:
route(http_method m, "/info", handler)takes a runtime http_method value and is the canonical form for config-driven or table-driven registration (PRD-HDL-REQ-006).
route(method_set methods, "/info", handler) additionally allows registering a handler under several methods in a single critical section — either every slot is registered, or none of them are:
ws.route(http_method::GET | http_method::HEAD, "/info",
[](const http_request&) {
return http_response::string("info");
});A method_set is the bitwise-or of http_method values. This is the only entry point through which CONNECT and TRACE are reachable as lambdas: route(http_method::CONNECT, "/proxy", handler).
For http_resource subclasses (the class form):
ws.register_path("/count", std::make_unique<counter>()); // exact match
ws.register_prefix("/static", std::make_unique<files>()); // subtree matchBoth methods have std::shared_ptr<http_resource> overloads for the case where you need to retain a reference to the resource:
auto cnt = std::make_shared<counter>();
ws.register_path("/count", cnt);
// cnt is still usable from outside the webserver.Parameterised paths. Brace syntax captures path segments:
ws.register_path("/users/{id}", std::make_unique<user_resource>());The captured value is available inside the handler via http_request::get_arg("id").
Per-segment regex constraints. Add a regex after a pipe to constrain a segment:
ws.register_path("/users/{id|[0-9]+}", std::make_unique<user_resource>());Only requests where the id segment matches [0-9]+ will match this registration.
Regex validation is on by default; disable with regex_checking(false) on the builder if you need pure literal-path semantics.
Unregistration. unregister_path(path) and unregister_prefix(path) remove an exact-match or prefix-match registration respectively. unregister_resource(path) is a kind-agnostic convenience that atomically clears whichever kind (exact or prefix) is registered at path — use it when the caller does not track how the resource was registered.
http_request is read-only inside a handler. The accessors are designed around std::string_view so reading headers and arguments does not allocate.
| Accessor | Returns | Notes |
|---|---|---|
| get_path() | std::string_view | The decoded path |
| get_path_pieces() | const std::vector<...>& | Split path components |
| get_method() | httpserver::http_method | Strongly-typed enum; see http_method.hpp |
| get_version() | std::string_view | "HTTP/1.1", "HTTP/2", … |
| get_querystring() | std::string_view | Raw query string (no decoding) |
| get_connection_id() | uintptr-style id | Stable identifier for the underlying connection |
| get_requestor() | std::string_view | Connecting peer's IP address (text form) |
| get_requestor_port() | unsigned short | Connecting peer's port |
| Accessor | Returns | Notes |
|---|---|---|
| get_headers() | const map& | All request headers |
| get_header(name) | std::string_view | First value for a named header |
| get_args() | const map& | All query / form arguments |
| get_arg(name) | std::string_view | First value for a query / form arg |
| get_arg_flat(name) | std::string_view | Alias for get_arg; explicit "first value only" form |
| get_cookies() | const map& | All cookies |
| get_cookie(name) | std::string_view | First value for a named cookie |
| get_footers() | const map& | Chunked trailers |
| Accessor | Returns | Notes |
|---|---|---|
| get_files() | const map& | Uploaded files keyed by form field name |
| get_content() | std::string_view | The raw request body |
| get_content_size_limit() | size_t | Configured cap, in bytes |
See examples/file_upload.cpp and examples/file_upload_with_callback.cpp for working programs.
| Accessor | Returns | Notes |
|---|---|---|
| get_user() | std::string_view | Basic-auth user; empty when HAVE_BAUTH is off |
| get_pass() | std::string_view | Basic-auth password; empty when HAVE_BAUTH is off |
| get_digested_user() | std::string_view | Digest-auth user; empty when HAVE_DAUTH is off |
| check_digest_auth(realm, password, nonce_timeout, signal_stale, algo) | digest_auth_result | Validates a digest auth response against a plaintext password |
| check_digest_auth_digest(realm, ha1, ...) | digest_auth_result | Same as above but against a pre-computed HA1 hash |
Security note. Basic auth (get_user/get_pass) transmits credentials as Base64 — effectively cleartext. Digest auth (get_digested_user) avoids transmitting the password but is still vulnerable to man-in-the-middle attacks without TLS. Both are only safe when the server is configured with TLS (HAVE_GNUTLS, .use_ssl(true)). See Feature availability and examples/basic_authentication.cpp.
digest_auth_result is a strongly-typed enum:
When the build was compiled with GnuTLS (HAVE_GNUTLS) and the client presented an X.509 certificate during the TLS handshake, these accessors return the certificate details. On a non-TLS build, or when the client did not present a certificate, they return empty / -1 / false:
| Accessor | Returns | Notes |
|---|---|---|
| has_client_certificate() | bool | True if a client cert was presented |
| is_client_cert_verified() | bool | True if the chain validated against the configured trust store |
| get_client_cert_dn() | std::string_view | Subject Distinguished Name |
| get_client_cert_issuer_dn() | std::string_view | Issuer Distinguished Name |
| get_client_cert_cn() | std::string_view | Subject Common Name |
| get_client_cert_fingerprint_sha256() | std::string_view | Hex-encoded SHA-256 fingerprint |
| get_client_cert_not_before() | time_t | Validity-start timestamp; -1 if unavailable |
| get_client_cert_not_after() | time_t | Validity-end timestamp; -1 if unavailable |
See examples/client_cert_auth.cpp for a worked mTLS example.
Every string_view returned by http_request is valid for the duration of the handler invocation and no longer. Copy what you need to keep (e.g. into a std::string); do not hand a view to a deferred callback. The references returned by get_headers(), get_args(), get_path_pieces(), get_files(), and get_cookies() follow the same rule. http_request is single-threaded per request: sharing one http_request across threads is undefined.
http_method (declared in http_method.hpp) covers the canonical HTTP methods. method_set is a bitset used by atomic multi-method registration on route() (see Routing).
http_response is a value type — move-only, returned by value, never shared_ptr-wrapped. There is no class hierarchy of body subclasses; the body shape is a runtime detail of one type. Build a response with one of the seven factories described below and decorate it with the fluent with_* mutators.
| Factory | Body shape | Use when |
|---|---|---|
| http_response::string(body, [status, content_type]) | In-memory string (small bodies live inline via SBO) | The body is already in memory |
| http_response::file(path) | Stream a file from disk | The body is a static or generated file on disk |
| http_response::iovec(entries) | Scatter-gather over a vector of iovec_entry (zero-copy) | The body is assembled from several existing buffers |
| http_response::pipe(fd) | Stream from a pipe / FIFO | The body is being produced by another process or thread |
| http_response::empty([status]) | Empty body | 204 No Content, redirects, HEAD responses |
| http_response::deferred(producer, [closure, content_type]) | Body produced incrementally by a callback | The body cannot be materialised up-front (long-poll, streaming) |
| http_response::unauthorized(realm, [status, content_type, algorithm]) | 401 with the proper WWW-Authenticate header | Reject a request that lacks valid credentials |
iovec_entry is the element type of the iovec() vector:
struct iovec_entry {
const void* base; // borrowed for the response's lifetime
size_t length;
};The base pointer is borrowed: the caller must keep the underlying storage alive until the response has been fully written to the wire. For owning scatter-gather, copy your strings into a std::vector<std::string> that the response captures by value.
Every http_response exposes with_status, with_header, with_footer, and with_cookie. These return *this (by reference on lvalues, by rvalue-reference on rvalues), so calls can chain:
return httpserver::http_response::string("hi")
.with_header("X-Trace-Id", trace_id)
.with_status(201);The fluent helpers are intentionally narrow — every other mutation (content type, body data) is set at the factory call. This keeps the "build it, return it, done" idiom uniform across response shapes.
There is no throw-as-status idiom. To return a 404 from a handler, build it explicitly:
if (!found) {
return httpserver::http_response::empty().with_status(404);
}Or with a body:
return httpserver::http_response::string("user not found").with_status(404);For 401 responses, the dedicated http_response::unauthorized factory sets the right WWW-Authenticate header for you:
return httpserver::http_response::unauthorized("MyRealm");See examples/setting_headers.cpp, examples/iovec_response_example.cpp, examples/minimal_file_response.cpp, examples/pipe_response_example.cpp, examples/empty_response_example.cpp, examples/minimal_deferred.cpp, and examples/binary_buffer_response.cpp for working programs covering every response shape.
libhttpserver supports both IPv4 and IPv6 and manages them transparently. The only requirement for IPv6 is that it is enabled on the underlying server — set use_ipv6(true) on create_webserver (or use_dual_stack(true) for both stacks on the same socket).
You can populate the deny list and the allow list (with individual IPs or wildcard ranges) at runtime using these methods on webserver:
The IP string format can represent both IPv4 and IPv6. Addresses are normalised internally by the webserver to a common representation, so any valid IPv4 or IPv6 textual representation works. To express a range, omit the octet you want to wildcard and specify '*' in its place.
Examples of valid IPs include:
By default (ACCEPT policy) the deny list is the exception list: use deny_ip to refuse specific addresses and admit everyone else. To invert this into an allow list — refuse everyone except specific addresses — set the default policy to REJECT and populate the allow list with allow_ip:
webserver ws{create_webserver(8080)
.default_policy(http::http_utils::REJECT)};
ws.allow_ip("192.168.0.*"); // permits 192.168.0.0/24, refuses everything elseSee examples/minimal_ip_access_control.cpp for a worked example of both modes.
libhttpserver supports four authentication mechanisms, all of which can be combined freely (e.g. mTLS and digest auth in a two-factor configuration):
class user_pass_resource : public httpserver::http_resource {
public:
httpserver::http_response render_get(const httpserver::http_request& req) override {
if (req.get_user() != "myuser" || req.get_pass() != "mypass") {
return httpserver::http_response::unauthorized("test@example.com");
}
return httpserver::http_response::string(
std::string{req.get_user()} + " " + std::string{req.get_pass()});
}
};
int main() {
httpserver::webserver ws{httpserver::create_webserver(8080)};
ws.register_path("/hello", std::make_unique<user_pass_resource>());
ws.start(true);
}To test:
curl -XGET -v -u myuser:mypass http://localhost:8080/helloYou will get back the user and password you passed. Try passing wrong credentials to see the failure response. See examples/basic_authentication.cpp for the full source.
check_digest_auth returns a digest_auth_result enum with fine-grained status codes (see Request for the full list). The skeleton of a digest-protected handler is:
httpserver::http_response render_get(const httpserver::http_request& req) override {
if (req.get_digested_user().empty()) {
return httpserver::http_response::unauthorized(
"test@example.com", 401, "text/plain",
httpserver::http::http_utils::digest_algorithm::SHA_256);
}
auto result = req.check_digest_auth(
"test@example.com", "mypass", 300, 0,
httpserver::http::http_utils::digest_algorithm::SHA_256);
if (result == httpserver::http::http_utils::digest_auth_result::NONCE_STALE) {
return httpserver::http_response::unauthorized(
"test@example.com", 401, "text/plain",
httpserver::http::http_utils::digest_algorithm::SHA_256)
.with_header("Stale", "true");
}
if (result != httpserver::http::http_utils::digest_auth_result::OK) {
return httpserver::http_response::unauthorized(
"test@example.com", 401, "text/plain",
httpserver::http::http_utils::digest_algorithm::SHA_256);
}
return httpserver::http_response::string("SUCCESS");
}To test:
curl -XGET -v --digest --user myuser:mypass localhost:8080/helloYou'll get SUCCESS in response; observe the response message in detail to see the full digest handshake. See examples/digest_authentication.cpp.
The per-resource pattern above duplicates auth logic across every resource. Centralized authentication lets you define the policy once and have it applied automatically to every request:
// auth runs once per request before any render_*; return std::nullopt to allow
auto auth = [](const httpserver::http_request& req)
-> std::optional<httpserver::http_response> {
if (req.get_user() != "admin" || req.get_pass() != "secret") {
return httpserver::http_response::unauthorized("MyRealm");
}
return std::nullopt;
};
httpserver::webserver ws{httpserver::create_webserver(8080)
.auth_handler(auth)
.auth_skip_paths({"/health", "/public/*"})};
ws.register_path("/api", std::make_unique<api_resource>());
ws.register_path("/health", std::make_unique<health_resource>());
ws.start(true);The auth_handler callback runs for every request before the resource's render method. It receives the http_request and can:
auth_skip_paths accepts a vector of paths that should bypass the handler:
To test (without auth — returns 401):
curl -v http://localhost:8080/apiWith valid auth — returns 200:
curl -u admin:secret http://localhost:8080/apiHealth endpoint (skip path) — works without auth:
curl http://localhost:8080/healthSee examples/centralized_authentication.cpp.
Client-certificate authentication (mutual TLS, mTLS) provides strong authentication by requiring the client to present an X.509 certificate during the TLS handshake. To enable mTLS:
class secure : public httpserver::http_resource {
public:
httpserver::http_response render_get(const httpserver::http_request& req) override {
if (!req.has_client_certificate()) {
return httpserver::http_response::string("client certificate required")
.with_status(401);
}
if (!req.is_client_cert_verified()) {
return httpserver::http_response::string("certificate not verified")
.with_status(403);
}
std::string cn{req.get_client_cert_cn()};
return httpserver::http_response::string("Welcome, " + cn + "!");
}
};
int main() {
httpserver::webserver ws{httpserver::create_webserver(8443)
.use_ssl(true)
.https_mem_key("server_key.pem")
.https_mem_cert("server_cert.pem")
.https_mem_trust("ca_cert.pem")};
ws.register_path("/secure", std::make_unique<secure>());
ws.start(true);
}Available client-certificate methods (require GnuTLS support):
Test with a client certificate:
curl -k --cert client_cert.pem --key client_key.pem https://localhost:8443/secureWithout a client certificate (will be rejected):
curl -k https://localhost:8443/secureSee examples/client_cert_auth.cpp.
SNI lets a server host multiple TLS certificates on a single IP address. The client indicates which hostname it's connecting to during the TLS handshake, and the server can select the appropriate certificate. Configure an SNI callback that returns the (cert_pem, key_pem) pair for each server name:
std::map<std::string, std::pair<std::string, std::string>> certs;
auto sni = [](const std::string& server_name)
-> std::pair<std::string, std::string> {
auto it = certs.find(server_name);
if (it != certs.end()) return it->second;
return {"", ""}; // fall back to the default certificate
};
httpserver::webserver ws{httpserver::create_webserver(443)
.use_ssl(true)
.https_mem_key("default_key.pem")
.https_mem_cert("default_cert.pem")
.sni_callback(sni)};SNI support requires libmicrohttpd 1.0.0 or later compiled with GnuTLS.
For environments where X.509 certificates are impractical, libhttpserver also supports TLS-PSK via cred_type(http::http_utils::PSK) plus a psk_cred_handler callback returning the PSK for a given identity. See examples/minimal_https_psk.cpp.
libhttpserver provides WebSocket support when libmicrohttpd is built with WebSocket functionality. To use WebSockets, derive from the websocket_handler class and implement on_message.
The websocket_handler class provides the following virtual methods:
The websocket_session class provides methods to interact with the client:
WebSocket handlers are registered with register_ws_resource, which takes ownership of a websocket_handler subclass via std::unique_ptr (or shares it via std::shared_ptr):
class echo : public httpserver::websocket_handler {
public:
void on_message(httpserver::websocket_session& session,
std::string_view msg) override {
session.send_text("Echo: " + std::string{msg});
}
};
int main() {
httpserver::webserver ws{httpserver::create_webserver(8080)};
ws.register_ws_resource("/echo", std::make_unique<echo>());
// or, with shared ownership:
auto handler = std::make_shared<echo>();
ws.register_ws_resource("/echo2", handler);
ws.start(true);
}On a build with HAVE_WEBSOCKET disabled — for example, when the system libmicrohttpd was built without WebSocket support — register_ws_resource throws feature_unavailable. See Feature availability for how to detect this at runtime without preprocessor gates, and examples/websocket_echo.cpp for a worked example.
libhttpserver exposes several methods for integrating with external event loops and for querying daemon state at runtime.
When using the server without internal threading (e.g. with listen_socket(false) or with start_method(EXTERNAL_SELECT)), you can drive the event loop yourself:
A small example combining everything:
// Bind to an OS-chosen port, print the actual one, drive the loop
// externally, then drain on shutdown.
webserver ws{create_webserver(0).start_method(http::http_utils::EXTERNAL_SELECT)};
ws.start(false);
std::cout << "listening on port " << ws.get_bound_port() << '\n';
while (running) {
ws.run_wait(1000);
}
ws.quiesce();
ws.stop_and_wait();See examples/daemon_info.cpp and examples/external_event_loop.cpp for runnable demonstrations.
libhttpserver provides a set of constants and helpers to help you build your HTTP server. The full list of named constants (status codes, common methods, common content types, named algorithms) lives in src/httpserver/http_utils.hpp — it would be redundant to enumerate it here.
The following utility functions are available on http::http_utils:
The hook bus is the single extension surface for observing or short-circuiting the request lifecycle. It replaces v1's single-slot patchwork (one log_access callback, one not_found_handler, one method_not_allowed_handler, one internal_error_handler, one auth_handler) with eleven distinct phases spanning connection, request, routing, handler, and response. Each phase accepts multiple subscribers and is observable both server-wide (via webserver::add_hook) and, for the five post-route-resolution phases, per-route (via http_resource::add_hook). The contract is captured in DR-012 and specs/architecture/04-components/hooks.md.
Each call returns a move-only hook_handle. The handle's destructor removes the registration; hook_handle::detach() disarms the destructor so the registration persists for the webserver's lifetime.
| Phase | Fires at | Short-circuit | Per-route eligible |
|---|---|---|---|
| connection_opened | New TCP / TLS connection accepted by MHD | No | No |
| accept_decision | After the default-policy / deny-list / allow-list verdict; the connection has been accepted or denied | No | No |
| request_received | Request line and headers parsed, body not yet consumed | Yes (hook_action) | No |
| body_chunk | Each upload-body chunk delivered by MHD | Yes (hook_action) | No |
| route_resolved | After URL → resource resolution; carries the matched route or "no match" | No | No |
| before_handler | After route resolution and method check, immediately before the handler runs | Yes (hook_action) | Yes |
| handler_exception | Exception escapes the handler, before internal_error_handler is consulted | Yes (hook_action) | Yes |
| after_handler | Handler returned a response, before it is queued on the wire (mutation point) | Yes (hook_action) | Yes |
| response_sent | Response handed to MHD_queue_response; carries status, bytes_queued, elapsed | No | Yes |
| request_completed | Request lifecycle finished (success or failure); last hook to fire per request | No | Yes |
| connection_closed | Connection torn down (peer close or server close) | No | No |
Phases marked "Short-circuit" return a hook_action: hook_action::pass() lets the chain continue; hook_action::respond_with(response) aborts the chain at that position. The wrapped response is sent on the wire in place of any handler output. Subsequent hooks in the same phase are not invoked. Observation-only phases (connection_opened, accept_decision, connection_closed, route_resolved, response_sent, request_completed) ignore the return; the chain always runs to completion.
http_resource::add_hook(phase, fn) accepts only the five post-route-resolution phases: before_handler, handler_exception, after_handler, response_sent, request_completed. Per-route hooks fire after the server-wide chain at the same phase, and only if that server-wide chain did not short-circuit. Passing any other phase throws std::invalid_argument naming the rejected phase. See examples/per_route_auth.cpp for a worked example.
Each of the v1 single-slot setters is an alias for an add_hook call at the corresponding phase. The aliases survive for ergonomic call sites; new code can use either form.
| v1 setter | Equivalent add_hook call |
|---|---|
| log_access(fn) | ws.add_hook(hook_phase::response_sent, fn) |
| not_found_handler(fn) | ws.add_hook(hook_phase::route_resolved, fn) |
| method_not_allowed_handler(fn) | ws.add_hook(hook_phase::before_handler, fn) |
| internal_error_handler(fn) | ws.add_hook(hook_phase::handler_exception, fn) |
| auth_handler(fn) | ws.add_hook(hook_phase::before_handler, fn) |
The aliases install observation-stub hooks under the same dispatch plumbing, so the on-the-wire behaviour is identical regardless of which form the caller used.
Distilled from specs/architecture/05-cross-cutting.md §5.1 and DR-008 (specs/architecture/11-decisions/DR-008.md):
Distilled from specs/architecture/05-cross-cutting.md §5.2 and DR-009 (specs/architecture/11-decisions/DR-009.md):
Install custom error handlers on the builder:
auto cfg = httpserver::create_webserver(8080)
.not_found_handler([](const httpserver::http_request&) {
return httpserver::http_response::string("nope").with_status(404);
})
.method_not_allowed_handler([](const httpserver::http_request&) {
return httpserver::http_response::empty().with_status(405);
})
.internal_error_handler([](const httpserver::http_request&, std::string_view what) {
// CWE-209: 'what' may contain file paths, SQL fragments, or
// attacker-influenced input. Log it internally; do NOT echo
// it to the HTTP client.
(void)what; // e.g. logger->error(what);
return httpserver::http_response::string("Internal Server Error").with_status(500);
});
httpserver::webserver ws{cfg};See examples/custom_error.cpp for a worked example. Note that the snippet above — which echoes what verbatim to the wire — is illustrative of the explicit-handler case; the default (no internal_error_handler configured) path now sends a fixed body, and only the log_error callback receives the verbatim message. To restore the v1 verbose-body default for development, chain .expose_exception_messages(true) on the builder (see Custom error handlers).
Several capabilities are gated by build-time flags. v2.0 makes the gating visible at the API level so application code does not need preprocessor guards on HAVE_* macros.
| Build flag | When disabled | Public-API behavior |
|---|---|---|
| HAVE_BAUTH | Basic-auth disabled | get_user, get_pass return empty string_view; features().basic_auth == false; create_webserver::basic_auth(true) throws feature_unavailable at webserver construction |
| HAVE_DAUTH | Digest-auth disabled | get_digested_user returns empty; check_digest_auth returns a sentinel result; features().digest_auth == false; create_webserver::digest_auth(true) throws feature_unavailable |
| HAVE_GNUTLS | TLS disabled | All get_client_cert_* accessors return empty / -1 / false; features().tls == false; create_webserver::use_ssl(true) throws feature_unavailable |
| HAVE_WEBSOCKET | WebSocket disabled | register_ws_resource throws feature_unavailable; features().websocket == false |
webserver::features() returns a small struct of four bools — one per flag — so callers can branch without preprocessor help:
if (ws.features().tls) {
// safe to read client-certificate accessors on this build
}
if (ws.features().websocket) {
ws.register_ws_resource("/sock", std::make_unique<my_socket>());
}Derives from std::runtime_error. Its what() names both the disabled feature and the build flag that gates it, so log lines pinpoint which flag a deployment is missing. Catch it where you call a feature-gated method:
try {
ws.register_ws_resource("/sock", std::make_unique<my_socket>());
} catch (const httpserver::feature_unavailable& e) {
std::cerr << "websocket support is not available: " << e.what() << '\n';
}Deny lists, IP-allow handling, and similar features that do not depend on external libraries are always available: webserver::deny_ip(addr) / webserver::remove_denied_ip(addr) and webserver::allow_ip(addr) / webserver::remove_allowed_ip(addr) install and clear per-server access rules at runtime regardless of build flags.
If you're porting an application from the v1 line, the rename / removed / added cheat sheet — every API surface that moved — lives in RELEASE_NOTES.md. The packaging is parallel-installable across major versions, so applications can link the major they need side by side while porting.
Every example is a standalone program under examples/. A fully grouped index — with one-line descriptions of every binary — lives in examples/README.md. The summary below points to the canonical example for each topic covered in this manual.
This manual is for libhttpserver, a C++ library for creating an embedded RESTful HTTP server (and more).
Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.3 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. A copy of the license is included in the file LICENSE.
The library itself is distributed under the GNU Lesser General Public License v2.1 or later — see COPYING.LESSER for the full text.
libhttpserver builds on the work of the GNU libmicrohttpd developers and the many contributors to libhttpserver itself (see the project's git history for the complete list). Particular thanks to everyone who has filed issues, sent patches, and stress-tested the v2.0 redesign.
If libhttpserver is useful to you, consider sponsoring continued maintenance: https://github.com/sponsors/etr.
| Back | FazBrowse Home | New Git URL |