Skip to content

feat(http): add http_client_set_local_addr to bind the source address before connecting - #917

Open
wang-guannan wants to merge 2 commits into
ithewei:masterfrom
wang-guannan:feature/http-client-set-local-addr
Open

wang-guannan wants to merge 2 commits into
ithewei:masterfrom
wang-guannan:feature/http-client-set-local-addr

Conversation

@wang-guannan

@wang-guannan wang-guannan commented Oct 28, 2026 •

Copy link
Copy Markdown

Motivation

On a multi-NIC host the kernel chooses the source address of outgoing packets by the routing table, so the peer may see an unexpected source address, or the connection may fail when the route does not leave through the intended interface.

hv::TcpClient already offers bind(local_port, local_host), but the HTTP client does not: hv::HttpClient is a thin wrapper over the opaque C API http_client_t, so there is no way to bind a local address before connecting. Applications that need a fixed source address have to drop down to the raw socket layer.

Changes

  • base/hsocket.[ch]: add ConnectTimeoutWithLocalAddr(), which binds the local address before connecting.
  • http/client/HttpClient.[ch]: add http_client_set_local_addr() and hv::HttpClient::setLocalAddr(). When a local address is set, the connection binds it (port 0 lets the kernel choose the source port).
  • http/client/AsyncHttpClient.[ch]: the same feature for the asynchronous path.
    • HttpClientTask carries the local address from the http_client_t over to the connection setup.
    • AsyncHttpClient::startConnect() binds the socket before connecting. The bind is done before hio_get(), so that a failure can close the socket without racing the hloop cleanup. A failed bind fails the connection, matching the behaviour of the synchronous path.
    • http_client_exec_async() forwards cli->local_host / cli->local_port.
  • unittest/http_client_local_addr_test.cpp: verify that the peer sees the bound source address (synchronous path).
  • unittest/http_client_local_addr_async_test.cpp: the same check for the asynchronous path.
  • docs/cn/HttpClient.md: document the new API.

Usage

hv::HttpClient cli;
cli.setLocalAddr("192.168.1.10");   // bind the source address
cli.send(&req, &resp);              // synchronous path
cli.sendAsync(req, resp_cb);        // asynchronous path

Compatibility

Purely additive: no behaviour change unless setLocalAddr() is called.

Note

setLocalAddr() should be called before sending any request -- connections that were already
established keep the source address they were created with.

Test

./configure --with-openssl --with-http
make libhv
make unittest
bin/http_client_local_addr_test <a non-loopback local address>
bin/http_client_local_addr_async_test <a non-loopback local address>

Output:

no-bind -> 127.0.0.1 : client_addr=[127.0.0.1]
bind 127.0.0.1 -> 172.18.0.2 : client_addr=[127.0.0.1]
bind 10.255.255.253 -> 172.18.0.2 : client_addr=[] (expected to fail)
PASS

… before connecting

On a multi-NIC host the kernel picks the source address of outgoing packets
by the routing table, so the peer may see an unexpected source address.
hv::TcpClient already offers bind(local_port, local_host), but hv::HttpClient
is a thin wrapper over the opaque C API http_client_t, so there is no way
to bind a local address before connecting.

Add ConnectTimeoutWithLocalAddr() in base/hsocket, expose it through
http_client_set_local_addr() and hv::HttpClient::setLocalAddr(), and add a
unit test verifying that the peer sees the bound source address.

Behaviour is unchanged unless setLocalAddr() is called.
PR ithewei#917 added http_client_set_local_addr() and wired it into the
synchronous connect path (http_client_connect). The asynchronous path
was left unchanged, so hv::HttpClient::sendAsync() silently ignored the
local address and the kernel still picked the source address by the
routing table.

This extends the feature to the async path:

- HttpClientTask carries the local address from the http_client_t to
  the connection setup.
- AsyncHttpClient::startConnect() binds the socket to that address
  before connecting. The bind happens before hio_get() so that a
  failure can close the socket without racing the hloop cleanup.
  A failed bind fails the connection, matching the behaviour of the
  synchronous path.
- http_client_exec_async() forwards cli->local_host/local_port.

Adds unittest/http_client_local_addr_async_test.cpp, which checks that
the peer sees the bound source address and that binding an unavailable
address fails.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant