C++ HTTP Hosting Public Contract¶
C++ contract table of contents | Language interface table of contents | Previous: C++ Per-Language Interface | Next: C++ Embedded HTTP Server
This document is the formal contract C++ HTTP hosting must provide.
1. Request Processing Contract¶
C++ HTTP hosting provides route registration, JSON binding, DI handler execution, and HTTP response generation as one request processing flow. The flow below is an example to explain the public hosting contract, and doesn't use a specific sample as the contract's basis.
HTTP client
POST /games
|
v
C++ HTTP hosting
options.http().map_post<create_game_http_handler_t>("/games")
|
v
DI handler
create_game_http_handler_t::handle(
create_game_http_req_t request)
|
v
request_client_t.request(play_channel, ...).async<create_game_res_t>()
|
v
HTTP JSON response
create_game_http_res_t {
room_id, game_name, owner_play_endpoint,
play_endpoints, play_nodes, required_level
}
The C++ framework aligns this flow to the following meaning.
- The HTTP server is a hosted service tied to the app lifecycle.
- An HTTP route registers method and path with
map_get,map_post,map_put,map_delete(the same concept as ASP.NET Core Minimal API'sMapGetfamily; the C++ public surface usessnake_caseper §7's convention). - An HTTP endpoint supports both
http://andhttps://. - A request body converts to a JSON DTO.
- A route handler is resolved from DI.
- A handler is injected
request_client_tto request a zlink channel. - A DTO the handler returns becomes the JSON response body.
- A normal response defaults to
200 OK. - A payload decode failure is
400 Bad Request. - A missing route is
404 Not Found. - A handler or zlink request failure maps to HTTP status based on error kind.
- App shutdown closes the HTTP accept loop and in-progress request dispatch.
HTTP hosting is a JSON API capability the framework server package provides. MVC/view, static file, template engine, and ORM aren't in this contract's scope.
2. Public API¶
An ordinary application registers HTTP endpoint and route inside
app.add_zlink_framework(...).
auto app = zlink::framework::app_t::create();
app.add_zlink_framework([&](auto &options) {
auto mesh = options.add_route_mesh(sample_names_t::application_mesh)
.listen(7300) // opens the endpoint this RouteMesh receives peer messages on.
.set_routing_id(topology.application_rid); // identifies this node within the same mesh.
mesh.channel(sample_names_t::api_channel)
.server() // publishes this node as a request-processing candidate for api_channel.
.add_handler_group("api"); // connects the DI handler group below to the Channel handler.
mesh.channel(sample_names_t::play_channel)
.client(); // registers only the play_channel call path, with no Server membership.
options.http()
.listen(topology.api_http_endpoint) // opens the listener an HTTP client connects to.
.map_post<create_game_http_handler_t>("/games"); // connects POST /games to the DI handler.
options.handlers()
.group ("api")
.add<authenticate_player_handler_t> (); // registers the handler to resolve in the api group.
});
options.http() doesn't expose a low-level Boost.Beast, socket,
acceptor, thread, or executor type.
3. Handler Signature Forms¶
An HTTP handler declares DTO and DI dependency the same way as a
channel handler. But since HTTP needs transport metadata, raw body,
and direct response control, handle(...)'s argument and return value
can be declared in multiple forms. Every handler signature form below
is the HTTP hosting public contract.
struct create_game_http_req_t {
static constexpr const char *packet_name = "CreateGameHttpReq";
std::string game_name;
};
struct create_game_http_res_t {
static constexpr const char *packet_name = "CreateGameHttpRes";
std::string room_id;
std::string game_name;
std::string owner_play_endpoint;
std::vector<std::string> play_endpoints;
std::vector<play_node_info_t> play_nodes;
int required_level = 0;
};
class create_game_http_handler_t {
public:
using request_type = create_game_http_req_t;
using reply_type = create_game_http_res_t;
explicit create_game_http_handler_t(
zlink::framework::request_client_t &client,
zlink::framework::logger_t<create_game_http_handler_t> &logger);
task_t<create_game_http_res_t> handle(const create_game_http_req_t &request);
};
A handler's handle(...) allows both a sync return and a task_t<T>
return. A C++ HTTP handler projects .NET's Task<IResult> or
Task<T> meaning as task_t<T>.
task_t<create_game_http_res_t>
create_game_http_handler_t::handle(const create_game_http_req_t &request)
{
auto room = co_await _client
.request (sample_names_t::play_channel,
create_game_req_t{request.game_name})
.async<create_game_res_t> ();
co_return create_game_http_res_t {room.room_id,
room.game_name,
room.owner_play_endpoint,
room.play_endpoints,
room.play_nodes,
room.required_level};
}
The request_type, reply_type, constructor-dependency, handle(...)
convention stays the same as a message handler. HTTP doesn't make a
separate constructor injection convention.
The HTTP handler signature forms that must be supported are below.
- typed DTO:
reply_type handle(const request_type &request) - typed DTO async:
task_t<reply_type> handle(const request_type &request) - typed DTO + context:
reply_type handle(const request_type &request, http_context_t &context) - typed DTO + context async:
task_t<reply_type> handle(const request_type &request, http_context_t &context) - typed DTO + request:
reply_type handle(const request_type &request, const http_request_t &http) - typed DTO + request async:
task_t<reply_type> handle(const request_type &request, const http_request_t &http) - typed DTO + request + context:
reply_type handle(const request_type &request, const http_request_t &http, http_context_t &context) - typed DTO + request + context async:
task_t<reply_type> handle(const request_type &request, const http_request_t &http, http_context_t &context) - typed response:
http_response_t handle(const request_type &request) - typed response + context:
http_response_t handle(const request_type &request, http_context_t &context) - typed response async:
task_t<http_response_t> handle(const request_type &request) - typed response + context async:
task_t<http_response_t> handle(const request_type &request, http_context_t &context) - typed response + request:
http_response_t handle(const request_type &request, const http_request_t &http) - typed response + request async:
task_t<http_response_t> handle(const request_type &request, const http_request_t &http) - typed response + request + context:
http_response_t handle(const request_type &request, const http_request_t &http, http_context_t &context) - typed response + request + context async:
task_t<http_response_t> handle(const request_type &request, const http_request_t &http, http_context_t &context) - raw HTTP request:
http_response_t handle(const http_request_t &request) - raw HTTP request async:
task_t<http_response_t> handle(const http_request_t &request)
http_request_t and http_response_t are zlink framework public
types. Directly using a Boost.Beast, Boost.Asio, OpenSSL stream, or
socket type in a handler signature violates the public contract.
map_*<THandler>(...) determines the signature form above at compile
time. If request_type exists, it registers as a typed route; if only
handle(const http_request_t&) exists, it registers as a raw route.
For a typed route with multiple overloads, argument form is checked
before return type. The form receiving both http_request_t and
http_context_t is selected first, then http_request_t,
http_context_t, and DTO-only form in that order.
Providing both a typed route and a raw route signature on one handler
at once makes route mode ambiguous, so it must fail via static
assertion or startup validation.
Handler signature is determined in the following order.
- If a
request_typealias exists, it's considered a typed route candidate. - If there's no
request_typealias buthandle(const http_request_t&)exists, it's considered a raw route. - A typed route must have one of
reply_typeorhttp_response_tas return form. - It fails if a typed route and raw route signature exist together on one handler.
- If there are multiple signatures inside a typed route, only one is selected by the priority below.
Typed route call priority:
handle(const request_type&, const http_request_t&, http_context_t&)handle(const request_type&, const http_request_t&)handle(const request_type&, http_context_t&)handle(const request_type&)
Each signature's return value must be one of reply_type,
http_response_t, task_t<reply_type>, task_t<http_response_t>.
This priority checks argument form before return type. If the same
handler provides both reply_type handle(request, http, context) and
http_response_t handle(request, http), the first signature is
selected.
If the selected signature returns http_response_t, status/header/body
are controlled directly; if it returns reply_type, the serializer
converts the DTO to HTTP body.
The http_request_t argument means raw HTTP metadata is needed, so it
takes priority over http_context_t.
Raw route call priority:
task_t<http_response_t> handle(const http_request_t&)http_response_t handle(const http_request_t&)
A raw route doesn't require request_type, reply_type aliases. It
fails if a raw route handler returns a reply_type DTO. This is
because a raw route can't have the framework infer a response
serializer.
Handler signatures that must fail:
| Condition | Reason for failure |
|---|---|
request_type exists but there's no callable typed handle(...) |
The route can't be executed |
request_type exists but the DTO return signature has no reply_type |
The DTO serializer can't be known |
No request_type and no raw handle(http_request_t) |
Route mode can't be decided |
| Both a typed signature and a raw signature provided at once | Typed/raw route mode is ambiguous |
A raw handler returns reply_type or an arbitrary DTO |
A raw response serializer can't be inferred |
| A handler takes a Beast/Asio/OpenSSL type | Violates the public dependency boundary |
| Two or more same-priority overloads are callable | Overload selection is ambiguous |
4. Route Builder¶
The support scope is typed JSON route and raw HTTP route. GET,
POST, PUT, DELETE must be registrable with the same convention.
The C++ configuration and host interface
solely owns the http_options_builder_t declaration. This
section defines the route builder's usage semantics and the HTTP
request/response contract.
namespace zlink::framework {
struct http_context_t {
http_method_t method;
std::string path;
std::string correlation_id;
std::map<std::string, std::string> request_headers;
std::map<std::string, std::string> response_headers;
std::optional<std::string> response_body;
int response_status;
http_context_t &response_header(std::string name, std::string value);
http_context_t &json_response(int status, std::string body);
};
struct http_request_t {
http_method_t method;
std::string path;
std::string target;
std::string query_string;
std::string correlation_id;
std::map<std::string, std::string> headers;
std::map<std::string, std::string> route_values;
std::map<std::string, std::string> query_values;
std::string body;
std::string content_type;
std::string remote_endpoint;
};
struct http_response_t {
int status = 200;
std::string body;
std::string content_type = "application/json";
std::map<std::string, std::string> headers;
http_response_t &header(std::string name, std::string value);
};
} // namespace zlink::framework
http_request_t field contract:
| Field | Meaning |
|---|---|
method |
The HTTP method used for route matching |
path |
Path with query string removed |
target |
The original request target. Includes path and query string |
query_string |
The query string after ?. Empty string if absent |
correlation_id |
X-Correlation-Id, X-Request-Id, or a runtime-generated id |
headers |
HTTP header name/value. Header name uses the runtime's canonical form |
route_values |
{name} path segment binding result |
query_values |
Query string binding result |
body |
Request body that passed limit validation |
content_type |
The Content-Type header value. Empty string if absent |
remote_endpoint |
The client endpoint when known. Empty string if unknown |
http_response_t field contract:
| Field | Meaning |
|---|---|
status |
HTTP status code. Default is 200 |
body |
Response body bytes. A string is treated as UTF-8 text or a binary-safe byte buffer |
content_type |
The Content-Type response header. Default is application/json |
headers |
Response header name/value |
http_request_t and http_response_t are copies of values the runtime
owns during request processing. A handler must not store a reference
to this object. Its lifetime isn't guaranteed after the request
completes.
The listen(...) endpoint uses the http://host:port and
https://host:port formats.
http://127.0.0.1:18080http://0.0.0.0:18080http://[::1]:18080https://127.0.0.1:18443https://0.0.0.0:18443https://[::1]:18443
Using an https:// endpoint requires setting the TLS server
certificate and private key together. TLS is an HTTP hosting transport
option, and isn't described mixed with the zlink channel transport
configuration.
options.http()
.listen("https://0.0.0.0:8443")
.configure_tls([](auto &tls) {
tls.certificate_file("certs/server.crt")
.private_key_file("certs/server.key");
})
.map_post<create_game_http_handler_t>("/games");
map_post<THandler>(path) performs the following work at once for a
typed route.
- Registers
THandlerto the service collection. - Registers the JSON serializer for
THandler::request_typeandTHandler::reply_type. - Registers method and path to the route table.
- Builds a DI scope per request and resolves
THandler. - Runs
handle(...)on the framework handler coroutine executor. - Serializes the result DTO to the JSON response body.
- If the handler provides
handle(request, http_context_t&), it can receive HTTP context, such as correlation id and header, together. An existing handler that only provideshandle(request)still works as is.
A raw route doesn't require a request_type serializer. The runtime
builds http_request_t and hands it to the handler, and uses the
http_response_t the handler returns as the HTTP response as is. A
raw route also passes through the same middleware, correlation id,
timeout, limit, logging, and metrics policy.
Invoker construction pseudocode is below.
template <typename THandler>
http_route_invoker_t make_invoker()
{
if constexpr (has_request_type<THandler>) {
static_assert(!has_raw_http_only_shape<THandler>);
register_json_serializer<typename THandler::request_type>();
if constexpr (returns_typed_dto<THandler>) {
register_json_serializer<typename THandler::reply_type>();
}
return make_typed_invoker<THandler>();
} else {
static_assert(has_raw_http_shape<THandler>);
return make_raw_invoker<THandler>();
}
}
Typed invoker processing order:
- Merges body, route value, and query value into one binding JSON.
- Builds the DTO with the
request_typeserializer. - Builds
http_request_tandhttp_context_t. - Calls the handler overload by priority.
- If the result is
reply_type, builds the JSON response together withhttp_context_t's status/header. - If the result is
http_response_t, builds the HTTP response based on the response object.
Raw invoker processing order:
- Doesn't check whether content type is JSON.
- Applies the same body/header/route/query limit.
- Builds
http_request_t. - Calls the raw handler.
- Builds the HTTP response based on the returned
http_response_t.
Response precedence:
| Handler result | Priority |
|---|---|
Returns http_response_t |
http_response_t's status/header/content type/body take top priority |
Returns DTO + http_context_t::json_response(...) set |
Uses the context's status/body/header |
| Returns DTO + only context header/status set | Uses context status/header + DTO JSON body |
| Only returns DTO | Uses 200 OK, application/json, DTO JSON body |
Middleware after(...) runs after the handler result is built. If
after(...) adds a response header, it can overwrite an existing
header of the same name. However, Content-Length is computed by the
runtime based on the final body, so a handler or middleware doesn't fix
it directly.
Route parameter and query string binding follow a simplified version of ASP.NET Core model binding.
The handler request DTO merges values from body, route, and query. On conflict, priority is route parameter, query string, body in that order. This priority is fixed in the startup validation document and tests.
use<TMiddleware>() calls TMiddleware::before(http_context_t&) and
TMiddleware::after(http_context_t&) before and after the route
handler. A middleware doesn't take a raw Beast request or socket — it
only uses http_context_t's correlation id and framework header map.
If a request has X-Correlation-Id or X-Request-Id, that value is
sent back as the response's X-Correlation-Id; otherwise the runtime
generates a request correlation id.
If a middleware calls json_response(status, body) in before(...),
the HTTP runtime returns that JSON response without calling the
handler. This short-circuit path also goes through after(...)
middleware, so logging/correlation processing can be put in one place.
map_health(path), map_readiness(path), map_liveness(path) expose
the app.health() report as an HTTP JSON endpoint. If readiness or
liveness is unhealthy, that endpoint returns
503 Service Unavailable. This route doesn't require the user to build
a separate handler, and the health aggregation rule is owned by
contracts/eventing/health.hpp and the runtime diagnostics
implementation.
5. Request / Response Contract¶
The default typed route's HTTP contract is below.
| Item | Contract |
|---|---|
| Method | GET, POST, PUT, DELETE |
| Scheme | http or https |
| Content type | application/json |
| Request body | THandler::request_type JSON for a method with a body |
| Route parameter | Binds a {name} segment to a request DTO field |
| Query string | Binds ?name=value to a request DTO field |
| Response body | THandler::reply_type JSON |
| Success status | 200 OK |
| Route not found | 404 Not Found |
| Method mismatch | 405 Method Not Allowed |
| Unsupported content type | 400 Bad Request (protocol_error) |
| Invalid JSON | 400 Bad Request |
| Body limit exceeded | 413 Payload Too Large |
| Serializer registration | map_*<THandler> registers the request/reply JSON serializer |
| Handler failure | Error-kind-based status mapping |
The HTTP response defaults to Content-Type: application/json. An
error response is also returned as a JSON object.
6. Error Mapping¶
A framework error kind maps to HTTP status.
| Framework error | HTTP status | Reason |
|---|---|---|
protocol_error |
400 Bad Request |
The client body didn't convert to a DTO, or the request meaning doesn't match the framework contract |
not_found |
404 Not Found |
The target route, channel, or service wasn't found |
framework_exception_t::code() == std::errc::timed_out |
504 Gateway Timeout |
A zlink request behind HTTP hosting didn't finish in time |
framework_exception_t::code() indicates a shutdown boundary and the host is shutting down |
503 Service Unavailable |
The host is shutting down |
internal_failure |
500 Internal Server Error |
An internal handler or runtime failure |
If the HTTP server runtime detects a request that exceeded the body
size limit, it closes with 413 Payload Too Large. If a JSON route
receives a content type incompatible with application/json, it
throws protocol_error and closes with 400 Bad Request. Both
statuses are a server request validation failure, not a handler
failure.
An exception with an unclear error kind closes with
500 Internal Server Error and leaves the cause in log/monitoring. The
HTTP client isn't directly exposed the C++ exception type name.
7. Lifecycle¶
The HTTP server runs as a hosted_service_t.
app.run(...)builds the service provider and then starts the HTTP hosted service.- Once
app.stop()or a signal shutdown comes in, it closes the acceptor and doesn't accept a new request. - It waits for an in-progress request to complete within the graceful shutdown timeout.
- It doesn't indefinitely build a new zlink submit during shutdown.
An HTTP endpoint switches to ready only after the framework client the handler is injected becomes ready. On shutdown, it first rejects a new HTTP request and gives an in-progress handler a chance to complete within the messaging drain deadline.
7.1 Middleware¶
HTTP middleware projects ASP.NET Core's cross-cutting pipeline concept
into C++ form. The core scope is options.http().use<TMiddleware>()
and the http_context_t-based before/after hook. A per-route filter
type isn't put as a separate public API — middleware checks
method/path to handle it.
The required axes are below.
- Exception filter
- Logging filter
- Validation filter
- Auth filter
- Correlation id filter
- Custom middleware registration
Middleware can use DI. It doesn't expose a boost::beast
request/response in the public API — it uses an abstract type like the
framework's http_context_t. http_context_t provides request id,
method, path, header lookup, response status setting, and short-circuit
JSON response setting.
8. Installed Header Boundary¶
HTTP hosting's public contract is provided by the following installed headers.
The public header doesn't expose a Boost.Beast, Boost.Asio, OpenSSL/SSL
context, TCP socket, acceptor, or request parser type. The application
and extension don't include this implementation dependency — they only
use the framework's http_request_t, http_response_t, and
http_context_t.
9. C++ HTTP Hosting Surface¶
| Capability | C++ framework |
|---|---|
| Application creation | app_t::create() |
| HTTP endpoint | options.http().listen(url) |
| HTTPS certificate | options.http().listen("https://...").configure_tls(...) |
| Framework registration | app.add_zlink_framework(...) |
| POST route | options.http().map_post<handler_t>("/games") |
| GET route | options.http().map_get<handler_t>("/games/{id}") |
| Request body binding | Builds request_type with the JSON serializer |
| Route/query binding | Merges route parameter and query string into request_type |
| Dependency injection | Automatic injection based on constructor parameters |
| Middleware/filter | options.http().use<TMiddleware>() and http_context_t |
| Execution interruption | Host shutdown/drain policy. A default cancellation argument isn't exposed on the public handler signature |
| Typed reply | The handler returns a reply_type DTO |
10. Public Contract Decision¶
| Item | Contract |
|---|---|
| Package location | HTTP hosting is provided by the framework server package. |
| Implementation dependency exposure | Beast, Asio, and SSL implementation types aren't exposed in the public header and signature. |
| HTTPS/TLS | Certificate and private key configuration are provided as a public option. |
| Route matching | Supports literal path and {name} path parameter. |
| Method | Supports GET, POST, PUT, DELETE with the same builder convention. |
| Cancellation | Doesn't add a separate cancellation token to the handler signature — applies the request timeout and host drain contract. |
| Response customization | A typed DTO defaults to 200 OK status. Direct status and header control is provided by a handler that returns http_response_t. |
| Server hardening | Follows Embedded HTTP Server's limit, timeout, and shutdown contract. |
11. Regression Test¶
The minimal test covers the axes below.
- Contract header compile:
#include <zlink/framework.hpp>and#include <zlink/framework/contracts/http/http.hpp> - Route registry: duplicate registration of the same method/path and system route conflict fail startup validation
- Per-method route:
GET,POST,PUT,DELETE - Per-scheme listen:
http://,https:// - HTTPS TLS option: a missing certificate/private key fails startup validation
- HTTPS loopback: JSON request/response succeeds with a test certificate
- HTTP handler e2e: calls
GET,POST,PUT,DELETEroutes with a loopback client, verifying DTO binding, DI handler execution, JSON response, and status mapping - Handler signature matrix: calls every sync/async signature of DTO, DTO+context, DTO+request, response return, and raw request
- Raw HTTP request:
http_request_tincludes method, target, header, route/query, body - Raw HTTP response:
http_response_t's status, header, content type, body are returned as is - Ambiguous handler signature: an ambiguous
handle(...)combination fails via static assertion or startup validation - Response precedence: fixes the priority of
http_response_t,json_response, context header/status, and DTO default response - Route parameter binding: assigns
/games/{gameId}to a DTO field - Query string binding: assigns
?page=1to a DTO field - Fixes body/route/query merge priority
- JSON binding: converts request body to DTO and returns reply DTO as JSON
- DI: an HTTP handler receives
request_client_tthrough constructor injection - Middleware: before hook registration order, after hook reverse-order execution, per-request state preservation, short-circuit
- Error mapping: invalid JSON is
400, unknown route is404, timeout is504 - Server validation: unsupported content type is
400, body limit exceeded is413 - Embedded server lifecycle: keep-alive, request timeout, graceful shutdown drain, and connection metrics are verified by Embedded HTTP Server regression tests
- Lifecycle:
app.stop()stops accepting a new request and completes after draining an in-progress request
Handler signature regression matrix:
| Test | Expectation |
|---|---|
| DTO sync | reply_type handle(request) returns 200 JSON |
| DTO async | task_t<reply_type> handle(request) returns JSON after await |
| DTO context sync | Context header/status is reflected in the response |
| DTO context async | The async handler and context change are reflected together |
| DTO request sync | Can read http_request_t's header/query/body |
| DTO request async | The async handler receives http_request_t and completes normally |
| Response sync | http_response_t status/header/body are returned as is |
| Response context | http_response_t takes priority over context body |
| Response request | Reads http_request_t and responds with http_response_t |
| Raw request sync | Receives raw body with no serializer and responds |
| Raw request async | The raw request async handler completes normally |
| Raw content type | A non-JSON content type is also allowed on a raw route |
| Ambiguous route mode | Fails if a typed signature and raw signature exist on one handler |
| Invalid return type | Fails if a raw route returns a DTO |
| Content length | The Content-Length a handler gave is corrected to the runtime's final value |
C++ contract table of contents | Language interface table of contents | Previous: C++ Per-Language Interface | Next: C++ Embedded HTTP Server