0
Fork 0
mirror of https://github.com/caddyserver/caddy.git synced 2024-12-23 22:27:38 -05:00

proxy: recognize client's cancellation

fix issue #1589

Signed-off-by: Tw <tw19881113@gmail.com>
This commit is contained in:
Tw 2017-04-19 22:56:51 +08:00
parent 705cb98865
commit 0146bb4e49
7 changed files with 87 additions and 41 deletions

View file

@ -9,8 +9,6 @@ import (
"net/http" "net/http"
"strings" "strings"
"errors"
"github.com/mholt/caddy" "github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver" "github.com/mholt/caddy/caddyhttp/httpserver"
) )
@ -155,7 +153,7 @@ func (w *gzipResponseWriter) Push(target string, opts *http.PushOptions) error {
return pusher.Push(target, opts) return pusher.Push(target, opts)
} }
return errors.New("push is unavailable (probably chained http.ResponseWriter does not implement http.Pusher)") return httpserver.NonFlusherError{Underlying: w.ResponseWriter}
} }
// Interface guards // Interface guards

View file

@ -9,7 +9,6 @@ import (
"net/http" "net/http"
"strings" "strings"
"errors"
"github.com/mholt/caddy/caddyhttp/httpserver" "github.com/mholt/caddy/caddyhttp/httpserver"
) )
@ -141,7 +140,7 @@ func (rww *responseWriterWrapper) Push(target string, opts *http.PushOptions) er
return pusher.Push(target, opts) return pusher.Push(target, opts)
} }
return errors.New("push is unavailable (probably chained http.ResponseWriter does not implement http.Pusher)") return httpserver.NonPusherError{Underlying: rww.ResponseWriter}
} }
// Interface guards // Interface guards

View file

@ -8,6 +8,7 @@ var (
_ error = NonHijackerError{} _ error = NonHijackerError{}
_ error = NonFlusherError{} _ error = NonFlusherError{}
_ error = NonCloseNotifierError{} _ error = NonCloseNotifierError{}
_ error = NonPusherError{}
) )
// NonHijackerError is more descriptive error caused by a non hijacker // NonHijackerError is more descriptive error caused by a non hijacker
@ -42,3 +43,14 @@ type NonCloseNotifierError struct {
func (c NonCloseNotifierError) Error() string { func (c NonCloseNotifierError) Error() string {
return fmt.Sprintf("%T is not a closeNotifier", c.Underlying) return fmt.Sprintf("%T is not a closeNotifier", c.Underlying)
} }
// NonPusherError is more descriptive error caused by a non pusher
type NonPusherError struct {
// underlying type which doesn't implement pusher
Underlying interface{}
}
// Implement Error
func (c NonPusherError) Error() string {
return fmt.Sprintf("%T is not a pusher", c.Underlying)
}

View file

@ -2,7 +2,6 @@ package httpserver
import ( import (
"bufio" "bufio"
"errors"
"net" "net"
"net/http" "net/http"
"time" "time"
@ -103,7 +102,7 @@ func (r *ResponseRecorder) Push(target string, opts *http.PushOptions) error {
return pusher.Push(target, opts) return pusher.Push(target, opts)
} }
return errors.New("push is unavailable (probably chained http.ResponseWriter does not implement http.Pusher)") return NonPusherError{Underlying: r.ResponseWriter}
} }
// Interface guards // Interface guards

View file

