diff --git a/CMakeLists.txt b/CMakeLists.txt
index f0fefb7..659a029 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -60,6 +60,19 @@ endif()
# ggml as subdirectory, inherits GGML_CUDA, GGML_METAL, etc. from cmake flags
add_subdirectory(ggml)
+# cpp-httplib (HTTP server library, no SSL, behind reverse proxy in prod).
+# Used by tts-server.
+add_subdirectory(vendor/cpp-httplib)
+
+# yyjson (MIT, fast JSON parser/writer). Used by tts-server.
+add_library(yyjson STATIC vendor/yyjson/yyjson.c)
+target_include_directories(yyjson PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/vendor/yyjson)
+if(MSVC)
+ target_compile_options(yyjson PRIVATE /W0)
+else()
+ target_compile_options(yyjson PRIVATE -w)
+endif()
+
# Shared compile options and ggml linkage
macro(link_ggml_backends target)
target_include_directories(${target} PRIVATE
@@ -154,6 +167,12 @@ add_executable(qwen-tts tools/qwen-tts.cpp)
target_link_libraries(qwen-tts PRIVATE qwen-core)
link_ggml_backends(qwen-tts)
+# tts-server : OpenAI-compatible HTTP server over the TTS pipeline.
+# Always built : it is part of the project, not an optional add-on.
+add_executable(tts-server tools/tts-server.cpp)
+target_link_libraries(tts-server PRIVATE qwen-core httplib yyjson)
+link_ggml_backends(tts-server)
+
# test-abi-c : pure C99 smoke test that locks in the public ABI contract.
# Compiles qwen.h with a C compiler under -Wall -Werror -pedantic and
# links against the static lib. The test never loads a model ; failure
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 8e8f8a1..ad8ddd4 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -521,7 +521,7 @@ Sampling:
Debug:
--no-fa Disable flash attention
- --clamp-fp16 Clamp hidden states + V to FP16 range
+ --clamp-fp16 Clamp hidden states to FP16 range
--dump
Dump intermediate tensors (f32) to
```
diff --git a/examples/client.sh b/examples/client.sh
new file mode 100755
index 0000000..6ee64e4
--- /dev/null
+++ b/examples/client.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+# Call the qwentts OpenAI-compatible TTS server.
+# Default response_format is pcm : audio streams chunked as it is generated.
+
+host="${1:-127.0.0.1}"
+port="${2:-8080}"
+
+# Streaming pcm, piped straight into a player as it arrives (real time).
+# s16le mono 24 kHz. ffplay reads the raw stream from stdin.
+curl -s -X POST "http://${host}:${port}/v1/audio/speech" \
+ -H "Content-Type: application/json" \
+ -d '{"input":"The quick brown fox jumps over the lazy dog."}' \
+ | ffplay -f s16le -ar 24000 -ch_layout mono -nodisp -autoexit -i -
+
+# One-shot wav written to a file.
+curl -s -X POST "http://${host}:${port}/v1/audio/speech" \
+ -H "Content-Type: application/json" \
+ -d '{"input":"This one is written to a file.","response_format":"wav"}' \
+ --output out.wav
+echo "wrote out.wav"
diff --git a/examples/server.sh b/examples/server.sh
new file mode 100755
index 0000000..6dde944
--- /dev/null
+++ b/examples/server.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+# Start the qwentts OpenAI-compatible TTS server.
+
+./build/tts-server \
+ --model models/qwen-talker-1.7b-base-Q8_0.gguf \
+ --codec models/qwen-tokenizer-12hz-Q8_0.gguf \
+ --host 127.0.0.1 --port 8080 --lang English
diff --git a/src/qwen.cpp b/src/qwen.cpp
index bc9f8f2..9951c50 100644
--- a/src/qwen.cpp
+++ b/src/qwen.cpp
@@ -397,4 +397,19 @@ int qt_duration_sec_to_tokens(const struct qt_context * q, float duration_sec) {
return pipeline_tts_duration_sec_to_tokens(&q->pt, duration_sec);
}
+int qt_n_speakers(const struct qt_context * q) {
+ if (!q) {
+ qt_set_error("qt_n_speakers: q is NULL");
+ return 0;
+ }
+ return (int) q->pt.speakers.size();
+}
+
+const char * qt_speaker_name(const struct qt_context * q, int i) {
+ if (!q || i < 0 || i >= (int) q->pt.speakers.size()) {
+ return NULL;
+ }
+ return q->pt.speakers[(size_t) i].name.c_str();
+}
+
} // extern "C"
diff --git a/src/qwen.h b/src/qwen.h
index 9398818..8d62f55 100644
--- a/src/qwen.h
+++ b/src/qwen.h
@@ -291,6 +291,14 @@ QT_API enum qt_status qt_synthesize(struct qt_context * q, const struct qt_tts_p
// Clamps to a minimum of one frame.
QT_API int qt_duration_sec_to_tokens(const struct qt_context * q, float duration_sec);
+// Number of named speakers in the loaded model. custom_voice carries a
+// speaker table ; base and voice_design return 0.
+QT_API int qt_n_speakers(const struct qt_context * q);
+
+// Name of speaker i, valid for i in [0, qt_n_speakers). Returns NULL when
+// i is out of range. The pointer stays valid until qt_free. UTF-8.
+QT_API const char * qt_speaker_name(const struct qt_context * q, int i);
+
#ifdef __cplusplus
}
#endif
diff --git a/src/tts-server.h b/src/tts-server.h
new file mode 100644
index 0000000..35d32c3
--- /dev/null
+++ b/src/tts-server.h
@@ -0,0 +1,319 @@
+#pragma once
+// tts-server.h: shared OpenAI-compatible TTS HTTP core for the *.cpp ports.
+//
+// One synthesis context lives GPU resident for the process lifetime. The
+// project tool fills a tts_backend adapter that wires its own ABI
+// (qt_synthesize / ov_synthesize) into the generic sink, then calls
+// tts_server_run. The HTTP layer, tuning, OAI parsing and audio framing
+// are identical across projects ; only the adapter differs.
+//
+// Endpoints:
+// POST /v1/audio/speech OAI text-to-speech
+// GET /v1/models single loaded model
+// GET /v1/voices named speakers (empty when the model has none)
+// GET /health liveness probe
+//
+// Audio out: response_format "pcm" streams s16le 24 kHz mono chunked as it
+// is generated (real time), "wav" returns a one-shot RIFF file. pcm is the
+// default so streaming is on unless the client asks for a file.
+
+#include "../vendor/cpp-httplib/httplib.h"
+#include "audio-io.h"
+#include "yyjson.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+// One synthesis request parsed from the OAI JSON body.
+struct tts_request {
+ std::string input; // text to speak
+ std::string voice; // OAI voice, mapped to a speaker by the adapter
+ std::string instructions; // OAI instructions, mapped to the ABI instruct field
+ std::string format; // "pcm" (stream) or "wav" (one-shot)
+ float speed; // OAI speed, parsed then ignored (no time stretch in the ABI)
+};
+
+// The adapter pushes mono f32 24 kHz audio here. Returns false to abort the
+// synthesis (client gone or cancellation), which propagates into the ABI
+// on_chunk and stops generation.
+using tts_sink = std::function;
+
+// Adapter implemented by each project tool.
+struct tts_backend {
+ std::string model_id; // reported by GET /v1/models
+ std::vector voices; // reported by GET /v1/voices, may be empty
+ // Run synthesis. When the request streams, the adapter routes the ABI
+ // on_chunk to sink ; otherwise it pushes the whole buffer once. Returns
+ // the ABI status (0 on success), and fills err with the ABI message on
+ // failure. The shared layer maps the status to an HTTP code.
+ std::function synthesize;
+};
+
+struct server_config {
+ std::string host = "127.0.0.1";
+ int port = 8080;
+};
+
+// Single GPU context : synthesis is serialised FIFO across connections.
+static std::mutex g_synth_mutex;
+static httplib::Server * g_svr = nullptr;
+
+static void tts_on_signal(int) {
+ if (g_svr) {
+ g_svr->stop();
+ }
+}
+
+// Clamp to [-1, 1] and scale to s16. lrintf rounds to nearest, ties to even.
+static inline int16_t tts_f32_to_s16(float x) {
+ float v = x < -1.0f ? -1.0f : (x > 1.0f ? 1.0f : x);
+ return (int16_t) lrintf(v * 32767.0f);
+}
+
+// Append a mono f32 block as s16le bytes onto out.
+static void tts_append_s16le(std::string & out, const float * samples, int n_samples) {
+ size_t base = out.size();
+ out.resize(base + (size_t) n_samples * 2);
+ char * p = &out[base];
+ for (int i = 0; i < n_samples; i++) {
+ int16_t s = tts_f32_to_s16(samples[i]);
+ *p++ = (char) ((uint16_t) s & 0xff);
+ *p++ = (char) (((uint16_t) s >> 8) & 0xff);
+ }
+}
+
+// Write a JSON error body in the OAI error envelope and set the status.
+static void tts_json_error(httplib::Response & res, int status, const char * type, const char * message) {
+ yyjson_mut_doc * doc = yyjson_mut_doc_new(NULL);
+ yyjson_mut_val * root = yyjson_mut_obj(doc);
+ yyjson_mut_doc_set_root(doc, root);
+ yyjson_mut_val * err = yyjson_mut_obj(doc);
+ yyjson_mut_obj_add_str(doc, err, "message", message);
+ yyjson_mut_obj_add_str(doc, err, "type", type);
+ yyjson_mut_obj_add_val(doc, root, "error", err);
+ char * json = yyjson_mut_write(doc, 0, NULL);
+ res.status = status;
+ res.set_content(json ? json : "{}", "application/json");
+ if (json) {
+ free(json);
+ }
+ yyjson_mut_doc_free(doc);
+}
+
+// Parse the OAI body into req. Returns false and fills err on bad input.
+static bool tts_parse_request(const std::string & body, tts_request & req, std::string & err) {
+ yyjson_doc * doc = yyjson_read(body.c_str(), body.size(), 0);
+ if (!doc) {
+ err = "request body is not valid JSON";
+ return false;
+ }
+ yyjson_val * root = yyjson_doc_get_root(doc);
+ if (!yyjson_is_obj(root)) {
+ err = "request body must be a JSON object";
+ yyjson_doc_free(doc);
+ return false;
+ }
+
+ yyjson_val * input = yyjson_obj_get(root, "input");
+ if (!yyjson_is_str(input) || yyjson_get_len(input) == 0) {
+ err = "'input' must be a non-empty string";
+ yyjson_doc_free(doc);
+ return false;
+ }
+ req.input = yyjson_get_str(input);
+
+ yyjson_val * voice = yyjson_obj_get(root, "voice");
+ req.voice = yyjson_is_str(voice) ? yyjson_get_str(voice) : "";
+
+ yyjson_val * instructions = yyjson_obj_get(root, "instructions");
+ req.instructions = yyjson_is_str(instructions) ? yyjson_get_str(instructions) : "";
+
+ yyjson_val * fmt = yyjson_obj_get(root, "response_format");
+ req.format = yyjson_is_str(fmt) ? yyjson_get_str(fmt) : "pcm";
+
+ yyjson_val * speed = yyjson_obj_get(root, "speed");
+ req.speed = yyjson_is_num(speed) ? (float) yyjson_get_num(speed) : 1.0f;
+
+ yyjson_doc_free(doc);
+
+ if (req.format != "pcm" && req.format != "wav") {
+ err = "response_format must be 'pcm' or 'wav'";
+ return false;
+ }
+ return true;
+}
+
+// Map an ABI status to an HTTP code. The two ABIs share numeric values:
+// -1 invalid params, -2 mode/instruct invalid -> client error ; the rest
+// are server side failures.
+static int tts_status_to_http(int rc) {
+ if (rc == 0) {
+ return 200;
+ }
+ if (rc == -1 || rc == -2) {
+ return 400;
+ }
+ return 502;
+}
+
+static void tts_handle_speech(const tts_backend & be, const httplib::Request & http_req, httplib::Response & res) {
+ tts_request req;
+ std::string err;
+ if (!tts_parse_request(http_req.body, req, err)) {
+ tts_json_error(res, 400, "invalid_request_error", err.c_str());
+ return;
+ }
+
+ if (req.format == "wav") {
+ // One-shot : collect the whole utterance, then emit a RIFF file.
+ std::vector buf;
+ tts_sink sink = [&buf](const float * s, int n) {
+ buf.insert(buf.end(), s, s + n);
+ return true;
+ };
+ std::string synth_err;
+ int rc;
+ {
+ std::lock_guard lock(g_synth_mutex);
+ rc = be.synthesize(req, sink, synth_err);
+ }
+ if (rc != 0) {
+ tts_json_error(res, tts_status_to_http(rc), "server_error",
+ synth_err.empty() ? "synthesis failed" : synth_err.c_str());
+ return;
+ }
+ std::string wav = audio_encode_wav(buf.data(), (int) buf.size(), 24000, WAV_S16);
+ res.set_content(std::move(wav), "audio/wav");
+ return;
+ }
+
+ // Streaming : run synthesis inside the chunked provider on the connection
+ // thread, pushing s16le frames as the codec produces them. A failed
+ // sink.write means the client disconnected, which aborts generation and
+ // frees the GPU instead of finishing a stream nobody reads.
+ res.set_header("Cache-Control", "no-cache");
+ res.set_header("X-Accel-Buffering", "no");
+ res.set_chunked_content_provider("audio/pcm", [&be, req](size_t, httplib::DataSink & sink) mutable -> bool {
+ tts_sink push = [&sink](const float * s, int n) {
+ std::string bytes;
+ tts_append_s16le(bytes, s, n);
+ return sink.write(bytes.data(), bytes.size());
+ };
+ std::string synth_err;
+ {
+ std::lock_guard lock(g_synth_mutex);
+ be.synthesize(req, push, synth_err);
+ }
+ sink.done();
+ return true;
+ });
+}
+
+static void tts_handle_models(const tts_backend & be, const httplib::Request &, httplib::Response & res) {
+ yyjson_mut_doc * doc = yyjson_mut_doc_new(NULL);
+ yyjson_mut_val * root = yyjson_mut_obj(doc);
+ yyjson_mut_doc_set_root(doc, root);
+ yyjson_mut_obj_add_str(doc, root, "object", "list");
+ yyjson_mut_val * data = yyjson_mut_arr(doc);
+ yyjson_mut_val * one = yyjson_mut_obj(doc);
+ yyjson_mut_obj_add_str(doc, one, "id", be.model_id.c_str());
+ yyjson_mut_obj_add_str(doc, one, "object", "model");
+ yyjson_mut_obj_add_str(doc, one, "owned_by", "local");
+ yyjson_mut_arr_add_val(data, one);
+ yyjson_mut_obj_add_val(doc, root, "data", data);
+ char * json = yyjson_mut_write(doc, 0, NULL);
+ res.set_content(json ? json : "{}", "application/json");
+ if (json) {
+ free(json);
+ }
+ yyjson_mut_doc_free(doc);
+}
+
+static void tts_handle_voices(const tts_backend & be, const httplib::Request &, httplib::Response & res) {
+ yyjson_mut_doc * doc = yyjson_mut_doc_new(NULL);
+ yyjson_mut_val * root = yyjson_mut_obj(doc);
+ yyjson_mut_doc_set_root(doc, root);
+ yyjson_mut_val * arr = yyjson_mut_arr(doc);
+ for (const std::string & v : be.voices) {
+ yyjson_mut_val * one = yyjson_mut_obj(doc);
+ yyjson_mut_obj_add_str(doc, one, "name", v.c_str());
+ yyjson_mut_arr_add_val(arr, one);
+ }
+ yyjson_mut_obj_add_val(doc, root, "voices", arr);
+ char * json = yyjson_mut_write(doc, 0, NULL);
+ res.set_content(json ? json : "{}", "application/json");
+ if (json) {
+ free(json);
+ }
+ yyjson_mut_doc_free(doc);
+}
+
+static void tts_handle_health(const httplib::Request &, httplib::Response & res) {
+ res.set_content("{\"status\":\"ok\"}", "application/json");
+}
+
+static int tts_server_run(const tts_backend & be, const server_config & cfg) {
+ httplib::Server svr;
+ g_svr = &svr;
+
+ // per-operation socket idle timeouts. read is small (text in), write is
+ // generous to cover a long streamed utterance without tripping on a slow
+ // client.
+ svr.set_read_timeout(60);
+ svr.set_write_timeout(120);
+
+ // reject oversized bodies. text plus an optional reference clip stays
+ // well under this.
+ svr.set_payload_max_length(32 * 1024 * 1024);
+
+ // Nagle coalescing holds small packets back for tens of ms ; streamed
+ // PCM chunks must leave the socket the moment they are written.
+ svr.set_tcp_nodelay(true);
+
+ // SO_REUSEADDR lets us rebind a port still in TIME_WAIT after a restart.
+ // SO_REUSEPORT is deliberately not set : a second instance on the same
+ // port then fails with EADDRINUSE instead of silently sharing the socket
+ // and splitting traffic between two daemons.
+ svr.set_socket_options([](socket_t sock) {
+ int one = 1;
+#ifdef _WIN32
+ setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char *) &one, sizeof(one));
+#else
+ setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
+#endif
+ });
+
+ // permissive CORS so a browser client can call the API directly.
+ svr.set_default_headers({
+ { "Access-Control-Allow-Origin", "*" }
+ });
+ svr.Options("/.*", [](const httplib::Request &, httplib::Response & res) {
+ res.set_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
+ res.set_header("Access-Control-Allow-Headers", "Content-Type");
+ });
+
+ svr.Post("/v1/audio/speech",
+ [&be](const httplib::Request & req, httplib::Response & res) { tts_handle_speech(be, req, res); });
+ svr.Get("/v1/models",
+ [&be](const httplib::Request & req, httplib::Response & res) { tts_handle_models(be, req, res); });
+ svr.Get("/v1/voices",
+ [&be](const httplib::Request & req, httplib::Response & res) { tts_handle_voices(be, req, res); });
+ svr.Get("/health", tts_handle_health);
+
+ signal(SIGINT, tts_on_signal);
+ signal(SIGTERM, tts_on_signal);
+
+ fprintf(stderr, "[Server] model %s\n", be.model_id.c_str());
+ fprintf(stderr, "[Server] listening on %s:%d\n", cfg.host.c_str(), cfg.port);
+ if (!svr.listen(cfg.host.c_str(), cfg.port)) {
+ fprintf(stderr, "[Server] FATAL: cannot bind %s:%d\n", cfg.host.c_str(), cfg.port);
+ return 1;
+ }
+ return 0;
+}
diff --git a/tests/abi-c.c b/tests/abi-c.c
index ef0fea7..8a55eee 100644
--- a/tests/abi-c.c
+++ b/tests/abi-c.c
@@ -159,6 +159,17 @@ int main(void) {
return 4;
}
+ /* Speaker getters resolve and behave on a NULL handle : count is 0 and
+ * any index returns NULL. No model is needed to lock in the contract. */
+ if (qt_n_speakers(NULL) != 0) {
+ fprintf(stderr, "[Probe] qt_n_speakers(NULL) must be 0\n");
+ return 9;
+ }
+ if (qt_speaker_name(NULL, 0) != NULL) {
+ fprintf(stderr, "[Probe] qt_speaker_name(NULL, 0) must be NULL\n");
+ return 10;
+ }
+
/* Restore the default stderr fallback before exit so the trailing
* [Qwen] log lines from the cleanup paths land where the user
* expects them. */
diff --git a/tools/qwen-tts.cpp b/tools/qwen-tts.cpp
index 4fdb6ff..e127258 100644
--- a/tools/qwen-tts.cpp
+++ b/tools/qwen-tts.cpp
@@ -55,7 +55,7 @@ static void print_usage(const char * prog) {
" --sub-top-p Sub-talker top-p (default: 1.0)\n\n"
"Debug:\n"
" --no-fa Disable flash attention\n"
- " --clamp-fp16 Clamp hidden states + V to FP16 range\n"
+ " --clamp-fp16 Clamp hidden states to FP16 range\n"
" --dump Dump intermediate tensors (f32) to \n",
prog);
}
diff --git a/tools/tts-server.cpp b/tools/tts-server.cpp
new file mode 100644
index 0000000..b152b32
--- /dev/null
+++ b/tools/tts-server.cpp
@@ -0,0 +1,131 @@
+// tts-server.cpp: OpenAI-compatible HTTP server backed by the qwentts
+// ABI. Loads a talker + codec once, GPU resident, and serves synthesis over
+// POST /v1/audio/speech. The shared core lives in src/tts-server.h ; this
+// file only wires the qt_* ABI into the generic adapter.
+
+#include "qwen.h"
+#include "tts-server.h"
+#include "version.h"
+
+#include
+#include
+#include
+
+static void print_usage(const char * prog) {
+ fprintf(stderr, "qwentts.cpp %s\n\n", QWEN_VERSION);
+ fprintf(stderr,
+ "Usage: %s --model --codec [options]\n\n"
+ "Required:\n"
+ " --model Talker LM GGUF (qwen-talker-*.gguf)\n"
+ " --codec Codec GGUF (qwen-tokenizer-*.gguf)\n\n"
+ "Optional:\n"
+ " --host Listen address (default: 127.0.0.1)\n"
+ " --port Listen port (default: 8080)\n"
+ " --lang Language label (default: english)\n"
+ " --no-fa Disable flash attention\n"
+ " --clamp-fp16 Clamp hidden states to FP16 range\n",
+ prog);
+}
+
+// Trim a path down to its file name for the reported model id.
+static std::string basename_of(const char * path) {
+ std::string s = path;
+ size_t p = s.find_last_of("/\\");
+ return p == std::string::npos ? s : s.substr(p + 1);
+}
+
+int main(int argc, char ** argv) {
+ const char * talker_path = NULL;
+ const char * codec_path = NULL;
+ std::string lang = "english";
+ server_config cfg;
+ bool use_fa = true;
+ bool clamp_fp16 = false;
+
+ for (int i = 1; i < argc; i++) {
+ const char * arg = argv[i];
+ if (!std::strcmp(arg, "--model") && i + 1 < argc) {
+ talker_path = argv[++i];
+ } else if (!std::strcmp(arg, "--codec") && i + 1 < argc) {
+ codec_path = argv[++i];
+ } else if (!std::strcmp(arg, "--host") && i + 1 < argc) {
+ cfg.host = argv[++i];
+ } else if (!std::strcmp(arg, "--port") && i + 1 < argc) {
+ cfg.port = std::atoi(argv[++i]);
+ } else if (!std::strcmp(arg, "--lang") && i + 1 < argc) {
+ lang = argv[++i];
+ } else if (!std::strcmp(arg, "--no-fa")) {
+ use_fa = false;
+ } else if (!std::strcmp(arg, "--clamp-fp16")) {
+ clamp_fp16 = true;
+ } else if (!std::strcmp(arg, "--help") || !std::strcmp(arg, "-h")) {
+ print_usage(argv[0]);
+ return 0;
+ } else {
+ fprintf(stderr, "[CLI] ERROR: unknown arg: %s\n", arg);
+ print_usage(argv[0]);
+ return 1;
+ }
+ }
+
+ if (!talker_path || !codec_path) {
+ print_usage(argv[0]);
+ return 0;
+ }
+
+ struct qt_init_params iparams;
+ qt_init_default_params(&iparams);
+ iparams.talker_path = talker_path;
+ iparams.codec_path = codec_path;
+ iparams.use_fa = use_fa;
+ iparams.clamp_fp16 = clamp_fp16;
+
+ struct qt_context * q = qt_init(&iparams);
+ if (!q) {
+ fprintf(stderr, "[Server] FATAL: %s\n", qt_last_error());
+ return 1;
+ }
+
+ tts_backend be;
+ be.model_id = basename_of(talker_path);
+ int n = qt_n_speakers(q);
+ for (int i = 0; i < n; i++) {
+ be.voices.push_back(qt_speaker_name(q, i));
+ }
+
+ // The adapter always drives the streaming pipeline : on_chunk routes to
+ // the shared sink, which either streams to the socket (pcm) or fills a
+ // one-shot buffer (wav). Either way the audio path is identical.
+ be.synthesize = [q, &lang](const tts_request & req, const tts_sink & sink, std::string & err) -> int {
+ struct qt_tts_params p;
+ qt_tts_default_params(&p);
+ p.text = req.input.c_str();
+ p.lang = lang.c_str();
+ if (!req.voice.empty() && qt_n_speakers(q) > 0) {
+ p.speaker = req.voice.c_str();
+ }
+ if (!req.instructions.empty()) {
+ p.instruct = req.instructions.c_str();
+ }
+
+ // Trampoline : the C ABI on_chunk forwards to the C++ sink.
+ const tts_sink * sink_ptr = &sink;
+ p.on_chunk = [](const float * s, int ns, void * u) -> bool {
+ return (*static_cast(u))(s, ns);
+ };
+ p.on_chunk_user_data = (void *) sink_ptr;
+
+ struct qt_audio out = {};
+ enum qt_status rc = qt_synthesize(q, &p, &out);
+ qt_audio_free(&out);
+ if (rc != QT_STATUS_OK) {
+ err = qt_last_error();
+ return (int) rc;
+ }
+ return 0;
+ };
+
+ int rc = tts_server_run(be, cfg);
+ qt_free(q);
+ return rc;
+}
diff --git a/vendor/cpp-httplib/CMakeLists.txt b/vendor/cpp-httplib/CMakeLists.txt
new file mode 100644
index 0000000..a27404d
--- /dev/null
+++ b/vendor/cpp-httplib/CMakeLists.txt
@@ -0,0 +1,15 @@
+# cpp-httplib: HTTP server library (no SSL, behind reverse proxy in prod)
+
+add_library(httplib STATIC httplib.cpp)
+target_include_directories(httplib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
+
+# suppress warnings in third-party code
+if(MSVC)
+ target_compile_options(httplib PRIVATE /w)
+else()
+ target_compile_options(httplib PRIVATE -w)
+endif()
+
+if(WIN32)
+ target_link_libraries(httplib PRIVATE ws2_32)
+endif()
diff --git a/vendor/cpp-httplib/LICENSE b/vendor/cpp-httplib/LICENSE
new file mode 100644
index 0000000..3e5ed35
--- /dev/null
+++ b/vendor/cpp-httplib/LICENSE
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2017 yhirose
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/vendor/cpp-httplib/httplib.cpp b/vendor/cpp-httplib/httplib.cpp
new file mode 100644
index 0000000..b9d05d9
--- /dev/null
+++ b/vendor/cpp-httplib/httplib.cpp
@@ -0,0 +1,16526 @@
+#include "httplib.h"
+namespace httplib {
+
+/*
+ * Implementation that will be part of the .cc file if split into .h + .cc.
+ */
+
+namespace stream {
+
+// stream::Result implementations
+Result::Result() : chunk_size_(8192) {}
+
+Result::Result(ClientImpl::StreamHandle &&handle, size_t chunk_size)
+ : handle_(std::move(handle)), chunk_size_(chunk_size) {}
+
+Result::Result(Result &&other) noexcept
+ : handle_(std::move(other.handle_)), buffer_(std::move(other.buffer_)),
+ current_size_(other.current_size_), chunk_size_(other.chunk_size_),
+ finished_(other.finished_) {
+ other.current_size_ = 0;
+ other.finished_ = true;
+}
+
+Result &Result::operator=(Result &&other) noexcept {
+ if (this != &other) {
+ handle_ = std::move(other.handle_);
+ buffer_ = std::move(other.buffer_);
+ current_size_ = other.current_size_;
+ chunk_size_ = other.chunk_size_;
+ finished_ = other.finished_;
+ other.current_size_ = 0;
+ other.finished_ = true;
+ }
+ return *this;
+}
+
+bool Result::is_valid() const { return handle_.is_valid(); }
+Result::operator bool() const { return is_valid(); }
+
+int Result::status() const {
+ return handle_.response ? handle_.response->status : -1;
+}
+
+const Headers &Result::headers() const {
+ static const Headers empty_headers;
+ return handle_.response ? handle_.response->headers : empty_headers;
+}
+
+std::string Result::get_header_value(const std::string &key,
+ const char *def) const {
+ return handle_.response ? handle_.response->get_header_value(key, def) : def;
+}
+
+bool Result::has_header(const std::string &key) const {
+ return handle_.response ? handle_.response->has_header(key) : false;
+}
+
+Error Result::error() const { return handle_.error; }
+Error Result::read_error() const { return handle_.get_read_error(); }
+bool Result::has_read_error() const { return handle_.has_read_error(); }
+
+bool Result::next() {
+ if (!handle_.is_valid() || finished_) { return false; }
+
+ if (buffer_.size() < chunk_size_) { buffer_.resize(chunk_size_); }
+
+ ssize_t n = handle_.read(&buffer_[0], chunk_size_);
+ if (n > 0) {
+ current_size_ = static_cast(n);
+ return true;
+ }
+
+ current_size_ = 0;
+ finished_ = true;
+ return false;
+}
+
+const char *Result::data() const { return buffer_.data(); }
+size_t Result::size() const { return current_size_; }
+
+std::string Result::read_all() {
+ std::string result;
+ while (next()) {
+ result.append(data(), size());
+ }
+ return result;
+}
+
+} // namespace stream
+
+namespace sse {
+
+// SSEMessage implementations
+SSEMessage::SSEMessage() : event("message") {}
+
+void SSEMessage::clear() {
+ event = "message";
+ data.clear();
+ id.clear();
+}
+
+// SSEClient implementations
+SSEClient::SSEClient(Client &client, const std::string &path)
+ : client_(client), path_(path) {}
+
+SSEClient::SSEClient(Client &client, const std::string &path,
+ const Headers &headers)
+ : client_(client), path_(path), headers_(headers) {}
+
+SSEClient::~SSEClient() { stop(); }
+
+SSEClient &SSEClient::on_message(MessageHandler handler) {
+ on_message_ = std::move(handler);
+ return *this;
+}
+
+SSEClient &SSEClient::on_event(const std::string &type,
+ MessageHandler handler) {
+ event_handlers_[type] = std::move(handler);
+ return *this;
+}
+
+SSEClient &SSEClient::on_open(OpenHandler handler) {
+ on_open_ = std::move(handler);
+ return *this;
+}
+
+SSEClient &SSEClient::on_error(ErrorHandler handler) {
+ on_error_ = std::move(handler);
+ return *this;
+}
+
+SSEClient &SSEClient::set_reconnect_interval(int ms) {
+ reconnect_interval_ms_ = ms;
+ return *this;
+}
+
+SSEClient &SSEClient::set_max_reconnect_attempts(int n) {
+ max_reconnect_attempts_ = n;
+ return *this;
+}
+
+SSEClient &SSEClient::set_headers(const Headers &headers) {
+ std::lock_guard lock(headers_mutex_);
+ headers_ = headers;
+ return *this;
+}
+
+bool SSEClient::is_connected() const { return connected_.load(); }
+
+const std::string &SSEClient::last_event_id() const {
+ return last_event_id_;
+}
+
+void SSEClient::start() {
+ running_.store(true);
+ run_event_loop();
+}
+
+void SSEClient::start_async() {
+ running_.store(true);
+ async_thread_ = std::thread([this]() { run_event_loop(); });
+}
+
+void SSEClient::stop() {
+ running_.store(false);
+ client_.stop(); // Cancel any pending operations
+ if (async_thread_.joinable()) { async_thread_.join(); }
+}
+
+bool SSEClient::parse_sse_line(const std::string &line, SSEMessage &msg,
+ int &retry_ms) {
+ // Blank line signals end of event
+ if (line.empty() || line == "\r") { return true; }
+
+ // Lines starting with ':' are comments (ignored)
+ if (!line.empty() && line[0] == ':') { return false; }
+
+ // Find the colon separator
+ auto colon_pos = line.find(':');
+ if (colon_pos == std::string::npos) {
+ // Line with no colon is treated as field name with empty value
+ return false;
+ }
+
+ auto field = line.substr(0, colon_pos);
+ std::string value;
+
+ // Value starts after colon, skip optional single space
+ if (colon_pos + 1 < line.size()) {
+ auto value_start = colon_pos + 1;
+ if (line[value_start] == ' ') { value_start++; }
+ value = line.substr(value_start);
+ // Remove trailing \r if present
+ if (!value.empty() && value.back() == '\r') { value.pop_back(); }
+ }
+
+ // Handle known fields
+ if (field == "event") {
+ msg.event = value;
+ } else if (field == "data") {
+ // Multiple data lines are concatenated with newlines
+ if (!msg.data.empty()) { msg.data += "\n"; }
+ msg.data += value;
+ } else if (field == "id") {
+ // Empty id is valid (clears the last event ID)
+ msg.id = value;
+ } else if (field == "retry") {
+ // Parse retry interval in milliseconds
+ {
+ int v = 0;
+ auto res =
+ detail::from_chars(value.data(), value.data() + value.size(), v);
+ if (res.ec == std::errc{}) { retry_ms = v; }
+ }
+ }
+ // Unknown fields are ignored per SSE spec
+
+ return false;
+}
+
+void SSEClient::run_event_loop() {
+ auto reconnect_count = 0;
+
+ while (running_.load()) {
+ // Build headers, including Last-Event-ID if we have one
+ Headers request_headers;
+ {
+ std::lock_guard lock(headers_mutex_);
+ request_headers = headers_;
+ }
+ if (!last_event_id_.empty()) {
+ request_headers.emplace("Last-Event-ID", last_event_id_);
+ }
+
+ // Open streaming connection
+ auto result = stream::Get(client_, path_, request_headers);
+
+ // Connection error handling
+ if (!result) {
+ connected_.store(false);
+ if (on_error_) { on_error_(result.error()); }
+
+ if (!should_reconnect(reconnect_count)) { break; }
+ wait_for_reconnect();
+ reconnect_count++;
+ continue;
+ }
+
+ if (result.status() != StatusCode::OK_200) {
+ connected_.store(false);
+ if (on_error_) { on_error_(Error::Connection); }
+
+ // For certain errors, don't reconnect.
+ // Note: 401 is intentionally absent so that handlers can refresh
+ // credentials via set_headers() and let the client reconnect.
+ if (result.status() == StatusCode::NoContent_204 ||
+ result.status() == StatusCode::NotFound_404 ||
+ result.status() == StatusCode::Forbidden_403) {
+ break;
+ }
+
+ if (!should_reconnect(reconnect_count)) { break; }
+ wait_for_reconnect();
+ reconnect_count++;
+ continue;
+ }
+
+ // Connection successful
+ connected_.store(true);
+ reconnect_count = 0;
+ if (on_open_) { on_open_(); }
+
+ // Event receiving loop
+ std::string buffer;
+ SSEMessage current_msg;
+
+ while (running_.load() && result.next()) {
+ buffer.append(result.data(), result.size());
+
+ // Process complete lines in the buffer
+ size_t line_start = 0;
+ size_t newline_pos;
+
+ while ((newline_pos = buffer.find('\n', line_start)) !=
+ std::string::npos) {
+ auto line = buffer.substr(line_start, newline_pos - line_start);
+ line_start = newline_pos + 1;
+
+ // Parse the line and check if event is complete
+ auto event_complete =
+ parse_sse_line(line, current_msg, reconnect_interval_ms_);
+
+ if (event_complete && !current_msg.data.empty()) {
+ // Update last_event_id for reconnection
+ if (!current_msg.id.empty()) { last_event_id_ = current_msg.id; }
+
+ // Dispatch event to appropriate handler
+ dispatch_event(current_msg);
+
+ current_msg.clear();
+ }
+ }
+
+ // Keep unprocessed data in buffer
+ buffer.erase(0, line_start);
+ }
+
+ // Connection ended
+ connected_.store(false);
+
+ if (!running_.load()) { break; }
+
+ // Check for read errors
+ if (result.has_read_error()) {
+ if (on_error_) { on_error_(result.read_error()); }
+ }
+
+ if (!should_reconnect(reconnect_count)) { break; }
+ wait_for_reconnect();
+ reconnect_count++;
+ }
+
+ connected_.store(false);
+}
+
+void SSEClient::dispatch_event(const SSEMessage &msg) {
+ // Check for specific event type handler first
+ auto it = event_handlers_.find(msg.event);
+ if (it != event_handlers_.end()) {
+ it->second(msg);
+ return;
+ }
+
+ // Fall back to generic message handler
+ if (on_message_) { on_message_(msg); }
+}
+
+bool SSEClient::should_reconnect(int count) const {
+ if (!running_.load()) { return false; }
+ if (max_reconnect_attempts_ == 0) { return true; } // unlimited
+ return count < max_reconnect_attempts_;
+}
+
+void SSEClient::wait_for_reconnect() {
+ // Use small increments to check running_ flag frequently
+ auto waited = 0;
+ while (running_.load() && waited < reconnect_interval_ms_) {
+ std::this_thread::sleep_for(std::chrono::milliseconds(100));
+ waited += 100;
+ }
+}
+
+} // namespace sse
+
+#ifdef CPPHTTPLIB_SSL_ENABLED
+/*
+ * TLS abstraction layer - internal function declarations
+ * These are implementation details and not part of the public API.
+ */
+namespace tls {
+
+// Client context
+ctx_t create_client_context();
+void free_context(ctx_t ctx);
+bool set_min_version(ctx_t ctx, Version version);
+bool load_ca_pem(ctx_t ctx, const char *pem, size_t len);
+bool load_ca_file(ctx_t ctx, const char *file_path);
+bool load_ca_dir(ctx_t ctx, const char *dir_path);
+bool load_system_certs(ctx_t ctx);
+bool set_client_cert_pem(ctx_t ctx, const char *cert, const char *key,
+ const char *password);
+bool set_client_cert_file(ctx_t ctx, const char *cert_path,
+ const char *key_path, const char *password);
+
+// Server context
+ctx_t create_server_context();
+bool set_server_cert_pem(ctx_t ctx, const char *cert, const char *key,
+ const char *password);
+bool set_server_cert_file(ctx_t ctx, const char *cert_path,
+ const char *key_path, const char *password);
+bool set_client_ca_file(ctx_t ctx, const char *ca_file, const char *ca_dir);
+void set_verify_client(ctx_t ctx, bool require);
+
+// Session management
+session_t create_session(ctx_t ctx, socket_t sock);
+void free_session(session_t session);
+bool set_sni(session_t session, const char *hostname);
+bool set_hostname(session_t session, const char *hostname);
+
+// Handshake (non-blocking capable)
+TlsError connect(session_t session);
+TlsError accept(session_t session);
+
+// Handshake with timeout (blocking until timeout)
+bool connect_nonblocking(session_t session, socket_t sock, time_t timeout_sec,
+ time_t timeout_usec, TlsError *err);
+bool accept_nonblocking(session_t session, socket_t sock, time_t timeout_sec,
+ time_t timeout_usec, TlsError *err);
+
+// I/O (non-blocking capable)
+ssize_t read(session_t session, void *buf, size_t len, TlsError &err);
+ssize_t write(session_t session, const void *buf, size_t len, TlsError &err);
+int pending(const_session_t session);
+void shutdown(session_t session, bool graceful);
+
+// Connection state
+bool is_peer_closed(session_t session, socket_t sock);
+
+// Certificate verification
+cert_t get_peer_cert(const_session_t session);
+void free_cert(cert_t cert);
+bool verify_hostname(cert_t cert, const char *hostname);
+uint64_t hostname_mismatch_code();
+long get_verify_result(const_session_t session);
+
+// Certificate introspection
+std::string get_cert_subject_cn(cert_t cert);
+std::string get_cert_issuer_name(cert_t cert);
+bool get_cert_sans(cert_t cert, std::vector &sans);
+bool get_cert_validity(cert_t cert, time_t ¬_before, time_t ¬_after);
+std::string get_cert_serial(cert_t cert);
+bool get_cert_der(cert_t cert, std::vector &der);
+const char *get_sni(const_session_t session);
+
+// CA store management
+ca_store_t create_ca_store(const char *pem, size_t len);
+void free_ca_store(ca_store_t store);
+bool set_ca_store(ctx_t ctx, ca_store_t store);
+size_t get_ca_certs(ctx_t ctx, std::vector &certs);
+std::vector get_ca_names(ctx_t ctx);
+
+// Dynamic certificate update (for servers)
+bool update_server_cert(ctx_t ctx, const char *cert_pem, const char *key_pem,
+ const char *password);
+bool update_server_client_ca(ctx_t ctx, const char *ca_pem);
+
+// Certificate verification callback
+bool set_verify_callback(ctx_t ctx, VerifyCallback callback);
+long get_verify_error(const_session_t session);
+std::string verify_error_string(long error_code);
+
+// TlsError information
+uint64_t peek_error();
+uint64_t get_error();
+std::string error_string(uint64_t code);
+
+} // namespace tls
+#endif // CPPHTTPLIB_SSL_ENABLED
+
+/*
+ * Group 1: detail namespace - Non-SSL utilities
+ */
+
+namespace detail {
+
+bool set_socket_opt_impl(socket_t sock, int level, int optname,
+ const void *optval, socklen_t optlen) {
+ return setsockopt(sock, level, optname,
+#ifdef _WIN32
+ reinterpret_cast(optval),
+#else
+ optval,
+#endif
+ optlen) == 0;
+}
+
+bool set_socket_opt_time(socket_t sock, int level, int optname,
+ time_t sec, time_t usec) {
+#ifdef _WIN32
+ auto timeout = static_cast(sec * 1000 + usec / 1000);
+#else
+ timeval timeout;
+ timeout.tv_sec = static_cast(sec);
+ timeout.tv_usec = static_cast(usec);
+#endif
+ return set_socket_opt_impl(sock, level, optname, &timeout, sizeof(timeout));
+}
+
+bool is_hex(char c, int &v) {
+ if (isdigit(c)) {
+ v = c - '0';
+ return true;
+ } else if ('A' <= c && c <= 'F') {
+ v = c - 'A' + 10;
+ return true;
+ } else if ('a' <= c && c <= 'f') {
+ v = c - 'a' + 10;
+ return true;
+ }
+ return false;
+}
+
+bool from_hex_to_i(const std::string &s, size_t i, size_t cnt,
+ int &val) {
+ if (i >= s.size()) { return false; }
+
+ val = 0;
+ for (; cnt; i++, cnt--) {
+ if (!s[i]) { return false; }
+ auto v = 0;
+ if (is_hex(s[i], v)) {
+ val = val * 16 + v;
+ } else {
+ return false;
+ }
+ }
+ return true;
+}
+
+std::string from_i_to_hex(size_t n) {
+ static const auto charset = "0123456789abcdef";
+ std::string ret;
+ do {
+ ret = charset[n & 15] + ret;
+ n >>= 4;
+ } while (n > 0);
+ return ret;
+}
+
+std::string compute_etag(const FileStat &fs) {
+ if (!fs.is_file()) { return std::string(); }
+
+ // If mtime cannot be determined (negative value indicates an error
+ // or sentinel), do not generate an ETag. Returning a neutral / fixed
+ // value like 0 could collide with a real file that legitimately has
+ // mtime == 0 (epoch) and lead to misleading validators.
+ auto mtime_raw = fs.mtime();
+ if (mtime_raw < 0) { return std::string(); }
+
+ auto mtime = static_cast(mtime_raw);
+ auto size = fs.size();
+
+ return std::string("W/\"") + from_i_to_hex(mtime) + "-" +
+ from_i_to_hex(size) + "\"";
+}
+
+// Format time_t as HTTP-date (RFC 9110 Section 5.6.7): "Sun, 06 Nov 1994
+// 08:49:37 GMT" This implementation is defensive: it validates `mtime`, checks
+// return values from `gmtime_r`/`gmtime_s`, and ensures `strftime` succeeds.
+std::string file_mtime_to_http_date(time_t mtime) {
+ if (mtime < 0) { return std::string(); }
+
+ struct tm tm_buf;
+#ifdef _WIN32
+ if (gmtime_s(&tm_buf, &mtime) != 0) { return std::string(); }
+#else
+ if (gmtime_r(&mtime, &tm_buf) == nullptr) { return std::string(); }
+#endif
+ char buf[64];
+ if (strftime(buf, sizeof(buf), "%a, %d %b %Y %H:%M:%S GMT", &tm_buf) == 0) {
+ return std::string();
+ }
+
+ return std::string(buf);
+}
+
+// Parse HTTP-date (RFC 9110 Section 5.6.7) to time_t. Returns -1 on failure.
+time_t parse_http_date(const std::string &date_str) {
+ struct tm tm_buf;
+
+ // Create a classic locale object once for all parsing attempts
+ const std::locale classic_locale = std::locale::classic();
+
+ // Try to parse using std::get_time (C++11, cross-platform)
+ auto try_parse = [&](const char *fmt) -> bool {
+ std::istringstream ss(date_str);
+ ss.imbue(classic_locale);
+
+ memset(&tm_buf, 0, sizeof(tm_buf));
+ ss >> std::get_time(&tm_buf, fmt);
+
+ return !ss.fail();
+ };
+
+ // RFC 9110 preferred format (HTTP-date): "Sun, 06 Nov 1994 08:49:37 GMT"
+ if (!try_parse("%a, %d %b %Y %H:%M:%S")) {
+ // RFC 850 format: "Sunday, 06-Nov-94 08:49:37 GMT"
+ if (!try_parse("%A, %d-%b-%y %H:%M:%S")) {
+ // asctime format: "Sun Nov 6 08:49:37 1994"
+ if (!try_parse("%a %b %d %H:%M:%S %Y")) {
+ return static_cast(-1);
+ }
+ }
+ }
+
+#ifdef _WIN32
+ return _mkgmtime(&tm_buf);
+#elif defined _AIX
+ return mktime(&tm_buf);
+#else
+ return timegm(&tm_buf);
+#endif
+}
+
+bool is_weak_etag(const std::string &s) {
+ // Check if the string is a weak ETag (starts with 'W/"')
+ return s.size() > 3 && s[0] == 'W' && s[1] == '/' && s[2] == '"';
+}
+
+bool is_strong_etag(const std::string &s) {
+ // Check if the string is a strong ETag (starts and ends with '"', at least 2
+ // chars)
+ return s.size() >= 2 && s[0] == '"' && s.back() == '"';
+}
+
+size_t to_utf8(int code, char *buff) {
+ if (code < 0x0080) {
+ buff[0] = static_cast(code & 0x7F);
+ return 1;
+ } else if (code < 0x0800) {
+ buff[0] = static_cast(0xC0 | ((code >> 6) & 0x1F));
+ buff[1] = static_cast(0x80 | (code & 0x3F));
+ return 2;
+ } else if (code < 0xD800) {
+ buff[0] = static_cast(0xE0 | ((code >> 12) & 0xF));
+ buff[1] = static_cast(0x80 | ((code >> 6) & 0x3F));
+ buff[2] = static_cast(0x80 | (code & 0x3F));
+ return 3;
+ } else if (code < 0xE000) { // D800 - DFFF is invalid...
+ return 0;
+ } else if (code < 0x10000) {
+ buff[0] = static_cast(0xE0 | ((code >> 12) & 0xF));
+ buff[1] = static_cast(0x80 | ((code >> 6) & 0x3F));
+ buff[2] = static_cast(0x80 | (code & 0x3F));
+ return 3;
+ } else if (code < 0x110000) {
+ buff[0] = static_cast(0xF0 | ((code >> 18) & 0x7));
+ buff[1] = static_cast(0x80 | ((code >> 12) & 0x3F));
+ buff[2] = static_cast(0x80 | ((code >> 6) & 0x3F));
+ buff[3] = static_cast(0x80 | (code & 0x3F));
+ return 4;
+ }
+
+ // NOTREACHED
+ return 0;
+}
+
+} // namespace detail
+
+namespace ws {
+namespace impl {
+
+bool is_valid_utf8(const std::string &s) {
+ size_t i = 0;
+ auto n = s.size();
+ while (i < n) {
+ auto c = static_cast(s[i]);
+ size_t len;
+ uint32_t cp;
+ if (c < 0x80) {
+ i++;
+ continue;
+ } else if ((c & 0xE0) == 0xC0) {
+ len = 2;
+ cp = c & 0x1F;
+ } else if ((c & 0xF0) == 0xE0) {
+ len = 3;
+ cp = c & 0x0F;
+ } else if ((c & 0xF8) == 0xF0) {
+ len = 4;
+ cp = c & 0x07;
+ } else {
+ return false;
+ }
+ if (i + len > n) { return false; }
+ for (size_t j = 1; j < len; j++) {
+ auto b = static_cast(s[i + j]);
+ if ((b & 0xC0) != 0x80) { return false; }
+ cp = (cp << 6) | (b & 0x3F);
+ }
+ // Overlong encoding check
+ if (len == 2 && cp < 0x80) { return false; }
+ if (len == 3 && cp < 0x800) { return false; }
+ if (len == 4 && cp < 0x10000) { return false; }
+ // Surrogate halves (U+D800..U+DFFF) and beyond U+10FFFF are invalid
+ if (cp >= 0xD800 && cp <= 0xDFFF) { return false; }
+ if (cp > 0x10FFFF) { return false; }
+ i += len;
+ }
+ return true;
+}
+
+} // namespace impl
+} // namespace ws
+
+namespace detail {
+
+// NOTE: This code came up with the following stackoverflow post:
+// https://stackoverflow.com/questions/180947/base64-decode-snippet-in-c
+std::string base64_encode(const std::string &in) {
+ static const auto lookup =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+ std::string out;
+ out.reserve(in.size());
+
+ auto val = 0;
+ auto valb = -6;
+
+ for (auto c : in) {
+ val = (val << 8) + static_cast(c);
+ valb += 8;
+ while (valb >= 0) {
+ out.push_back(lookup[(val >> valb) & 0x3F]);
+ valb -= 6;
+ }
+ }
+
+ if (valb > -6) { out.push_back(lookup[((val << 8) >> (valb + 8)) & 0x3F]); }
+
+ while (out.size() % 4) {
+ out.push_back('=');
+ }
+
+ return out;
+}
+
+std::string sha1(const std::string &input) {
+ // RFC 3174 SHA-1 implementation
+ auto left_rotate = [](uint32_t x, uint32_t n) -> uint32_t {
+ return (x << n) | (x >> (32 - n));
+ };
+
+ uint32_t h0 = 0x67452301;
+ uint32_t h1 = 0xEFCDAB89;
+ uint32_t h2 = 0x98BADCFE;
+ uint32_t h3 = 0x10325476;
+ uint32_t h4 = 0xC3D2E1F0;
+
+ // Pre-processing: adding padding bits
+ std::string msg = input;
+ uint64_t original_bit_len = static_cast(msg.size()) * 8;
+ msg.push_back(static_cast(0x80));
+ while (msg.size() % 64 != 56) {
+ msg.push_back(0);
+ }
+
+ // Append original length in bits as 64-bit big-endian
+ for (int i = 56; i >= 0; i -= 8) {
+ msg.push_back(static_cast((original_bit_len >> i) & 0xFF));
+ }
+
+ // Process each 512-bit chunk
+ for (size_t offset = 0; offset < msg.size(); offset += 64) {
+ uint32_t w[80];
+
+ for (size_t i = 0; i < 16; i++) {
+ w[i] =
+ (static_cast(static_cast(msg[offset + i * 4]))
+ << 24) |
+ (static_cast(static_cast(msg[offset + i * 4 + 1]))
+ << 16) |
+ (static_cast(static_cast(msg[offset + i * 4 + 2]))
+ << 8) |
+ (static_cast(
+ static_cast(msg[offset + i * 4 + 3])));
+ }
+
+ for (int i = 16; i < 80; i++) {
+ w[i] = left_rotate(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1);
+ }
+
+ uint32_t a = h0, b = h1, c = h2, d = h3, e = h4;
+
+ for (int i = 0; i < 80; i++) {
+ uint32_t f, k;
+ if (i < 20) {
+ f = (b & c) | ((~b) & d);
+ k = 0x5A827999;
+ } else if (i < 40) {
+ f = b ^ c ^ d;
+ k = 0x6ED9EBA1;
+ } else if (i < 60) {
+ f = (b & c) | (b & d) | (c & d);
+ k = 0x8F1BBCDC;
+ } else {
+ f = b ^ c ^ d;
+ k = 0xCA62C1D6;
+ }
+
+ uint32_t temp = left_rotate(a, 5) + f + e + k + w[i];
+ e = d;
+ d = c;
+ c = left_rotate(b, 30);
+ b = a;
+ a = temp;
+ }
+
+ h0 += a;
+ h1 += b;
+ h2 += c;
+ h3 += d;
+ h4 += e;
+ }
+
+ // Produce the final hash as a 20-byte binary string
+ std::string hash(20, '\0');
+ for (size_t i = 0; i < 4; i++) {
+ hash[i] = static_cast((h0 >> (24 - i * 8)) & 0xFF);
+ hash[4 + i] = static_cast((h1 >> (24 - i * 8)) & 0xFF);
+ hash[8 + i] = static_cast((h2 >> (24 - i * 8)) & 0xFF);
+ hash[12 + i] = static_cast((h3 >> (24 - i * 8)) & 0xFF);
+ hash[16 + i] = static_cast((h4 >> (24 - i * 8)) & 0xFF);
+ }
+ return hash;
+}
+
+std::string websocket_accept_key(const std::string &client_key) {
+ const std::string magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+ return base64_encode(sha1(client_key + magic));
+}
+
+bool is_websocket_upgrade(const Request &req) {
+ if (req.method != "GET") { return false; }
+
+ // Check Upgrade: websocket (case-insensitive)
+ auto upgrade_it = req.headers.find("Upgrade");
+ if (upgrade_it == req.headers.end()) { return false; }
+ auto upgrade_val = case_ignore::to_lower(upgrade_it->second);
+ if (upgrade_val != "websocket") { return false; }
+
+ // Check Connection header contains "Upgrade"
+ auto connection_it = req.headers.find("Connection");
+ if (connection_it == req.headers.end()) { return false; }
+ auto connection_val = case_ignore::to_lower(connection_it->second);
+ if (connection_val.find("upgrade") == std::string::npos) { return false; }
+
+ // Check Sec-WebSocket-Key is a valid base64-encoded 16-byte value (24 chars)
+ // RFC 6455 Section 4.2.1
+ auto ws_key = req.get_header_value("Sec-WebSocket-Key");
+ if (ws_key.size() != 24 || ws_key[22] != '=' || ws_key[23] != '=') {
+ return false;
+ }
+ static const std::string b64chars =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+ for (size_t i = 0; i < 22; i++) {
+ if (b64chars.find(ws_key[i]) == std::string::npos) { return false; }
+ }
+
+ // Check Sec-WebSocket-Version: 13
+ auto version = req.get_header_value("Sec-WebSocket-Version");
+ if (version != "13") { return false; }
+
+ return true;
+}
+
+bool write_websocket_frame(Stream &strm, ws::Opcode opcode,
+ const char *data, size_t len, bool fin,
+ bool mask) {
+ // First byte: FIN + opcode
+ uint8_t header[2];
+ header[0] = static_cast((fin ? 0x80 : 0x00) |
+ (static_cast(opcode) & 0x0F));
+
+ // Second byte: MASK + payload length
+ if (len < 126) {
+ header[1] = static_cast(len);
+ if (mask) { header[1] |= 0x80; }
+ if (strm.write(reinterpret_cast(header), 2) < 0) { return false; }
+ } else if (len <= 0xFFFF) {
+ header[1] = 126;
+ if (mask) { header[1] |= 0x80; }
+ if (strm.write(reinterpret_cast(header), 2) < 0) { return false; }
+ uint8_t ext[2];
+ ext[0] = static_cast((len >> 8) & 0xFF);
+ ext[1] = static_cast(len & 0xFF);
+ if (strm.write(reinterpret_cast(ext), 2) < 0) { return false; }
+ } else {
+ header[1] = 127;
+ if (mask) { header[1] |= 0x80; }
+ if (strm.write(reinterpret_cast(header), 2) < 0) { return false; }
+ uint8_t ext[8];
+ for (int i = 7; i >= 0; i--) {
+ ext[7 - i] =
+ static_cast((static_cast(len) >> (i * 8)) & 0xFF);
+ }
+ if (strm.write(reinterpret_cast(ext), 8) < 0) { return false; }
+ }
+
+ if (mask) {
+ // Generate random mask key
+ thread_local std::mt19937 rng(std::random_device{}());
+ uint8_t mask_key[4];
+ auto r = rng();
+ std::memcpy(mask_key, &r, 4);
+ if (strm.write(reinterpret_cast(mask_key), 4) < 0) { return false; }
+
+ // Write masked payload in chunks
+ const size_t chunk_size = 4096;
+ std::vector buf((std::min)(len, chunk_size));
+ for (size_t offset = 0; offset < len; offset += chunk_size) {
+ size_t n = (std::min)(chunk_size, len - offset);
+ for (size_t i = 0; i < n; i++) {
+ buf[i] =
+ data[offset + i] ^ static_cast(mask_key[(offset + i) % 4]);
+ }
+ if (strm.write(buf.data(), n) < 0) { return false; }
+ }
+ } else {
+ if (len > 0) {
+ if (strm.write(data, len) < 0) { return false; }
+ }
+ }
+
+ return true;
+}
+
+} // namespace detail
+
+namespace ws {
+namespace impl {
+
+bool read_websocket_frame(Stream &strm, Opcode &opcode,
+ std::string &payload, bool &fin,
+ bool expect_masked, size_t max_len) {
+ // Read first 2 bytes
+ uint8_t header[2];
+ if (strm.read(reinterpret_cast(header), 2) != 2) { return false; }
+
+ fin = (header[0] & 0x80) != 0;
+
+ // RSV1, RSV2, RSV3 must be 0 when no extension is negotiated
+ if (header[0] & 0x70) { return false; }
+
+ opcode = static_cast(header[0] & 0x0F);
+ bool masked = (header[1] & 0x80) != 0;
+ uint64_t payload_len = header[1] & 0x7F;
+
+ // RFC 6455 Section 5.5: control frames MUST NOT be fragmented and
+ // MUST have a payload length of 125 bytes or less
+ bool is_control = (static_cast(opcode) & 0x08) != 0;
+ if (is_control) {
+ if (!fin) { return false; }
+ if (payload_len > 125) { return false; }
+ }
+
+ if (masked != expect_masked) { return false; }
+
+ // Extended payload length
+ if (payload_len == 126) {
+ uint8_t ext[2];
+ if (strm.read(reinterpret_cast(ext), 2) != 2) { return false; }
+ payload_len = (static_cast(ext[0]) << 8) | ext[1];
+ } else if (payload_len == 127) {
+ uint8_t ext[8];
+ if (strm.read(reinterpret_cast(ext), 8) != 8) { return false; }
+ // RFC 6455 Section 5.2: the most significant bit MUST be 0
+ if (ext[0] & 0x80) { return false; }
+ payload_len = 0;
+ for (int i = 0; i < 8; i++) {
+ payload_len = (payload_len << 8) | ext[i];
+ }
+ }
+
+ if (payload_len > max_len) { return false; }
+
+ // Read mask key if present
+ uint8_t mask_key[4] = {0};
+ if (masked) {
+ if (strm.read(reinterpret_cast(mask_key), 4) != 4) { return false; }
+ }
+
+ // Read payload
+ payload.resize(static_cast(payload_len));
+ if (payload_len > 0) {
+ size_t total_read = 0;
+ while (total_read < payload_len) {
+ auto n = strm.read(&payload[total_read],
+ static_cast(payload_len - total_read));
+ if (n <= 0) { return false; }
+ total_read += static_cast(n);
+ }
+ }
+
+ // Unmask if needed
+ if (masked) {
+ for (size_t i = 0; i < payload.size(); i++) {
+ payload[i] ^= static_cast(mask_key[i % 4]);
+ }
+ }
+
+ return true;
+}
+
+} // namespace impl
+} // namespace ws
+
+namespace detail {
+
+bool is_valid_path(const std::string &path) {
+ size_t level = 0;
+ size_t i = 0;
+
+ // Skip slash
+ while (i < path.size() && path[i] == '/') {
+ i++;
+ }
+
+ while (i < path.size()) {
+ // Read component
+ auto beg = i;
+ while (i < path.size() && path[i] != '/') {
+ if (path[i] == '\0') {
+ return false;
+ } else if (path[i] == '\\') {
+ return false;
+ }
+ i++;
+ }
+
+ auto len = i - beg;
+ assert(len > 0);
+
+ if (!path.compare(beg, len, ".")) {
+ ;
+ } else if (!path.compare(beg, len, "..")) {
+ if (level == 0) { return false; }
+ level--;
+ } else {
+ level++;
+ }
+
+ // Skip slash
+ while (i < path.size() && path[i] == '/') {
+ i++;
+ }
+ }
+
+ return true;
+}
+
+bool canonicalize_path(const char *path, std::string &resolved) {
+#if defined(_WIN32)
+ char buf[_MAX_PATH];
+ if (_fullpath(buf, path, _MAX_PATH) == nullptr) { return false; }
+ resolved = buf;
+#elif defined(PATH_MAX)
+ char buf[PATH_MAX];
+ if (realpath(path, buf) == nullptr) { return false; }
+ resolved = buf;
+#else
+ auto buf = realpath(path, nullptr);
+ auto guard = scope_exit([&]() { std::free(buf); });
+ if (buf == nullptr) { return false; }
+ resolved = buf;
+#endif
+ return true;
+}
+
+bool is_path_within_base(const std::string &resolved_path,
+ const std::string &resolved_base) {
+#if defined(_WIN32)
+ return _strnicmp(resolved_path.c_str(), resolved_base.c_str(),
+ resolved_base.size()) == 0;
+#else
+ return strncmp(resolved_path.c_str(), resolved_base.c_str(),
+ resolved_base.size()) == 0;
+#endif
+}
+
+FileStat::FileStat(const std::string &path) {
+#if defined(_WIN32)
+ auto wpath = u8string_to_wstring(path.c_str());
+ ret_ = _wstat(wpath.c_str(), &st_);
+#else
+ ret_ = stat(path.c_str(), &st_);
+#endif
+}
+bool FileStat::is_file() const {
+ return ret_ >= 0 && S_ISREG(st_.st_mode);
+}
+bool FileStat::is_dir() const {
+ return ret_ >= 0 && S_ISDIR(st_.st_mode);
+}
+
+time_t FileStat::mtime() const {
+ return ret_ >= 0 ? static_cast(st_.st_mtime)
+ : static_cast(-1);
+}
+
+size_t FileStat::size() const {
+ return ret_ >= 0 ? static_cast(st_.st_size) : 0;
+}
+
+std::string encode_path(const std::string &s) {
+ std::string result;
+ result.reserve(s.size());
+
+ for (size_t i = 0; s[i]; i++) {
+ switch (s[i]) {
+ case ' ': result += "%20"; break;
+ case '+': result += "%2B"; break;
+ case '\r': result += "%0D"; break;
+ case '\n': result += "%0A"; break;
+ case '\'': result += "%27"; break;
+ case ',': result += "%2C"; break;
+ // case ':': result += "%3A"; break; // ok? probably...
+ case ';': result += "%3B"; break;
+ default:
+ auto c = static_cast(s[i]);
+ if (c >= 0x80) {
+ result += '%';
+ char hex[4];
+ auto len = snprintf(hex, sizeof(hex) - 1, "%02X", c);
+ assert(len == 2);
+ result.append(hex, static_cast(len));
+ } else {
+ result += s[i];
+ }
+ break;
+ }
+ }
+
+ return result;
+}
+
+std::string file_extension(const std::string &path) {
+ std::smatch m;
+ thread_local auto re = std::regex("\\.([a-zA-Z0-9]+)$");
+ if (std::regex_search(path, m, re)) { return m[1].str(); }
+ return std::string();
+}
+
+bool is_space_or_tab(char c) { return c == ' ' || c == '\t'; }
+
+template
+bool parse_header(const char *beg, const char *end, T fn);
+
+template
+bool parse_header(const char *beg, const char *end, T fn) {
+ // Skip trailing spaces and tabs.
+ while (beg < end && is_space_or_tab(end[-1])) {
+ end--;
+ }
+
+ auto p = beg;
+ while (p < end && *p != ':') {
+ p++;
+ }
+
+ auto name = std::string(beg, p);
+ if (!detail::fields::is_field_name(name)) { return false; }
+
+ if (p == end) { return false; }
+
+ auto key_end = p;
+
+ if (*p++ != ':') { return false; }
+
+ while (p < end && is_space_or_tab(*p)) {
+ p++;
+ }
+
+ if (p <= end) {
+ auto key_len = key_end - beg;
+ if (!key_len) { return false; }
+
+ auto key = std::string(beg, key_end);
+ auto val = std::string(p, end);
+
+ if (!detail::fields::is_field_value(val)) { return false; }
+
+ // RFC 9110 ยง5.5: header field values are opaque octets and MUST NOT be
+ // percent-decoded by the recipient. Applications that need to interpret a
+ // value as a URI component should call httplib::decode_uri_component()
+ // (or decode_path_component()) explicitly.
+ fn(key, val);
+
+ return true;
+ }
+
+ return false;
+}
+
+bool parse_trailers(stream_line_reader &line_reader, Headers &dest,
+ const Headers &src_headers) {
+ // NOTE: In RFC 9112, '7.1 Chunked Transfer Coding' mentions "The chunked
+ // transfer coding is complete when a chunk with a chunk-size of zero is
+ // received, possibly followed by a trailer section, and finally terminated by
+ // an empty line". https://www.rfc-editor.org/rfc/rfc9112.html#section-7.1
+ //
+ // In '7.1.3. Decoding Chunked', however, the pseudo-code in the section
+ // doesn't care for the existence of the final CRLF. In other words, it seems
+ // to be ok whether the final CRLF exists or not in the chunked data.
+ // https://www.rfc-editor.org/rfc/rfc9112.html#section-7.1.3
+ //
+ // According to the reference code in RFC 9112, cpp-httplib now allows
+ // chunked transfer coding data without the final CRLF.
+
+ // RFC 7230 Section 4.1.2 - Headers prohibited in trailers
+ thread_local case_ignore::unordered_set prohibited_trailers = {
+ "transfer-encoding",
+ "content-length",
+ "host",
+ "authorization",
+ "www-authenticate",
+ "proxy-authenticate",
+ "proxy-authorization",
+ "cookie",
+ "set-cookie",
+ "cache-control",
+ "expect",
+ "max-forwards",
+ "pragma",
+ "range",
+ "te",
+ "age",
+ "expires",
+ "date",
+ "location",
+ "retry-after",
+ "vary",
+ "warning",
+ "content-encoding",
+ "content-type",
+ "content-range",
+ "trailer"};
+
+ case_ignore::unordered_set declared_trailers;
+ auto trailer_header = get_header_value(src_headers, "Trailer", "", 0);
+ if (trailer_header && std::strlen(trailer_header)) {
+ auto len = std::strlen(trailer_header);
+ split(trailer_header, trailer_header + len, ',',
+ [&](const char *b, const char *e) {
+ const char *kbeg = b;
+ const char *kend = e;
+ while (kbeg < kend && (*kbeg == ' ' || *kbeg == '\t')) {
+ ++kbeg;
+ }
+ while (kend > kbeg && (kend[-1] == ' ' || kend[-1] == '\t')) {
+ --kend;
+ }
+ std::string key(kbeg, static_cast(kend - kbeg));
+ if (!key.empty() &&
+ prohibited_trailers.find(key) == prohibited_trailers.end()) {
+ declared_trailers.insert(key);
+ }
+ });
+ }
+
+ size_t trailer_header_count = 0;
+ while (strcmp(line_reader.ptr(), "\r\n") != 0) {
+ if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; }
+ if (trailer_header_count >= CPPHTTPLIB_HEADER_MAX_COUNT) { return false; }
+
+ constexpr auto line_terminator_len = 2;
+ auto line_beg = line_reader.ptr();
+ auto line_end =
+ line_reader.ptr() + line_reader.size() - line_terminator_len;
+
+ if (!parse_header(line_beg, line_end,
+ [&](const std::string &key, const std::string &val) {
+ if (declared_trailers.find(key) !=
+ declared_trailers.end()) {
+ dest.emplace(key, val);
+ trailer_header_count++;
+ }
+ })) {
+ return false;
+ }
+
+ if (!line_reader.getline()) { return false; }
+ }
+
+ return true;
+}
+
+std::pair trim(const char *b, const char *e, size_t left,
+ size_t right) {
+ while (b + left < e && is_space_or_tab(b[left])) {
+ left++;
+ }
+ while (right > 0 && is_space_or_tab(b[right - 1])) {
+ right--;
+ }
+ return std::make_pair(left, right);
+}
+
+std::string trim_copy(const std::string &s) {
+ auto r = trim(s.data(), s.data() + s.size(), 0, s.size());
+ return s.substr(r.first, r.second - r.first);
+}
+
+std::string trim_double_quotes_copy(const std::string &s) {
+ if (s.length() >= 2 && s.front() == '"' && s.back() == '"') {
+ return s.substr(1, s.size() - 2);
+ }
+ return s;
+}
+
+void
+divide(const char *data, std::size_t size, char d,
+ std::function
+ fn) {
+ const auto it = std::find(data, data + size, d);
+ const auto found = static_cast(it != data + size);
+ const auto lhs_data = data;
+ const auto lhs_size = static_cast(it - data);
+ const auto rhs_data = it + found;
+ const auto rhs_size = size - lhs_size - found;
+
+ fn(lhs_data, lhs_size, rhs_data, rhs_size);
+}
+
+void
+divide(const std::string &str, char d,
+ std::function
+ fn) {
+ divide(str.data(), str.size(), d, std::move(fn));
+}
+
+void split(const char *b, const char *e, char d,
+ std::function fn) {
+ return split(b, e, d, (std::numeric_limits::max)(), std::move(fn));
+}
+
+void split(const char *b, const char *e, char d, size_t m,
+ std::function fn) {
+ size_t i = 0;
+ size_t beg = 0;
+ size_t count = 1;
+
+ while (e ? (b + i < e) : (b[i] != '\0')) {
+ if (b[i] == d && count < m) {
+ auto r = trim(b, e, beg, i);
+ if (r.first < r.second) { fn(&b[r.first], &b[r.second]); }
+ beg = i + 1;
+ count++;
+ }
+ i++;
+ }
+
+ if (i) {
+ auto r = trim(b, e, beg, i);
+ if (r.first < r.second) { fn(&b[r.first], &b[r.second]); }
+ }
+}
+
+bool split_find(const char *b, const char *e, char d, size_t m,
+ std::function fn) {
+ size_t i = 0;
+ size_t beg = 0;
+ size_t count = 1;
+
+ while (e ? (b + i < e) : (b[i] != '\0')) {
+ if (b[i] == d && count < m) {
+ auto r = trim(b, e, beg, i);
+ if (r.first < r.second) {
+ auto found = fn(&b[r.first], &b[r.second]);
+ if (found) { return true; }
+ }
+ beg = i + 1;
+ count++;
+ }
+ i++;
+ }
+
+ if (i) {
+ auto r = trim(b, e, beg, i);
+ if (r.first < r.second) {
+ auto found = fn(&b[r.first], &b[r.second]);
+ if (found) { return true; }
+ }
+ }
+
+ return false;
+}
+
+bool split_find(const char *b, const char *e, char d,
+ std::function fn) {
+ return split_find(b, e, d, (std::numeric_limits::max)(),
+ std::move(fn));
+}
+
+stream_line_reader::stream_line_reader(Stream &strm, char *fixed_buffer,
+ size_t fixed_buffer_size)
+ : strm_(strm), fixed_buffer_(fixed_buffer),
+ fixed_buffer_size_(fixed_buffer_size) {}
+
+const char *stream_line_reader::ptr() const {
+ if (growable_buffer_.empty()) {
+ return fixed_buffer_;
+ } else {
+ return growable_buffer_.data();
+ }
+}
+
+size_t stream_line_reader::size() const {
+ if (growable_buffer_.empty()) {
+ return fixed_buffer_used_size_;
+ } else {
+ return growable_buffer_.size();
+ }
+}
+
+bool stream_line_reader::end_with_crlf() const {
+ auto end = ptr() + size();
+ return size() >= 2 && end[-2] == '\r' && end[-1] == '\n';
+}
+
+bool stream_line_reader::getline() {
+ fixed_buffer_used_size_ = 0;
+ growable_buffer_.clear();
+
+#ifndef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR
+ char prev_byte = 0;
+#endif
+
+ for (size_t i = 0;; i++) {
+ if (size() >= CPPHTTPLIB_MAX_LINE_LENGTH) {
+ // Treat exceptionally long lines as an error to
+ // prevent infinite loops/memory exhaustion
+ return false;
+ }
+ char byte;
+ auto n = strm_.read(&byte, 1);
+
+ if (n < 0) {
+ return false;
+ } else if (n == 0) {
+ if (i == 0) {
+ return false;
+ } else {
+ break;
+ }
+ }
+
+ append(byte);
+
+#ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR
+ if (byte == '\n') { break; }
+#else
+ if (prev_byte == '\r' && byte == '\n') { break; }
+ prev_byte = byte;
+#endif
+ }
+
+ return true;
+}
+
+void stream_line_reader::append(char c) {
+ if (fixed_buffer_used_size_ < fixed_buffer_size_ - 1) {
+ fixed_buffer_[fixed_buffer_used_size_++] = c;
+ fixed_buffer_[fixed_buffer_used_size_] = '\0';
+ } else {
+ if (growable_buffer_.empty()) {
+ assert(fixed_buffer_[fixed_buffer_used_size_] == '\0');
+ growable_buffer_.assign(fixed_buffer_, fixed_buffer_used_size_);
+ }
+ growable_buffer_ += c;
+ }
+}
+
+mmap::mmap(const char *path) { open(path); }
+
+mmap::~mmap() { close(); }
+
+bool mmap::open(const char *path) {
+ close();
+
+#if defined(_WIN32)
+ auto wpath = u8string_to_wstring(path);
+ if (wpath.empty()) { return false; }
+
+ hFile_ =
+ ::CreateFile2(wpath.c_str(), GENERIC_READ,
+ FILE_SHARE_READ | FILE_SHARE_WRITE, OPEN_EXISTING, NULL);
+
+ if (hFile_ == INVALID_HANDLE_VALUE) { return false; }
+
+ LARGE_INTEGER size{};
+ if (!::GetFileSizeEx(hFile_, &size)) { return false; }
+ // If the following line doesn't compile due to QuadPart, update Windows SDK.
+ // See:
+ // https://github.com/yhirose/cpp-httplib/issues/1903#issuecomment-2316520721
+ if (static_cast(size.QuadPart) >
+ (std::numeric_limits::max)()) {
+ // `size_t` might be 32-bits, on 32-bits Windows.
+ return false;
+ }
+ size_ = static_cast(size.QuadPart);
+
+ hMapping_ =
+ ::CreateFileMappingFromApp(hFile_, NULL, PAGE_READONLY, size_, NULL);
+
+ // Special treatment for an empty file...
+ if (hMapping_ == NULL && size_ == 0) {
+ close();
+ is_open_empty_file = true;
+ return true;
+ }
+
+ if (hMapping_ == NULL) {
+ close();
+ return false;
+ }
+
+ addr_ = ::MapViewOfFileFromApp(hMapping_, FILE_MAP_READ, 0, 0);
+
+ if (addr_ == nullptr) {
+ close();
+ return false;
+ }
+#else
+ fd_ = ::open(path, O_RDONLY);
+ if (fd_ == -1) { return false; }
+
+ struct stat sb;
+ if (fstat(fd_, &sb) == -1) {
+ close();
+ return false;
+ }
+ size_ = static_cast(sb.st_size);
+
+ addr_ = ::mmap(NULL, size_, PROT_READ, MAP_PRIVATE, fd_, 0);
+
+ // Special treatment for an empty file...
+ if (addr_ == MAP_FAILED && size_ == 0) {
+ close();
+ is_open_empty_file = true;
+ return false;
+ }
+#endif
+
+ return true;
+}
+
+bool mmap::is_open() const {
+ return is_open_empty_file ? true : addr_ != nullptr;
+}
+
+size_t mmap::size() const { return size_; }
+
+const char *mmap::data() const {
+ return is_open_empty_file ? "" : static_cast(addr_);
+}
+
+void mmap::close() {
+#if defined(_WIN32)
+ if (addr_) {
+ ::UnmapViewOfFile(addr_);
+ addr_ = nullptr;
+ }
+
+ if (hMapping_) {
+ ::CloseHandle(hMapping_);
+ hMapping_ = NULL;
+ }
+
+ if (hFile_ != INVALID_HANDLE_VALUE) {
+ ::CloseHandle(hFile_);
+ hFile_ = INVALID_HANDLE_VALUE;
+ }
+
+ is_open_empty_file = false;
+#else
+ if (addr_ != nullptr) {
+ munmap(addr_, size_);
+ addr_ = nullptr;
+ }
+
+ if (fd_ != -1) {
+ ::close(fd_);
+ fd_ = -1;
+ }
+#endif
+ size_ = 0;
+}
+int close_socket(socket_t sock) noexcept {
+#ifdef _WIN32
+ return closesocket(sock);
+#else
+ return close(sock);
+#endif
+}
+
+template ssize_t handle_EINTR(T fn) {
+ ssize_t res = 0;
+ while (true) {
+ res = fn();
+ if (res < 0 && errno == EINTR) {
+ std::this_thread::sleep_for(std::chrono::microseconds{1});
+ continue;
+ }
+ break;
+ }
+ return res;
+}
+
+ssize_t read_socket(socket_t sock, void *ptr, size_t size, int flags) {
+ return handle_EINTR([&]() {
+ return recv(sock,
+#ifdef _WIN32
+ static_cast(ptr), static_cast(size),
+#else
+ ptr, size,
+#endif
+ flags);
+ });
+}
+
+ssize_t send_socket(socket_t sock, const void *ptr, size_t size,
+ int flags) {
+ return handle_EINTR([&]() {
+ return send(sock,
+#ifdef _WIN32
+ static_cast(ptr), static_cast(size),
+#else
+ ptr, size,
+#endif
+ flags);
+ });
+}
+
+int poll_wrapper(struct pollfd *fds, nfds_t nfds, int timeout) {
+#ifdef _WIN32
+ return ::WSAPoll(fds, nfds, timeout);
+#else
+ return ::poll(fds, nfds, timeout);
+#endif
+}
+
+ssize_t select_impl(socket_t sock, short events, time_t sec,
+ time_t usec) {
+ struct pollfd pfd;
+ pfd.fd = sock;
+ pfd.events = events;
+ pfd.revents = 0;
+
+ auto timeout = static_cast(sec * 1000 + usec / 1000);
+
+ return handle_EINTR([&]() { return poll_wrapper(&pfd, 1, timeout); });
+}
+
+ssize_t select_read(socket_t sock, time_t sec, time_t usec) {
+ return select_impl(sock, POLLIN, sec, usec);
+}
+
+ssize_t select_write(socket_t sock, time_t sec, time_t usec) {
+ return select_impl(sock, POLLOUT, sec, usec);
+}
+
+Error wait_until_socket_is_ready(socket_t sock, time_t sec,
+ time_t usec) {
+ struct pollfd pfd_read;
+ pfd_read.fd = sock;
+ pfd_read.events = POLLIN | POLLOUT;
+ pfd_read.revents = 0;
+
+ auto timeout = static_cast(sec * 1000 + usec / 1000);
+
+ auto poll_res =
+ handle_EINTR([&]() { return poll_wrapper(&pfd_read, 1, timeout); });
+
+ if (poll_res == 0) { return Error::ConnectionTimeout; }
+
+ if (poll_res > 0 && pfd_read.revents & (POLLIN | POLLOUT)) {
+ auto error = 0;
+ socklen_t len = sizeof(error);
+ auto res = getsockopt(sock, SOL_SOCKET, SO_ERROR,
+ reinterpret_cast(&error), &len);
+ auto successful = res >= 0 && !error;
+ return successful ? Error::Success : Error::Connection;
+ }
+
+ return Error::Connection;
+}
+
+bool is_socket_alive(socket_t sock) {
+ const auto val = detail::select_read(sock, 0, 0);
+ if (val == 0) {
+ return true;
+ } else if (val < 0 && errno == EBADF) {
+ return false;
+ }
+ char buf[1];
+ return detail::read_socket(sock, &buf[0], sizeof(buf), MSG_PEEK) > 0;
+}
+
+class SocketStream final : public Stream {
+public:
+ SocketStream(socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec,
+ time_t write_timeout_sec, time_t write_timeout_usec,
+ time_t max_timeout_msec = 0,
+ std::chrono::time_point start_time =
+ (std::chrono::steady_clock::time_point::min)());
+ ~SocketStream() override;
+
+ bool is_readable() const override;
+ bool wait_readable() const override;
+ bool wait_writable() const override;
+ bool is_peer_alive() const override;
+ ssize_t read(char *ptr, size_t size) override;
+ ssize_t write(const char *ptr, size_t size) override;
+ void get_remote_ip_and_port(std::string &ip, int &port) const override;
+ void get_local_ip_and_port(std::string &ip, int &port) const override;
+ socket_t socket() const override;
+ time_t duration() const override;
+ void set_read_timeout(time_t sec, time_t usec = 0) override;
+
+private:
+ socket_t sock_;
+ time_t read_timeout_sec_;
+ time_t read_timeout_usec_;
+ time_t write_timeout_sec_;
+ time_t write_timeout_usec_;
+ time_t max_timeout_msec_;
+ const std::chrono::time_point start_time_;
+
+ std::vector read_buff_;
+ size_t read_buff_off_ = 0;
+ size_t read_buff_content_size_ = 0;
+
+ static const size_t read_buff_size_ = 1024l * 4;
+};
+
+bool keep_alive(const std::atomic &svr_sock, socket_t sock,
+ time_t keep_alive_timeout_sec) {
+ using namespace std::chrono;
+
+ const auto interval_usec =
+ CPPHTTPLIB_KEEPALIVE_TIMEOUT_CHECK_INTERVAL_USECOND;
+
+ // Avoid expensive `steady_clock::now()` call for the first time
+ if (select_read(sock, 0, interval_usec) > 0) { return true; }
+
+ const auto start = steady_clock::now() - microseconds{interval_usec};
+ const auto timeout = seconds{keep_alive_timeout_sec};
+
+ while (true) {
+ if (svr_sock == INVALID_SOCKET) {
+ break; // Server socket is closed
+ }
+
+ auto val = select_read(sock, 0, interval_usec);
+ if (val < 0) {
+ break; // Ssocket error
+ } else if (val == 0) {
+ if (steady_clock::now() - start > timeout) {
+ break; // Timeout
+ }
+ } else {
+ return true; // Ready for read
+ }
+ }
+
+ return false;
+}
+
+template
+bool
+process_server_socket_core(const std::atomic &svr_sock, socket_t sock,
+ size_t keep_alive_max_count,
+ time_t keep_alive_timeout_sec, T callback) {
+ assert(keep_alive_max_count > 0);
+ auto ret = false;
+ auto count = keep_alive_max_count;
+ while (count > 0 && keep_alive(svr_sock, sock, keep_alive_timeout_sec)) {
+ auto close_connection = count == 1;
+ auto connection_closed = false;
+ ret = callback(close_connection, connection_closed);
+ if (!ret || connection_closed) { break; }
+ count--;
+ }
+ return ret;
+}
+
+template
+bool
+process_server_socket(const std::atomic &svr_sock, socket_t sock,
+ size_t keep_alive_max_count,
+ time_t keep_alive_timeout_sec, time_t read_timeout_sec,
+ time_t read_timeout_usec, time_t write_timeout_sec,
+ time_t write_timeout_usec, T callback) {
+ return process_server_socket_core(
+ svr_sock, sock, keep_alive_max_count, keep_alive_timeout_sec,
+ [&](bool close_connection, bool &connection_closed) {
+ SocketStream strm(sock, read_timeout_sec, read_timeout_usec,
+ write_timeout_sec, write_timeout_usec);
+ return callback(strm, close_connection, connection_closed);
+ });
+}
+
+bool process_client_socket(
+ socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec,
+ time_t write_timeout_sec, time_t write_timeout_usec,
+ time_t max_timeout_msec,
+ std::chrono::time_point start_time,
+ std::function callback) {
+ SocketStream strm(sock, read_timeout_sec, read_timeout_usec,
+ write_timeout_sec, write_timeout_usec, max_timeout_msec,
+ start_time);
+ return callback(strm);
+}
+
+int shutdown_socket(socket_t sock) noexcept {
+#ifdef _WIN32
+ return shutdown(sock, SD_BOTH);
+#else
+ return shutdown(sock, SHUT_RDWR);
+#endif
+}
+
+std::string escape_abstract_namespace_unix_domain(const std::string &s) {
+ if (s.size() > 1 && s[0] == '\0') {
+ auto ret = s;
+ ret[0] = '@';
+ return ret;
+ }
+ return s;
+}
+
+std::string
+unescape_abstract_namespace_unix_domain(const std::string &s) {
+ if (s.size() > 1 && s[0] == '@') {
+ auto ret = s;
+ ret[0] = '\0';
+ return ret;
+ }
+ return s;
+}
+
+int getaddrinfo_with_timeout(const char *node, const char *service,
+ const struct addrinfo *hints,
+ struct addrinfo **res, time_t timeout_sec) {
+#ifdef CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO
+ if (timeout_sec <= 0) {
+ // No timeout specified, use standard getaddrinfo
+ return getaddrinfo(node, service, hints, res);
+ }
+
+#ifdef _WIN32
+ // Windows-specific implementation using GetAddrInfoEx with overlapped I/O
+ OVERLAPPED overlapped = {};
+ HANDLE event = CreateEventW(nullptr, TRUE, FALSE, nullptr);
+ if (!event) { return EAI_FAIL; }
+
+ overlapped.hEvent = event;
+
+ PADDRINFOEXW result_addrinfo = nullptr;
+ HANDLE cancel_handle = nullptr;
+
+ ADDRINFOEXW hints_ex = {};
+ if (hints) {
+ hints_ex.ai_flags = hints->ai_flags;
+ hints_ex.ai_family = hints->ai_family;
+ hints_ex.ai_socktype = hints->ai_socktype;
+ hints_ex.ai_protocol = hints->ai_protocol;
+ }
+
+ auto wnode = u8string_to_wstring(node);
+ auto wservice = u8string_to_wstring(service);
+
+ auto ret = ::GetAddrInfoExW(wnode.data(), wservice.data(), NS_DNS, nullptr,
+ hints ? &hints_ex : nullptr, &result_addrinfo,
+ nullptr, &overlapped, nullptr, &cancel_handle);
+
+ if (ret == WSA_IO_PENDING) {
+ auto wait_result =
+ ::WaitForSingleObject(event, static_cast(timeout_sec * 1000));
+ if (wait_result == WAIT_TIMEOUT) {
+ if (cancel_handle) { ::GetAddrInfoExCancel(&cancel_handle); }
+ ::CloseHandle(event);
+ return EAI_AGAIN;
+ }
+
+ DWORD bytes_returned;
+ if (!::GetOverlappedResult((HANDLE)INVALID_SOCKET, &overlapped,
+ &bytes_returned, FALSE)) {
+ ::CloseHandle(event);
+ return ::WSAGetLastError();
+ }
+ }
+
+ ::CloseHandle(event);
+
+ if (ret == NO_ERROR || ret == WSA_IO_PENDING) {
+ *res = reinterpret_cast(result_addrinfo);
+ return 0;
+ }
+
+ return ret;
+#elif TARGET_OS_MAC && defined(__clang__)
+ if (!node) { return EAI_NONAME; }
+ // macOS implementation using CFHost API for asynchronous DNS resolution
+ CFStringRef hostname_ref = CFStringCreateWithCString(
+ kCFAllocatorDefault, node, kCFStringEncodingUTF8);
+ if (!hostname_ref) { return EAI_MEMORY; }
+
+ CFHostRef host_ref = CFHostCreateWithName(kCFAllocatorDefault, hostname_ref);
+ CFRelease(hostname_ref);
+ if (!host_ref) { return EAI_MEMORY; }
+
+ // Set up context for callback
+ struct CFHostContext {
+ bool completed = false;
+ bool success = false;
+ CFArrayRef addresses = nullptr;
+ std::mutex mutex;
+ std::condition_variable cv;
+ } context;
+
+ CFHostClientContext client_context;
+ memset(&client_context, 0, sizeof(client_context));
+ client_context.info = &context;
+
+ // Set callback
+ auto callback = [](CFHostRef theHost, CFHostInfoType /*typeInfo*/,
+ const CFStreamError *error, void *info) {
+ auto ctx = static_cast(info);
+ std::lock_guard lock(ctx->mutex);
+
+ if (error && error->error != 0) {
+ ctx->success = false;
+ } else {
+ Boolean hasBeenResolved;
+ ctx->addresses = CFHostGetAddressing(theHost, &hasBeenResolved);
+ if (ctx->addresses && hasBeenResolved) {
+ CFRetain(ctx->addresses);
+ ctx->success = true;
+ } else {
+ ctx->success = false;
+ }
+ }
+ ctx->completed = true;
+ ctx->cv.notify_one();
+ };
+
+ if (!CFHostSetClient(host_ref, callback, &client_context)) {
+ CFRelease(host_ref);
+ return EAI_SYSTEM;
+ }
+
+ // Schedule on run loop
+ CFRunLoopRef run_loop = CFRunLoopGetCurrent();
+ CFHostScheduleWithRunLoop(host_ref, run_loop, kCFRunLoopDefaultMode);
+
+ // Start resolution
+ CFStreamError stream_error;
+ if (!CFHostStartInfoResolution(host_ref, kCFHostAddresses, &stream_error)) {
+ CFHostUnscheduleFromRunLoop(host_ref, run_loop, kCFRunLoopDefaultMode);
+ CFRelease(host_ref);
+ return EAI_FAIL;
+ }
+
+ // Wait for completion with timeout
+ auto timeout_time =
+ std::chrono::steady_clock::now() + std::chrono::seconds(timeout_sec);
+ bool timed_out = false;
+
+ {
+ std::unique_lock lock(context.mutex);
+
+ while (!context.completed) {
+ auto now = std::chrono::steady_clock::now();
+ if (now >= timeout_time) {
+ timed_out = true;
+ break;
+ }
+
+ // Run the runloop for a short time
+ lock.unlock();
+ CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, true);
+ lock.lock();
+ }
+ }
+
+ // Clean up
+ CFHostUnscheduleFromRunLoop(host_ref, run_loop, kCFRunLoopDefaultMode);
+ CFHostSetClient(host_ref, nullptr, nullptr);
+
+ if (timed_out || !context.completed) {
+ CFHostCancelInfoResolution(host_ref, kCFHostAddresses);
+ CFRelease(host_ref);
+ return EAI_AGAIN;
+ }
+
+ if (!context.success || !context.addresses) {
+ CFRelease(host_ref);
+ return EAI_NODATA;
+ }
+
+ // Convert CFArray to addrinfo
+ CFIndex count = CFArrayGetCount(context.addresses);
+ if (count == 0) {
+ CFRelease(context.addresses);
+ CFRelease(host_ref);
+ return EAI_NODATA;
+ }
+
+ struct addrinfo *result_addrinfo = nullptr;
+ struct addrinfo **current = &result_addrinfo;
+
+ for (CFIndex i = 0; i < count; i++) {
+ CFDataRef addr_data =
+ static_cast(CFArrayGetValueAtIndex(context.addresses, i));
+ if (!addr_data) continue;
+
+ const struct sockaddr *sockaddr_ptr =
+ reinterpret_cast(CFDataGetBytePtr(addr_data));
+ socklen_t sockaddr_len = static_cast(CFDataGetLength(addr_data));
+
+ // Allocate addrinfo structure
+ *current = static_cast(malloc(sizeof(struct addrinfo)));
+ if (!*current) {
+ freeaddrinfo(result_addrinfo);
+ CFRelease(context.addresses);
+ CFRelease(host_ref);
+ return EAI_MEMORY;
+ }
+
+ memset(*current, 0, sizeof(struct addrinfo));
+
+ // Set up addrinfo fields
+ (*current)->ai_family = sockaddr_ptr->sa_family;
+ (*current)->ai_socktype = hints ? hints->ai_socktype : SOCK_STREAM;
+ (*current)->ai_protocol = hints ? hints->ai_protocol : IPPROTO_TCP;
+ (*current)->ai_addrlen = sockaddr_len;
+
+ // Copy sockaddr
+ (*current)->ai_addr = static_cast(malloc(sockaddr_len));
+ if (!(*current)->ai_addr) {
+ freeaddrinfo(result_addrinfo);
+ CFRelease(context.addresses);
+ CFRelease(host_ref);
+ return EAI_MEMORY;
+ }
+ memcpy((*current)->ai_addr, sockaddr_ptr, sockaddr_len);
+
+ // Set port if service is specified
+ if (service && *service) {
+ int port = 0;
+ if (parse_port(service, strlen(service), port)) {
+ if (sockaddr_ptr->sa_family == AF_INET) {
+ reinterpret_cast((*current)->ai_addr)
+ ->sin_port = htons(static_cast(port));
+ } else if (sockaddr_ptr->sa_family == AF_INET6) {
+ reinterpret_cast((*current)->ai_addr)
+ ->sin6_port = htons(static_cast(port));
+ }
+ }
+ }
+
+ current = &((*current)->ai_next);
+ }
+
+ CFRelease(context.addresses);
+ CFRelease(host_ref);
+
+ *res = result_addrinfo;
+ return 0;
+#elif defined(_GNU_SOURCE) && defined(__GLIBC__) && \
+ (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 2))
+ // #2431: gai_cancel() is non-blocking and may return EAI_NOTCANCELED while
+ // the resolver worker still references the stack-local gaicb. The cancel
+ // path therefore waits (gai_suspend with no timeout) for the worker to
+ // actually finish before letting the stack frame go. The trade-off is that
+ // a wedged DNS server can hold this thread for the system resolver timeout
+ // (~30s by default) past the caller's connection timeout.
+ struct gaicb request {};
+ struct gaicb *requests[1] = {&request};
+ struct sigevent sevp {};
+ struct timespec timeout {
+ timeout_sec, 0
+ };
+
+ request.ar_name = node;
+ request.ar_service = service;
+ request.ar_request = hints;
+ sevp.sigev_notify = SIGEV_NONE;
+
+ int rc = getaddrinfo_a(GAI_NOWAIT, requests, 1, &sevp);
+ if (rc != 0) { return rc; }
+
+ auto cleanup = scope_exit([&] {
+ if (request.ar_result) { freeaddrinfo(request.ar_result); }
+ });
+
+ int wait_result = gai_suspend(requests, 1, &timeout);
+
+ if (wait_result == 0 || wait_result == EAI_ALLDONE) {
+ int gai_result = gai_error(&request);
+ if (gai_result == 0) {
+ *res = request.ar_result;
+ request.ar_result = nullptr;
+ return 0;
+ }
+ return gai_result;
+ }
+
+ gai_cancel(&request);
+ while (gai_error(&request) == EAI_INPROGRESS) {
+ gai_suspend(requests, 1, nullptr);
+ }
+ return wait_result;
+#else
+ // Fallback implementation using thread-based timeout for other Unix systems.
+
+ struct GetAddrInfoState {
+ ~GetAddrInfoState() {
+ if (info) { freeaddrinfo(info); }
+ }
+
+ std::mutex mutex;
+ std::condition_variable result_cv;
+ bool completed = false;
+ int result = EAI_SYSTEM;
+ std::string node;
+ std::string service;
+ struct addrinfo hints;
+ struct addrinfo *info = nullptr;
+ };
+
+ // Allocate on the heap, so the resolver thread can keep using the data.
+ auto state = std::make_shared();
+ if (node) { state->node = node; }
+ state->service = service;
+ state->hints = *hints;
+
+ std::thread resolve_thread([state]() {
+ auto thread_result =
+ getaddrinfo(state->node.c_str(), state->service.c_str(), &state->hints,
+ &state->info);
+
+ std::lock_guard lock(state->mutex);
+ state->result = thread_result;
+ state->completed = true;
+ state->result_cv.notify_one();
+ });
+
+ // Wait for completion or timeout
+ std::unique_lock lock(state->mutex);
+ auto finished =
+ state->result_cv.wait_for(lock, std::chrono::seconds(timeout_sec),
+ [&] { return state->completed; });
+
+ if (finished) {
+ // Operation completed within timeout
+ resolve_thread.join();
+ *res = state->info;
+ state->info = nullptr; // Pass ownership to caller
+ return state->result;
+ } else {
+ // Timeout occurred
+ resolve_thread.detach(); // Let the thread finish in background
+ return EAI_AGAIN; // Return timeout error
+ }
+#endif
+#else
+ (void)(timeout_sec); // Unused parameter for non-blocking getaddrinfo
+ return getaddrinfo(node, service, hints, res);
+#endif
+}
+
+template
+socket_t create_socket(const std::string &host, const std::string &ip, int port,
+ int address_family, int socket_flags, bool tcp_nodelay,
+ bool ipv6_v6only, SocketOptions socket_options,
+ BindOrConnect bind_or_connect, time_t timeout_sec = 0) {
+ // Get address info
+ const char *node = nullptr;
+ struct addrinfo hints;
+ struct addrinfo *result;
+
+ memset(&hints, 0, sizeof(struct addrinfo));
+ hints.ai_socktype = SOCK_STREAM;
+ hints.ai_protocol = IPPROTO_IP;
+
+ if (!ip.empty()) {
+ node = ip.c_str();
+ // Ask getaddrinfo to convert IP in c-string to address
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_flags = AI_NUMERICHOST;
+ } else {
+ if (!host.empty()) { node = host.c_str(); }
+ hints.ai_family = address_family;
+ hints.ai_flags = socket_flags;
+ }
+
+#if !defined(_WIN32) || defined(CPPHTTPLIB_HAVE_AFUNIX_H)
+ if (hints.ai_family == AF_UNIX) {
+ const auto addrlen = host.length();
+ if (addrlen > sizeof(sockaddr_un::sun_path)) { return INVALID_SOCKET; }
+
+#ifdef SOCK_CLOEXEC
+ auto sock = socket(hints.ai_family, hints.ai_socktype | SOCK_CLOEXEC,
+ hints.ai_protocol);
+#else
+ auto sock = socket(hints.ai_family, hints.ai_socktype, hints.ai_protocol);
+#endif
+
+ if (sock != INVALID_SOCKET) {
+ sockaddr_un addr{};
+ addr.sun_family = AF_UNIX;
+
+ auto unescaped_host = unescape_abstract_namespace_unix_domain(host);
+ std::copy(unescaped_host.begin(), unescaped_host.end(), addr.sun_path);
+
+ hints.ai_addr = reinterpret_cast(&addr);
+ hints.ai_addrlen = static_cast(
+ sizeof(addr) - sizeof(addr.sun_path) + addrlen);
+
+#ifndef SOCK_CLOEXEC
+#ifndef _WIN32
+ fcntl(sock, F_SETFD, FD_CLOEXEC);
+#endif
+#endif
+
+ if (socket_options) { socket_options(sock); }
+
+#ifdef _WIN32
+ // Setting SO_REUSEADDR seems not to work well with AF_UNIX on windows, so
+ // remove the option.
+ set_socket_opt(sock, SOL_SOCKET, SO_REUSEADDR, 0);
+#endif
+
+ bool dummy;
+ if (!bind_or_connect(sock, hints, dummy)) {
+ close_socket(sock);
+ sock = INVALID_SOCKET;
+ }
+ }
+ return sock;
+ }
+#endif
+
+ auto service = std::to_string(port);
+
+ if (getaddrinfo_with_timeout(node, service.c_str(), &hints, &result,
+ timeout_sec)) {
+#if defined __linux__ && !defined __ANDROID__
+ res_init();
+#endif
+ return INVALID_SOCKET;
+ }
+ auto se = detail::scope_exit([&] { freeaddrinfo(result); });
+
+ for (auto rp = result; rp; rp = rp->ai_next) {
+ // Create a socket
+#ifdef _WIN32
+ auto sock =
+ WSASocketW(rp->ai_family, rp->ai_socktype, rp->ai_protocol, nullptr, 0,
+ WSA_FLAG_NO_HANDLE_INHERIT | WSA_FLAG_OVERLAPPED);
+ /**
+ * Since the WSA_FLAG_NO_HANDLE_INHERIT is only supported on Windows 7 SP1
+ * and above the socket creation fails on older Windows Systems.
+ *
+ * Let's try to create a socket the old way in this case.
+ *
+ * Reference:
+ * https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsasocketa
+ *
+ * WSA_FLAG_NO_HANDLE_INHERIT:
+ * This flag is supported on Windows 7 with SP1, Windows Server 2008 R2 with
+ * SP1, and later
+ *
+ */
+ if (sock == INVALID_SOCKET) {
+ sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
+ }
+#else
+
+#ifdef SOCK_CLOEXEC
+ auto sock =
+ socket(rp->ai_family, rp->ai_socktype | SOCK_CLOEXEC, rp->ai_protocol);
+#else
+ auto sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
+#endif
+
+#endif
+ if (sock == INVALID_SOCKET) { continue; }
+
+#if !defined _WIN32 && !defined SOCK_CLOEXEC
+ if (fcntl(sock, F_SETFD, FD_CLOEXEC) == -1) {
+ close_socket(sock);
+ continue;
+ }
+#endif
+
+ if (tcp_nodelay) { set_socket_opt(sock, IPPROTO_TCP, TCP_NODELAY, 1); }
+
+ if (rp->ai_family == AF_INET6) {
+ set_socket_opt(sock, IPPROTO_IPV6, IPV6_V6ONLY, ipv6_v6only ? 1 : 0);
+ }
+
+ if (socket_options) { socket_options(sock); }
+
+ // bind or connect
+ auto quit = false;
+ if (bind_or_connect(sock, *rp, quit)) { return sock; }
+
+ close_socket(sock);
+
+ if (quit) { break; }
+ }
+
+ return INVALID_SOCKET;
+}
+
+void set_nonblocking(socket_t sock, bool nonblocking) {
+#ifdef _WIN32
+ auto flags = nonblocking ? 1UL : 0UL;
+ ioctlsocket(sock, FIONBIO, &flags);
+#else
+ auto flags = fcntl(sock, F_GETFL, 0);
+ fcntl(sock, F_SETFL,
+ nonblocking ? (flags | O_NONBLOCK) : (flags & (~O_NONBLOCK)));
+#endif
+}
+
+bool is_connection_error() {
+#ifdef _WIN32
+ return WSAGetLastError() != WSAEWOULDBLOCK;
+#else
+ return errno != EINPROGRESS;
+#endif
+}
+
+bool bind_ip_address(socket_t sock, const std::string &host) {
+ struct addrinfo hints;
+ struct addrinfo *result;
+
+ memset(&hints, 0, sizeof(struct addrinfo));
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ hints.ai_protocol = 0;
+
+ if (getaddrinfo_with_timeout(host.c_str(), "0", &hints, &result, 0)) {
+ return false;
+ }
+
+ auto se = detail::scope_exit([&] { freeaddrinfo(result); });
+
+ auto ret = false;
+ for (auto rp = result; rp; rp = rp->ai_next) {
+ const auto &ai = *rp;
+ if (!::bind(sock, ai.ai_addr, static_cast(ai.ai_addrlen))) {
+ ret = true;
+ break;
+ }
+ }
+
+ return ret;
+}
+
+#if !defined _WIN32 && !defined ANDROID && !defined _AIX && !defined __MVS__
+#define USE_IF2IP
+#endif
+
+#ifdef USE_IF2IP
+std::string if2ip(int address_family, const std::string &ifn) {
+ struct ifaddrs *ifap;
+ getifaddrs(&ifap);
+ auto se = detail::scope_exit([&] { freeifaddrs(ifap); });
+
+ std::string addr_candidate;
+ for (auto ifa = ifap; ifa; ifa = ifa->ifa_next) {
+ if (ifa->ifa_addr && ifn == ifa->ifa_name &&
+ (AF_UNSPEC == address_family ||
+ ifa->ifa_addr->sa_family == address_family)) {
+ if (ifa->ifa_addr->sa_family == AF_INET) {
+ auto sa = reinterpret_cast(ifa->ifa_addr);
+ char buf[INET_ADDRSTRLEN];
+ if (inet_ntop(AF_INET, &sa->sin_addr, buf, INET_ADDRSTRLEN)) {
+ return std::string(buf, INET_ADDRSTRLEN);
+ }
+ } else if (ifa->ifa_addr->sa_family == AF_INET6) {
+ auto sa = reinterpret_cast(ifa->ifa_addr);
+ if (!IN6_IS_ADDR_LINKLOCAL(&sa->sin6_addr)) {
+ char buf[INET6_ADDRSTRLEN] = {};
+ if (inet_ntop(AF_INET6, &sa->sin6_addr, buf, INET6_ADDRSTRLEN)) {
+ // equivalent to mac's IN6_IS_ADDR_UNIQUE_LOCAL
+ auto s6_addr_head = sa->sin6_addr.s6_addr[0];
+ if (s6_addr_head == 0xfc || s6_addr_head == 0xfd) {
+ addr_candidate = std::string(buf, INET6_ADDRSTRLEN);
+ } else {
+ return std::string(buf, INET6_ADDRSTRLEN);
+ }
+ }
+ }
+ }
+ }
+ }
+ return addr_candidate;
+}
+#endif
+
+socket_t create_client_socket(
+ const std::string &host, const std::string &ip, int port,
+ int address_family, bool tcp_nodelay, bool ipv6_v6only,
+ SocketOptions socket_options, time_t connection_timeout_sec,
+ time_t connection_timeout_usec, time_t read_timeout_sec,
+ time_t read_timeout_usec, time_t write_timeout_sec,
+ time_t write_timeout_usec, const std::string &intf, Error &error) {
+ auto sock = create_socket(
+ host, ip, port, address_family, 0, tcp_nodelay, ipv6_v6only,
+ std::move(socket_options),
+ [&](socket_t sock2, struct addrinfo &ai, bool &quit) -> bool {
+ if (!intf.empty()) {
+#ifdef USE_IF2IP
+ auto ip_from_if = if2ip(address_family, intf);
+ if (ip_from_if.empty()) { ip_from_if = intf; }
+ if (!bind_ip_address(sock2, ip_from_if)) {
+ error = Error::BindIPAddress;
+ return false;
+ }
+#endif
+ }
+
+ set_nonblocking(sock2, true);
+
+ auto ret =
+ ::connect(sock2, ai.ai_addr, static_cast(ai.ai_addrlen));
+
+ if (ret < 0) {
+ if (is_connection_error()) {
+ error = Error::Connection;
+ return false;
+ }
+ error = wait_until_socket_is_ready(sock2, connection_timeout_sec,
+ connection_timeout_usec);
+ if (error != Error::Success) {
+ if (error == Error::ConnectionTimeout) { quit = true; }
+ return false;
+ }
+ }
+
+ set_nonblocking(sock2, false);
+ set_socket_opt_time(sock2, SOL_SOCKET, SO_RCVTIMEO, read_timeout_sec,
+ read_timeout_usec);
+ set_socket_opt_time(sock2, SOL_SOCKET, SO_SNDTIMEO, write_timeout_sec,
+ write_timeout_usec);
+
+ error = Error::Success;
+ return true;
+ },
+ connection_timeout_sec); // Pass DNS timeout
+
+ if (sock != INVALID_SOCKET) {
+ error = Error::Success;
+ } else {
+ if (error == Error::Success) { error = Error::Connection; }
+ }
+
+ return sock;
+}
+
+bool get_ip_and_port(const struct sockaddr_storage &addr,
+ socklen_t addr_len, std::string &ip, int &port) {
+ if (addr.ss_family == AF_INET) {
+ port = ntohs(reinterpret_cast(&addr)->sin_port);
+ } else if (addr.ss_family == AF_INET6) {
+ port =
+ ntohs(reinterpret_cast(&addr)->sin6_port);
+ } else {
+ return false;
+ }
+
+ std::array ipstr{};
+ if (getnameinfo(reinterpret_cast(&addr), addr_len,
+ ipstr.data(), static_cast(ipstr.size()), nullptr,
+ 0, NI_NUMERICHOST)) {
+ return false;
+ }
+
+ ip = ipstr.data();
+ return true;
+}
+
+void get_local_ip_and_port(socket_t sock, std::string &ip, int &port) {
+ struct sockaddr_storage addr;
+ socklen_t addr_len = sizeof(addr);
+ if (!getsockname(sock, reinterpret_cast(&addr),
+ &addr_len)) {
+ get_ip_and_port(addr, addr_len, ip, port);
+ }
+}
+
+void get_remote_ip_and_port(socket_t sock, std::string &ip, int &port) {
+ struct sockaddr_storage addr;
+ socklen_t addr_len = sizeof(addr);
+
+ if (!getpeername(sock, reinterpret_cast(&addr),
+ &addr_len)) {
+#ifndef _WIN32
+ if (addr.ss_family == AF_UNIX) {
+#if defined(__linux__)
+ struct ucred ucred;
+ socklen_t len = sizeof(ucred);
+ if (getsockopt(sock, SOL_SOCKET, SO_PEERCRED, &ucred, &len) == 0) {
+ port = ucred.pid;
+ }
+#elif defined(SOL_LOCAL) && defined(SO_PEERPID)
+ pid_t pid;
+ socklen_t len = sizeof(pid);
+ if (getsockopt(sock, SOL_LOCAL, SO_PEERPID, &pid, &len) == 0) {
+ port = pid;
+ }
+#endif
+ return;
+ }
+#endif
+ get_ip_and_port(addr, addr_len, ip, port);
+ }
+}
+
+// Recursive form retained so operator""_t below can compute hashes for
+// switch-case labels at compile time (C++11 constexpr forbids loops). Do not
+// call from runtime paths with arbitrary-length inputs โ use str2tag()
+// instead, which is iterative and stack-safe.
+constexpr unsigned int str2tag_core(const char *s, size_t l,
+ unsigned int h) {
+ return (l == 0)
+ ? h
+ : str2tag_core(
+ s + 1, l - 1,
+ // Unsets the 6 high bits of h, therefore no overflow happens
+ (((std::numeric_limits::max)() >> 6) &
+ h * 33) ^
+ static_cast(*s));
+}
+
+unsigned int str2tag(const std::string &s) {
+ // Iterative form of str2tag_core: the recursive constexpr version is kept
+ // for compile-time UDL evaluation of short string literals, but at runtime
+ // we may receive arbitrarily long inputs (e.g. fuzzed Content-Type) that
+ // would blow the stack with one frame per character.
+ unsigned int h = 0;
+ for (auto c : s) {
+ h = (((std::numeric_limits::max)() >> 6) & h * 33) ^
+ static_cast(c);
+ }
+ return h;
+}
+
+namespace udl {
+
+constexpr unsigned int operator""_t(const char *s, size_t l) {
+ return str2tag_core(s, l, 0);
+}
+
+} // namespace udl
+
+std::string
+find_content_type(const std::string &path,
+ const std::map &user_data,
+ const std::string &default_content_type) {
+ auto ext = file_extension(path);
+
+ auto it = user_data.find(ext);
+ if (it != user_data.end()) { return it->second; }
+
+ using udl::operator""_t;
+
+ switch (str2tag(ext)) {
+ default: return default_content_type;
+
+ case "css"_t: return "text/css";
+ case "csv"_t: return "text/csv";
+ case "htm"_t:
+ case "html"_t: return "text/html";
+ case "js"_t:
+ case "mjs"_t: return "text/javascript";
+ case "txt"_t: return "text/plain";
+ case "vtt"_t: return "text/vtt";
+
+ case "apng"_t: return "image/apng";
+ case "avif"_t: return "image/avif";
+ case "bmp"_t: return "image/bmp";
+ case "gif"_t: return "image/gif";
+ case "png"_t: return "image/png";
+ case "svg"_t: return "image/svg+xml";
+ case "webp"_t: return "image/webp";
+ case "ico"_t: return "image/x-icon";
+ case "tif"_t: return "image/tiff";
+ case "tiff"_t: return "image/tiff";
+ case "jpg"_t:
+ case "jpeg"_t: return "image/jpeg";
+
+ case "mp4"_t: return "video/mp4";
+ case "mpeg"_t: return "video/mpeg";
+ case "webm"_t: return "video/webm";
+
+ case "mp3"_t: return "audio/mp3";
+ case "mpga"_t: return "audio/mpeg";
+ case "weba"_t: return "audio/webm";
+ case "wav"_t: return "audio/wave";
+
+ case "otf"_t: return "font/otf";
+ case "ttf"_t: return "font/ttf";
+ case "woff"_t: return "font/woff";
+ case "woff2"_t: return "font/woff2";
+
+ case "7z"_t: return "application/x-7z-compressed";
+ case "atom"_t: return "application/atom+xml";
+ case "pdf"_t: return "application/pdf";
+ case "json"_t: return "application/json";
+ case "rss"_t: return "application/rss+xml";
+ case "tar"_t: return "application/x-tar";
+ case "xht"_t:
+ case "xhtml"_t: return "application/xhtml+xml";
+ case "xslt"_t: return "application/xslt+xml";
+ case "xml"_t: return "application/xml";
+ case "gz"_t: return "application/gzip";
+ case "zip"_t: return "application/zip";
+ case "wasm"_t: return "application/wasm";
+ }
+}
+
+std::string
+extract_media_type(const std::string &content_type,
+ std::map *params = nullptr) {
+ // Extract type/subtype from Content-Type value (RFC 2045)
+ // e.g. "application/json; charset=utf-8" -> "application/json"
+ auto media_type = content_type;
+ auto semicolon_pos = media_type.find(';');
+ if (semicolon_pos != std::string::npos) {
+ auto param_str = media_type.substr(semicolon_pos + 1);
+ media_type = media_type.substr(0, semicolon_pos);
+
+ if (params) {
+ // Parse parameters: key=value pairs separated by ';'
+ split(param_str.data(), param_str.data() + param_str.size(), ';',
+ [&](const char *b, const char *e) {
+ std::string key;
+ std::string val;
+ split(b, e, '=', [&](const char *b2, const char *e2) {
+ if (key.empty()) {
+ key.assign(b2, e2);
+ } else {
+ val.assign(b2, e2);
+ }
+ });
+ if (!key.empty()) {
+ params->emplace(trim_copy(key), trim_double_quotes_copy(val));
+ }
+ });
+ }
+ }
+
+ // Trim whitespace from media type
+ return trim_copy(media_type);
+}
+
+bool can_compress_content_type(const std::string &content_type) {
+ using udl::operator""_t;
+
+ auto mime_type = extract_media_type(content_type);
+ auto tag = str2tag(mime_type);
+
+ switch (tag) {
+ case "image/svg+xml"_t:
+ case "application/javascript"_t:
+ case "application/x-javascript"_t:
+ case "application/json"_t:
+ case "application/ld+json"_t:
+ case "application/xml"_t:
+ case "application/xhtml+xml"_t:
+ case "application/rss+xml"_t:
+ case "application/atom+xml"_t:
+ case "application/xslt+xml"_t:
+ case "application/protobuf"_t: return true;
+
+ case "text/event-stream"_t: return false;
+
+ default: return !mime_type.rfind("text/", 0);
+ }
+}
+
+bool parse_quality(const char *b, const char *e, std::string &token,
+ double &quality) {
+ quality = 1.0;
+ token.clear();
+
+ // Split on first ';': left = token name, right = parameters
+ const char *params_b = nullptr;
+ std::size_t params_len = 0;
+
+ divide(
+ b, static_cast(e - b), ';',
+ [&](const char *lb, std::size_t llen, const char *rb, std::size_t rlen) {
+ auto r = trim(lb, lb + llen, 0, llen);
+ if (r.first < r.second) { token.assign(lb + r.first, lb + r.second); }
+ params_b = rb;
+ params_len = rlen;
+ });
+
+ if (token.empty()) { return false; }
+ if (params_len == 0) { return true; }
+
+ // Scan parameters for q= (stops on first match)
+ bool invalid = false;
+ split_find(params_b, params_b + params_len, ';',
+ (std::numeric_limits::max)(),
+ [&](const char *pb, const char *pe) -> bool {
+ // Match exactly "q=" or "Q=" (not "query=" etc.)
+ auto len = static_cast(pe - pb);
+ if (len < 2) { return false; }
+ if ((pb[0] != 'q' && pb[0] != 'Q') || pb[1] != '=') {
+ return false;
+ }
+
+ // Trim the value portion
+ auto r = trim(pb, pe, 2, len);
+ if (r.first >= r.second) {
+ invalid = true;
+ return true;
+ }
+
+ double v = 0.0;
+ auto res = from_chars(pb + r.first, pb + r.second, v);
+ if (res.ec != std::errc{} || v < 0.0 || v > 1.0) {
+ invalid = true;
+ return true;
+ }
+ quality = v;
+ return true;
+ });
+
+ return !invalid;
+}
+
+EncodingType encoding_type(const Request &req, const Response &res) {
+ if (!can_compress_content_type(res.get_header_value("Content-Type"))) {
+ return EncodingType::None;
+ }
+
+ const auto &s = req.get_header_value("Accept-Encoding");
+ if (s.empty()) { return EncodingType::None; }
+
+ // Single-pass: iterate tokens and track the best supported encoding.
+ // Server preference breaks ties (br > gzip > zstd).
+ EncodingType best = EncodingType::None;
+ double best_q = 0.0; // q=0 means "not acceptable"
+
+ // Server preference: Brotli > Gzip > Zstd (lower = more preferred)
+ auto priority = [](EncodingType t) -> int {
+ switch (t) {
+ case EncodingType::Brotli: return 0;
+ case EncodingType::Gzip: return 1;
+ case EncodingType::Zstd: return 2;
+ default: return 3;
+ }
+ };
+
+ std::string name;
+ split(s.data(), s.data() + s.size(), ',', [&](const char *b, const char *e) {
+ double quality = 1.0;
+ if (!parse_quality(b, e, name, quality)) { return; }
+ if (quality <= 0.0) { return; }
+
+ EncodingType type = EncodingType::None;
+#ifdef CPPHTTPLIB_BROTLI_SUPPORT
+ if (case_ignore::equal(name, "br")) { type = EncodingType::Brotli; }
+#endif
+#ifdef CPPHTTPLIB_ZLIB_SUPPORT
+ if (type == EncodingType::None && case_ignore::equal(name, "gzip")) {
+ type = EncodingType::Gzip;
+ }
+#endif
+#ifdef CPPHTTPLIB_ZSTD_SUPPORT
+ if (type == EncodingType::None && case_ignore::equal(name, "zstd")) {
+ type = EncodingType::Zstd;
+ }
+#endif
+
+ if (type == EncodingType::None) { return; }
+
+ // Higher q-value wins; for equal q, server preference breaks ties
+ if (quality > best_q ||
+ (quality == best_q && priority(type) < priority(best))) {
+ best_q = quality;
+ best = type;
+ }
+ });
+
+ return best;
+}
+
+std::unique_ptr make_compressor(EncodingType type) {
+#ifdef CPPHTTPLIB_ZLIB_SUPPORT
+ if (type == EncodingType::Gzip) {
+ return detail::make_unique();
+ }
+#endif
+#ifdef CPPHTTPLIB_BROTLI_SUPPORT
+ if (type == EncodingType::Brotli) {
+ return detail::make_unique();
+ }
+#endif
+#ifdef CPPHTTPLIB_ZSTD_SUPPORT
+ if (type == EncodingType::Zstd) {
+ return detail::make_unique();
+ }
+#endif
+ (void)type;
+ return nullptr;
+}
+
+const char *encoding_name(EncodingType type) {
+ switch (type) {
+ case EncodingType::Gzip: return "gzip";
+ case EncodingType::Brotli: return "br";
+ case EncodingType::Zstd: return "zstd";
+ default: return "";
+ }
+}
+
+bool nocompressor::compress(const char *data, size_t data_length,
+ bool /*last*/, Callback callback) {
+ if (!data_length) { return true; }
+ return callback(data, data_length);
+}
+
+#ifdef CPPHTTPLIB_ZLIB_SUPPORT
+gzip_compressor::gzip_compressor() {
+ std::memset(&strm_, 0, sizeof(strm_));
+ strm_.zalloc = Z_NULL;
+ strm_.zfree = Z_NULL;
+ strm_.opaque = Z_NULL;
+
+ is_valid_ = deflateInit2(&strm_, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 31, 8,
+ Z_DEFAULT_STRATEGY) == Z_OK;
+}
+
+gzip_compressor::~gzip_compressor() { deflateEnd(&strm_); }
+
+bool gzip_compressor::compress(const char *data, size_t data_length,
+ bool last, Callback callback) {
+ assert(is_valid_);
+
+ do {
+ constexpr size_t max_avail_in =
+ (std::numeric_limits::max)();
+
+ strm_.avail_in = static_cast(
+ (std::min)(data_length, max_avail_in));
+ strm_.next_in = const_cast(reinterpret_cast(data));
+
+ data_length -= strm_.avail_in;
+ data += strm_.avail_in;
+
+ auto flush = (last && data_length == 0) ? Z_FINISH : Z_NO_FLUSH;
+ auto ret = Z_OK;
+
+ std::array buff{};
+ do {
+ strm_.avail_out = static_cast(buff.size());
+ strm_.next_out = reinterpret_cast(buff.data());
+
+ ret = deflate(&strm_, flush);
+ if (ret == Z_STREAM_ERROR) { return false; }
+
+ if (!callback(buff.data(), buff.size() - strm_.avail_out)) {
+ return false;
+ }
+ } while (strm_.avail_out == 0);
+
+ assert((flush == Z_FINISH && ret == Z_STREAM_END) ||
+ (flush == Z_NO_FLUSH && ret == Z_OK));
+ assert(strm_.avail_in == 0);
+ } while (data_length > 0);
+
+ return true;
+}
+
+gzip_decompressor::gzip_decompressor() {
+ std::memset(&strm_, 0, sizeof(strm_));
+ strm_.zalloc = Z_NULL;
+ strm_.zfree = Z_NULL;
+ strm_.opaque = Z_NULL;
+
+ // 15 is the value of wbits, which should be at the maximum possible value
+ // to ensure that any gzip stream can be decoded. The offset of 32 specifies
+ // that the stream type should be automatically detected either gzip or
+ // deflate.
+ is_valid_ = inflateInit2(&strm_, 32 + 15) == Z_OK;
+}
+
+gzip_decompressor::~gzip_decompressor() { inflateEnd(&strm_); }
+
+bool gzip_decompressor::is_valid() const { return is_valid_; }
+
+bool gzip_decompressor::decompress(const char *data, size_t data_length,
+ Callback callback) {
+ assert(is_valid_);
+
+ auto ret = Z_OK;
+
+ do {
+ constexpr size_t max_avail_in =
+ (std::numeric_limits::max)();
+
+ strm_.avail_in = static_cast(
+ (std::min)(data_length, max_avail_in));
+ strm_.next_in = const_cast(reinterpret_cast(data));
+
+ data_length -= strm_.avail_in;
+ data += strm_.avail_in;
+
+ std::array buff{};
+ while (strm_.avail_in > 0 && ret == Z_OK) {
+ strm_.avail_out = static_cast(buff.size());
+ strm_.next_out = reinterpret_cast(buff.data());
+
+ ret = inflate(&strm_, Z_NO_FLUSH);
+
+ assert(ret != Z_STREAM_ERROR);
+ switch (ret) {
+ case Z_NEED_DICT:
+ case Z_DATA_ERROR:
+ case Z_MEM_ERROR: inflateEnd(&strm_); return false;
+ }
+
+ if (!callback(buff.data(), buff.size() - strm_.avail_out)) {
+ return false;
+ }
+ }
+
+ if (ret != Z_OK && ret != Z_STREAM_END) { return false; }
+
+ } while (data_length > 0);
+
+ return true;
+}
+#endif
+
+#ifdef CPPHTTPLIB_BROTLI_SUPPORT
+brotli_compressor::brotli_compressor() {
+ state_ = BrotliEncoderCreateInstance(nullptr, nullptr, nullptr);
+}
+
+brotli_compressor::~brotli_compressor() {
+ BrotliEncoderDestroyInstance(state_);
+}
+
+bool brotli_compressor::compress(const char *data, size_t data_length,
+ bool last, Callback callback) {
+ std::array buff{};
+
+ auto operation = last ? BROTLI_OPERATION_FINISH : BROTLI_OPERATION_PROCESS;
+ auto available_in = data_length;
+ auto next_in = reinterpret_cast(data);
+
+ for (;;) {
+ if (last) {
+ if (BrotliEncoderIsFinished(state_)) { break; }
+ } else {
+ if (!available_in) { break; }
+ }
+
+ auto available_out = buff.size();
+ auto next_out = buff.data();
+
+ if (!BrotliEncoderCompressStream(state_, operation, &available_in, &next_in,
+ &available_out, &next_out, nullptr)) {
+ return false;
+ }
+
+ auto output_bytes = buff.size() - available_out;
+ if (output_bytes) {
+ callback(reinterpret_cast(buff.data()), output_bytes);
+ }
+ }
+
+ return true;
+}
+
+brotli_decompressor::brotli_decompressor() {
+ decoder_s = BrotliDecoderCreateInstance(0, 0, 0);
+ decoder_r = decoder_s ? BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT
+ : BROTLI_DECODER_RESULT_ERROR;
+}
+
+brotli_decompressor::~brotli_decompressor() {
+ if (decoder_s) { BrotliDecoderDestroyInstance(decoder_s); }
+}
+
+bool brotli_decompressor::is_valid() const { return decoder_s; }
+
+bool brotli_decompressor::decompress(const char *data,
+ size_t data_length,
+ Callback callback) {
+ if (decoder_r == BROTLI_DECODER_RESULT_SUCCESS ||
+ decoder_r == BROTLI_DECODER_RESULT_ERROR) {
+ return 0;
+ }
+
+ auto next_in = reinterpret_cast(data);
+ size_t avail_in = data_length;
+ size_t total_out;
+
+ decoder_r = BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT;
+
+ std::array buff{};
+ while (decoder_r == BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT) {
+ char *next_out = buff.data();
+ size_t avail_out = buff.size();
+
+ decoder_r = BrotliDecoderDecompressStream(
+ decoder_s, &avail_in, &next_in, &avail_out,
+ reinterpret_cast(&next_out), &total_out);
+
+ if (decoder_r == BROTLI_DECODER_RESULT_ERROR) { return false; }
+
+ if (!callback(buff.data(), buff.size() - avail_out)) { return false; }
+ }
+
+ return decoder_r == BROTLI_DECODER_RESULT_SUCCESS ||
+ decoder_r == BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT;
+}
+#endif
+
+#ifdef CPPHTTPLIB_ZSTD_SUPPORT
+zstd_compressor::zstd_compressor() {
+ ctx_ = ZSTD_createCCtx();
+ ZSTD_CCtx_setParameter(ctx_, ZSTD_c_compressionLevel, ZSTD_fast);
+}
+
+zstd_compressor::~zstd_compressor() { ZSTD_freeCCtx(ctx_); }
+
+bool zstd_compressor::compress(const char *data, size_t data_length,
+ bool last, Callback callback) {
+ std::array buff{};
+
+ ZSTD_EndDirective mode = last ? ZSTD_e_end : ZSTD_e_continue;
+ ZSTD_inBuffer input = {data, data_length, 0};
+
+ bool finished;
+ do {
+ ZSTD_outBuffer output = {buff.data(), CPPHTTPLIB_COMPRESSION_BUFSIZ, 0};
+ size_t const remaining = ZSTD_compressStream2(ctx_, &output, &input, mode);
+
+ if (ZSTD_isError(remaining)) { return false; }
+
+ if (!callback(buff.data(), output.pos)) { return false; }
+
+ finished = last ? (remaining == 0) : (input.pos == input.size);
+
+ } while (!finished);
+
+ return true;
+}
+
+zstd_decompressor::zstd_decompressor() { ctx_ = ZSTD_createDCtx(); }
+
+zstd_decompressor::~zstd_decompressor() { ZSTD_freeDCtx(ctx_); }
+
+bool zstd_decompressor::is_valid() const { return ctx_ != nullptr; }
+
+bool zstd_decompressor::decompress(const char *data, size_t data_length,
+ Callback callback) {
+ std::array buff{};
+ ZSTD_inBuffer input = {data, data_length, 0};
+
+ while (input.pos < input.size) {
+ ZSTD_outBuffer output = {buff.data(), CPPHTTPLIB_COMPRESSION_BUFSIZ, 0};
+ size_t const remaining = ZSTD_decompressStream(ctx_, &output, &input);
+
+ if (ZSTD_isError(remaining)) { return false; }
+
+ if (!callback(buff.data(), output.pos)) { return false; }
+ }
+
+ return true;
+}
+#endif
+
+std::unique_ptr
+create_decompressor(const std::string &encoding) {
+ std::unique_ptr decompressor;
+
+ if (encoding == "gzip" || encoding == "deflate") {
+#ifdef CPPHTTPLIB_ZLIB_SUPPORT
+ decompressor = detail::make_unique();
+#endif
+ } else if (encoding.find("br") != std::string::npos) {
+#ifdef CPPHTTPLIB_BROTLI_SUPPORT
+ decompressor = detail::make_unique();
+#endif
+ } else if (encoding == "zstd" || encoding.find("zstd") != std::string::npos) {
+#ifdef CPPHTTPLIB_ZSTD_SUPPORT
+ decompressor = detail::make_unique();
+#endif
+ }
+
+ return decompressor;
+}
+
+// Returns the best available compressor and its Content-Encoding name.
+// Priority: Brotli > Gzip > Zstd (matches server-side preference).
+std::pair, const char *>
+create_compressor() {
+#ifdef CPPHTTPLIB_BROTLI_SUPPORT
+ return {detail::make_unique(), "br"};
+#elif defined(CPPHTTPLIB_ZLIB_SUPPORT)
+ return {detail::make_unique(), "gzip"};
+#elif defined(CPPHTTPLIB_ZSTD_SUPPORT)
+ return {detail::make_unique(), "zstd"};
+#else
+ return {nullptr, nullptr};
+#endif
+}
+
+bool is_prohibited_header_name(const std::string &name) {
+ using udl::operator""_t;
+
+ switch (str2tag(name)) {
+ case "REMOTE_ADDR"_t:
+ case "REMOTE_PORT"_t:
+ case "LOCAL_ADDR"_t:
+ case "LOCAL_PORT"_t: return true;
+ default: return false;
+ }
+}
+
+bool has_header(const Headers &headers, const std::string &key) {
+ if (is_prohibited_header_name(key)) { return false; }
+ return headers.find(key) != headers.end();
+}
+
+const char *get_header_value(const Headers &headers,
+ const std::string &key, const char *def,
+ size_t id) {
+ if (is_prohibited_header_name(key)) {
+#ifndef CPPHTTPLIB_NO_EXCEPTIONS
+ std::string msg = "Prohibited header name '" + key + "' is specified.";
+ throw std::invalid_argument(msg);
+#else
+ return "";
+#endif
+ }
+
+ auto rng = headers.equal_range(key);
+ auto it = rng.first;
+ std::advance(it, static_cast(id));
+ if (it != rng.second) { return it->second.c_str(); }
+ return def;
+}
+
+size_t get_header_value_count(const Headers &headers,
+ const std::string &key) {
+ auto r = headers.equal_range(key);
+ return static_cast(std::distance(r.first, r.second));
+}
+
+template
+typename Map::mapped_type
+get_multimap_value(const Map &m, const std::string &key, size_t id) {
+ auto rng = m.equal_range(key);
+ auto it = rng.first;
+ std::advance(it, static_cast(id));
+ if (it != rng.second) { return it->second; }
+ return typename Map::mapped_type();
+}
+
+void set_header(Headers &headers, const std::string &key,
+ const std::string &val) {
+ if (fields::is_field_name(key) && fields::is_field_value(val)) {
+ headers.emplace(key, val);
+ }
+}
+
+bool read_headers(Stream &strm, Headers &headers) {
+ const auto bufsiz = 2048;
+ char buf[bufsiz];
+ stream_line_reader line_reader(strm, buf, bufsiz);
+
+ size_t header_count = 0;
+
+ for (;;) {
+ if (!line_reader.getline()) { return false; }
+
+ // Check if the line ends with CRLF.
+ auto line_terminator_len = 2;
+ if (line_reader.end_with_crlf()) {
+ // Blank line indicates end of headers.
+ if (line_reader.size() == 2) { break; }
+ } else {
+#ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR
+ // Blank line indicates end of headers.
+ if (line_reader.size() == 1) { break; }
+ line_terminator_len = 1;
+#else
+ continue; // Skip invalid line.
+#endif
+ }
+
+ if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; }
+
+ // Check header count limit
+ if (header_count >= CPPHTTPLIB_HEADER_MAX_COUNT) { return false; }
+
+ // Exclude line terminator
+ auto end = line_reader.ptr() + line_reader.size() - line_terminator_len;
+
+ if (!parse_header(line_reader.ptr(), end,
+ [&](const std::string &key, const std::string &val) {
+ headers.emplace(key, val);
+ })) {
+ return false;
+ }
+
+ header_count++;
+ }
+
+ // RFC 9110 Section 8.6: Reject requests with multiple Content-Length
+ // headers that have different values to prevent request smuggling.
+ auto cl_range = headers.equal_range("Content-Length");
+ if (cl_range.first != cl_range.second) {
+ const auto &first_val = cl_range.first->second;
+ for (auto it = std::next(cl_range.first); it != cl_range.second; ++it) {
+ if (it->second != first_val) { return false; }
+ }
+ }
+
+ return true;
+}
+
+bool read_websocket_upgrade_response(Stream &strm,
+ const std::string &expected_accept,
+ std::string &selected_subprotocol) {
+ // Read status line
+ const auto bufsiz = 2048;
+ char buf[bufsiz];
+ stream_line_reader line_reader(strm, buf, bufsiz);
+ if (!line_reader.getline()) { return false; }
+
+ // Check for "HTTP/1.1 101"
+ auto line = std::string(line_reader.ptr(), line_reader.size());
+ if (line.find("HTTP/1.1 101") == std::string::npos) { return false; }
+
+ // Parse headers using existing read_headers
+ Headers headers;
+ if (!read_headers(strm, headers)) { return false; }
+
+ // Verify Upgrade: websocket (case-insensitive)
+ auto upgrade_it = headers.find("Upgrade");
+ if (upgrade_it == headers.end()) { return false; }
+ auto upgrade_val = case_ignore::to_lower(upgrade_it->second);
+ if (upgrade_val != "websocket") { return false; }
+
+ // Verify Connection header contains "Upgrade" (case-insensitive)
+ auto connection_it = headers.find("Connection");
+ if (connection_it == headers.end()) { return false; }
+ auto connection_val = case_ignore::to_lower(connection_it->second);
+ if (connection_val.find("upgrade") == std::string::npos) { return false; }
+
+ // Verify Sec-WebSocket-Accept header value
+ auto it = headers.find("Sec-WebSocket-Accept");
+ if (it == headers.end() || it->second != expected_accept) { return false; }
+
+ // Extract negotiated subprotocol
+ auto proto_it = headers.find("Sec-WebSocket-Protocol");
+ if (proto_it != headers.end()) { selected_subprotocol = proto_it->second; }
+
+ return true;
+}
+
+enum class ReadContentResult {
+ Success, // Successfully read the content
+ PayloadTooLarge, // The content exceeds the specified payload limit
+ Error // An error occurred while reading the content
+};
+
+ReadContentResult read_content_with_length(
+ Stream &strm, size_t len, DownloadProgress progress,
+ ContentReceiverWithProgress out,
+ size_t payload_max_length = (std::numeric_limits::max)()) {
+ char buf[CPPHTTPLIB_RECV_BUFSIZ];
+
+ detail::BodyReader br;
+ br.stream = &strm;
+ br.has_content_length = true;
+ br.content_length = len;
+ br.payload_max_length = payload_max_length;
+ br.chunked = false;
+ br.bytes_read = 0;
+ br.last_error = Error::Success;
+
+ size_t r = 0;
+ while (r < len) {
+ auto read_len = static_cast(len - r);
+ auto to_read = (std::min)(read_len, CPPHTTPLIB_RECV_BUFSIZ);
+ auto n = detail::read_body_content(&strm, br, buf, to_read);
+ if (n <= 0) {
+ // Check if it was a payload size error
+ if (br.last_error == Error::ExceedMaxPayloadSize) {
+ return ReadContentResult::PayloadTooLarge;
+ }
+ return ReadContentResult::Error;
+ }
+
+ if (!out(buf, static_cast(n), r, len)) {
+ return ReadContentResult::Error;
+ }
+ r += static_cast(n);
+
+ if (progress) {
+ if (!progress(r, len)) { return ReadContentResult::Error; }
+ }
+ }
+
+ return ReadContentResult::Success;
+}
+
+ReadContentResult
+read_content_without_length(Stream &strm, size_t payload_max_length,
+ ContentReceiverWithProgress out) {
+ char buf[CPPHTTPLIB_RECV_BUFSIZ];
+ size_t r = 0;
+ for (;;) {
+ auto n = strm.read(buf, CPPHTTPLIB_RECV_BUFSIZ);
+ if (n == 0) { return ReadContentResult::Success; }
+ if (n < 0) { return ReadContentResult::Error; }
+
+ // Check if adding this data would exceed the payload limit
+ if (r > payload_max_length ||
+ payload_max_length - r < static_cast(n)) {
+ return ReadContentResult::PayloadTooLarge;
+ }
+
+ if (!out(buf, static_cast(n), r, 0)) {
+ return ReadContentResult::Error;
+ }
+ r += static_cast(n);
+ }
+
+ return ReadContentResult::Success;
+}
+
+template
+ReadContentResult read_content_chunked(Stream &strm, T &x,
+ size_t payload_max_length,
+ ContentReceiverWithProgress out) {
+ detail::ChunkedDecoder dec(strm);
+
+ char buf[CPPHTTPLIB_RECV_BUFSIZ];
+ size_t total_len = 0;
+
+ for (;;) {
+ size_t chunk_offset = 0;
+ size_t chunk_total = 0;
+ auto n = dec.read_payload(buf, sizeof(buf), chunk_offset, chunk_total);
+ if (n < 0) { return ReadContentResult::Error; }
+
+ if (n == 0) {
+ if (!dec.parse_trailers_into(x.trailers, x.headers)) {
+ return ReadContentResult::Error;
+ }
+ return ReadContentResult::Success;
+ }
+
+ if (total_len > payload_max_length ||
+ payload_max_length - total_len < static_cast(n)) {
+ return ReadContentResult::PayloadTooLarge;
+ }
+
+ if (!out(buf, static_cast(n), chunk_offset, chunk_total)) {
+ return ReadContentResult::Error;
+ }
+
+ total_len += static_cast(n);
+ }
+}
+
+bool is_chunked_transfer_encoding(const Headers &headers) {
+ return case_ignore::equal(
+ get_header_value(headers, "Transfer-Encoding", "", 0), "chunked");
+}
+
+template
+bool prepare_content_receiver(T &x, int &status,
+ ContentReceiverWithProgress receiver,
+ bool decompress, size_t payload_max_length,
+ bool &exceed_payload_max_length, U callback) {
+ if (decompress) {
+ std::string encoding = x.get_header_value("Content-Encoding");
+ std::unique_ptr decompressor;
+
+ if (!encoding.empty()) {
+ decompressor = detail::create_decompressor(encoding);
+ if (!decompressor) {
+ // Unsupported encoding or no support compiled in
+ status = StatusCode::UnsupportedMediaType_415;
+ return false;
+ }
+ }
+
+ if (decompressor) {
+ if (decompressor->is_valid()) {
+ size_t decompressed_size = 0;
+ ContentReceiverWithProgress out = [&](const char *buf, size_t n,
+ size_t off, size_t len) {
+ return decompressor->decompress(
+ buf, n, [&](const char *buf2, size_t n2) {
+ // Guard against zip-bomb: check
+ // decompressed size against limit.
+ if (payload_max_length > 0 &&
+ (decompressed_size >= payload_max_length ||
+ n2 > payload_max_length - decompressed_size)) {
+ exceed_payload_max_length = true;
+ return false;
+ }
+ decompressed_size += n2;
+ return receiver(buf2, n2, off, len);
+ });
+ };
+ return callback(std::move(out));
+ } else {
+ status = StatusCode::InternalServerError_500;
+ return false;
+ }
+ }
+ }
+
+ ContentReceiverWithProgress out = [&](const char *buf, size_t n, size_t off,
+ size_t len) {
+ return receiver(buf, n, off, len);
+ };
+ return callback(std::move(out));
+}
+
+template
+bool read_content(Stream &strm, T &x, size_t payload_max_length, int &status,
+ DownloadProgress progress,
+ ContentReceiverWithProgress receiver, bool decompress) {
+ bool exceed_payload_max_length = false;
+ return prepare_content_receiver(
+ x, status, std::move(receiver), decompress, payload_max_length,
+ exceed_payload_max_length, [&](const ContentReceiverWithProgress &out) {
+ auto ret = true;
+ // Note: exceed_payload_max_length may also be set by the decompressor
+ // wrapper in prepare_content_receiver when the decompressed payload
+ // size exceeds the limit.
+
+ if (is_chunked_transfer_encoding(x.headers)) {
+ auto result = read_content_chunked(strm, x, payload_max_length, out);
+ if (result == ReadContentResult::Success) {
+ ret = true;
+ } else if (result == ReadContentResult::PayloadTooLarge) {
+ exceed_payload_max_length = true;
+ ret = false;
+ } else {
+ ret = false;
+ }
+ } else if (!has_header(x.headers, "Content-Length")) {
+ auto result =
+ read_content_without_length(strm, payload_max_length, out);
+ if (result == ReadContentResult::Success) {
+ ret = true;
+ } else if (result == ReadContentResult::PayloadTooLarge) {
+ exceed_payload_max_length = true;
+ ret = false;
+ } else {
+ ret = false;
+ }
+ } else {
+ auto is_invalid_value = false;
+ auto len = get_header_value_u64(x.headers, "Content-Length",
+ (std::numeric_limits::max)(),
+ 0, is_invalid_value);
+
+ if (is_invalid_value) {
+ ret = false;
+ } else if (len > 0) {
+ auto result = read_content_with_length(
+ strm, len, std::move(progress), out, payload_max_length);
+ ret = (result == ReadContentResult::Success);
+ if (result == ReadContentResult::PayloadTooLarge) {
+ exceed_payload_max_length = true;
+ }
+ }
+ }
+
+ if (!ret) {
+ status = exceed_payload_max_length ? StatusCode::PayloadTooLarge_413
+ : StatusCode::BadRequest_400;
+ }
+ return ret;
+ });
+}
+
+ssize_t write_request_line(Stream &strm, const std::string &method,
+ const std::string &path) {
+ std::string s = method;
+ s += ' ';
+ s += path;
+ s += " HTTP/1.1\r\n";
+ return strm.write(s.data(), s.size());
+}
+
+ssize_t write_response_line(Stream &strm, int status) {
+ std::string s = "HTTP/1.1 ";
+ s += std::to_string(status);
+ s += ' ';
+ s += httplib::status_message(status);
+ s += "\r\n";
+ return strm.write(s.data(), s.size());
+}
+
+ssize_t write_headers(Stream &strm, const Headers &headers) {
+ ssize_t write_len = 0;
+ for (const auto &x : headers) {
+ std::string s;
+ s = x.first;
+ s += ": ";
+ s += x.second;
+ s += "\r\n";
+
+ auto len = strm.write(s.data(), s.size());
+ if (len < 0) { return len; }
+ write_len += len;
+ }
+ auto len = strm.write("\r\n");
+ if (len < 0) { return len; }
+ write_len += len;
+ return write_len;
+}
+
+bool write_data(Stream &strm, const char *d, size_t l) {
+ size_t offset = 0;
+ while (offset < l) {
+ auto length = strm.write(d + offset, l - offset);
+ if (length < 0) { return false; }
+ offset += static_cast(length);
+ }
+ return true;
+}
+
+template
+bool write_content_with_progress(Stream &strm,
+ const ContentProvider &content_provider,
+ size_t offset, size_t length,
+ T is_shutting_down,
+ const UploadProgress &upload_progress,
+ Error &error) {
+ size_t end_offset = offset + length;
+ size_t start_offset = offset;
+ auto ok = true;
+ DataSink data_sink;
+
+ data_sink.write = [&](const char *d, size_t l) -> bool {
+ if (ok) {
+ if (write_data(strm, d, l)) {
+ offset += l;
+
+ if (upload_progress && length > 0) {
+ size_t current_written = offset - start_offset;
+ if (!upload_progress(current_written, length)) {
+ ok = false;
+ return false;
+ }
+ }
+ } else {
+ ok = false;
+ }
+ }
+ return ok;
+ };
+
+ data_sink.is_writable = [&]() -> bool { return strm.is_peer_alive(); };
+
+ while (offset < end_offset && !is_shutting_down()) {
+ if (!strm.wait_writable() || !strm.is_peer_alive()) {
+ error = Error::Write;
+ return false;
+ } else if (!content_provider(offset, end_offset - offset, data_sink)) {
+ error = Error::Canceled;
+ return false;
+ } else if (!ok) {
+ error = Error::Write;
+ return false;
+ }
+ }
+
+ if (offset < end_offset) { // exited due to is_shutting_down(), not completion
+ error = Error::Write;
+ return false;
+ }
+
+ error = Error::Success;
+ return true;
+}
+
+template
+bool write_content(Stream &strm, const ContentProvider &content_provider,
+ size_t offset, size_t length, T is_shutting_down,
+ Error &error) {
+ return write_content_with_progress(strm, content_provider, offset, length,
+ is_shutting_down, nullptr, error);
+}
+
+template
+bool write_content(Stream &strm, const ContentProvider &content_provider,
+ size_t offset, size_t length,
+ const T &is_shutting_down) {
+ auto error = Error::Success;
+ return write_content(strm, content_provider, offset, length, is_shutting_down,
+ error);
+}
+
+template
+bool
+write_content_without_length(Stream &strm,
+ const ContentProvider &content_provider,
+ const T &is_shutting_down) {
+ size_t offset = 0;
+ auto data_available = true;
+ auto ok = true;
+ DataSink data_sink;
+
+ data_sink.write = [&](const char *d, size_t l) -> bool {
+ if (ok) {
+ offset += l;
+ if (!write_data(strm, d, l)) { ok = false; }
+ }
+ return ok;
+ };
+
+ data_sink.is_writable = [&]() -> bool { return strm.is_peer_alive(); };
+
+ data_sink.done = [&](void) { data_available = false; };
+
+ while (data_available && !is_shutting_down()) {
+ if (!strm.wait_writable() || !strm.is_peer_alive()) {
+ return false;
+ } else if (!content_provider(offset, 0, data_sink)) {
+ return false;
+ } else if (!ok) {
+ return false;
+ }
+ }
+ return !data_available; // true only if done() was called, false if shutting
+ // down
+}
+
+template
+bool
+write_content_chunked(Stream &strm, const ContentProvider &content_provider,
+ const T &is_shutting_down, U &compressor, Error &error) {
+ size_t offset = 0;
+ auto data_available = true;
+ auto ok = true;
+ DataSink data_sink;
+
+ data_sink.write = [&](const char *d, size_t l) -> bool {
+ if (ok) {
+ data_available = l > 0;
+ offset += l;
+
+ std::string payload;
+ if (compressor.compress(d, l, false,
+ [&](const char *data, size_t data_len) {
+ payload.append(data, data_len);
+ return true;
+ })) {
+ if (!payload.empty()) {
+ // Emit chunked response header and footer for each chunk
+ auto chunk =
+ from_i_to_hex(payload.size()) + "\r\n" + payload + "\r\n";
+ if (!write_data(strm, chunk.data(), chunk.size())) { ok = false; }
+ }
+ } else {
+ ok = false;
+ }
+ }
+ return ok;
+ };
+
+ data_sink.is_writable = [&]() -> bool { return strm.is_peer_alive(); };
+
+ auto done_with_trailer = [&](const Headers *trailer) {
+ if (!ok) { return; }
+
+ data_available = false;
+
+ std::string payload;
+ if (!compressor.compress(nullptr, 0, true,
+ [&](const char *data, size_t data_len) {
+ payload.append(data, data_len);
+ return true;
+ })) {
+ ok = false;
+ return;
+ }
+
+ if (!payload.empty()) {
+ // Emit chunked response header and footer for each chunk
+ auto chunk = from_i_to_hex(payload.size()) + "\r\n" + payload + "\r\n";
+ if (!write_data(strm, chunk.data(), chunk.size())) {
+ ok = false;
+ return;
+ }
+ }
+
+ constexpr const char done_marker[] = "0\r\n";
+ if (!write_data(strm, done_marker, str_len(done_marker))) { ok = false; }
+
+ // Trailer
+ if (trailer) {
+ for (const auto &kv : *trailer) {
+ std::string field_line = kv.first + ": " + kv.second + "\r\n";
+ if (!write_data(strm, field_line.data(), field_line.size())) {
+ ok = false;
+ }
+ }
+ }
+
+ constexpr const char crlf[] = "\r\n";
+ if (!write_data(strm, crlf, str_len(crlf))) { ok = false; }
+ };
+
+ data_sink.done = [&](void) { done_with_trailer(nullptr); };
+
+ data_sink.done_with_trailer = [&](const Headers &trailer) {
+ done_with_trailer(&trailer);
+ };
+
+ while (data_available && !is_shutting_down()) {
+ if (!strm.wait_writable() || !strm.is_peer_alive()) {
+ error = Error::Write;
+ return false;
+ } else if (!content_provider(offset, 0, data_sink)) {
+ error = Error::Canceled;
+ return false;
+ } else if (!ok) {
+ error = Error::Write;
+ return false;
+ }
+ }
+
+ if (data_available) { // exited due to is_shutting_down(), not done()
+ error = Error::Write;
+ return false;
+ }
+
+ error = Error::Success;
+ return true;
+}
+
+template
+bool write_content_chunked(Stream &strm,
+ const ContentProvider &content_provider,
+ const T &is_shutting_down, U &compressor) {
+ auto error = Error::Success;
+ return write_content_chunked(strm, content_provider, is_shutting_down,
+ compressor, error);
+}
+
+template
+bool redirect(T &cli, Request &req, Response &res,
+ const std::string &path, const std::string &location,
+ Error &error) {
+ Request new_req = req;
+ new_req.path = path;
+ new_req.redirect_count_ -= 1;
+
+ if (res.status == StatusCode::SeeOther_303 &&
+ (req.method != "GET" && req.method != "HEAD")) {
+ new_req.method = "GET";
+ new_req.body.clear();
+ new_req.headers.clear();
+ }
+
+ Response new_res;
+
+ auto ret = cli.send(new_req, new_res, error);
+ if (ret) {
+ req = std::move(new_req);
+ res = std::move(new_res);
+
+ if (res.location.empty()) { res.location = location; }
+ }
+ return ret;
+}
+
+std::string params_to_query_str(const Params ¶ms) {
+ std::string query;
+
+ for (auto it = params.begin(); it != params.end(); ++it) {
+ if (it != params.begin()) { query += '&'; }
+ query += encode_query_component(it->first);
+ query += '=';
+ query += encode_query_component(it->second);
+ }
+ return query;
+}
+
+void parse_query_text(const char *data, std::size_t size,
+ Params ¶ms) {
+ std::set cache;
+ split(data, data + size, '&', [&](const char *b, const char *e) {
+ std::string kv(b, e);
+ if (cache.find(kv) != cache.end()) { return; }
+ cache.insert(std::move(kv));
+
+ std::string key;
+ std::string val;
+ divide(b, static_cast(e - b), '=',
+ [&](const char *lhs_data, std::size_t lhs_size, const char *rhs_data,
+ std::size_t rhs_size) {
+ key.assign(lhs_data, lhs_size);
+ val.assign(rhs_data, rhs_size);
+ });
+
+ if (!key.empty()) {
+ params.emplace(decode_query_component(key), decode_query_component(val));
+ }
+ });
+}
+
+void parse_query_text(const std::string &s, Params ¶ms) {
+ parse_query_text(s.data(), s.size(), params);
+}
+
+// Normalize a query string by decoding and re-encoding each key/value pair
+// while preserving the original parameter order. This avoids double-encoding
+// and ensures consistent encoding without reordering (unlike Params which
+// uses std::multimap and sorts keys).
+std::string normalize_query_string(const std::string &query) {
+ std::string result;
+ split(query.data(), query.data() + query.size(), '&',
+ [&](const char *b, const char *e) {
+ std::string key;
+ std::string val;
+ divide(b, static_cast(e - b), '=',
+ [&](const char *lhs_data, std::size_t lhs_size,
+ const char *rhs_data, std::size_t rhs_size) {
+ key.assign(lhs_data, lhs_size);
+ val.assign(rhs_data, rhs_size);
+ });
+
+ if (!key.empty()) {
+ auto dec_key = decode_query_component(key);
+ auto dec_val = decode_query_component(val);
+
+ if (!result.empty()) { result += '&'; }
+ result += encode_query_component(dec_key);
+ if (!val.empty() || std::find(b, e, '=') != e) {
+ result += '=';
+ result += encode_query_component(dec_val);
+ }
+ }
+ });
+ return result;
+}
+
+bool parse_multipart_boundary(const std::string &content_type,
+ std::string &boundary) {
+ std::map params;
+ extract_media_type(content_type, ¶ms);
+ auto it = params.find("boundary");
+ if (it == params.end()) { return false; }
+ boundary = it->second;
+ return !boundary.empty();
+}
+
+void parse_disposition_params(const std::string &s, Params ¶ms) {
+ std::set cache;
+ split(s.data(), s.data() + s.size(), ';', [&](const char *b, const char *e) {
+ std::string kv(b, e);
+ if (cache.find(kv) != cache.end()) { return; }
+ cache.insert(kv);
+
+ std::string key;
+ std::string val;
+ split(b, e, '=', [&](const char *b2, const char *e2) {
+ if (key.empty()) {
+ key.assign(b2, e2);
+ } else {
+ val.assign(b2, e2);
+ }
+ });
+
+ if (!key.empty()) {
+ params.emplace(trim_double_quotes_copy((key)),
+ trim_double_quotes_copy((val)));
+ }
+ });
+}
+
+#ifdef CPPHTTPLIB_NO_EXCEPTIONS
+bool parse_range_header(const std::string &s, Ranges &ranges) {
+#else
+bool parse_range_header(const std::string &s, Ranges &ranges) try {
+#endif
+ auto is_valid = [](const std::string &str) {
+ return std::all_of(str.cbegin(), str.cend(),
+ [](unsigned char c) { return std::isdigit(c); });
+ };
+
+ if (s.size() > 7 && s.compare(0, 6, "bytes=") == 0) {
+ const auto pos = static_cast