@marcel91200 / caddy Mirror · read-only / commits / ca0ca67f
Fork 0

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 }