Skip to content

Commit 93c1957

Browse files
neildgopherbot
authored andcommitted
internal/http3: add Transport and ClientConn
Add the first rudiments of an HTTP/3 client. The client currently opens a QUIC connection and creates a control stream on it, and nothing else. Add surrounding test infrastructure for examining the client's behavior. Tests use the experimental testing/synctest package and will only run when the Go version is at least Go 1.24 and GOEXPERIMENT=synctest is set. For golang/go#70914 Change-Id: I19803187a8e62c461f60d7a1d44c2a408377e342 Reviewed-on: https://go-review.googlesource.com/c/net/+/642516 Reviewed-by: Jonathan Amsterdam <[email protected]> Auto-Submit: Damien Neil <[email protected]> Reviewed-by: Brad Fitzpatrick <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 45432b5 commit 93c1957

File tree

6 files changed

+896
-0
lines changed

6 files changed

+896
-0
lines changed

Diff for: internal/http3/http3_test.go

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright 2024 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build go1.24 && goexperiment.synctest
6+
7+
package http3
8+
9+
import (
10+
"os"
11+
"slices"
12+
"testing"
13+
"testing/synctest"
14+
)
15+
16+
func init() {
17+
// testing/synctest requires asynctimerchan=0 (the default as of Go 1.23),
18+
// but the x/net go.mod is currently selecting go1.18.
19+
//
20+
// Set asynctimerchan=0 explicitly.
21+
//
22+
// TODO: Remove this when the x/net go.mod Go version is >= go1.23.
23+
os.Setenv("GODEBUG", os.Getenv("GODEBUG")+",asynctimerchan=0")
24+
}
25+
26+
// runSynctest runs f in a synctest.Run bubble.
27+
// It arranges for t.Cleanup functions to run within the bubble.
28+
func runSynctest(t *testing.T, f func(t testing.TB)) {
29+
synctest.Run(func() {
30+
ct := &cleanupT{T: t}
31+
defer ct.done()
32+
f(ct)
33+
})
34+
}
35+
36+
// runSynctestSubtest runs f in a subtest in a synctest.Run bubble.
37+
func runSynctestSubtest(t *testing.T, name string, f func(t testing.TB)) {
38+
t.Run(name, func(t *testing.T) {
39+
runSynctest(t, f)
40+
})
41+
}
42+
43+
// cleanupT wraps a testing.T and adds its own Cleanup method.
44+
// Used to execute cleanup functions within a synctest bubble.
45+
type cleanupT struct {
46+
*testing.T
47+
cleanups []func()
48+
}
49+
50+
// Cleanup replaces T.Cleanup.
51+
func (t *cleanupT) Cleanup(f func()) {
52+
t.cleanups = append(t.cleanups, f)
53+
}
54+
55+
func (t *cleanupT) done() {
56+
for _, f := range slices.Backward(t.cleanups) {
57+
f()
58+
}
59+
}

Diff for: internal/http3/settings.go

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build go1.24
6+
7+
package http3
8+
9+
import (
10+
"golang.org/x/net/internal/quic/quicwire"
11+
"golang.org/x/net/quic"
12+
)
13+
14+
const (
15+
// https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.4.1
16+
settingsMaxFieldSectionSize = 0x06
17+
18+
// https://www.rfc-editor.org/rfc/rfc9204.html#section-5
19+
settingsQPACKMaxTableCapacity = 0x01
20+
settingsQPACKBlockedStreams = 0x07
21+
)
22+
23+
// writeSettings writes a complete SETTINGS frame.
24+
// Its parameter is a list of alternating setting types and values.
25+
func (st *stream) writeSettings(settings ...int64) {
26+
var size int64
27+
for _, s := range settings {
28+
// Settings values that don't fit in a QUIC varint ([0,2^62)) will panic here.
29+
size += int64(quicwire.SizeVarint(uint64(s)))
30+
}
31+
st.writeVarint(int64(frameTypeSettings))
32+
st.writeVarint(size)
33+
for _, s := range settings {
34+
st.writeVarint(s)
35+
}
36+
}
37+
38+
// readSettings reads a complete SETTINGS frame, including the frame header.
39+
func (st *stream) readSettings(f func(settingType, value int64) error) error {
40+
frameType, err := st.readFrameHeader()
41+
if err != nil || frameType != frameTypeSettings {
42+
return &quic.ApplicationError{
43+
Code: uint64(errH3MissingSettings),
44+
Reason: "settings not sent on control stream",
45+
}
46+
}
47+
for st.lim > 0 {
48+
settingsType, err := st.readVarint()
49+
if err != nil {
50+
return err
51+
}
52+
settingsValue, err := st.readVarint()
53+
if err != nil {
54+
return err
55+
}
56+
57+
// Use of HTTP/2 settings where there is no corresponding HTTP/3 setting
58+
// is an error.
59+
// https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.4.1-5
60+
switch settingsType {
61+
case 0x02, 0x03, 0x04, 0x05:
62+
return &quic.ApplicationError{
63+
Code: uint64(errH3SettingsError),
64+
Reason: "use of reserved setting",
65+
}
66+
}
67+
68+
if err := f(settingsType, settingsValue); err != nil {
69+
return err
70+
}
71+
}
72+
return st.endFrame()
73+
}

