MEDIUM: httpclient/lua: allow multiple requests from a single core.httpclient() instance
Some checks are pending
Contrib / admin/halog/ (push) Waiting to run
Contrib / dev/flags/ (push) Waiting to run
Contrib / dev/haring/ (push) Waiting to run
Contrib / dev/hpack/ (push) Waiting to run
Contrib / dev/poll/ (push) Waiting to run
FreeBSD / clang (push) Waiting to run
VTest / Generate Build Matrix (push) Waiting to run
VTest / (push) Blocked by required conditions
Windows / Windows, gcc, all features (push) Waiting to run

Refactor the Lua HTTP client to defer initialization. core.httpclient()
no longer initializes the internal HTTP client immediately. Instead,
initialization now occurs within hlua_httpclient_send() when a request
method (e.g., get, put, head) is invoked.

The HTTPClient class now serves as a factory for accessing methods, while
a new class, HTTPClientRequest, has been introduced to represent individual
requests and manage the HTTP client lifecycle.

This change allows multiple requests to be executed using a single
HTTP client instance:

  local hc = core.httpclient()
  local res1 = hc:get({url = "...", headers = ...})
  local res2 = hc:post({url = "...", headers = ...})
  local res3 = hc:put({url = "...", headers = ...})

This refactor maintains backward compatibility, as existing scripts that
instantiate a new core.httpclient() for every request will continue to
work as expected.
This commit is contained in:
William Lallemand 2026-06-14 01:04:05 +02:00
parent 339d25636d
commit 0748799ad1
4 changed files with 38 additions and 31 deletions

View file

@ -893,9 +893,7 @@ Core class
**context**: init, task, action
This function returns a new object of a *httpclient* class. An *httpclient*
object must be used to process one and only one request. It must never be
reused to process several requests.
This function returns a new object of a *httpclient* class.
:returns: A :ref:`httpclient_class` object.
@ -2587,10 +2585,7 @@ HTTPClient class
.. js:class:: HTTPClient
The httpclient class allows issue of outbound HTTP requests through a simple
API without the knowledge of HAProxy internals. Any instance must be used to
process one and only one request. It must never be reused to process several
requests.
API without the knowledge of HAProxy internals.
.. js:function:: HTTPClient.get(httpclient, request)
.. js:function:: HTTPClient.head(httpclient, request)
.. js:function:: HTTPClient.put(httpclient, request)

View file

@ -47,6 +47,7 @@
#define CLASS_HTTP "HTTP"
#define CLASS_HTTP_MSG "HTTPMessage"
#define CLASS_HTTPCLIENT "HTTPClient"
#define CLASS_HTTPCLIENT_REQ "HTTPClientRequest"
#define CLASS_MAP "Map"
#define CLASS_APPLET_TCP "AppletTCP"
#define CLASS_APPLET_HTTP "AppletHTTP"

View file

@ -18,6 +18,8 @@ core.register_service("fakeserv", "http", function(applet)
end)
local function cron()
local httpclient = core.httpclient()
-- wait for until the correct port is set through the c0 request..
while vtc_port == 0 do
core.msleep(1)
@ -30,19 +32,16 @@ local function cron()
body = body .. i .. ' ABCDEFGHIJKLMNOPQRSTUVWXYZ\n'
end
core.Info("First httpclient request")
local httpclient = core.httpclient()
local response = httpclient:post{url="http://127.0.0.1:" .. vtc_port, body=body}
core.Info("Received: " .. response.body)
body = response.body
core.Info("Second httpclient request")
local httpclient2 = core.httpclient()
local response2 = httpclient2:post{url="http://127.0.0.1:" .. vtc_port2, body=body}
local response2 = httpclient:post{url="http://127.0.0.1:" .. vtc_port2, body=body}
core.Info("Third httpclient request")
local httpclient3 = core.httpclient()
local response3 = httpclient3:get{url="http://127.0.0.1", dst = vtc_port3, headers={ [ "Host" ] = { "foobar.haproxy.local" } }}
local response3 = httpclient:get{url="http://127.0.0.1", dst = vtc_port3, headers={ [ "Host" ] = { "foobar.haproxy.local" } }}
end

View file

