]> git.kaiwu.me - haproxy.git/commitdiff
MINOR: http-act: Add 'no-fail' option on 'wait-for-body' action
authorChristopher Faulet <cfaulet@haproxy.com>
Mon, 2 Nov 2026 20:20:02 +0000 (21:20 +0100)
committerChristopher Faulet <cfaulet@haproxy.com>
Mon, 2 Nov 2026 20:25:24 +0000 (21:25 +0100)
With this option, the "wait-for-body" action does not return an error at the
end of the timeout but continues the message analysis with the body received
so far, instead of responding with a 408 "Request Timeout" error to the
client on the request side or a 504 "Gateway Timeout" error on the response
side. The remaining body bytes, if any, are streamed to the server or to the
client once received.

doc/configuration.txt
include/haproxy/http_ana.h
reg-tests/http-messaging/http_wait_for_body_no_fail.vtc [new file with mode: 0644]
src/http_act.c
src/http_ana.c

index bf3a95e48b256ec92acb135bb1c33845736767d6..700fee9806a59eb33c916e717e5e2aa737e1b1c5 100644 (file)
@@ -17865,7 +17865,7 @@ use-service <service-name>
     http-request use-service prometheus-exporter if { path /metrics }
 
 
-wait-for-body time <time> [ at-least <bytes> ] [use-large-buffer]
+wait-for-body time <time> [ at-least <bytes> ] [use-large-buffer] [no-fail]
   Usable in:  QUIC Ini|    TCP RqCon| RqSes| RqCnt| RsCnt|    HTTP Req| Res| Aft
                     - |          -  |   -  |   -  |   -  |          X |  X |  -
 
@@ -17889,6 +17889,11 @@ wait-for-body time <time> [ at-least <bytes> ] [use-large-buffer]
   one is to small to store the message body. To be used, "tune.bufsize.large"
   global option must be defined.
 
+  "no-fail" option may be set to not return an error to the client at the end
+  of the <time> timeout but to continue the message analysis with the body
+  received so far. The remaining body bytes, if any, are forwarded once
+  received.
+
   This action may be used as a replacement for "option http-buffer-request".
 
   Arguments :
@@ -17902,6 +17907,7 @@ wait-for-body time <time> [ at-least <bytes> ] [use-large-buffer]
 
   Example:
     http-request wait-for-body time 1s at-least 1k if METH_POST
+    http-response wait-for-body time 1s no-fail if { status eq 200 }
 
   See also : "option http-buffer-request" and "tune.bufsize.large"
 
