Repository: quic-go/quic-go
Pull request #5153 (2025-05-24 03:30:03)

http3: convert RequestStream from an interface to a struct
Part of #5152.


Patch:
diff --git a/http3/client.go b/http3/client.go
index 3691d4d7..7c2c4192 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 a2c8ba61..8adfc9c5 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 e030ab97..93d398ae 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/transport.go b/http3/transport.go
index f1824e59..6f876063 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)
 }
 


Test patch:
diff --git a/http3/mock_clientconn_test.go b/http3/mock_clientconn_test.go
index deb40ede..a2403dfd 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/integrationtests/self/http_datagram_test.go b/integrationtests/self/http_datagram_test.go
index a4fef85f..6c8e6ff9 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)