@ -47,6 +47,7 @@
#ifdef USE_LUA
static int class_httpclient_ref; /* httpclient LUA class */
static int class_httpclient_request_ref; /* httpclient request LUA class */
#endif
static struct proxy *httpclient_proxy;
@ -1536,7 +1537,7 @@ void hlua_httpclient_destroy_all(struct hlua *hlua)
__LJMP static struct hlua_httpclient *hlua_checkhttpclient(lua_State *L, int ud)
{
return MAY_LJMP(hlua_checkudata(L, ud, class_httpclient_ref));
return MAY_LJMP(hlua_checkudata(L, ud, class_httpclient_request_ref));
}
@ -1554,11 +1555,20 @@ __LJMP static int hlua_httpclient_gc(lua_State *L)
httpclient_stop_and_destroy(hlua_hc->hc);
hlua_hc->hc = NULL;
}
return 0;
}
__LJMP static int hlua_httpclient_factory_new(lua_State *L)
{
lua_newtable(L);
lua_rawgeti(L, LUA_REGISTRYINDEX, class_httpclient_ref);
lua_setmetatable(L, -2);
return 1;
}
__LJMP static int hlua_httpclient_new(lua_State *L)
{
struct hlua_httpclient *hlua_hc;
@ -1580,14 +1590,11 @@ __LJMP static int hlua_httpclient_new(lua_State *L)
lua_rawseti(L, -2, 0);
memset(hlua_hc, 0, sizeof(*hlua_hc));
hlua_hc->hc = httpclient_new(hlua, 0, IST_NULL);
if (!hlua_hc->hc)
goto err;
MT_LIST_APPEND(&hlua->hc_list, &hlua_hc->by_hlua);
/* Pop a class stream metatable and affect it to the userdata. */
lua_rawgeti(L, LUA_REGISTRYINDEX, class_httpclient_ref);
lua_rawgeti(L, LUA_REGISTRYINDEX, class_httpclient_request_ref);
lua_setmetatable(L, -2);
return 1;
@ -1869,13 +1876,15 @@ __LJMP static int hlua_httpclient_send(lua_State *L, enum http_meth_t meth)
if (lua_gettop(L) != 2 || lua_type(L, -1) != LUA_TTABLE)
WILL_LJMP(luaL_error(L, "'get' needs a table as argument"));
/* Create the internal httpclient request object and replace the factory at index 1 */
hlua_httpclient_new(L);
lua_replace(L, 1);
hlua_hc = hlua_checkhttpclient(L, 1);
/* An HTTPclient instance must never process more that one request. So
* at this stage, it must never have been started.
*/
if (httpclient_started(hlua_hc->hc))
WILL_LJMP(luaL_error(L, "httpclient instance cannot be reused. It must process at most one request"));
hlua_hc->hc = httpclient_new(hlua, 0, IST_NULL);
if (!hlua_hc->hc)
WILL_LJMP(luaL_error(L, "out of memory"));
lua_pushnil(L); /* first key */
while (lua_next(L, 2)) {
@ -2013,7 +2022,16 @@ __LJMP static int hlua_httpclient_delete(lua_State *L)
*/
static int hlua_http_client_init_state(lua_State *L, char **errmsg)
{
/* Create and fill the metatable. */
/* Register HTTPClientRequest */
lua_newtable(L);
/* Register the garbage collector entry. */
lua_pushstring(L, "__gc");
lua_pushcclosure(L, hlua_httpclient_gc, 0);
lua_settable(L, -3);
class_httpclient_request_ref = hlua_register_metatable(L, CLASS_HTTPCLIENT_REQ);
/* Register HTTPClient */
lua_newtable(L);
lua_pushstring(L, "__index");
lua_newtable(L);
@ -2023,17 +2041,11 @@ static int hlua_http_client_init_state(lua_State *L, char **errmsg)
hlua_class_function(L, "post", hlua_httpclient_post);
hlua_class_function(L, "delete", hlua_httpclient_delete);
lua_settable(L, -3); /* Sets the __index entry. */
/* Register the garbage collector entry. */
lua_pushstring(L, "__gc");
lua_pushcclosure(L, hlua_httpclient_gc, 0);
lua_settable(L, -3); /* Push the last 2 entries in the table at index -3 */
class_httpclient_ref = hlua_register_metatable(L, CLASS_HTTPCLIENT);
lua_getglobal(L, "core");
hlua_class_function(L, "httpclient", hlua_httpclient_new);
hlua_class_function(L, "httpclient", hlua_httpclient_factory_new);
lua_pop(L, 1);
return ERR_NONE;