AtlatestRepositorysigil-http
sigil-http / tree / test / integrationlive-timeouts-main.sgl
1
;;; Live positive controls for the blocking-segment bounds.2
;;;3
;;; Run by test/integration/run-timeout-tests.sh, which stands up the4
;;; servers and passes their ports in the environment.5
;;;6
;;; Named `-main.sgl` rather than `test-*.sgl` deliberately: `sigil test`7
;;; globs test/**/test-*.sgl, so a `test-` name here would be picked up by a8
;;; bare `sigil test` and fail with no servers running. The other files in9
;;; this directory follow the same convention for the same reason.10
;;;11
;;; WHY THIS FILE EXISTS. Every in-suite test of the bounds asserts a12
;;; FAILURE: a stalled peer must give up on time. Not one of them would13
;;; notice if the bounds broke every SUCCESSFUL request, because a client14
;;; that could no longer complete a TLS handshake at all would satisfy all15
;;; of them. The whole point here is the other direction: real servers,16
;;; bounds armed, requests must still work.17
;;;18
;;; The HTTPS case is the load-bearing one. The handshake bound is new code19
;;; in the middle of every outbound TLS connection in the estate.21
(import (sigil test)22
(sigil core)23
(sigil time)24
(sigil tls)25
(sigil http client)26
(sigil http response))28
(define (env-port name)29
(let ((v (getenv name)))30
(and v (string->number v))))32
(define http-port (env-port "SIGIL_HTTP_TEST_HTTP_PORT"))33
(define tls-port (env-port "SIGIL_HTTP_TEST_TLS_PORT"))34
(define drip-port (env-port "SIGIL_HTTP_TEST_DRIP_PORT"))36
(define (http-url port path)37
(string-append "http://127.0.0.1:" (number->string port) path))39
(define (https-url port path)40
(string-append "https://127.0.0.1:" (number->string port) path))42
(define (seconds-of thunk)43
(let* ((t0 (current-second))44
(v (thunk)))45
(cons (- (current-second) t0) v)))47
;; A missing port is a HARNESS FAILURE, not a reason to skip. A test that48
;; quietly passes because it never ran is the exact failure mode these49
;; bounds exist to eliminate.50
(test-group "live timeout positive controls"52
(test "harness: all three servers were configured"53
(assert-true (and http-port tls-port drip-port)))55
;; --- Default behaviour is unchanged --------------------------------57
(test "plain http with NO timeout arguments still works"58
(let ((res (http-get (http-url http-port "/"))))59
(assert-true (http-response? res))60
(assert-equal 200 (http-response-status res))))62
(test "https with NO timeout arguments still works"63
;; The handshake bound must be genuinely opt-in. With no arguments the64
;; original blocking handshake has to run and succeed.65
(let ((res (http-get (https-url tls-port "/"))))66
(assert-true (http-response? res))67
(assert-equal 200 (http-response-status res))))69
;; --- The bounds do not break healthy requests ----------------------71
(test "https with the handshake bound ARMED still completes a real handshake"72
(let* ((measured (seconds-of73
(lambda () (http-get (https-url tls-port "/")74
connect-timeout: 5 timeout: 5))))75
(elapsed (car measured))76
(res (cdr measured)))77
(assert-true (http-response? res))78
(assert-equal 200 (http-response-status res))79
;; Promptly, not just eventually: a bound that made every handshake80
;; wait out its deadline before succeeding would still return 200.81
(assert-true (< elapsed 2))))83
(test "plain http with both bounds armed still works"84
(let ((res (http-get (http-url http-port "/")85
connect-timeout: 5 timeout: 5)))86
(assert-true (http-response? res))87
(assert-equal 200 (http-response-status res))))89
(test "a POST body still arrives intact through the bounded write loop"90
;; The write path is no longer one blocking call; it is a partial-write91
;; loop over byte offsets. An off-by-one there would corrupt or truncate92
;; the body while every timeout test stayed green. The server echoes the93
;; body length back.94
(let* ((body (make-string 100000 #\z))95
(res (http-post (http-url http-port "/echo-length") body96
connect-timeout: 5 timeout: 10)))97
(assert-true (http-response? res))98
(assert-equal 200 (http-response-status res))99
(assert-equal "100000" (http-response-body res))))101
(test "a POST body still arrives intact over TLS"102
(let* ((body (make-string 100000 #\z))103
(res (http-post (https-url tls-port "/echo-length") body104
connect-timeout: 5 timeout: 10)))105
(assert-true (http-response? res))106
(assert-equal 200 (http-response-status res))107
(assert-equal "100000" (http-response-body res))))109
(test "a partial write resumes at the right offset"110
;; The write is no longer one blocking call; it is a loop over byte111
;; offsets that resumes after a partial send. Every OTHER test of that112
;; loop asserts only that it TIMED OUT, so an off-by-one in the resume113
;; (passing the remaining length as `end`, say) would corrupt or truncate114
;; bodies while the whole suite stayed green.115
;;116
;; 4 MiB is above the measured EAGAIN threshold on this host (~2.6 MiB),117
;; so partial writes definitely occur, and the server is draining, so the118
;; loop definitely completes. The echoed length is the correctness check.119
(let* ((body (make-string (* 4 1024 1024) #\w))120
(res (http-post (http-url http-port "/echo-length") body121
connect-timeout: 10 timeout: 30)))122
(assert-true (http-response? res))123
(assert-equal 200 (http-response-status res))124
(assert-equal "4194304" (http-response-body res))))126
;; --- The handshake bound, against BOTH stall geometries -------------128
(test "tls-connect/status reports connected for a live peer"129
;; The only place in either repo that asserts the "connected" status.130
;; Without it, `*status_out = "connected"` could be deleted from131
;; tls_connect_core and every other test would still pass.132
(let ((r (tls-connect/status "127.0.0.1" tls-port 5000 5000)))133
(assert-equal "connected" (car r))134
(assert-true (cdr r))135
(tls-close (cdr r))))137
(test "the handshake bound fires against a DRIPPING peer, not just a silent one"138
;; THE CONTROL MY FIRST RIG WAS MISSING, and the reason the first version139
;; of this bound was wrong.140
;;141
;; A peer that sends NOTHING cannot distinguish a per-read bound from a142
;; per-handshake one: both fire. This peer sends a valid TLS record143
;; header declaring a 16384-byte body and then drips one byte every144
;; 600 ms. Against a per-read bound each byte restarts the clock:145
;; measured at 13.06 s for a 1000 ms bound, and never firing at all while146
;; the drip continued. Against a total bound it fires on time.147
(let* ((measured (seconds-of148
(lambda () (tls-connect/status "127.0.0.1" drip-port149
1000 1000))))150
(elapsed (car measured))151
(r (cdr measured)))152
;; It must be a TIMEOUT, not a protocol rejection. If the drip peer's153
;; header were malformed the client would bail instantly with154
;; "handshake-failed" and this test would prove nothing.155
(assert-equal "handshake-timeout" (car r))156
(assert-false (cdr r))157
;; It must have WAITED, so we know bytes were arriving and being158
;; consumed rather than the connection failing outright.159
(assert-true (>= elapsed 0.5))160
;; And it must not have waited much past its own deadline. This is the161
;; assertion that fails on a per-read bound: 13.06 s against 1 s.162
(assert-true (< elapsed 4))))164
(test "http-fetch-bytes still returns the body byte-for-byte"165
;; http-fetch-bytes now sets the connection non-blocking so its own idle166
;; deadlines can fire. If that broke its reads, this returns short.167
(let ((r (http-fetch-bytes 'GET (http-url http-port "/bytes") timeout: 10)))168
(assert-true (dict? r))169
(assert-equal 200 (dict-ref r status: #f))170
(assert-equal 65536 (bytevector-length (dict-ref r body: #f))))))172
(run-tests)