6.0.0-rc4Source

Using Nt HTTP Server

Serving HTTP from an application: routes with typed path arguments, responses, streaming, HTTPS and local sockets.

Link Nt::HttpServer (or Nt6::HttpServer from an installed package) and include <QtHttpServer>:

find_package(Nt6 6 REQUIRED)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE Nt6::HttpServer)

Starting a Server

A QHttpServer does not listen by itself. Create a QTcpServer, make it listen, and bind it. Port 0 picks a free port:

auto *tcpServer = new QTcpServer;
if (!tcpServer->listen(QHostAddress::LocalHost, port) || !server.bind(tcpServer)) {
    std::fprintf(stderr, "cannot listen on port %u: %s\n", port, qPrintable(tcpServer->errorString()));
    delete tcpServer;
    return 1;
}
std::printf("listening on http://127.0.0.1:%u/\n", tcpServer->serverPort());

The server runs from the event loop, so the program must call QCoreApplication::exec(). One QHttpServer can serve several QTcpServer and QLocalServer objects at once.

Routes

QHttpServer::route() adds a rule that maps a path to a handler. Each <arg> in the path pattern is converted to the type of the matching handler parameter. The handler may also take the request, and the responder as its last parameter:

// A fixed path.
server.route("/", [] { return "Hello from Nt HTTP Server\n"; });

// <arg> becomes the parameter: a QString takes one path segment.
server.route("/hello/<arg>", [](const QString &name) { return QString("Hello, %1!\n").arg(name); });

// A number must fit the type, so /square/abc and /square/99999999999 do not match.
server.route("/square/<arg>", [](int n) { return QJsonObject{{"n", n}, {"square", qint64(n) * n}}; });

// Only POST, with access to the request.
server.route("/add", QHttpServerRequest::Method::Post, [](const QHttpServerRequest &request) {
    const QJsonObject in = QJsonDocument::fromJson(request.body()).object();
    if (!in.contains("a") || !in.contains("b"))
        return QHttpServerResponse("expected {\"a\": number, \"b\": number}\n",
                                   QHttpServerResponse::StatusCode::BadRequest);
    return QHttpServerResponse(QJsonObject{{"sum", in.value("a").toDouble() + in.value("b").toDouble()}});
});

A path argument can be any integer type, float, double, QString, QByteArray or QUrl. A number must be written in decimal and fit the type; a path whose argument does not convert does not match the rule. Strings match one path segment and are percent-decoded; a QUrl matches the rest of the path, slashes included. If the pattern ends with / and has one <arg> fewer than the handler has path parameters, <arg> is appended.

The rules are tried in the order they were added, and the first one whose pattern and method match handles the request. A request that no rule matches gets 404 Not Found, or whatever QHttpServer::setMissingHandler() answers.

A handler that belongs to an object takes the object as a context: the rule stops matching when the object is destroyed, and a member function is called on it:

server.route("/status", &monitor, &Monitor::status);

Responses

A handler returns a QHttpServerResponse, or a value that converts to one:

server.route("/teapot", [] { return QHttpServerResponse::StatusCode::ImATeapot; });
server.route("/version", [] { return QJsonObject{{"version", QT_VERSION_STR}}; });
server.route("/robots.txt", [] { return QHttpServerResponse("text/plain", "User-agent: *\nDisallow: /\n"); });
server.route("/source", [] { return QHttpServerResponse::fromFile(QStringLiteral(__FILE__)); });

To change every response, for example to add a header, use QHttpServer::addAfterRequestHandler():

server.addAfterRequestHandler(&server, [](const QHttpServerRequest &, QHttpServerResponse &response) {
    QHttpHeaders headers = response.headers();
    headers.append(QHttpHeaders::WellKnownHeader::Server, "hello_server (Nt)");
    response.setHeaders(std::move(headers));
});

Writing the Response Yourself

A handler that takes a QHttpServerResponder writes the response through it, and returns nothing. This is the way to stream a body in pieces, to send a file without loading it into memory, or to answer later:

// Counts to five, one line per piece of the body.
server.route("/count", [](QHttpServerResponder &responder) {
    responder.writeBeginChunked("text/plain");
    for (int i = 1; i <= 5; ++i)
        responder.writeChunk(QByteArray::number(i) + "\n");
    responder.writeEndChunked("done\n");
});

// Streams a file next to this one from disk; the responder deletes the QFile when it is sent.
server.route("/files/<arg>", [](const QString &name, QHttpServerResponder &responder) {
    if (name.contains("..")) {
        responder.write(QHttpServerResponder::StatusCode::Forbidden);
        return;
    }
    auto *file = new QFile(QFileInfo(QStringLiteral(__FILE__)).absolutePath() + "/" + name);
    if (!file->open(QIODevice::ReadOnly)) {
        delete file;
        responder.write(QHttpServerResponder::StatusCode::NotFound);
        return;
    }
    responder.write(file, "text/plain");
});

// Answers a second later. The responder is moved out of the handler, which returns at once.
server.route("/later", [&app](QHttpServerResponder &responder) {
    auto later = std::make_shared<QHttpServerResponder>(std::move(responder));
    QTimer::singleShot(1000, &app, [later] { later->write("sorry for the wait\n", "text/plain"); });
});

A responder can be moved into a lambda or a member to answer later. The connection waits for that answer before it reads the next request.

HTTPS and Local Sockets

Bind a QSslServer instead of a QTcpServer to serve HTTPS. Requests reach the handlers after the TLS handshake, and their URLs have the scheme https:

auto *tls = new QSslServer;
tls->setSslConfiguration(configuration);   // certificate and private key
tls->listen(QHostAddress::Any, 8443);
server.bind(tls);

A QLocalServer serves HTTP on a Unix domain socket, which only processes on the same machine can reach:

auto *local = new QLocalServer;
local->listen("/run/app/http.sock");
server.bind(local);

Try it with curl --unix-socket /run/app/http.sock http://localhost/.

Limits and Safety

The server checks every request before a handler sees it. A request it cannot parse, a request line over 8 KiB, header fields over 64 KiB, a body over 64 MiB, or a request with both Transfer-Encoding and Content-Length is refused with a 4xx status, and the connection is closed. A handler can rely on the request being well formed, but not on its contents: path arguments, header values and the body come from the client.

There is no idle timeout: a client may keep a connection open as long as it likes. Put the server behind a reverse proxy when it faces the Internet.