HTTP Server

Http::Server provides a multi-threaded HTTP/HTTPS web server with routing, static file serving, and dynamic content generation. It uses a thread pool to handle concurrent requests efficiently.

Http::Server is designed to be:

  • fast — multi-threaded request handling
  • flexible — route mapping and handlers
  • complete — static files, dynamic content, redirects
  • secure — HTTPS support via Https::Server

Basic usage

Simple server

// libjoin.
#include <join/httpserver.hpp>

using namespace join;

// Create server with 4 worker threads
Http::Server server(4);

// Serve static files from /var/www
server.baseLocation("/var/www");
server.addDocumentRoot("/*", "*");

// Start server on port 8080
Http::Endpoint endpoint(Http::v4(), 8080);
server.create(endpoint);

// The server runs until close() is called:
// always call close() before the server is destroyed (see "Close server gracefully")

HTTPS server

// Create HTTPS server
Https::Server server(4);

// Set certificate and key
server.setCertificate("/etc/ssl/cert.pem", "/etc/ssl/key.pem");

// Start on port 443
Https::Endpoint endpoint(Https::v4(), 443);
server.create(endpoint);

Construction

// Default: uses hardware concurrency
Http::Server server;

// Specify worker threads
Http::Server server(8);

// HTTPS server
Https::Server secureServer(4);

Routes

Every route is made of a directory and a file name, both matched with fnmatch patterns. A request path is split at its last /: /api/users gives the directory /api/ and the file name users.

  • Directories always end with a /: use "/api/", not "/api"
  • A directory pattern only matches that exact directory: use "/images/*" to also match its subdirectories
  • Routes are checked in registration order and the first match wins: register catch-all routes such as addDocumentRoot("/*", "*") last
// /api/users
server.addExecute(HttpMethod::Get, "/api/", "users", handler);

// any .png file in /images/ and its subdirectories
server.addDocumentRoot("/images/*", "*.png");

// index.html in any directory
server.addDocumentRoot("/*", "index.html");

Static file serving

Document root

// Set base directory
server.baseLocation("/var/www");

// Serve all files from base
server.addDocumentRoot("/*", "*");

// Requests to /images/logo.png serve /var/www/images/logo.png

File aliases

An alias serves one given file for the matching requests:

// "/" serves the home page
server.addAlias("/", "", "/var/www/index.html");

// any request in /logo/ serves the same image
server.addAlias("/logo/", "*", "/opt/assets/logo.png");

Dynamic content

Route handlers

// GET /api/users
server.addExecute(
    HttpMethod::Get,
    "/api/",
    "users",
    [](Http::Worker* worker) {
        std::string json = R"({"users":["Alice","Bob"]})";

        worker->header("Content-Type", "application/json");
        worker->header("Content-Length", std::to_string(json.size()));
        worker->sendHeaders();

        *worker << json;
        worker->flush();
    }
);

// POST /api/submit
server.addExecute(
    HttpMethod::Post,
    "/api/",
    "submit",
    [](Http::Worker* worker) {
        // Read request body
        std::string body(worker->contentLength(), '\0');
        worker->read(&body[0], body.size());

        // Send response: headers first, then the body
        std::string json = R"({"status":"success"})";

        worker->header("Content-Type", "application/json");
        worker->header("Content-Length", std::to_string(json.size()));
        worker->sendHeaders();

        *worker << json;
        worker->flush();
    }
);

Multiple methods

// Handle GET and POST
server.addExecute(
    HttpMethod::Get | HttpMethod::Post,
    "/api/",
    "endpoint",
    [](Http::Worker* worker) {
        // Read the request body (empty for GET)
        std::string body(worker->contentLength(), '\0');
        worker->read(&body[0], body.size());

        worker->header("Content-Length", "0");
        worker->sendHeaders();
        worker->flush();
    }
);

Worker API

Reading request data

hasHeader() and header(name) read the request headers:

[](Http::Worker* worker) {
    // Check headers
    if (worker->hasHeader("Content-Type"))
    {
        std::string type = worker->header("Content-Type");
    }

    // Read body
    std::string body(worker->contentLength(), '\0');
    worker->read(&body[0], body.size());
}

Always read the whole request body before answering, even if you don’t need it: on a keep-alive connection, unread bytes would be taken for the next request.

A chunked or compressed request body (Transfer-Encoding or Content-Encoding header) has no Content-Length: read it until its end instead.

[](Http::Worker* worker) {
    std::string body(std::istreambuf_iterator<char>(*worker), {});
    worker->clear();  // ready to send the response
}

