Skip to content

Commit e10c755

Browse files
authored
[k2] add web-builtins (#1383)
add header_register_callback add headers_sent add send_http_103_early_hints as noop
1 parent d12211b commit e10c755

File tree

8 files changed

+140
-63
lines changed

8 files changed

+140
-63
lines changed

builtin-functions/kphp-light/stdlib/server-functions.txt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ function setrawcookie ($name ::: string, $value ::: string, $expire_or_options :
4545

4646
function headers_list () ::: string[];
4747

48+
function headers_sent (?string &$filename = null, ?int &$line = null) ::: bool;
49+
50+
function header_register_callback (callable():void $callback) ::: bool;
51+
4852
function ip2long ($ip ::: string) ::: int | false;
4953

5054
function ip2ulong ($ip ::: string) ::: string | false;
@@ -64,8 +68,7 @@ function memory_get_peak_usage ($real_usage ::: bool = false) ::: int;
6468
// ===== UNSUPPORTED =====
6569

6670
/** @kphp-extern-func-info stub */
67-
function headers_sent (?string &$filename = null, ?int &$line = null) ::: bool;
68-
71+
function send_http_103_early_hints($headers ::: string[]) ::: void;
6972

7073
function get_engine_version() ::: string;
7174

@@ -125,8 +128,6 @@ function setlocale ($category ::: int, $locale ::: string) ::: string | false;
125128

126129
function debug_backtrace() ::: string[][];
127130

128-
/** @kphp-extern-func-info stub generation-required */
129-
function send_http_103_early_hints($headers ::: string[]) ::: void;
130131
/** @kphp-extern-func-info stub generation-required */
131132
function ignore_user_abort ($enable ::: ?bool = null) ::: int;
132133
/** @kphp-extern-func-info stub generation-required */

builtin-functions/kphp-light/stdlib/system-functions.txt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,6 @@ function escapeshellcmd($cmd ::: string): string;
3434
/** @kphp-extern-func-info stub */
3535
function exec($command ::: string, &$output ::: mixed = [], int &$result_code = 0): string|false;
3636

37-
/** @kphp-extern-func-info stub */
38-
function header_register_callback (callable():void $callback) ::: bool;
39-
4037
/** @kphp-extern-func-info stub generation-required */
4138
function getenv(string $varname = '', bool $local_only = false): mixed;
4239

runtime-light/server/http/http-server-state.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include "runtime-common/core/allocator/script-allocator.h"
1515
#include "runtime-common/core/runtime-core.h"
1616
#include "runtime-common/core/std/containers.h"
17+
#include "runtime-light/coroutine/task.h"
1718
#include "runtime-light/streams/stream.h"
1819

1920
namespace kphp::http {
@@ -31,6 +32,8 @@ enum status : uint16_t {
3132
BAD_REQUEST = 400,
3233
};
3334

35+
enum class response_state : uint8_t { not_started, sending_headers, headers_sent, sending_body, completed };
36+
3437
namespace headers {
3538

3639
inline constexpr std::string_view HOST = "host";
@@ -60,6 +63,10 @@ struct HttpServerInstanceState final : private vk::not_copyable {
6063
uint64_t status_code{kphp::http::status::NO_STATUS};
6164
kphp::http::method http_method{kphp::http::method::other};
6265
kphp::http::connection_kind connection_kind{kphp::http::connection_kind::close};
66+
kphp::http::response_state response_state{kphp::http::response_state::not_started};
67+
68+
// The headers_registered_callback function should only be invoked once
69+
std::optional<kphp::coro::task<>> headers_registered_callback;
6370

6471
private:
6572
kphp::stl::multimap<kphp::stl::string<kphp::memory::script_allocator>, kphp::stl::string<kphp::memory::script_allocator>, kphp::memory::script_allocator>

runtime-light/server/http/init-functions.cpp

Lines changed: 75 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,35 @@ std::string_view process_headers(const tl::K2InvokeHttp& invoke_http, PhpScriptB
195195
return content_type;
196196
}
197197

198+
string get_http_response_body(HttpServerInstanceState& http_server_instance_st) noexcept {
199+
string body{};
200+
if (http_server_instance_st.http_method != kphp::http::method::head) {
201+
auto& output_instance_st{OutputInstanceState::get()};
202+
const auto system_buffer{output_instance_st.output_buffers.system_buffer()};
203+
const auto user_buffers{output_instance_st.output_buffers.user_buffers()};
204+
205+
const auto body_size{std::ranges::fold_left(user_buffers | std::views::transform([](const auto& buffer) noexcept { return buffer.size(); }),
206+
system_buffer.get().size(), std::plus<string::size_type>{})};
207+
body.reserve_at_least(body_size);
208+
209+
body.append(system_buffer.get().buffer(), system_buffer.get().size());
210+
const auto appender{[&body](const auto& buffer) noexcept { body.append(buffer.buffer(), buffer.size()); }};
211+
std::ranges::for_each(user_buffers | std::views::filter([](const auto& buffer) noexcept { return buffer.size() > 0; }), appender);
212+
213+
const bool gzip_encoded{static_cast<bool>(http_server_instance_st.encoding & HttpServerInstanceState::ENCODING_GZIP)};
214+
const bool deflate_encoded{static_cast<bool>(http_server_instance_st.encoding & HttpServerInstanceState::ENCODING_DEFLATE)};
215+
// compress body if needed
216+
if (gzip_encoded || deflate_encoded) {
217+
auto encoded_body{kphp::zlib::encode({body.c_str(), static_cast<size_t>(body.size())}, kphp::zlib::DEFAULT_COMPRESSION_LEVEL,
218+
gzip_encoded ? kphp::zlib::ENCODING_GZIP : kphp::zlib::ENCODING_DEFLATE)};
219+
if (encoded_body.has_value()) [[likely]] {
220+
body = std::move(*encoded_body);
221+
}
222+
}
223+
}
224+
return body;
225+
}
226+
198227
} // namespace
199228

200229
namespace kphp::http {
@@ -337,60 +366,59 @@ void init_server(kphp::component::stream request_stream) noexcept {
337366
kphp::coro::task<> finalize_server() noexcept {
338367
auto& http_server_instance_st{HttpServerInstanceState::get()};
339368

340-
string body{};
341-
if (http_server_instance_st.http_method != method::head) {
342-
auto& output_instance_st{OutputInstanceState::get()};
343-
const auto system_buffer{output_instance_st.output_buffers.system_buffer()};
344-
const auto user_buffers{output_instance_st.output_buffers.user_buffers()};
345-
346-
const auto body_size{std::ranges::fold_left(user_buffers | std::views::transform([](const auto& buffer) noexcept { return buffer.size(); }),
347-
system_buffer.get().size(), std::plus<string::size_type>{})};
348-
body.reserve_at_least(body_size);
349-
350-
body.append(system_buffer.get().buffer(), system_buffer.get().size());
351-
const auto appender{[&body](const auto& buffer) noexcept { body.append(buffer.buffer(), buffer.size()); }};
352-
std::ranges::for_each(user_buffers | std::views::filter([](const auto& buffer) noexcept { return buffer.size() > 0; }), appender);
353-
369+
string response_body{};
370+
tl::HttpResponse http_response{};
371+
switch (http_server_instance_st.response_state) {
372+
case kphp::http::response_state::not_started:
373+
http_server_instance_st.response_state = kphp::http::response_state::sending_headers;
374+
if (http_server_instance_st.headers_registered_callback.has_value()) {
375+
co_await *std::exchange(http_server_instance_st.headers_registered_callback, std::nullopt);
376+
}
377+
[[fallthrough]];
378+
case kphp::http::response_state::sending_headers: {
354379
const bool gzip_encoded{static_cast<bool>(http_server_instance_st.encoding & HttpServerInstanceState::ENCODING_GZIP)};
355380
const bool deflate_encoded{static_cast<bool>(http_server_instance_st.encoding & HttpServerInstanceState::ENCODING_DEFLATE)};
356-
// compress body if needed
357381
if (gzip_encoded || deflate_encoded) {
358-
auto encoded_body{kphp::zlib::encode({body.c_str(), static_cast<size_t>(body.size())}, kphp::zlib::DEFAULT_COMPRESSION_LEVEL,
359-
gzip_encoded ? kphp::zlib::ENCODING_GZIP : kphp::zlib::ENCODING_DEFLATE)};
360-
if (encoded_body.has_value()) [[likely]] {
361-
body = std::move(*encoded_body);
362-
363-
auto& static_SB{RuntimeContext::get().static_SB};
364-
static_SB.clean() << headers::CONTENT_ENCODING.data() << ": " << (gzip_encoded ? ENCODING_GZIP.data() : ENCODING_DEFLATE.data());
365-
kphp::http::header({static_SB.c_str(), static_SB.size()}, true, status::NO_STATUS);
366-
}
382+
auto& static_SB{RuntimeContext::get().static_SB};
383+
static_SB.clean() << kphp::http::headers::CONTENT_ENCODING.data() << ": " << (gzip_encoded ? ENCODING_GZIP.data() : ENCODING_DEFLATE.data());
384+
kphp::http::header({static_SB.c_str(), static_SB.size()}, true, kphp::http::status::NO_STATUS);
367385
}
386+
// fill headers
387+
http_response.http_response.headers.value.reserve(http_server_instance_st.headers().size());
388+
std::transform(http_server_instance_st.headers().cbegin(), http_server_instance_st.headers().cend(),
389+
std::back_inserter(http_response.http_response.headers.value), [](const auto& header_entry) noexcept {
390+
const auto& [name, value]{header_entry};
391+
return tl::httpHeaderEntry{
392+
.is_sensitive = {}, .name = {.value = {name.data(), name.size()}}, .value = {.value = {value.data(), value.size()}}};
393+
});
394+
http_server_instance_st.response_state = kphp::http::response_state::headers_sent;
395+
[[fallthrough]];
368396
}
397+
case kphp::http::response_state::headers_sent: {
398+
response_body = get_http_response_body(http_server_instance_st);
399+
const auto status_code{http_server_instance_st.status_code == status::NO_STATUS ? status::OK : http_server_instance_st.status_code};
400+
http_response.http_response.version = tl::HttpVersion{.version = tl::HttpVersion::Version::V11};
401+
http_response.http_response.status_code = {.value = static_cast<int32_t>(status_code)};
402+
http_response.http_response.body = {reinterpret_cast<const std::byte*>(response_body.c_str()), response_body.size()};
403+
http_server_instance_st.response_state = kphp::http::response_state::sending_body;
404+
[[fallthrough]];
405+
}
406+
case kphp::http::response_state::sending_body: {
407+
tl::storer tls{http_response.footprint()};
408+
http_response.store(tls);
369409

370-
const auto status_code{http_server_instance_st.status_code == status::NO_STATUS ? status::OK : http_server_instance_st.status_code};
371-
372-
tl::HttpResponse http_response{.http_response = tl::httpResponse{.version = tl::HttpVersion{.version = tl::HttpVersion::Version::V11},
373-
.status_code = {.value = static_cast<int32_t>(status_code)},
374-
.headers = {},
375-
.body = {reinterpret_cast<const std::byte*>(body.c_str()), body.size()}}};
376-
// fill headers
377-
http_response.http_response.headers.value.reserve(http_server_instance_st.headers().size());
378-
std::transform(http_server_instance_st.headers().cbegin(), http_server_instance_st.headers().cend(),
379-
std::back_inserter(http_response.http_response.headers.value), [](const auto& header_entry) noexcept {
380-
const auto& [name, value]{header_entry};
381-
return tl::httpHeaderEntry{
382-
.is_sensitive = {}, .name = {.value = {name.data(), name.size()}}, .value = {.value = {value.data(), value.size()}}};
383-
});
384-
385-
tl::storer tls{http_response.footprint()};
386-
http_response.store(tls);
387-
388-
if (!http_server_instance_st.request_stream.has_value()) [[unlikely]] {
389-
kphp::log::error("can't send HTTP response since there is no available stream");
410+
if (!http_server_instance_st.request_stream.has_value()) [[unlikely]] {
411+
kphp::log::error("can't send HTTP response since there is no available stream");
412+
}
413+
const auto& request_stream{*http_server_instance_st.request_stream};
414+
if (auto expected{co_await kphp::component::send_response(request_stream, tls.view())}; !expected) [[unlikely]] {
415+
kphp::log::error("can't write HTTP response: stream -> {}, error code -> {}", request_stream.descriptor(), expected.error());
416+
}
417+
http_server_instance_st.response_state = kphp::http::response_state::completed;
418+
[[fallthrough]];
390419
}
391-
const auto& request_stream{*http_server_instance_st.request_stream};
392-
if (auto expected{co_await kphp::component::send_response(request_stream, tls.view())}; !expected) [[unlikely]] {
393-
kphp::log::error("can't write HTTP response: stream -> {}, error code -> {}", request_stream.descriptor(), expected.error());
420+
case kphp::http::response_state::completed:
421+
co_return;
394422
}
395423
}
396424

runtime-light/stdlib/server/http-functions.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,17 @@ void header(std::string_view header_view, bool replace, int64_t response_code) n
103103
}
104104

105105
auto& http_server_instance_st{HttpServerInstanceState::get()};
106+
switch (http_server_instance_st.response_state) {
107+
case kphp::http::response_state::not_started:
108+
case kphp::http::response_state::sending_headers:
109+
break;
110+
case kphp::http::response_state::headers_sent:
111+
case kphp::http::response_state::sending_body:
112+
case kphp::http::response_state::completed:
113+
// don't add header since it will not be sent
114+
return;
115+
}
116+
106117
// HTTP status special case
107118
if (http_status_header(header_view)) {
108119
if (const auto opt_status_code{valid_http_status_header(header_view)}; opt_status_code.has_value()) [[likely]] {

runtime-light/stdlib/server/http-functions.h

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44

55
#pragma once
66

7+
#include <concepts>
78
#include <cstdint>
89
#include <string_view>
910
#include <utility>
1011

1112
#include "runtime-common/core/runtime-core.h"
1213
#include "runtime-common/stdlib/server/url-functions.h"
14+
#include "runtime-light/coroutine/task.h"
1315
#include "runtime-light/server/http/http-server-state.h"
14-
#include "runtime-light/stdlib/diagnostics/logs.h"
1516

1617
namespace kphp::http {
1718

@@ -48,12 +49,45 @@ inline array<string> f$headers_list() noexcept {
4849

4950
inline bool f$headers_sent([[maybe_unused]] Optional<std::optional<std::reference_wrapper<string>>> filename = {},
5051
[[maybe_unused]] Optional<std::optional<std::reference_wrapper<string>>> line = {}) noexcept {
51-
kphp::log::warning("called stub headers_sent");
52-
return false;
52+
const auto& http_server_instance_st{HttpServerInstanceState::get()};
53+
switch (http_server_instance_st.response_state) {
54+
case kphp::http::response_state::not_started:
55+
case kphp::http::response_state::sending_headers:
56+
return false;
57+
case kphp::http::response_state::headers_sent:
58+
case kphp::http::response_state::sending_body:
59+
case kphp::http::response_state::completed:
60+
return true;
61+
}
5362
}
5463

55-
template<typename F>
56-
bool f$header_register_callback(F&& /*unused*/) noexcept {
57-
kphp::log::warning("called stub header_register_callback");
64+
template<std::invocable F>
65+
bool f$header_register_callback(F&& f) noexcept {
66+
auto& http_server_instance_st{HttpServerInstanceState::get()};
67+
switch (http_server_instance_st.response_state) {
68+
case kphp::http::response_state::not_started:
69+
break;
70+
case kphp::http::response_state::sending_headers:
71+
case kphp::http::response_state::headers_sent:
72+
case kphp::http::response_state::sending_body:
73+
case kphp::http::response_state::completed:
74+
return false;
75+
}
76+
77+
auto headers_callback_task{std::invoke(
78+
[](F f) noexcept -> kphp::coro::task<> {
79+
if constexpr (kphp::coro::is_async_function_v<F>) {
80+
co_await std::invoke(std::move(f));
81+
} else {
82+
std::invoke(std::move(f));
83+
}
84+
},
85+
std::forward<F>(f))};
86+
87+
http_server_instance_st.headers_registered_callback.emplace(std::move(headers_callback_task));
5888
return true;
5989
}
90+
91+
inline void f$send_http_103_early_hints([[maybe_unused]] const array<string>& headers) noexcept {
92+
// noop
93+
}

tests/python/tests/http_server/test_gzip_header_reset.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ def test_single_gzip_buffer(self):
88
response = self.gzip_request("gzip")
99
self.assertEqual(response.headers["Content-Encoding"], "gzip")
1010

11-
@pytest.mark.k2_skip
1211
def test_single_gzip_buffer_closed(self):
1312
response = self.gzip_request("reset")
1413
with self.assertRaises(KeyError):

tests/python/tests/http_server/test_header_register_callback.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
from python.lib.testcase import WebServerAutoTestCase
66

77

8-
@pytest.mark.k2_skip_suite
98
class TestHeaderRegisterCallback(WebServerAutoTestCase):
109

10+
@pytest.mark.k2_skip
1111
def test_rpc_in_callback(self):
1212
self.web_server.http_request(uri='/test_header_register_callback?act_in_callback=rpc&port={}'.format(str(typing.cast(KphpServer, self.web_server).master_port)), timeout=5)
1313
self.web_server.assert_log(["test_header_register_callback/rpc_in_callback"], timeout=5)
1414

1515
def test_exit_in_callback(self):
16-
response = self.web_server.http_request(uri='/test_header_register_callback?act_in_callback=exit&port={}'.format(str(typing.cast(KphpServer, self.web_server).master_port)), timeout=5)
16+
response = self.web_server.http_request(uri='/test_header_register_callback?act_in_callback=exit', timeout=5)
1717
self.assertEqual(response.status_code, 200)
1818
self.assertEqual(response.content, b"test_header_register_callback/exit_in_callback")

0 commit comments

Comments
 (0)