Skip to content

Commit 0843de0

Browse files
Merge pull request #187 from basecamp/chunked-encoding-404s
Report malformed chunked requests as 400, not 500
2 parents a232870 + 289bec8 commit 0843de0

File tree

6 files changed

+106
-6
lines changed

6 files changed

+106
-6
lines changed

internal/server/errors.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package server
2+
3+
func isChunkedEncodingError(err error) bool {
4+
if err == nil {
5+
return false
6+
}
7+
8+
// The chunked encoding support in the stdlib returns these failures as plain
9+
// errors using errors.New, so matching them means string matching on the
10+
// error message, unfortunately.
11+
switch err.Error() {
12+
case "invalid byte in chunk length",
13+
"http chunk length too large",
14+
"malformed chunked encoding",
15+
"trailer header without chunked transfer encoding",
16+
"too many trailers":
17+
return true
18+
}
19+
20+
return false
21+
}

internal/server/request_buffer_middleware.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package server
22

33
import (
4+
"errors"
45
"log/slog"
56
"net/http"
67
)
@@ -22,11 +23,14 @@ func WithRequestBufferMiddleware(maxMemBytes, maxBytes int64, next http.Handler)
2223
func (h *RequestBufferMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
2324
requestBuffer, err := NewBufferedReadCloser(r.Body, h.maxBytes, h.maxMemBytes)
2425
if err != nil {
25-
if err == ErrMaximumSizeExceeded {
26-
http.Error(w, "Request too large", http.StatusRequestEntityTooLarge)
26+
if errors.Is(err, ErrMaximumSizeExceeded) {
27+
SetErrorResponse(w, r, http.StatusRequestEntityTooLarge, nil)
28+
} else if isChunkedEncodingError(err) {
29+
slog.Info("Malformed chunked request", "path", r.URL.Path, "error", err)
30+
SetErrorResponse(w, r, http.StatusBadRequest, nil)
2731
} else {
2832
slog.Error("Error buffering request", "path", r.URL.Path, "error", err)
29-
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
33+
SetErrorResponse(w, r, http.StatusInternalServerError, nil)
3034
}
3135
return
3236
}

internal/server/request_buffer_middleware_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package server
22

33
import (
4+
"bufio"
5+
"net"
46
"net/http"
57
"net/http/httptest"
68
"strings"
79
"testing"
810

911
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
1013
)
1114

1215
func TestRequestBufferMiddleware(t *testing.T) {
@@ -35,3 +38,31 @@ func TestRequestBufferMiddleware(t *testing.T) {
3538
assert.Equal(t, http.StatusRequestEntityTooLarge, w.Result().StatusCode)
3639
})
3740
}
41+
42+
func TestRequestBufferMiddleware_MalformedChunkedEncoding(t *testing.T) {
43+
middleware := WithRequestBufferMiddleware(1024, 4096, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
44+
w.Write([]byte("ok"))
45+
}))
46+
47+
server := httptest.NewServer(middleware)
48+
t.Cleanup(server.Close)
49+
50+
conn, err := net.Dial("tcp", server.Listener.Addr().String())
51+
require.NoError(t, err)
52+
defer conn.Close()
53+
54+
rawRequest := "POST / HTTP/1.1\r\n" +
55+
"Host: example.com\r\n" +
56+
"Transfer-Encoding: chunked\r\n" +
57+
"\r\n" +
58+
"ZZ\r\n"
59+
60+
_, err = conn.Write([]byte(rawRequest))
61+
require.NoError(t, err)
62+
63+
resp, err := http.ReadResponse(bufio.NewReader(conn), nil)
64+
require.NoError(t, err)
65+
defer resp.Body.Close()
66+
67+
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
68+
}

internal/server/response_buffer_middleware.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package server
22

33
import (
44
"bufio"
5+
"errors"
56
"log/slog"
67
"net"
78
"net/http"
@@ -31,12 +32,12 @@ func (h *ResponseBufferMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Requ
3132

3233
err := responseWriter.Send()
3334
if err != nil {
34-
if err == ErrMaximumSizeExceeded {
35+
if errors.Is(err, ErrMaximumSizeExceeded) {
3536
slog.Info("Response exceeded max response limit", "path", r.URL.Path)
36-
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
37+
SetErrorResponse(w, r, http.StatusInternalServerError, nil)
3738
} else {
3839
slog.Error("Error sending response", "path", r.URL.Path, "error", err)
39-
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
40+
SetErrorResponse(w, r, http.StatusInternalServerError, nil)
4041
}
4142
return
4243
}

internal/server/target.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,12 @@ func (t *Target) handleProxyError(w http.ResponseWriter, r *http.Request, err er
382382
return
383383
}
384384

385+
if isChunkedEncodingError(err) {
386+
slog.Info("Malformed request", "target", t.Address(), "path", r.URL.Path, "error", err)
387+
SetErrorResponse(w, r, http.StatusBadRequest, nil)
388+
return
389+
}
390+
385391
slog.Error("Error while proxying", "target", t.Address(), "path", r.URL.Path, "error", err)
386392
SetErrorResponse(w, r, http.StatusBadGateway, nil)
387393
}

internal/server/target_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,43 @@ func TestTarget_EnforceMaxBodySizes(t *testing.T) {
543543
})
544544
}
545545

546+
func TestTarget_MalformedChunkedEncodingWithoutBuffering(t *testing.T) {
547+
targetOptions := TargetOptions{
548+
BufferRequests: false,
549+
HealthCheckConfig: defaultHealthCheckConfig,
550+
}
551+
552+
target := testTargetWithOptions(t, targetOptions, func(w http.ResponseWriter, r *http.Request) {
553+
w.Write([]byte("ok"))
554+
})
555+
556+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
557+
r, err := target.StartRequest(r)
558+
require.NoError(t, err)
559+
target.SendRequest(w, r)
560+
}))
561+
t.Cleanup(server.Close)
562+
563+
conn, err := net.Dial("tcp", server.Listener.Addr().String())
564+
require.NoError(t, err)
565+
defer conn.Close()
566+
567+
rawRequest := "POST / HTTP/1.1\r\n" +
568+
"Host: example.com\r\n" +
569+
"Transfer-Encoding: chunked\r\n" +
570+
"\r\n" +
571+
"ZZ\r\n"
572+
573+
_, err = conn.Write([]byte(rawRequest))
574+
require.NoError(t, err)
575+
576+
resp, err := http.ReadResponse(bufio.NewReader(conn), nil)
577+
require.NoError(t, err)
578+
defer resp.Body.Close()
579+
580+
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
581+
}
582+
546583
func TestTarget_buildHealthCheckURL(t *testing.T) {
547584
tests := []struct {
548585
name string

0 commit comments

Comments
 (0)