From 4667b801d38130acddf3b777d385794f7d54954c Mon Sep 17 00:00:00 2001 From: Astazium <113135541+Astazium@users.noreply.github.com> Date: Tue, 22 Sep 2026 16:10:52 +0300 Subject: [PATCH] improve headers api --- dev/tests/network_http_server.lua | 2 +- doc/en/scripting/builtins/libnetwork.md | 8 +++--- doc/ru/scripting/builtins/libnetwork.md | 8 +++--- res/scripts/classes.lua | 9 ++++--- src/logic/scripting/lua/libs/libnetwork.cpp | 29 +++++++++++++++++---- src/network/Sockets.cpp | 29 +++++++++++---------- src/network/commons.hpp | 5 ++-- 7 files changed, 57 insertions(+), 33 deletions(-) diff --git a/dev/tests/network_http_server.lua b/dev/tests/network_http_server.lua index 145bb249e..68e227d11 100644 --- a/dev/tests/network_http_server.lua +++ b/dev/tests/network_http_server.lua @@ -10,7 +10,7 @@ do if request.path == "/hello" then return { status = 200, - headers = {"Content-Type: text/plain"}, + headers = {["Content-Type"] = "text/plain"}, body = "Hello, " .. request.query } end diff --git a/doc/en/scripting/builtins/libnetwork.md b/doc/en/scripting/builtins/libnetwork.md index d3c3f3ab7..d6f95b18e 100644 --- a/doc/en/scripting/builtins/libnetwork.md +++ b/doc/en/scripting/builtins/libnetwork.md @@ -197,7 +197,7 @@ The ServerSocket class for the HTTP server is identical to the TCP server's The `handler` may respond in two ways: -* return a response table `{status: int, headers: table, body: string|Bytearray}` +* return a response table `{status: int, headers: table, body: string|Bytearray}` (any field may be omitted; `status` defaults to `200`); * or call `request:respond(status, body, headers)` itself, e.g. from a coroutine, for a delayed answer. In that case the handler's return value @@ -213,7 +213,7 @@ The `request` class has the following fields and methods: request.method -> string, e.g. "GET" request.path -> string, decoded path without the query string request.query -> string, raw query string (part after '?', if any) -request.headers -> table, "Name: value" entries +request.headers -> table, {["Name"] = "value", ...} request.body -> string|Bytearray request.remote_addr -> string request.remote_port -> int @@ -226,14 +226,14 @@ request:json() -> any request:respond( [optional] status: int=200, [optional] body: string|Bytearray, - [optional] headers: table + [optional] headers: table ) -- Builds a JSON response table ready to be returned from a handler. network.http_json( data: any, [optional] status: int=200, - [optional] headers: table + [optional] headers: table ) --> table ``` diff --git a/doc/ru/scripting/builtins/libnetwork.md b/doc/ru/scripting/builtins/libnetwork.md index ea0b97f0c..f1cf46573 100644 --- a/doc/ru/scripting/builtins/libnetwork.md +++ b/doc/ru/scripting/builtins/libnetwork.md @@ -267,7 +267,7 @@ network.http_open( Ответить на запрос `handler` может двумя способами: -* вернуть таблицу ответа `{status: int, headers: table, body: string}` +* вернуть таблицу ответа `{status: int, headers: table, body: string}` (любое поле можно опустить; `status` по умолчанию равен `200`); * или самостоятельно вызвать `request:respond(status, body, headers)`, например из корутины, для отложенного ответа. В этом случае возвращаемое @@ -282,7 +282,7 @@ network.http_open( request.method -> string, например "GET" request.path -> string, декодированный путь без строки запроса request.query -> string, необработанная строка запроса (часть после '?', если есть) -request.headers -> table, записи вида "Имя: значение" +request.headers -> table, {["Имя"] = "значение", ...} request.body -> string|Bytearray request.remote_addr -> string request.remote_port -> int @@ -295,14 +295,14 @@ request:json() -> any request:respond( [опционально] status: int=200, [опционально] body: string|Bytearray, - [опционально] headers: table + [опционально] headers: table ) -- Собирает таблицу JSON-ответа, готовую для возврата из обработчика. network.http_json( data: any, [опционально] status: int=200, - [опционально] headers: table + [опционально] headers: table ) -> table ``` diff --git a/res/scripts/classes.lua b/res/scripts/classes.lua index 5d17670de..efa0da894 100644 --- a/res/scripts/classes.lua +++ b/res/scripts/classes.lua @@ -233,7 +233,10 @@ local HttpRequest = {__index={ return end self.responded = true - http_respond(self.server_id, self.id, status or 200, headers or {}, body or "") + http_respond( + self.server_id, self.id, status or 200, + headers or {}, body or "" + ) end, json=function(self) return json.parse(self.body) @@ -252,7 +255,7 @@ end network.http_json = function(data, status, headers) return { status = status or 200, - headers = table.extend({"Content-Type: application/json"}, headers or {}), + headers = table.extend({["Content-Type"]="application/json"}, headers or {}), body = json.tostring(data) } end @@ -292,7 +295,7 @@ function Router.__call(self, request) end end end - return {status=404, headers={"Content-Type: text/plain"}, body="Not Found"} + return {status=404, headers={["Content-Type"]="text/plain"}, body="Not Found"} end local function clean(iterable, checkFun, ...) diff --git a/src/logic/scripting/lua/libs/libnetwork.cpp b/src/logic/scripting/lua/libs/libnetwork.cpp index 9804407d1..2ea1ee9db 100644 --- a/src/logic/scripting/lua/libs/libnetwork.cpp +++ b/src/logic/scripting/lua/libs/libnetwork.cpp @@ -91,6 +91,25 @@ static std::vector read_headers(lua::State* L, int index) { return headers; } +static std::vector> read_header_pairs( + lua::State* L, int index +) { + std::vector> headers; + if (lua::istable(L, index)) { + lua::pushnil(L); + while (lua::next(L, index)) { + if (lua::type(L, -2) == LUA_TSTRING) { + headers.emplace_back( + std::string(lua::tolstring(L, -2)), + std::string(lua::tolstring(L, -1)) + ); + } + lua::pop(L); + } + } + return headers; +} + static int request_id = 1; static int l_request(lua::State* L, network::Network& network) { @@ -371,7 +390,7 @@ static int l_http_respond(lua::State* L, network::Network& network) { network::HttpServerResponse response; response.status = lua::tointeger(L, 3); - response.headers = read_headers(L, 4); + response.headers = read_header_pairs(L, 4); if (lua::type(L, 5) == LUA_TCDATA) { response.body = lua::bytearray_as_string(L, 5); @@ -595,10 +614,10 @@ static int l_pull_events(lua::State* L) { lua::pushlstring(L, req.query); lua::rawseti(L, 6); - lua::createtable(L, req.headers.size(), 0); - for (size_t j = 0; j < req.headers.size(); j++) { - lua::pushlstring(L, req.headers[j]); - lua::rawseti(L, j + 1); + lua::createtable(L, 0, req.headers.size()); + for (const auto& header : req.headers) { + lua::pushlstring(L, header.second); + lua::setfield(L, header.first); } lua::rawseti(L, 7); diff --git a/src/network/Sockets.cpp b/src/network/Sockets.cpp index 1e34ac3ca..44daa1a7b 100644 --- a/src/network/Sockets.cpp +++ b/src/network/Sockets.cpp @@ -751,13 +751,13 @@ namespace { } } - bool http_header_has(const std::vector& headers, const std::string& name) { + bool http_header_has( + const std::vector>& headers, + const std::string& name + ) { auto lname = util::lower_case(name); for (const auto& header : headers) { - if (header.find(':') == std::string::npos) continue; - auto [hname, hvalue] = util::split_at(header, ':'); - util::trim(hname); - if (util::lower_case(hname) == lname) { + if (util::lower_case(header.first) == lname) { return true; } } @@ -769,7 +769,7 @@ namespace { out += "HTTP/1.1 " + std::to_string(response.status) + " " + http_reason_phrase(response.status) + "\r\n"; for (const auto& header : response.headers) { - out += header + "\r\n"; + out += header.first + ": " + header.second + "\r\n"; } if (!http_header_has(response.headers, "Content-Length")) { out += "Content-Length: " + std::to_string(response.body.size()) + "\r\n"; @@ -990,7 +990,7 @@ namespace { size_t headEnd; while ((headEnd = buffer.find("\r\n\r\n")) == std::string::npos) { if (buffer.size() > HTTP_MAX_HEADER_SIZE) { - finish({431, {"Content-Type: text/plain"}, http_reason_phrase(431)}); + finish({431, {{"Content-Type", "text/plain"}}, http_reason_phrase(431)}); return; } if (!http_recv_more(descriptor, buffer)) { @@ -1012,7 +1012,7 @@ namespace { std::string target = tokens.size() > 1 ? tokens[1] : ""; if (method.empty() || target.empty()) { - finish({400, {"Content-Type: text/plain"}, http_reason_phrase(400)}); + finish({400, {{"Content-Type", "text/plain"}}, http_reason_phrase(400)}); return; } @@ -1024,7 +1024,7 @@ namespace { } path = url_decode(path); - std::vector headers; + std::vector> headers; std::string contentLength; std::string transferEncoding; @@ -1039,7 +1039,6 @@ namespace { auto [name, value] = util::split_at(line, ':'); util::trim(name); util::trim(value); - headers.push_back(name + ": " + value); auto lname = util::lower_case(name); if (lname == "content-length") { @@ -1047,10 +1046,12 @@ namespace { } else if (lname == "transfer-encoding") { transferEncoding = util::lower_case(value); } + + headers.emplace_back(std::move(name), std::move(value)); } if (transferEncoding.find("chunked") != std::string::npos) { - finish({501, {"Content-Type: text/plain"}, http_reason_phrase(501)}); + finish({501, {{"Content-Type", "text/plain"}}, http_reason_phrase(501)}); return; } @@ -1059,13 +1060,13 @@ namespace { try { bodyLength = std::stoull(contentLength); } catch (...) { - finish({400, {"Content-Type: text/plain"}, http_reason_phrase(400)}); + finish({400, {{"Content-Type", "text/plain"}}, http_reason_phrase(400)}); return; } } if (bodyLength > HTTP_MAX_BODY_SIZE) { - finish({413, {"Content-Type: text/plain"}, http_reason_phrase(413)}); + finish({413, {{"Content-Type", "text/plain"}}, http_reason_phrase(413)}); return; } @@ -1112,7 +1113,7 @@ namespace { response = std::move(pending->response); } else { response.status = 503; - response.headers = {"Content-Type: text/plain"}; + response.headers = {{"Content-Type", "text/plain"}}; response.body = http_reason_phrase(503); } } diff --git a/src/network/commons.hpp b/src/network/commons.hpp index 533dfe0c3..8dfaeaca3 100644 --- a/src/network/commons.hpp +++ b/src/network/commons.hpp @@ -6,6 +6,7 @@ #include "delegates.hpp" #include +#include #include #include @@ -44,7 +45,7 @@ namespace network { std::string method; std::string path; std::string query; - std::vector headers; + std::vector> headers; std::string body; std::string remoteAddr; int remotePort = 0; @@ -52,7 +53,7 @@ namespace network { struct HttpServerResponse { int status = 200; - std::vector headers; + std::vector> headers; std::string body; };