From 2675d0845f881c93131a5da183a5d9c1c66155ce Mon Sep 17 00:00:00 2001 From: Marten Seemann Date: Thu, 29 May 2025 11:08:39 +0800 Subject: [PATCH] http3: convert RequestStream from an interface to a struct (#5153) --- http3/client.go | 4 +- http3/conn.go | 2 +- http3/http_stream.go | 43 ++++++++------------- http3/mock_clientconn_test.go | 10 ++--- http3/transport.go | 2 +- integrationtests/self/http_datagram_test.go | 2 +- 6 files changed, 27 insertions(+), 36 deletions(-) diff --git a/http3/client.go b/http3/client.go index 3691d4d79..7c2c41928 100644 --- a/http3/client.go +++ b/http3/client.go @@ -127,7 +127,7 @@ func newClientConn( } // OpenRequestStream opens a new request stream on the HTTP/3 connection. -func (c *ClientConn) OpenRequestStream(ctx context.Context) (RequestStream, error) { +func (c *ClientConn) OpenRequestStream(ctx context.Context) (*RequestStream, error) { return c.openRequestStream(ctx, c.requestWriter, nil, c.disableCompression, c.maxResponseHeaderBytes) } @@ -300,7 +300,7 @@ func (c *ClientConn) sendRequestBody(str Stream, body io.ReadCloser, contentLeng return err } -func (c *ClientConn) doRequest(req *http.Request, str *requestStream) (*http.Response, error) { +func (c *ClientConn) doRequest(req *http.Request, str *RequestStream) (*http.Response, error) { trace := httptrace.ContextClientTrace(req.Context()) if err := str.SendRequestHeader(req); err != nil { traceWroteRequest(trace, err) diff --git a/http3/conn.go b/http3/conn.go index a2c8ba61e..8adfc9c53 100644 --- a/http3/conn.go +++ b/http3/conn.go @@ -119,7 +119,7 @@ func (c *connection) openRequestStream( reqDone chan<- struct{}, disableCompression bool, maxHeaderBytes uint64, -) (*requestStream, error) { +) (*RequestStream, error) { if c.perspective == protocol.PerspectiveClient { c.streamMx.Lock() maxStreamID := c.maxStreamID diff --git a/http3/http_stream.go b/http3/http_stream.go index e030ab970..93d398ae7 100644 --- a/http3/http_stream.go +++ b/http3/http_stream.go @@ -23,23 +23,7 @@ type Stream interface { ReceiveDatagram(context.Context) ([]byte, error) } -// A RequestStream is an HTTP/3 request stream. // When writing to and reading from the stream, data is framed in HTTP/3 DATA frames. -type RequestStream interface { - Stream - - // SendRequestHeader sends the HTTP request. - // It is invalid to call it more than once. - // It is invalid to call it after Write has been called. - SendRequestHeader(req *http.Request) error - - // ReadResponse reads the HTTP response from the stream. - // It is invalid to call it more than once. - // It doesn't set Response.Request and Response.TLS. - // It is invalid to call it after Read has been called. - ReadResponse() (*http.Response, error) -} - type stream struct { quic.Stream conn *connection @@ -135,9 +119,11 @@ func (s *stream) StreamID() protocol.StreamID { return s.Stream.StreamID() } -// The stream conforms to the quic.Stream interface, but instead of writing to and reading directly -// from the QUIC stream, it writes to and reads from the HTTP stream. -type requestStream struct { +// A RequestStream is a low-level abstraction representing an HTTP/3 request stream. +// It decouples sending of the HTTP request from reading the HTTP response, allowing +// the application to optimistically use the stream (and, for example, send datagrams) +// before receiving the response. +type RequestStream struct { *stream responseBody io.ReadCloser // set by ReadResponse @@ -156,8 +142,6 @@ type requestStream struct { firstByte bool } -var _ RequestStream = &requestStream{} - func newRequestStream( str *stream, requestWriter *requestWriter, @@ -167,8 +151,8 @@ func newRequestStream( maxHeaderBytes uint64, rsp *http.Response, trace *httptrace.ClientTrace, -) *requestStream { - return &requestStream{ +) *RequestStream { + return &RequestStream{ stream: str, requestWriter: requestWriter, reqDone: reqDone, @@ -180,14 +164,17 @@ func newRequestStream( } } -func (s *requestStream) Read(b []byte) (int, error) { +func (s *RequestStream) Read(b []byte) (int, error) { if s.responseBody == nil { return 0, errors.New("http3: invalid use of RequestStream.Read: need to call ReadResponse first") } return s.responseBody.Read(b) } -func (s *requestStream) SendRequestHeader(req *http.Request) error { +// SendRequestHeader sends the HTTP request. +// It is invalid to call it more than once. +// It is invalid to call it after Write has been called. +func (s *RequestStream) SendRequestHeader(req *http.Request) error { if s.sentRequest { return errors.New("http3: invalid duplicate use of SendRequestHeader") } @@ -200,7 +187,11 @@ func (s *requestStream) SendRequestHeader(req *http.Request) error { return s.requestWriter.WriteRequestHeader(s.Stream, req, s.requestedGzip) } -func (s *requestStream) ReadResponse() (*http.Response, error) { +// ReadResponse reads the HTTP response from the stream. +// It is invalid to call it more than once. +// It doesn't set Response.Request and Response.TLS. +// It is invalid to call it after Read has been called. +func (s *RequestStream) ReadResponse() (*http.Response, error) { fp := &frameParser{ conn: s.conn, r: &tracingReader{ diff --git a/http3/mock_clientconn_test.go b/http3/mock_clientconn_test.go index deb40ede2..a2403dfd6 100644 --- a/http3/mock_clientconn_test.go +++ b/http3/mock_clientconn_test.go @@ -42,10 +42,10 @@ func (m *MockClientConn) EXPECT() *MockClientConnMockRecorder { } // OpenRequestStream mocks base method. -func (m *MockClientConn) OpenRequestStream(arg0 context.Context) (RequestStream, error) { +func (m *MockClientConn) OpenRequestStream(arg0 context.Context) (*RequestStream, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "OpenRequestStream", arg0) - ret0, _ := ret[0].(RequestStream) + ret0, _ := ret[0].(*RequestStream) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -63,19 +63,19 @@ type MockClientConnOpenRequestStreamCall struct { } // Return rewrite *gomock.Call.Return -func (c *MockClientConnOpenRequestStreamCall) Return(arg0 RequestStream, arg1 error) *MockClientConnOpenRequestStreamCall { +func (c *MockClientConnOpenRequestStreamCall) Return(arg0 *RequestStream, arg1 error) *MockClientConnOpenRequestStreamCall { c.Call = c.Call.Return(arg0, arg1) return c } // Do rewrite *gomock.Call.Do -func (c *MockClientConnOpenRequestStreamCall) Do(f func(context.Context) (RequestStream, error)) *MockClientConnOpenRequestStreamCall { +func (c *MockClientConnOpenRequestStreamCall) Do(f func(context.Context) (*RequestStream, error)) *MockClientConnOpenRequestStreamCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockClientConnOpenRequestStreamCall) DoAndReturn(f func(context.Context) (RequestStream, error)) *MockClientConnOpenRequestStreamCall { +func (c *MockClientConnOpenRequestStreamCall) DoAndReturn(f func(context.Context) (*RequestStream, error)) *MockClientConnOpenRequestStreamCall { c.Call = c.Call.DoAndReturn(f) return c } diff --git a/http3/transport.go b/http3/transport.go index f1824e594..6f8760635 100644 --- a/http3/transport.go +++ b/http3/transport.go @@ -39,7 +39,7 @@ type RoundTripOpt struct { } type clientConn interface { - OpenRequestStream(context.Context) (RequestStream, error) + OpenRequestStream(context.Context) (*RequestStream, error) RoundTrip(*http.Request) (*http.Response, error) } diff --git a/integrationtests/self/http_datagram_test.go b/integrationtests/self/http_datagram_test.go index a4fef85f2..6c8e6ff94 100644 --- a/integrationtests/self/http_datagram_test.go +++ b/integrationtests/self/http_datagram_test.go @@ -90,7 +90,7 @@ func TestHTTPSettings(t *testing.T) { }) } -func dialAndOpenHTTPDatagramStream(t *testing.T, addr string) http3.RequestStream { +func dialAndOpenHTTPDatagramStream(t *testing.T, addr string) *http3.RequestStream { t.Helper() u, err := url.Parse(addr)