Brazier is a modern C++ MVC framework designed for creating high-performance asynchronous APIs. The framework combines the power of Boost.Asio with modern C++20/23 features.
- Full asynchrony based on Boost.Asio coroutines
- MVC architecture with clear separation of concerns
- Built-in database migration system
- JWT authentication support
- Redis caching
- WebSockets technology support
- HTTPS / TLS support with SNI, ALPN-ready, mTLS
- High performance with minimal overhead
- PostgreSQL 12+ (for database operations)
- CMake 3.14+ (build system)
- vcpkg (C++ dependency manager)
- C++ compiler with C++20 support (GCC 10+, Clang 10+, MSVC 2019 16.8+)
- .env file with environment configuration
- Brazier libraries (pre-built)
- Windows 10/11
- Linux (Ubuntu 20.04+, CentOS 8+)
- macOS 11+
your_project/
├── config.json # Environment configuration
├── CMakeLists.txt # Build file
├── vcpkg.json # vcpkg dependencies
├── your_project/
│ └── Proj.cpp # Main application file
├── libs/
│ └── brazier/
│ ├── x64-windows/ # Windows libraries
│ └── x64-linux/ # Linux libraries
└── include/ # Header files
cmake_minimum_required(VERSION 3.14)
set(PROJECT_NAME "your_project")
project(${PROJECT_NAME} VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
if(NOT DEFINED CMAKE_TOOLCHAIN_FILE AND DEFINED ENV{VCPKG_ROOT})
set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" CACHE STRING "")
endif()
if(MSVC)
add_compile_options(/utf-8)
endif()
find_package(Boost CONFIG REQUIRED COMPONENTS beast asio filesystem system thread date_time smart_ptr optional variant algorithm)
find_package(fmt CONFIG REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)
find_package(jwt-cpp CONFIG REQUIRED)
find_package(hiredis CONFIG REQUIRED)
find_package(ZLIB REQUIRED)
find_package(PostgreSQL REQUIRED)
find_package(OpenSSL REQUIRED)
# Add all the other packages you need here
if(WIN32)
set(brazier_DIR "${CMAKE_SOURCE_DIR}/libs/brazier/x64-windows")
elseif(UNIX)
set(brazier_DIR "${CMAKE_SOURCE_DIR}/libs/brazier/x64-linux")
endif()
add_library(brazier SHARED IMPORTED)
if(WIN32)
set_target_properties(brazier PROPERTIES
IMPORTED_IMPLIB "${brazier_DIR}/lib/brazier.lib"
IMPORTED_LOCATION "${brazier_DIR}/bin/brazier.dll"
INTERFACE_INCLUDE_DIRECTORIES "${brazier_DIR}/include"
)
elseif(UNIX)
set_target_properties(brazier PROPERTIES
IMPORTED_IMPLIB "${brazier_DIR}/lib/libbrazier.a"
IMPORTED_LOCATION "${brazier_DIR}/bin/libbrazier.dylib"
INTERFACE_INCLUDE_DIRECTORIES "${brazier_DIR}/include"
)
endif()
target_include_directories(brazier INTERFACE
"${brazier_DIR}"
"${brazier_DIR}/include"
)
target_link_libraries(brazier INTERFACE
Boost::beast
Boost::asio
Boost::filesystem
Boost::system
Boost::thread
Boost::date_time
Boost::smart_ptr
Boost::optional
Boost::variant
Boost::algorithm
fmt::fmt
nlohmann_json::nlohmann_json
jwt-cpp::jwt-cpp
hiredis::hiredis
ZLIB::ZLIB
PostgreSQL::PostgreSQL
OpenSSL::SSL
OpenSSL::Crypto
# Add all the other packages you need here
)
add_executable(${PROJECT_NAME} ${PROJECT_NAME}/${PROJECT_NAME}.cpp)
target_link_libraries(${PROJECT_NAME} PRIVATE brazier)
target_include_directories(${PROJECT_NAME} PRIVATE
${CMAKE_SOURCE_DIR}/include
)
if(WIN32)
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/vcpkg_installed/x64-windows/include)
endif()
if(WIN32)
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
"${brazier_DIR}/bin/brazier.dll"
$<TARGET_FILE_DIR:${PROJECT_NAME}>
COMMENT "Copying brazier.dll to output directory"
)
elseif(UNIX)
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
"${brazier_DIR}/bin/libbrazier.dylib"
$<TARGET_FILE_DIR:${PROJECT_NAME}>
COMMENT "Copying libbrazier.dylib to output directory"
)
endif(){
"name": "your_project",
"version": "1.0.0",
"dependencies": [
"boost-algorithm",
"boost-asio",
"boost-beast",
"boost-date-time",
"boost-filesystem",
"boost-optional",
"boost-smart-ptr",
"boost-system",
"boost-thread",
"boost-variant",
"fmt",
"hiredis",
"jwt-cpp",
"libpq",
"nlohmann-json",
"zlib"
]
}Add all the other packages you need to the end of the list
# Windows
.\build.bat
# Linux/macOS
sh build.sh
ConfigManager is a flexible configuration management system in brazier that provides:
- Loading configuration from JSON files
- Working with nested parameters using dot notation
- Automatic saving of changes
- Support for different filesystem drivers
- Type-safe value retrieval with default values
{
"server": {
"host": "0.0.0.0",
"port": 3501,
"keep-alive-timeout": 60
},
"https_server": {
"host": "0.0.0.0",
"port": 8443,
"tls": {
"cert_file": "app/certs/server.crt",
"key_file": "app/certs/server.key",
"ca_file": "",
"require_client_cert": false,
"verify_client_cert": false,
"handshake_timeout": 15,
"conf": []
}
},
"database": {
"host": "127.0.0.1",
"port": 5432,
"username": "kirill",
"password": "qwerty",
"database": "light"
},
"redis": {
"host": "127.0.0.1",
"port": 6379
},
"nosql": {
"host": "127.0.0.1",
"port": 6379
},
"filesystem": {
"drivers": {
"local": {
"root": "./storage"
},
"root": {
"root": "./"
},
"default": "local"
}
}
}To initialize the global configuration manager, you need to run ConfigManager::initGlobal() at the beginning of program execution.
#include <brazier/vendor/ConfigManager.hpp>
int main() {
ConfigManager::initGlobal();
std::string host = global_config->get("server.host", "127.0.0.1");
int port = global_config->get("server.port", 3501);
brazier::Server server(host, port);
server.initialize();
server.run();
return 0;
}You can immediately convert data to the required types and work with nested config parameters.
std::string host = config.getNested<std::string>("server.host", "localhost");
int port = config.getNested<int>("server.port", 8080);
bool debug = config.getNested<bool>("app.debug", false);
json database = config.getNested<json>("database", json::object());auto& config = *global_config;
config.save(); // Manual save
config.reload(); // Reload from file
config.setAutoSave(true); // Auto-save on changes
config.setAutoSave(false); // Manual save only
config.clear(); // Clear all valuesBrazier provides a high-performance logging system with colored output, log rotation, and signal handling. Logging is done both to file and console with color coding.
- Colored logging to console
- Automatic log rotation
- Thread safety using mutexes
- High performance with time caching
- Signal handling for graceful shutdown
- File logging with buffering
| Level | Color | Description |
|---|---|---|
| ERROR | Red | Critical errors |
| WARNING | Yellow | Warnings |
| INFO | Blue | Informational messages |
| SUCCESS | Green | Successful operations |
| HELP | Dark blue | Support messages |
| CMD | Purple | Commands to use |
| DEBUG | Debug information |
Logger::log("Database connection established", "SUCCESS");
std::string username = "john_doe";
Logger::log("User " + username + " logged in", "INFO");
Logger::log("Debugging username", "DEBUG");
try {
} catch (const std::exception& e) {
Logger::log(std::string("Exception: ") + e.what(), "ERROR");
}All logs are saved and can be found at your_project_path/Storage/Logs
your_project_path/
└── Storage/
└── Logs/
├── app.log # Current log file
└── app_old.log # Previous log after rotation
The system automatically rotates logs when the line limit is reached:
- Current file:
debug.log - Old file:
debug_old.log - Default limit: 10,000 lines
Logger::log("Custom business event", "BUSINESS");
Logger::log("Performance metric", "METRIC");
Logger::log("Security audit", "AUDIT");2024-01-15 14:30:25 [INFO] Application started
2024-01-15 14:30:26 [SUCCESS] Database connected
2024-01-15 14:30:27 [ERROR] Connection timeout
The brazier logging system is ready for use in production environments and provides all necessary functions for effective application monitoring and debugging.
Brazier provides a powerful routing system with support for asynchronous handlers, parameterized paths, and CORS. The system is built on Boost.Beast and Boost.Asio coroutines.
Central class for managing routes and handling requests.
Represents a single endpoint with parameters and a handler.
Simplifies route declaration through macros.
auto userController = std::make_shared<UserController>();
// Basic routes
R(GET, "/api/users", userController, getUsers);
R(POST, "/api/users", userController, createUser);
// Parameterized routes
R(GET, "/api/users/:id", userController, getUserById);
R(PUT, "/api/users/:id", userController, updateUser);
R(DELETE_, "/api/users/:id", userController, deleteUser);
// Nested parameters
R(GET, "/api/users/:userId/posts/:postId", userController, getUserPost);
// CORS for API endpoints
CORS("/api/users", userController);
CORS("/api/users/:id", userController);R(GET, "/api/health", healthController, checkHealth);
R(GET, "/api/version", infoController, getVersion);R(GET, "/api/users/:id", userController, getUserById);
R(GET, "/api/categories/:categoryId/products/:productId", productController, getProduct);boost::asio::awaitable<void> getUserPost(const Request& req, Response& res, const Params& params) {
auto userId = params.at("userId");
auto postId = params.at("postId");
auto user = co_await userService.findById(userId);
auto post = co_await postService.findByUserAndId(userId, postId);
nlohmann::json response = {
{"user", user},
{"post", post}
};
res.result(http::status::ok);
res.body() = response.dump();
res.set(http::field::content_type, "application/json");
co_return;
}boost::asio::awaitable<void> createUser(const Request& req, Response& res, const Params& params) {
try {
auto body = nlohmann::json::parse(req.body());
if (!body.contains("email") || !body.contains("name")) {
res.result(http::status::bad_request);
co_return;
}
auto user = co_await userService.create(body);
res.result(http::status::created);
res.body() = user.toJson().dump();
res.set(http::field::content_type, "application/json");
} catch (const std::exception& e) {
res.result(http::status::bad_request);
}
co_return;
}Brazier ships with a first-class HTTPS server built on Boost.Beast + Boost.Asio + OpenSSL.
It is API-compatible with the plain HTTP Server class — switching between them, or running
both side by side, is a couple of lines in your main.
- TLS 1.2 / 1.3 by default, with per-server override via
conf - Session resumption via RFC 5077 tickets with automatic key rotation
- Hot-reload of certificates without restarting the server
- Certificate from file OR from memory (PEM string)
- HSTS header sent automatically on every response
- Keep-alive over TLS with configurable idle timeout
- Graceful shutdown with
close_notifyand a bounded drain timeout - Handshake timeout to protect against Slowloris-style attacks
- mTLS (mutual TLS / client certificates) with custom CA
- Multi-io_context — N worker threads, one per CPU core
SO_REUSEPORTon Linux/macOS, round-robin dispatch on WindowsTCP_DEFER_ACCEPTon Linux — less wake-ups, more throughput- Dynamic limits — body size, header size, and connection count auto-tuned to the host hardware at startup
- Raw OpenSSL tuning through
SSL_CONF_cmd— no code changes for cipher / protocol tweaks - Same Router / Engine / Middleware as the HTTP server — routing is transport-agnostic
- OpenSSL 3.x (installed via vcpkg — the
opensslpackage) - Boost 1.82+ (
boost::asio::cancel_afteris used internally) - A PEM-encoded certificate chain and matching private key
Add an https_server section to your config.json alongside the existing server:
{
"server": {
"host": "0.0.0.0",
"port": 3501,
"keep-alive-timeout": 60
},
"https_server": {
"host": "0.0.0.0",
"port": 8443,
"tls": {
"cert_file": "app/certs/server.crt",
"key_file": "app/certs/server.key",
"ca_file": "",
"require_client_cert": false,
"verify_client_cert": false,
"handshake_timeout": 15,
"conf": []
}
},
"http": {
"keep_alive_timeout": 60,
"max_connections": 50000,
"max_body_size": 1048576,
"max_header_size": 8192
}
}| Key | Type | Default | Description |
|---|---|---|---|
cert_file |
string |
server.crt |
Path to PEM certificate chain (leaf + intermediates) |
key_file |
string |
server.key |
Path to PEM private key |
cert_pem |
string |
"" |
Certificate as an in-memory PEM string (overrides cert_file) |
key_pem |
string |
"" |
Private key as an in-memory PEM string (overrides key_file) |
ca_file |
string |
"" |
Path to a CA bundle — required for mTLS |
require_client_cert |
bool |
false |
Reject connections without a client certificate |
verify_client_cert |
bool |
false |
Verify client certificate if presented (but don't require) |
handshake_timeout |
int |
15 |
Seconds to wait for TLS handshake before closing |
conf |
array |
[] |
Raw SSL_CONF_cmd commands — see below |
Paths are resolved relative to the process's current working directory, not to the
config.jsonfile. Either run the binary from the project root, or use absolute paths. On Windows, always use forward slashes ("C:/certs/server.crt") — backslashes are escape characters in JSON.
The conf array passes commands directly to SSL_CONF_cmd. Each entry is
a two-element array ["command", "value"], or a one-element array for
commands without arguments.
"conf": [
["CipherString", "ECDHE+AESGCM:ECDHE+CHACHA20"],
["Options", "-SessionTicket"],
["MinProtocol", "TLSv1.3"]
]These are passed verbatim to OpenSSL. See the OpenSSL documentation for
SSL_CONF_cmd for the full list. Brazier does not validate them — if
OpenSSL rejects a command, initialize() fails with a clear error.
Global HTTP limits shared by both Server and HttpsServer.
| Key | Type | Default | Description |
|---|---|---|---|
keep_alive_timeout |
int |
60 |
Seconds to keep an idle connection open (legacy keep-alive-timeout at top level is also accepted) |
max_connections |
int |
ulimit × 0.8 |
Max simultaneous connections; if unset, derived from RLIMIT_NOFILE |
max_body_size |
int |
auto | Max request body in bytes; auto-derived from RAM and max_connections |
max_header_size |
int |
auto | Max total request header size; auto-derived from RAM and max_connections |
max_connections_testing |
int |
0 |
Test-only override for max_connections |
max_body_size_testing |
int |
0 |
Test-only override for max_body_size |
max_header_size_testing |
int |
0 |
Test-only override for max_header_size |
Auto-derivation rules:
max_connections=RLIMIT_NOFILE × 0.8(POSIX) or16384 × 0.8(Windows)max_body_size=(RAM × 25%) / max_connections, clamped to[64 KB, 16 MB]max_header_size=(RAM × 1%) / max_connections, clamped to[4 KB, 32 KB]
Any explicit value in the config wins over auto-derivation. The _testing
keys take priority over everything else and are intended for the test suite.
#include "../include/brazier/Core"
#include "../include/brazier/DB"
#include "../include/brazier/Http"
#include "../include/brazier/Engine.hpp"
int main() {
try {
brazier::ConfigManager::initGlobal("config.json");
brazier::global_config->setAutoSave(false);
std::string host = brazier::global_config->get(
"https_server.host", "0.0.0.0");
int port = brazier::global_config->get(
"https_server.port", 8443);
brazier::HttpsServer server(host, port);
if (!server.initialize()) return 1;
server.run(); // blocks until stop()
return 0;
}
catch (const std::exception& e) {
std::cerr << "Fatal error: " << e.what() << std::endl;
return 1;
}
}HttpsServer reads everything it needs from https_server.* in
global_config: host, port, TLS settings, the conf array, handshake
timeout, and mTLS flags. You don't pass them explicitly — just make sure
ConfigManager::initGlobal() runs first.
When the certificate path is only known at runtime, or you're running
multiple HttpsServer instances with different certificates, pass a
TlsConfig directly:
brazier::HttpsServer::TlsConfig tls;
tls.cert_file = "/var/lib/myapp/api.crt";
tls.key_file = "/var/lib/myapp/api.key";
tls.conf = {
{ "min_protocol", "TLSv1.3" }
};
tls.handshake_timeout = std::chrono::seconds(10);
brazier::HttpsServer api("0.0.0.0", 9443, tls);
api.initialize();
api.run();When TlsConfig is passed explicitly, https_server.tls.* from the JSON
is ignored entirely — the code-level config wins.
If the certificate comes from a secret manager, a mounted Kubernetes secret,
or any source other than a plain file path, put the PEM content directly
into config.json using cert_pem / key_pem instead of cert_file /
key_file:
"https_server": {
"tls": {
"cert_pem": "-----BEGIN CERTIFICATE-----\nMIIF...\n-----END CERTIFICATE-----\n",
"key_pem": "-----BEGIN PRIVATE KEY-----\nMIIE...\n-----END PRIVATE KEY-----\n"
}
}Note the \n escapes — PEM is multi-line, and JSON strings must escape
newlines. The nlohmann::json parser converts them to real newlines before
OpenSSL sees them.
cert_pem may contain the full chain (leaf + intermediates) — Brazier parses
and registers each certificate automatically. key_pem must be the matching
private key; the pair is validated with SSL_CTX_check_private_key at load
time. If the key does not match the certificate, initialize() fails with
"Certificate/private key mismatch".
cert_pem takes priority over cert_file. If both are set, the in-memory
copy is used. This lets you supply a file path for hot-reload and a cached
PEM for the initial load — but note that reloadTls() (no args) needs
cert_file / key_file to know where to re-read from.
For programmatic sources (Vault API, custom secret fetch, database), skip
the config entirely and pass TlsConfig directly:
brazier::HttpsServer::TlsConfig tls;
tls.cert_pem = fetchPemFromVault("tls/server.crt");
tls.key_pem = fetchPemFromVault("tls/server.key");
brazier::HttpsServer server("0.0.0.0", 8443, tls);
server.initialize();When TlsConfig is passed explicitly, https_server.tls.* from config.json
is ignored entirely — the code-level config wins.
Reload certificates without restarting the server or dropping existing connections:
// Re-read from files (cert_file / key_file must be set)
server.reloadTls();
// Or supply a new config explicitly — useful for Vault / K8s / DB sources
brazier::HttpsServer::TlsConfig new_tls = server.getTlsConfig();
new_tls.cert_pem = fetchPemFromVault("tls/server.crt");
new_tls.key_pem = fetchPemFromVault("tls/server.key");
server.reloadTls(new_tls);Guarantees:
- Existing connections continue on the old
SSL_CTXand finish normally. - New handshakes use the new
SSL_CTX. - If the new config is invalid, the operation fails and the old
SSL_CTXstays active — the server is never left in a broken state. - Session ticket keys are shared across contexts, so resumption keeps working across reloads.
The reload is implemented with std::shared_ptr<ssl::context> plus a
std::shared_mutex. The cost per connection is one atomic refcount and one
shared-lock acquire — invoked once per connection, not per request.
Common trigger patterns:
// 1. Console command
std::thread([&server] {
std::string line;
while (std::getline(std::cin, line)) {
if (line == "reload") server.reloadTls();
if (line == "quit") server.stop();
}
}).detach();
// 2. SIGHUP (Linux / macOS)
std::signal(SIGHUP, [](int) {
g_reload_requested.store(true, std::memory_order_release);
});
// ... in a background thread ...
if (g_reload_requested.exchange(false)) server.reloadTls();
// 3. File watcher
std::thread([&server] {
fs::file_time_type last{};
while (server.running()) {
auto now = fs::last_write_time("app/certs/server.crt");
if (last != fs::file_time_type{} && now != last) {
server.reloadTls();
}
last = now;
std::this_thread::sleep_for(std::chrono::seconds(5));
}
}).detach();Brazier uses stateless session tickets (RFC 5077) — no per-session state is kept on the server. This is the only resumption mechanism in TLS 1.3, and it is the recommended mechanism for TLS 1.2 as well.
Tickets are encrypted with rotating keys:
- Rotation interval — a new key every 12 hours.
- Key lifetime — old keys are kept for 48 hours, so clients with valid tickets can still resume.
- Per-server isolation — each
HttpsServerhas its own key store. Multiple servers in the same process do not share tickets.
Resumption typically costs 0.3–0.8 ms versus 2–3 ms for a full handshake — roughly a 10× CPU reduction on resumed sessions. This matters most for connection-churn workloads.
On startup, HttpsServer creates N io_context instances, one per CPU core:
| Platform | io_contexts | Acceptors | Dispatch |
|---|---|---|---|
| Linux / macOS | hardware_concurrency() |
same as io_contexts | SO_REUSEPORT — kernel hashes 4-tuples |
| Windows | hardware_concurrency() |
1 | round-robin co_spawn from a single acceptor |
Each io_context owns its own thread, and each thread owns its own IOCP (Windows) or epoll instance (Linux). There is no cross-thread signalling on the hot path — completions for a connection always run on the thread that owns its io_context.
The startup log tells you exactly what happened:
[SUCCESS] HTTPS server initialized on 0.0.0.0:8443 [TLS, io_contexts=12, acceptors=1, dispatch=round-robin, SO_REUSEPORT=no, TCP_DEFER_ACCEPT=no]
[INFO] Starting 12 io_context(s) over 12 thread(s), dispatch=round-robin
mTLS requires each client to present a certificate signed by a CA you trust. This is common for service-to-service communication, IoT devices, and internal APIs.
Enable it in the config:
"tls": {
"cert_file": "app/certs/server.crt",
"key_file": "app/certs/server.key",
"ca_file": "app/certs/ca.crt",
"require_client_cert": true
}With require_client_cert: true, the server sends a CertificateRequest
during the handshake. Clients that cannot present a valid certificate are
rejected before any HTTP request is processed — the TLS handshake
simply fails.
Set verify_client_cert: true (without require_client_cert) to ask for a
client certificate but accept clients that don't present one. Useful when
client certs are optional.
ca_fileis required whenrequire_client_cert: true. Without a CA bundle the server has no way to validate client certificates.
For every response, HttpsServer sets:
| Header | Value | Why |
|---|---|---|
Server |
brazier (configurable) |
Identifies the framework |
Strict-Transport-Security |
max-age=31536000 (configurable) |
HSTS — tells browsers to use HTTPS for a year |
Connection |
keep-alive or close |
Matches the request |
Configure via http.server_name, http.hsts_enabled, and http.hsts_header.
You don't need to set these in your controllers.
| Phase | Timeout | Config key |
|---|---|---|
| TLS handshake | 15 s | https_server.tls.handshake_timeout |
| Idle keep-alive | 60 s | http.keep_alive_timeout (or legacy keep-alive-timeout) |
| Graceful TLS shutdown | 5 s | — |
| Graceful server shutdown | 10 s | — |
If any of these fire, the connection is closed cleanly — you'll see a
corresponding [DEBUG] HTTPS client disconnected line in the log, not an
error.
server.stop(); // returns after all in-flight connections complete (up to 10 s)stop() performs a graceful shutdown:
- Acceptors are closed — no new connections.
- The work guard is released —
io_context::run()exits when idle. - Waits up to 10 seconds for the active connection count to reach zero.
io_context::stop()— force-cancels anything still running.- Worker threads are joined.
If run() is executing on a different thread, join that thread after
stop() returns. Do not call stop() from inside a request handler —
it will deadlock waiting for the calling connection to close.
// Basic request (accepts self-signed certificates)
curl -vk https://localhost:8443/
// Strict verification against your own CA
curl -v --cacert app/certs/server.crt https://localhost:8443/
// Inspect the handshake
openssl s_client -connect localhost:8443 -servername localhost
// Verify that TLS 1.1 is rejected
openssl s_client -connect localhost:8443 -tls1_1Windows
curl.exeuses Schannel, which does not support ECDSA server certificates. If your cert is ECDSA P-256, use curl fromGit for Windows(which links against OpenSSL), or stick withopenssl s_client.ab(ApacheBench) uses its own OpenSSL and works with both cert types.
-
use_certificate_chain_file: cannot find the file— the path incert_fileis relative to the process's current working directory, not the config file. Run from the project root or use absolute paths. -
Certificate/private key mismatch— the cert and key incert_file/key_filedon't belong to the same pair. Brazier rejects this at startup with a clear error instead of failing later at handshake time. -
SSL_CONF_cmd failed for 'min_protocol=...'— some OpenSSL builds (notably via vcpkg) don't registermin_protocolinSSL_CONF_cmd. Use TLS options in code instead, or check the OpenSSL documentation for the correct command name in your build. -
no shared cipher/alert 40on handshake — the client and server could not agree on a cipher suite. The usual cause is a mismatch between the certificate key type and the client's cipher list: an ECDSA certificate cannot satisfy an RSA-only client, and vice versa. -
Browser says "certificate not trusted" — expected for self-signed certificates. Either click through the warning, add the cert to the user root store, or use a certificate from a public CA.
-
WSAECONNRESET/WSAECONNABORTEDin logs — these are normal client disconnect events, not errors. Brazier logs them atDEBUGlevel. -
TLS shutdown erroron every connection — this isAPPLICATION_DATA_AFTER_CLOSE_NOTIFY, a benign artefact of clients that send data after the shutdown alert. Logged atDEBUGand safe to ignore. -
Hot-reload says
reloadTls: source is PEM, nothing to reload— you supplied the certificate only ascert_pem/key_pem, with nocert_file/key_file. Set the file paths as well, or callreloadTls(new_tls)with fresh PEM strings from your secret source.
Brazier provides a complete WebSocket routing system with support for parameterized paths, multiple message types, and global handlers. The system integrates seamlessly with the existing HTTP router.
WebSocketRouter is a singleton class that manages all WebSocket routes and their handlers.
| Method | Description |
|---|---|
instance() |
Returns singleton instance |
add_route(path, handler) |
Registers a new WebSocket route |
find_route(target, params) |
Finds route matching the target path |
set_global_message_handler(handler) |
Sets global text message handler |
set_global_binary_handler(handler) |
Sets global binary message handler |
set_global_close_handler(handler) |
Sets global close handler |
set_global_error_handler(handler) |
Sets global error handler |
Handles new WebSocket connection establishment.
Handler Signature:
void handler(std::shared_ptr<WebSocketSession> session, const Params& params)Use Case: Initialize connection, send welcome message, set up session-specific data.
class ChatController {
public:
void onConnect(std::shared_ptr<WebSocketSession> session, const Params& params) {
std::string room = params.at("room");
session->send("Welcome to room: " + room);
}
};
WS_R("/chat/:room", ChatController, onConnect);Handles incoming text messages.
Handler Signature:
void handler(const std::string& message, std::shared_ptr<WebSocketSession> session)Use Case: Process chat messages, commands, JSON data.
class ChatController {
public:
void onMessage(const std::string& message, std::shared_ptr<WebSocketSession> session) {
if (message == "ping") {
session->send("pong");
} else {
session->send("Echo: " + message);
}
}
};
WS_MSG("/chat/:room", ChatController, onMessage);Handles incoming binary messages.
Handler Signature:
void handler(std::vector<uint8_t>&& data, std::shared_ptr<WebSocketSession> session)Use Case: File uploads, image transfers, binary protocols.
class FileController {
public:
void onBinary(std::vector<uint8_t>&& data, std::shared_ptr<WebSocketSession> session) {
std::ofstream file("upload_" + std::to_string(time(nullptr)) + ".bin", std::ios::binary);
file.write(reinterpret_cast<const char*>(data.data()), data.size());
session->send("File received: " + std::to_string(data.size()) + " bytes");
}
};
WS_BIN("/upload", FileController, onBinary);Handles connection closure events.
Handler Signature:
void handler(std::shared_ptr<WebSocketSession> session)Use Case: Cleanup resources, update user lists, log disconnections.
class ChatController {
public:
void onClose(std::shared_ptr<WebSocketSession> session) {
active_users_.erase(session->get_id());
WebSocketManager::instance().broadcast("User " + std::string(session->get_id()) + " left");
}
};
WS_CLOSE("/chat/:room", ChatController, onClose);Handles WebSocket errors.
Handler Signature:
void handler(const std::string& error, std::shared_ptr<WebSocketSession> session)Use Case: Logging, error recovery, client notification.
class ChatController {
public:
void onError(const std::string& error, std::shared_ptr<WebSocketSession> session) {
Logger::log("WebSocket error: " + error, "ERROR");
session->send("Error occurred: " + error);
}
};
WS_ERR("/chat/:room", ChatController, onError);| Macro | Description |
|---|---|
WS_R(path, controller, handler) |
Register WebSocket connect handler |
WS_MSG(path, controller, handler) |
Register text message handler |
WS_BIN(path, controller, handler) |
Register binary message handler |
WS_CLOSE(path, controller, handler) |
Register close handler |
WS_ERR(path, controller, handler) |
Register error handler |
Global handlers are called for every WebSocket connection, regardless of the route path.
| Macro | Description |
|---|---|
WS_G_MSG(controller, handler) |
Global text message handler |
WS_G_BIN(controller, handler) |
Global binary message handler |
WS_G_CLOSE(controller, handler) |
Global close handler |
WS_G_ERR(controller, handler) |
Global error handler |
class GlobalController {
public:
void onGlobalMessage(const std::string& message, std::shared_ptr<WebSocketSession> session) {
// Log every message
Logger::log("Message from " + std::string(session->get_id()) + ": " + message);
// Common commands available on any path
if (message == "help") {
session->send("Available commands: stats, time, ping");
} else if (message == "stats") {
session->send("Active sessions: " +
std::to_string(WebSocketManager::instance().get_session_count()));
}
}
void onGlobalClose(std::shared_ptr<WebSocketSession> session) {
Logger::log("Session closed: " + std::string(session->get_id()));
}
};
// Register global handlers (called once)
WS_G_MSG(GlobalController, onGlobalMessage);
WS_G_CLOSE(GlobalController, onGlobalClose);Route-specific handlers have priority over global handlers
Global handlers are called when no route-specific handler exists
Global close and error handlers are always called in addition to route-specific ones
Client connects
↓
Find matching route
↓
┌─────────────────────────────────────┐
│ If route has message handler: │
│ → Use route handler │
│ Else: │
│ → Use global message handler │
└─────────────────────────────────────┘
↓
Close and Error: route + global both called
Controllers in brazier are central components that handle HTTP requests and return responses. They implement application business logic, work with data models, and form HTTP responses.
#include <brazier/Core>
#include <brazier/DB>
#include <brazier/Http>
class UserController : public brazier::Controller {
public:
using Request = http::request<http::string_body>;
using Response = http::response<http::string_body>;
using Params = std::unordered_map<std::string, std::string>;
// Override REST methods
boost::asio::awaitable<void> show(const Request& req, Response& res, const Params& params) override;
boost::asio::awaitable<void> store(const Request& req, Response& res, const Params& params) override;
boost::asio::awaitable<void> update(const Request& req, Response& res, const Params& params) override;
boost::asio::awaitable<void> delete_(const Request& req, Response& res, const Params& params) override;
// Custom methods
boost::asio::awaitable<void> login(const Request& req, Response& res, const Params& params);
boost::asio::awaitable<void> logout(const Request& req, Response& res, const Params& params);
// CORS handler
void setCors(const Request& req, Response& res) override;
};The base Controller class provides default implementations that return 405 Method Not Allowed.
You can override the REST methods.
| Method | HTTP Method | Default Behavior | Typical Use |
|---|---|---|---|
show(req, res, params) |
GET | 405 Not Allowed | Get single resource by ID |
store(req, res, params) |
POST | 405 Not Allowed | Create new resource |
update(req, res, params) |
PUT | 405 Not Allowed | Update existing resource |
delete_(req, res, params) |
DELETE | 405 Not Allowed | Delete resource |
class UserController : public brazier::Controller {
public:
// GET /users/:id - Get single user
boost::asio::awaitable<void> show(const Request& req, Response& res, const Params& params) override {
auto userId = std::stoi(params.at("id"));
auto user = co_await User::find(userId);
if (!user) {
res.result(http::status::not_found);
co_return;
}
res.result(http::status::ok);
res.body() = user.toJson().dump();
res.set(http::field::content_type, "application/json");
co_return;
}
// POST /users - Create new user
boost::asio::awaitable<void> store(const Request& req, Response& res, const Params& params) override {
auto body = nlohmann::json::parse(req.body());
auto user = co_await User::create(body);
res.result(http::status::created);
res.body() = user.toJson().dump();
res.set(http::field::content_type, "application/json");
co_return;
}
// PUT /users/:id - Update user
boost::asio::awaitable<void> update(const Request& req, Response& res, const Params& params) override {
auto userId = std::stoi(params.at("id"));
auto body = nlohmann::json::parse(req.body());
auto user = co_await User::update(userId, body);
res.result(http::status::ok);
res.body() = user.toJson().dump();
co_return;
}
// DELETE /users/:id - Delete user
boost::asio::awaitable<void> delete_(const Request& req, Response& res, const Params& params) override {
auto userId = std::stoi(params.at("id"));
co_await User::remove(userId);
res.result(http::status::no_content);
co_return;
}
};You can add any number of custom methods beyond the base REST methods:
class UserController : public brazier::Controller {
public:
// Custom method - not in base Controller
boost::asio::awaitable<void> login(const Request& req, Response& res, const Params& params) {
auto body = nlohmann::json::parse(req.body());
std::string email = body["email"];
std::string password = body["password"];
auto user = co_await User::authenticate(email, password);
if (!user) {
res.result(http::status::unauthorized);
co_return;
}
std::string token = generateJWT(user);
nlohmann::json response;
response["token"] = token;
response["user"] = user.toJson();
res.result(http::status::ok);
res.body() = response.dump();
res.set(http::field::content_type, "application/json");
co_return;
}
boost::asio::awaitable<void> logout(const Request& req, Response& res, const Params& params) {
// Clear session, invalidate token
res.result(http::status::ok);
co_return;
}
boost::asio::awaitable<void> profile(const Request& req, Response& res, const Params& params) {
// Get current user profile
auto userId = getCurrentUserId(req);
auto user = co_await User::find(userId);
res.result(http::status::ok);
res.body() = user.toJson().dump();
co_return;
}
};The base Controller class provides default CORS handlers:
void Controller::setCors(const Request& req, Response& res) {
res.set(http::field::access_control_allow_origin, "*");
res.set(http::field::access_control_allow_credentials, "true");
res.set(http::field::access_control_allow_methods, "GET, POST, PUT, DELETE, OPTIONS");
res.set(http::field::access_control_allow_headers, "*");
res.set(http::field::access_control_expose_headers, "Set-Cookie");
}
void Controller::setCorsHeaders(const Request& req, Response& res) {
this->setCors(req, res);
}class ApiController : public brazier::Controller {
public:
void setCors(const Request& req, Response& res) override {
// Restrict to specific origin
res.set(http::field::access_control_allow_origin, "https://myapp.com");
res.set(http::field::access_control_allow_credentials, "true");
res.set(http::field::access_control_allow_methods, "GET, POST, PUT, DELETE");
res.set(http::field::access_control_allow_headers, "Content-Type, Authorization, X-API-Key");
res.set(http::field::access_control_expose_headers, "X-Total-Count");
}
};
class PublicController : public brazier::Controller {
public:
void setCors(const Request& req, Response& res) override {
// Allow any origin (public API)
std::string origin = req.at(http::field::origin);
res.set(http::field::access_control_allow_origin, origin);
res.set(http::field::access_control_allow_methods, "GET, OPTIONS");
}
};boost::asio::awaitable<void> createUser(const Request& req, Response& res, const Params& params) {
try {
// Parse JSON body
auto body = nlohmann::json::parse(req.body());
// Validate required fields
if (!body.contains("name") || !body.contains("email")) {
res.result(http::status::bad_request);
res.body() = R"({"error":"Missing required fields"})";
co_return;
}
std::string name = body["name"];
std::string email = body["email"];
// Create user
auto user = co_await User::create(name, email);
res.result(http::status::created);
res.body() = user.toJson().dump();
res.set(http::field::content_type, "application/json");
} catch (const nlohmann::json::parse_error& e) {
res.result(http::status::bad_request);
res.body() = R"({"error":"Invalid JSON"})";
} catch (const std::exception& e) {
res.result(http::status::internal_server_error);
res.body() = R"({"error":"Internal server error"})";
}
co_return;
}boost::asio::awaitable<void> getUser(const Request& req, Response& res, const Params& params) {
// Get parameters from URL (e.g., /users/:id)
auto userId = params.at("id");
// Optional parameters with default
std::string fields = params.count("fields") ? params.at("fields") : "all";
auto user = co_await User::find(std::stoi(userId));
if (!user) {
res.result(http::status::not_found);
res.body() = R"({"error":"User not found"})";
co_return;
}
nlohmann::json response;
if (fields == "all") {
response = user.toJson();
} else {
response = user.toJson(fields);
}
res.result(http::status::ok);
res.body() = response.dump();
res.set(http::field::content_type, "application/json");
co_return;
}boost::asio::awaitable<void> getUsers(const Request& req, Response& res, const Params& params) {
// Parse query string
auto target = std::string(req.target());
auto query_pos = target.find('?');
std::unordered_map<std::string, std::string> query;
if (query_pos != std::string::npos) {
std::string query_string = target.substr(query_pos + 1);
// Parse key=value&key2=value2
// ...
}
int page = std::stoi(query.count("page") ? query["page"] : "1");
int limit = std::stoi(query.count("limit") ? query["limit"] : "20");
auto users = co_await User::paginate(page, limit);
nlohmann::json response;
response["data"] = users;
response["page"] = page;
response["limit"] = limit;
res.result(http::status::ok);
res.body() = response.dump();
res.set(http::field::content_type, "application/json");
co_return;
}boost::asio::awaitable<void> authEndpoint(const Request& req, Response& res, const Params& params) {
// Get authorization header
auto auth = req.find(http::field::authorization);
if (auth == req.end()) {
res.result(http::status::unauthorized);
co_return;
}
std::string token = auth->value().to_string();
// Remove "Bearer " prefix if present
if (token.rfind("Bearer ", 0) == 0) {
token = token.substr(7);
}
auto user = co_await validateToken(token);
if (!user) {
res.result(http::status::unauthorized);
co_return;
}
// Continue with request
co_return;
}// 200 OK with JSON
res.result(http::status::ok);
res.body() = json.dump();
res.set(http::field::content_type, "application/json");
res.prepare_payload();
// 201 Created
res.result(http::status::created);
res.body() = createdResource.toJson().dump();
res.set(http::field::location, "/api/users/" + std::to_string(id));
res.prepare_payload();
// 204 No Content (for DELETE)
res.result(http::status::no_content);
res.body() = "";
res.prepare_payload();
// 400 Bad Request
res.result(http::status::bad_request);
res.body() = R"({"error":"Invalid input"})";
res.prepare_payload();
// 401 Unauthorized
res.result(http::status::unauthorized);
res.set(http::field::www_authenticate, "Bearer");
res.body() = R"({"error":"Authentication required"})";
res.prepare_payload();
// 403 Forbidden
res.result(http::status::forbidden);
res.body() = R"({"error":"Access denied"})";
res.prepare_payload();
// 404 Not Found
res.result(http::status::not_found);
res.body() = R"({"error":"Resource not found"})";
res.prepare_payload();
// 405 Method Not Allowed
res.result(http::status::method_not_allowed);
res.set(http::field::allow, "GET, POST");
res.body() = R"({"error":"Method not allowed"})";
res.prepare_payload();
// 500 Internal Server Error
res.result(http::status::internal_server_error);
res.body() = R"({"error":"Internal server error"})";
res.prepare_payload();res.set(http::field::content_type, "application/json");
res.set(http::field::cache_control, "no-cache");
res.set(http::field::expires, "0");
res.set(http::field::x_custom_header, "custom_value");
res.set("X-RateLimit-Limit", "100");
res.set("X-RateLimit-Remaining", "95");A convenience container that extends std::vector<std::shared_ptr> with helper methods for common collection operations — persistence, deletion, JSON serialization, and functional-style querying.
It is designed to work with model classes that follow the brazier ORM conventions (specifically, a static saveMany(...) method and instance methods delete_() / toJson()).
#include <brazier/DB>Collection inherits all constructors from std::vector<std::shared_ptr<ModelType>>, plus a few convenience overloads.
Collection<User> users;
Collection<User> users = { std::make_shared<User>(...), std::make_shared<User>(...) };
std::vector<std::shared_ptr<User>> vec = ...;
Collection<User> users(vec);
Collection<User> users(std::move(vec));bool save()
Persists the entire collection by delegating to ModelType::saveMany(*this).
Returns true on success, false on failure.
Returns false immediately (without calling saveMany) if the collection is empty.
Collection<User> users;
users.push_back(std::make_shared<User>("User 1"));
users.push_back(std::make_shared<User>("User 2"));
if (!users.save()) {
fail();
}Note: The exact semantics of saveMany are defined by your model. Collection only forwards the call.
bool delete_()
Deletes every model in the collection by calling item->delete_() on each element.
Iterates over the whole collection, continuing on error.
If an individual delete_() throws, the exception is logged via
Logger::log(..., "ERROR") and the loop continues with the next item.
Returns true only if all deletions succeeded; false if the collection is empty or any item failed.
The trailing underscore in delete_ avoids clashing with the C++ keyword delete.
Collection<User> users = User::where("inactive = true");
bool ok = users.delete_();
if (!ok) {
fail();
}json toJson() const
Returns a nlohmann::json array where each element is the result of item->toJson().
Collection<User> users = ...;
json j = users.toJson();
std::cout << j.dump(2) << std::endl;Output:
[
{ "id": 1, "name": "User 1" },
{ "id": 2, "name": "User 2" }
]All query methods take a predicate of type
std::function<bool(std::shared_ptr<ModelType>)> and do not modify the collection.
Collection<ModelType> filter(predicate) const
Returns a new Collection containing only the elements for which predicate returns true.
auto admins = users.filter([](auto u) { return u->isAdmin(); });
bool all(predicate) constReturns true if the predicate holds for every element. Returns true for an empty collection (vacuous truth, matching std::all_of).
bool everyoneActive = users.all([](auto u) { return u->active; });
bool any(predicate) constReturns true if the predicate holds for at least one element. Returns false for an empty collection.
bool hasAdmin = users.any([](auto u) { return u->isAdmin(); });
std::shared_ptr<ModelType> find(predicate) constReturns the first element matching the predicate, or nullptr if none match.
auto alice = users.find([](auto u) { return u->name == "Alice"; });
Brazier provides a comprehensive set of utility classes (helpers) for common tasks:
- working with cookies;
- code generation;
- data validation;
- HTTP requests;
- email sending;
- other helper functions.
Purpose
Generate random codes for various purposes: verification codes, one-time passwords, authentication tokens, backup codes, and API keys.
| Method | Description | Return Type |
|---|---|---|
generateRandomCode(length) |
Generate random alphanumeric code | std::string |
generateNumericCode(length) |
Generate numeric-only code | std::string |
generateMultipleCodes(count, length, numeric) |
Generate multiple codes at once | std::vector<std::string> |
base64_encode(bytes) |
Base64 encode string | std::string |
Single code generation
#include <brazier/App/Http/Helpers/Code.hpp>
// Alphanumeric code (letters + digits)
std::string verificationCode = Code::generateRandomCode(16);
// Example output: "Ab3kL9mN2pXy8zQ1w"
// Numeric code (only digits)
std::string smsCode = Code::generateNumericCode(6);
// Example output: "384290"
// Email verification token
std::string emailToken = Code::generateRandomCode(32);
Multiple codes generation
cpp
// Generate 10 backup codes (alphanumeric, 8 chars each)
auto backupCodes = Code::generateMultipleCodes(10, 8);
// Example output: ["Ab3kL9mN", "Xy8zQ1wV", "r4F7tY2p", ...]
// Generate 5 numeric OTP codes (4 digits each)
auto otpCodes = Code::generateMultipleCodes(5, 4, true);
// Example output: ["3842", "9017", "5623", "7481", "2395"]
// Generate API keys
auto apiKeys = Code::generateMultipleCodes(100, 32);
Base64 encoding
cpp
std::string original = "Hello World";
std::string encoded = Code::base64_encode(original);
// Output: "SGVsbG8gV29ybGQ="
// For SMTP authentication
std::string authString = '\0' + username + '\0' + password;
std::string encodedAuth = Code::base64_encode(authString);
Real-world examples
cpp
// Generate verification code for email
std::string generateEmailVerificationCode() {
return Code::generateRandomCode(32);
}
// Generate 2FA backup codes
std::vector<std::string> generate2FABackupCodes() {
return Code::generateMultipleCodes(10, 8);
}
// Generate session token
std::string generateSessionToken() {
return Code::generateRandomCode(64);
}
// Generate numeric OTP
std::string generateOTP() {
return Code::generateNumericCode(6);
}Purpose
Simplified HTTP cookie handling: parsing cookies from requests and setting cookies in responses.
| Method | Description | Return Type |
|---|---|---|
parseCookies(cookieHeader) |
Parse cookie header string into key-value map | std::map<std::string, std::string> |
set(response, cookies) |
Set multiple cookies in HTTP response | void |
Parsing cookies from request
#include <brazier/App/Http/Helpers/Cookie.hpp>
boost::asio::awaitable<void> show(const Request& req, Response& res, const Params& params) {
// Get cookie header from request
auto cookieHeader = req.find(http::field::cookie);
if (cookieHeader != req.end()) {
auto cookies = Cookie::parseCookies(std::string(cookieHeader->value()));
std::string sessionId = cookies["session_id"];
std::string userId = cookies["user_id"];
std::string theme = cookies["theme"];
// Use cookie values
if (cookies.find("auth_token") != cookies.end()) {
// User is authenticated
}
}
co_return;
}Setting cookies in response
boost::asio::awaitable<void> login(const Request& req, Response& res, const Params& params) {
// Single cookie
std::map<std::string, std::string> cookies;
cookies["session_id"] = Code::generateRandomCode(32);
Cookie::set(res, cookies);
// Multiple cookies
std::map<std::string, std::string> authCookies = {
{"auth_token", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"},
{"user_id", "12345"},
{"session_id", Code::generateRandomCode(32)},
{"refresh_token", Code::generateRandomCode(64)}
};
Cookie::set(res, authCookies);
res.result(http::status::ok);
co_return;
}Complete authentication example
class AuthController : public brazier::Controller {
public:
boost::asio::awaitable<void> login(const Request& req, Response& res, const Params& params) {
auto body = nlohmann::json::parse(req.body());
std::string email = body["email"];
std::string password = body["password"];
auto user = co_await authenticate(email, password);
if (!user) {
res.result(http::status::unauthorized);
co_return;
}
// Set authentication cookies
std::map<std::string, std::string> cookies = {
{"access_token", generateAccessToken(user)},
{"refresh_token", generateRefreshToken(user)},
{"user_id", std::to_string(user.id)},
{"session_id", Code::generateRandomCode(32)}
};
Cookie::set(res, cookies);
res.result(http::status::ok);
co_return;
}
boost::asio::awaitable<void> logout(const Request& req, Response& res, const Params& params) {
// Clear cookies by setting expired values
std::map<std::string, std::string> expiredCookies = {
{"access_token", ""},
{"refresh_token", ""},
{"user_id", ""},
{"session_id", ""}
};
Cookie::set(res, expiredCookies);
res.result(http::status::ok);
co_return;
}
};Purpose
Validate user input data: passwords, email addresses, and other common data types.
| Method | Description | Return Type |
|---|---|---|
password(password) |
Validate password | bool |
email(email) |
Validate email format (RFC 5322) | bool |
passwordError(password) |
Get detailed password error message | std::string |
Password Requirements:
- Length: 8-20 characters
- At least one digit (0-9)
- At least one uppercase letter (A-Z)
- At least one special character (!@#$%^&* etc.)
Basic validation
#include <brazier/App/Http/Helpers/Validator.hpp>
// Password validation
if (Validator::password("MySecure123!")) {
// Password is valid
} else {
std::string error = Validator::passwordError("MySecure123!");
}
// Email validation
if (Validator::email("user@example.com")) {
// Email is valid
} else {
// Invalid email format
}Complete registration example
boost::asio::awaitable<void> register(const Request& req, Response& res, const Params& params) {
auto body = nlohmann::json::parse(req.body());
std::string email = body["email"];
std::string password = body["password"];
// Validate email
if (!Validator::email(email)) {
res.result(http::status::bad_request);
res.body() = R"({"error":"Invalid email format"})";
co_return;
}
// Validate password
if (!Validator::password(password)) {
std::string error = Validator::passwordError(password);
nlohmann::json response;
response["error"] = error;
res.body() = response.dump();
res.result(http::status::bad_request);
co_return;
}
// Create user
auto user = co_await User::create(email, password);
res.result(http::status::created);
co_return;
}Password validation with detailed errors
std::string validatePasswordWithDetails(const std::string& password) {
if (password.size() < 8) {
return "Password must be at least 8 characters long";
}
if (password.size() > 20) {
return "Password must not exceed 20 characters";
}
if (Validator::password(password)) {
return ""; // Valid
}
return Validator::passwordError(password);
}
// Usage
std::string error = validatePasswordWithDetails("weak");
if (!error.empty()) {
// Show error to user
}The HttpClient class provides asynchronous methods for performing HTTP/HTTPS requests to external APIs.
It is built on Boost.Beast and Boost.Asio with C++20 coroutine support (awaitable). All network
operations are non‑blocking, making it suitable for concurrent environments.
| Method | Description | Return Type |
|---|---|---|
get(url, body = json{}) |
GET request; body is converted to query string |
net::awaitable<Response> |
post(url, body) |
POST request with JSON body | net::awaitable<Response> |
put(url, body) |
PUT request to update a resource | net::awaitable<Response> |
del(url, body = json{}) |
DELETE request; body is optional |
net::awaitable<Response> |
set_timeout(timeout) |
Sets timeout (milliseconds) for all operations | void |
set_verify_ssl(verify) |
Enable/disable SSL certificate verification (on by default) | void |
is_success(response) |
Checks if the response status is 2xx | bool |
Include the header:
#include <brazier/Http>All request calls must be performed inside a coroutine launched via net::co_spawn or similar.
#include <brazier/Http>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/use_awaitable.hpp>
using namespace brazier;
net::awaitable<void> fetch_users() {
HttpClient client;
nlohmann::json params = {
{"page", 1},
{"limit", 10}
};
Response response = co_await client.get("https://api.example.com/users", params);
if (client.is_success(response)) {
auto users = nlohmann::json::parse(response.body());
// process users
}
}
int main() {
net::io_context ioc;
net::co_spawn(ioc, fetch_users(), net::detached);
ioc.run();
}net::awaitable<void> create_user() {
HttpClient client;
nlohmann::json userData = {
{"name", "John Doe"},
{"email", "john@example.com"},
{"age", 30}
};
Response response = co_await client.post("https://api.example.com/users", userData);
if (response.result() == http::status::created) {
auto createdUser = nlohmann::json::parse(response.body());
std::cout << "User created with ID: " << createdUser["id"] << std::endl;
}
}net::awaitable<void> update_user() {
HttpClient client;
nlohmann::json updateData = {
{"name", "Jane Doe"},
{"age", 31}
};
Response response = co_await client.put("https://api.example.com/users/123", updateData);
if (client.is_success(response)) {
// update successful
}
}net::awaitable<void> delete_user() {
HttpClient client;
nlohmann::json emptyBody; // or just {}
Response response = co_await client.del("https://api.example.com/users/123", emptyBody);
if (response.result() == http::status::no_content) {
// user deleted
}
}net::awaitable<void> fetch_with_timeout() {
HttpClient client;
client.set_timeout(std::chrono::milliseconds(5000)); // 5 seconds
try {
Response response = co_await client.get("https://slow-api.example.com/data");
// process response
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
}net::awaitable<void> test_self_signed() {
HttpClient client;
client.set_verify_ssl(false);
Response response = co_await client.get("https://self-signed.badssl.com");
// ...
}- Coroutines: all request methods must be called from within a coroutine launched with
net::co_spawnand the appropriate executor. - Exceptions: errors (invalid URL, timeout, DNS, SSL) throw exceptions. Always handle them in your code.
- Thread safety:
HttpClientobjects are not thread‑safe; use separate instances per thread or synchronise access. - Headers: the library automatically adds
Host,User‑Agent,Accept, andConnection: close. For POST/PUT/DELETE it also setsContent‑Type: application/json. - Timeouts: apply to each phase (connect, write, read). A timeout throws an exception.
Purpose
Send emails using SMTP protocol with SSL/TLS encryption support.
| Method | Description | Return Type |
|---|---|---|
send(recipient, message, sender_name) |
Send email to recipient | net::awaitable<int> |
Configuration
The SmtpClient uses environment variables for configuration:
| Variable | Description |
|---|---|
SMTP_HOST |
SMTP server hostname |
SMTP_PORT |
SMTP server port |
SMTP_USERNAME |
Authentication username |
SMTP_PASSWORD |
Authentication password |
S_HOST |
Local hostname for EHLO |
Basic email sending
#include <brazier/App/Http/Helpers/SmtpClient.hpp>
boost::asio::awaitable<void> sendEmail(const Request& req, Response& res, const Params& params) {
SmtpClient smtp;
std::string recipient = "user@example.com";
std::string message = "<h1>Welcome!</h1><p>Thank you for registering.</p>";
std::string senderName = "My App Support";
int result = co_await smtp.send(recipient, message, senderName);
if (result == 0) {
res.result(http::status::ok);
res.body() = "Email sent successfully";
} else {
res.result(http::status::internal_server_error);
res.body() = "Failed to send email";
}
co_return;
}HTML email with formatting
std::string createHtmlEmail(const std::string& username, const std::string& verificationCode) {
std::string html = R"(
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; }
.container { padding: 20px; background-color: #f4f4f4; }
.content { background-color: white; padding: 20px; border-radius: 5px; }
.code { font-size: 24px; font-weight: bold; color: #0066cc; }
</style>
</head>
<body>
<div class="container">
<div class="content">
<h2>Welcome, )" + username + R"(!</h2>
<p>Please verify your email address using the code below:</p>
<p class="code">)" + verificationCode + R"(</p>
<p>This code expires in 24 hours.</p>
</div>
</div>
</body>
</html>
)";
return html;
}
boost::asio::awaitable<void> sendVerificationEmail(const std::string& email, const std::string& username) {
SmtpClient smtp;
std::string verificationCode = Code::generateRandomCode(32);
std::string htmlMessage = createHtmlEmail(username, verificationCode);
int result = co_await smtp.send(email, htmlMessage, "Verification Service");
if (result == 0) {
// Save verification code to database
co_await saveVerificationCode(email, verificationCode);
}
co_return;
}Complete registration with email
class AuthController : public brazier::Controller {
public:
boost::asio::awaitable<void> register(const Request& req, Response& res, const Params& params) {
auto body = nlohmann::json::parse(req.body());
std::string email = body["email"];
std::string username = body["username"];
std::string password = body["password"];
// Validate input
if (!Validator::email(email)) {
res.result(http::status::bad_request);
res.body() = R"({"error":"Invalid email"})";
co_return;
}
if (!Validator::password(password)) {
nlohmann::json response;
response["error"] = Validator::passwordError(password);
res.body() = response.dump();
res.result(http::status::bad_request);
co_return;
}
// Create user
auto user = co_await User::create(email, username, password);
// Send welcome email
SmtpClient smtp;
std::string welcomeMessage = createWelcomeEmail(username);
co_await smtp.send(email, welcomeMessage, "MyApp Team");
// Send verification email
std::string verificationCode = Code::generateRandomCode(32);
std::string verifyMessage = createVerificationEmail(username, verificationCode);
co_await smtp.send(email, verifyMessage, "Verification Service");
// Save verification code
co_await saveVerificationCode(email, verificationCode);
res.result(http::status::created);
nlohmann::json response;
response["message"] = "User registered. Please check your email for verification.";
res.body() = response.dump();
co_return;
}
private:
std::string createWelcomeEmail(const std::string& username) {
return R"(
<h1>Welcome, )" + username + R"(!</h1>
<p>Thank you for joining MyApp!</p>
<p>We're excited to have you on board.</p>
)";
}
std::string createVerificationEmail(const std::string& username, const std::string& code) {
return R"(
<h1>Verify Your Email</h1>
<p>Hello )" + username + R"(,</p>
<p>Please click the link below to verify your email address:</p>
<p><a href="https://myapp.com/verify?code=)" + code + R"(">Verify Email</a></p>
<p>This link expires in 24 hours.</p>
)";
}
};