C++ library for easy networking
kurlyk is yet another library implementing HTTP and WebSocket clients for C++. Built as a wrapper around curl and Simple-WebSocket-Server, it provides a simplified interface for working with HTTP and WebSocket in C++ applications. It supports asynchronous HTTP requests with rate limiting and retry attempts, as well as WebSocket connections.
If, for some reason, other libraries such as easyhttp-cpp, curl_request, curlpp-async, curlwrapper, curl-Easy-cpp, curlpp11, easycurl, curl-cpp-wrapper… did not work for you, kurlyk may be worth trying.
- Asynchronous HTTP and WebSocket requests.
- Background worker or synchronous processing via
kurlyk::process(). - HTTP callback API and
std::futureAPI. - Rate limits (partitioned by key), retry, proxy, custom headers, cookies, and timeouts.
- Streaming HTTP responses with a callback for each chunk.
- WebSocket events, message sending, and automatic reconnection.
- Bounded admission/backpressure for the HTTP pending queue and WebSocket send queue.
- Bearer token and API-key auth providers (
BearerTokenAuthProvider,ApiKeyAuthProvider). - OAuth2 Authorization Code + PKCE client (
OAuthPkceClient) using standalone HTTP helpers. - Support for C++11 and newer toolchains.
#include <kurlyk.hpp>
#include <iostream>
int main() {
kurlyk::HttpClient client("https://httpbin.org");
auto response = client.get("/ip", kurlyk::QueryParams(), kurlyk::Headers()).get();
if (response && response->ready) {
std::cout << response->content << std::endl;
}
return 0;
}For C++11/14 or manual lifecycle management, use kurlyk::init() / kurlyk::deinit().
#include <kurlyk.hpp>
#include <iostream>
#include <thread>
#include <chrono>
int main() {
kurlyk::WebSocketClient client("wss://echo-websocket.fly.dev/");
client.on_event([](std::unique_ptr<kurlyk::WebSocketEventData> event) {
if (event->event_type == kurlyk::WebSocketEventType::WS_OPEN) {
event->sender->send_message("Hello");
}
if (event->event_type == kurlyk::WebSocketEventType::WS_MESSAGE) {
std::cout << event->message << std::endl;
}
});
client.connect();
std::this_thread::sleep_for(std::chrono::seconds(5));
client.disconnect_and_wait();
return 0;
}Examples are located in the examples folder. Build all repository targets with CMake:
cmake -S . -B build-examples -DKURLYK_BUILD_EXAMPLES=ON
cmake --build build-examples --config ReleaseFor MinGW, you can select the generator and compilers explicitly:
cmake -S . -B build-examples-mingw -G "MinGW Makefiles" `
-DCMAKE_C_COMPILER=gcc `
-DCMAKE_CXX_COMPILER=g++ `
-DKURLYK_BUILD_EXAMPLES=ON
cmake --build build-examples-mingw --config ReleaseFor MinGW, dependencies are already available in the repository as git submodules in the libs folder, and the fallback CMake options below can build missing dependencies automatically.
The HTTP client can be used through callbacks, futures, or low-level helpers. With auto-init enabled by default, you can create HttpClient without manual kurlyk::init() / kurlyk::deinit() calls; manual lifecycle management is needed for C++11/14, synchronous mode, or explicit worker control.
#include <kurlyk.hpp>
#include <iostream>
void print_response(const kurlyk::HttpResponsePtr& response) {
if (!response) {
KURLYK_PRINT << "response is null" << std::endl;
return;
}
KURLYK_PRINT
<< "ready: " << std::boolalpha << response->ready << std::endl
<< "response: " << response->content << std::endl
<< "error_code: " << response->error_code.message() << std::endl
<< "status_code: " << response->status_code << std::endl
<< "----------------------------------------" << std::endl;
}Callback overloads of get(...), post(...), and request(...) return bool: true when the request is accepted into the queue and false when it is rejected during admission. The callback itself receives kurlyk::HttpResponsePtr and is not limited to the final response: in streaming mode it is called for each chunk with stream_chunk == true, and during retry it may receive an intermediate non-ready response for a failed attempt. The final result is identified by response && response->ready.
int main() {
kurlyk::HttpClient client("https://httpbin.org");
client.get("/ip", kurlyk::QueryParams(), kurlyk::Headers(),
[](const kurlyk::HttpResponsePtr response) {
print_response(response);
});
client.post("/post", kurlyk::QueryParams(), {{"Content-Type", "application/json"}},
"{\"text\":\"Sample POST Content\"}",
[](const kurlyk::HttpResponsePtr response) {
print_response(response);
});
KURLYK_PRINT << "Press Enter to exit..." << std::endl;
std::cin.get();
return 0;
}If a request is rejected before it enters the pending queue, the future becomes ready immediately and returns an HttpResponse with error_code = QueueLimitExceeded or ShuttingDown.
int main() {
kurlyk::HttpClient client("https://httpbin.org");
auto future_response = client.get("/get", kurlyk::QueryParams{{"param", "value"}}, kurlyk::Headers());
print_response(future_response.get());
auto future_post = client.post("/post", kurlyk::QueryParams(),
kurlyk::Headers{{"Header", "Value"}}, "Async POST Content");
print_response(future_post.get());
return 0;
}int main() {
kurlyk::HttpClient client("https://httpbin.org");
client.set_proxy("127.0.0.1", 8080, "username", "password", kurlyk::ProxyType::HTTP);
client.get("/ip", kurlyk::QueryParams(), kurlyk::Headers(),
[](const kurlyk::HttpResponsePtr response) {
print_response(response);
});
KURLYK_PRINT << "Press Enter to exit..." << std::endl;
std::cin.get();
return 0;
}Low-level helpers are useful when you need the ID of a concrete request or direct access to the standalone HTTP API.
int main() {
const uint64_t request_id = kurlyk::http_get(
"https://httpbin.org/ip",
kurlyk::QueryParams(),
kurlyk::Headers(),
[](const kurlyk::HttpResponsePtr response) {
print_response(response);
});
KURLYK_PRINT << "Request id: " << request_id << std::endl;
KURLYK_PRINT << "Press Enter to exit..." << std::endl;
std::cin.get();
kurlyk::cancel_request_by_id(request_id).wait();
return 0;
}int main() {
auto result = kurlyk::http_get(
"https://httpbin.org/ip",
kurlyk::QueryParams(),
kurlyk::Headers());
KURLYK_PRINT << "Request id: " << result.first << std::endl;
print_response(result.second.get());
return 0;
}WebSocketClient connects to a server, reports events through on_event(...), and lets you send messages through the sender from an event or through the client itself. For a simple scenario, handle WS_OPEN, WS_MESSAGE, WS_CLOSE, and WS_ERROR.
#include <kurlyk.hpp>
#include <thread>
#include <chrono>
int main() {
kurlyk::WebSocketClient client("wss://echo-websocket.fly.dev/");
client.on_event([](std::unique_ptr<kurlyk::WebSocketEventData> event) {
switch (event->event_type) {
case kurlyk::WebSocketEventType::WS_OPEN:
KURLYK_PRINT << "Connection established" << std::endl;
event->sender->send_message("Hello, WebSocket!");
break;
case kurlyk::WebSocketEventType::WS_MESSAGE:
KURLYK_PRINT << "Message received: " << event->message << std::endl;
break;
case kurlyk::WebSocketEventType::WS_CLOSE:
KURLYK_PRINT << "Connection closed: " << event->message
<< "; Status code: " << event->status_code << std::endl;
break;
case kurlyk::WebSocketEventType::WS_ERROR:
KURLYK_PRINT << "Error: " << event->error_code.message() << std::endl;
break;
}
});
client.connect();
std::this_thread::sleep_for(std::chrono::seconds(10));
client.disconnect_and_wait();
return 0;
}send_message(...) returns bool and fits simple code. submit_message(...) returns SubmitResult and lets you distinguish successful queue admission from an admission reject.
client.send_message("Hello");
kurlyk::SubmitResult submit = client.submit_message(
"Hello with admission check",
0,
[](const std::error_code& ec) {
if (ec) {
std::cout << "Send failed: " << ec.message() << std::endl;
}
});
if (!submit) {
std::cout << "WebSocket submit rejected: " << submit.error_code.message() << std::endl;
}To protect the producer, you can limit the size of the outbound WebSocket send/close queue. A value of 0 means the queue is unbounded.
kurlyk::WebSocketClient client("wss://echo-websocket.fly.dev/");
client.set_max_send_queue_size(32);In C++17+, auto-initialization is available by default, so simple examples can create HttpClient or WebSocketClient immediately. For C++11/14 or manual mode, disable auto init and call init() / deinit() yourself.
#define KURLYK_AUTO_INIT 0
#include <kurlyk.hpp>
int main() {
kurlyk::init(true);
kurlyk::HttpClient client("https://httpbin.org");
kurlyk::deinit();
return 0;
}kurlyk::deinit() is the normal cleanup call for both asynchronous init(true) and synchronous init(false) modes. kurlyk::shutdown() remains available for explicit manager cleanup/reset scenarios, but normal manual lifetime management should use the init() / deinit() pair.
A rate limit controls how quickly HTTP requests are released to the network backend. For a simple per-client speed limit, use set_rate_limit_rps(...) or set_rate_limit_rpm(...).
kurlyk::HttpClient client("https://api.example.com");
client.set_rate_limit_rps(1);To share one limit between multiple clients, use HttpRateLimitHandlePtr:
auto limit = kurlyk::create_rate_limit_rps(5);
kurlyk::HttpClient client_a("https://api.example.com");
kurlyk::HttpClient client_b("https://api.example.com");
client_a.set_rate_limit_handle(limit);
client_b.set_rate_limit_handle(limit);HttpRateLimitHandlePtr keeps the physical limit data alive while at least one handle copy exists. Pending, active, and retrying requests copy their assigned handles, so remove_limit(id) or remove_limit(handle) only releases the manager-owned reference and does not invalidate already queued requests.
Prefer handle-based APIs for new code:
HttpClient::set_rate_limit_handle(...)HttpClient::assign_rate_limit_handle(...)- per-request overloads that accept
HttpRateLimitHandlePtr
ID-based APIs such as set_rate_limit_id(...) and per-request long specific_rate_limit_id overloads remain legacy lookup helpers. If the manager-owned handle has already been removed, lookup by ID returns an empty handle and the request is submitted without that additional specific limit.
HTTP requests can be cancelled by the ID of one concrete request or by the ID of a related request group. HttpClient assigns one group_id to all requests created by that client, so HttpClient::cancel_requests() cancels the client's request group.
uint64_t request_id = kurlyk::http_get(
"https://httpbin.org/delay/5",
kurlyk::QueryParams(),
kurlyk::Headers(),
[](kurlyk::HttpResponsePtr response) {
print_response(response);
});
kurlyk::cancel_request_by_id(request_id).wait();HTTP requests have two identifiers:
request_idis the ID of one concrete request and is used bycancel_request_by_id(...).group_idis the ID of a related request group and is used bycancel_requests_by_group_id(...).
For manually constructed low-level requests, set group_id explicitly if you want to cancel a group:
const uint64_t group_id = kurlyk::generate_group_id();
std::unique_ptr<kurlyk::HttpRequest> request(new kurlyk::HttpRequest());
request->request_id = kurlyk::generate_request_id();
request->group_id = group_id;
request->method = "GET";
request->set_url("https://httpbin.org", "/delay/5");
kurlyk::submit_http_request(std::move(request), [](kurlyk::HttpResponsePtr response) {
print_response(response);
});
kurlyk::cancel_requests_by_group_id(group_id).wait();A rate limit slows request dispatch. Backpressure limits how many requests or messages are accepted into a queue at all.
kurlyk::set_max_pending_requests(64);
std::unique_ptr<kurlyk::HttpRequest> request(new kurlyk::HttpRequest());
request->request_id = kurlyk::generate_request_id();
request->method = "GET";
request->set_url("https://httpbin.org/get", kurlyk::QueryParams());
kurlyk::SubmitResult submit = kurlyk::submit_http_request(
std::move(request),
[](kurlyk::HttpResponsePtr response) {
if (response && response->error_code) {
std::cout << "HTTP runtime error: " << response->error_code.message() << std::endl;
}
});
if (!submit) {
std::cout << "HTTP submit rejected: " << submit.error_code.message() << std::endl;
}Current bounded queues:
- HTTP uses a global pending-queue limit configured with
kurlyk::set_max_pending_requests(...). - WebSocket uses a per-client outbound send/close queue limit configured with
WebSocketClient::set_max_send_queue_size(...). - A value of
0means an unbounded queue.
Public admission helpers:
kurlyk::SubmitResultkurlyk::submit_http_request(...)IWebSocketSender::submit_message(...)IWebSocketSender::submit_close(...)WebSocketClient::submit_message(...)WebSocketClient::submit_close(...)
Compatibility notes:
- older
boolAPIs remain available as wrappers; - HTTP future overloads become ready immediately on admission reject and return an
HttpResponsewitherror_codeinstead of throwingruntime_error; - this pass does not bound
NetworkWorker, WebSocket FSM queues, or accumulated event queues.
Streaming lets you receive an HTTP body in parts instead of waiting for the whole response to finish. The callback is invoked for each chunk with response->stream_chunk == true and response->ready == false, and the last callback is the normal completed response with ready == true.
int main() {
kurlyk::http_post(
"https://api.example.com/v1/chat/completions",
kurlyk::QueryParams(),
kurlyk::Headers{{"Content-Type", "application/json"}},
R"({"stream":true})",
true,
[](kurlyk::HttpResponsePtr response) {
if (!response) return;
if (response->stream_chunk) {
std::cout << response->content;
return;
}
if (response->error_code) {
std::cerr << response->error_code.message() << std::endl;
}
});
std::cin.get();
return 0;
}For requests created through HttpClient, enable streaming on the client:
kurlyk::HttpClient client("http://httpbin.org");
client.set_streaming(true);Use stream_chunk first to classify body chunk callbacks. A chunk callback is not a success marker; the final ready response remains the authoritative transfer result. If at least one streaming chunk was emitted, kurlyk does not retry that transfer automatically, because the caller may already have forwarded bytes to a downstream client.
Retry repeats a failed HTTP request after a configured delay. It is useful for temporary network errors and unstable upstream APIs.
kurlyk::HttpClient client("https://httpbin.org");
client.set_retry_attempts(3, 1000);The number of performed attempts is available in HttpResponse::retry_attempt. For streaming requests, automatic retry is not performed after at least one body chunk has been emitted.
Proxy settings can be configured on HttpClient, and then they apply to requests created by that client.
kurlyk::HttpClient client("https://httpbin.org");
client.set_proxy("127.0.0.1", 8080, "username", "password", kurlyk::ProxyType::HTTP);- MSVC
- MinGW (GCC)
MSVC builds are currently considered unstable. The confirmed configuration is C++17 with Visual Studio 2022 (generator: Visual Studio 17 2022).
Add the path to the library headers to your project:
kurlyk/include
kurlyk is a header-only library, so including it with #include <kurlyk.hpp> is enough to start using it.
To use kurlyk in a MinGW environment, you need these dependencies:
-
For WebSocket:
- Simple-WebSocket-Server
- Boost.Asio or standalone Asio
- OpenSSL (LTS version Win64 OpenSSL v3.0.15)
-
For HTTP:
All dependencies are also included as submodules in the libs folder.
Add OpenSSL paths to the project, for example for version 3.4.0:
OpenSSL-Win64/include
OpenSSL-Win64/lib/VC/x64/MD
OpenSSL-Win64/bin
Link OpenSSL libraries from lib/VC/x64/MD:
capi.lib
dasync.lib
libcrypto.lib
libssl.lib
openssl.lib
ossltest.lib
padlock.lib
Add the path to asio:
asio/asio/include
For standalone Asio, define ASIO_STANDALONE in project settings or before including kurlyk.hpp:
#define ASIO_STANDALONE
#include <kurlyk.hpp>For Boost.Asio, you do not need to define ASIO_STANDALONE.
Add paths for curl, for example for version 8.11.0:
curl-8.11.0_1-win64-mingw/bin
curl-8.11.0_1-win64-mingw/include
curl-8.11.0_1-win64-mingw/lib
Link the curl libraries from the lib folder:
libcurl.a
libcurl.dll.a
Add the path to the library headers:
Simple-WebSocket-Server
Also add these libraries to the linker:
ws2_32
wsock32
crypt32
The library supports automatically downloading dependencies when they are missing. Fallback availability depends on the compiler and linkage type.
| Dependency | MinGW (Shared) | MinGW (Static) | MSVC (Shared) | MSVC (Static) |
|---|---|---|---|---|
| OpenSSL | yes | yes | yes | yes |
| curl | yes | yes | yes | no |
Asio and Simple-WebSocket-Server are header-only libraries and work for all listed build variants.
| Option | Description |
|---|---|
KURLYK_USE_FALLBACK_OPENSSL |
Enables OpenSSL fallback. |
KURLYK_USE_FALLBACK_CURL |
Enables libcurl fallback. |
KURLYK_USE_FALLBACK_ASIO |
Enables Asio fallback. |
KURLYK_USE_FALLBACK_SIMPLE_WS_SERVER |
Enables Simple-WebSocket-Server fallback. |
KURLYK_OPENSSL_SHARED |
Loads OpenSSL as a shared library when fallback is enabled. |
KURLYK_CURL_SHARED |
Loads libcurl as a shared library when fallback is enabled. |
KURLYK_BUILD_EXAMPLES |
Builds all targets from the examples/ directory. |
Define these macros before including kurlyk.hpp to configure the library:
| Macro | Default | Description |
|---|---|---|
KURLYK_AUTO_INIT |
1 |
Automatically registers managers during static initialization. |
KURLYK_AUTO_INIT_USE_ASYNC |
1 |
Starts the network thread in the background when auto init is enabled. Set to 0 for manual processing. |
KURLYK_HTTP_SUPPORT |
1 |
Enables or disables the HTTP subsystem. |
KURLYK_WEBSOCKET_SUPPORT |
1 |
Enables or disables the WebSocket subsystem. |
KURLYK_AUTH_SUPPORT |
1 |
Enables authentication providers (BearerTokenAuthProvider, ApiKeyAuthProvider). |
KURLYK_OAUTH_SUPPORT |
KURLYK_AUTH_SUPPORT |
Enables the OAuth2 PKCE client (OAuthPkceClient). Requires KURLYK_AUTH_SUPPORT=1. |
KURLYK_JSON_SUPPORT |
0 |
Enables nlohmann::json include and JSON-aware types, plus enum-to-JSON helpers in type_utils.hpp. |
| Path | Purpose |
|---|---|
include/ |
Public header-only library. |
include/kurlyk/core |
Core infrastructure with NetworkWorker and base interfaces. |
include/kurlyk/http |
HTTP client and request management. |
include/kurlyk/websocket |
WebSocket client and connection management. |
include/kurlyk/types |
Shared enums, cookie, proxy config, and helpers. |
include/kurlyk/utils |
Encoding, URL, HTTP, path, and error helpers. |
tests/integration |
Windows dependency and integration build checks. |
tests/odr |
Header-only ODR checks. |
tests/smoke |
Portable header smoke checks. |
examples/ |
Usage examples. |
The repository includes agent configuration and orchestration metadata for AI coding tools (e.g., Claude Code with oh-my-claudecode). The following MCP servers and plugins are recommended for working with the codebase:
| Category | MCP server / plugin | Purpose |
|---|---|---|
| Document lookup | context7 | SDK/API docs resolution before web search |
| Web search | DDG Search (no key) | Primary web search fallback |
| Web search | Tavily | Deep web search and extraction |
| Web content | Fetch | Markdown/JSON/TXT fetch for known URLs |
| Browser automation | Playwright | UI automation and screenshot testing |
| Repo operations | GitHub | Issues, PRs, and repository file operations |
| Code intelligence | Codebase Memory | Graph-based code discovery and call chains |
| Large output | Context-Mode | Batch execution, indexing, and analysis |
| Structural code | AST grep (OMC plugin) | Structural search and replace |
| Runtime | Python REPL (OMC plugin) | In-session script execution |
| Diagnostics | LSP | Symbols, definitions, and diagnostics |
For the full tool priority chain and fallback rules, see .claude/rules/tool-priority.md.
Run the Windows integration suite:
powershell -ExecutionPolicy Bypass -File tests/integration/run_integration_tests.ps1Run the ODR suite:
powershell -ExecutionPolicy Bypass -File tests/odr/run_odr_tests.ps1Run the auth unit tests:
cmake -S tests/auth -B build-auth-tests -G "MinGW Makefiles" -DKURLYK_BUILD_EXAMPLES=OFF
-DKURLYK_USE_FALLBACK_OPENSSL=ON -DKURLYK_USE_FALLBACK_CURL=ON
-DKURLYK_USE_FALLBACK_ASIO=ON -DKURLYK_USE_FALLBACK_SIMPLE_WS_SERVER=ON
cmake --build build-auth-tests --config Release
ctest --test-dir build-auth-testsBuild the portable smoke test manually:
c++ tests/smoke/header_smoke.cpp -Iinclude -std=c++11 -o header_smoke
./header_smoke
c++ tests/smoke/header_smoke.cpp -Iinclude -std=c++17 -o header_smoke
./header_smokekurlyk provides lightweight, header-only authentication helpers on top of the existing HTTP layer.
BearerTokenAuthProvider— injectsAuthorization: Bearer <token>.ApiKeyAuthProvider— injects an API key as a custom header or as a query parameter (with automatic percent-encoding).OAuthPkceClient— builds authorization URLs and exchanges authorization codes for tokens using PKCE (RFC 7636). It uses the standalonekurlyk::http_posthelper, so it does not require anHttpClientinstance.
See the full guides for details:
| Platform | Coverage |
|---|---|
| Windows | MinGW and MSVC integration builds with fallback dependencies, HTTP backpressure regression, and local WebSocket integration coverage. |
| Windows extras | ODR checks for singleton and auto-initialization headers. |
| Linux | C++11/C++17 header smoke test with HTTP/WebSocket disabled. |
| macOS | C++11/C++17 header smoke test with HTTP/WebSocket disabled. |
Generate Doxygen documentation:
doxygen DoxyfilePublished documentation: https://newyaroslav.github.io/kurlyk/.
This library is distributed under the MIT license. See LICENSE for details.
If you have questions or issues while using the library, consult the documentation or ask a question in GitHub Issues.
In short, kurlyk!

