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.