improve headers api

This commit is contained in:
Astazium 2026-09-22 16:10:52 +03:00
parent e8d36427ba
commit 4667b801d3
7 changed files with 57 additions and 33 deletions

View file

@ -10,7 +10,7 @@ do
if request.path == "/hello" then if request.path == "/hello" then
return { return {
status = 200, status = 200,
headers = {"Content-Type: text/plain"}, headers = {["Content-Type"] = "text/plain"},
body = "Hello, " .. request.query body = "Hello, " .. request.query
} }
end end

View file

@ -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: The `handler` may respond in two ways:
* return a response table `{status: int, headers: table<string>, body: string|Bytearray}` * return a response table `{status: int, headers: table<string,string>, body: string|Bytearray}`
(any field may be omitted; `status` defaults to `200`); (any field may be omitted; `status` defaults to `200`);
* or call `request:respond(status, body, headers)` itself, e.g. from a * 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 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.method -> string, e.g. "GET"
request.path -> string, decoded path without the query string request.path -> string, decoded path without the query string
request.query -> string, raw query string (part after '?', if any) request.query -> string, raw query string (part after '?', if any)
request.headers -> table<string>, "Name: value" entries request.headers -> table<string,string>, {["Name"] = "value", ...}
request.body -> string|Bytearray request.body -> string|Bytearray
request.remote_addr -> string request.remote_addr -> string
request.remote_port -> int request.remote_port -> int
@ -226,14 +226,14 @@ request:json() -> any
request:respond( request:respond(
[optional] status: int=200, [optional] status: int=200,
[optional] body: string|Bytearray, [optional] body: string|Bytearray,
[optional] headers: table<string> [optional] headers: table<string,string>
) )
-- Builds a JSON response table ready to be returned from a handler. -- Builds a JSON response table ready to be returned from a handler.
network.http_json( network.http_json(
data: any, data: any,
[optional] status: int=200, [optional] status: int=200,
[optional] headers: table<string> [optional] headers: table<string,string>
) --> table ) --> table
``` ```

View file