index 4dd80e64743562726fb179bcaeee7780d213bacf..2b3445a73c753f8efc1a0cd9d3e192d3fe789933 100644 (file)
@@ -49,7 +49,7 @@ int http_req_replace_stline(int action, const char *replace, int len,
 int http_res_set_status(unsigned int status, struct ist reason, struct stream *s);
 void http_check_request_for_cacheability(struct stream *s, struct channel *req);
 void http_check_response_for_cacheability(struct stream *s, struct channel *res);
-enum rule_result http_wait_for_msg_body(struct stream *s, struct channel *chn, unsigned int time, unsigned int bytes, unsigned int large_buffer);
+enum rule_result http_wait_for_msg_body(struct stream *s, struct channel *chn, unsigned int time, unsigned int bytes, unsigned int large_buffer, unsigned int no_fail);
 void http_perform_server_redirect(struct stream *s, struct stconn *sc);
 void http_server_error(struct stream *s, struct stconn *sc, int err, int finst, struct http_reply *msg);
 void http_reply_and_close(struct stream *s, short status, struct http_reply *msg);
diff --git a/reg-tests/http-messaging/http_wait_for_body_no_fail.vtc b/reg-tests/http-messaging/http_wait_for_body_no_fail.vtc
new file mode 100644 (file)
index 0000000..917896d
--- /dev/null
@@ -0,0 +1,72 @@
+varnishtest "A test for the no-fail option of the wait-for-body HTTP action"
+feature ignore_unknown_macro
+
+#REGTEST_TYPE=slow
+
+server s1 {
+    rxreq
+    expect req.bodylen == 1000
+    txresp
+} -start
+
+server s2 {
+    rxreq
+    send "HTTP/1.1 200 OK\r\n"
+    send "Content-Length: 1000\r\n\r\n"
+    delay 0.15
+    send_n 13 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789=====================================\n"
+} -start
+
+haproxy h1 -conf {
+    global
+    .if feature(THREAD)
+        thread-groups 1
+    .endif
+
+    defaults
+        mode http
+        timeout client  "${HAPROXY_TEST_TIMEOUT-5s}"
+        timeout server  "${HAPROXY_TEST_TIMEOUT-5s}"
+        timeout connect "${HAPROXY_TEST_TIMEOUT-5s}"
+
+    frontend fe1
+        bind "fd@${fe1}"
+        http-request wait-for-body time 50ms no-fail
+        default_backend be1
+
+    backend be1
+        server srv1 ${s1_addr}:${s1_port}
+
+    frontend fe2
+        bind "fd@${fe2}"
+        http-response wait-for-body time 50ms no-fail
+        default_backend be2
+
+    backend be2
+        server srv1 ${s2_addr}:${s2_port}
+} -start
+
+# The wait-for-body timeout expires with only part of the request body
+# received. Thanks to the no-fail option, the request is processed and
+# forwarded to the server anyway. The remaining body bytes are streamed to
+# the server once received.
+client c1 -connect ${h1_fe1_sock} {
+    send "POST / HTTP/1.1\r\n"
+    send "Content-Length: 1000\r\n\r\n"
+    send "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789=====================================\n"
+    delay 0.15
+    send_n 12 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789=====================================\n"
+    rxresp
+    expect resp.status == 200
+} -run
+
+# Same test on the response side. The wait-for-body timeout expires with
+# only part of the response body received, but the response is processed
+# and forwarded to the client anyway. The remaining body bytes are streamed
+# to the client once received.
+client c2 -connect ${h1_fe2_sock} {
+    txreq
+    rxresp
+    expect resp.status == 200
+    expect resp.bodylen == 1000
+} -run
index 640e139627d7e8798f78a568c6435f022e35f7a2..6e10f4942f5f562c87af37162152a362d03f868d 100644 (file)
@@ -2708,7 +2708,8 @@ static enum act_parse_ret parse_http_return(const char **args, int *orig_arg, st
  * Otherwise ACT_RET_YIELD is returned to wait for more data. ACT_RET_INV is
  * returned if a parsing error is raised by lower level and ACT_RET_ERR if an
  * internal error occurred. Finally ACT_RET_ABRT is returned when a timeout
- * occurred.
+ * occurred, unless the 'no-fail' option is set, in which case ACT_RET_CONT is
+ * returned to continue the analysis.
  */
 static enum act_return http_action_wait_for_body(struct act_rule *rule, struct proxy *px,
                                                 struct session *sess, struct stream *s, int flags)
@@ -2717,8 +2718,9 @@ static enum act_return http_action_wait_for_body(struct act_rule *rule, struct p
        unsigned int time = (uintptr_t)rule->arg.act.p[0];
        unsigned int bytes = (uintptr_t)rule->arg.act.p[1];
        unsigned int large_buffer = (uintptr_t)rule->arg.act.p[2];
+       unsigned int no_fail = (uintptr_t)rule->arg.act.p[3];
 
-       switch (http_wait_for_msg_body(s, chn, time, bytes, large_buffer)) {
+       switch (http_wait_for_msg_body(s, chn, time, bytes, large_buffer, no_fail)) {
        case HTTP_RULE_RES_CONT:
                return ACT_RET_CONT;
        case HTTP_RULE_RES_YIELD:
@@ -2756,18 +2758,19 @@ static enum act_parse_ret parse_http_wait_for_body(const char **args, int *orig_
                                                   struct act_rule *rule, char **err)
 {
        int cur_arg;
-       unsigned int time, bytes, large_buffer;
+       unsigned int time, bytes, large_buffer, no_fail;
        const char *res;
 
        cur_arg = *orig_arg;
        if (!*args[cur_arg]) {
-               memprintf(err, "expects time <time> [ at-least <bytes> ]");
+               memprintf(err, "expects time <time> [ at-least <bytes> ] [use-large-buffer] [no-fail]");
                return ACT_RET_PRS_ERR;
        }
 
        time = UINT_MAX; /* To be sure it is set */
        bytes = 0; /* Default value, wait all the body */
        large_buffer = 0; /* Don't use large buffers by default */
+       no_fail = 0; /* Report an error on timeout by default */
        while (*(args[cur_arg])) {
                if (strcmp(args[cur_arg], "time") == 0) {
                        if (!*args[cur_arg + 1]) {
@@ -2803,19 +2806,22 @@ static enum act_parse_ret parse_http_wait_for_body(const char **args, int *orig_
                }
                else if (strcmp(args[cur_arg], "use-large-buffer") == 0)
                        large_buffer = 1;
+               else if (strcmp(args[cur_arg], "no-fail") == 0)
+                       no_fail = 1;
                else
                        break;
                cur_arg++;
        }
 
        if (time == UINT_MAX) {
-               memprintf(err, "expects time <time> [ at-least <bytes> ]");
+               memprintf(err, "expects time <time> [ at-least <bytes> ] [use-large-buffer] [no-fail]");
                return ACT_RET_PRS_ERR;
        }
 
        rule->arg.act.p[0] = (void *)(uintptr_t)time;
        rule->arg.act.p[1] = (void *)(uintptr_t)bytes;
        rule->arg.act.p[2] = (void *)(uintptr_t)large_buffer;
+       rule->arg.act.p[3] = (void *)(uintptr_t)no_fail;
 
        *orig_arg = cur_arg;
 
index f5f807134a19cd1598d24df528ff64fff449a165..a75c41a24efb1f23307a72f0a449d286fa3c6719 100644 (file)
@@ -849,7 +849,7 @@ int http_wait_for_request_body(struct stream *s, struct channel *req, int an_bit
                        &s->txn.http->req);
 
 
-       switch (http_wait_for_msg_body(s, req, s->be->timeout.httpreq, 0, 0)) {
+       switch (http_wait_for_msg_body(s, req, s->be->timeout.httpreq, 0, 0, 0)) {
        case HTTP_RULE_RES_CONT:
                s->waiting_entity.type = STRM_ENTITY_NONE;
                s->waiting_entity.ptr = NULL;
@@ -4353,10 +4353,11 @@ static int http_handle_stats(struct stream *s, struct channel *req, struct proxy
  *
  * If a timeout occurred, this function is responsible to emit the right response
  * to the client, depending on the channel (408 on request side, 504 on response
- * side). All other errors must be handled by the caller.
+ * side), unless <no_fail> is set. All other errors must be handled by the caller.
  */
 enum rule_result http_wait_for_msg_body(struct stream *s, struct channel *chn,
-                                       unsigned int time, unsigned int bytes, unsigned int large_buffer)
+                                       unsigned int time, unsigned int bytes,
+                                       unsigned int large_buffer, unsigned int no_fail)
 {
        struct session *sess = s->sess;
        struct http_txn *txn = s->txn.http;
@@ -4417,6 +4418,8 @@ enum rule_result http_wait_for_msg_body(struct stream *s, struct channel *chn,
        }
 
        if ((chn->flags & CF_READ_TIMEOUT) || tick_is_expired(chn->analyse_exp, now_ms)) {
+               if (no_fail)
+                       goto end; /* on timeout, stop waiting and continue the analysis */
                if (!(chn->flags & CF_ISRESP))
                        goto abort_req;
                goto abort_res;