From ad624da4e4a56d9a7e3b967ea9e2f26d0b5b0f8d Mon Sep 17 00:00:00 2001 From: Yang Song Date: Thu, 23 Jul 2020 15:26:53 +0800 Subject: [PATCH 1/2] Enable HTTP/2 server to receive and send unknown frames. Signed-off-by: Yang Song --- http2/frame.go | 4 +- http2/server.go | 206 +++++++++++++++++++++++++++++++++++++++++++ http2/server_test.go | 164 ++++++++++++++++++++++++++++++++++ http2/write.go | 15 ++++ 4 files changed, 388 insertions(+), 1 deletion(-) diff --git a/http2/frame.go b/http2/frame.go index 514c126c5..7648ee2a8 100644 --- a/http2/frame.go +++ b/http2/frame.go @@ -896,7 +896,9 @@ func (f *Framer) WriteGoAway(maxStreamID uint32, code ErrCode, debugData []byte) } // An UnknownFrame is the frame type returned when the frame type is unknown -// or no specific frame type parser exists. +// or no specific frame type parser exists. UnknownFrame will not be used for +// opening or closing a stream even if their flags indicate so. UnknownFrame +// will not be counted against flow control. type UnknownFrame struct { FrameHeader p []byte diff --git a/http2/server.go b/http2/server.go index 345b7cd85..17daa188f 100644 --- a/http2/server.go +++ b/http2/server.go @@ -134,6 +134,111 @@ type Server struct { // so that we don't embed a Mutex in this struct, which will make the // struct non-copyable, which might break some callers. state *serverInternalState + + // EnableUnknownFrames enables the server to handle unknown frames. We only support unknown frames + // associated with a stream. Connection level unknown frames are not supported. Unknown frames + // should not be used to open or close a stream. In addition, unknown frames should be sent after + // all HEADERS frames in a request. Otherwise, the unknown frames will be ignored. Unknown frames + // can be sent before HEADERS frames in a response. + EnableUnknownFrames bool +} + +// contextKey is a value for use with context.WithValue. It's used as +// a pointer so it fits in an interface{} without allocation. +type contextKey struct { + name string +} + +// UnknownFrameReaderKey is a context key. It can be used in http.Request.Context() to access the server's +// UnknownFrameReader. The associated value will be of type *UnknownFrameReader. +var UnknownFrameReaderKey = &contextKey{"http2-UnknownFrameReaderKey"} + +// unknownFrameTimeout is the timeout used to lock the unknown frame buffer. +var unknownFrameTimeout time.Duration = 5 * time.Second + +// UnknownFrameReader is an interface to receive unknown frames sent to the server. +// An instance of UnknownFrameReader is stored in the context of http.Request. An example usage: +// func ServeHTTP(rw http.RequestWriter, r *http.Request) { +// ... +// ufr := r.Context().Value(http2.UnknownFrameReaderKey) +// reader, ok := ufr.(UnknownFrameReader) +// f, err := reader.ReadUnknownFrame(context.Background()) +// ... +// } +type UnknownFrameReader interface { + // Call this function to get an unknown frame received by the server. + // If the server hasn't received any new unknown frame, the function will return + // error "haven't received unknown frames". If there will be no more unknown frames, the + // function will return error "no more unknown frame". + ReadUnknownFrame(ctx context.Context) (*UnknownFrame, error) +} + +type unknownFramesReceived struct { + mu sync.Mutex + unknownFrames []*UnknownFrame // a buffer stores received unknown frames + noMore bool // If true, indicates there is no more unknown frames on the stream. +} + +// Add newly received unknown frame to the buffer. +func (r *unknownFramesReceived) addUnknownFrame(f *UnknownFrame) error { + timer := time.NewTimer(time.Duration(unknownFrameTimeout)) + defer timer.Stop() + done := make(chan bool) + go func() { + r.mu.Lock() + r.unknownFrames = append(r.unknownFrames, f) + r.mu.Unlock() + done <- true + return + }() + select { + case <-done: + return nil + case <-timer.C: + return fmt.Errorf("can't add unknown frame to buffer and hit a timeout") + } +} + +func (r *unknownFramesReceived) ReadUnknownFrame(ctx context.Context) (*UnknownFrame, error) { + timer := time.NewTimer(time.Duration(unknownFrameTimeout)) + defer timer.Stop() + + fc := make(chan *UnknownFrame) + empty := make(chan bool) + noMore := make(chan bool) + go func() { + r.mu.Lock() + length := len(r.unknownFrames) + r.mu.Unlock() + if length == 0 { + if r.noMore { + noMore <- true + return + } + empty <- true + return + } + + r.mu.Lock() + uf := r.unknownFrames[0] + r.unknownFrames = r.unknownFrames[1:] + fc <- uf + r.mu.Unlock() + }() + for { + select { + case <-ctx.Done(): + return &UnknownFrame{}, fmt.Errorf("context canceled") + case <-empty: + return &UnknownFrame{}, fmt.Errorf("haven't received unknown frames") + case <-noMore: + return &UnknownFrame{}, fmt.Errorf("no more unknown frame") + case f := <-fc: + return f, nil + case <-timer.C: + return &UnknownFrame{}, fmt.Errorf("can't grab the lock, time out") + } + } } func (s *Server) initialConnRecvWindowSize() int32 { @@ -593,6 +698,8 @@ type stream struct { trailer http.Header // accumulated trailers reqTrailer http.Header // handler's Request.Trailer + + unknownFramesReceived *unknownFramesReceived } func (sc *serverConn) Framer() *Framer { return sc.framer } @@ -1054,6 +1161,41 @@ func (sc *serverConn) writeFrameFromHandler(wr FrameWriteRequest) error { } } +// writeUnknownFrameFromHandler writes unknown frame response from a handler on +// the given stream. +func (sc *serverConn) writeUnknownFrameFromHandler(stream *stream, t FrameType, flags Flags, payload []byte) error { + ch := make(chan error, 1) + writeArg := &writeUnknownFrame{t, flags, stream.id, payload} + err := sc.writeFrameFromHandler(FrameWriteRequest{ + write: writeArg, + stream: stream, + done: ch, + }) + if err != nil { + return err + } + select { + case err = <-ch: + return err + case <-sc.doneServing: + return errClientDisconnected + case <-stream.cw: + // If both ch and stream.cw were ready (as might + // happen on the final Write after an http.Handler + // ends), prefer the write result. Otherwise this + // might just be us successfully closing the stream. + // The writeFrameAsync and serve goroutines guarantee + // that the ch send will happen before the stream.cw + // close. + select { + case err = <-ch: + return err + default: + return errStreamClosed + } + } +} + // writeFrame schedules a frame to write and sends it if there's nothing // already being written. // @@ -1424,6 +1566,8 @@ func (sc *serverConn) processFrame(f Frame) error { // A client cannot push. Thus, servers MUST treat the receipt of a PUSH_PROMISE // frame as a connection error (Section 5.4.1) of type PROTOCOL_ERROR. return ConnectionError(ErrCodeProtocol) + case *UnknownFrame: + return sc.processUnknownFrame(f) default: sc.vlogf("http2: server ignoring frame: %v", f.Header()) return nil @@ -1729,6 +1873,38 @@ func (sc *serverConn) processGoAway(f *GoAwayFrame) error { return nil } +func (sc *serverConn) processUnknownFrame(f *UnknownFrame) error { + sc.serveG.check() + if sc.inGoAway && sc.goAwayCode != ErrCodeNo { + return nil + } + + if !sc.srv.EnableUnknownFrames { + sc.vlogf("http2: server ignoring frame: %v", f.Header()) + return nil + } + + // The stream must be in open or half closed state in order to receive unknown frame. + id := f.Header().StreamID + _, st := sc.state(id) + if st == nil || st.gotTrailerHeader || st.resetQueued { + // This includes sending a RST_STREAM if the stream is + // in stateHalfClosedLocal (which currently means that + // the http.Handler returned, so it's done reading & + // done writing). + if st != nil && st.resetQueued { + // Already have a stream error in flight. Don't send another. + return nil + } + return streamError(id, ErrCodeStreamClosed) + } + err := st.unknownFramesReceived.addUnknownFrame(f) + if err != nil { + return err + } + return nil +} + // isPushed reports whether the stream is server-initiated. func (st *stream) isPushed() bool { return st.id%2 == 0 @@ -1748,6 +1924,9 @@ func (st *stream) endStream() { st.body.CloseWithError(io.EOF) } st.state = stateHalfClosedRemote + if sc.srv.EnableUnknownFrames { + st.unknownFramesReceived.noMore = true + } } // copyTrailersToHandlerRequest is run in the Handler's goroutine in @@ -1878,6 +2057,10 @@ func (sc *serverConn) processHeaders(f *MetaHeadersFrame) error { sc.conn.SetReadDeadline(time.Time{}) } + if sc.srv.EnableUnknownFrames { + st.unknownFramesReceived = &unknownFramesReceived{unknownFrames: make([]*UnknownFrame, 0, 10), noMore: false} + req = req.WithContext(context.WithValue(req.Context(), UnknownFrameReaderKey, st.unknownFramesReceived)) + } go sc.runHandler(rw, req, handler) return nil } @@ -2312,6 +2495,20 @@ func (b *requestBody) Read(p []byte) (n int, err error) { return } +// WriteUnknownFrame writes unknown frames. +// When EnableUnknownFrames is true, http.ResponseWriter passed to the handler function can be cast to +// WriteUnknownFrame and write unknown frames. For example: +// func ServeHTTP(rw http.RequestWriter, r *http.Request) { +// ... +// ufw, ok := rw.(WriteUnknownFrame) +// ufw.WriteUnknownFrame(frameType, frameFlags, unknownFrameBody) +// ... +// } +type WriteUnknownFrame interface { + // Call WriteUnknownFrame to write an unknown frame. + WriteUnknownFrame(t FrameType, flags Flags, payload []byte) error +} + // responseWriter is the http.ResponseWriter implementation. It's // intentionally small (1 pointer wide) to minimize garbage. The // responseWriterState pointer inside is zeroed at the end of a @@ -2812,6 +3009,15 @@ func (w *responseWriter) Push(target string, opts *http.PushOptions) error { } } +// WriteUnknownFrame writes unknown frames. +func (w *responseWriter) WriteUnknownFrame(t FrameType, flags Flags, payload []byte) error { + if w.rws.stream.sc.srv.EnableUnknownFrames == false { + return errors.New("the server does not enable unknown frames") + } + + return w.rws.stream.sc.writeUnknownFrameFromHandler(w.rws.stream, t, flags, payload) +} + type startPushRequest struct { parent *stream method string diff --git a/http2/server_test.go b/http2/server_test.go index 23c118828..a8af752e9 100644 --- a/http2/server_test.go +++ b/http2/server_test.go @@ -614,6 +614,18 @@ func (st *serverTester) wantPushPromise() *PushPromiseFrame { return ppf } +func (st *serverTester) wantUnknownFrame() *UnknownFrame { + f, err := st.readFrame() + if err != nil { + st.t.Fatalf("Error while expecting a metadata frame: %v", err) + } + hf, ok := f.(*UnknownFrame) + if !ok { + st.t.Fatalf("got a %T; want *UnknownFrame", f) + } + return hf +} + func TestServer(t *testing.T) { gotReq := make(chan bool, 1) st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) { @@ -4098,3 +4110,155 @@ func TestContentEncodingNoSniffing(t *testing.T) { }) } } + +func TestServerWithReceivedUnknownFrames(t *testing.T) { + const testBody = "" + const frameType = 0xff + const frameFlags = 1 + const streamID = 1 + unknownFrameBody := make([]byte, defaultMaxReadFrameSize) + writeReq := func(st *serverTester) { + st.writeHeaders(HeadersFrameParam{ + StreamID: 1, // clients send odd numbers + BlockFragment: st.encodeHeader("a", "b"), + EndStream: false, + EndHeaders: true, + }) + st.fr.WriteRawFrame(frameType, frameFlags, streamID, unknownFrameBody) + st.writeData(1, true, []byte(testBody)) + } + checkReq := func(r *http.Request) { + slurp, err := ioutil.ReadAll(r.Body) + if string(slurp) != testBody { + t.Errorf("read body %q; want %q", slurp, testBody) + } + if err != nil { + t.Fatalf("Body slurp: %v", err) + } + } + + gotReq := make(chan bool, 1) + + st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) { + if r.Body == nil { + t.Fatal("nil Body") + } + checkReq(r) + + ufr := r.Context().Value(UnknownFrameReaderKey) + reader, ok := ufr.(UnknownFrameReader) + if !ok { + t.Errorf("can't get reader with error: %v", ok) + } + f, err := reader.ReadUnknownFrame(context.Background()) + if err != nil { + t.Errorf("reader.ReadUnknownFrame() fails") + } + if f.Type != frameType { + t.Errorf("frame type %v; want %v", f.Type, frameType) + } + if f.Flags != frameFlags { + t.Errorf("frame flags %v; want %v", f.Flags, frameFlags) + } + if f.StreamID != streamID { + t.Errorf("frame streamm id %v; want %v", f.StreamID, streamID) + } + if string(f.p) != string(unknownFrameBody) { + t.Errorf("frame body %v; want %v", f.p, unknownFrameBody) + } + + gotReq <- true + }, func(s *Server) { + s.EnableUnknownFrames = true + }) + defer st.Close() + + st.greet() + writeReq(st) + + select { + case <-gotReq: + case <-time.After(5 * time.Second): + t.Error("timeout waiting for request") + } +} + +// The same as testServerResponse but with unknown frames. +func testServerResponseWithUnknownFrame(t testing.TB, handler func(w http.ResponseWriter, r *http.Request), client func(*serverTester)) { + st := newServerTester(t, handler, func(s *Server) { + s.EnableUnknownFrames = true + }) + defer st.Close() + + donec := make(chan bool) + go func() { + defer close(donec) + st.greet() + client(st) + }() + + select { + case <-donec: + case <-time.After(5 * time.Second): + t.Fatal("timeout in client") + } +} + +func TestServerWritesUnknownFrameInResponse(t *testing.T) { + const msg = "this is HTML." + const frameType = 0xff + const frameFlags = 1 + const streamID = 1 + unknownFrameBody := make([]byte, defaultMaxReadFrameSize) + testServerResponseWithUnknownFrame(t, func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Foo", "Bar") + w.(http.Flusher).Flush() + ufw, ok := w.(WriteUnknownFrame) + if !ok { + t.Errorf("can't cast w to WriteUnknownFrame") + } + ufw.WriteUnknownFrame(frameType, frameFlags, unknownFrameBody) + w, ok = ufw.(http.ResponseWriter) + if !ok { + t.Errorf("can't cast ufw to http.ResponseWriter") + } + io.WriteString(w, msg) + }, func(st *serverTester) { + getSlash(st) + hf := st.wantHeaders() + if hf.StreamEnded() { + t.Fatal("response HEADERS had END_STREAM") + } + if !hf.HeadersEnded() { + t.Fatal("response HEADERS didn't have END_HEADERS") + } + goth := st.decodeHeader(hf.HeaderBlockFragment()) + wanth := [][2]string{ + {":status", "200"}, + {"foo", "Bar"}, + } + if !reflect.DeepEqual(goth, wanth) { + t.Errorf("Header mismatch.\n got: %v\nwant: %v", goth, wanth) + } + uf := st.wantUnknownFrame() + if uf.Type != frameType { + t.Errorf("frame type %v; want %v", uf.Type, frameType) + } + if uf.Flags != frameFlags { + t.Errorf("frame flags %v; want %v", uf.Flags, frameFlags) + } + if uf.StreamID != streamID { + t.Errorf("frame streamm id %v; want %v", uf.StreamID, streamID) + } + if string(uf.p) != string(unknownFrameBody) { + t.Errorf("frame body %v; want %v", uf.p, unknownFrameBody) + } + df := st.wantData() + if got := string(df.Data()); got != msg { + t.Errorf("got DATA %q; want %q", got, msg) + } + if !df.StreamEnded() { + t.Fatalf("expected DATA to have END_STREAM flag") + } + }) +} diff --git a/http2/write.go b/http2/write.go index 3849bc263..1aec9b9b6 100644 --- a/http2/write.go +++ b/http2/write.go @@ -328,6 +328,21 @@ func (wu writeWindowUpdate) writeFrame(ctx writeContext) error { return ctx.Framer().WriteWindowUpdate(wu.streamID, wu.n) } +type writeUnknownFrame struct { + t FrameType + flags Flags + streamID uint32 + p []byte +} + +func (w *writeUnknownFrame) writeFrame(ctx writeContext) error { + return ctx.Framer().WriteRawFrame(w.t, w.flags, w.streamID, w.p) +} + +func (w *writeUnknownFrame) staysWithinBuffer(max int) bool { + return frameHeaderLen+len(w.p) <= max +} + // encodeHeaders encodes an http.Header. If keys is not nil, then (k, h[k]) // is encoded only if k is in keys. func encodeHeaders(enc *hpack.Encoder, h http.Header, keys []string) { From e3b5b50ba0ffc416c6b412ef83b1fc98a2f2713e Mon Sep 17 00:00:00 2001 From: soya3129 Date: Sun, 26 Jul 2020 16:02:53 +0800 Subject: [PATCH 2/2] Add unknown frame handling to RoundTrip(). Signed-off-by: soya3129 --- http2/transport.go | 68 +++++++++++++++++++++++++++++++- http2/transport_test.go | 87 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+), 1 deletion(-) diff --git a/http2/transport.go b/http2/transport.go index 76a92e0ca..28e98b1a3 100644 --- a/http2/transport.go +++ b/http2/transport.go @@ -296,6 +296,8 @@ type clientStream struct { trailer http.Header // accumulated trailers resTrailer *http.Header // client's Response.Trailer + + unknownFramesReceived *unknownFramesReceived } // awaitRequestCancel waits for the user to cancel a request or for the done @@ -430,6 +432,42 @@ func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { return t.RoundTripOpt(req, RoundTripOpt{}) } +// UnknownFrameInfo is a struct to store unknown frame fields. +type UnknownFrameInfo struct { + Type FrameType + Flags Flags + Body []byte +} + +type bodyWithUnknownFrame struct { + body io.ReadCloser + unknownFrames []*UnknownFrameInfo +} + +func (b bodyWithUnknownFrame) Read(p []byte) (n int, err error) { + return b.body.Read(p) +} + +func (b bodyWithUnknownFrame) Close() error { + return b.body.Close() +} + +// AddUnknownFrame adds an unknown frame to http.Request. This function can be +// called multiple times, in case multiple unknown frames need to be added. +// This function must be called before RoundTrip(). +func AddUnknownFrame(req *http.Request, uf *UnknownFrameInfo) (*http.Request) { + if (req.Body == nil) { + req.Body = bodyWithUnknownFrame{body: http.NoBody, unknownFrames: make([]*UnknownFrameInfo, 0, 10)} + } + rbwu, ok := req.Body.(bodyWithUnknownFrame) + if !ok { + rbwu = bodyWithUnknownFrame{body: req.Body, unknownFrames: make([]*UnknownFrameInfo, 0, 10)} + req.Body = rbwu + } + rbwu.unknownFrames = append(rbwu.unknownFrames, uf) + return req +} + // authorityAddr returns a given authority (a host/IP, or host:port / ip:port) // and returns a host:port. The port 443 is added if needed. func authorityAddr(scheme string, authority string) (addr string) { @@ -1082,7 +1120,18 @@ func (cc *ClientConn) roundTrip(req *http.Request) (res *http.Response, gotErrAf cc.wmu.Lock() endStream := !hasBody && !hasTrailers - werr := cc.writeHeaders(cs.ID, endStream, int(cc.maxFrameSize), hdrs) + rbwu, ok := req.Body.(bodyWithUnknownFrame) + sendEmptyData := endStream && ok + werr := cc.writeHeaders(cs.ID, endStream && !sendEmptyData, int(cc.maxFrameSize), hdrs) + if ok { + for _, f := range rbwu.unknownFrames { + werr = cc.fr.WriteRawFrame(f.Type, f.Flags, cs.ID, f.Body) + cc.bw.Flush() + } + } + if sendEmptyData { + werr = cc.fr.WriteData(cs.ID, true, nil) + } cc.wmu.Unlock() traceWroteHeaders(cs.trace) cc.mu.Unlock() @@ -1845,6 +1894,8 @@ func (rl *clientConnReadLoop) run() error { err = rl.processWindowUpdate(f) case *PingFrame: err = rl.processPing(f) + case *UnknownFrame: + err = rl.processUnknownFrame(f) default: cc.logf("Transport: unhandled response frame type %T", f) } @@ -2073,6 +2124,10 @@ type transportResponseBody struct { cs *clientStream } +func (b transportResponseBody) ReadUnknownFrame(ctx context.Context) (*UnknownFrame, error) { + return b.cs.unknownFramesReceived.ReadUnknownFrame(ctx) +} + func (b transportResponseBody) Read(p []byte) (n int, err error) { cs := b.cs cc := cs.cc @@ -2478,6 +2533,17 @@ func (rl *clientConnReadLoop) processPushPromise(f *PushPromiseFrame) error { return ConnectionError(ErrCodeProtocol) } +func (rl *clientConnReadLoop) processUnknownFrame(f *UnknownFrame) error { + cs := rl.cc.streamByID(f.StreamID, false) + if cs == nil { + return nil + } + if cs.unknownFramesReceived == nil { + cs.unknownFramesReceived = &unknownFramesReceived{unknownFrames: make([]*UnknownFrame, 0, 10), noMore: false} + } + return cs.unknownFramesReceived.addUnknownFrame(f) +} + func (cc *ClientConn) writeStreamReset(streamID uint32, code ErrCode, err error) { // TODO: map err to more interesting error codes, once the // HTTP community comes up with some. But currently for diff --git a/http2/transport_test.go b/http2/transport_test.go index ec7493c76..16d525116 100644 --- a/http2/transport_test.go +++ b/http2/transport_test.go @@ -4732,3 +4732,90 @@ func TestClientConnTooIdle(t *testing.T) { } } } + +func startH2ServerWithUnknownFrame(t *testing.T, frameType FrameType, frameFlags Flags, unknownFrameBody []byte) net.Listener { + h2Server := &Server{} + h2Server.EnableUnknownFrames = true + l := newLocalListener(t) + go func() { + conn, err := l.Accept() + if err != nil { + t.Error(err) + return + } + h2Server.ServeConn(&fakeTLSConn{conn}, &ServeConnOpts{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Foo", "Bar") + ufw, ok := w.(WriteUnknownFrame) + if !ok { + t.Errorf("can't cast w to WriteUnknownFrame") + } + ufw.WriteUnknownFrame(frameType, frameFlags, unknownFrameBody) + fmt.Fprintf(w, "Hello, %v, http: %v", r.URL.Path, r.TLS == nil) + })}) + }() + return l +} + +func TestTransportH2WithUnknownFrame(t *testing.T) { + const frameType = 0xff + const frameFlags = 1 + unknownFrameBody := make([]byte, defaultMaxReadFrameSize) + uf := &UnknownFrameInfo{frameType, frameFlags, unknownFrameBody} + + l := startH2ServerWithUnknownFrame(t, frameType, frameFlags, unknownFrameBody) + defer l.Close() + req, err := http.NewRequest("GET", "http://"+l.Addr().String()+"/foobar", nil) + req = AddUnknownFrame(req, uf) + if err != nil { + t.Fatal(err) + } + var gotConnCnt int32 + trace := &httptrace.ClientTrace{ + GotConn: func(connInfo httptrace.GotConnInfo) { + if !connInfo.Reused { + atomic.AddInt32(&gotConnCnt, 1) + } + }, + } + req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace)) + tr := &Transport{ + AllowHTTP: true, + DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) { + return net.Dial(network, addr) + }, + } + + res, err := tr.RoundTrip(req) + rwuf, ok := res.Body.(UnknownFrameReader) + if !ok { + t.Errorf("can't cast res to UnknownFrameReader") + } + f, err := rwuf.ReadUnknownFrame(context.Background()) + if err != nil { + t.Errorf("ReadUnknownFrame failed: %v", err) + } + if got, want := f.FrameHeader.Type, FrameType(frameType); got != want { + t.Errorf("unknown frame type got %v, want %v", got, want) + } + if got, want := f.FrameHeader.Flags, Flags(frameFlags); got != want { + t.Errorf("unknown frame flags got %v, want %v", got, want) + } + got, want := f.p, unknownFrameBody + if len(got) != len(want) { + t.Errorf("unknown frame payload got %v, want %v", got, want) + } + + if res.ProtoMajor != 2 { + t.Fatal("proto not h2c") + } + body, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Fatal(err) + } + if got, want := string(body), "Hello, /foobar, http: true"; got != want { + t.Fatalf("response got %v, want %v", got, want) + } + if got, want := gotConnCnt, int32(1); got != want { + t.Errorf("Too many got connections: %d", gotConnCnt) + } +}