Merge pull request #763 from aaronlehmann/close-notifier

Use CloseNotifier to supress spurious HTTP 400 errors on early disconnect
This commit is contained in:
Richard Scothern 2015-08-03 13:57:32 -07:00
commit db12c889e1
10 changed files with 175 additions and 32 deletions

2
Godeps/Godeps.json generated
View file

@ -67,7 +67,7 @@
}, },
{ {
"ImportPath": "github.com/gorilla/handlers", "ImportPath": "github.com/gorilla/handlers",
"Rev": "0e84b7d810c16aed432217e330206be156bafae0" "Rev": "60c7bfde3e33c201519a200a4507a158cc03a17b"
}, },
{ {
"ImportPath": "github.com/gorilla/mux", "ImportPath": "github.com/gorilla/mux",

View file

@ -1,8 +1,8 @@
language: go language: go
go: go:
- 1.0
- 1.1 - 1.1
- 1.2 - 1.2
- 1.3 - 1.3
- 1.4
- tip - tip

View file

@ -1,6 +1,52 @@
gorilla/handlers gorilla/handlers
================ ================
[![Build Status](https://travis-ci.org/gorilla/handlers.png?branch=master)](https://travis-ci.org/gorilla/handlers) [![GoDoc](https://godoc.org/github.com/gorilla/handlers?status.svg)](https://godoc.org/github.com/gorilla/handlers) [![Build Status](https://travis-ci.org/gorilla/handlers.svg?branch=master)](https://travis-ci.org/gorilla/handlers)
Package handlers is a collection of handlers (aka "HTTP middleware") for use
with Go's `net/http` package (or any framework supporting `http.Handler`), including:
* `LoggingHandler` for logging HTTP requests in the Apache [Common Log
Format](http://httpd.apache.org/docs/2.2/logs.html#common).
* `CombinedLoggingHandler` for logging HTTP requests in the Apache [Combined Log
Format](http://httpd.apache.org/docs/2.2/logs.html#combined) commonly used by
both Apache and nginx.
* `CompressHandler` for gzipping responses.
* `ContentTypeHandler` for validating requests against a list of accepted
content types.
* `MethodHandler` for matching HTTP methods against handlers in a
`map[string]http.Handler`
* `ProxyHeaders` for populating `r.RemoteAddr` and `r.URL.Scheme` based on the
`X-Forwarded-For`, `X-Real-IP`, `X-Forwarded-Proto` and RFC7239 `Forwarded`
headers when running a Go server behind a HTTP reverse proxy.
* `CanonicalHost` for re-directing to the preferred host when handling multiple
domains (i.e. multiple CNAME aliases).
Other handlers are documented [on the Gorilla
website](http://www.gorillatoolkit.org/pkg/handlers).
## Example
A simple example using `handlers.LoggingHandler` and `handlers.CompressHandler`:
```go
import (
"net/http"
"github.com/gorilla/handlers"
)
func main() {
r := http.NewServeMux()
// Only log requests to our admin dashboard to stdout
r.Handle("/admin", handlers.LoggingHandler(os.Stdout, http.HandlerFunc(ShowAdminDashboard)))
r.HandleFunc("/", ShowIndex)
// Wrap our server with our gzip handler to gzip compress all responses.
http.ListenAndServe(":8000", handlers.CompressHandler(r))
}
```
## License
BSD licensed. See the included LICENSE file for details.
*Warning:* This package is a work in progress and the APIs are subject to change.
Consider this a v0 project.

View file

@ -15,6 +15,7 @@ import (
type compressResponseWriter struct { type compressResponseWriter struct {
io.Writer io.Writer
http.ResponseWriter http.ResponseWriter
http.Hijacker
} }
func (w *compressResponseWriter) Header() http.Header { func (w *compressResponseWriter) Header() http.Header {
@ -30,6 +31,8 @@ func (w *compressResponseWriter) Write(b []byte) (int, error) {
return w.Writer.Write(b) return w.Writer.Write(b)
} }
// CompressHandler gzip compresses HTTP responses for clients that support it
// via the 'Accept-Encoding' header.
func CompressHandler(h http.Handler) http.Handler { func CompressHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
L: L:
@ -42,10 +45,17 @@ func CompressHandler(h http.Handler) http.Handler {
gw := gzip.NewWriter(w) gw := gzip.NewWriter(w)
defer gw.Close() defer gw.Close()
h, hok := w.(http.Hijacker)
if !hok { /* w is not Hijacker... oh well... */
h = nil
}
w = &compressResponseWriter{ w = &compressResponseWriter{
Writer: gw, Writer: gw,
ResponseWriter: w, ResponseWriter: w,
Hijacker: h,
} }
break L break L
case "deflate": case "deflate":
w.Header().Set("Content-Encoding", "deflate") w.Header().Set("Content-Encoding", "deflate")
@ -54,10 +64,17 @@ func CompressHandler(h http.Handler) http.Handler {
fw, _ := flate.NewWriter(w, flate.DefaultCompression) fw, _ := flate.NewWriter(w, flate.DefaultCompression)
defer fw.Close() defer fw.Close()
h, hok := w.(http.Hijacker)
if !hok { /* w is not Hijacker... oh well... */
h = nil
}
w = &compressResponseWriter{ w = &compressResponseWriter{
Writer: fw, Writer: fw,
ResponseWriter: w, ResponseWriter: w,
Hijacker: h,
} }
break L break L
} }
} }

View file

@ -2,9 +2,6 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
/*
Package handlers is a collection of handlers for use with Go's net/http package.
*/
package handlers package handlers
import ( import (
@ -29,7 +26,7 @@ import (
// available methods. // available methods.
// //
// If the request's method doesn't match any of its keys the handler responds with // If the request's method doesn't match any of its keys the handler responds with
// a status of 406, Method not allowed and sets the Allow header to a comma-separated list // a status of 405, Method not allowed and sets the Allow header to a comma-separated list
// of available methods. // of available methods.
type MethodHandler map[string]http.Handler type MethodHandler map[string]http.Handler
@ -65,12 +62,7 @@ type combinedLoggingHandler struct {
func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
t := time.Now() t := time.Now()
var logger loggingResponseWriter logger := makeLogger(w)
if _, ok := w.(http.Hijacker); ok {
logger = &hijackLogger{responseLogger: responseLogger{w: w}}
} else {
logger = &responseLogger{w: w}
}
url := *req.URL url := *req.URL
h.handler.ServeHTTP(logger, req) h.handler.ServeHTTP(logger, req)
writeLog(h.writer, req, url, t, logger.Status(), logger.Size()) writeLog(h.writer, req, url, t, logger.Status(), logger.Size())
@ -78,19 +70,31 @@ func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
func (h combinedLoggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (h combinedLoggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
t := time.Now() t := time.Now()
var logger loggingResponseWriter logger := makeLogger(w)
if _, ok := w.(http.Hijacker); ok {
logger = &hijackLogger{responseLogger: responseLogger{w: w}}
} else {
logger = &responseLogger{w: w}
}
url := *req.URL url := *req.URL
h.handler.ServeHTTP(logger, req) h.handler.ServeHTTP(logger, req)
writeCombinedLog(h.writer, req, url, t, logger.Status(), logger.Size()) writeCombinedLog(h.writer, req, url, t, logger.Status(), logger.Size())
} }
func makeLogger(w http.ResponseWriter) loggingResponseWriter {
var logger loggingResponseWriter = &responseLogger{w: w}
if _, ok := w.(http.Hijacker); ok {
logger = &hijackLogger{responseLogger{w: w}}
}
h, ok1 := logger.(http.Hijacker)
c, ok2 := w.(http.CloseNotifier)
if ok1 && ok2 {
return hijackCloseNotifier{logger, h, c}
}
if ok2 {
return &closeNotifyWriter{logger, c}
}
return logger
}
type loggingResponseWriter interface { type loggingResponseWriter interface {
http.ResponseWriter http.ResponseWriter
http.Flusher
Status() int Status() int
Size() int Size() int
} }
@ -130,6 +134,13 @@ func (l *responseLogger) Size() int {
return l.size return l.size
} }
func (l *responseLogger) Flush() {
f, ok := l.w.(http.Flusher)
if ok {
f.Flush()
}
}
type hijackLogger struct { type hijackLogger struct {
responseLogger responseLogger
} }
@ -144,6 +155,17 @@ func (l *hijackLogger) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return conn, rw, err return conn, rw, err
} }
type closeNotifyWriter struct {
loggingResponseWriter
http.CloseNotifier
}
type hijackCloseNotifier struct {
loggingResponseWriter
http.Hijacker
http.CloseNotifier
}
const lowerhex = "0123456789abcdef" const lowerhex = "0123456789abcdef"
func appendQuoted(buf []byte, s string) []byte { func appendQuoted(buf []byte, s string) []byte {

View file

@ -103,8 +103,13 @@ func GetRequestID(ctx Context) string {
// WithResponseWriter returns a new context and response writer that makes // WithResponseWriter returns a new context and response writer that makes
// interesting response statistics available within the context. // interesting response statistics available within the context.
func WithResponseWriter(ctx Context, w http.ResponseWriter) (Context, http.ResponseWriter) { func WithResponseWriter(ctx Context, w http.ResponseWriter) (Context, http.ResponseWriter) {
closeNotifier, ok := w.(http.CloseNotifier)
if !ok {
panic("the ResponseWriter does not implement CloseNotifier")
}
irw := &instrumentedResponseWriter{ irw := &instrumentedResponseWriter{
ResponseWriter: w, ResponseWriter: w,
CloseNotifier: closeNotifier,
Context: ctx, Context: ctx,
} }
@ -262,6 +267,7 @@ func (ctx *muxVarsContext) Value(key interface{}) interface{} {
// context. // context.
type instrumentedResponseWriter struct { type instrumentedResponseWriter struct {
http.ResponseWriter http.ResponseWriter
http.CloseNotifier
Context Context
mu sync.Mutex mu sync.Mutex

View file

@ -110,6 +110,13 @@ func (trw *testResponseWriter) Header() http.Header {
return trw.header return trw.header
} }
// CloseNotify is only here to make the testResponseWriter implement the
// http.CloseNotifier interface, which WithResponseWriter expects to be
// implemented.
func (trw *testResponseWriter) CloseNotify() <-chan bool {
return nil
}
func (trw *testResponseWriter) Write(p []byte) (n int, err error) { func (trw *testResponseWriter) Write(p []byte) (n int, err error) {
if trw.status == 0 { if trw.status == 0 {
trw.status = http.StatusOK trw.status = http.StatusOK

View file

@ -2,7 +2,6 @@ package handlers
import ( import (
"fmt" "fmt"
"io"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
@ -170,10 +169,8 @@ func (buh *blobUploadHandler) PatchBlobData(w http.ResponseWriter, r *http.Reque
// TODO(dmcgowan): support Content-Range header to seek and write range // TODO(dmcgowan): support Content-Range header to seek and write range
// Copy the data if err := copyFullPayload(w, r, buh.Upload, buh, "blob PATCH", &buh.Errors); err != nil {
if _, err := io.Copy(buh.Upload, r.Body); err != nil { // copyFullPayload reports the error if necessary
ctxu.GetLogger(buh).Errorf("unknown error copying into upload: %v", err)
buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
return return
} }
@ -211,10 +208,8 @@ func (buh *blobUploadHandler) PutBlobUploadComplete(w http.ResponseWriter, r *ht
return return
} }
// Read in the data, if any. if err := copyFullPayload(w, r, buh.Upload, buh, "blob PUT", &buh.Errors); err != nil {
if _, err := io.Copy(buh.Upload, r.Body); err != nil { // copyFullPayload reports the error if necessary
ctxu.GetLogger(buh).Errorf("unknown error copying into upload: %v", err)
buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
return return
} }

View file

@ -1,8 +1,12 @@
package handlers package handlers
import ( import (
"errors"
"io" "io"
"net/http" "net/http"
ctxu "github.com/docker/distribution/context"
"github.com/docker/distribution/registry/api/errcode"
) )
// closeResources closes all the provided resources after running the target // closeResources closes all the provided resources after running the target
@ -15,3 +19,44 @@ func closeResources(handler http.Handler, closers ...io.Closer) http.Handler {
handler.ServeHTTP(w, r) handler.ServeHTTP(w, r)
}) })
} }
// copyFullPayload copies the payload of a HTTP request to destWriter. If it
// receives less content than expected, and the client disconnected during the
// upload, it avoids sending a 400 error to keep the logs cleaner.
func copyFullPayload(responseWriter http.ResponseWriter, r *http.Request, destWriter io.Writer, context ctxu.Context, action string, errSlice *errcode.Errors) error {
// Get a channel that tells us if the client disconnects
var clientClosed <-chan bool
if notifier, ok := responseWriter.(http.CloseNotifier); ok {
clientClosed = notifier.CloseNotify()
} else {
panic("the ResponseWriter does not implement CloseNotifier")
}
// Read in the data, if any.
copied, err := io.Copy(destWriter, r.Body)
if clientClosed != nil && (err != nil || (r.ContentLength > 0 && copied < r.ContentLength)) {
// Didn't recieve as much content as expected. Did the client
// disconnect during the request? If so, avoid returning a 400
// error to keep the logs cleaner.
select {
case <-clientClosed:
// Set the response code to "499 Client Closed Request"
// Even though the connection has already been closed,
// this causes the logger to pick up a 499 error
// instead of showing 0 for the HTTP status.
responseWriter.WriteHeader(499)
ctxu.GetLogger(context).Error("client disconnected during " + action)
return errors.New("client disconnected")
default:
}
}
if err != nil {
ctxu.GetLogger(context).Errorf("unknown error reading request payload: %v", err)
*errSlice = append(*errSlice, errcode.ErrorCodeUnknown.WithDetail(err))
return err
}
return nil
}

View file

@ -1,6 +1,7 @@
package handlers package handlers
import ( import (
"bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
@ -112,10 +113,14 @@ func (imh *imageManifestHandler) PutImageManifest(w http.ResponseWriter, r *http
return return
} }
dec := json.NewDecoder(r.Body) var jsonBuf bytes.Buffer
if err := copyFullPayload(w, r, &jsonBuf, imh, "image manifest PUT", &imh.Errors); err != nil {
// copyFullPayload reports the error if necessary
return
}
var manifest manifest.SignedManifest var manifest manifest.SignedManifest
if err := dec.Decode(&manifest); err != nil { if err := json.Unmarshal(jsonBuf.Bytes(), &manifest); err != nil {
imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err)) imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err))
return return
} }