httpclient: add support for chunked transfer encoding for request bodies

Tue, 17 Feb 2026 21:02:57 +0100

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Tue, 17 Feb 2026 21:02:57 +0100
changeset 680
02935baa186b
parent 679
4885cd4c3754
child 681
e9705d51866a

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);
 }

mercurial