A lightweight, powerful web server designed for constrained environments
Beauty was born with the realization that Asio is supported on ESP32 microcontrollers, however Beauty runs anywhere Asio is supported, from tiny IoT devices to high-performance servers.
Inspired by Express.js - Familiar middleware pattern with predictable execution order
Born for Embedded - Developed specifically for ESP32 but scales to any platform
Production Ready - Used in real-world IoT deployments and web applications
| Feature | Description |
|---|---|
| HTTP/1.1 Support | Full HTTP/1.1 with configurable persistent connections |
| Multi-part Upload | Handle large file uploads with customizable validation |
| WebSocket Protocol | RFC 6455 compliant real-time communication |
| Flexible File System | Adapt to any storage (LittleFS, SPIFFS, std::fstream) |
| High Performance | Asynchronous, lock-free, predictable memory usage |
| Development Friendly | Mock, develop and test on PC, deploy to embedded |
- π Beauty
- Asio (non-boost) - Async I/O operations
- >=C++11 - The core library is kept compatible with C++11 for maximum portability
Get up and running in under 30 seconds:
# Clone and build
mkdir build && cd build
cmake --build . ..
# Launch with built-in demos
./build/examples/beauty_example 127.0.0.1 8080 www
# Open http://127.0.0.1:8080 and explore:
# β’ Static file serving
# β’ Low-level Web API
# β’ Full JSON REST API
# β’ Multi-part file uploads
# β’ WebSocket chat & data streaming
# β’ Flow control demonstrationsπ‘ Tip: The test_scripts folder can be used to explore more advanced features like 100-continue.
Beauty seamlessly integrates with both PlatformIO/Arduino and ESP-IDF frameworks. Since Asio's io_context::run() is blocking, we recommend using an RTOS thread for most applications:
π§ Framework Support: Add Beauty as an external library following your framework's documentation
// included for this example
#include <functional>
#include <chrono>
#include <Arduino.h>
#include <WiFi.h>
#include <asio.hpp>
#include <beauty/server.hpp>
#include <beauty/reply.hpp>
// these includes needs implementation, see examples
#include "my_file_io.hpp"
#include "my_file_api.hpp"
// somewhere in main..
void httpServerThread(void*) {
// Wifi needs to be setup in the same thread
WiFi.onEvent(WiFiEvent);
WiFi.begin();
MyFileIO fio; // see examples folder for LittleFs
// configurable keep-alive support
beauty::Settings settings(std::chrono::seconds(5), 1000, 20);
// the asio::context
asio::io_context ioc;
// must use an alternative constructor compared to PC example
beauty::Server server(ioc, 80, settings, 1024);
server.setFileIO(&fio);
// middlewares (just one in this example), see examples folder
MyFileApi fileApiHandler;
// add middlewares to server in invokation order
using namespace std::placeholders;
server.addRequestHandler(std::bind(&MyFileApi::handleRequest, &fileApiHandler, _1, _2));
// uncomment to print debug message from server
// server.setDebugMsgHandler([](const std::string& msg) { Serial.println(msg.c_str()); });
// starts the asio::io_context and hence the server, this is blocking and
// the reason we're running in a thread.
ioc.run();
Serial.println("Unexpected io_context termination");
}
// somewhere in setup() ..
xTaskCreate(httpServerThread, // Function that should be called
"beauty", // Name of the task (for debugging)
8192, // Stack size (bytes)
NULL, // Parameter to pass
1, // Task priority
NULL // Task handle
Beauty follows a middleware-first architecture inspired by Express.js:
HTTP Request β Middleware Stack β File Handler β HTTP Response
(in order added) (fallback)
Request Flow:
- π Middleware Stack: Executed in the order you add them
- π File Handler: Automatic fallback for static files (if configured)
- β 404/501: Proper error responses if no handler matches
π‘ Tip: Put your most frequently accessed routes first in the middleware stack for better performance!
Beauty's Server class runs on top of Asio's io_context and provides platform-optimized constructors:
| Platform | Constructor | Use Case |
|---|---|---|
| π§ ESP32 | Server(io_context, port, settings, maxContentSize) |
Embedded systems |
| π» PC/Server | Server(io_context, address, port, settings, maxContentSize) |
Development & production |
| Parameter | Description | π‘ Tips |
|---|---|---|
ioContext |
Asio's event loop engine | Shared across all async operations |
address |
Network interface (PC only) | Use "0.0.0.0" for all interfaces |
port |
TCP port to bind | Set to 0 for OS-assigned port |
settings |
HTTP persistence & timeouts | Configure for your use case |
maxContentSize |
Buffer size per connection | Min 1024 bytes, scale with needs |
π Performance Tip: Each connection allocates 2 buffers (read/write) of
maxContentSize. Plan your memory accordingly!
| Method | Purpose | π‘ When to Use |
|---|---|---|
addRequestHandler(callback) |
Add middleware/API handlers | REST APIs, custom routing logic |
setFileIO(IFileIO*) |
Configure file system adapter | Static files, uploads, embedded storage |
setExpect100ContinueHandler(callback) |
Handle large upload validation | Auth checks and file size limitations before accepting big files |
setWsEndpoints(vector<shared_ptr<WsEndpoint>>) |
Register WebSocket endpoints | Real-time communication |
setDebugMsgHandler(callback) |
Custom debug message handler | Development debugging, production logging |
π Reference: Find callback definitions in
src/beauty/beauty_common.hpp
Beauty's Settings class gives you fine-grained control over resource usage and connection behavior - perfect for constrained environments:
beauty::Settings settings(
std::chrono::seconds(30), // keepAliveTimeout
100, // keepAliveMax
50 // connectionLimit
);| Setting | Purpose | π― Optimization |
|---|---|---|
keepAliveTimeout_ |
HTTP Keep-Alive timeout | 0s = HTTP/1.0 mode, >0s = HTTP/1.1 |
keepAliveMax_ |
Max requests per connection | Balance connection reuse vs memory |
connectionLimit_ |
Max concurrent connections | Critical for embedded: prevents OOM |
wsReceiveTimeout_ |
WebSocket message timeout | Detect dead clients |
wsPingInterval_ |
WebSocket ping frequency | Keep connections alive through NAT |
wsPongTimeout_ |
WebSocket pong response timeout | Clean up unresponsive clients |
πΎ Memory Management:
connectionLimit_is your best friend on ESP32 - set it based on available RAM!
β‘ Performance: Keep-Alive reduces connection overhead but accumulates memory usage per connection.
Middleware in Beauty is beautifully simple - just implement the handlerCallback function:
void handleRequest(const beauty::Request& req, beauty::Reply& rep) {
// Your magic happens here! β¨
if (req.requestPath_ == "/api/hello") {
rep.send(beauty::Reply::ok, "application/json", R"({"message": "Hello World!"})");
}
}π― Design Philosophy: One function, total control. Check out our examples for inspiration!
The Request object is your read-only window into what the client sent. It's fully parsed and ready to use:
| Property | Example | Purpose |
|---|---|---|
method_ |
"GET", "POST" |
HTTP method |
uri_ |
"/api/users?page=1" |
Full URI with query string |
requestPath_ |
"/api/users" |
Clean path without query |
body_ |
std::vector<char> |
Request body data |
headers_ |
std::vector<Header> |
All HTTP headers |
keepAlive_ |
true/false |
Connection persistence |
// Parse query parameters: /api/users?page=1&limit=10
auto page = req.getQueryParam("page"); // page.exists_, page.value_
auto limit = req.getQueryParam("limit");
// Parse form data: Content-Type: application/x-www-form-urlencoded
auto username = req.getFormParam("username");
// Path matching: for simple routing
if (req.startsWith("/api/")) {
// Handle API requests
}π Immutable by Design: Request objects are read-only to prevent accidental modifications
The Reply object is your canvas for crafting responses. Modify it to send exactly what your clients need:
| Method | Use Case | Example |
|---|---|---|
send(status) |
Status-only responses | rep.send(beauty::Reply::not_found) |
send(status, contentType) |
With content_ buffer |
JSON, HTML, custom data |
sendPtr(status, contentType, data, size) |
Direct memory pointer | Pre-loaded data and buffers <= maxContentSize |
sendBig(status, contentType, totalSize, callback) |
Reply with large data | Any content > maxContentSize |
sendStreaming(status, contentType, callback) |
Unlimited streaming | Server-sent events, real-time data |
Use sendBig() for data larger than your configured maxContentSize as it allows Beauty to send the reply back in chunks that fit within maxContentSize. Note that the total data size must be known upfront for proper Content-Length header.
// Send large data without loading it all into memory
rep.sendBig(Reply::ok, "application/json", totalDataSize,
[&dataSource](const std::string &id, char* buf, size_t maxSize) -> int {
if (maxSize == 0) {
// Beauty is signaling end of transfer, clean up resources
return 0;
}
// Return number of bytes written to buf, 0 or negative for end of data
// Use id to track read position for each connection
return dataSource->getNextChunk(id, buf, maxSize);
});π‘ Tip:
- Perfect for large sensor data, database result streaming, large file generation, or API pagination
- The callback runs on each chunk, so keep it fast and efficient
Use sendStreaming() for unlimited streaming where the total size is unknown. Uses HTTP chunked transfer encoding, allowing infinite streams like server-sent events or real-time data feeds.
// Stream unlimited data (server-sent events, real-time feeds)
rep.sendStreaming(Reply::ok, "text/event-stream",
[&eventSource](const std::string &id, char* buf, size_t maxSize) -> int {
// Return number of bytes written to buf, 0 or negative to end stream
// Stream can run indefinitely until client disconnects or you return 0
return eventSource->getNextEvent(id, buf, maxSize);
});π‘ Tip:
- Perfect for server-sent events, real-time dashboards, varying buffers, or infinite data feeds
- No Content-Length header sent - client receives data as it's generated
- Return 0 from callback to close the stream gracefully
// Custom headers (takes full control)
rep.addHeader("Content-Type", "application/octet-stream");
rep.addHeader("Content-Disposition", "attachment; filename=" + filename);
// Note: When using addHeader(), you control ALL headers except Content-Length and Connection
// File handling magic
rep.filePath_ = "/custom/path.html"; // Redirect file requests
rep.fileExtension_ = ".json"; // Override detected extension// JSON API Response
rep.content_ = R"({"users": [], "total": 0})";
rep.send(beauty::Reply::ok, "application/json");
// Custom Error with CORS
rep.addHeader("Access-Control-Allow-Origin", "*");
rep.addHeader("Content-Type", "text/plain");
rep.content_ = "API key required";
rep.send(beauty::Reply::unauthorized);
// File Download
rep.addHeader("Content-Disposition", "attachment; filename=data.csv");
rep.send(beauty::Reply::ok, "text/csv", csvData.data(), csvData.size());π‘ Tip: Use
content_for dynamic data, direct pointers for static/large files
The 100-continue mechanism allows HTTP clients to send request headers first, wait for server approval (100 Continue response), and only then send the potentially large request body. This is particularly useful for:
- Authentication/authorization validation before accepting large uploads
- Content-Type validation
- Content-Length limits checking
- Bandwidth optimization by avoiding unnecessary data transfer
Beauty supports 100-continue as described in RFC 7231, section 5.1.1. By
default, Beauty will approve all requests with the Expect: 100-continue header.
However, a custom handler can be set to implement application-specific logic to
approve or reject such requests. A handler can e.g. check for valid
authentication tokens, validate content types, or enforce content length
limits.
A custom 100-continue handler can be set using the setExpectContinueHandler
method of the Server class. For simplicity, the handler have the same signature as
a request handler. I.e. the handler should send the 200 OK status code in the
Reply object if the request is approved, or an appropriate error status code.
The handler may be registered as follows:
server.setExpectContinueHandler([](const beauty::Request& req, beauty::Reply& rep) -> void {
// Validate headers before accepting body
std::string auth = req.getHeaderValue("Authorization");
if (auth.empty()) {
rep.send(beauty::Reply::status_type::bad_request);
}
// Check content length
if (req.getContentLength() > MAX_ALLOWED_FILE_SIZE) {
rep.send(beauty::Reply::status_type::payload_too_large);
}
// Approve the request
rep.send(beauty::Reply::status_type::ok); // Actually sends 100 Continue
});As the request and reply classes store data in std::vector<char> it becomes
a bit hard to manipulate their data as e.g. JSON documents. Therefore, the
HttpResult class provides a convenient way to do so.
For high portability and low footprint, HttpResult uses cJSON, so cJSON needs to be imported to your project. If you have a different preferred JSON library, you may use HttpResult as inspiration.
HttpResult takes a reference to the Reply::content_ buffer:
HttpResult res(rep.content_);// Parse JSON from request body
if (res.parseJsonRequest(req.body_)) {
// Access parsed values with type safety
std::string name = res.getString("name", "default");
int age = res.getInt("age", 0);
bool active = res.getBool("active", false);
double score = res.getDouble("score", 0.0);
// Check if key exists
if (res.containsKey("email")) {
std::string email = res.getString("email");
}
}For complex nested JSON structures, arrays, or when you need to use advanced cJSON functions directly, use the getRoot() method:
// Parse complex nested JSON
if (res.parseJsonRequest(req.body_)) {
// Get direct access to cJSON root for advanced operations
cJSON* root = res.requestBody_.getRoot();
// Navigate nested objects
cJSON* user = cJSON_GetObjectItem(root, "user");
if (user && cJSON_IsObject(user)) {
cJSON* address = cJSON_GetObjectItem(user, "address");
if (address && cJSON_IsObject(address)) {
cJSON* street = cJSON_GetObjectItem(address, "street");
if (street && cJSON_IsString(street)) {
std::string streetName = street->valuestring;
}
}
}
// Process arrays
cJSON* items = cJSON_GetObjectItem(root, "items");
if (items && cJSON_IsArray(items)) {
int arraySize = cJSON_GetArraySize(items);
for (int i = 0; i < arraySize; i++) {
cJSON* item = cJSON_GetArrayItem(items, i);
if (item && cJSON_IsObject(item)) {
cJSON* id = cJSON_GetObjectItem(item, "id");
if (id && cJSON_IsNumber(id)) {
int itemId = id->valueint;
// Process item...
}
}
}
}
}Simple key-value responses:
// Single string value
res.singleJsonKeyValue("message", "Hello World");
// Single numeric value
res.singleJsonKeyValue("count", 42);
// Single boolean value
res.singleJsonKeyValue("success", true);Complex JSON structures:
// Build custom JSON response
res.buildJsonResponse([&]() -> cJSON* {
cJSON* root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "status", "ok");
cJSON_AddNumberToObject(root, "timestamp", time(nullptr));
cJSON* users = cJSON_CreateArray();
for (const auto& user : userList) {
cJSON* userObj = cJSON_CreateObject();
cJSON_AddStringToObject(userObj, "name", user.name.c_str());
cJSON_AddNumberToObject(userObj, "id", user.id);
cJSON_AddItemToArray(users, userObj);
}
cJSON_AddItemToObject(root, "users", users);
return root;
});Direct array responses:
// Return JSON array as root element
res.buildJsonResponse([&]() -> cJSON* {
cJSON* filesArray = cJSON_CreateArray();
for (const auto& file : files) {
cJSON* fileObj = cJSON_CreateObject();
cJSON_AddStringToObject(fileObj, "name", file.name.c_str());
cJSON_AddNumberToObject(fileObj, "size", file.size);
cJSON_AddItemToArray(filesArray, fileObj);
}
return filesArray;
});Error responses:
// Generate error response with status code
res.jsonError(Reply::status_type::bad_request, "Invalid input data");After building the JSON response, send it:
rep.send(res.statusCode_, "application/json");For HTML, plain text, or other non-JSON responses, use the streaming operator:
// HTML response
res << "<!DOCTYPE html>"
<< "<html><head><title>My Page</title></head>"
<< "<body>"
<< "<h1>Welcome to Beauty Server</h1>"
<< "<p>Current time: " << getCurrentTime() << "</p>"
<< "</body></html>";
rep.send(res.statusCode_, "text/html");// Plain text response
res << "Server status: OK\n"
<< "Uptime: " << getUptime() << " seconds\n"
<< "Active connections: " << getConnectionCount();
rep.send(res.statusCode_, "text/plain");// CSV response
res << "Name,Age,City\n"
<< "John,25,New York\n"
<< "Jane,30,London\n";
rep.send(res.statusCode_, "text/csv");Beauty provides a lightweight, router implementation. It avoids heavy dependencies like std::regex and uses string operations for path matching. The router provides some key features listed below but is not required when implementing a Web API request handler.
- Lightweight: No regex dependencies, uses simple string operations
- Embedded-friendly: Minimal memory footprint and predictable performance
- Path Parameters: Supports parameterized paths like
/users/{userId} - Multiple HTTP Methods: Support for GET, POST, PUT, DELETE, etc.
- HTTP/1.1 Compliant: Full compliance with HTTP/1.1 specification
- CORS Support: Complete Cross-Origin Resource Sharing implementation
- Automatic HEAD Support: HEAD requests automatically supported for all GET routes
- OPTIONS Method: Automatic method discovery and CORS preflight handling
- Proper Error Responses: 405 Method Not Allowed with Allow header
- Easy Integration: Designed to work seamlessly with Beauty's request handling
The router provides comprehensive HTTP/1.1 compliance and CORS support:
- HEAD Method: Automatically supported for all GET routes
- OPTIONS Method: Returns allowed methods for any resource
- 405 Responses: Method Not Allowed responses include proper Allow header
- Proper Status Codes: Compliant with HTTP/1.1 specification
// Configure CORS for cross-origin requests
beauty::CorsConfig corsConfig;
corsConfig.allowedOrigins.insert("https://myapp.com");
corsConfig.allowedOrigins.insert("http://localhost:3000");
corsConfig.allowedHeaders.insert("Authorization");
corsConfig.allowCredentials = true;
corsConfig.maxAge = 3600; // 1 hour preflight cache
router.configureCors(corsConfig);CORS Features:
- Preflight Handling: Automatic CORS preflight request processing
- Origin Validation: Configurable allowed origins (including wildcards)
- Header Control: Specify allowed and exposed headers
- Credentials Support: Configurable cookie/auth header support
- Cache Control: Configurable preflight response caching
What CORS Enables:
- Modern web applications with separate frontend/backend
- Cross-domain API access from browsers
- Integration with JavaScript frameworks (React, Vue, Angular)
- Web based mobile apps (e.g. PWA)
#include "beauty/router.hpp"beauty::Router router;
// Add routes
router.addRoute("GET", "/users",
[](const beauty::Request& req, beauty::Reply& rep, const std::unordered_map<std::string, std::string>& params) {
// Handle GET /users
});
router.addRoute("GET", "/users/{userId}",
[](const beauty::Request& req, beauty::Reply& rep, const std::unordered_map<std::string, std::string>& params) {
std::string userId = params.at("userId");
// Handle GET /users/{userId}
});// Using the Router, the request handler is implemented as:
void handleRequest(const beauty::Request& req, beauty::Reply& rep) {
if (router.handle(req, rep) == HandlerResult::Matched) {
return; // Request was handled by router
}
}The router supports simple path patterns with parameters:
/users- Exact match/users/{userId}- Match with parameter/users/{userId}/posts/{postId}- Multiple parameters/api/v1/users/{userId}- Mixed literal and parameter segments
Parameters are automatically extracted and passed to handlers:
router.addRoute("GET", "/users/{userId}/posts/{postId}",
[](const beauty::Request& req, beauty::Reply& rep, const std::map<std::string, std::string>& params) {
std::string userId = params.at("userId");
std::string postId = params.at("postId");
// Use the parameters...
});See my_router_api.cpp for a complete working example that demonstrates:
- Setting up multiple routes with different HTTP methods
- Handling path parameters
- Returning JSON responses
- CORS configuration for cross-origin requests
- HTTP/1.1 compliance features (HEAD, OPTIONS, 405 responses)
- Cross-origin testing examples with curl commands
- Integration with Beauty's HttpResult
- Memory: Minimal overhead, stores only parsed route segments and CORS config
- CPU: Simple string comparisons, no regex compilation or matching
- Predictable: Linear time complexity O(n) where n is the number of route segments
- Embedded-friendly: No dynamic regex compilation, fixed memory usage per route
- CORS Efficient: CORS headers only added when needed (cross-origin requests)
- HTTP/1.1 Compliance: HEAD and OPTIONS responses generated without handler execution
The router is included in the Beauty examples CMake configuration. To build:
mkdir build && cd build
cmake -DBUILD_EXAMPLES=On ..
makeStart the example server and test the '/api/users' paths:
./build/examples/beauty_example 127.0.0.1 8080 www/Beauty provides WebSocket support in compliance with RFC 6455. The API provides the primitives to build custom queue and application-controlled back-pressure handling. The WebSocket connections reuse the existing HTTP connection buffers, making the memory overhead minimal:
- RFC 6455 Compliant: Full WebSocket protocol implementation
- Path-based Endpoints: Support for multiple WebSocket endpoints on different paths
- Customizable Flow Control: Allows for advanced back-pressure handling
- Thread-safe: Built on Asio's single-threaded event loop model
- Connection Management: Automatic connection lifecycle management
- Ping/Pong Handling: Built-in connection health monitoring
WebSocket functionality is implemented through endpoint classes that inherit from WsEndpoint:
#include "beauty/ws_endpoint.hpp"
class MyChatEndpoint : public beauty::WsEndpoint {
public:
MyChatEndpoint() : WsEndpoint("/ws/chat") {}
void onWsOpen(const std::string& connectionId) override {
sendText(connectionId, "Welcome to the chat!");
}
void onWsMessage(const std::string& connectionId, const beauty::WsMessage& message) override {
// Echo message to all connected clients
std::string msg(message.content_.begin(), message.content_.end());
for (const auto& connId : getActiveConnections()) {
sendText(connId, "User " + connectionId + ": " + msg);
}
}
void onWsClose(const std::string& connectionId) override {
for (const auto& connId : getActiveConnections()) {
sendText(connId, "User " + connectionId + " has left the chat.");
}
}
void onWsError(const std::string& connectionId, const std::string& error) override {
// Handle connection errors
}
};// Create endpoints
auto chatEndpoint = std::make_shared<MyChatEndpoint>();
auto dataEndpoint = std::make_shared<MyDataEndpoint>();
// Register with server
server.setWsEndpoints({chatEndpoint, dataEndpoint});Clients connect to specific WebSocket endpoints using their configured paths:
// Connect to chat endpoint
const chatSocket = new WebSocket('ws://localhost:8080/ws/chat');
// Connect to data endpoint
const dataSocket = new WebSocket('ws://localhost:8080/ws/data');class MyEndpoint : public beauty::WsEndpoint {
public:
// Called when a client connects
void onWsOpen(const std::string& connectionId) override;
// Called when a message is received
void onWsMessage(const std::string& connectionId, const beauty::WsMessage& message) override;
// Called when a client disconnects
void onWsClose(const std::string& connectionId) override;
// Called on connection errors
void onWsError(const std::string& connectionId, const std::string& error) override;
};// Send to specific connection
bool sendText(const std::string& connectionId, const std::string& message);
bool sendBinary(const std::string& connectionId, const std::vector<char>& data);
bool sendClose(const std::string& connectionId, uint16_t statusCode = 1000, const std::string& reason = "");
// Get connection information
std::vector<std::string> getActiveConnections() const;For production applications that need to handle varying client performance or bursty data producers, Beauty allows writing advanced flow control managment to prevent memory buildup and maintain system responsiveness.
// Check if connection can send (not in middle of write operation)
bool canSendTo(const std::string& connectionId) const;
// Send with optional callbacks for flow control or monitoring
WriteResult sendText(const std::string& connectionId,
const std::string& message,
WriteCompleteCallback callback);
WriteResult sendBinary(const std::string& connectionId,
const std::vector<char>& data,
WriteCompleteCallback callback);Beauty provides building blocks for flow control rather than making policy decisions. Here are common strategies: See examples/pc/my_data_streaming_endpoint.cpp for a complete working example.
void broadcastData(const std::string& data) {
for (const auto& connId : getActiveConnections()) {
if (canSendTo(connId)) {
sendText(connId, data);
} else {
// Drop message for slow clients
statisticsDropped_[connId]++;
}
}
}void sendImportantMessage(const std::string& connId, const std::string& msg) {
if (canSendTo(connId)) {
sendText(connId, msg);
} else if (messageQueue_[connId].size() < MAX_QUEUE_SIZE) {
messageQueue_[connId].push(msg);
} else {
// Queue full, handle accordingly
handleQueueFull(connId);
}
}void adaptiveSend(const std::string& connId, const std::string& data) {
auto& stats = connectionStats_[connId];
if (canSendTo(connId) || stats.getDropRate() < 0.1) { // Less than 10% drop rate
auto result = sendText(connId, data,
[&stats](const std::error_code& ec, std::size_t bytes) {
if (!ec) stats.messagesSent++;
});
if (result == WriteResult::WRITE_IN_PROGRESS) {
stats.messagesDropped++;
}
} else {
stats.messagesDropped++;
}
}enum class WriteResult {
SUCCESS, // Message queued for sending
WRITE_IN_PROGRESS, // Connection is busy with another write
CONNECTION_CLOSED // Connection is closed or invalid
};Beauty automatically handles the WebSocket handshake process:
- Client sends HTTP Upgrade request with WebSocket headers
- Beauty validates the request and generates proper Sec-WebSocket-Accept header
- HTTP 101 Switching Protocols response is sent
- Connection is upgraded to WebSocket protocol
- Text frames: UTF-8 encoded text messages
- Binary frames: Raw binary data
- Close frames: Connection termination with status codes
- Ping/Pong frames: Automatic connection health monitoring
- Automatic ping/pong handling for connection health
- Configurable timeouts and limits
- Graceful connection shutdown
- Error handling and recovery
WebSocket connections reuse the existing HTTP connection buffers, making the memory overhead minimal:
- Each WebSocket connection uses the same
maxContentSizebuffer as HTTP - No additional per-connection memory allocation for WebSocket protocol
- Efficient buffer reuse for frame parsing and encoding
Beauty includes a comprehensive WebSocket test interface at /ws/chat and /ws/data that allows:
- Testing multiple endpoint connections simultaneously
- Flow control simulation with bursty data
- Real-time statistics monitoring
- Interactive message sending and broadcasting