# haproxy-0004 — http_capture_headers O(H×C) per-request header capture scan ## Ecosystem haproxy (C) ## Severity MEDIUM — hot path: called for every HTTP request and response with `capture request header` or `capture response header` directives configured ## Location `src/http_ana.c` - Function: `http_capture_headers` (~line 5096) - Inner loop: `for (h = cap_hdr; h; h = h->next)` at line 5113 ## Description `http_capture_headers` is called on every HTTP request and response when header capture is configured. It iterates over all H headers in the HTX message, and for each header performs a linear O(C) walk of the `cap_hdr` linked list to check for a name match: ```c static void http_capture_headers(struct htx *htx, char **cap, struct cap_hdr *cap_hdr) { for (pos = htx_get_first(htx); pos != -1; pos = htx_get_next(htx, pos)) { // ... get header name n ... for (h = cap_hdr; h; h = h->next) { // O(C) per header if (h->namelen && (h->namelen == n.len) && (strncasecmp(n.ptr, h->name, h->namelen) == 0)) { // capture value } } } } ``` Total cost per request: O(H × C) where: - H = number of headers in the request/response (typically 10-50, up to 100+) - C = number of configured capture headers (can be dozens with complex configs) Since this runs on every HTTP request+response, it compounds: - Large H (100 headers) × large C (50 capture directives) = 5000 strncasecmp calls per request ## Fix Build a hash map from `cap_hdr` name → `cap_hdr *` at config parse time. On each request, look up each header name in O(1): ```c --- a/include/haproxy/proxy-t.h +++ b/include/haproxy/proxy-t.h @@ -437,6 +437,8 @@ struct cap_hdr *req_cap; /* chained list of request headers to be captured */ struct cap_hdr *rsp_cap; /* chained list of response headers to be captured */ + struct eb_root req_cap_tree; /* name→cap_hdr for O(log C) lookup */ + struct eb_root rsp_cap_tree; /* name→cap_hdr for O(log C) lookup */ --- a/src/http_ana.c +++ b/src/http_ana.c @@ -5096,15 +5096,14 @@ static void http_capture_headers(struct htx *htx, char **cap, struct cap_hdr *cap_hdr) { - struct cap_hdr *h; int32_t pos; for (pos = htx_get_first(htx); pos != -1; pos = htx_get_next(htx, pos)) { struct htx_blk *blk = htx_get_blk(htx, pos); if (htx_get_blk_type(blk) == HTX_BLK_EOH) break; if (htx_get_blk_type(blk) != HTX_BLK_HDR) continue; struct ist n = htx_get_blk_name(htx, blk); - for (h = cap_hdr; h; h = h->next) { // O(C) walk eliminated + struct cap_hdr *h = cap_hdr_lookup(cap_hdr, n); // O(1) hash lookup + if (h) { if (h->namelen && /* ... match ... */) { // capture value } } } } ``` ## Complexity | Variant | Cost | |---------|------| | Before | O(H × C) per request+response pair | | After | O(H) per request+response pair — O(1) hash lookup per header | | Speedup | C× (number of capture headers) | ## Notes - With C=1 (one capture directive) this is trivially already O(H); the defect matters when C grows with complex proxy configurations - The cap_hdr list is immutable after config parse, making it safe to index - HAProxy already uses various tree structures (ceb-trees) for similar purposes