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
return {
status = 200,
headers = {"Content-Type: text/plain"},
headers = {["Content-Type"] = "text/plain"},
body = "Hello, " .. request.query
}
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:
* 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`);
* 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<string>, "Name: value" entries
request.headers -> table<string,string>, {["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<string>
[optional] headers: table<string,string>
)
-- Builds a JSON response table ready to be returned from a handler.
network.http_json(
data: any,
[optional] status: int=200,
[optional] headers: table<string>
[optional] headers: table<string,string>
) --> table
```

View file

@ -267,7 +267,7 @@ network.http_open(
Ответить на запрос `handler` может двумя способами:
* вернуть таблицу ответа `{status: int, headers: table<string>, body: string}`
* вернуть таблицу ответа `{status: int, headers: table<string,string>, 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<string>, записи вида "Имя: значение"
request.headers -> table<string,string>, {["Имя"] = "значение", ...}
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<string>
[опционально] headers: table<string,string>
)
-- Собирает таблицу JSON-ответа, готовую для возврата из обработчика.
network.http_json(
data: any,
[опционально] status: int=200,
[опционально] headers: table<string>
[опционально] headers: table<string,string>
) -> table
```

View file

@ -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, ...)

View file

@ -91,6 +91,25 @@ static std::vector<std::string> read_headers(lua::State* L, int index) {
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 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);

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);
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<std::string> headers;
std::vector<std::pair<std::string, std::string>> 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);
}
}

View file

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