internal/http3: make read-data tests usable for server handlers · golang/net@5f45c77 (original) (raw)

``

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 && goexperiment.synctest

`

``

6

+

``

7

`+

package http3

`

``

8

+

``

9

`+

import (

`

``

10

`+

"bytes"

`

``

11

`+

"fmt"

`

``

12

`+

"io"

`

``

13

`+

"net/http"

`

``

14

`+

"testing"

`

``

15

`+

)

`

``

16

+

``

17

`+

// TestReadData tests servers reading request bodies, and clients reading response bodies.

`

``

18

`+

func TestReadData(t *testing.T) {

`

``

19

`+

// These tests consist of a series of steps,

`

``

20

`+

// where each step is either something arriving on the stream

`

``

21

`+

// or the client/server reading from the body.

`

``

22

`+

type (

`

``

23

`+

// HEADERS frame arrives (headers).

`

``

24

`+

receiveHeaders struct {

`

``

25

`+

contentLength int64 // -1 for no content-length

`

``

26

`+

}

`

``

27

`+

// DATA frame header arrives.

`

``

28

`+

receiveDataHeader struct {

`

``

29

`+

size int64

`

``

30

`+

}

`

``

31

`+

// DATA frame content arrives.

`

``

32

`+

receiveData struct {

`

``

33

`+

size int64

`

``

34

`+

}

`

``

35

`+

// HEADERS frame arrives (trailers).

`

``

36

`+

receiveTrailers struct{}

`

``

37

`+

// Some other frame arrives.

`

``

38

`+

receiveFrame struct {

`

``

39

`+

ftype frameType

`

``

40

`+

data []byte

`

``

41

`+

}

`

``

42

`+

// Stream closed, ending the body.

`

``

43

`+

receiveEOF struct{}

`

``

44

`+

// Server reads from Request.Body, or client reads from Response.Body.

`

``

45

`+

wantBody struct {

`

``

46

`+

size int64

`

``

47

`+

eof bool

`

``

48

`+

}

`

``

49

`+

wantError struct{}

`

``

50

`+

)

`

``

51

`+

for _, test := range []struct {

`

``

52

`+

name string

`

``

53

`+

respHeader http.Header

`

``

54

`+

steps []any

`

``

55

`+

wantError bool

`

``

56

`+

}{{

`

``

57

`+

name: "no content length",

`

``

58

`+

steps: []any{

`

``

59

`+

receiveHeaders{contentLength: -1},

`

``

60

`+

receiveDataHeader{size: 10},

`

``

61

`+

receiveData{size: 10},

`

``

62

`+

receiveEOF{},

`

``

63

`+

wantBody{size: 10, eof: true},

`

``

64

`+

},

`

``

65

`+

}, {

`

``

66

`+

name: "valid content length",

`

``

67

`+

steps: []any{

`

``

68

`+

receiveHeaders{contentLength: 10},

`

``

69

`+

receiveDataHeader{size: 10},

`

``

70

`+

receiveData{size: 10},

`

``

71

`+

receiveEOF{},

`

``

72

`+

wantBody{size: 10, eof: true},

`

``

73

`+

},

`

``

74

`+

}, {

`

``

75

`+

name: "data frame exceeds content length",

`

``

76

`+

steps: []any{

`

``

77

`+

receiveHeaders{contentLength: 5},

`

``

78

`+

receiveDataHeader{size: 10},

`

``

79

`+

receiveData{size: 10},

`

``

80

`+

wantError{},

`

``

81

`+

},

`

``

82

`+

}, {

`

``

83

`+

name: "data frame after all content read",

`

``

84

`+

steps: []any{

`

``

85

`+

receiveHeaders{contentLength: 5},

`

``

86

`+

receiveDataHeader{size: 5},

`

``

87

`+

receiveData{size: 5},

`

``

88

`+

wantBody{size: 5},

`

``

89

`+

receiveDataHeader{size: 1},

`

``

90

`+

receiveData{size: 1},

`

``

91

`+

wantError{},

`

``

92

`+

},

`

``

93

`+

}, {

`

``

94

`+

name: "content length too long",

`

``

95

`+

steps: []any{

`

``

96

`+

receiveHeaders{contentLength: 10},

`

``

97

`+

receiveDataHeader{size: 5},

`

``

98

`+

receiveData{size: 5},

`

``

99

`+

receiveEOF{},

`

``

100

`+

wantBody{size: 5},

`

``

101

`+

wantError{},

`

``

102

`+

},

`

``

103

`+

}, {

`

``

104

`+

name: "stream ended by trailers",

`

``

105

`+

steps: []any{

`

``

106

`+

receiveHeaders{contentLength: -1},

`

``

107

`+

receiveDataHeader{size: 5},

`

``

108

`+

receiveData{size: 5},

`

``

109

`+

receiveTrailers{},

`

``

110

`+

wantBody{size: 5, eof: true},

`

``

111

`+

},

`

``

112

`+

}, {

`

``

113

`+

name: "trailers and content length too long",

`

``

114

`+

steps: []any{

`

``

115

`+

receiveHeaders{contentLength: 10},

`

``

116

`+

receiveDataHeader{size: 5},

`

``

117

`+

receiveData{size: 5},

`

``

118

`+

wantBody{size: 5},

`

``

119

`+

receiveTrailers{},

`

``

120

`+

wantError{},

`

``

121

`+

},

`

``

122

`+

}, {

`

``

123

`+

name: "unknown frame before headers",

`

``

124

`+

steps: []any{

`

``

125

`+

receiveFrame{

`

``

126

`+

ftype: 0x1f + 0x21, // reserved frame type

`

``

127

`+

data: []byte{1, 2, 3, 4},

`

``

128

`+

},

`

``

129

`+

receiveHeaders{contentLength: -1},

`

``

130

`+

receiveDataHeader{size: 10},

`

``

131

`+

receiveData{size: 10},

`

``

132

`+

wantBody{size: 10},

`

``

133

`+

},

`

``

134

`+

}, {

`

``

135

`+

name: "unknown frame after headers",

`

``

136

`+

steps: []any{

`

``

137

`+

receiveHeaders{contentLength: -1},

`

``

138

`+

receiveFrame{

`

``

139

`+

ftype: 0x1f + 0x21, // reserved frame type

`

``

140

`+

data: []byte{1, 2, 3, 4},

`

``

141

`+

},

`

``

142

`+

receiveDataHeader{size: 10},

`

``

143

`+

receiveData{size: 10},

`

``

144

`+

wantBody{size: 10},

`

``

145

`+

},

`

``

146

`+

}, {

`

``

147

`+

name: "invalid frame",

`

``

148

`+

steps: []any{

`

``

149

`+

receiveHeaders{contentLength: -1},

`

``

150

`+

receiveFrame{

`

``

151

`+

ftype: frameTypeSettings, // not a valid frame on this stream

`

``

152

`+

data: []byte{1, 2, 3, 4},

`

``

153

`+

},

`

``

154

`+

wantError{},

`

``

155

`+

},

`

``

156

`+

}, {

`

``

157

`+

name: "data frame consumed by several reads",

`

``

158

`+

steps: []any{

`

``

159

`+

receiveHeaders{contentLength: -1},

`

``

160

`+

receiveDataHeader{size: 16},

`

``

161

`+

receiveData{size: 16},

`

``

162

`+

wantBody{size: 2},

`

``

163

`+

wantBody{size: 4},

`

``

164

`+

wantBody{size: 8},

`

``

165

`+

wantBody{size: 2},

`

``

166

`+

},

`

``

167

`+

}, {

`

``

168

`+

name: "read multiple frames",

`

``

169

`+

steps: []any{

`

``

170

`+

receiveHeaders{contentLength: -1},

`

``

171

`+

receiveDataHeader{size: 2},

`

``

172

`+

receiveData{size: 2},

`

``

173

`+

receiveDataHeader{size: 4},

`

``

174

`+

receiveData{size: 4},

`

``

175

`+

receiveDataHeader{size: 8},

`

``

176

`+

receiveData{size: 8},

`

``

177

`+

wantBody{size: 2},

`

``

178

`+

wantBody{size: 4},

`

``

179

`+

wantBody{size: 8},

`

``

180

`+

},

`

``

181

`+

}} {

`

``

182

+

``

183

`+

runTest := func(t testing.TB, h http.Header, st *testQUICStream, body func() io.ReadCloser) {

`

``

184

`+

var (

`

``

185

`+

bytesSent int

`

``

186

`+

bytesReceived int

`

``

187

`+

)

`

``

188

`+

for _, step := range test.steps {

`

``

189

`+

switch step := step.(type) {

`

``

190

`+

case receiveHeaders:

`

``

191

`+

header := h.Clone()

`

``

192

`+

if step.contentLength != -1 {

`

``

193

`+

header["content-length"] = []string{

`

``

194

`+

fmt.Sprint(step.contentLength),

`

``

195

`+

}

`

``

196

`+

}

`

``

197

`+

st.writeHeaders(header)

`

``

198

`+

case receiveDataHeader:

`

``

199

`+

t.Logf("receive DATA frame header: size=%v", step.size)

`

``

200

`+

st.writeVarint(int64(frameTypeData))

`

``

201

`+

st.writeVarint(step.size)

`

``

202

`+

st.Flush()

`

``

203

`+

case receiveData:

`

``

204

`+

t.Logf("receive DATA frame content: size=%v", step.size)

`

``

205

`+

for range step.size {

`

``

206

`+

st.stream.stream.WriteByte(byte(bytesSent))

`

``

207

`+

bytesSent++

`

``

208

`+

}

`

``

209

`+

st.Flush()

`

``

210

`+

case receiveTrailers:

`

``

211

`+

st.writeHeaders(http.Header{

`

``

212

`+

"x-trailer": []string{"trailer"},

`

``

213

`+

})

`

``

214

`+

case receiveFrame:

`

``

215

`+

st.writeVarint(int64(step.ftype))

`

``

216

`+

st.writeVarint(int64(len(step.data)))

`

``

217

`+

st.Write(step.data)

`

``

218

`+

st.Flush()

`

``

219

`+

case receiveEOF:

`

``

220

`+

t.Logf("receive EOF on request stream")

`

``

221

`+

st.stream.stream.CloseWrite()

`

``

222

`+

case wantBody:

`

``

223

`+

t.Logf("read %v bytes from response body", step.size)

`

``

224

`+

want := make([]byte, step.size)

`

``

225

`+

for i := range want {

`

``

226

`+

want[i] = byte(bytesReceived)

`

``

227

`+

bytesReceived++

`

``

228

`+

}

`

``

229

`+

got := make([]byte, step.size)

`

``

230

`+

n, err := body().Read(got)

`

``

231

`+

got = got[:n]

`

``

232

`+

if !bytes.Equal(got, want) {

`

``

233

`+

t.Errorf("resp.Body.Read:")

`

``

234

`+

t.Errorf(" got: {%x}", got)

`

``

235

`+

t.Fatalf(" want: {%x}", want)

`

``

236

`+

}

`

``

237

`+

if err != nil {

`

``

238

`+

if step.eof && err == io.EOF {

`

``

239

`+

continue

`

``

240

`+

}

`

``

241

`+

t.Fatalf("resp.Body.Read: unexpected error %v", err)

`

``

242

`+

}

`

``

243

`+

if step.eof {

`

``

244

`+

if n, err := body().Read([]byte{0}); n != 0 || err != io.EOF {

`

``

245

`+

t.Fatalf("resp.Body.Read() = %v, %v; want io.EOF", n, err)

`

``

246

`+

}

`

``

247

`+

}

`

``

248

`+

case wantError:

`

``

249

`+

if n, err := body().Read([]byte{0}); n != 0 || err == nil || err == io.EOF {

`

``

250

`+

t.Fatalf("resp.Body.Read() = %v, %v; want error", n, err)

`

``

251

`+

}

`

``

252

`+

default:

`

``

253

`+

t.Fatalf("unknown test step %T", step)

`

``

254

`+

}

`

``

255

`+

}

`

``

256

+

``

257

`+

}

`

``

258

+

``

259

`+

runSynctestSubtest(t, test.name+"/client", func(t testing.TB) {

`

``

260

`+

tc := newTestClientConn(t)

`

``

261

`+

tc.greet()

`

``

262

+

``

263

`+

req, _ := http.NewRequest("GET", "https://example.tld/", nil)

`

``

264

`+

rt := tc.roundTrip(req)

`

``

265

`+

st := tc.wantStream(streamTypeRequest)

`

``

266

`+

st.wantHeaders(nil)

`

``

267

+

``

268

`+

header := http.Header{

`

``

269

`+

":status": []string{"200"},

`

``

270

`+

}

`

``

271

`+

runTest(t, header, st, func() io.ReadCloser {

`

``

272

`+

return rt.response().Body

`

``

273

`+

})

`

``

274

`+

})

`

``

275

`+

}

`

``

276

`+

}

`