Implement HTTP client in consistent style with server.c

Refactored HTTP client implementation to follow the same coding
patterns and style as server.c:

- Added static helper functions (http_client_conn_init,
  http_client_conn_free, check_response_buffer, request_builder_to_conn)
- Implemented buffer limit setters (http_client_set_input_limit,
  http_client_set_output_limit)
- Fully implemented request builder API (url, header, body, send)
- Implemented event processing with state machine approach
- Added connection reuse support via http_free_response
- Used consistent naming, formatting, and error handling patterns
- Renamed builder_to_conn to request_builder_to_conn to avoid
  symbol conflicts with server.c during amalgamation

The implementation follows the same architecture as the server:
- Connection state machine for managing request/response lifecycle
- ByteQueue for input/output buffering
- Ready queue for completed responses
- Generation counters for builder validation
- Socket manager integration for async I/O
This commit is contained in:
Claude
2025-11-21 19:17:18 +00:00
parent 2d407b63c2
commit 9868ae2e33
4 changed files with 842 additions and 113 deletions
+338 -40
View File
@@ -1,19 +1,31 @@
void http_client_conn_init(HTTP_ClientConn *conn)
static void http_client_conn_init(HTTP_ClientConn *conn,
SocketHandle handle, uint32_t input_buffer_limit,
uint32_t output_buffer_limit)
{
// TODO
conn->state = HTTP_CLIENT_CONN_WAIT_LINE;
conn->handle = handle;
conn->gen = 0;
byte_queue_init(&conn->input, input_buffer_limit);
byte_queue_init(&conn->output, output_buffer_limit);
}
void http_client_conn_free(HTTP_ClientConn *conn)
static void http_client_conn_free(HTTP_ClientConn *conn)
{
// TODO
byte_queue_free(&conn->output);
byte_queue_free(&conn->input);
}
int http_client_init(HTTP_Client *client)
{
client->input_buffer_limit = 1<<20;
client->output_buffer_limit = 1<<20;
client->num_conns = 0;
for (int i = 0; i < HTTP_CLIENT_CAPACITY; i++)
for (int i = 0; i < HTTP_CLIENT_CAPACITY; i++) {
client->conns[i].state = HTTP_CLIENT_CONN_FREE;
client->conns[i].gen = 0;
}
client->num_ready = 0;
client->ready_head = 0;
@@ -38,6 +50,16 @@ void http_client_free(HTTP_Client *client)
}
}
void http_client_set_input_limit(HTTP_Client *client, uint32_t limit)
{
client->input_buffer_limit = limit;
}
void http_client_set_output_limit(HTTP_Client *client, uint32_t limit)
{
client->output_buffer_limit = limit;
}
int http_client_wakeup(HTTP_Client *client)
{
if (socket_manager_wakeup(&client->sockets) < 0)
@@ -45,35 +67,6 @@ int http_client_wakeup(HTTP_Client *client)
return 0;
}
int http_client_get_builder(HTTP_Client *client,
HTTP_Response *response, HTTP_RequestBuilder *builder)
{
// TODO
}
void http_request_builder_url(HTTP_RequestBuilder builder,
HTTP_String url)
{
// TODO
}
void http_request_builder_header(HTTP_RequestBuilder builder,
HTTP_String str)
{
// TODO
}
void http_request_builder_body(HTTP_RequestBuilder builder,
HTTP_String str)
{
// TODO
}
int http_request_builder_send(HTTP_RequestBuilder builder)
{
// TODO
}
int http_client_register_events(HTTP_Client *client,
EventRegister *reg)
{
@@ -82,25 +75,319 @@ int http_client_register_events(HTTP_Client *client,
return 0;
}
// Get a connection pointer from a request builder.
// If the builder is invalid, returns NULL.
static HTTP_ClientConn*
request_builder_to_conn(HTTP_RequestBuilder builder)
{
HTTP_Client *client = builder.client;
if (client == NULL)
return NULL;
if (builder.index >= HTTP_CLIENT_CAPACITY)
return NULL;
HTTP_ClientConn *conn = &client->conns[builder.index];
if (builder.gen != conn->gen)
return NULL;
return conn;
}
int http_client_get_builder(HTTP_Client *client,
HTTP_Response *response, HTTP_RequestBuilder *builder)
{
HTTP_ClientConn *conn = NULL;
if (response != NULL && response->context != NULL) {
// Reuse the connection from the previous response
conn = (HTTP_ClientConn*) response->context;
// Mark the response as freed
response->context = NULL;
// Reset the connection for a new request
byte_queue_read_ack(&conn->input, byte_queue_read_buf(&conn->input).len);
byte_queue_read_ack(&conn->output, byte_queue_read_buf(&conn->output).len);
conn->state = HTTP_CLIENT_CONN_WAIT_LINE;
} else {
// Find a free connection slot
if (client->num_conns == HTTP_CLIENT_CAPACITY)
return -1;
int i = 0;
while (client->conns[i].state != HTTP_CLIENT_CONN_FREE) {
i++;
if (i >= HTTP_CLIENT_CAPACITY)
return -1;
}
conn = &client->conns[i];
conn->state = HTTP_CLIENT_CONN_WAIT_LINE;
conn->gen = 0;
conn->handle = SOCKET_HANDLE_INVALID;
conn->client = client;
byte_queue_init(&conn->input, client->input_buffer_limit);
byte_queue_init(&conn->output, client->output_buffer_limit);
client->num_conns++;
}
*builder = (HTTP_RequestBuilder) {
client,
conn - client->conns,
conn->gen
};
return 0;
}
void http_request_builder_url(HTTP_RequestBuilder builder,
HTTP_String url)
{
HTTP_ClientConn *conn = request_builder_to_conn(builder);
if (conn == NULL)
return;
if (conn->state != HTTP_CLIENT_CONN_WAIT_LINE)
return;
// Parse the URL to extract components
HTTP_URL parsed_url;
if (http_parse_url(url.ptr, url.len, &parsed_url) != 1)
return;
// Store parsed URL for connection establishment
conn->url = parsed_url;
// Determine HTTP method (default to GET)
HTTP_String method = HTTP_STR("GET");
// Build request line: METHOD path HTTP/1.1\r\n
byte_queue_write_fmt(&conn->output, "%.*s %.*s HTTP/1.1\r\n",
method.len, method.ptr,
parsed_url.path.len, parsed_url.path.ptr);
// Add Host header automatically
byte_queue_write_fmt(&conn->output, "Host: %.*s",
parsed_url.authority.host.text.len,
parsed_url.authority.host.text.ptr);
if (parsed_url.authority.port > 0) {
byte_queue_write_fmt(&conn->output, ":%d", parsed_url.authority.port);
}
byte_queue_write(&conn->output, "\r\n", 2);
conn->state = HTTP_CLIENT_CONN_WAIT_HEADER;
}
void http_request_builder_header(HTTP_RequestBuilder builder,
HTTP_String str)
{
HTTP_ClientConn *conn = request_builder_to_conn(builder);
if (conn == NULL)
return;
if (conn->state != HTTP_CLIENT_CONN_WAIT_HEADER)
return;
// Validate header: must contain a colon and no control characters
bool has_colon = false;
for (int i = 0; i < str.len; i++) {
char c = str.ptr[i];
if (c == ':')
has_colon = true;
// Reject control characters (especially \r and \n)
if (c < 0x20 && c != '\t')
return;
}
if (!has_colon)
return;
byte_queue_write(&conn->output, str.ptr, str.len);
byte_queue_write(&conn->output, "\r\n", 2);
}
void http_request_builder_body(HTTP_RequestBuilder builder,
HTTP_String str)
{
HTTP_ClientConn *conn = request_builder_to_conn(builder);
if (conn == NULL)
return;
// Transition from WAIT_HEADER to WAIT_BODY if needed
if (conn->state == HTTP_CLIENT_CONN_WAIT_HEADER) {
// End headers section
byte_queue_write(&conn->output, "\r\n", 2);
conn->state = HTTP_CLIENT_CONN_WAIT_BODY;
}
if (conn->state != HTTP_CLIENT_CONN_WAIT_BODY)
return;
byte_queue_write(&conn->output, str.ptr, str.len);
}
int http_request_builder_send(HTTP_RequestBuilder builder)
{
HTTP_ClientConn *conn = request_builder_to_conn(builder);
if (conn == NULL)
return -1;
// Finalize the request
if (conn->state == HTTP_CLIENT_CONN_WAIT_HEADER) {
// No body, just end headers
byte_queue_write(&conn->output, "\r\n", 2);
}
// Establish connection if not already connected
if (conn->handle == SOCKET_HANDLE_INVALID) {
// Determine if connection should be secure
bool secure = false;
if (conn->url.scheme.len == 5 &&
strncmp(conn->url.scheme.ptr, "https", 5) == 0) {
secure = true;
}
// Prepare connection target
ConnectTarget target;
target.port = conn->url.authority.port;
if (target.port <= 0)
target.port = secure ? 443 : 80;
// Set up target based on host type
if (conn->url.authority.host.mode == HTTP_HOST_MODE_NAME) {
target.type = CONNECT_TARGET_NAME;
target.name = (String) {
conn->url.authority.host.name.ptr,
conn->url.authority.host.name.len
};
} else if (conn->url.authority.host.mode == HTTP_HOST_MODE_IPV4) {
target.type = CONNECT_TARGET_IPV4;
target.ipv4 = (IPv4) { conn->url.authority.host.ipv4.data };
} else if (conn->url.authority.host.mode == HTTP_HOST_MODE_IPV6) {
target.type = CONNECT_TARGET_IPV6;
for (int i = 0; i < 8; i++)
target.ipv6.data[i] = conn->url.authority.host.ipv6.data[i];
} else {
return -1;
}
if (socket_connect(&conn->client->sockets, 1, &target, secure, conn) < 0)
return -1;
}
conn->state = HTTP_CLIENT_CONN_FLUSHING;
conn->gen++;
return 0;
}
// Look at the input buffer to see if a complete response
// was buffered. If it was, change the connection's status
// to COMPLETE and push it to the ready queue.
static void
check_response_buffer(HTTP_Client *client, HTTP_ClientConn *conn)
{
assert(conn->state == HTTP_CLIENT_CONN_BUFFERING);
ByteView src = byte_queue_read_buf(&conn->input);
int ret = http_parse_response(src.ptr, src.len, &conn->response);
if (ret < 0) {
// Invalid response
byte_queue_read_ack(&conn->input, 0);
socket_close(&client->sockets, conn->handle);
} else if (ret == 0) {
// Still waiting
byte_queue_read_ack(&conn->input, 0);
// If the queue reached its limit and we still didn't receive
// a complete response, abort the exchange.
if (byte_queue_full(&conn->input))
socket_close(&client->sockets, conn->handle);
} else {
// Ready
assert(ret == 1);
conn->state = HTTP_CLIENT_CONN_COMPLETE;
conn->response.context = conn;
// Push to the ready queue
assert(client->num_ready < HTTP_CLIENT_CAPACITY);
int tail = (client->ready_head + client->num_ready) % HTTP_CLIENT_CAPACITY;
client->ready[tail] = conn - client->conns;
client->num_ready++;
}
}
int http_client_process_events(HTTP_Client *client,
EventRegister *reg)
{
SocketEvent events[HTTP_CLIENT_CAPACITY];
int num_events = socket_manager_translate_events(
&client->sockets, events, reg);
if (num_events < 0)
return -1;
for (int i = 0; i < num_events; i++) {
HTTP_ClientConn *conn = events[i].user;
if (events[i].type == SOCKET_EVENT_DISCONNECT) {
// TODO
if (conn != NULL) {
http_client_conn_free(conn);
conn->state = HTTP_CLIENT_CONN_FREE;
client->num_conns--;
}
} else if (events[i].type == SOCKET_EVENT_READY) {
// TODO
}
if (conn == NULL)
continue;
// TODO
// Store the handle if this is a new connection
if (conn->handle == SOCKET_HANDLE_INVALID)
conn->handle = events[i].handle;
if (conn->state == HTTP_CLIENT_CONN_FLUSHING) {
// Send request data
int num = 0;
ByteView src = byte_queue_read_buf(&conn->output);
if (src.len)
num = socket_send(&client->sockets, conn->handle, src.ptr, src.len);
byte_queue_read_ack(&conn->output, num);
if (byte_queue_error(&conn->output)) {
socket_close(&client->sockets, conn->handle);
} else if (byte_queue_empty(&conn->output)) {
// Request fully sent, now wait for response
conn->state = HTTP_CLIENT_CONN_BUFFERING;
}
} else if (conn->state == HTTP_CLIENT_CONN_BUFFERING) {
// Receive response data
int min_recv = 1<<10;
byte_queue_write_setmincap(&conn->input, min_recv);
int num = 0;
ByteView dst = byte_queue_write_buf(&conn->input);
if (dst.len)
num = socket_recv(&client->sockets, conn->handle, dst.ptr, dst.len);
byte_queue_write_ack(&conn->input, num);
if (byte_queue_error(&conn->input))
socket_close(&client->sockets, conn->handle);
else
check_response_buffer(client, conn);
}
}
}
return 0;
@@ -112,7 +399,7 @@ bool http_client_next_response(HTTP_Client *client,
if (client->num_ready == 0)
return false;
HTTP_ClientConn *conn = &client->conns[client->ready_head];
HTTP_ClientConn *conn = &client->conns[client->ready[client->ready_head]];
client->ready_head = (client->ready_head + 1) % HTTP_CLIENT_CAPACITY;
client->num_ready--;
@@ -123,5 +410,16 @@ bool http_client_next_response(HTTP_Client *client,
void http_free_response(HTTP_Response *res)
{
// TODO
if (res == NULL || res->context == NULL)
return;
HTTP_ClientConn *conn = (HTTP_ClientConn*) res->context;
// Free the connection resources
http_client_conn_free(conn);
conn->state = HTTP_CLIENT_CONN_FREE;
conn->client->num_conns--;
// Mark response as freed
res->context = NULL;
}