FastLED 3.10.8
Loading...
Searching...
No Matches
server.cpp.hpp
Go to the documentation of this file.
1
3
4#pragma once
5
7#include "fl/stl/stdio.h" // fl::snprintf — avoids _svfprintf_r (#2773 item 1.1)
8#include "fl/stl/atomic.h"
9#include "fl/task/executor.h"
10#include "platforms/esp/is_esp.h" // ok platform headers - for FL_IS_ESP32 // IWYU pragma: keep
11
12// Common includes needed by both POSIX/Windows and ESP32 implementations
13#include "fl/stl/cctype.h"
14#include "fl/stl/cstring.h"
15#include "fl/stl/limits.h"
16#include "fl/stl/malloc.h"
17#include "fl/log/log.h"
18
19// ESP32 HTTP server header (must be before namespace declarations)
20// Requires IDF 4.0+ for HTTPD_500_INTERNAL_SERVER_ERROR, httpd_resp_send_err, etc.
21#include "platforms/esp/esp_version.h" // ok platform headers // IWYU pragma: keep
22#if defined(FL_IS_ESP32) && !defined(FASTLED_HAS_NETWORKING) && ESP_IDF_VERSION_4_OR_HIGHER
23#include "fl/stl/singleton.h" // for Singleton
24// IWYU pragma: begin_keep
25#include <esp_http_server.h>
26// IWYU pragma: end_keep
27#endif
28
29#ifdef FASTLED_HAS_NETWORKING
30
31#include "platforms/time_platform.h"
32
33// Platform-specific socket includes
34#ifdef FL_IS_WIN
35 #include "platforms/win/socket_win.h" // ok platform headers // IWYU pragma: keep
36 #define SOCKET_ERROR_WOULD_BLOCK WSAEWOULDBLOCK
37 #define SOCKET_ERROR_IN_PROGRESS WSAEINPROGRESS
38#else
39 #include "platforms/posix/socket_posix.h" // ok platform headers (includes all system socket headers) // IWYU pragma: keep
40#include "fl/stl/noexcept.h"
41 #define SOCKET_ERROR_WOULD_BLOCK EWOULDBLOCK
42 #define SOCKET_ERROR_IN_PROGRESS EINPROGRESS
43#endif
44
45namespace fl {
46namespace asio {
47namespace http {
48
49// ========== Async System Integration ==========
50
54class Server::ServerAsyncRunner : public task::Runner {
55public:
56 explicit ServerAsyncRunner(Server* server) FL_NO_EXCEPT : mServer(server) {}
57
58 void update() FL_NO_EXCEPT override {
59 if (mServer) {
60 mServer->update(); // Call server's update() and discard return value
61 }
62 }
63
64 bool has_active_tasks() const FL_NO_EXCEPT override {
65 return mServer && mServer->is_running();
66 }
67
68 size_t active_task_count() const FL_NO_EXCEPT override {
69 return (mServer && mServer->is_running()) ? mServer->mClientSockets.size() : 0;
70 }
71
72private:
73 Server* mServer;
74};
75
76namespace {
77
78// Connection timeout (30 seconds)
79constexpr u32 CONNECTION_TIMEOUT_MS = 30000;
80
81// Helper: Case-insensitive string comparison
82bool iequals(const string& a, const string& b) FL_NO_EXCEPT {
83 if (a.size() != b.size()) return false;
84 for (size_t i = 0; i < a.size(); ++i) {
85 if (fl::tolower(a[i]) != fl::tolower(b[i])) return false;
86 }
87 return true;
88}
89
90// Helper: Trim whitespace from string
91string trim(const string& s) FL_NO_EXCEPT {
92 size_t start = 0;
93 while (start < s.size() && fl::isspace(s[start])) ++start;
94 size_t end = s.size();
95 while (end > start && fl::isspace(s[end - 1])) --end;
96 return s.substr(start, end - start);
97}
98
99// Helper: Split string by delimiter
100vector<string> split(const string& s, char delimiter) FL_NO_EXCEPT {
101 vector<string> tokens;
102 size_t start = 0;
103 size_t end = s.find(delimiter);
104 while (end != string::npos) {
105 tokens.push_back(s.substr(start, end - start));
106 start = end + 1;
107 end = s.find(delimiter, start);
108 }
109 tokens.push_back(s.substr(start));
110 return tokens;
111}
112
113// Helper: Parse query string (e.g., "?id=123&name=test")
114map<string, string> parse_query_string(const string& query) FL_NO_EXCEPT {
115 map<string, string> params;
116 if (query.empty() || query[0] != '?') return params;
117
118 vector<string> pairs = split(query.substr(1), '&');
119 for (const auto& pair : pairs) {
120 size_t eq_pos = pair.find('=');
121 if (eq_pos != string::npos) {
122 string key = pair.substr(0, eq_pos);
123 string value = pair.substr(eq_pos + 1);
124 params[key] = value;
125 }
126 }
127 return params;
128}
129
130// Helper: Get HTTP status text
131const char* status_text(int code) FL_NO_EXCEPT {
132 switch (code) {
133 case 200: return "OK";
134 case 400: return "Bad Request";
135 case 404: return "Not Found";
136 case 500: return "Internal Server Error";
137 default: return "Unknown";
138 }
139}
140
141// Helper: Set socket to non-blocking mode
142bool set_nonblocking(int fd) FL_NO_EXCEPT {
143#ifdef FL_IS_WIN
144 u_long mode = 1;
145 return ioctlsocket(fd, FIONBIO, &mode) == 0;
146#else
147 int flags = fcntl(fd, F_GETFL, 0);
148 if (flags == -1) return false;
149 return fcntl(fd, F_SETFL, flags | O_NONBLOCK) != -1;
150#endif
151}
152
153} // anonymous namespace
154
155//==============================================================================
156// Request implementation
157//==============================================================================
158
159optional<string> Request::header(const string& name) const FL_NO_EXCEPT {
160 for (auto it = mHeaders.begin(); it != mHeaders.end(); ++it) {
161 if (iequals(it->first, name)) {
162 return it->second;
163 }
164 }
165 return nullopt;
166}
167
168optional<string> Request::query(const string& param) const FL_NO_EXCEPT {
169 auto it = mQuery.find(param);
170 if (it != mQuery.end()) {
171 return it->second;
172 }
173 return nullopt;
174}
175
176//==============================================================================
177// Response implementation
178//==============================================================================
179
181 mHeaders["Content-Type"] = "text/plain";
182}
183
185 mStatusCode = code;
186 return *this;
187}
188
189Response& Response::header(const string& name, const string& value) FL_NO_EXCEPT {
190 mHeaders[name] = value;
191 return *this;
192}
193
194Response& Response::body(const string& content) FL_NO_EXCEPT {
195 mBody = content;
196 return *this;
197}
198
199Response& Response::json(const class json& data) FL_NO_EXCEPT {
200 mBody = data.to_string();
201 mHeaders["Content-Type"] = "application/json";
202 return *this;
203}
204
205Response Response::ok(const string& body) FL_NO_EXCEPT {
206 Response resp;
207 resp.status(200).body(body);
208 return resp;
209}
210
212 Response resp;
213 resp.status(404).body("Not Found\n");
214 return resp;
215}
216
217Response Response::bad_request(const string& message) FL_NO_EXCEPT {
218 Response resp;
219 resp.status(400).body(message + "\n");
220 return resp;
221}
222
223Response Response::internal_error(const string& message) FL_NO_EXCEPT {
224 Response resp;
225 resp.status(500).body(message + "\n");
226 return resp;
227}
228
229string Response::to_string() const FL_NO_EXCEPT {
230 string result;
231
232 // Status line
233 result += "HTTP/1.0 ";
235 result += " ";
236 result += status_text(mStatusCode);
237 result += "\r\n";
238
239 // Headers
240 for (auto it = mHeaders.begin(); it != mHeaders.end(); ++it) {
241 result += it->first + ": " + it->second + "\r\n";
242 }
243
244 // Content-Length (auto-calculated)
245 result += "Content-Length: " + fl::to_string(mBody.size()) + "\r\n";
246
247 // End of headers
248 result += "\r\n";
249
250 // Body
251 result += mBody;
252
253 return result;
254}
255
256//==============================================================================
257// HttpServer implementation
258//==============================================================================
259
261 // Register as engine event listener for automatic cleanup on exit
263}
264
266 // Unregister from engine events
268 stop();
269}
270
272 // Automatically stop server on engine shutdown
273 stop();
274}
275
276bool Server::start(int port) FL_NO_EXCEPT {
277 if (mRunning) {
278 mLastError = "Server already running";
279 return false;
280 }
281
282 if (!setup_listen_socket(port)) {
283 return false;
284 }
285
286 mPort = port;
287 mRunning = true;
288 mLastError.clear();
289
290 // Register with async system for automatic updates
291 if (!mAsyncRunner) {
292 mAsyncRunner = fl::make_unique<ServerAsyncRunner>(this);
293 task::Executor::instance().register_runner(mAsyncRunner.get());
294 }
295
296 return true;
297}
298
300 if (!mRunning) return;
301
302 // Unregister from async system
303 if (mAsyncRunner) {
305 mAsyncRunner.reset();
306 }
307
308 // Close all client connections
309 for (auto& client : mClientSockets) {
310 if (client.fd != -1) {
311 close(client.fd);
312 }
313 }
314 mClientSockets.clear();
315
316 // Close listen socket
317 if (mListenSocket != -1) {
318 close(mListenSocket);
319 mListenSocket = -1;
320 }
321
322 // Free route handlers to prevent leaks when server lives in a shared library
323 // (LSAN runs before shared library static destructors)
324 mRoutes.clear();
325
326 mRunning = false;
327}
328
329void Server::route(const string& method, const string& path, RouteHandler handler) FL_NO_EXCEPT {
330 mRoutes.push_back({method, path, handler});
331}
332
333void Server::get(const string& path, RouteHandler handler) FL_NO_EXCEPT {
334 route("GET", path, handler);
335}
336
337void Server::post(const string& path, RouteHandler handler) FL_NO_EXCEPT {
338 route("POST", path, handler);
339}
340
341void Server::put(const string& path, RouteHandler handler) FL_NO_EXCEPT {
342 route("PUT", path, handler);
343}
344
345void Server::del(const string& path, RouteHandler handler) FL_NO_EXCEPT {
346 route("DELETE", path, handler);
347}
348
350 if (!mRunning) return 0;
351
354 return process_requests();
355}
356
358 // Create socket (initialization handled by socket wrapper)
359 mListenSocket = socket(AF_INET, SOCK_STREAM, 0);
360 if (mListenSocket < 0) {
361 mLastError = "Failed to create socket";
362 return false;
363 }
364
365 // Set socket options
366 int opt = 1;
367 if (setsockopt(mListenSocket, SOL_SOCKET, SO_REUSEADDR,
368 (const char*)(&opt), sizeof(opt)) < 0) {
369 close(mListenSocket);
370 mListenSocket = -1;
371 mLastError = "Failed to set SO_REUSEADDR";
372 return false;
373 }
374
375 // Set non-blocking
376 if (!set_nonblocking(mListenSocket)) {
377 close(mListenSocket);
378 mListenSocket = -1;
379 mLastError = "Failed to set non-blocking mode";
380 return false;
381 }
382
383 // Bind socket
384 sockaddr_in addr{};
385 addr.sin_family = AF_INET;
386 addr.sin_addr.s_addr = INADDR_ANY;
387 addr.sin_port = htons(static_cast<u16>(port));
388
389 if (bind(mListenSocket, (sockaddr*)(&addr), sizeof(addr)) < 0) {
390 close(mListenSocket);
391 mListenSocket = -1;
392 mLastError = "Failed to bind to port";
393 return false;
394 }
395
396 // Listen
397 if (listen(mListenSocket, 5) < 0) {
398 close(mListenSocket);
399 mListenSocket = -1;
400 mLastError = "Failed to listen on socket";
401 return false;
402 }
403
404 return true;
405}
406
408 sockaddr_in client_addr{};
409 socklen_t addr_len = sizeof(client_addr);
410
411 // Accept all pending connections (non-blocking)
412 while (true) {
413 int client_fd = accept(mListenSocket,
414 (sockaddr*)(&client_addr),
415 &addr_len);
416
417 if (client_fd < 0) {
418#ifdef FL_IS_WIN
419 if (WSAGetLastError() == SOCKET_ERROR_WOULD_BLOCK) break;
420#else
421 if (errno == EWOULDBLOCK || errno == EAGAIN) break;
422#endif
423 // Other error - log and continue
424 break;
425 }
426
427 // Set client socket to non-blocking
428 if (!set_nonblocking(client_fd)) {
429 close(client_fd);
430 continue;
431 }
432
433 // Add to client list
434 ClientConnection client;
435 client.fd = client_fd;
436 client.connect_time = fl::platforms::millis();
437 mClientSockets.push_back(client);
438 }
439}
440
442 size_t requests_processed = 0;
443
444 // Process all clients (iterate backwards to allow removal)
445 for (size_t i = mClientSockets.size(); i > 0; --i) {
446 size_t index = i - 1;
447 ClientConnection& client = mClientSockets[index];
448
449 // Try to read request
450 optional<Request> req = read_request(client);
451 if (!req) {
452 continue; // No complete request yet
453 }
454
455 // Find handler
456 optional<RouteHandler> handler = find_handler(req->method(), req->path());
457
458 Response resp;
459 if (handler) {
460 // Call handler
461 resp = (*handler)(*req);
462 } else {
463 // No handler found - 404
464 resp = Response::not_found();
465 }
466
467 // Send response
468 send_response(client.fd, resp);
469
470 // Close connection (HTTP/1.0 - no keep-alive)
471 close_client(index);
472
473 requests_processed++;
474 }
475
476 return requests_processed;
477}
478
479optional<Request> Server::read_request(ClientConnection& client) FL_NO_EXCEPT {
480 // Read data from socket
481 char buffer[4096];
482
483#ifdef FL_IS_WIN
484 int bytes = recv(client.fd, buffer, sizeof(buffer), 0);
485 if (bytes <= 0) {
486 if (WSAGetLastError() != SOCKET_ERROR_WOULD_BLOCK) {
487 return nullopt; // Connection closed or error
488 }
489 return nullopt; // No data yet
490 }
491#else
492 ssize_t bytes = recv(client.fd, buffer, sizeof(buffer), MSG_DONTWAIT);
493 if (bytes <= 0) {
494 if (errno != EWOULDBLOCK && errno != EAGAIN) {
495 return nullopt; // Connection closed or error
496 }
497 return nullopt; // No data yet
498 }
499#endif
500
501 client.buffer.append(buffer, static_cast<size_t>(bytes));
502
503 // Check if we have complete HTTP request (ends with \r\n\r\n)
504 size_t header_end = client.buffer.find("\r\n\r\n");
505 if (header_end == string::npos) {
506 return nullopt; // Headers not complete yet
507 }
508
509 // Parse request
510 Request req;
511
512 // Split into lines
513 string header_section = client.buffer.substr(0, header_end);
514 vector<string> lines = split(header_section, '\n');
515
516 if (lines.empty()) {
517 return nullopt;
518 }
519
520 // Parse request line: "GET /path HTTP/1.1\r"
521 string request_line = trim(lines[0]);
522 vector<string> parts = split(request_line, ' ');
523 if (parts.size() < 3) {
524 return nullopt;
525 }
526
527 req.mMethod = parts[0];
528
529 // Split path and query string
530 string full_path = parts[1];
531 size_t query_pos = full_path.find('?');
532 if (query_pos != string::npos) {
533 req.mPath = full_path.substr(0, query_pos);
534 req.mQuery = parse_query_string(full_path.substr(query_pos));
535 } else {
536 req.mPath = full_path;
537 }
538
539 req.mHttpVersion = parts[2];
540
541 // Parse headers
542 for (size_t i = 1; i < lines.size(); ++i) {
543 string line = trim(lines[i]);
544 if (line.empty()) continue;
545
546 size_t colon_pos = line.find(':');
547 if (colon_pos != string::npos) {
548 string name = trim(line.substr(0, colon_pos));
549 string value = trim(line.substr(colon_pos + 1));
550 req.mHeaders[name] = value;
551 }
552 }
553
554 // Read body if Content-Length present
555 optional<string> content_length = req.header("Content-Length");
556 if (content_length) {
557 int body_len = atoi(content_length->c_str());
558 if (body_len > 0) {
559 size_t body_start = header_end + 4;
560 size_t total_needed = body_start + static_cast<size_t>(body_len);
561
562 if (client.buffer.size() < total_needed) {
563 return nullopt; // Body not complete yet
564 }
565
566 req.mBody = client.buffer.substr(body_start, static_cast<size_t>(body_len));
567 }
568 }
569
570 return req;
571}
572
573bool Server::send_response(int client_fd, const Response& response) FL_NO_EXCEPT {
574 string data = response.to_string();
575 const char* ptr = data.c_str();
576 size_t remaining = data.size();
577
578 while (remaining > 0) {
579#ifdef FL_IS_WIN
580 int sent = send(client_fd, ptr, static_cast<int>(remaining), 0);
581#else
582 ssize_t sent = send(client_fd, ptr, remaining, 0);
583#endif
584
585 if (sent <= 0) {
586 return false;
587 }
588
589 ptr += sent;
590 remaining -= static_cast<size_t>(sent);
591 }
592
593 return true;
594}
595
596optional<RouteHandler> Server::find_handler(const string& method, const string& path) const FL_NO_EXCEPT {
597 for (const auto& entry : mRoutes) {
598 if (entry.method == method && entry.path == path) {
599 return entry.handler;
600 }
601 }
602 return nullopt;
603}
604
605void Server::close_client(size_t index) FL_NO_EXCEPT {
606 if (index >= mClientSockets.size()) return;
607
608 close(mClientSockets[index].fd);
609 mClientSockets.erase(mClientSockets.begin() + static_cast<ptrdiff_t>(index));
610}
611
613 u32 now = fl::platforms::millis();
614
615 for (size_t i = mClientSockets.size(); i > 0; --i) {
616 size_t index = i - 1;
617 const ClientConnection& client = mClientSockets[index];
618
619 if (now - client.connect_time > CONNECTION_TIMEOUT_MS) {
620 close_client(index);
621 }
622 }
623}
624
625} // namespace http
626} // namespace asio
627} // namespace fl
628
629#elif defined(FL_IS_ESP32) && ESP_IDF_VERSION_4_OR_HIGHER
630
631// ============================================================================
632// ESP32 Implementation using esp_http_server.h (IDF 4.0+)
633// ============================================================================
634
635namespace fl {
636namespace asio {
637namespace http {
638
639// ========== Main-task request marshaling (FastLED#3588) ==========
640//
641// Route handlers used to execute ON the esp_http_server task,
642// concurrently with the main loop's fl:: object churn - reproducibly
643// corrupting main-task heap objects on classic ESP32 under load.
644// Requests are now marshaled: the httpd task extracts plain-C request
645// data, queues it, and blocks on a semaphore; the main task (via the
646// task::Executor pump) runs the fl:: handler and hands back a
647// heap-allocated Response. No fl:: object is ever constructed or
648// mutated off the main task.
649
650namespace {
651
652struct EspPendingRequest {
653 Server* server;
654 fl::size route_index;
655 char method[8];
656 char uri[160];
657 char* body; // fl::malloc'd raw body (may be null)
658 fl::size body_len;
659 Response* response; // filled by the main task
660 SemaphoreHandle_t done;
661 // Ownership handshake (the original stack-allocated design left a
662 // DANGLING pointer in the queue when the 5 s handler wait timed
663 // out — the runner then wrote through freed httpd stack memory).
664 // 0 = queued, 1 = runner processing, 3 = abandoned by httpd.
666};
667
668void espRequestCleanup(EspPendingRequest* item) {
669 if (!item) return;
670 if (item->response) {
671 delete item->response; // ok bare allocation - see marshaling protocol
672 }
673 if (item->body) {
674 fl::free(item->body);
675 }
676 if (item->done) {
677 vSemaphoreDelete(item->done);
678 }
679 delete item; // ok bare allocation - see marshaling protocol
680}
681
682// FL_LINT_ALLOW_GLOBAL(constant-initialized ESP-IDF queue handle; Singleton<T> adds pointer storage and a branch without improving linker elision)
683QueueHandle_t s_esp_request_queue = nullptr;
684
685} // anonymous namespace
686
688class Server::ServerAsyncRunner : public task::Runner {
689public:
690 explicit ServerAsyncRunner(Server* server) : mServer(server) {}
691
692 void update() FL_NO_EXCEPT override {
693 if (!s_esp_request_queue) {
694 return;
695 }
696 EspPendingRequest* item = nullptr;
697 while (xQueueReceive(s_esp_request_queue, &item, 0) == pdTRUE) {
698 if (!item) {
699 continue;
700 }
701 int expected = 0;
702 if (!item->state.compare_exchange_strong(expected, 1)) {
703 // httpd side abandoned this request (its wait timed
704 // out) — we own the cleanup.
705 espRequestCleanup(item);
706 continue;
707 }
708 Request fl_req;
709 fl_req.mMethod = item->method;
710 fl_req.mPath = item->uri;
711 fl_req.mHttpVersion = "HTTP/1.1";
712 if (item->body && item->body_len > 0) {
713 fl_req.mBody = string(item->body, item->body_len);
714 }
715 Response* resp = new Response(); // ok bare allocation - freed via espRequestCleanup
716 if (item->server && item->route_index < item->server->mRoutes.size()) {
717 *resp = item->server->mRoutes[item->route_index].handler(fl_req);
718 } else {
719 *resp = Response::not_found();
720 }
721 item->response = resp;
722 xSemaphoreGive(item->done);
723 }
724 }
725
726 bool has_active_tasks() const FL_NO_EXCEPT override {
727 return mServer && mServer->is_running();
728 }
729
730 size_t active_task_count() const FL_NO_EXCEPT override {
731 return (mServer && mServer->is_running()) ? 1 : 0;
732 }
733
734private:
735 Server* mServer;
736};
737
738// ========== ESP32 Helpers ==========
739
740namespace {
741
742// Map method string to httpd_method_t
743httpd_method_t to_httpd_method(const string& method) {
744 if (method == "GET") return HTTP_GET;
745 if (method == "POST") return HTTP_POST;
746 if (method == "PUT") return HTTP_PUT;
747 if (method == "DELETE") return HTTP_DELETE;
748 return HTTP_GET;
749}
750
751// Context passed to ESP-IDF URI handler callback
752struct EspRouteContext {
753 Server* server;
754 size_t route_index;
755};
756
757// Forward declaration - implemented as Server static method for private access
758esp_err_t esp_route_handler(httpd_req_t* req) FL_NO_EXCEPT;
759
760} // anonymous namespace
761
762// Static method on Server - has access to private members of Request/Response.
763// Signature uses void* to avoid ESP-IDF types in header; cast to httpd_req_t* here.
764// This is called from the ESP-IDF HTTP server task.
765int Server::handle_esp_request(void* raw_req) FL_NO_EXCEPT {
766 // Runs on the esp_http_server task. NO fl:: objects here - only
767 // plain C extraction; the fl:: handler executes on the main task
768 // (see ServerAsyncRunner::update, FastLED#3588).
769 auto* req = static_cast<httpd_req_t*>(raw_req);
770 auto* ctx = static_cast<EspRouteContext*>(req->user_ctx);
771 if (!ctx || !ctx->server || !s_esp_request_queue) {
772 httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "No context");
773 return ESP_FAIL;
774 }
775
776 EspPendingRequest* item = new EspPendingRequest(); // ok bare allocation - freed via espRequestCleanup by whichever side owns it last
777 item->server = ctx->server;
778 item->route_index = ctx->route_index;
779 item->body = nullptr;
780 item->body_len = 0;
781 item->response = nullptr;
782 item->method[0] = '\0';
783 item->uri[0] = '\0';
784 item->state.store(0);
785 switch (req->method) {
786 case HTTP_POST: fl::strncpy(item->method, "POST", sizeof(item->method) - 1); break;
787 case HTTP_PUT: fl::strncpy(item->method, "PUT", sizeof(item->method) - 1); break;
788 case HTTP_DELETE: fl::strncpy(item->method, "DELETE", sizeof(item->method) - 1); break;
789 default: fl::strncpy(item->method, "GET", sizeof(item->method) - 1); break;
790 }
791 fl::strncpy(item->uri, req->uri, sizeof(item->uri) - 1);
792 // Strip query string from the path
793 for (char* c = item->uri; *c; ++c) {
794 if (*c == '?') { *c = '\0'; break; }
795 }
796
797 if (req->content_len > 0 && req->content_len <= 8192) {
798 int total_len = static_cast<int>(req->content_len);
799 char* buf = static_cast<char*>(fl::malloc(static_cast<fl::size>(total_len) + 1));
800 if (buf) {
801 int received = 0;
802 while (received < total_len) {
803 int ret = httpd_req_recv(req, buf + received, total_len - received);
804 if (ret <= 0) {
805 break;
806 }
807 received += ret;
808 }
809 buf[received] = '\0';
810 item->body = buf;
811 item->body_len = static_cast<fl::size>(received);
812 }
813 }
814
815 item->done = xSemaphoreCreateBinary();
816 if (!item->done) {
817 espRequestCleanup(item);
818 httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "No memory");
819 return ESP_FAIL;
820 }
821
822 if (xQueueSend(s_esp_request_queue, &item, pdMS_TO_TICKS(100)) != pdTRUE) {
823 espRequestCleanup(item); // never queued — we own it
824 httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Server busy");
825 return ESP_FAIL;
826 }
827
828 esp_err_t result = ESP_OK;
829 if (xSemaphoreTake(item->done, pdMS_TO_TICKS(5000)) != pdTRUE) {
830 int expected = 0;
831 if (item->state.compare_exchange_strong(expected, 3)) {
832 // Runner has not started: it will see 'abandoned' and clean
833 // up. We must NOT free — the pointer is still in the queue.
834 httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Main loop busy");
835 return ESP_FAIL;
836 }
837 // Runner won the race and is (or was) processing — wait for its
838 // give, then fall through and serve normally.
839 (void)xSemaphoreTake(item->done, portMAX_DELAY);
840 }
841
842 if (item->response) {
843 Response* resp = item->response;
844 char status_str[32];
845 fl::snprintf(status_str, sizeof(status_str), "%d", resp->mStatusCode);
846 httpd_resp_set_status(req, status_str);
847 for (auto it = resp->mHeaders.begin(); it != resp->mHeaders.end(); ++it) {
848 httpd_resp_set_hdr(req, it->first.c_str(), it->second.c_str());
849 }
850 httpd_resp_send(req, resp->mBody.c_str(), resp->mBody.size());
851 } else {
852 httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "No response");
853 result = ESP_FAIL;
854 }
855 espRequestCleanup(item); // served — we own the cleanup
856 return result;
857}
858
859namespace {
860
861// ESP-IDF URI handler callback - delegates to Server::handle_esp_request
862esp_err_t esp_route_handler(httpd_req_t* req) FL_NO_EXCEPT {
863 return static_cast<esp_err_t>(Server::handle_esp_request(static_cast<void*>(req)));
864}
865
866} // anonymous namespace
867
868// ========== Request implementation ==========
869
870optional<string> Request::header(const string& name) const FL_NO_EXCEPT {
871 for (auto it = mHeaders.begin(); it != mHeaders.end(); ++it) {
872 if (it->first == name) {
873 return it->second;
874 }
875 }
876 return nullopt;
877}
878
879optional<string> Request::query(const string& param) const FL_NO_EXCEPT {
880 auto it = mQuery.find(param);
881 if (it != mQuery.end()) {
882 return it->second;
883 }
884 return nullopt;
885}
886
887// ========== Response implementation ==========
888
890 mHeaders["Content-Type"] = "text/plain";
891}
892
894 mStatusCode = code;
895 return *this;
896}
897
898Response& Response::header(const string& name, const string& value) FL_NO_EXCEPT {
899 mHeaders[name] = value;
900 return *this;
901}
902
903Response& Response::body(const string& content) FL_NO_EXCEPT {
904 mBody = content;
905 return *this;
906}
907
908Response& Response::json(const class json& data) FL_NO_EXCEPT {
909 mBody = data.to_string();
910 mHeaders["Content-Type"] = "application/json";
911 return *this;
912}
913
914Response Response::ok(const string& body) FL_NO_EXCEPT {
915 Response resp;
916 resp.status(200).body(body);
917 return resp;
918}
919
921 Response resp;
922 resp.status(404).body("Not Found\n");
923 return resp;
924}
925
926Response Response::bad_request(const string& message) FL_NO_EXCEPT {
927 Response resp;
928 resp.status(400).body(message + "\n");
929 return resp;
930}
931
932Response Response::internal_error(const string& message) FL_NO_EXCEPT {
933 Response resp;
934 resp.status(500).body(message + "\n");
935 return resp;
936}
937
938string Response::to_string() const FL_NO_EXCEPT {
939 // Not used on ESP32 (responses sent via httpd_resp_send)
940 return mBody;
941}
942
943// ========== Server implementation (ESP32) ==========
944
945// ESP32 stores httpd_handle_t and route contexts via mListenSocket as an opaque int
946// and mClientSockets is unused. We use a static map for the httpd handle.
947
948// We store the httpd_handle_t in a file-scoped variable since the Server class
949// members (mListenSocket, mClientSockets) are typed for POSIX sockets.
950// FL_LINT_ALLOW_GLOBAL(constant-initialized ESP-IDF server handle; Singleton<T> adds pointer storage and a branch without improving linker elision)
951static httpd_handle_t s_esp_httpd = nullptr;
952using EspRouteContexts = fl::vector<fl::unique_ptr<EspRouteContext>>;
953
954// Initialize ownership only when HTTP is used; retain static backing storage.
955static EspRouteContexts& espRouteContexts() FL_NO_EXCEPT {
957}
958
961}
962
965 stop();
966}
967
969 stop();
970}
971
972bool Server::start(int port) FL_NO_EXCEPT {
973 if (mRunning) {
974 mLastError = "Server already running";
975 return false;
976 }
977
978 if (!s_esp_request_queue) {
979 s_esp_request_queue = xQueueCreate(4, sizeof(EspPendingRequest*));
980 }
981 if (!mAsyncRunner) {
982 mAsyncRunner = fl::make_unique<ServerAsyncRunner>(this);
983 task::Executor::instance().register_runner(mAsyncRunner.get());
984 }
985
986 httpd_config_t config = HTTPD_DEFAULT_CONFIG();
987 config.server_port = static_cast<u16>(port);
988 if (mRoutes.size() > (fl::numeric_limits<u16>::max)()) {
989 mLastError = "Too many HTTP routes";
990 FL_WARN("[HTTP] Too many routes for ESP-IDF HTTP server");
991 task::Executor::instance().unregister_runner(mAsyncRunner.get());
992 mAsyncRunner.reset();
993 return false;
994 }
995 if (mRoutes.size() > config.max_uri_handlers) {
996 config.max_uri_handlers = static_cast<u16>(mRoutes.size());
997 }
998 // Route handlers execute ON the httpd task and run full fl::json /
999 // fl::string / Response machinery. The IDF default stack (4 KB) is
1000 // not enough for that: overflows landed in adjacent heap blocks and
1001 // corrupted unrelated main-task objects (FastLED#3588 — classic
1002 // ESP32 crashed building RPC responses while a client hammered the
1003 // server). FreeRTOS heap-allocated task stacks give no early
1004 // warning, so size generously.
1005 config.stack_size = 8192;
1006
1007 esp_err_t err = httpd_start(&s_esp_httpd, &config);
1008 if (err != ESP_OK) {
1009 mLastError = "httpd_start failed";
1010 FL_WARN("[HTTP] httpd_start failed: " << esp_err_to_name(err));
1011 task::Executor::instance().unregister_runner(mAsyncRunner.get());
1012 mAsyncRunner.reset();
1013 return false;
1014 }
1015
1016 // Register all routes that were added before start()
1017 for (size_t i = 0; i < mRoutes.size(); ++i) {
1018 auto ctx = fl::make_unique<EspRouteContext>(EspRouteContext{this, i});
1019
1020 httpd_uri_t uri_handler = {};
1021 uri_handler.uri = mRoutes[i].path.c_str();
1022 uri_handler.method = to_httpd_method(mRoutes[i].method);
1023 uri_handler.handler = esp_route_handler;
1024 uri_handler.user_ctx = ctx.get();
1025
1026 esp_err_t reg_err = httpd_register_uri_handler(s_esp_httpd, &uri_handler);
1027 if (reg_err != ESP_OK) {
1028 mLastError = "Failed to register route " + mRoutes[i].path;
1029 FL_WARN("[HTTP] Failed to register route " << mRoutes[i].path.c_str() << ": " << esp_err_to_name(reg_err));
1030 httpd_stop(s_esp_httpd);
1031 s_esp_httpd = nullptr;
1032 espRouteContexts().clear();
1033 task::Executor::instance().unregister_runner(mAsyncRunner.get());
1034 mAsyncRunner.reset();
1035 return false;
1036 }
1037 espRouteContexts().push_back(fl::move(ctx));
1038 }
1039
1040 mPort = port;
1041 mRunning = true;
1042 mLastError.clear();
1043
1044 FL_WARN("[HTTP] Server started on port " << port);
1045 return true;
1046}
1047
1049 if (!mRunning) return;
1050
1051 // Stop the httpd first so no request can be queued mid-teardown,
1052 // then drain and unregister the main-task runner (FastLED#3588).
1053 if (s_esp_httpd) {
1054 httpd_stop(s_esp_httpd);
1055 s_esp_httpd = nullptr;
1056 }
1057 if (mAsyncRunner) {
1058 mAsyncRunner->update(); // serve any request already queued
1060 mAsyncRunner.reset();
1061 }
1062
1063 // Clean up route contexts (unique_ptr auto-deletes on clear)
1064 espRouteContexts().clear();
1065
1066 // Free route handlers
1067 mRoutes.clear();
1068
1069 mRunning = false;
1070 FL_WARN("[HTTP] Server stopped");
1071}
1072
1073void Server::route(const string& method, const string& path, RouteHandler handler) FL_NO_EXCEPT {
1074 mRoutes.push_back({method, path, handler});
1075
1076 // If server is already running, register the route immediately
1077 if (mRunning && s_esp_httpd) {
1078 size_t idx = mRoutes.size() - 1;
1079 auto ctx = fl::make_unique<EspRouteContext>(EspRouteContext{this, idx});
1080
1081 httpd_uri_t uri_handler = {};
1082 uri_handler.uri = mRoutes[idx].path.c_str();
1083 uri_handler.method = to_httpd_method(method);
1084 uri_handler.handler = esp_route_handler;
1085 uri_handler.user_ctx = ctx.get();
1086
1087 httpd_register_uri_handler(s_esp_httpd, &uri_handler);
1088 espRouteContexts().push_back(fl::move(ctx));
1089 }
1090}
1091
1092void Server::get(const string& path, RouteHandler handler) FL_NO_EXCEPT {
1093 route("GET", path, handler);
1094}
1095
1096void Server::post(const string& path, RouteHandler handler) FL_NO_EXCEPT {
1097 route("POST", path, handler);
1098}
1099
1100void Server::put(const string& path, RouteHandler handler) FL_NO_EXCEPT {
1101 route("PUT", path, handler);
1102}
1103
1104void Server::del(const string& path, RouteHandler handler) FL_NO_EXCEPT {
1105 route("DELETE", path, handler);
1106}
1107
1109 // ESP-IDF HTTP server runs in its own task, no polling needed
1110 return 0;
1111}
1112
1113} // namespace http
1114} // namespace asio
1115} // namespace fl
1116
1117#else // !FASTLED_HAS_NETWORKING && !(FL_IS_ESP32 && IDF4+)
1118
1119// Stub implementation for platforms without networking (includes ESP32 IDF 3.x)
1120namespace fl {
1121namespace asio {
1122namespace http {
1123
1124// Minimal definition for unique_ptr<ServerAsyncRunner> destruction
1126
1128optional<string> Request::query(const string&) const FL_NO_EXCEPT { return nullopt; }
1129
1131Response& Response::status(int) FL_NO_EXCEPT { return *this; }
1132Response& Response::header(const string&, const string&) FL_NO_EXCEPT { return *this; }
1133Response& Response::body(const string&) FL_NO_EXCEPT { return *this; }
1134Response& Response::json(const class json&) FL_NO_EXCEPT { return *this; }
1135
1136Response Response::ok(const string&) FL_NO_EXCEPT { return Response(); }
1140
1141string Response::to_string() const FL_NO_EXCEPT { return ""; }
1142
1144Server::~Server() FL_NO_EXCEPT = default;
1146bool Server::start(int) FL_NO_EXCEPT { return false; }
1148void Server::route(const string&, const string&, RouteHandler) FL_NO_EXCEPT {}
1153size_t Server::update() FL_NO_EXCEPT { return 0; }
1154
1155} // namespace http
1156} // namespace asio
1157} // namespace fl
1158
1159#endif // FASTLED_HAS_NETWORKING
bool done
fl::HttpServer server
TestState state
fl::UICheckbox listen("Listen", true)
static void removeListener(Listener *listener) FL_NO_EXCEPT
static void addListener(Listener *listener, int priority=0) FL_NO_EXCEPT
static T & instance() FL_NO_EXCEPT
Definition singleton.h:41
optional< string > query(const string &param) const FL_NO_EXCEPT
Get query parameter value by name.
optional< string > header(const string &name) const FL_NO_EXCEPT
Get header value by name (case-insensitive)
HTTP request object (immutable, passed by const reference)
Definition server.h:48
Response & header(const string &name, const string &value) FL_NO_EXCEPT
Add HTTP header.
Response & json(const class json &data) FL_NO_EXCEPT
Set JSON response body with automatic Content-Type header.
string to_string() const FL_NO_EXCEPT
Response & body(const string &content) FL_NO_EXCEPT
Set response body.
static Response bad_request(const string &message) FL_NO_EXCEPT
Factory method for 400 Bad Request response.
static Response ok(const string &body="") FL_NO_EXCEPT
Factory method for 200 OK response.
Response & status(int code) FL_NO_EXCEPT
Set HTTP status code.
static Response not_found() FL_NO_EXCEPT
Factory method for 404 Not Found response.
map< string, string > mHeaders
Definition server.h:144
static Response internal_error(const string &message) FL_NO_EXCEPT
Factory method for 500 Internal Server Error response.
Response() FL_NO_EXCEPT
HTTP response builder (fluent interface)
Definition server.h:95
void accept_connections() FL_NO_EXCEPT
void stop() FL_NO_EXCEPT
Stop server and close all connections.
bool setup_listen_socket(int port) FL_NO_EXCEPT
vector< ClientConnection > mClientSockets
Definition server.h:252
void post(const string &path, RouteHandler handler) FL_NO_EXCEPT
Convenience method for POST routes.
Server() FL_NO_EXCEPT
Constructor.
void get(const string &path, RouteHandler handler) FL_NO_EXCEPT
Convenience method for GET routes.
bool start(int port=8080) FL_NO_EXCEPT
Start server on specified port.
vector< RouteEntry > mRoutes
Definition server.h:251
void put(const string &path, RouteHandler handler) FL_NO_EXCEPT
Convenience method for PUT routes.
void del(const string &path, RouteHandler handler) FL_NO_EXCEPT
Convenience method for DELETE routes.
void cleanup_stale_connections() FL_NO_EXCEPT
size_t process_requests() FL_NO_EXCEPT
bool send_response(int client_fd, const Response &response) FL_NO_EXCEPT
void onExit() FL_NO_EXCEPT override
size_t update() FL_NO_EXCEPT
Update server (process pending requests non-blocking)
void route(const string &method, const string &path, RouteHandler handler) FL_NO_EXCEPT
Register route handler using fl::function.
fl::unique_ptr< ServerAsyncRunner > mAsyncRunner
Definition server.h:255
optional< RouteHandler > find_handler(const string &method, const string &path) const FL_NO_EXCEPT
void close_client(size_t index) FL_NO_EXCEPT
~Server() FL_NO_EXCEPT
Destructor (stops server if running)
optional< Request > read_request(ClientConnection &client) FL_NO_EXCEPT
static constexpr fl::size npos
Definition string.h:204
void register_runner(Runner *r) FL_NO_EXCEPT
Register a runner.
void unregister_runner(Runner *r) FL_NO_EXCEPT
Unregister a runner.
static Executor & instance() FL_NO_EXCEPT
Task executor — runs registered task runners and manages the run loop.
#define FL_WARN(...)
Definition log.h:437
Centralized logging categories for FastLED hardware interfaces and subsystems.
constexpr remove_reference< T >::type && move(T &&t) FL_NO_EXCEPT
Definition move.h:30
fl::enable_if<!fl::is_array< T >::value, unique_ptr< T > >::type make_unique(Args &&... args) FL_NO_EXCEPT
Definition unique_ptr.h:263
function< Response(const Request &)> RouteHandler
Route handler function signature Takes const reference to Request, returns Response by value.
Definition server.h:151
u8 u8 fract8 blur_amount FL_NO_EXCEPT
Definition blur.h:186
constexpr int type_rank< T >::value
int snprintf(char *buffer, fl::size size, const char *format) FL_NO_EXCEPT
AtomicFake< T > atomic
Definition atomic.h:26
FASTLED_FORCE_INLINE void free(void *ptr) FL_NO_EXCEPT
Definition malloc.h:23
MapRedBlackTree< Key, T, Compare, fl::allocator_slab< char > > map
Definition map.h:283
bool isspace(char c) FL_NO_EXCEPT
Check if character is whitespace (space, tab, newline, carriage return)
Definition cctype.h:18
FASTLED_FORCE_INLINE int atoi(const char *str) FL_NO_EXCEPT
Definition cstdlib.h:69
Optional< T > optional
Definition optional.h:16
FASTLED_FORCE_INLINE void * malloc(size_t size) FL_NO_EXCEPT
Definition malloc.h:21
char tolower(char c) FL_NO_EXCEPT
Convert character to lowercase.
Definition cctype.h:32
expected< T, E > result
Alias for expected (Rust-style naming)
Definition result.h:31
FASTLED_FORCE_INLINE char * strncpy(char *dest, const char *src, size_t n) FL_NO_EXCEPT
Definition cstring.h:76
constexpr nullopt_t nullopt
Definition optional.h:13
string to_string(T value) FL_NO_EXCEPT
Definition string.h:459
fl::ptrdiff_t ptrdiff_t
Definition s16x16x4.h:63
Base definition for an LED controller.
Definition crgb.hpp:179
static constexpr T max() FL_NO_EXCEPT
Definition limits.h:108