Diff for: internal/http3/stream.go

+66
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
package http3
88

99
import (
10+
"context"
1011
"io"
1112

1213
"golang.org/x/net/quic"
@@ -24,6 +25,36 @@ type stream struct {
2425
lim int64
2526
}
2627

28+
// newConnStream creates a new stream on a connection.
29+
// It writes the stream header for unidirectional streams.
30+
//
31+
// The stream returned by newStream is not flushed,
32+
// and will not be sent to the peer until the caller calls
33+
// Flush or writes enough data to the stream.
34+
func newConnStream(ctx context.Context, qconn *quic.Conn, stype streamType) (*stream, error) {
35+
var qs *quic.Stream
36+
var err error
37+
if stype == streamTypeRequest {
38+
// Request streams are bidirectional.
39+
qs, err = qconn.NewStream(ctx)
40+
} else {
41+
// All other streams are unidirectional.
42+
qs, err = qconn.NewSendOnlyStream(ctx)
43+
}
44+
if err != nil {
45+
return nil, err
46+
}
47+
st := &stream{
48+
stream: qs,
49+
lim: -1, // no limit
50+
}
51+
if stype != streamTypeRequest {
52+
// Unidirectional stream header.
53+
st.writeVarint(int64(stype))
54+
}
55+
return st, err
56+
}
57+
2758
func newStream(qs *quic.Stream) *stream {
2859
return &stream{
2960
stream: qs,
@@ -106,6 +137,41 @@ func (st *stream) Read(b []byte) (int, error) {
106137
return n, nil
107138
}
108139

140+
// discardUnknownFrame discards an unknown frame.
141+
//
142+
// HTTP/3 requires that unknown frames be ignored on all streams.
143+
// However, a known frame appearing in an unexpected place is a fatal error,
144+
// so this returns an error if the frame is one we know.
145+
func (st *stream) discardUnknownFrame(ftype frameType) error {
146+
switch ftype {
147+
case frameTypeData,
148+
frameTypeHeaders,
149+
frameTypeCancelPush,
150+
frameTypeSettings,
151+
frameTypePushPromise,
152+
frameTypeGoaway,
153+
frameTypeMaxPushID:
154+
return &quic.ApplicationError{
155+
Code: uint64(errH3FrameUnexpected),
156+
Reason: "unexpected " + ftype.String() + " frame",
157+
}
158+
}
159+
return st.discardFrame()
160+
}
161+
162+
// discardFrame discards any remaining data in the current frame and resets the read limit.
163+
func (st *stream) discardFrame() error {
164+
// TODO: Consider adding a *quic.Stream method to discard some amount of data.
165+
for range st.lim {
166+
_, err := st.stream.ReadByte()
167+
if err != nil {
168+
return errH3FrameError
169+
}
170+
}
171+
st.lim = -1
172+
return nil
173+
}
174+
109175
// Write writes to the stream.
110176
func (st *stream) Write(b []byte) (int, error) { return st.stream.Write(b) }
111177

Diff for: internal/http3/stream_test.go

+20
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,26 @@ func TestStreamReadByte(t *testing.T) {
243243
}
244244
}
245245

246+
func TestStreamDiscardFrame(t *testing.T) {
247+
const typ = 10
248+
data := []byte("hello")
249+
st1, st2 := newStreamPair(t)
250+
st1.writeVarint(typ) // type
251+
st1.writeVarint(int64(len(data))) // size
252+
st1.Write(data) // data
253+
st1.stream.CloseWrite()
254+
255+
if got, err := st2.readFrameHeader(); err != nil || got != typ {
256+
t.Fatalf("st.readFrameHeader() = %v, %v; want %v, nil", got, err, typ)
257+
}
258+
if err := st2.discardFrame(); err != nil {
259+
t.Fatalf("st.discardFrame() = %v", err)
260+
}
261+
if b, err := io.ReadAll(st2); err != nil || len(b) > 0 {
262+
t.Fatalf("after discarding frame, read %x, %v; want EOF", b, err)
263+
}
264+
}
265+
246266
func newStreamPair(t *testing.T) (s1, s2 *stream) {
247267
t.Helper()
248268
q1, q2 := newQUICStreamPair(t)

0 commit comments

Comments
 (0)