1
0
mirror of https://github.com/golang/go synced 2024-11-20 08:54:40 -07:00

net/http, net/http/httptest: make http2's TrailerPrefix work for http1

Go's http1 implementation originally had a mechanism to send HTTP
trailers based on pre-declaring the trailer keys whose values you'd
later let after the header was written.

http2 copied the same mechanism, but it was found to be unsufficient
for gRPC's wire protocol. A second trailer mechanism was added later
(but only to http2) for handlers that want to send a trailer without
knowing in advance they'd need to.

Copy the same mechanism back to http1 and document it.

Fixes #15754

Change-Id: I8c40d55e28b0e5b7087d3d1a904a392c56ee1f9b
Reviewed-on: https://go-review.googlesource.com/32479
Reviewed-by: Ian Lance Taylor <iant@golang.org>
This commit is contained in:
Brad Fitzpatrick 2016-11-01 15:24:11 +00:00
parent 3c2f607274
commit b2c54afe14
4 changed files with 111 additions and 12 deletions

View File

@ -1270,3 +1270,34 @@ func testNoSniffExpectRequestBody(t *testing.T, h2 bool) {
t.Errorf("status code = %v; want %v", res.StatusCode, StatusUnauthorized) t.Errorf("status code = %v; want %v", res.StatusCode, StatusUnauthorized)
} }
} }
func TestServerUndeclaredTrailers_h1(t *testing.T) { testServerUndeclaredTrailers(t, h1Mode) }
func TestServerUndeclaredTrailers_h2(t *testing.T) { testServerUndeclaredTrailers(t, h2Mode) }
func testServerUndeclaredTrailers(t *testing.T, h2 bool) {
defer afterTest(t)
cst := newClientServerTest(t, h2, HandlerFunc(func(w ResponseWriter, r *Request) {
w.Header().Set("Foo", "Bar")
w.Header().Set("Trailer:Foo", "Baz")
w.(Flusher).Flush()
w.Header().Add("Trailer:Foo", "Baz2")
w.Header().Set("Trailer:Bar", "Quux")
}))
defer cst.close()
res, err := cst.c.Get(cst.ts.URL)
if err != nil {
t.Fatal(err)
}
if _, err := io.Copy(ioutil.Discard, res.Body); err != nil {
t.Fatal(err)
}
res.Body.Close()
delete(res.Header, "Date")
delete(res.Header, "Content-Type")
if want := (Header{"Foo": {"Bar"}}); !reflect.DeepEqual(res.Header, want) {
t.Errorf("Header = %#v; want %#v", res.Header, want)
}
if want := (Header{"Foo": {"Baz", "Baz2"}, "Bar": {"Quux"}}); !reflect.DeepEqual(res.Trailer, want) {
t.Errorf("Trailer = %#v; want %#v", res.Trailer, want)
}
}

View File

@ -203,6 +203,17 @@ func (rw *ResponseRecorder) Result() *http.Response {
res.Trailer[k] = vv2 res.Trailer[k] = vv2
} }
} }
for k, vv := range rw.HeaderMap {
if !strings.HasPrefix(k, http.TrailerPrefix) {
continue
}
if res.Trailer == nil {
res.Trailer = make(http.Header)
}
for _, v := range vv {
res.Trailer.Add(strings.TrimPrefix(k, http.TrailerPrefix), v)
}
}
return res return res
} }

View File

