Skip to content

Commit 95dfa3e

Browse files
authored
Merge branch 'main' into rework-document-tree
2 parents 25a547a + 71cb002 commit 95dfa3e

1 file changed

Lines changed: 140 additions & 42 deletions

File tree

src/odr/http_server.cpp

Lines changed: 140 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -8,46 +8,111 @@
88

99
#include <httplib/httplib.h>
1010

11+
#include <atomic>
12+
#include <memory>
13+
#include <sstream>
14+
1115
namespace odr {
1216

1317
class HttpServer::Impl {
1418
public:
1519
Impl(Config config, std::shared_ptr<Logger> logger)
16-
: m_config{std::move(config)}, m_logger{std::move(logger)} {
17-
m_server.Get("/",
18-
[](const httplib::Request & /*req*/, httplib::Response &res) {
19-
res.set_content("Hello World!", "text/plain");
20-
});
21-
22-
m_server.Get("/file/" + std::string(prefix_pattern),
23-
[&](const httplib::Request &req, httplib::Response &res) {
24-
serve_file(req, res);
25-
});
26-
m_server.Get("/file/" + std::string(prefix_pattern) + "/(.*)",
27-
[&](const httplib::Request &req, httplib::Response &res) {
28-
serve_file(req, res);
29-
});
20+
: m_config{std::move(config)}, m_logger{std::move(logger)},
21+
m_server{std::make_unique<httplib::Server>()} {
22+
// Set up exception handler to catch any internal httplib exceptions.
23+
// This prevents crashes when exceptions occur during request processing.
24+
m_server->set_exception_handler([this](const httplib::Request & /*req*/,
25+
httplib::Response &res,
26+
std::exception_ptr ep) {
27+
try {
28+
if (ep) {
29+
std::rethrow_exception(ep);
30+
}
31+
} catch (const std::exception &e) {
32+
ODR_ERROR(*m_logger, "Exception in HTTP handler: " << e.what());
33+
} catch (...) {
34+
ODR_ERROR(*m_logger, "Unknown exception in HTTP handler");
35+
}
36+
res.status = 500;
37+
res.set_content("Internal Server Error", "text/plain");
38+
});
39+
40+
m_server->Get("/",
41+
[](const httplib::Request & /*req*/, httplib::Response &res) {
42+
res.set_content("Hello World!", "text/plain");
43+
});
44+
45+
m_server->Get("/file/" + std::string(prefix_pattern),
46+
[this](const httplib::Request &req, httplib::Response &res) {
47+
if (m_stopping.load(std::memory_order_acquire)) {
48+
res.status = 503;
49+
res.set_content("Service Unavailable", "text/plain");
50+
return;
51+
}
52+
serve_file(req, res);
53+
});
54+
m_server->Get("/file/" + std::string(prefix_pattern) + "/(.*)",
55+
[this](const httplib::Request &req, httplib::Response &res) {
56+
if (m_stopping.load(std::memory_order_acquire)) {
57+
res.status = 503;
58+
res.set_content("Service Unavailable", "text/plain");
59+
return;
60+
}
61+
serve_file(req, res);
62+
});
63+
}
64+
65+
~Impl() {
66+
// Ensure the server is properly stopped before destruction.
67+
// This prevents crashes from worker threads accessing freed memory.
68+
//
69+
// IMPORTANT: We must fully stop and destroy the server BEFORE
70+
// m_content is destroyed. httplib's thread pool threads may still
71+
// be running after stop() returns - they only fully stop when the
72+
// Server destructor joins them. By explicitly destroying the server
73+
// here (via unique_ptr::reset), we ensure all threads are joined
74+
// before any other members are destroyed.
75+
m_stopping.store(true, std::memory_order_release);
76+
if (m_server) {
77+
m_server->stop();
78+
m_server.reset(); // Destroy server, join all thread pool threads
79+
}
80+
// Now safe to let other members destruct - no threads are running
3081
}
3182

83+
// Prevent copying - the lambdas capture 'this' so copying would be unsafe
84+
Impl(const Impl &) = delete;
85+
Impl &operator=(const Impl &) = delete;
86+
3287
const Config &config() const { return m_config; }
3388

3489
const std::shared_ptr<Logger> &logger() const { return m_logger; }
3590

3691
void serve_file(const httplib::Request &req, httplib::Response &res) {
37-
std::string id = req.matches[1].str();
38-
std::string path = req.matches.size() > 1 ? req.matches[2].str() : "";
39-
40-
std::unique_lock lock{m_mutex};
41-
auto it = m_content.find(id);
42-
if (it == m_content.end()) {
43-
ODR_ERROR(*m_logger, "Content not found for ID: " << id);
44-
res.status = 404;
45-
return;
92+
try {
93+
std::string id = req.matches[1].str();
94+
std::string path = req.matches.size() > 1 ? req.matches[2].str() : "";
95+
96+
std::unique_lock lock{m_mutex};
97+
auto it = m_content.find(id);
98+
if (it == m_content.end()) {
99+
ODR_ERROR(*m_logger, "Content not found for ID: " << id);
100+
res.status = 404;
101+
return;
102+
}
103+
auto [_, service] = it->second;
104+
lock.unlock();
105+
106+
serve_file(res, service, path);
107+
} catch (const std::exception &e) {
108+
ODR_ERROR(*m_logger, "Error handling request: " << e.what());
109+
res.status = 500;
110+
res.set_content("Internal Server Error", "text/plain");
111+
} catch (...) {
112+
ODR_ERROR(*m_logger, "Unknown error handling request");
113+
res.status = 500;
114+
res.set_content("Internal Server Error", "text/plain");
46115
}
47-
auto [_, service] = it->second;
48-
lock.unlock();
49-
50-
serve_file(res, service, path);
51116
}
52117

53118
void serve_file(httplib::Response &res, const HtmlService &service,
@@ -60,17 +125,27 @@ class HttpServer::Impl {
60125

61126
ODR_VERBOSE(*m_logger, "Serving file: " << path);
62127

63-
httplib::ContentProviderWithoutLength content_provider =
64-
[service = service, path = path](const std::size_t offset,
65-
httplib::DataSink &sink) -> bool {
66-
if (offset != 0) {
67-
throw std::runtime_error("Invalid offset: " + std::to_string(offset) +
68-
". Must be 0.");
69-
}
70-
service.write(path, sink.os);
71-
return false;
72-
};
73-
res.set_content_provider(service.mimetype(path), content_provider);
128+
// Buffer content to avoid streaming issues on Android.
129+
// Using ContentProviderWithoutLength (chunked transfer encoding) can cause
130+
// SIGSEGV crashes in httplib::Server::write_response_core when:
131+
// 1. The client disconnects during transfer
132+
// 2. Exceptions are thrown during content generation
133+
// 3. The server is stopped while requests are in-flight
134+
// By buffering content first, we can handle errors gracefully and use
135+
// Content-Length based responses which are more reliable.
136+
try {
137+
std::ostringstream buffer;
138+
service.write(path, buffer);
139+
res.set_content(buffer.str(), service.mimetype(path));
140+
} catch (const std::exception &e) {
141+
ODR_ERROR(*m_logger, "Error serving file " << path << ": " << e.what());
142+
res.status = 500;
143+
res.set_content("Internal Server Error", "text/plain");
144+
} catch (...) {
145+
ODR_ERROR(*m_logger, "Unknown error serving file: " << path);
146+
res.status = 500;
147+
res.set_content("Internal Server Error", "text/plain");
148+
}
74149
}
75150

76151
void connect_service(HtmlService service, const std::string &prefix) {
@@ -88,7 +163,7 @@ class HttpServer::Impl {
88163
void listen(const std::string &host, const std::uint32_t port) {
89164
ODR_VERBOSE(*m_logger, "Listening on " << host << ":" << port);
90165

91-
m_server.listen(host, static_cast<int>(port));
166+
m_server->listen(host, static_cast<int>(port));
92167
}
93168

94169
void clear() {
@@ -107,17 +182,32 @@ class HttpServer::Impl {
107182
void stop() {
108183
ODR_VERBOSE(*m_logger, "Stopping HTTP server...");
109184

110-
clear();
185+
// Set stopping flag first to reject new requests immediately.
186+
// This prevents new requests from starting while we're shutting down.
187+
m_stopping.store(true, std::memory_order_release);
188+
189+
if (m_server) {
190+
// Stop the server to prevent new connections.
191+
// Note: httplib::Server::stop() signals shutdown but thread pool
192+
// threads may still be running. They only fully stop when the
193+
// Server is destroyed. For explicit stop() calls (not destructor),
194+
// we destroy the server here to ensure threads are joined.
195+
m_server->stop();
196+
m_server.reset(); // Destroy server, join all thread pool threads
197+
}
111198

112-
m_server.stop();
199+
// Clear content after server is fully destroyed to avoid use-after-free.
200+
clear();
113201
}
114202

115203
private:
116204
Config m_config;
117205

118206
std::shared_ptr<Logger> m_logger;
119207

120-
httplib::Server m_server;
208+
// Flag to indicate server is shutting down - checked by handlers
209+
// to reject new requests during shutdown.
210+
std::atomic<bool> m_stopping{false};
121211

122212
struct Content {
123213
std::string id;
@@ -126,6 +216,14 @@ class HttpServer::Impl {
126216

127217
std::mutex m_mutex;
128218
std::unordered_map<std::string, Content> m_content;
219+
220+
// IMPORTANT: m_server is declared LAST and as unique_ptr so we can
221+
// explicitly destroy it in the destructor BEFORE other members.
222+
// httplib's Server destructor joins thread pool threads, so we must
223+
// ensure threads are fully stopped before m_content is destroyed.
224+
// Using unique_ptr allows us to call reset() to trigger destruction
225+
// at a controlled point in the destructor.
226+
std::unique_ptr<httplib::Server> m_server;
129227
};
130228

131229
HttpServer::HttpServer(const Config &config, std::shared_ptr<Logger> logger)

0 commit comments

Comments
 (0)