Sending responses

header(name, value) sets a response header. Headers must be sent before the body:

[](Http::Worker* worker) {
    std::string html = "<html><body>Hello</body></html>";

    // Set response headers
    worker->header("Content-Type", "text/html");
    worker->header("Content-Length", std::to_string(html.size()));
    worker->header("Cache-Control", "no-cache");

    // Write headers
    worker->sendHeaders();

    // Write body
    *worker << html;

    // Flush
    worker->flush();
}

Helper methods

Each helper sends a complete response: call one of them per request.

// Send a file
worker->sendFile("/var/www/index.html");

// or send an error
worker->sendError("404", "Not Found");

// or send a redirect
worker->sendRedirect("302", "Found", "https://example.com/new");

Redirects

Simple redirect

// Redirect every request in /old/ and its subdirectories to the /new/ page
server.addRedirect("/old/*", "*", "/new/");

// Redirect to external URL
server.addRedirect("/external/", "*", "https://example.com");

Dynamic redirects

// Redirect every HTTP request to HTTPS, keeping host, path and query
server.addRedirect("/*", "*", "https://$host$urn");

// Available variables:
// $root, $scheme, $host, $port, $path, $query, $urn

Authentication

Access control

The access handler receives the authorization type and credentials of the request. When it refuses access, it must set err to HttpErrc::Unauthorized (401) or HttpErrc::Forbidden (403).

// Define access handler
auto authHandler = [](const std::string& type,
                      const std::string& credentials,
                      std::error_code& err) -> bool {
    if (type != "Bearer")
    {
        err = make_error_code(HttpErrc::Unauthorized);
        return false;
    }

    if (credentials != "my-secret-token")
    {
        err = make_error_code(HttpErrc::Forbidden);
        return false;
    }

    return true;
};

// Apply to route
server.addExecute(
    HttpMethod::Get,
    "/api/",
    "protected",
    handler,
    authHandler  // Access control
);

Requests without an Authorization header are answered with 401 Unauthorized.


Configuration

Keep-alive

// Set keep-alive timeout and max requests
server.keepAlive(
    std::chrono::seconds(30),  // Timeout
    100                         // Max requests per connection
);

// Get settings
auto timeout = server.keepAliveTimeout();
int max = server.keepAliveMax();

Base location

// Set document root
server.baseLocation("/var/www/html");

// Get current location
std::string location = server.baseLocation();

HTTPS configuration

Certificates

Https::Server server(4);

// Set server certificate and private key
server.setCertificate(
    "/etc/ssl/server-cert.pem",
    "/etc/ssl/server-key.pem"
);

// Set CA certificate for client verification
server.setCaCertificate("/etc/ssl/ca-cert.pem");

// Require client certificates
server.setVerify(true, 5);  // depth=5

Cipher configuration

// TLS 1.2 and below
server.setCipher("ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256");

// TLS 1.3
server.setCipher_1_3("TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256");

// Curves (OpenSSL 3.0+)
server.setCurve("P-521:P-384:P-256");

Complete examples

REST API server

// libjoin.
#include <join/httpserver.hpp>

using namespace join;

// Send a JSON body with its Content-Length
void sendJson(Http::Worker* worker, const std::string& json)
{
    worker->header("Content-Type", "application/json");
    worker->header("Content-Length", std::to_string(json.size()));
    worker->sendHeaders();
    *worker << json;
    worker->flush();
}

int main()
{
    Http::Server server(4);

    // GET /api/users
    server.addExecute(HttpMethod::Get, "/api/", "users", [](Http::Worker* worker) {
        sendJson(worker, R"({"users":["Alice","Bob"]})");
    });

    // POST /api/users
    server.addExecute(HttpMethod::Post, "/api/", "users", [](Http::Worker* worker) {
        std::string body(worker->contentLength(), '\0');
        worker->read(&body[0], body.size());

        sendJson(worker, body);  // echo the created user
    });

    // DELETE /api/users
    server.addExecute(HttpMethod::Delete, "/api/", "users", [](Http::Worker* worker) {
        worker->sendError("204", "No Content");
    });

    Http::Endpoint endpoint(Http::v4(), 8080);
    server.create(endpoint);

    // ... keep running, then stop (see "Close server gracefully")
    server.close();
}

Static + dynamic content

Http::Server server(4);

// API endpoints first...
server.addExecute(HttpMethod::Get, "/api/", "*", [](Http::Worker* worker) {
    std::string json = R"({"api":"v1"})";

    worker->header("Content-Type", "application/json");
    worker->header("Content-Length", std::to_string(json.size()));
    worker->sendHeaders();
    *worker << json;
    worker->flush();
});