@ -2,6 +2,7 @@
package proxy package proxy
import ( import (
"context"
"errors" "errors"
"net" "net"
"net/http" "net/http"
@ -103,7 +104,8 @@ func (p Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
replacer := httpserver.NewReplacer(r, nil, "") replacer := httpserver.NewReplacer(r, nil, "")
// outreq is the request that makes a roundtrip to the backend // outreq is the request that makes a roundtrip to the backend
outreq := createUpstreamRequest(r) outreq, cancel := createUpstreamRequest(w, r)
defer cancel()
// If we have more than one upstream host defined and if retrying is enabled // If we have more than one upstream host defined and if retrying is enabled
// by setting try_duration to a non-zero value, caddy will try to // by setting try_duration to a non-zero value, caddy will try to
@ -131,7 +133,11 @@ func (p Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
// loop and try to select another host, or false if we // loop and try to select another host, or false if we
// should break and stop retrying. // should break and stop retrying.
start := time.Now() start := time.Now()
keepRetrying := func() bool { keepRetrying := func(backendErr error) bool {
// if downstream has canceled the request, break
if backendErr == context.Canceled {
return false
}
// if we've tried long enough, break // if we've tried long enough, break
if time.Since(start) >= upstream.GetTryDuration() { if time.Since(start) >= upstream.GetTryDuration() {
return false return false
@ -150,7 +156,7 @@ func (p Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
if backendErr == nil { if backendErr == nil {
backendErr = errors.New("no hosts available upstream") backendErr = errors.New("no hosts available upstream")
} }
if !keepRetrying() { if !keepRetrying(backendErr) {
break break
} }
continue continue
@ -238,7 +244,7 @@ func (p Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
} }
// if we've tried long enough, break // if we've tried long enough, break
if !keepRetrying() { if !keepRetrying(backendErr) {
break break
} }
} }
@ -267,9 +273,23 @@ func (p Proxy) match(r *http.Request) Upstream {
// that can be sent upstream. // that can be sent upstream.
// //
// Derived from reverseproxy.go in the standard Go httputil package. // Derived from reverseproxy.go in the standard Go httputil package.
func createUpstreamRequest(r *http.Request) *http.Request { func createUpstreamRequest(rw http.ResponseWriter, r *http.Request) (*http.Request, context.CancelFunc) {
outreq := new(http.Request) // Original incoming server request may be canceled by the
*outreq = *r // includes shallow copies of maps, but okay // user or by std lib(e.g. too many idle connections).
ctx, cancel := context.WithCancel(r.Context())
if cn, ok := rw.(http.CloseNotifier); ok {
notifyChan := cn.CloseNotify()
go func() {
select {
case <-notifyChan:
cancel()
case <-ctx.Done():
}
}()
}
outreq := r.WithContext(ctx) // includes shallow copies of maps, but okay
// We should set body to nil explicitly if request body is empty. // We should set body to nil explicitly if request body is empty.
// For server requests the Request Body is always non-nil. // For server requests the Request Body is always non-nil.
if r.ContentLength == 0 { if r.ContentLength == 0 {
@ -319,7 +339,7 @@ func createUpstreamRequest(r *http.Request) *http.Request {
outreq.Header.Set("X-Forwarded-For", clientIP) outreq.Header.Set("X-Forwarded-For", clientIP)
} }
return outreq return outreq, cancel
} }
func createRespHeaderUpdateFn(rules http.Header, replacer httpserver.Replacer) respUpdateFn { func createRespHeaderUpdateFn(rules http.Header, replacer httpserver.Replacer) respUpdateFn {

View file

@ -12,7 +12,6 @@ import (
"net" "net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/http/httptrace"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
@ -101,7 +100,7 @@ func TestReverseProxy(t *testing.T) {
// Make sure {upstream} placeholder is set // Make sure {upstream} placeholder is set
r.Body = ioutil.NopCloser(strings.NewReader("test")) r.Body = ioutil.NopCloser(strings.NewReader("test"))
rr := httpserver.NewResponseRecorder(httptest.NewRecorder()) rr := httpserver.NewResponseRecorder(testResponseRecorder{httptest.NewRecorder()})
rr.Replacer = httpserver.NewReplacer(r, rr, "-") rr.Replacer = httpserver.NewReplacer(r, rr, "-")
p.ServeHTTP(rr, r) p.ServeHTTP(rr, r)
@ -1123,7 +1122,18 @@ func TestReverseProxyLargeBody(t *testing.T) {
} }
func TestCancelRequest(t *testing.T) { func TestCancelRequest(t *testing.T) {
reqInFlight := make(chan struct{})
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
close(reqInFlight) // cause the client to cancel its request
select {
case <-time.After(10 * time.Second):
t.Error("Handler never saw CloseNotify")
return
case <-w.(http.CloseNotifier).CloseNotify():
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("Hello, client")) w.Write([]byte("Hello, client"))
})) }))
defer backend.Close() defer backend.Close()
@ -1140,26 +1150,21 @@ func TestCancelRequest(t *testing.T) {
defer cancel() defer cancel()
req = req.WithContext(ctx) req = req.WithContext(ctx)
// add GotConn hook to cancel the request
gotC := make(chan struct{})
defer close(gotC)
trace := &httptrace.ClientTrace{
GotConn: func(connInfo httptrace.GotConnInfo) {
gotC <- struct{}{}
},
}
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
// wait for canceling the request // wait for canceling the request
go func() { go func() {
<-gotC <-reqInFlight
cancel() cancel()
}() }()
status, err := p.ServeHTTP(httptest.NewRecorder(), req) rec := httptest.NewRecorder()
if status != 0 || err != nil { status, err := p.ServeHTTP(rec, req)
t.Errorf("expect proxy handle normally, but not, status:%d, err:%q", expectedStatus, expectErr := http.StatusBadGateway, context.Canceled
status, err) if status != expectedStatus || err != expectErr {
t.Errorf("expect proxy handle return status[%d] with error[%v], but got status[%d] with error[%v]",
expectedStatus, expectErr, status, err)
}
if body := rec.Body.String(); body != "" {
t.Errorf("expect a blank response, but got %q", body)
} }
} }
@ -1310,6 +1315,28 @@ func (c *fakeConn) Close() error { return nil }
func (c *fakeConn) Read(b []byte) (int, error) { return c.readBuf.Read(b) } func (c *fakeConn) Read(b []byte) (int, error) { return c.readBuf.Read(b) }
func (c *fakeConn) Write(b []byte) (int, error) { return c.writeBuf.Write(b) } func (c *fakeConn) Write(b []byte) (int, error) { return c.writeBuf.Write(b) }
// testResponseRecorder wraps `httptest.ResponseRecorder`,
// also implements `http.CloseNotifier`, `http.Hijacker` and `http.Pusher`.
type testResponseRecorder struct {
*httptest.ResponseRecorder
}
func (testResponseRecorder) CloseNotify() <-chan bool { return nil }
func (t testResponseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return nil, nil, httpserver.NonHijackerError{Underlying: t}
}
func (t testResponseRecorder) Push(target string, opts *http.PushOptions) error {
return httpserver.NonPusherError{Underlying: t}
}
// Interface guards
var (
_ http.Pusher = testResponseRecorder{}
_ http.Flusher = testResponseRecorder{}
_ http.CloseNotifier = testResponseRecorder{}
_ http.Hijacker = testResponseRecorder{}
)
func BenchmarkProxy(b *testing.B) { func BenchmarkProxy(b *testing.B) {
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, client")) w.Write([]byte("Hello, client"))

View file

@ -12,7 +12,6 @@
package proxy package proxy
import ( import (
"context"
"crypto/tls" "crypto/tls"
"io" "io"
"net" "net"
@ -252,14 +251,6 @@ func (rp *ReverseProxy) ServeHTTP(rw http.ResponseWriter, outreq *http.Request,
rp.Director(outreq) rp.Director(outreq)
// Original incoming server request may be canceled by the
// user or by std lib(e.g. too many idle connections).
// Now we issue the new outgoing client request which
// doesn't depend on the original one. (issue 1345)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
outreq = outreq.WithContext(ctx)
res, err := transport.RoundTrip(outreq) res, err := transport.RoundTrip(outreq)
if err != nil { if err != nil {
return err return err