88
99#include < httplib/httplib.h>
1010
11+ #include < atomic>
12+ #include < memory>
13+ #include < sstream>
14+
1115namespace odr {
1216
1317class HttpServer ::Impl {
1418public:
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
115203private:
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
131229HttpServer::HttpServer (const Config &config, std::shared_ptr<Logger> logger)
0 commit comments