@ -267,7 +267,7 @@ network.http_open(
Ответить на запрос `handler` может двумя способами: Ответить на запрос `handler` может двумя способами:
* вернуть таблицу ответа `{status: int, headers: table<string>, body: string}` * вернуть таблицу ответа `{status: int, headers: table<string,string>, body: string}`
(любое поле можно опустить; `status` по умолчанию равен `200`); (любое поле можно опустить; `status` по умолчанию равен `200`);
* или самостоятельно вызвать `request:respond(status, body, headers)`, * или самостоятельно вызвать `request:respond(status, body, headers)`,
например из корутины, для отложенного ответа. В этом случае возвращаемое например из корутины, для отложенного ответа. В этом случае возвращаемое
@ -282,7 +282,7 @@ network.http_open(
request.method -> string, например "GET" request.method -> string, например "GET"
request.path -> string, декодированный путь без строки запроса request.path -> string, декодированный путь без строки запроса
request.query -> string, необработанная строка запроса (часть после '?', если есть) request.query -> string, необработанная строка запроса (часть после '?', если есть)
request.headers -> table<string>, записи вида "Имя: значение" request.headers -> table<string,string>, {["Имя"] = "значение", ...}
request.body -> string|Bytearray request.body -> string|Bytearray
request.remote_addr -> string request.remote_addr -> string
request.remote_port -> int request.remote_port -> int
@ -295,14 +295,14 @@ request:json() -> any
request:respond( request:respond(
[опционально] status: int=200, [опционально] status: int=200,
[опционально] body: string|Bytearray, [опционально] body: string|Bytearray,
[опционально] headers: table<string> [опционально] headers: table<string,string>
) )
-- Собирает таблицу JSON-ответа, готовую для возврата из обработчика. -- Собирает таблицу JSON-ответа, готовую для возврата из обработчика.
network.http_json( network.http_json(
data: any, data: any,
[опционально] status: int=200, [опционально] status: int=200,
[опционально] headers: table<string> [опционально] headers: table<string,string>
) -> table ) -> table
``` ```

View file

@ -233,7 +233,10 @@ local HttpRequest = {__index={
return return
end end
self.responded = true 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, end,
json=function(self) json=function(self)
return json.parse(self.body) return json.parse(self.body)
@ -252,7 +255,7 @@ end
network.http_json = function(data, status, headers) network.http_json = function(data, status, headers)
return { return {
status = status or 200, 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) body = json.tostring(data)
} }
end end
@ -292,7 +295,7 @@ function Router.__call(self, request)
end end
end 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 end
local function clean(iterable, checkFun, ...) local function clean(iterable, checkFun, ...)

View file

@ -91,6 +91,25 @@ static std::vector<std::string> read_headers(lua::State* L, int index) {
return headers; return headers;
} }
static std::vector<std::pair<std::string, std::string>> read_header_pairs(
lua::State* L, int index
) {
std::vector<std::pair<std::string, std::string>> 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 request_id = 1;
static int l_request(lua::State* L, network::Network& network) { 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; network::HttpServerResponse response;
response.status = lua::tointeger(L, 3); 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) { if (lua::type(L, 5) == LUA_TCDATA) {
response.body = lua::bytearray_as_string(L, 5); 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::pushlstring(L, req.query);
lua::rawseti(L, 6); lua::rawseti(L, 6);
lua::createtable(L, req.headers.size(), 0); lua::createtable(L, 0, req.headers.size());
for (size_t j = 0; j < req.headers.size(); j++) { for (const auto& header : req.headers) {
lua::pushlstring(L, req.headers[j]); lua::pushlstring(L, header.second);
lua::rawseti(L, j + 1); lua::setfield(L, header.first);
} }
lua::rawseti(L, 7); lua::rawseti(L, 7);

View file

@ -751,13 +751,13 @@ namespace {
} }
} }
bool http_header_has(const std::vector<std::string>& headers, const std::string& name) { bool http_header_has(
const std::vector<std::pair<std::string, std::string>>& headers,
const std::string& name
) {
auto lname = util::lower_case(name); auto lname = util::lower_case(name);
for (const auto& header : headers) { for (const auto& header : headers) {
if (header.find(':') == std::string::npos) continue; if (util::lower_case(header.first) == lname) {
auto [hname, hvalue] = util::split_at(header, ':');
util::trim(hname);
if (util::lower_case(hname) == lname) {
return true; return true;
} }
} }
@ -769,7 +769,7 @@ namespace {
out += "HTTP/1.1 " + std::to_string(response.status) + " " + out += "HTTP/1.1 " + std::to_string(response.status) + " " +
http_reason_phrase(response.status) + "\r\n"; http_reason_phrase(response.status) + "\r\n";
for (const auto& header : response.headers) { 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")) { if (!http_header_has(response.headers, "Content-Length")) {
out += "Content-Length: " + std::to_string(response.body.size()) + "\r\n"; out += "Content-Length: " + std::to_string(response.body.size()) + "\r\n";
@ -990,7 +990,7 @@ namespace {
size_t headEnd; size_t headEnd;
while ((headEnd = buffer.find("\r\n\r\n")) == std::string::npos) { while ((headEnd = buffer.find("\r\n\r\n")) == std::string::npos) {
if (buffer.size() > HTTP_MAX_HEADER_SIZE) { 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; return;
} }
if (!http_recv_more(descriptor, buffer)) { if (!http_recv_more(descriptor, buffer)) {
@ -1012,7 +1012,7 @@ namespace {
std::string target = tokens.size() > 1 ? tokens[1] : ""; std::string target = tokens.size() > 1 ? tokens[1] : "";
if (method.empty() || target.empty()) { 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; return;
} }
@ -1024,7 +1024,7 @@ namespace {
} }
path = url_decode(path); path = url_decode(path);
std::vector<std::string> headers; std::vector<std::pair<std::string, std::string>> headers;
std::string contentLength; std::string contentLength;
std::string transferEncoding; std::string transferEncoding;
@ -1039,7 +1039,6 @@ namespace {
auto [name, value] = util::split_at(line, ':'); auto [name, value] = util::split_at(line, ':');
util::trim(name); util::trim(name);
util::trim(value); util::trim(value);
headers.push_back(name + ": " + value);
auto lname = util::lower_case(name); auto lname = util::lower_case(name);
if (lname == "content-length") { if (lname == "content-length") {
@ -1047,10 +1046,12 @@ namespace {
} else if (lname == "transfer-encoding") { } else if (lname == "transfer-encoding") {
transferEncoding = util::lower_case(value); transferEncoding = util::lower_case(value);
} }
headers.emplace_back(std::move(name), std::move(value));
} }
if (transferEncoding.find("chunked") != std::string::npos) { 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; return;
} }
@ -1059,13 +1060,13 @@ namespace {
try { try {
bodyLength = std::stoull(contentLength); bodyLength = std::stoull(contentLength);
} catch (...) { } catch (...) {
finish({400, {"Content-Type: text/plain"}, http_reason_phrase(400)}); finish({400, {{"Content-Type", "text/plain"}}, http_reason_phrase(400)});
return; return;
} }
} }
if (bodyLength > HTTP_MAX_BODY_SIZE) { 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; return;
} }
@ -1112,7 +1113,7 @@ namespace {
response = std::move(pending->response); response = std::move(pending->response);
} else { } else {
response.status = 503; response.status = 503;
response.headers = {"Content-Type: text/plain"}; response.headers = {{"Content-Type", "text/plain"}};
response.body = http_reason_phrase(503); response.body = http_reason_phrase(503);
} }
} }

View file

@ -6,6 +6,7 @@
#include "delegates.hpp" #include "delegates.hpp"
#include <memory> #include <memory>
#include <utility>
#include <vector> #include <vector>
#include <mutex> #include <mutex>
@ -44,7 +45,7 @@ namespace network {
std::string method; std::string method;
std::string path; std::string path;
std::string query; std::string query;
std::vector<std::string> headers; std::vector<std::pair<std::string, std::string>> headers;
std::string body; std::string body;
std::string remoteAddr; std::string remoteAddr;
int remotePort = 0; int remotePort = 0;
@ -52,7 +53,7 @@ namespace network {
struct HttpServerResponse { struct HttpServerResponse {
int status = 200; int status = 200;
std::vector<std::string> headers; std::vector<std::pair<std::string, std::string>> headers;
std::string body; std::string body;
}; };