]> git.kaiwu.me - haproxy.git/commitdiff
REGTESTS: http-messaging: test the option httpclose
authorWilly Tarreau <w@1wt.eu>
Tue, 22 Sep 2026 20:39:11 +0000 (20:39 +0000)
committerWilly Tarreau <w@1wt.eu>
Wed, 30 Sep 2026 15:25:37 +0000 (17:25 +0200)
The option only appeared in other tests as a convenient way to get rid
of the idle connections, its own behavior was never checked, and
"option http-pretend-keepalive", which modifies it, was not covered
either.

The five proxies here cover the four ways it can be declared. The side
whose connection is closed is the one the option is declared on: the
client one for a frontend, the server one for a backend, both for a
listener, and the "no option" form undoes what a defaults section had
set. The header it adds in that direction is checked by the server for
the request side and by the client for the response side.

Whether the client connection was closed is directly observable, and
whether the server one was is reported by bc_reused, since a connection
which was closed cannot be reused by the next request: the frontend
case reuses its server connection while the listener case cannot. This
is also how "http-pretend-keepalive" is checked, the server not seeing
the header while its connection is closed all the same.

reg-tests/http-messaging/httpclose.vtc [new file with mode: 0644]

diff --git a/reg-tests/http-messaging/httpclose.vtc b/reg-tests/http-messaging/httpclose.vtc
new file mode 100644 (file)
index 0000000..497bd53
--- /dev/null
@@ -0,0 +1,215 @@
+varnishtest "Test the option httpclose"
+
+# "option httpclose" only appeared in other tests as a way to get rid of
+# the idle connections, its own behavior was never checked. It closes
+# the connection of the side it is declared on, the frontend for the
+# client one and the backend for the server one, a listener counting for
+# both, and it adds a "Connection: close" header in that direction if
+# there is none.
+#
+# Whether the client connection was closed is directly observable, and
+# whether the server one was is reported by bc_reused: a connection
+# which was closed cannot be reused by the next request, so the fetch
+# stays at zero. The backends which are expected to reuse their
+# connection ask for "http-reuse always" and the process is pinned to a
+# single thread since the idle connections are per thread.
+
+feature ignore_unknown_macro
+
+# fe1: only the client connection is closed, the server one is reused by
+# the second client, hence a single connection serving two requests
+server s1 {
+       rxreq
+       expect req.http.connection == "<undef>"
+       txresp
+       rxreq
+       expect req.http.connection == "<undef>"
+       txresp
+} -start
+
+# be2: only the server connection is closed, and the request carries the
+# header, hence one connection per request
+server s2 {
+       rxreq
+       expect req.http.connection == "close"
+       txresp
+} -repeat 2 -start
+
+# fe3 is a listener, so both sides are closed
+server s3 {
+       rxreq
+       expect req.http.connection == "close"
+       txresp
+} -repeat 2 -start
+
+# be4 adds "http-pretend-keepalive": the server must not see the header
+# although its connection is closed once the response is received
+server s4 {
+       rxreq
+       expect req.http.connection == "<undef>"
+       txresp
+} -repeat 2 -start
+
+# fe5 and be5 undo the option inherited from their defaults section, so
+# both sides stay in keep-alive
+server s5 {
+       rxreq
+       txresp
+       rxreq
+       txresp
+} -start
+
+haproxy h1 -conf {
+    global
+    .if feature(THREAD)
+        thread-groups 1
+    .endif
+        chroot /
+       nbthread 1
+
+    defaults common
+       mode http
+       timeout connect "${HAPROXY_TEST_TIMEOUT-5s}"
+       timeout client  "${HAPROXY_TEST_TIMEOUT-5s}"
+       timeout server  "${HAPROXY_TEST_TIMEOUT-5s}"
+       http-reuse always
+
+    # the same, with the option, for the two proxies which undo it
+    defaults closing
+       mode http
+       timeout connect "${HAPROXY_TEST_TIMEOUT-5s}"
+       timeout client  "${HAPROXY_TEST_TIMEOUT-5s}"
+       timeout server  "${HAPROXY_TEST_TIMEOUT-5s}"
+       http-reuse always
+       option httpclose
+
+    frontend fe1 from common
+       bind "fd@${fe1}"
+       option httpclose
+       default_backend be1
+
+    backend be1 from common
+       http-after-response set-header x-reused "%[bc_reused]"
+       server s1 ${s1_addr}:${s1_port}
+
+    frontend fe2 from common
+       bind "fd@${fe2}"
+       default_backend be2
+
+    backend be2 from common
+       option httpclose
+       http-after-response set-header x-reused "%[bc_reused]"
+       server s2 ${s2_addr}:${s2_port}
+
+    listen fe3 from common
+       bind "fd@${fe3}"
+       option httpclose
+       http-after-response set-header x-reused "%[bc_reused]"
+       server s3 ${s3_addr}:${s3_port}
+
+    frontend fe4 from common
+       bind "fd@${fe4}"
+       default_backend be4
+
+    backend be4 from common
+       option httpclose
+       option http-pretend-keepalive
+       http-after-response set-header x-reused "%[bc_reused]"
+       server s4 ${s4_addr}:${s4_port}
+
+    # these two inherit the option and undo it
+    frontend fe5 from closing
+       bind "fd@${fe5}"
+       no option httpclose
+       default_backend be5
+
+    backend be5 from closing
+       no option httpclose
+       http-after-response set-header x-reused "%[bc_reused]"
+       server s5 ${s5_addr}:${s5_port}
+} -start
+
+# the option is on the frontend: the response announces the close, the
+# client connection is closed, and the server connection is not
+client c1a -connect ${h1_fe1_sock} {
+       txreq
+       rxresp
+       expect resp.status == 200
+       expect resp.http.connection == "close"
+       expect resp.http.x-reused == "0"
+       expect_close
+} -run
+
+client c1b -connect ${h1_fe1_sock} {
+       txreq
+       rxresp
+       expect resp.status == 200
+       expect resp.http.connection == "close"
+       # the server connection of the previous client was left intact
+       expect resp.http.x-reused == "1"
+       expect_close
+} -run
+
+# the option is on the backend: the client connection survives two
+# requests while each of them gets a new server connection
+client c2 -connect ${h1_fe2_sock} {
+       txreq
+       rxresp
+       expect resp.status == 200
+       expect resp.http.connection == "<undef>"
+       expect resp.http.x-reused == "0"
+
+       txreq
+       rxresp
+       expect resp.status == 200
+       expect resp.http.connection == "<undef>"
+       expect resp.http.x-reused == "0"
+} -run
+
+# a listener closes both sides
+client c3a -connect ${h1_fe3_sock} {
+       txreq
+       rxresp
+       expect resp.status == 200
+       expect resp.http.connection == "close"
+       expect resp.http.x-reused == "0"
+       expect_close
+} -run
+
+client c3b -connect ${h1_fe3_sock} {
+       txreq
+       rxresp
+       expect resp.status == 200
+       expect resp.http.connection == "close"
+       # the server connection was closed too, so there is nothing to reuse
+       expect resp.http.x-reused == "0"
+       expect_close
+} -run
+
+# "http-pretend-keepalive" only hides the header from the server
+client c4 -connect ${h1_fe4_sock} {
+       txreq
+       rxresp
+       expect resp.status == 200
+       expect resp.http.x-reused == "0"
+
+       txreq
+       rxresp
+       expect resp.status == 200
+       expect resp.http.x-reused == "0"
+} -run
+
+# "no option httpclose" restores the keep-alive on both sides
+client c5 -connect ${h1_fe5_sock} {
+       txreq
+       rxresp
+       expect resp.status == 200
+       expect resp.http.connection == "<undef>"
+       expect resp.http.x-reused == "0"
+
+       txreq
+       rxresp
+       expect resp.status == 200
+       expect resp.http.connection == "<undef>"
+       expect resp.http.x-reused == "1"
+} -run