Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 11, 12:15 UTC
reverseproxy: make stream copy buffer size configurable (#7627)
Zen Dodd committed
Apr 11, 2026 at 06:49 UTC
ca0ca67fbdb831c026d334dfd77ecc653f321876
5 files changed
+129
-4
caddytest/integration/caddyfile_adapt/reverse_proxy_stream_buffer_size.caddyfiletest
new
+56
@@ -0,0 +1,56 @@
1
+https://example.com {
2
+ reverse_proxy https://localhost:54321 {
3
+ stream_buffer_size 8KB
4
+ }
5
+}
6
+
7
+----------
8
+{
9
+ "apps": {
10
+ "http": {
11
+ "servers": {
12
+ "srv0": {
13
+ "listen": [
14
+ ":443"
15
+ ],
16
+ "routes": [
17
+ {
18
+ "match": [
19
+ {
20
+ "host": [
21
+ "example.com"
22
+ ]
23
+ }
24
+ ],
25
+ "handle": [
26
+ {
27
+ "handler": "subroute",
28
+ "routes": [
29
+ {
30
+ "handle": [
31
+ {
32
+ "handler": "reverse_proxy",
33
+ "stream_buffer_size": 8000,
34
+ "transport": {
35
+ "protocol": "http",
36
+ "tls": {}
37
+ },
38
+ "upstreams": [
39
+ {
40
+ "dial": "localhost:54321"
41
+ }
42
+ ]
43
+ }
44
+ ]
45
+ }
46
+ ]
47
+ }
48
+ ],
49
+ "terminal": true
50
+ }
51
+ ]
52
+ }
53
+ }
54
+ }
55
+ }
56
+}
modules/caddyhttp/reverseproxy/caddyfile.go
+4
-1
@@ -96,6 +96,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
96
// flush_interval <duration>
97
// request_buffers <size>
98
// response_buffers <size>
99
+// stream_buffer_size <size>
100
// stream_timeout <duration>
101
// stream_close_delay <duration>
102
// verbose_logs
@@ -646,7 +647,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
647
h.FlushInterval = caddy.Duration(dur)
648
}
649
649
- case "request_buffers", "response_buffers":
650
+ case "request_buffers", "response_buffers", "stream_buffer_size":
651
subdir := d.Val()
652
if !d.NextArg() {
653
return d.ArgErr()
@@ -670,6 +671,8 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
671
h.RequestBuffers = size
672
case "response_buffers":
673
h.ResponseBuffers = size
674
+ case "stream_buffer_size":
675
+ h.StreamBufferSize = int(size)
676
}
677
678
case "stream_timeout":
modules/caddyhttp/reverseproxy/reverseproxy.go
+6
@@ -171,6 +171,12 @@ type Handler struct {
171
// forcibly closed at the end of the timeout. Default: no timeout.
172
StreamTimeout caddy.Duration `json:"stream_timeout,omitempty"`
173
174
+ // The size of the buffer used for each direction of streaming
175
+ // requests such as WebSockets. If zero, the default size is 32 KiB.
176
+ // This only affects upgraded bidirectional streams, not normal
177
+ // request or response buffering.
178
+ StreamBufferSize int `json:"stream_buffer_size,omitempty"`
179
+
180
// If nonzero, streaming requests such as WebSockets will not be
181
// closed when the proxy config is unloaded, and instead the stream
182
// will remain open until the delay is complete. In other words,
modules/caddyhttp/reverseproxy/streaming.go
+17
-3
@@ -204,7 +204,12 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup,
204
defer deleteFrontConn()
205
defer deleteBackConn()
206
207
- spc := switchProtocolCopier{user: conn, backend: backConn, wg: wg}
207
+ spc := switchProtocolCopier{
208
+ user: conn,
209
+ backend: backConn,
210
+ wg: wg,
211
+ bufferSize: h.StreamBufferSize,
212
+ }
213
214
// setup the timeout if requested
215
var timeoutc <-chan time.Time
@@ -636,20 +641,29 @@ func (m *maxLatencyWriter) stop() {
641
type switchProtocolCopier struct {
642
user, backend io.ReadWriteCloser
643
wg *sync.WaitGroup
644
+ bufferSize int
645
}
646
647
func (c switchProtocolCopier) copyFromBackend(errc chan<- error) {
642
- _, err := io.Copy(c.user, c.backend)
648
+ _, err := io.CopyBuffer(c.user, c.backend, c.buffer())
649
errc <- err
650
c.wg.Done()
651
}
652
653
func (c switchProtocolCopier) copyToBackend(errc chan<- error) {
648
- _, err := io.Copy(c.backend, c.user)
654
+ _, err := io.CopyBuffer(c.backend, c.user, c.buffer())
655
errc <- err
656
c.wg.Done()
657
}
658
659
+func (c switchProtocolCopier) buffer() []byte {
660
+ size := c.bufferSize
661
+ if size <= 0 {
662
+ size = defaultBufferSize
663
+ }
664
+ return make([]byte, size)
665
+}
666
+
667
var streamingBufPool = sync.Pool{
668
New: func() any {
669
// The Pool's New function should generally only return pointer
modules/caddyhttp/reverseproxy/streaming_test.go
+46
@@ -2,8 +2,10 @@ package reverseproxy
2
3
import (
4
"bytes"
5
+ "io"
6
"net/http/httptest"
7
"strings"
8
+ "sync"
9
"testing"
10
11
"github.com/caddyserver/caddy/v2"
@@ -34,3 +36,47 @@ func TestHandlerCopyResponse(t *testing.T) {
36
}
37
}
38
}
39
+
40
+func TestSwitchProtocolCopierBufferSize(t *testing.T) {
41
+ var wg sync.WaitGroup
42
+ var errc = make(chan error, 1)
43
+ var dst bytes.Buffer
44
+
45
+ copier := switchProtocolCopier{
46
+ user: nopReadWriteCloser{Reader: strings.NewReader("hello")},
47
+ backend: nopReadWriteCloser{Writer: &dst},
48
+ wg: &wg,
49
+ bufferSize: 7,
50
+ }
51
+
52
+ buf := copier.buffer()
53
+ if got := len(buf); got != 7 {
54
+ t.Fatalf("buffer len = %d, want 7", got)
55
+ }
56
+
57
+ wg.Add(1)
58
+ go copier.copyToBackend(errc)
59
+ wg.Wait()
60
+
61
+ if err := <-errc; err != nil {
62
+ t.Fatalf("copyToBackend() error = %v", err)
63
+ }
64
+ if got := dst.String(); got != "hello" {
65
+ t.Fatalf("copied data = %q, want %q", got, "hello")
66
+ }
67
+}
68
+
69
+func TestSwitchProtocolCopierDefaultBufferSize(t *testing.T) {
70
+ copier := switchProtocolCopier{}
71
+ buf := copier.buffer()
72
+ if got := len(buf); got != defaultBufferSize {
73
+ t.Fatalf("buffer len = %d, want %d", got, defaultBufferSize)
74
+ }
75
+}
76
+
77
+type nopReadWriteCloser struct {
78
+ io.Reader
79
+ io.Writer
80
+}
81
+
82
+func (nopReadWriteCloser) Close() error { return nil }