Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 13, 00:30 UTC
caddyhttp: Make use of `http.ResponseController` (#5654)
* caddyhttp: Make use of http.ResponseController Also syncs the reverseproxy implementation with stdlib's which now uses ResponseController as well https://github.com/golang/go/commit/2449bbb5e614954ce9e99c8a481ea2ee73d72d61 * Enable full-duplex for HTTP/1.1 * Appease linter * Add warning for builds with Go 1.20, so it's less surprising to users * Improved godoc for EnableFullDuplex, copied text from stdlib * Only wrap in encode if not already wrapped
Francis Lavoie committed
Aug 2, 2023 at 16:03 UTC
cd486c25d168caf58f4b6fe5d3252df9432901ec
14 files changed
+166
-94
caddyconfig/httpcaddyfile/serveroptions.go
+8
@@ -41,6 +41,7 @@ type serverOptions struct {
41
IdleTimeout caddy.Duration
42
KeepAliveInterval caddy.Duration
43
MaxHeaderBytes int
44
+ EnableFullDuplex bool
45
Protocols []string
46
StrictSNIHost *bool
47
TrustedProxiesRaw json.RawMessage
@@ -157,6 +158,12 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
158
}
159
serverOpts.MaxHeaderBytes = int(size)
160
161
+ case "enable_full_duplex":
162
+ if d.NextArg() {
163
+ return nil, d.ArgErr()
164
+ }
165
+ serverOpts.EnableFullDuplex = true
166
+
167
case "log_credentials":
168
if d.NextArg() {
169
return nil, d.ArgErr()
@@ -327,6 +334,7 @@ func applyServerOptions(
334
server.IdleTimeout = opts.IdleTimeout
335
server.KeepAliveInterval = opts.KeepAliveInterval
336
server.MaxHeaderBytes = opts.MaxHeaderBytes
337
+ server.EnableFullDuplex = opts.EnableFullDuplex
338
server.Protocols = opts.Protocols
339
server.StrictSNIHost = opts.StrictSNIHost
340
server.TrustedProxiesRaw = opts.TrustedProxiesRaw
caddytest/integration/caddyfile_adapt/global_server_options_single.txt
+2
@@ -11,6 +11,7 @@
11
idle 30s
12
}
13
max_header_size 100MB
14
+ enable_full_duplex
15
log_credentials
16
protocols h1 h2 h2c h3
17
strict_sni_host
@@ -45,6 +46,7 @@ foo.com {
46
"write_timeout": 30000000000,
47
"idle_timeout": 30000000000,
48
"max_header_bytes": 100000000,
49
+ "enable_full_duplex": true,
50
"routes": [
51
{
52
"match": [
caddytest/integration/stream_test.go
+1
-3
@@ -176,9 +176,7 @@ func testH2ToH2CStreamServeH2C(t *testing.T) *http.Server {
176
177
w.Header().Set("Cache-Control", "no-store")
178
w.WriteHeader(200)
179
- if f, ok := w.(http.Flusher); ok {
180
- f.Flush()
181
- }
179
+ http.NewResponseController(w).Flush()
180
181
buf := make([]byte, 4*1024)
182
modules/caddyhttp/app.go
+8
@@ -20,7 +20,9 @@ import (
20
"fmt"
21
"net"
22
"net/http"
23
+ "runtime"
24
"strconv"
25
+ "strings"
26
"sync"
27
"time"
28
@@ -325,9 +327,15 @@ func (app *App) Provision(ctx caddy.Context) error {
327
328
// Validate ensures the app's configuration is valid.
329
func (app *App) Validate() error {
330
+ isGo120 := strings.Contains(runtime.Version(), "go1.20")
331
+
332
// each server must use distinct listener addresses
333
lnAddrs := make(map[string]string)
334
for srvName, srv := range app.Servers {
335
+ if isGo120 && srv.EnableFullDuplex {
336
+ app.logger.Warn("enable_full_duplex is not supported in Go 1.20, use a build made with Go 1.21 or later", zap.String("server", srvName))
337
+ }
338
+
339
for _, addr := range srv.Listen {
340
listenAddr, err := caddy.ParseNetworkAddress(addr)
341
if err != nil {
modules/caddyhttp/duplex_go120.go
new
+25
@@ -0,0 +1,25 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+//go:build !go1.21
16
+
17
+package caddyhttp
18
+
19
+import (
20
+ "net/http"
21
+)
22
+
23
+func enableFullDuplex(w http.ResponseWriter) {
24
+ // Do nothing, Go 1.20 and earlier do not support full duplex
25
+}
modules/caddyhttp/duplex_go121.go
new
+25
@@ -0,0 +1,25 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+//go:build go1.21
16
+
17
+package caddyhttp
18
+
19
+import (
20
+ "net/http"
21
+)
22
+
23
+func enableFullDuplex(w http.ResponseWriter) {
24
+ http.NewResponseController(w).EnableFullDuplex()
25
+}
modules/caddyhttp/encode/encode.go
+14
-13
@@ -167,10 +167,10 @@ func (enc *Encode) openResponseWriter(encodingName string, w http.ResponseWriter
167
// initResponseWriter initializes the responseWriter instance
168
// allocated in openResponseWriter, enabling mid-stack inlining.
169
func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, wrappedRW http.ResponseWriter) *responseWriter {
170
- if httpInterfaces, ok := wrappedRW.(caddyhttp.HTTPInterfaces); ok {
171
- rw.HTTPInterfaces = httpInterfaces
170
+ if rww, ok := wrappedRW.(*caddyhttp.ResponseWriterWrapper); ok {
171
+ rw.ResponseWriter = rww
172
} else {
173
- rw.HTTPInterfaces = &caddyhttp.ResponseWriterWrapper{ResponseWriter: wrappedRW}
173
+ rw.ResponseWriter = &caddyhttp.ResponseWriterWrapper{ResponseWriter: wrappedRW}
174
}
175
rw.encodingName = encodingName
176
rw.config = enc
@@ -182,7 +182,7 @@ func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, w
182
// using the encoding represented by encodingName and
183
// configured by config.
184
type responseWriter struct {
185
- caddyhttp.HTTPInterfaces
185
+ http.ResponseWriter
186
encodingName string
187
w Encoder
188
config *Encode
@@ -211,7 +211,8 @@ func (rw *responseWriter) Flush() {
211
// to rw.Write (see bug in #4314)
212
return
213
}
214
- rw.HTTPInterfaces.Flush()
214
+ //nolint:bodyclose
215
+ http.NewResponseController(rw).Flush()
216
}
217
218
// Hijack implements http.Hijacker. It will flush status code if set. We don't track actual hijacked
@@ -219,11 +220,12 @@ func (rw *responseWriter) Flush() {
220
func (rw *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
221
if !rw.wroteHeader {
222
if rw.statusCode != 0 {
222
- rw.HTTPInterfaces.WriteHeader(rw.statusCode)
223
+ rw.ResponseWriter.WriteHeader(rw.statusCode)
224
}
225
rw.wroteHeader = true
226
}
226
- return rw.HTTPInterfaces.Hijack()
227
+ //nolint:bodyclose
228
+ return http.NewResponseController(rw).Hijack()
229
}
230
231
// Write writes to the response. If the response qualifies,
@@ -260,7 +262,7 @@ func (rw *responseWriter) Write(p []byte) (int, error) {
262
// by the standard library
263
if !rw.wroteHeader {
264
if rw.statusCode != 0 {
263
- rw.HTTPInterfaces.WriteHeader(rw.statusCode)
265
+ rw.ResponseWriter.WriteHeader(rw.statusCode)
266
}
267
rw.wroteHeader = true
268
}
@@ -268,7 +270,7 @@ func (rw *responseWriter) Write(p []byte) (int, error) {
270
if rw.w != nil {
271
return rw.w.Write(p)
272
} else {
271
- return rw.HTTPInterfaces.Write(p)
273
+ return rw.ResponseWriter.Write(p)
274
}
275
}
276
@@ -284,7 +286,7 @@ func (rw *responseWriter) Close() error {
286
287
// issue #5059, don't write status code if not set explicitly.
288
if rw.statusCode != 0 {
287
- rw.HTTPInterfaces.WriteHeader(rw.statusCode)
289
+ rw.ResponseWriter.WriteHeader(rw.statusCode)
290
}
291
rw.wroteHeader = true
292
}
@@ -301,7 +303,7 @@ func (rw *responseWriter) Close() error {
303
304
// Unwrap returns the underlying ResponseWriter.
305
func (rw *responseWriter) Unwrap() http.ResponseWriter {
304
- return rw.HTTPInterfaces
306
+ return rw.ResponseWriter
307
}
308
309
// init should be called before we write a response, if rw.buf has contents.
@@ -310,7 +312,7 @@ func (rw *responseWriter) init() {
312
rw.config.Match(rw) {
313
314
rw.w = rw.config.writerPools[rw.encodingName].Get().(Encoder)
313
- rw.w.Reset(rw.HTTPInterfaces)
315
+ rw.w.Reset(rw.ResponseWriter)
316
rw.Header().Del("Content-Length") // https://github.com/golang/go/issues/14975
317
rw.Header().Set("Content-Encoding", rw.encodingName)
318
rw.Header().Add("Vary", "Accept-Encoding")
@@ -429,5 +431,4 @@ var (
431
_ caddy.Provisioner = (*Encode)(nil)
432
_ caddy.Validator = (*Encode)(nil)
433
_ caddyhttp.MiddlewareHandler = (*Encode)(nil)
432
- _ caddyhttp.HTTPInterfaces = (*responseWriter)(nil)
434
)
modules/caddyhttp/headers/headers.go
+1
-1
@@ -371,5 +371,5 @@ func (rww *responseWriterWrapper) Write(d []byte) (int, error) {
371
var (
372
_ caddy.Provisioner = (*Handler)(nil)
373
_ caddyhttp.MiddlewareHandler = (*Handler)(nil)
374
- _ caddyhttp.HTTPInterfaces = (*responseWriterWrapper)(nil)
374
+ _ http.ResponseWriter = (*responseWriterWrapper)(nil)
375
)
modules/caddyhttp/push/handler.go
+2
-1
@@ -251,5 +251,6 @@ const pushedLink = "http.handlers.push.pushed_link"
251
var (
252
_ caddy.Provisioner = (*Handler)(nil)
253
_ caddyhttp.MiddlewareHandler = (*Handler)(nil)
254
- _ caddyhttp.HTTPInterfaces = (*linkPusher)(nil)
254
+ _ http.ResponseWriter = (*linkPusher)(nil)
255
+ _ http.Pusher = (*linkPusher)(nil)
256
)
modules/caddyhttp/responsewriter.go
+14
-43
@@ -24,34 +24,14 @@ import (
24
)
25
26
// ResponseWriterWrapper wraps an underlying ResponseWriter and
27
-// promotes its Pusher/Flusher/Hijacker methods as well. To use
28
-// this type, embed a pointer to it within your own struct type
29
-// that implements the http.ResponseWriter interface, then call
30
-// methods on the embedded value. You can make sure your type
31
-// wraps correctly by asserting that it implements the
32
-// HTTPInterfaces interface.
27
+// promotes its Pusher method as well. To use this type, embed
28
+// a pointer to it within your own struct type that implements
29
+// the http.ResponseWriter interface, then call methods on the
30
+// embedded value.
31
type ResponseWriterWrapper struct {
32
http.ResponseWriter
33
}
34
37
-// Hijack implements http.Hijacker. It simply calls the underlying
38
-// ResponseWriter's Hijack method if there is one, or returns
39
-// ErrNotImplemented otherwise.
40
-func (rww *ResponseWriterWrapper) Hijack() (net.Conn, *bufio.ReadWriter, error) {
41
- if hj, ok := rww.ResponseWriter.(http.Hijacker); ok {
42
- return hj.Hijack()
43
- }
44
- return nil, nil, ErrNotImplemented
45
-}
46
-
47
-// Flush implements http.Flusher. It simply calls the underlying
48
-// ResponseWriter's Flush method if there is one.
49
-func (rww *ResponseWriterWrapper) Flush() {
50
- if f, ok := rww.ResponseWriter.(http.Flusher); ok {
51
- f.Flush()
52
- }
53
-}
54
-
35
// Push implements http.Pusher. It simply calls the underlying
36
// ResponseWriter's Push method if there is one, or returns
37
// ErrNotImplemented otherwise.
@@ -62,29 +42,18 @@ func (rww *ResponseWriterWrapper) Push(target string, opts *http.PushOptions) er
42
return ErrNotImplemented
43
}
44
65
-// ReadFrom implements io.ReaderFrom. It simply calls the underlying
66
-// ResponseWriter's ReadFrom method if there is one, otherwise it defaults
67
-// to io.Copy.
45
+// ReadFrom implements io.ReaderFrom. It simply calls io.Copy,
46
+// which uses io.ReaderFrom if available.
47
func (rww *ResponseWriterWrapper) ReadFrom(r io.Reader) (n int64, err error) {
69
- if rf, ok := rww.ResponseWriter.(io.ReaderFrom); ok {
70
- return rf.ReadFrom(r)
71
- }
48
return io.Copy(rww.ResponseWriter, r)
49
}
50
75
-// Unwrap returns the underlying ResponseWriter.
51
+// Unwrap returns the underlying ResponseWriter, necessary for
52
+// http.ResponseController to work correctly.
53
func (rww *ResponseWriterWrapper) Unwrap() http.ResponseWriter {
54
return rww.ResponseWriter
55
}
56
80
-// HTTPInterfaces mix all the interfaces that middleware ResponseWriters need to support.
81
-type HTTPInterfaces interface {
82
- http.ResponseWriter
83
- http.Pusher
84
- http.Flusher
85
- http.Hijacker
86
-}
87
-
57
// ErrNotImplemented is returned when an underlying
58
// ResponseWriter does not implement the required method.
59
var ErrNotImplemented = fmt.Errorf("method not implemented")
@@ -262,7 +231,8 @@ func (rr *responseRecorder) WriteResponse() error {
231
}
232
233
func (rr *responseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) {
265
- conn, brw, err := rr.ResponseWriterWrapper.Hijack()
234
+ //nolint:bodyclose
235
+ conn, brw, err := http.NewResponseController(rr.ResponseWriterWrapper).Hijack()
236
if err != nil {
237
return nil, nil, err
238
}
@@ -294,7 +264,7 @@ func (hc *hijackedConn) ReadFrom(r io.Reader) (int64, error) {
264
// responses instead of writing them to the client. See
265
// docs for NewResponseRecorder for proper usage.
266
type ResponseRecorder interface {
297
- HTTPInterfaces
267
+ http.ResponseWriter
268
Status() int
269
Buffer() *bytes.Buffer
270
Buffered() bool
@@ -309,12 +279,13 @@ type ShouldBufferFunc func(status int, header http.Header) bool
279
280
// Interface guards
281
var (
312
- _ HTTPInterfaces = (*ResponseWriterWrapper)(nil)
313
- _ ResponseRecorder = (*responseRecorder)(nil)
282
+ _ http.ResponseWriter = (*ResponseWriterWrapper)(nil)
283
+ _ ResponseRecorder = (*responseRecorder)(nil)
284
285
// Implementing ReaderFrom can be such a significant
286
// optimization that it should probably be required!
287
// see PR #5022 (25%-50% speedup)
288
_ io.ReaderFrom = (*ResponseWriterWrapper)(nil)
289
_ io.ReaderFrom = (*responseRecorder)(nil)
290
+ _ io.ReaderFrom = (*hijackedConn)(nil)
291
)
modules/caddyhttp/reverseproxy/reverseproxy.go
+2
-3
@@ -962,9 +962,8 @@ func (h *Handler) finalizeResponse(
962
// Force chunking if we saw a response trailer.
963
// This prevents net/http from calculating the length for short
964
// bodies and adding a Content-Length.
965
- if fl, ok := rw.(http.Flusher); ok {
966
- fl.Flush()
967
- }
965
+ //nolint:bodyclose
966
+ http.NewResponseController(rw).Flush()
967
}
968
969
// total duration spent proxying, including writing response body
modules/caddyhttp/reverseproxy/streaming.go
+31
-29
@@ -20,6 +20,7 @@ package reverseproxy
20
21
import (
22
"context"
23
+ "errors"
24
"fmt"
25
"io"
26
weakrand "math/rand"
@@ -51,17 +52,19 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, rw http.ResponseWrit
52
return
53
}
54
54
- hj, ok := rw.(http.Hijacker)
55
- if !ok {
56
- logger.Error("can't switch protocols using non-Hijacker ResponseWriter", zap.String("type", fmt.Sprintf("%T", rw)))
57
- return
58
- }
55
backConn, ok := res.Body.(io.ReadWriteCloser)
56
if !ok {
57
logger.Error("internal error: 101 switching protocols response with non-writable body")
58
return
59
}
60
61
+ //nolint:bodyclose
62
+ conn, brw, hijackErr := http.NewResponseController(rw).Hijack()
63
+ if errors.Is(hijackErr, http.ErrNotSupported) {
64
+ h.logger.Sugar().Errorf("can't switch protocols using non-Hijacker ResponseWriter type %T", rw)
65
+ return
66
+ }
67
+
68
// adopted from https://github.com/golang/go/commit/8bcf2834afdf6a1f7937390903a41518715ef6f5
69
backConnCloseCh := make(chan struct{})
70
go func() {
@@ -81,9 +84,8 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, rw http.ResponseWrit
84
rw.WriteHeader(res.StatusCode)
85
86
logger.Debug("upgrading connection")
84
- conn, brw, err := hj.Hijack()
85
- if err != nil {
86
- logger.Error("hijack failed on protocol switch", zap.Error(err))
87
+ if hijackErr != nil {
88
+ h.logger.Error("hijack failed on protocol switch", zap.Error(hijackErr))
89
return
90
}
91
@@ -181,26 +183,28 @@ func (h Handler) isBidirectionalStream(req *http.Request, res *http.Response) bo
183
(ae == "identity" || ae == "")
184
}
185
184
-func (h Handler) copyResponse(dst io.Writer, src io.Reader, flushInterval time.Duration) error {
186
+func (h Handler) copyResponse(dst http.ResponseWriter, src io.Reader, flushInterval time.Duration) error {
187
+ var w io.Writer = dst
188
+
189
if flushInterval != 0 {
186
- if wf, ok := dst.(writeFlusher); ok {
187
- mlw := &maxLatencyWriter{
188
- dst: wf,
189
- latency: flushInterval,
190
- }
191
- defer mlw.stop()
190
+ mlw := &maxLatencyWriter{
191
+ dst: dst,
192
+ //nolint:bodyclose
193
+ flush: http.NewResponseController(dst).Flush,
194
+ latency: flushInterval,
195
+ }
196
+ defer mlw.stop()
197
193
- // set up initial timer so headers get flushed even if body writes are delayed
194
- mlw.flushPending = true
195
- mlw.t = time.AfterFunc(flushInterval, mlw.delayedFlush)
198
+ // set up initial timer so headers get flushed even if body writes are delayed
199
+ mlw.flushPending = true
200
+ mlw.t = time.AfterFunc(flushInterval, mlw.delayedFlush)
201
197
- dst = mlw
198
- }
202
+ w = mlw
203
}
204
205
buf := streamingBufPool.Get().(*[]byte)
206
defer streamingBufPool.Put(buf)
203
- _, err := h.copyBuffer(dst, src, *buf)
207
+ _, err := h.copyBuffer(w, src, *buf)
208
return err
209
}
210
@@ -439,13 +443,9 @@ type openConnection struct {
443
gracefulClose func() error
444
}
445
442
-type writeFlusher interface {
443
- io.Writer
444
- http.Flusher
445
-}
446
-
446
type maxLatencyWriter struct {
448
- dst writeFlusher
447
+ dst io.Writer
448
+ flush func() error
449
latency time.Duration // non-zero; negative means to flush immediately
450
451
mu sync.Mutex // protects t, flushPending, and dst.Flush
@@ -458,7 +458,8 @@ func (m *maxLatencyWriter) Write(p []byte) (n int, err error) {
458
defer m.mu.Unlock()
459
n, err = m.dst.Write(p)
460
if m.latency < 0 {
461
- m.dst.Flush()
461
+ //nolint:errcheck
462
+ m.flush()
463
return
464
}
465
if m.flushPending {
@@ -479,7 +480,8 @@ func (m *maxLatencyWriter) delayedFlush() {
480
if !m.flushPending { // if stop was called but AfterFunc already started this goroutine
481
return
482
}
482
- m.dst.Flush()
483
+ //nolint:errcheck
484
+ m.flush()
485
m.flushPending = false
486
}
487
modules/caddyhttp/reverseproxy/streaming_test.go
+5
-1
@@ -2,6 +2,7 @@ package reverseproxy
2
3
import (
4
"bytes"
5
+ "net/http/httptest"
6
"strings"
7
"testing"
8
)
@@ -13,12 +14,15 @@ func TestHandlerCopyResponse(t *testing.T) {
14
strings.Repeat("a", defaultBufferSize),
15
strings.Repeat("123456789 123456789 123456789 12", 3000),
16
}
17
+
18
dst := bytes.NewBuffer(nil)
19
+ recorder := httptest.NewRecorder()
20
+ recorder.Body = dst
21
22
for _, d := range testdata {
23
src := bytes.NewBuffer([]byte(d))
24
dst.Reset()
21
- err := h.copyResponse(dst, src, 0)
25
+ err := h.copyResponse(recorder, src, 0)
26
if err != nil {
27
t.Errorf("failed with error: %v", err)
28
}
modules/caddyhttp/server.go
+28
@@ -82,6 +82,26 @@ type Server struct {
82
// HTTP request headers.
83
MaxHeaderBytes int `json:"max_header_bytes,omitempty"`
84
85
+ // Enable full-duplex communication for HTTP/1 requests.
86
+ // Only has an effect if Caddy was built with Go 1.21 or later.
87
+ //
88
+ // For HTTP/1 requests, the Go HTTP server by default consumes any
89
+ // unread portion of the request body before beginning to write the
90
+ // response, preventing handlers from concurrently reading from the
91
+ // request and writing the response. Enabling this option disables
92
+ // this behavior and permits handlers to continue to read from the
93
+ // request while concurrently writing the response.
94
+ //
95
+ // For HTTP/2 requests, the Go HTTP server always permits concurrent
96
+ // reads and responses, so this option has no effect.
97
+ //
98
+ // Test thoroughly with your HTTP clients, as some older clients may
99
+ // not support full-duplex HTTP/1 which can cause them to deadlock.
100
+ // See https://github.com/golang/go/issues/57786 for more info.
101
+ //
102
+ // TODO: This is an EXPERIMENTAL feature. Subject to change or removal.
103
+ EnableFullDuplex bool `json:"enable_full_duplex,omitempty"`
104
+
105
// Routes describes how this server will handle requests.
106
// Routes are executed sequentially. First a route's matchers
107
// are evaluated, then its grouping. If it matches and has
@@ -264,6 +284,14 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
284
repl := caddy.NewReplacer()
285
r = PrepareRequest(r, repl, w, s)
286
287
+ // enable full-duplex for HTTP/1, ensuring the entire
288
+ // request body gets consumed before writing the response
289
+ if s.EnableFullDuplex {
290
+ // TODO: Remove duplex_go12*.go abstraction once our
291
+ // minimum Go version is 1.21 or later
292
+ enableFullDuplex(w)
293
+ }
294
+
295
// encode the request for logging purposes before
296
// it enters any handler chain; this is necessary
297
// to capture the original request in case it gets