mirror of
https://github.com/caddyserver/caddy.git
synced 2025-05-31 12:15:56 -04:00
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 2449bbb5e6
* 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
This commit is contained in:
parent
e198c605bd
commit
cd486c25d1
@ -41,6 +41,7 @@ type serverOptions struct {
|
|||||||
IdleTimeout caddy.Duration
|
IdleTimeout caddy.Duration
|
||||||
KeepAliveInterval caddy.Duration
|
KeepAliveInterval caddy.Duration
|
||||||
MaxHeaderBytes int
|
MaxHeaderBytes int
|
||||||
|
EnableFullDuplex bool
|
||||||
Protocols []string
|
Protocols []string
|
||||||
StrictSNIHost *bool
|
StrictSNIHost *bool
|
||||||
TrustedProxiesRaw json.RawMessage
|
TrustedProxiesRaw json.RawMessage
|
||||||
@ -157,6 +158,12 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
|
|||||||
}
|
}
|
||||||
serverOpts.MaxHeaderBytes = int(size)
|
serverOpts.MaxHeaderBytes = int(size)
|
||||||
|
|
||||||
|
case "enable_full_duplex":
|
||||||
|
if d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
serverOpts.EnableFullDuplex = true
|
||||||
|
|
||||||
case "log_credentials":
|
case "log_credentials":
|
||||||
if d.NextArg() {
|
if d.NextArg() {
|
||||||
return nil, d.ArgErr()
|
return nil, d.ArgErr()
|
||||||
@ -327,6 +334,7 @@ func applyServerOptions(
|
|||||||
server.IdleTimeout = opts.IdleTimeout
|
server.IdleTimeout = opts.IdleTimeout
|
||||||
server.KeepAliveInterval = opts.KeepAliveInterval
|
server.KeepAliveInterval = opts.KeepAliveInterval
|
||||||
server.MaxHeaderBytes = opts.MaxHeaderBytes
|
server.MaxHeaderBytes = opts.MaxHeaderBytes
|
||||||
|
server.EnableFullDuplex = opts.EnableFullDuplex
|
||||||
server.Protocols = opts.Protocols
|
server.Protocols = opts.Protocols
|
||||||
server.StrictSNIHost = opts.StrictSNIHost
|
server.StrictSNIHost = opts.StrictSNIHost
|
||||||
server.TrustedProxiesRaw = opts.TrustedProxiesRaw
|
server.TrustedProxiesRaw = opts.TrustedProxiesRaw
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
idle 30s
|
idle 30s
|
||||||
}
|
}
|
||||||
max_header_size 100MB
|
max_header_size 100MB
|
||||||
|
enable_full_duplex
|
||||||
log_credentials
|
log_credentials
|
||||||
protocols h1 h2 h2c h3
|
protocols h1 h2 h2c h3
|
||||||
strict_sni_host
|
strict_sni_host
|
||||||
@ -45,6 +46,7 @@ foo.com {
|
|||||||
"write_timeout": 30000000000,
|
"write_timeout": 30000000000,
|
||||||
"idle_timeout": 30000000000,
|
"idle_timeout": 30000000000,
|
||||||
"max_header_bytes": 100000000,
|
"max_header_bytes": 100000000,
|
||||||
|
"enable_full_duplex": true,
|
||||||
"routes": [
|
"routes": [
|
||||||
{
|
{
|
||||||
"match": [
|
"match": [
|
||||||
|
@ -176,9 +176,7 @@ func testH2ToH2CStreamServeH2C(t *testing.T) *http.Server {
|
|||||||
|
|
||||||
w.Header().Set("Cache-Control", "no-store")
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
w.WriteHeader(200)
|
w.WriteHeader(200)
|
||||||
if f, ok := w.(http.Flusher); ok {
|
http.NewResponseController(w).Flush()
|
||||||
f.Flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := make([]byte, 4*1024)
|
buf := make([]byte, 4*1024)
|
||||||
|
|
||||||
|
@ -20,7 +20,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -325,9 +327,15 @@ func (app *App) Provision(ctx caddy.Context) error {
|
|||||||
|
|
||||||
// Validate ensures the app's configuration is valid.
|
// Validate ensures the app's configuration is valid.
|
||||||
func (app *App) Validate() error {
|
func (app *App) Validate() error {
|
||||||
|
isGo120 := strings.Contains(runtime.Version(), "go1.20")
|
||||||
|
|
||||||
// each server must use distinct listener addresses
|
// each server must use distinct listener addresses
|
||||||
lnAddrs := make(map[string]string)
|
lnAddrs := make(map[string]string)
|
||||||
for srvName, srv := range app.Servers {
|
for srvName, srv := range app.Servers {
|
||||||
|
if isGo120 && srv.EnableFullDuplex {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
for _, addr := range srv.Listen {
|
for _, addr := range srv.Listen {
|
||||||
listenAddr, err := caddy.ParseNetworkAddress(addr)
|
listenAddr, err := caddy.ParseNetworkAddress(addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
25
modules/caddyhttp/duplex_go120.go
Normal file
25
modules/caddyhttp/duplex_go120.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//go:build !go1.21
|
||||||
|
|
||||||
|
package caddyhttp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func enableFullDuplex(w http.ResponseWriter) {
|
||||||
|
// Do nothing, Go 1.20 and earlier do not support full duplex
|
||||||
|
}
|
25
modules/caddyhttp/duplex_go121.go
Normal file
25
modules/caddyhttp/duplex_go121.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//go:build go1.21
|
||||||
|
|
||||||
|
package caddyhttp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func enableFullDuplex(w http.ResponseWriter) {
|
||||||
|
http.NewResponseController(w).EnableFullDuplex()
|
||||||
|
}
|
@ -167,10 +167,10 @@ func (enc *Encode) openResponseWriter(encodingName string, w http.ResponseWriter
|
|||||||
// initResponseWriter initializes the responseWriter instance
|
// initResponseWriter initializes the responseWriter instance
|
||||||
// allocated in openResponseWriter, enabling mid-stack inlining.
|
// allocated in openResponseWriter, enabling mid-stack inlining.
|
||||||
func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, wrappedRW http.ResponseWriter) *responseWriter {
|
func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, wrappedRW http.ResponseWriter) *responseWriter {
|
||||||
if httpInterfaces, ok := wrappedRW.(caddyhttp.HTTPInterfaces); ok {
|
if rww, ok := wrappedRW.(*caddyhttp.ResponseWriterWrapper); ok {
|
||||||
rw.HTTPInterfaces = httpInterfaces
|
rw.ResponseWriter = rww
|
||||||
} else {
|
} else {
|
||||||
rw.HTTPInterfaces = &caddyhttp.ResponseWriterWrapper{ResponseWriter: wrappedRW}
|
rw.ResponseWriter = &caddyhttp.ResponseWriterWrapper{ResponseWriter: wrappedRW}
|
||||||
}
|
}
|
||||||
rw.encodingName = encodingName
|
rw.encodingName = encodingName
|
||||||
rw.config = enc
|
rw.config = enc
|
||||||
@ -182,7 +182,7 @@ func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, w
|
|||||||
// using the encoding represented by encodingName and
|
// using the encoding represented by encodingName and
|
||||||
// configured by config.
|
// configured by config.
|
||||||
type responseWriter struct {
|
type responseWriter struct {
|
||||||
caddyhttp.HTTPInterfaces
|
http.ResponseWriter
|
||||||
encodingName string
|
encodingName string
|
||||||
w Encoder
|
w Encoder
|
||||||
config *Encode
|
config *Encode
|
||||||
@ -211,7 +211,8 @@ func (rw *responseWriter) Flush() {
|
|||||||
// to rw.Write (see bug in #4314)
|
// to rw.Write (see bug in #4314)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rw.HTTPInterfaces.Flush()
|
//nolint:bodyclose
|
||||||
|
http.NewResponseController(rw).Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hijack implements http.Hijacker. It will flush status code if set. We don't track actual hijacked
|
// 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() {
|
|||||||
func (rw *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
func (rw *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||||
if !rw.wroteHeader {
|
if !rw.wroteHeader {
|
||||||
if rw.statusCode != 0 {
|
if rw.statusCode != 0 {
|
||||||
rw.HTTPInterfaces.WriteHeader(rw.statusCode)
|
rw.ResponseWriter.WriteHeader(rw.statusCode)
|
||||||
}
|
}
|
||||||
rw.wroteHeader = true
|
rw.wroteHeader = true
|
||||||
}
|
}
|
||||||
return rw.HTTPInterfaces.Hijack()
|
//nolint:bodyclose
|
||||||
|
return http.NewResponseController(rw).Hijack()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write writes to the response. If the response qualifies,
|
// Write writes to the response. If the response qualifies,
|
||||||
@ -260,7 +262,7 @@ func (rw *responseWriter) Write(p []byte) (int, error) {
|
|||||||
// by the standard library
|
// by the standard library
|
||||||
if !rw.wroteHeader {
|
if !rw.wroteHeader {
|
||||||
if rw.statusCode != 0 {
|
if rw.statusCode != 0 {
|
||||||
rw.HTTPInterfaces.WriteHeader(rw.statusCode)
|
rw.ResponseWriter.WriteHeader(rw.statusCode)
|
||||||
}
|
}
|
||||||
rw.wroteHeader = true
|
rw.wroteHeader = true
|
||||||
}
|
}
|
||||||
@ -268,7 +270,7 @@ func (rw *responseWriter) Write(p []byte) (int, error) {
|
|||||||
if rw.w != nil {
|
if rw.w != nil {
|
||||||
return rw.w.Write(p)
|
return rw.w.Write(p)
|
||||||
} else {
|
} else {
|
||||||
return rw.HTTPInterfaces.Write(p)
|
return rw.ResponseWriter.Write(p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -284,7 +286,7 @@ func (rw *responseWriter) Close() error {
|
|||||||
|
|
||||||
// issue #5059, don't write status code if not set explicitly.
|
// issue #5059, don't write status code if not set explicitly.
|
||||||
if rw.statusCode != 0 {
|
if rw.statusCode != 0 {
|
||||||
rw.HTTPInterfaces.WriteHeader(rw.statusCode)
|
rw.ResponseWriter.WriteHeader(rw.statusCode)
|
||||||
}
|
}
|
||||||
rw.wroteHeader = true
|
rw.wroteHeader = true
|
||||||
}
|
}
|
||||||
@ -301,7 +303,7 @@ func (rw *responseWriter) Close() error {
|
|||||||
|
|
||||||
// Unwrap returns the underlying ResponseWriter.
|
// Unwrap returns the underlying ResponseWriter.
|
||||||
func (rw *responseWriter) Unwrap() http.ResponseWriter {
|
func (rw *responseWriter) Unwrap() http.ResponseWriter {
|
||||||
return rw.HTTPInterfaces
|
return rw.ResponseWriter
|
||||||
}
|
}
|
||||||
|
|
||||||
// init should be called before we write a response, if rw.buf has contents.
|
// init should be called before we write a response, if rw.buf has contents.
|
||||||
@ -310,7 +312,7 @@ func (rw *responseWriter) init() {
|
|||||||
rw.config.Match(rw) {
|
rw.config.Match(rw) {
|
||||||
|
|
||||||
rw.w = rw.config.writerPools[rw.encodingName].Get().(Encoder)
|
rw.w = rw.config.writerPools[rw.encodingName].Get().(Encoder)
|
||||||
rw.w.Reset(rw.HTTPInterfaces)
|
rw.w.Reset(rw.ResponseWriter)
|
||||||
rw.Header().Del("Content-Length") // https://github.com/golang/go/issues/14975
|
rw.Header().Del("Content-Length") // https://github.com/golang/go/issues/14975
|
||||||
rw.Header().Set("Content-Encoding", rw.encodingName)
|
rw.Header().Set("Content-Encoding", rw.encodingName)
|
||||||
rw.Header().Add("Vary", "Accept-Encoding")
|
rw.Header().Add("Vary", "Accept-Encoding")
|
||||||
@ -429,5 +431,4 @@ var (
|
|||||||
_ caddy.Provisioner = (*Encode)(nil)
|
_ caddy.Provisioner = (*Encode)(nil)
|
||||||
_ caddy.Validator = (*Encode)(nil)
|
_ caddy.Validator = (*Encode)(nil)
|
||||||
_ caddyhttp.MiddlewareHandler = (*Encode)(nil)
|
_ caddyhttp.MiddlewareHandler = (*Encode)(nil)
|
||||||
_ caddyhttp.HTTPInterfaces = (*responseWriter)(nil)
|
|
||||||
)
|
)
|
||||||
|
@ -371,5 +371,5 @@ func (rww *responseWriterWrapper) Write(d []byte) (int, error) {
|
|||||||
var (
|
var (
|
||||||
_ caddy.Provisioner = (*Handler)(nil)
|
_ caddy.Provisioner = (*Handler)(nil)
|
||||||
_ caddyhttp.MiddlewareHandler = (*Handler)(nil)
|
_ caddyhttp.MiddlewareHandler = (*Handler)(nil)
|
||||||
_ caddyhttp.HTTPInterfaces = (*responseWriterWrapper)(nil)
|
_ http.ResponseWriter = (*responseWriterWrapper)(nil)
|
||||||
)
|
)
|
||||||
|
@ -251,5 +251,6 @@ const pushedLink = "http.handlers.push.pushed_link"
|
|||||||
var (
|
var (
|
||||||
_ caddy.Provisioner = (*Handler)(nil)
|
_ caddy.Provisioner = (*Handler)(nil)
|
||||||
_ caddyhttp.MiddlewareHandler = (*Handler)(nil)
|
_ caddyhttp.MiddlewareHandler = (*Handler)(nil)
|
||||||
_ caddyhttp.HTTPInterfaces = (*linkPusher)(nil)
|
_ http.ResponseWriter = (*linkPusher)(nil)
|
||||||
|
_ http.Pusher = (*linkPusher)(nil)
|
||||||
)
|
)
|
||||||
|
@ -24,34 +24,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ResponseWriterWrapper wraps an underlying ResponseWriter and
|
// ResponseWriterWrapper wraps an underlying ResponseWriter and
|
||||||
// promotes its Pusher/Flusher/Hijacker methods as well. To use
|
// promotes its Pusher method as well. To use this type, embed
|
||||||
// this type, embed a pointer to it within your own struct type
|
// a pointer to it within your own struct type that implements
|
||||||
// that implements the http.ResponseWriter interface, then call
|
// the http.ResponseWriter interface, then call methods on the
|
||||||
// methods on the embedded value. You can make sure your type
|
// embedded value.
|
||||||
// wraps correctly by asserting that it implements the
|
|
||||||
// HTTPInterfaces interface.
|
|
||||||
type ResponseWriterWrapper struct {
|
type ResponseWriterWrapper struct {
|
||||||
http.ResponseWriter
|
http.ResponseWriter
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hijack implements http.Hijacker. It simply calls the underlying
|
|
||||||
// ResponseWriter's Hijack method if there is one, or returns
|
|
||||||
// ErrNotImplemented otherwise.
|
|
||||||
func (rww *ResponseWriterWrapper) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
|
||||||
if hj, ok := rww.ResponseWriter.(http.Hijacker); ok {
|
|
||||||
return hj.Hijack()
|
|
||||||
}
|
|
||||||
return nil, nil, ErrNotImplemented
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flush implements http.Flusher. It simply calls the underlying
|
|
||||||
// ResponseWriter's Flush method if there is one.
|
|
||||||
func (rww *ResponseWriterWrapper) Flush() {
|
|
||||||
if f, ok := rww.ResponseWriter.(http.Flusher); ok {
|
|
||||||
f.Flush()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push implements http.Pusher. It simply calls the underlying
|
// Push implements http.Pusher. It simply calls the underlying
|
||||||
// ResponseWriter's Push method if there is one, or returns
|
// ResponseWriter's Push method if there is one, or returns
|
||||||
// ErrNotImplemented otherwise.
|
// ErrNotImplemented otherwise.
|
||||||
@ -62,29 +42,18 @@ func (rww *ResponseWriterWrapper) Push(target string, opts *http.PushOptions) er
|
|||||||
return ErrNotImplemented
|
return ErrNotImplemented
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadFrom implements io.ReaderFrom. It simply calls the underlying
|
// ReadFrom implements io.ReaderFrom. It simply calls io.Copy,
|
||||||
// ResponseWriter's ReadFrom method if there is one, otherwise it defaults
|
// which uses io.ReaderFrom if available.
|
||||||
// to io.Copy.
|
|
||||||
func (rww *ResponseWriterWrapper) ReadFrom(r io.Reader) (n int64, err error) {
|
func (rww *ResponseWriterWrapper) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
if rf, ok := rww.ResponseWriter.(io.ReaderFrom); ok {
|
|
||||||
return rf.ReadFrom(r)
|
|
||||||
}
|
|
||||||
return io.Copy(rww.ResponseWriter, r)
|
return io.Copy(rww.ResponseWriter, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unwrap returns the underlying ResponseWriter.
|
// Unwrap returns the underlying ResponseWriter, necessary for
|
||||||
|
// http.ResponseController to work correctly.
|
||||||
func (rww *ResponseWriterWrapper) Unwrap() http.ResponseWriter {
|
func (rww *ResponseWriterWrapper) Unwrap() http.ResponseWriter {
|
||||||
return rww.ResponseWriter
|
return rww.ResponseWriter
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPInterfaces mix all the interfaces that middleware ResponseWriters need to support.
|
|
||||||
type HTTPInterfaces interface {
|
|
||||||
http.ResponseWriter
|
|
||||||
http.Pusher
|
|
||||||
http.Flusher
|
|
||||||
http.Hijacker
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrNotImplemented is returned when an underlying
|
// ErrNotImplemented is returned when an underlying
|
||||||
// ResponseWriter does not implement the required method.
|
// ResponseWriter does not implement the required method.
|
||||||
var ErrNotImplemented = fmt.Errorf("method not implemented")
|
var ErrNotImplemented = fmt.Errorf("method not implemented")
|
||||||
@ -262,7 +231,8 @@ func (rr *responseRecorder) WriteResponse() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (rr *responseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
func (rr *responseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||||
conn, brw, err := rr.ResponseWriterWrapper.Hijack()
|
//nolint:bodyclose
|
||||||
|
conn, brw, err := http.NewResponseController(rr.ResponseWriterWrapper).Hijack()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@ -294,7 +264,7 @@ func (hc *hijackedConn) ReadFrom(r io.Reader) (int64, error) {
|
|||||||
// responses instead of writing them to the client. See
|
// responses instead of writing them to the client. See
|
||||||
// docs for NewResponseRecorder for proper usage.
|
// docs for NewResponseRecorder for proper usage.
|
||||||
type ResponseRecorder interface {
|
type ResponseRecorder interface {
|
||||||
HTTPInterfaces
|
http.ResponseWriter
|
||||||
Status() int
|
Status() int
|
||||||
Buffer() *bytes.Buffer
|
Buffer() *bytes.Buffer
|
||||||
Buffered() bool
|
Buffered() bool
|
||||||
@ -309,12 +279,13 @@ type ShouldBufferFunc func(status int, header http.Header) bool
|
|||||||
|
|
||||||
// Interface guards
|
// Interface guards
|
||||||
var (
|
var (
|
||||||
_ HTTPInterfaces = (*ResponseWriterWrapper)(nil)
|
_ http.ResponseWriter = (*ResponseWriterWrapper)(nil)
|
||||||
_ ResponseRecorder = (*responseRecorder)(nil)
|
_ ResponseRecorder = (*responseRecorder)(nil)
|
||||||
|
|
||||||
// Implementing ReaderFrom can be such a significant
|
// Implementing ReaderFrom can be such a significant
|
||||||
// optimization that it should probably be required!
|
// optimization that it should probably be required!
|
||||||
// see PR #5022 (25%-50% speedup)
|
// see PR #5022 (25%-50% speedup)
|
||||||
_ io.ReaderFrom = (*ResponseWriterWrapper)(nil)
|
_ io.ReaderFrom = (*ResponseWriterWrapper)(nil)
|
||||||
_ io.ReaderFrom = (*responseRecorder)(nil)
|
_ io.ReaderFrom = (*responseRecorder)(nil)
|
||||||
|
_ io.ReaderFrom = (*hijackedConn)(nil)
|
||||||
)
|
)
|
||||||
|
@ -962,9 +962,8 @@ func (h *Handler) finalizeResponse(
|
|||||||
// Force chunking if we saw a response trailer.
|
// Force chunking if we saw a response trailer.
|
||||||
// This prevents net/http from calculating the length for short
|
// This prevents net/http from calculating the length for short
|
||||||
// bodies and adding a Content-Length.
|
// bodies and adding a Content-Length.
|
||||||
if fl, ok := rw.(http.Flusher); ok {
|
//nolint:bodyclose
|
||||||
fl.Flush()
|
http.NewResponseController(rw).Flush()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// total duration spent proxying, including writing response body
|
// total duration spent proxying, including writing response body
|
||||||
|
@ -20,6 +20,7 @@ package reverseproxy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
weakrand "math/rand"
|
weakrand "math/rand"
|
||||||
@ -51,17 +52,19 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, rw http.ResponseWrit
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
hj, ok := rw.(http.Hijacker)
|
|
||||||
if !ok {
|
|
||||||
logger.Error("can't switch protocols using non-Hijacker ResponseWriter", zap.String("type", fmt.Sprintf("%T", rw)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
backConn, ok := res.Body.(io.ReadWriteCloser)
|
backConn, ok := res.Body.(io.ReadWriteCloser)
|
||||||
if !ok {
|
if !ok {
|
||||||
logger.Error("internal error: 101 switching protocols response with non-writable body")
|
logger.Error("internal error: 101 switching protocols response with non-writable body")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nolint:bodyclose
|
||||||
|
conn, brw, hijackErr := http.NewResponseController(rw).Hijack()
|
||||||
|
if errors.Is(hijackErr, http.ErrNotSupported) {
|
||||||
|
h.logger.Sugar().Errorf("can't switch protocols using non-Hijacker ResponseWriter type %T", rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// adopted from https://github.com/golang/go/commit/8bcf2834afdf6a1f7937390903a41518715ef6f5
|
// adopted from https://github.com/golang/go/commit/8bcf2834afdf6a1f7937390903a41518715ef6f5
|
||||||
backConnCloseCh := make(chan struct{})
|
backConnCloseCh := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
@ -81,9 +84,8 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, rw http.ResponseWrit
|
|||||||
rw.WriteHeader(res.StatusCode)
|
rw.WriteHeader(res.StatusCode)
|
||||||
|
|
||||||
logger.Debug("upgrading connection")
|
logger.Debug("upgrading connection")
|
||||||
conn, brw, err := hj.Hijack()
|
if hijackErr != nil {
|
||||||
if err != nil {
|
h.logger.Error("hijack failed on protocol switch", zap.Error(hijackErr))
|
||||||
logger.Error("hijack failed on protocol switch", zap.Error(err))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,26 +183,28 @@ func (h Handler) isBidirectionalStream(req *http.Request, res *http.Response) bo
|
|||||||
(ae == "identity" || ae == "")
|
(ae == "identity" || ae == "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h Handler) copyResponse(dst io.Writer, src io.Reader, flushInterval time.Duration) error {
|
func (h Handler) copyResponse(dst http.ResponseWriter, src io.Reader, flushInterval time.Duration) error {
|
||||||
|
var w io.Writer = dst
|
||||||
|
|
||||||
if flushInterval != 0 {
|
if flushInterval != 0 {
|
||||||
if wf, ok := dst.(writeFlusher); ok {
|
mlw := &maxLatencyWriter{
|
||||||
mlw := &maxLatencyWriter{
|
dst: dst,
|
||||||
dst: wf,
|
//nolint:bodyclose
|
||||||
latency: flushInterval,
|
flush: http.NewResponseController(dst).Flush,
|
||||||
}
|
latency: flushInterval,
|
||||||
defer mlw.stop()
|
|
||||||
|
|
||||||
// set up initial timer so headers get flushed even if body writes are delayed
|
|
||||||
mlw.flushPending = true
|
|
||||||
mlw.t = time.AfterFunc(flushInterval, mlw.delayedFlush)
|
|
||||||
|
|
||||||
dst = mlw
|
|
||||||
}
|
}
|
||||||
|
defer mlw.stop()
|
||||||
|
|
||||||
|
// set up initial timer so headers get flushed even if body writes are delayed
|
||||||
|
mlw.flushPending = true
|
||||||
|
mlw.t = time.AfterFunc(flushInterval, mlw.delayedFlush)
|
||||||
|
|
||||||
|
w = mlw
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := streamingBufPool.Get().(*[]byte)
|
buf := streamingBufPool.Get().(*[]byte)
|
||||||
defer streamingBufPool.Put(buf)
|
defer streamingBufPool.Put(buf)
|
||||||
_, err := h.copyBuffer(dst, src, *buf)
|
_, err := h.copyBuffer(w, src, *buf)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -439,13 +443,9 @@ type openConnection struct {
|
|||||||
gracefulClose func() error
|
gracefulClose func() error
|
||||||
}
|
}
|
||||||
|
|
||||||
type writeFlusher interface {
|
|
||||||
io.Writer
|
|
||||||
http.Flusher
|
|
||||||
}
|
|
||||||
|
|
||||||
type maxLatencyWriter struct {
|
type maxLatencyWriter struct {
|
||||||
dst writeFlusher
|
dst io.Writer
|
||||||
|
flush func() error
|
||||||
latency time.Duration // non-zero; negative means to flush immediately
|
latency time.Duration // non-zero; negative means to flush immediately
|
||||||
|
|
||||||
mu sync.Mutex // protects t, flushPending, and dst.Flush
|
mu sync.Mutex // protects t, flushPending, and dst.Flush
|
||||||
@ -458,7 +458,8 @@ func (m *maxLatencyWriter) Write(p []byte) (n int, err error) {
|
|||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
n, err = m.dst.Write(p)
|
n, err = m.dst.Write(p)
|
||||||
if m.latency < 0 {
|
if m.latency < 0 {
|
||||||
m.dst.Flush()
|
//nolint:errcheck
|
||||||
|
m.flush()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if m.flushPending {
|
if m.flushPending {
|
||||||
@ -479,7 +480,8 @@ func (m *maxLatencyWriter) delayedFlush() {
|
|||||||
if !m.flushPending { // if stop was called but AfterFunc already started this goroutine
|
if !m.flushPending { // if stop was called but AfterFunc already started this goroutine
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
m.dst.Flush()
|
//nolint:errcheck
|
||||||
|
m.flush()
|
||||||
m.flushPending = false
|
m.flushPending = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ package reverseproxy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@ -13,12 +14,15 @@ func TestHandlerCopyResponse(t *testing.T) {
|
|||||||
strings.Repeat("a", defaultBufferSize),
|
strings.Repeat("a", defaultBufferSize),
|
||||||
strings.Repeat("123456789 123456789 123456789 12", 3000),
|
strings.Repeat("123456789 123456789 123456789 12", 3000),
|
||||||
}
|
}
|
||||||
|
|
||||||
dst := bytes.NewBuffer(nil)
|
dst := bytes.NewBuffer(nil)
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
recorder.Body = dst
|
||||||
|
|
||||||
for _, d := range testdata {
|
for _, d := range testdata {
|
||||||
src := bytes.NewBuffer([]byte(d))
|
src := bytes.NewBuffer([]byte(d))
|
||||||
dst.Reset()
|
dst.Reset()
|
||||||
err := h.copyResponse(dst, src, 0)
|
err := h.copyResponse(recorder, src, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed with error: %v", err)
|
t.Errorf("failed with error: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -82,6 +82,26 @@ type Server struct {
|
|||||||
// HTTP request headers.
|
// HTTP request headers.
|
||||||
MaxHeaderBytes int `json:"max_header_bytes,omitempty"`
|
MaxHeaderBytes int `json:"max_header_bytes,omitempty"`
|
||||||
|
|
||||||
|
// Enable full-duplex communication for HTTP/1 requests.
|
||||||
|
// Only has an effect if Caddy was built with Go 1.21 or later.
|
||||||
|
//
|
||||||
|
// For HTTP/1 requests, the Go HTTP server by default consumes any
|
||||||
|
// unread portion of the request body before beginning to write the
|
||||||
|
// response, preventing handlers from concurrently reading from the
|
||||||
|
// request and writing the response. Enabling this option disables
|
||||||
|
// this behavior and permits handlers to continue to read from the
|
||||||
|
// request while concurrently writing the response.
|
||||||
|
//
|
||||||
|
// For HTTP/2 requests, the Go HTTP server always permits concurrent
|
||||||
|
// reads and responses, so this option has no effect.
|
||||||
|
//
|
||||||
|
// Test thoroughly with your HTTP clients, as some older clients may
|
||||||
|
// not support full-duplex HTTP/1 which can cause them to deadlock.
|
||||||
|
// See https://github.com/golang/go/issues/57786 for more info.
|
||||||
|
//
|
||||||
|
// TODO: This is an EXPERIMENTAL feature. Subject to change or removal.
|
||||||
|
EnableFullDuplex bool `json:"enable_full_duplex,omitempty"`
|
||||||
|
|
||||||
// Routes describes how this server will handle requests.
|
// Routes describes how this server will handle requests.
|
||||||
// Routes are executed sequentially. First a route's matchers
|
// Routes are executed sequentially. First a route's matchers
|
||||||
// are evaluated, then its grouping. If it matches and has
|
// are evaluated, then its grouping. If it matches and has
|
||||||
@ -264,6 +284,14 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
repl := caddy.NewReplacer()
|
repl := caddy.NewReplacer()
|
||||||
r = PrepareRequest(r, repl, w, s)
|
r = PrepareRequest(r, repl, w, s)
|
||||||
|
|
||||||
|
// enable full-duplex for HTTP/1, ensuring the entire
|
||||||
|
// request body gets consumed before writing the response
|
||||||
|
if s.EnableFullDuplex {
|
||||||
|
// TODO: Remove duplex_go12*.go abstraction once our
|
||||||
|
// minimum Go version is 1.21 or later
|
||||||
|
enableFullDuplex(w)
|
||||||
|
}
|
||||||
|
|
||||||
// encode the request for logging purposes before
|
// encode the request for logging purposes before
|
||||||
// it enters any handler chain; this is necessary
|
// it enters any handler chain; this is necessary
|
||||||
// to capture the original request in case it gets
|
// to capture the original request in case it gets
|
||||||
|
Loading…
x
Reference in New Issue
Block a user