// ...then static files for everything else
server.baseLocation("/var/www");
server.addDocumentRoot("/*", "*");

Http::Endpoint endpoint(Http::v4(), 80);
server.create(endpoint);

File server with auth

Http::Server server(4);

auto checkAuth = [](const std::string& type,
                    const std::string& credentials,
                    std::error_code& err) -> bool {
    if (type != "Bearer" || credentials != "my-secret-token")
    {
        err = make_error_code(HttpErrc::Forbidden);
        return false;
    }
    return true;
};

// Protected downloads: /var/www/downloads/...
server.baseLocation("/var/www");
server.addDocumentRoot("/downloads/*", "*", checkAuth);

Http::Endpoint endpoint(Http::v4(), 8080);
server.create(endpoint);

Best practices

Worker threads

// Use hardware concurrency
Http::Server server(std::thread::hardware_concurrency());

// Or based on workload
Http::Server cpuBound(std::thread::hardware_concurrency() * 2);
Http::Server ioBound(std::thread::hardware_concurrency() * 4);

Always set Content-Length or use chunking

The client needs to know where the body ends. Either announce its size:

[](Http::Worker* worker) {
    std::string body = "Hello, World!";

    worker->header("Content-Length", std::to_string(body.size()));
    worker->sendHeaders();
    *worker << body;
    worker->flush();
}

or send it in chunks when the size is not known in advance:

[](Http::Worker* worker) {
    worker->header("Transfer-Encoding", "chunked");
    worker->sendHeaders();
    *worker << "Hello, ";
    *worker << "World!";
    worker->flush();
}

Error handling in handlers

[](Http::Worker* worker) {
    try
    {
        // Process request (may throw)
    }
    catch (const std::exception& e)
    {
        worker->sendError("500", "Internal Server Error");
    }
}

Close server gracefully

Block the termination signals before creating the server, so that the worker threads inherit the mask, then wait for a signal with sigwait() and close the server:

// libjoin.
#include <join/httpserver.hpp>

// C.
#include <signal.h>

using namespace join;

int main()
{
    sigset_t signals;
    sigemptyset(&signals);
    sigaddset(&signals, SIGINT);
    sigaddset(&signals, SIGTERM);
    pthread_sigmask(SIG_BLOCK, &signals, nullptr);

    Http::Server server(4);
    server.baseLocation("/var/www");
    server.addDocumentRoot("/*", "*");
    server.create(Http::Endpoint(Http::v4(), 8080));

    // Wait for Ctrl+C or kill
    int sig;
    sigwait(&signals, &sig);

    // Stop accepting requests and wait for the workers
    server.close();
}

Performance tips

File caching

The server automatically caches files in memory and tracks modifications:

// Files are cached automatically
// Cache invalidates on file modification
server.addDocumentRoot("/*", "*");

// No manual cache management needed

Keep-alive tuning

// Long timeout for interactive clients
server.keepAlive(std::chrono::seconds(60), 1000);

// Short timeout for API clients
server.keepAlive(std::chrono::seconds(5), 100);

Compression

The server compresses a response when the handler sets the Content-Encoding header (gzip or deflate). Check the client’s Accept-Encoding first, and use chunked transfer since the compressed size is not known in advance:

[](Http::Worker* worker) {
    std::string text = "Hello, World!";

    if (worker->header("Accept-Encoding").find("gzip") != std::string::npos)
    {
        worker->header("Content-Encoding", "gzip");
        worker->header("Transfer-Encoding", "chunked");
    }
    else
    {
        worker->header("Content-Length", std::to_string(text.size()));
    }

    worker->sendHeaders();
    *worker << text;
    worker->flush();
}

Security headers

The server automatically adds security headers:

// Automatically added:
// - Strict-Transport-Security (HTTPS only)
// - Content-Security-Policy
// - X-XSS-Protection
// - X-Content-Type-Options
// - X-Frame-Options

// Override if needed
[](Http::Worker* worker) {
    worker->header("X-Frame-Options", "DENY");
    worker->header("Content-Length", "0");
    worker->sendHeaders();
    worker->flush();
}

Summary

FeatureSupported
Multi-threaded✅
Static file serving✅
Dynamic content✅
Route pattern matching✅
HTTP/HTTPS✅
Keep-alive✅
Compression✅
Chunked transfer✅
Authentication✅
File caching✅
Redirects✅