A client that stops sending with little of its body left after the app has answered is timed out (closes #136)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
When less than 256 KiB of a client's body is left, Go's server reads the rest before it sends the app's answer, and the transport reads it as it closes the body after a failed write to the app. Neither read goes through requestBody.Read, so when SWWAF_CLIENT_REQUEST_TIMEOUT ran out smallwebwaf took the 504 branch, which set no read deadline, and the request held its connection for as long as the client kept it open. That branch now ends reading the client's body at the client's deadline too. Model: opus-5-5
This commit is contained in:
@@ -715,6 +715,12 @@ func (rq *request) requestTimedOut(limit string) {
|
||||
action: requestlog.ActionTimedOut,
|
||||
limit: limit,
|
||||
})
|
||||
// Go's server, before it sends the app's answer, and the
|
||||
// transport, as it closes the body, read what is left of a body
|
||||
// under 256 KiB without requestBody.Read, so waiting is false
|
||||
// while they wait for the client. Those reads end when the client
|
||||
// is out of time.
|
||||
rq.stopReadingBody(rq.clientRequestDeadline())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
@@ -248,6 +249,59 @@ func TestClientStopsSendingAfterTheAppHasAnswered(t *testing.T) {
|
||||
wantLimitHits(t, addr, clientRequestTimeout, 1)
|
||||
}
|
||||
|
||||
func TestClientStopsSendingWithLittleOfItsBodyLeft(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// appClosed is closed once smallwebwaf has closed its connection to
|
||||
// the app, which it does once it has the app's answer.
|
||||
appClosed := make(chan struct{})
|
||||
|
||||
// The app answers as soon as it has the headers, without reading the
|
||||
// body, and says it closes the connection.
|
||||
app := startApp(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||
conn, buf, err := http.NewResponseController(w).Hijack()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = buf.WriteString("HTTP/1.1 404 Not Found\r\n" +
|
||||
"Content-Length: 0\r\nConnection: close\r\n\r\n")
|
||||
_ = buf.Flush()
|
||||
_, _ = io.Copy(io.Discard, buf.Reader)
|
||||
_ = conn.Close()
|
||||
|
||||
close(appClosed)
|
||||
})
|
||||
addr, out := startProxy(t, app.URL, map[string]string{
|
||||
clientRequestTimeout: shortTimeoutSetting,
|
||||
metricsToken: token,
|
||||
})
|
||||
|
||||
// Less than 256 KiB of the body is left after the first bytes, little
|
||||
// enough for Go's server in smallwebwaf to read the rest itself before
|
||||
// it sends the app's answer.
|
||||
start := time.Now()
|
||||
conn := dial(t, addr)
|
||||
send(t, conn, "POST /upload HTTP/1.1\r\nHost: app\r\nContent-Length: 200000\r\n\r\n"+
|
||||
"the first bytes")
|
||||
|
||||
select {
|
||||
case <-appClosed:
|
||||
case <-time.After(waitLimit):
|
||||
t.Fatal("smallwebwaf did not close its connection to the app")
|
||||
}
|
||||
|
||||
// More of the body, which can no longer go to the app, and then
|
||||
// nothing.
|
||||
send(t, conn, strings.Repeat("x", 8<<10))
|
||||
|
||||
got := readResponse(t, conn)
|
||||
wantTimedOut(t, start)
|
||||
wantStatus(t, got, http.StatusNotFound)
|
||||
wantLine(t, out.requestLine(t), http.StatusNotFound, requestlog.ActionTimedOut)
|
||||
wantLimitHits(t, addr, clientRequestTimeout, 1)
|
||||
}
|
||||
|
||||
func TestAppTooSlowToAnswer(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user