@marcel91200 / caddy Mirror · read-only / commits / cd486c25
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 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