Tue, 17 Feb 2026 21:02:57 +0100
httpclient: add support for chunked transfer encoding for request bodies
| src/server/proxy/httpclient.c | file | annotate | diff | comparison | revisions |
--- a/src/server/proxy/httpclient.c Tue Feb 17 20:03:06 2026 +0100 +++ b/src/server/proxy/httpclient.c Tue Feb 17 21:02:57 2026 +0100 @@ -41,6 +41,7 @@ static int client_finished(EventHandler *ev, Event *event); static int client_send_request(HttpClient *client); +static int client_send_request_body(HttpClient *client); HttpClient* http_client_new(EventHandler *ev) { CxMempool *mp = cxMempoolCreate(32, CX_MEMPOOL_TYPE_PURE); @@ -252,57 +253,6 @@ return client_io(ev, event); } -static int client_send_request_body(HttpClient *client) { - size_t rbody_readsize = client->req_buffer_alloc; - size_t rbody_buf_offset = 0; - if(client->req_content_length == -1) { - // chunked transfer encoding: - // don't fill req_buffer completely, reserve some space for - // a chunk header, that will be inserted at the beginning - rbody_readsize -= 16; - rbody_buf_offset = 16; - } - while(!client->request_body_complete) { - ssize_t r = client->request_body_read(client, client->req_buffer + rbody_buf_offset, rbody_readsize, client->request_body_read_userdata); - if(r <= 0) { - if(r == HTTP_CLIENT_CALLBACK_WOULD_BLOCK) { - return 1; - } else if(r == 0) { - // EOF - client->request_body_complete = 1; - break; - } else { - // error - client->error = 1; - return 1; - } - } - - size_t startpos = 0; - if(client->req_content_length == -1) { - char chunkheader[16]; - int chunkheaderlen = snprintf(chunkheader, 16, "%zx\r\n", (size_t)r); - startpos = 16 - chunkheaderlen; - memcpy(client->req_buffer + startpos, chunkheader, chunkheaderlen); - } - - client->req_contentlength_pos += r; - client->req_buffer_pos = startpos; - client->req_buffer_len = r; - if(client_send_request(client)) { - return 1; - } - } - - if(client->req_content_length > 0 && client->req_content_length != client->req_contentlength_pos) { - // incomplete request body - client->error = 1; - return 1; - } - - return 0; -} - static int client_io(EventHandler *ev, Event *event) { HttpClient *client = event->cookie; if(client->req_buffer_pos < client->req_buffer_len) { @@ -434,7 +384,65 @@ return client->req_buffer_pos < client->req_buffer_len; } +static int client_send_request_body(HttpClient *client) { + size_t rbody_readsize = client->req_buffer_alloc; + size_t rbody_buf_offset = 0; + if(client->req_content_length == -1) { + // chunked transfer encoding: + // don't fill req_buffer completely, reserve some space for + // a chunk header, that will be inserted at the beginning + rbody_readsize -= 16; + rbody_buf_offset = 16; + } + while(!client->request_body_complete) { + ssize_t r = client->request_body_read(client, client->req_buffer + rbody_buf_offset, rbody_readsize, client->request_body_read_userdata); + if(r <= 0) { + if(r == HTTP_CLIENT_CALLBACK_WOULD_BLOCK) { + return 1; + } else if(r == 0) { + // EOF + client->request_body_complete = 1; + break; + } else { + // error + client->error = 1; + return 1; + } + } + size_t startpos = 0; + if(client->req_content_length == -1) { + char chunkheader[16]; + int chunkheaderlen = snprintf(chunkheader, 16, "%zx\r\n", (size_t)r); + startpos = 16 - chunkheaderlen; + memcpy(client->req_buffer + startpos, chunkheader, chunkheaderlen); + } + + client->req_contentlength_pos += r; + client->req_buffer_pos = startpos; + client->req_buffer_len = rbody_buf_offset + r; + if(client_send_request(client)) { + return 1; + } + } + + // chunked transfer encoding: terminate + if(client->req_content_length == -1 && client->request_body_complete != 2) { + memcpy(client->req_buffer, "0\r\n", 3); + client->req_buffer_pos = 0; + client->req_buffer_len = 3; + if(client_send_request(client)) { + return 1; + } + + } else if(client->req_content_length != client->req_contentlength_pos) { + // incomplete request body + client->error = 1; + return 1; + } + + return 0; +} /* --------------------------------- Tests --------------------------------- */ @@ -625,7 +633,145 @@ } } + +typedef struct TestRequestBody { + char *content; + size_t length; + size_t pos; + int chunksize; + int max_reads; // max number of reads until test_request_body_read returns 0 + int cur_reads; // current number of read-attempts +} TestRequestBody; + +static ssize_t test_request_body_read(HttpClient *client, void *buf, size_t size, void *userdata) { + TestRequestBody *req = userdata; + req->cur_reads++; + if(req->chunksize == 0 || req->cur_reads > req->max_reads) { + return -1; + } + size_t max = req->length - req->pos; + if(max == 0) { + return 0; + } + + size_t sz = req->chunksize > size ? size : req->chunksize; + if(sz > max) { + sz = max; + } + memcpy(buf, req->content + req->pos, sz); + req->pos += sz; + return sz; +} + +static CX_TEST(test_http_client_send_request_body_chunked) { + CX_TEST_DO { + EventHandler dummy; + HttpClient *client = http_client_new(&dummy); + create_req_buffer(client); + client->req_content_length = -1; + + int fds[2]; + util_socketpair(fds); + util_socket_setnonblock(fds[0], 1); + util_socket_setnonblock(fds[1], 1); + client->socketfd = fds[0]; + int sock = fds[1]; + + // response buffer + CxBuffer buf; + cxBufferInit(&buf, cxDefaultAllocator, NULL, 1024, CX_BUFFER_AUTO_EXTEND|CX_BUFFER_FREE_CONTENTS); + + // test + char request_body[1024]; + memset(request_body, 'x', 1024); + memset(request_body+128, 'y', 128); + memset(request_body+384, 'z', 128); + memset(request_body+640, ':', 128); + memset(request_body+896, '!', 128); + + TestRequestBody req; + req.content = request_body; + req.length = 1024; + req.pos = 0; + req.chunksize = 16; + req.max_reads = 8; + req.cur_reads = 0; + client->request_body_read = test_request_body_read; + client->request_body_read_userdata = &req; + + memset(client->req_buffer, '_', client->req_buffer_alloc); + client->req_buffer_pos = 0; + client->req_buffer_len = 0; + + // send the first 128 bytes + while(req.cur_reads <= req.max_reads) { + int ret = client_send_request_body(client); + CX_TEST_ASSERT(ret == 1); + CX_TEST_ASSERT(!client->error); + char buf2[1024]; + ssize_t r = read(sock, buf2, 1024); + if(r > 0) { + cxBufferWrite(buf2, 1, r, &buf); + } + } + + // because we are using chunked transfer encoding, the result buffer + // (buf) should contain more than 128 bytes (additional chunk headers) + CX_TEST_ASSERT(buf.pos == 160); + // check for chunk headers + CX_TEST_ASSERT(!cx_strcmp(cx_strn(buf.space, 4), "10\r\n")); + CX_TEST_ASSERT(!cx_strcmp(cx_strn(buf.space+20, 4), "10\r\n")); + CX_TEST_ASSERT(!cx_strcmp(cx_strn(buf.space+40, 4), "10\r\n")); + CX_TEST_ASSERT(!cx_strcmp(cx_strn(buf.space+60, 4), "10\r\n")); + CX_TEST_ASSERT(!cx_strcmp(cx_strn(buf.space+80, 4), "10\r\n")); + CX_TEST_ASSERT(!cx_strcmp(cx_strn(buf.space+100, 4), "10\r\n")); + CX_TEST_ASSERT(!cx_strcmp(cx_strn(buf.space+120, 4), "10\r\n")); + CX_TEST_ASSERT(!cx_strcmp(cx_strn(buf.space+140, 4), "10\r\n")); + + // change chunk size to 128 + req.max_reads = 9999; + req.chunksize = 128; + while(req.cur_reads <= req.max_reads) { + int ret = client_send_request_body(client); + CX_TEST_ASSERT(!client->error); + char buf2[2048]; + ssize_t r; + while((r = read(sock, buf2, 2048)) > 0) { + cxBufferWrite(buf2, 1, r, &buf); + } + if(ret == 0) { + break; + } + } + CX_TEST_ASSERT(req.cur_reads < req.max_reads); + CX_TEST_ASSERT(buf.size == 1084 + 3); + + // check chunks + char testbuf[128]; + memset(testbuf, 'x', 128); + cxstring x1 = cx_strn(buf.space + 296, 128); + CX_TEST_ASSERT(!cx_strcmp(x1, cx_strn(testbuf, 128))); + cxstring x2 = cx_strn(buf.space + 560, 128); + CX_TEST_ASSERT(!cx_strcmp(x2, cx_strn(testbuf, 128))); + cxstring x3 = cx_strn(buf.space + 824, 128); + CX_TEST_ASSERT(!cx_strcmp(x3, cx_strn(testbuf, 128))); + memset(testbuf, 'y', 128); + cxstring y1 = cx_strn(buf.space + 164, 128); + CX_TEST_ASSERT(!cx_strcmp(y1, cx_strn(testbuf, 128))); + cxstring z1 = cx_strn(buf.space + 428, 128); + memset(testbuf, 'z', 128); + CX_TEST_ASSERT(!cx_strcmp(z1, cx_strn(testbuf, 128))); + + // cleanup + close(fds[0]); + close(fds[1]); + http_client_free(client); + cxBufferDestroy(&buf); + } +} + void http_client_add_tests(CxTestSuite *suite) { cx_test_register(suite, test_http_client_send_request); + cx_test_register(suite, test_http_client_send_request_body_chunked); cx_test_register(suite, test_http_client_io_simple); }