Skip to content

Commit cd486c2

Browse files
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 golang/go@2449bbb * 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
1 parent e198c60 commit cd486c2

File tree

14 files changed

+166
-94
lines changed

14 files changed

+166
-94
lines changed

caddyconfig/httpcaddyfile/serveroptions.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ type serverOptions struct {
4141
IdleTimeout caddy.Duration
4242
KeepAliveInterval caddy.Duration
4343
MaxHeaderBytes int
44+
EnableFullDuplex bool
4445
Protocols []string
4546
StrictSNIHost *bool
4647
TrustedProxiesRaw json.RawMessage
@@ -157,6 +158,12 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
157158
}
158159
serverOpts.MaxHeaderBytes = int(size)
159160

161+
case "enable_full_duplex":
162+
if d.NextArg() {
163+
return nil, d.ArgErr()
164+
}
165+
serverOpts.EnableFullDuplex = true
166+
160167
case "log_credentials":
161168
if d.NextArg() {
162169
return nil, d.ArgErr()
@@ -327,6 +334,7 @@ func applyServerOptions(
327334
server.IdleTimeout = opts.IdleTimeout
328335
server.KeepAliveInterval = opts.KeepAliveInterval
329336
server.MaxHeaderBytes = opts.MaxHeaderBytes
337+
server.EnableFullDuplex = opts.EnableFullDuplex
330338
server.Protocols = opts.Protocols
331339
server.StrictSNIHost = opts.StrictSNIHost
332340
server.TrustedProxiesRaw = opts.TrustedProxiesRaw

caddytest/integration/caddyfile_adapt/global_server_options_single.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
idle 30s
1212
}
1313
max_header_size 100MB
14+
enable_full_duplex
1415
log_credentials
1516
protocols h1 h2 h2c h3
1617
strict_sni_host
@@ -45,6 +46,7 @@ foo.com {
4546
"write_timeout": 30000000000,
4647
"idle_timeout": 30000000000,
4748
"max_header_bytes": 100000000,
49+
"enable_full_duplex": true,
4850
"routes": [
4951
{
5052
"match": [

caddytest/integration/stream_test.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,9 +176,7 @@ func testH2ToH2CStreamServeH2C(t *testing.T) *http.Server {
176176

177177
w.Header().Set("Cache-Control", "no-store")
178178
w.WriteHeader(200)
179-
if f, ok := w.(http.Flusher); ok {
180-
f.Flush()
181-
}
179+
http.NewResponseController(w).Flush()
182180

183181
buf := make([]byte, 4*1024)
184182

modules/caddyhttp/app.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ import (
2020
"fmt"
2121
"net"
2222
"net/http"
23+
"runtime"
2324
"strconv"
25+
"strings"
2426
"sync"
2527
"time"
2628

@@ -325,9 +327,15 @@ func (app *App) Provision(ctx caddy.Context) error {
325327

326328
// Validate ensures the app's configuration is valid.
327329
func (app *App) Validate() error {
330+
isGo120 := strings.Contains(runtime.Version(), "go1.20")
331+
328332
// each server must use distinct listener addresses
329333
lnAddrs := make(map[string]string)
330334
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+
331339
for _, addr := range srv.Listen {
332340
listenAddr, err := caddy.ParseNetworkAddress(addr)
333341
if err != nil {

modules/caddyhttp/duplex_go120.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,10 @@ func (enc *Encode) openResponseWriter(encodingName string, w http.ResponseWriter
167167
// initResponseWriter initializes the responseWriter instance
168168
// allocated in openResponseWriter, enabling mid-stack inlining.
169169
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
172172
} else {
173-
rw.HTTPInterfaces = &caddyhttp.ResponseWriterWrapper{ResponseWriter: wrappedRW}
173+
rw.ResponseWriter = &caddyhttp.ResponseWriterWrapper{ResponseWriter: wrappedRW}
174174
}
175175
rw.encodingName = encodingName
176176
rw.config = enc
@@ -182,7 +182,7 @@ func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, w
182182
// using the encoding represented by encodingName and
183183
// configured by config.
184184
type responseWriter struct {
185-
caddyhttp.HTTPInterfaces
185+
http.ResponseWriter
186186
encodingName string
187187
w Encoder
188188
config *Encode
@@ -211,19 +211,21 @@ func (rw *responseWriter) Flush() {
211211
// to rw.Write (see bug in #4314)
212212
return
213213
}
214-
rw.HTTPInterfaces.Flush()
214+
//nolint:bodyclose
215+
http.NewResponseController(rw).Flush()
215216
}
216217

217218
// Hijack implements http.Hijacker. It will flush status code if set. We don't track actual hijacked
218219
// status assuming http middlewares will track its status.
219220
func (rw *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
220221
if !rw.wroteHeader {
221222
if rw.statusCode != 0 {
222-
rw.HTTPInterfaces.WriteHeader(rw.statusCode)
223+
rw.ResponseWriter.WriteHeader(rw.statusCode)
223224
}
224225
rw.wroteHeader = true
225226
}
226-
return rw.HTTPInterfaces.Hijack()
227+
//nolint:bodyclose
228+
return http.NewResponseController(rw).Hijack()
227229
}
228230

229231
// Write writes to the response. If the response qualifies,
@@ -260,15 +262,15 @@ func (rw *responseWriter) Write(p []byte) (int, error) {
260262
// by the standard library
261263
if !rw.wroteHeader {
262264
if rw.statusCode != 0 {
263-
rw.HTTPInterfaces.WriteHeader(rw.statusCode)
265+
rw.ResponseWriter.WriteHeader(rw.statusCode)
264266
}
265267
rw.wroteHeader = true
266268
}
267269

268270
if rw.w != nil {
269271
return rw.w.Write(p)
270272
} else {
271-
return rw.HTTPInterfaces.Write(p)
273+
return rw.ResponseWriter.Write(p)
272274
}
273275
}
274276

@@ -284,7 +286,7 @@ func (rw *responseWriter) Close() error {
284286

285287
// issue #5059, don't write status code if not set explicitly.
286288
if rw.statusCode != 0 {
287-
rw.HTTPInterfaces.WriteHeader(rw.statusCode)
289+
rw.ResponseWriter.WriteHeader(rw.statusCode)
288290
}
289291
rw.wroteHeader = true
290292
}
@@ -301,7 +303,7 @@ func (rw *responseWriter) Close() error {
301303

302304
// Unwrap returns the underlying ResponseWriter.
303305
func (rw *responseWriter) Unwrap() http.ResponseWriter {
304-
return rw.HTTPInterfaces
306+
return rw.ResponseWriter
305307
}
306308

307309
// init should be called before we write a response, if rw.buf has contents.
@@ -310,7 +312,7 @@ func (rw *responseWriter) init() {
310312
rw.config.Match(rw) {
311313

312314
rw.w = rw.config.writerPools[rw.encodingName].Get().(Encoder)
313-
rw.w.Reset(rw.HTTPInterfaces)
315+
rw.w.Reset(rw.ResponseWriter)
314316
rw.Header().Del("Content-Length") // https://github.com/golang/go/issues/14975
315317
rw.Header().Set("Content-Encoding", rw.encodingName)
316318
rw.Header().Add("Vary", "Accept-Encoding")
@@ -429,5 +431,4 @@ var (
429431
_ caddy.Provisioner = (*Encode)(nil)
430432
_ caddy.Validator = (*Encode)(nil)
431433
_ caddyhttp.MiddlewareHandler = (*Encode)(nil)
432-
_ caddyhttp.HTTPInterfaces = (*responseWriter)(nil)
433434
)

modules/caddyhttp/headers/headers.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,5 +371,5 @@ func (rww *responseWriterWrapper) Write(d []byte) (int, error) {
371371
var (
372372
_ caddy.Provisioner = (*Handler)(nil)
373373
_ caddyhttp.MiddlewareHandler = (*Handler)(nil)
374-
_ caddyhttp.HTTPInterfaces = (*responseWriterWrapper)(nil)
374+
_ http.ResponseWriter = (*responseWriterWrapper)(nil)
375375
)

modules/caddyhttp/push/handler.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,5 +251,6 @@ const pushedLink = "http.handlers.push.pushed_link"
251251
var (
252252
_ caddy.Provisioner = (*Handler)(nil)
253253
_ caddyhttp.MiddlewareHandler = (*Handler)(nil)
254-
_ caddyhttp.HTTPInterfaces = (*linkPusher)(nil)
254+
_ http.ResponseWriter = (*linkPusher)(nil)
255+
_ http.Pusher = (*linkPusher)(nil)
255256
)

modules/caddyhttp/responsewriter.go

Lines changed: 14 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -24,34 +24,14 @@ import (
2424
)
2525

2626
// 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.
3331
type ResponseWriterWrapper struct {
3432
http.ResponseWriter
3533
}
3634

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-
5535
// Push implements http.Pusher. It simply calls the underlying
5636
// ResponseWriter's Push method if there is one, or returns
5737
// ErrNotImplemented otherwise.
@@ -62,29 +42,18 @@ func (rww *ResponseWriterWrapper) Push(target string, opts *http.PushOptions) er
6242
return ErrNotImplemented
6343
}
6444

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.
6847
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-
}
7248
return io.Copy(rww.ResponseWriter, r)
7349
}
7450

75-
// Unwrap returns the underlying ResponseWriter.
51+
// Unwrap returns the underlying ResponseWriter, necessary for
52+
// http.ResponseController to work correctly.
7653
func (rww *ResponseWriterWrapper) Unwrap() http.ResponseWriter {
7754
return rww.ResponseWriter
7855
}
7956

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-
8857
// ErrNotImplemented is returned when an underlying
8958
// ResponseWriter does not implement the required method.
9059
var ErrNotImplemented = fmt.Errorf("method not implemented")
@@ -262,7 +231,8 @@ func (rr *responseRecorder) WriteResponse() error {
262231
}
263232

264233
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()
266236
if err != nil {
267237
return nil, nil, err
268238
}
@@ -294,7 +264,7 @@ func (hc *hijackedConn) ReadFrom(r io.Reader) (int64, error) {
294264
// responses instead of writing them to the client. See
295265
// docs for NewResponseRecorder for proper usage.
296266
type ResponseRecorder interface {
297-
HTTPInterfaces
267+
http.ResponseWriter
298268
Status() int
299269
Buffer() *bytes.Buffer
300270
Buffered() bool
@@ -309,12 +279,13 @@ type ShouldBufferFunc func(status int, header http.Header) bool
309279

310280
// Interface guards
311281
var (
312-
_ HTTPInterfaces = (*ResponseWriterWrapper)(nil)
313-
_ ResponseRecorder = (*responseRecorder)(nil)
282+
_ http.ResponseWriter = (*ResponseWriterWrapper)(nil)
283+
_ ResponseRecorder = (*responseRecorder)(nil)
314284

315285
// Implementing ReaderFrom can be such a significant
316286
// optimization that it should probably be required!
317287
// see PR #5022 (25%-50% speedup)
318288
_ io.ReaderFrom = (*ResponseWriterWrapper)(nil)
319289
_ io.ReaderFrom = (*responseRecorder)(nil)
290+
_ io.ReaderFrom = (*hijackedConn)(nil)
320291
)

0 commit comments

Comments
 (0)