@ -207,6 +207,7 @@ func TestRecorder(t *testing.T) {
w.Header().Set("Trailer-A", "valuea") w.Header().Set("Trailer-A", "valuea")
w.Header().Set("Trailer-C", "valuec") w.Header().Set("Trailer-C", "valuec")
w.Header().Set("Trailer-NotDeclared", "should be omitted") w.Header().Set("Trailer-NotDeclared", "should be omitted")
w.Header().Set("Trailer:Trailer-D", "with prefix")
}, },
check( check(
hasStatus(200), hasStatus(200),
@ -216,6 +217,7 @@ func TestRecorder(t *testing.T) {
hasTrailer("Trailer-A", "valuea"), hasTrailer("Trailer-A", "valuea"),
hasTrailer("Trailer-C", "valuec"), hasTrailer("Trailer-C", "valuec"),
hasNotTrailers("Non-Trailer", "Trailer-B", "Trailer-NotDeclared"), hasNotTrailers("Non-Trailer", "Trailer-B", "Trailer-NotDeclared"),
hasTrailer("Trailer-D", "with prefix"),
), ),
}, },
{ {

View File

@ -87,11 +87,25 @@ type Handler interface {
// has returned. // has returned.
type ResponseWriter interface { type ResponseWriter interface {
// Header returns the header map that will be sent by // Header returns the header map that will be sent by
// WriteHeader. Changing the header after a call to // WriteHeader. The Header map also is the mechanism with which
// WriteHeader (or Write) has no effect unless the modified // Handlers can set HTTP trailers.
// headers were declared as trailers by setting the //
// "Trailer" header before the call to WriteHeader (see example). // Changing the header map after a call to WriteHeader (or
// To suppress implicit response headers, set their value to nil. // Write) has no effect unless the modified headers are
// trailers.
//
// There are two ways to set Trailers. The preferred way is to
// predeclare in the headers which trailers you will later
// send by setting the "Trailer" header to the names of the
// trailer keys which will come later. In this case, those
// keys of the Header map are treated as if they were
// trailers. See the example. The second way, for trailer
// keys not known to the Handler until after the first Write,
// is to prefix the Header map keys with the TrailerPrefix
// constant value. See TrailerPrefix.
//
// To suppress implicit response headers (such as "Date"), set
// their value to nil.
Header() Header Header() Header
// Write writes the data to the connection as part of an HTTP reply. // Write writes the data to the connection as part of an HTTP reply.
@ -358,13 +372,7 @@ func (cw *chunkWriter) close() {
bw := cw.res.conn.bufw // conn's bufio writer bw := cw.res.conn.bufw // conn's bufio writer
// zero chunk to mark EOF // zero chunk to mark EOF
bw.WriteString("0\r\n") bw.WriteString("0\r\n")
if len(cw.res.trailers) > 0 { if trailers := cw.res.finalTrailers(); trailers != nil {
trailers := make(Header)
for _, h := range cw.res.trailers {
if vv := cw.res.handlerHeader[h]; len(vv) > 0 {
trailers[h] = vv
}
}
trailers.Write(bw) // the writer handles noting errors trailers.Write(bw) // the writer handles noting errors
} }
// final blank line after the trailers (whether // final blank line after the trailers (whether
@ -432,6 +440,43 @@ type response struct {
didCloseNotify int32 // atomic (only 0->1 winner should send) didCloseNotify int32 // atomic (only 0->1 winner should send)
} }
// TrailerPrefix is a magic prefix for ResponseWriter.Header map keys
// that, if present, signals that the map entry is actually for
// the response trailers, and not the response headers. The prefix
// is stripped after the ServeHTTP call finishes and the values are
// sent in the trailers.
//
// This mechanism is intended only for trailers that are not known
// prior to the headers being written. If the set of trailers is fixed
// or known before the header is written, the normal Go trailers mechanism
// is preferred:
// https://golang.org/pkg/net/http/#ResponseWriter
// https://golang.org/pkg/net/http/#example_ResponseWriter_trailers
const TrailerPrefix = "Trailer:"
// finalTrailers is called after the Handler exits and returns a non-nil
// value if the Handler set any trailers.
func (w *response) finalTrailers() Header {
var t Header
for k, vv := range w.handlerHeader {
if strings.HasPrefix(k, TrailerPrefix) {
if t == nil {
t = make(Header)
}
t[strings.TrimPrefix(k, TrailerPrefix)] = vv
}
}
for _, k := range w.trailers {
if t == nil {
t = make(Header)
}
for _, v := range w.handlerHeader[k] {
t.Add(k, v)
}
}
return t
}
type atomicBool int32 type atomicBool int32
func (b *atomicBool) isSet() bool { return atomic.LoadInt32((*int32)(b)) != 0 } func (b *atomicBool) isSet() bool { return atomic.LoadInt32((*int32)(b)) != 0 }
@ -1105,7 +1150,17 @@ func (cw *chunkWriter) writeHeader(p []byte) {
} }
var setHeader extraHeader var setHeader extraHeader
// Don't write out the fake "Trailer:foo" keys. See TrailerPrefix.
trailers := false trailers := false
for k := range cw.header {
if strings.HasPrefix(k, TrailerPrefix) {
if excludeHeader == nil {
excludeHeader = make(map[string]bool)
}
excludeHeader[k] = true
trailers = true
}
}
for _, v := range cw.header["Trailer"] { for _, v := range cw.header["Trailer"] {
trailers = true trailers = true
foreachHeaderElement(v, cw.res.declareTrailer) foreachHeaderElement(v, cw.res.declareTrailer)