}
// Tests that servers send trailers to a client and that the client can read them.
-func TestTrailersServerToClient_h1(t *testing.T) { testTrailersServerToClient(t, h1Mode, false) }
-func TestTrailersServerToClient_h2(t *testing.T) {
- t.Skip("skipping in http2 mode; golang.org/issue/13557")
- testTrailersServerToClient(t, h2Mode, false)
-}
+func TestTrailersServerToClient_h1(t *testing.T) { testTrailersServerToClient(t, h1Mode, false) }
+func TestTrailersServerToClient_h2(t *testing.T) { testTrailersServerToClient(t, h2Mode, false) }
func TestTrailersServerToClient_Flush_h1(t *testing.T) { testTrailersServerToClient(t, h1Mode, true) }
-func TestTrailersServerToClient_Flush_h2(t *testing.T) {
- t.Skip("skipping in http2 mode; golang.org/issue/13557")
- testTrailersServerToClient(t, h2Mode, true)
-}
+func TestTrailersServerToClient_Flush_h2(t *testing.T) { testTrailersServerToClient(t, h2Mode, true) }
func testTrailersServerToClient(t *testing.T, h2, flush bool) {
defer afterTest(t)
t.Fatal(err)
}
- delete(res.Header, "Date") // irrelevant for test
- if got, want := res.Header, (Header{
+ wantHeader := Header{
"Content-Type": {"text/plain; charset=utf-8"},
- }); !reflect.DeepEqual(got, want) {
- t.Errorf("Header = %v; want %v", got, want)
+ }
+ wantLen := -1
+ if h2 && !flush {
+ // In HTTP/1.1, any use of trailers forces HTTP/1.1
+ // chunking and a flush at the first write. That's
+ // unnecessary with HTTP/2's framing, so the server
+ // is able to calculate the length while still sending
+ // trailers afterwards.
+ wantLen = len(body)
+ wantHeader["Content-Length"] = []string{fmt.Sprint(wantLen)}
+ }
+ if res.ContentLength != int64(wantLen) {
+ t.Errorf("ContentLength = %v; want %v", res.ContentLength, wantLen)
+ }
+
+ delete(res.Header, "Date") // irrelevant for test
+ if !reflect.DeepEqual(res.Header, wantHeader) {
+ t.Errorf("Header = %v; want %v", res.Header, wantHeader)
}
if got, want := res.Trailer, (Header{
"io/ioutil"
"log"
"net"
+ "net/textproto"
"net/url"
"os"
"runtime"
bw *bufio.Writer // writing to a chunkWriter{this *responseWriterState}
// mutated by http.Handler goroutine:
- handlerHeader Header // nil until called
- snapHeader Header // snapshot of handlerHeader at WriteHeader time
- status int // status code passed to WriteHeader
- wroteHeader bool // WriteHeader called (explicitly or implicitly). Not necessarily sent to user yet.
- sentHeader bool // have we sent the header frame?
- handlerDone bool // handler has finished
+ handlerHeader Header // nil until called
+ snapHeader Header // snapshot of handlerHeader at WriteHeader time
+ trailers []string // set in writeChunk
+ status int // status code passed to WriteHeader
+ wroteHeader bool // WriteHeader called (explicitly or implicitly). Not necessarily sent to user yet.
+ sentHeader bool // have we sent the header frame?
+ handlerDone bool // handler has finished
sentContentLen int64 // non-zero if handler set a Content-Length header
wroteBytes int64
func (cw http2chunkWriter) Write(p []byte) (n int, err error) { return cw.rws.writeChunk(p) }
+func (rws *http2responseWriterState) hasTrailers() bool { return len(rws.trailers) != 0 }
+
+// declareTrailer is called for each Trailer header when the
+// response header is written. It notes that a header will need to be
+// written in the trailers at the end of the response.
+func (rws *http2responseWriterState) declareTrailer(k string) {
+ k = CanonicalHeaderKey(k)
+ switch k {
+ case "Transfer-Encoding", "Content-Length", "Trailer":
+
+ return
+ }
+ rws.trailers = append(rws.trailers, k)
+}
+
// writeChunk writes chunks from the bufio.Writer. But because
// bufio.Writer may bypass its chunking, sometimes p may be
// arbitrarily large.
if !rws.wroteHeader {
rws.writeHeader(200)
}
+
isHeadResp := rws.req.Method == "HEAD"
if !rws.sentHeader {
rws.sentHeader = true
date = time.Now().UTC().Format(TimeFormat)
}
- endStream := (rws.handlerDone && len(p) == 0) || isHeadResp
+
+ for _, v := range rws.snapHeader["Trailer"] {
+ http2foreachHeaderElement(v, rws.declareTrailer)
+ }
+
+ endStream := (rws.handlerDone && !rws.hasTrailers() && len(p) == 0) || isHeadResp
err = rws.conn.writeHeaders(rws.stream, &http2writeResHeaders{
streamID: rws.stream.id,
httpResCode: rws.status,
return 0, nil
}
- if err := rws.conn.writeDataFromHandler(rws.stream, p, rws.handlerDone); err != nil {
- return 0, err
+ endStream := rws.handlerDone && !rws.hasTrailers()
+ if len(p) > 0 || endStream {
+
+ if err := rws.conn.writeDataFromHandler(rws.stream, p, endStream); err != nil {
+ return 0, err
+ }
+ }
+
+ if rws.handlerDone && rws.hasTrailers() {
+ err = rws.conn.writeHeaders(rws.stream, &http2writeResHeaders{
+ streamID: rws.stream.id,
+ h: rws.handlerHeader,
+ trailers: rws.trailers,
+ endStream: true,
+ })
+ return len(p), err
}
return len(p), nil
}
http2responseWriterStatePool.Put(rws)
}
+// foreachHeaderElement splits v according to the "#rule" construction
+// in RFC 2616 section 2.1 and calls fn for each non-empty element.
+func http2foreachHeaderElement(v string, fn func(string)) {
+ v = textproto.TrimString(v)
+ if v == "" {
+ return
+ }
+ if !strings.Contains(v, ",") {
+ fn(v)
+ return
+ }
+ for _, f := range strings.Split(v, ",") {
+ if f = textproto.TrimString(f); f != "" {
+ fn(f)
+ }
+ }
+}
+
const (
// transportDefaultConnFlow is how many connection-level flow control
// tokens we give the server at start-up, past the default 64k.
peerReset chan struct{} // closed on peer reset
resetErr error // populated before peerReset is closed
+
+ // owned by clientConnReadLoop:
+ headersDone bool // got HEADERS w/ END_HEADERS
+ trailersDone bool // got second HEADERS frame w/ END_HEADERS
+
+ trailer Header // accumulated trailers
+ resTrailer Header // client's Response.Trailer
}
// awaitRequestCancel runs in its own goroutine and waits for the user's
return nil
}
+ if cs.headersDone {
+ rl.hdec.SetEmitFunc(cs.onNewTrailerField)
+ } else {
+ rl.hdec.SetEmitFunc(rl.onNewHeaderField)
+ }
_, err := rl.hdec.Write(frag)
if err != nil {
- return err
+ return http2ConnectionError(http2ErrCodeCompression)
}
if !headersEnded {
rl.continueStreamID = cs.ID
rl.continueStreamID = 0
+ if !cs.headersDone {
+ cs.headersDone = true
+ } else {
+
+ if cs.trailersDone {
+
+ return http2ConnectionError(http2ErrCodeProtocol)
+ }
+ cs.trailersDone = true
+ if !streamEnded {
+
+ return http2ConnectionError(http2ErrCodeProtocol)
+ }
+ rl.endStream(cs)
+ return nil
+ }
+
if rl.reqMalformed != nil {
cs.resc <- http2resAndError{err: rl.reqMalformed}
rl.cc.writeStreamReset(cs.ID, http2ErrCodeProtocol, rl.reqMalformed)
}
}
+ cs.resTrailer = res.Trailer
rl.activeRes[cs.ID] = cs
cs.resc <- http2resAndError{res: res}
rl.nextRes = nil
}
if f.StreamEnded() {
- cs.bufPipe.CloseWithError(io.EOF)
- delete(rl.activeRes, cs.ID)
+ rl.endStream(cs)
}
return nil
}
+func (rl *http2clientConnReadLoop) endStream(cs *http2clientStream) {
+
+ cs.bufPipe.closeWithErrorAndCode(io.EOF, cs.copyTrailers)
+ delete(rl.activeRes, cs.ID)
+}
+
+func (cs *http2clientStream) copyTrailers() {
+ for k, vv := range cs.trailer {
+ cs.resTrailer[k] = vv
+ }
+}
+
func (rl *http2clientConnReadLoop) processGoAway(f *http2GoAwayFrame) error {
cc := rl.cc
cc.t.connPool().MarkDead(cc)
if http2VerboseLogs {
cc.logf("Header field: %+v", f)
}
+
isPseudo := strings.HasPrefix(f.Name, ":")
if isPseudo {
if rl.sawRegHeader {
}
} else {
rl.sawRegHeader = true
- rl.nextRes.Header.Add(CanonicalHeaderKey(f.Name), f.Value)
+ key := CanonicalHeaderKey(f.Name)
+ if key == "Trailer" {
+ t := rl.nextRes.Trailer
+ if t == nil {
+ t = make(Header)
+ rl.nextRes.Trailer = t
+ }
+ http2foreachHeaderElement(f.Value, func(v string) {
+ t[CanonicalHeaderKey(v)] = nil
+ })
+ } else {
+ rl.nextRes.Header.Add(key, f.Value)
+ }
+ }
+}
+
+func (cs *http2clientStream) onNewTrailerField(f hpack.HeaderField) {
+ isPseudo := strings.HasPrefix(f.Name, ":")
+ if isPseudo {
+
+ return
+ }
+ key := CanonicalHeaderKey(f.Name)
+ if _, ok := cs.resTrailer[key]; ok {
+ if cs.trailer == nil {
+ cs.trailer = make(Header)
+ }
+ const tooBig = 1000 // TODO: arbitrary; use max header list size limits
+ if cur := cs.trailer[key]; len(cur) < tooBig {
+ cs.trailer[key] = append(cur, f.Value)
+ }
}
}
}
// writeResHeaders is a request to write a HEADERS and 0+ CONTINUATION frames
-// for HTTP response headers from a server handler.
+// for HTTP response headers or trailers from a server handler.
type http2writeResHeaders struct {
streamID uint32
- httpResCode int
- h Header // may be nil
+ httpResCode int // 0 means no ":status" line
+ h Header // may be nil
+ trailers []string // if non-nil, which keys of h to write. nil means all.
endStream bool
date string
func (w *http2writeResHeaders) writeFrame(ctx http2writeContext) error {
enc, buf := ctx.HeaderEncoder()
buf.Reset()
- enc.WriteField(hpack.HeaderField{Name: ":status", Value: http2httpCodeString(w.httpResCode)})
- keys := make([]string, 0, len(w.h))
- for k := range w.h {
- keys = append(keys, k)
+ if w.httpResCode != 0 {
+ enc.WriteField(hpack.HeaderField{
+ Name: ":status",
+ Value: http2httpCodeString(w.httpResCode),
+ })
}
- sort.Strings(keys)
- for _, k := range keys {
- vv := w.h[k]
- k = http2lowerHeader(k)
- isTE := k == "transfer-encoding"
- for _, v := range vv {
- if isTE && v != "trailers" {
- continue
- }
- enc.WriteField(hpack.HeaderField{Name: k, Value: v})
- }
- }
+ http2encodeHeaders(enc, w.h, w.trailers)
+
if w.contentType != "" {
enc.WriteField(hpack.HeaderField{Name: "content-type", Value: w.contentType})
}
}
headerBlock := buf.Bytes()
- if len(headerBlock) == 0 {
+ if len(headerBlock) == 0 && w.trailers == nil {
panic("unexpected empty hpack")
}
return ctx.Framer().WriteWindowUpdate(wu.streamID, wu.n)
}
+func http2encodeHeaders(enc *hpack.Encoder, h Header, keys []string) {
+
+ if keys == nil {
+ keys = make([]string, 0, len(h))
+ for k := range h {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+ }
+ for _, k := range keys {
+ vv := h[k]
+ k = http2lowerHeader(k)
+ isTE := k == "transfer-encoding"
+ for _, v := range vv {
+
+ if isTE && v != "trailers" {
+ continue
+ }
+ enc.WriteField(hpack.HeaderField{Name: k, Value: v})
+ }
+ }
+}
+
// frameWriteMsg is a request to write a frame.
type http2frameWriteMsg struct {
// write is the interface value that does the writing, once the