github.com/MerlinKodo/quic-go@v0.39.2/integrationtests/self/http_test.go (about) 1 package self_test 2 3 import ( 4 "bufio" 5 "bytes" 6 "compress/gzip" 7 "context" 8 "errors" 9 "fmt" 10 "io" 11 "net" 12 "net/http" 13 "os" 14 "strconv" 15 "time" 16 17 "golang.org/x/sync/errgroup" 18 19 "github.com/MerlinKodo/quic-go" 20 "github.com/MerlinKodo/quic-go/http3" 21 22 . "github.com/onsi/ginkgo/v2" 23 . "github.com/onsi/gomega" 24 "github.com/onsi/gomega/gbytes" 25 ) 26 27 type neverEnding byte 28 29 func (b neverEnding) Read(p []byte) (n int, err error) { 30 for i := range p { 31 p[i] = byte(b) 32 } 33 return len(p), nil 34 } 35 36 const deadlineDelay = 250 * time.Millisecond 37 38 var _ = Describe("HTTP tests", func() { 39 var ( 40 mux *http.ServeMux 41 client *http.Client 42 rt *http3.RoundTripper 43 server *http3.Server 44 stoppedServing chan struct{} 45 port int 46 ) 47 48 BeforeEach(func() { 49 mux = http.NewServeMux() 50 mux.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) { 51 defer GinkgoRecover() 52 io.WriteString(w, "Hello, World!\n") // don't check the error here. Stream may be reset. 53 }) 54 55 mux.HandleFunc("/prdata", func(w http.ResponseWriter, r *http.Request) { 56 defer GinkgoRecover() 57 sl := r.URL.Query().Get("len") 58 if sl != "" { 59 var err error 60 l, err := strconv.Atoi(sl) 61 Expect(err).NotTo(HaveOccurred()) 62 w.Write(GeneratePRData(l)) // don't check the error here. Stream may be reset. 63 } else { 64 w.Write(PRData) // don't check the error here. Stream may be reset. 65 } 66 }) 67 68 mux.HandleFunc("/prdatalong", func(w http.ResponseWriter, r *http.Request) { 69 defer GinkgoRecover() 70 w.Write(PRDataLong) // don't check the error here. Stream may be reset. 71 }) 72 73 mux.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) { 74 defer GinkgoRecover() 75 body, err := io.ReadAll(r.Body) 76 Expect(err).NotTo(HaveOccurred()) 77 w.Write(body) // don't check the error here. Stream may be reset. 78 }) 79 80 mux.HandleFunc("/remoteAddr", func(w http.ResponseWriter, r *http.Request) { 81 defer GinkgoRecover() 82 w.Header().Set("X-RemoteAddr", r.RemoteAddr) 83 w.WriteHeader(http.StatusOK) 84 }) 85 86 server = &http3.Server{ 87 Handler: mux, 88 TLSConfig: getTLSConfig(), 89 QuicConfig: getQuicConfig(nil), 90 } 91 92 addr, err := net.ResolveUDPAddr("udp", "0.0.0.0:0") 93 Expect(err).NotTo(HaveOccurred()) 94 conn, err := net.ListenUDP("udp", addr) 95 Expect(err).NotTo(HaveOccurred()) 96 port = conn.LocalAddr().(*net.UDPAddr).Port 97 98 stoppedServing = make(chan struct{}) 99 100 go func() { 101 defer GinkgoRecover() 102 server.Serve(conn) 103 close(stoppedServing) 104 }() 105 }) 106 107 AfterEach(func() { 108 Expect(rt.Close()).NotTo(HaveOccurred()) 109 Expect(server.Close()).NotTo(HaveOccurred()) 110 Eventually(stoppedServing).Should(BeClosed()) 111 }) 112 113 BeforeEach(func() { 114 rt = &http3.RoundTripper{ 115 TLSClientConfig: getTLSClientConfigWithoutServerName(), 116 DisableCompression: true, 117 QuicConfig: getQuicConfig(&quic.Config{MaxIdleTimeout: 10 * time.Second}), 118 } 119 client = &http.Client{Transport: rt} 120 }) 121 122 It("downloads a hello", func() { 123 resp, err := client.Get(fmt.Sprintf("https://localhost:%d/hello", port)) 124 Expect(err).ToNot(HaveOccurred()) 125 Expect(resp.StatusCode).To(Equal(200)) 126 body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 3*time.Second)) 127 Expect(err).ToNot(HaveOccurred()) 128 Expect(string(body)).To(Equal("Hello, World!\n")) 129 }) 130 131 It("sets content-length for small response", func() { 132 mux.HandleFunc("/small", func(w http.ResponseWriter, r *http.Request) { 133 defer GinkgoRecover() 134 w.Write([]byte("foobar")) 135 }) 136 137 resp, err := client.Get(fmt.Sprintf("https://localhost:%d/small", port)) 138 Expect(err).ToNot(HaveOccurred()) 139 Expect(resp.StatusCode).To(Equal(200)) 140 Expect(resp.Header.Get("Content-Length")).To(Equal(strconv.Itoa(len("foobar")))) 141 }) 142 143 It("requests to different servers with the same udpconn", func() { 144 resp, err := client.Get(fmt.Sprintf("https://localhost:%d/remoteAddr", port)) 145 Expect(err).ToNot(HaveOccurred()) 146 Expect(resp.StatusCode).To(Equal(200)) 147 addr1 := resp.Header.Get("X-RemoteAddr") 148 Expect(addr1).ToNot(Equal("")) 149 resp, err = client.Get(fmt.Sprintf("https://127.0.0.1:%d/remoteAddr", port)) 150 Expect(err).ToNot(HaveOccurred()) 151 Expect(resp.StatusCode).To(Equal(200)) 152 addr2 := resp.Header.Get("X-RemoteAddr") 153 Expect(addr2).ToNot(Equal("")) 154 Expect(addr1).To(Equal(addr2)) 155 }) 156 157 It("downloads concurrently", func() { 158 group, ctx := errgroup.WithContext(context.Background()) 159 for i := 0; i < 2; i++ { 160 group.Go(func() error { 161 req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://localhost:%d/hello", port), nil) 162 Expect(err).ToNot(HaveOccurred()) 163 resp, err := client.Do(req) 164 Expect(err).ToNot(HaveOccurred()) 165 Expect(resp.StatusCode).To(Equal(200)) 166 body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 3*time.Second)) 167 Expect(err).ToNot(HaveOccurred()) 168 Expect(string(body)).To(Equal("Hello, World!\n")) 169 170 return nil 171 }) 172 } 173 174 err := group.Wait() 175 Expect(err).ToNot(HaveOccurred()) 176 }) 177 178 It("sets and gets request headers", func() { 179 handlerCalled := make(chan struct{}) 180 mux.HandleFunc("/headers/request", func(w http.ResponseWriter, r *http.Request) { 181 defer GinkgoRecover() 182 Expect(r.Header.Get("foo")).To(Equal("bar")) 183 Expect(r.Header.Get("lorem")).To(Equal("ipsum")) 184 close(handlerCalled) 185 }) 186 187 req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://localhost:%d/headers/request", port), nil) 188 Expect(err).ToNot(HaveOccurred()) 189 req.Header.Set("foo", "bar") 190 req.Header.Set("lorem", "ipsum") 191 resp, err := client.Do(req) 192 Expect(err).ToNot(HaveOccurred()) 193 Expect(resp.StatusCode).To(Equal(200)) 194 Eventually(handlerCalled).Should(BeClosed()) 195 }) 196 197 It("sets and gets response headers", func() { 198 mux.HandleFunc("/headers/response", func(w http.ResponseWriter, r *http.Request) { 199 defer GinkgoRecover() 200 w.Header().Set("foo", "bar") 201 w.Header().Set("lorem", "ipsum") 202 }) 203 204 resp, err := client.Get(fmt.Sprintf("https://localhost:%d/headers/response", port)) 205 Expect(err).ToNot(HaveOccurred()) 206 Expect(resp.StatusCode).To(Equal(200)) 207 Expect(resp.Header.Get("foo")).To(Equal("bar")) 208 Expect(resp.Header.Get("lorem")).To(Equal("ipsum")) 209 }) 210 211 It("downloads a small file", func() { 212 resp, err := client.Get(fmt.Sprintf("https://localhost:%d/prdata", port)) 213 Expect(err).ToNot(HaveOccurred()) 214 Expect(resp.StatusCode).To(Equal(200)) 215 body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 5*time.Second)) 216 Expect(err).ToNot(HaveOccurred()) 217 Expect(body).To(Equal(PRData)) 218 }) 219 220 It("downloads a large file", func() { 221 resp, err := client.Get(fmt.Sprintf("https://localhost:%d/prdatalong", port)) 222 Expect(err).ToNot(HaveOccurred()) 223 Expect(resp.StatusCode).To(Equal(200)) 224 body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 20*time.Second)) 225 Expect(err).ToNot(HaveOccurred()) 226 Expect(body).To(Equal(PRDataLong)) 227 }) 228 229 It("downloads many hellos", func() { 230 const num = 150 231 232 for i := 0; i < num; i++ { 233 resp, err := client.Get(fmt.Sprintf("https://localhost:%d/hello", port)) 234 Expect(err).ToNot(HaveOccurred()) 235 Expect(resp.StatusCode).To(Equal(200)) 236 body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 3*time.Second)) 237 Expect(err).ToNot(HaveOccurred()) 238 Expect(string(body)).To(Equal("Hello, World!\n")) 239 } 240 }) 241 242 It("downloads many files, if the response is not read", func() { 243 const num = 150 244 245 for i := 0; i < num; i++ { 246 resp, err := client.Get(fmt.Sprintf("https://localhost:%d/prdata", port)) 247 Expect(err).ToNot(HaveOccurred()) 248 Expect(resp.StatusCode).To(Equal(200)) 249 Expect(resp.Body.Close()).To(Succeed()) 250 } 251 }) 252 253 It("posts a small message", func() { 254 resp, err := client.Post( 255 fmt.Sprintf("https://localhost:%d/echo", port), 256 "text/plain", 257 bytes.NewReader([]byte("Hello, world!")), 258 ) 259 Expect(err).ToNot(HaveOccurred()) 260 Expect(resp.StatusCode).To(Equal(200)) 261 body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 5*time.Second)) 262 Expect(err).ToNot(HaveOccurred()) 263 Expect(body).To(Equal([]byte("Hello, world!"))) 264 }) 265 266 It("uploads a file", func() { 267 resp, err := client.Post( 268 fmt.Sprintf("https://localhost:%d/echo", port), 269 "text/plain", 270 bytes.NewReader(PRData), 271 ) 272 Expect(err).ToNot(HaveOccurred()) 273 Expect(resp.StatusCode).To(Equal(200)) 274 body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 5*time.Second)) 275 Expect(err).ToNot(HaveOccurred()) 276 Expect(body).To(Equal(PRData)) 277 }) 278 279 It("uses gzip compression", func() { 280 mux.HandleFunc("/gzipped/hello", func(w http.ResponseWriter, r *http.Request) { 281 defer GinkgoRecover() 282 Expect(r.Header.Get("Accept-Encoding")).To(Equal("gzip")) 283 w.Header().Set("Content-Encoding", "gzip") 284 w.Header().Set("foo", "bar") 285 286 gw := gzip.NewWriter(w) 287 defer gw.Close() 288 gw.Write([]byte("Hello, World!\n")) 289 }) 290 291 client.Transport.(*http3.RoundTripper).DisableCompression = false 292 resp, err := client.Get(fmt.Sprintf("https://localhost:%d/gzipped/hello", port)) 293 Expect(err).ToNot(HaveOccurred()) 294 Expect(resp.StatusCode).To(Equal(200)) 295 Expect(resp.Uncompressed).To(BeTrue()) 296 297 body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 3*time.Second)) 298 Expect(err).ToNot(HaveOccurred()) 299 Expect(string(body)).To(Equal("Hello, World!\n")) 300 }) 301 302 It("cancels requests", func() { 303 handlerCalled := make(chan struct{}) 304 mux.HandleFunc("/cancel", func(w http.ResponseWriter, r *http.Request) { 305 defer GinkgoRecover() 306 defer close(handlerCalled) 307 for { 308 if _, err := w.Write([]byte("foobar")); err != nil { 309 Expect(r.Context().Done()).To(BeClosed()) 310 var http3Err *http3.Error 311 Expect(errors.As(err, &http3Err)).To(BeTrue()) 312 Expect(http3Err.ErrorCode).To(Equal(http3.ErrCode(0x10c))) 313 Expect(http3Err.Error()).To(Equal("H3_REQUEST_CANCELLED")) 314 return 315 } 316 } 317 }) 318 319 req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://localhost:%d/cancel", port), nil) 320 Expect(err).ToNot(HaveOccurred()) 321 ctx, cancel := context.WithCancel(context.Background()) 322 req = req.WithContext(ctx) 323 resp, err := client.Do(req) 324 Expect(err).ToNot(HaveOccurred()) 325 Expect(resp.StatusCode).To(Equal(200)) 326 cancel() 327 Eventually(handlerCalled).Should(BeClosed()) 328 _, err = resp.Body.Read([]byte{0}) 329 var http3Err *http3.Error 330 Expect(errors.As(err, &http3Err)).To(BeTrue()) 331 Expect(http3Err.ErrorCode).To(Equal(http3.ErrCode(0x10c))) 332 Expect(http3Err.Error()).To(Equal("H3_REQUEST_CANCELLED (local)")) 333 }) 334 335 It("allows streamed HTTP requests", func() { 336 done := make(chan struct{}) 337 mux.HandleFunc("/echoline", func(w http.ResponseWriter, r *http.Request) { 338 defer GinkgoRecover() 339 defer close(done) 340 w.WriteHeader(200) 341 w.(http.Flusher).Flush() 342 reader := bufio.NewReader(r.Body) 343 for { 344 msg, err := reader.ReadString('\n') 345 if err != nil { 346 return 347 } 348 _, err = w.Write([]byte(msg)) 349 Expect(err).ToNot(HaveOccurred()) 350 w.(http.Flusher).Flush() 351 } 352 }) 353 354 r, w := io.Pipe() 355 req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("https://localhost:%d/echoline", port), r) 356 Expect(err).ToNot(HaveOccurred()) 357 rsp, err := client.Do(req) 358 Expect(err).ToNot(HaveOccurred()) 359 Expect(rsp.StatusCode).To(Equal(200)) 360 361 reader := bufio.NewReader(rsp.Body) 362 for i := 0; i < 5; i++ { 363 msg := fmt.Sprintf("Hello world, %d!\n", i) 364 fmt.Fprint(w, msg) 365 msgRcvd, err := reader.ReadString('\n') 366 Expect(err).ToNot(HaveOccurred()) 367 Expect(msgRcvd).To(Equal(msg)) 368 } 369 Expect(req.Body.Close()).To(Succeed()) 370 Eventually(done).Should(BeClosed()) 371 }) 372 373 It("allows taking over the stream", func() { 374 mux.HandleFunc("/httpstreamer", func(w http.ResponseWriter, r *http.Request) { 375 defer GinkgoRecover() 376 w.WriteHeader(200) 377 w.(http.Flusher).Flush() 378 379 str := r.Body.(http3.HTTPStreamer).HTTPStream() 380 str.Write([]byte("foobar")) 381 382 // Do this in a Go routine, so that the handler returns early. 383 // This way, we can also check that the HTTP/3 doesn't close the stream. 384 go func() { 385 defer GinkgoRecover() 386 _, err := io.Copy(str, str) 387 Expect(err).ToNot(HaveOccurred()) 388 Expect(str.Close()).To(Succeed()) 389 }() 390 }) 391 392 req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://localhost:%d/httpstreamer", port), nil) 393 Expect(err).ToNot(HaveOccurred()) 394 rsp, err := client.Transport.(*http3.RoundTripper).RoundTripOpt(req, http3.RoundTripOpt{DontCloseRequestStream: true}) 395 Expect(err).ToNot(HaveOccurred()) 396 Expect(rsp.StatusCode).To(Equal(200)) 397 398 str := rsp.Body.(http3.HTTPStreamer).HTTPStream() 399 b := make([]byte, 6) 400 _, err = io.ReadFull(str, b) 401 Expect(err).ToNot(HaveOccurred()) 402 Expect(b).To(Equal([]byte("foobar"))) 403 404 data := GeneratePRData(8 * 1024) 405 _, err = str.Write(data) 406 Expect(err).ToNot(HaveOccurred()) 407 Expect(str.Close()).To(Succeed()) 408 repl, err := io.ReadAll(str) 409 Expect(err).ToNot(HaveOccurred()) 410 Expect(repl).To(Equal(data)) 411 }) 412 413 It("serves other QUIC connections", func() { 414 tlsConf := getTLSConfig() 415 tlsConf.NextProtos = []string{http3.NextProtoH3} 416 ln, err := quic.ListenAddr("localhost:0", tlsConf, nil) 417 Expect(err).ToNot(HaveOccurred()) 418 defer ln.Close() 419 done := make(chan struct{}) 420 go func() { 421 defer GinkgoRecover() 422 defer close(done) 423 conn, err := ln.Accept(context.Background()) 424 Expect(err).ToNot(HaveOccurred()) 425 Expect(server.ServeQUICConn(conn)).To(Succeed()) 426 }() 427 428 resp, err := client.Get(fmt.Sprintf("https://localhost:%d/hello", ln.Addr().(*net.UDPAddr).Port)) 429 Expect(err).ToNot(HaveOccurred()) 430 Expect(resp.StatusCode).To(Equal(http.StatusOK)) 431 client.Transport.(io.Closer).Close() 432 Eventually(done).Should(BeClosed()) 433 }) 434 435 if go120 { 436 It("supports read deadlines", func() { 437 mux.HandleFunc("/read-deadline", func(w http.ResponseWriter, r *http.Request) { 438 defer GinkgoRecover() 439 err := setReadDeadline(w, time.Now().Add(deadlineDelay)) 440 Expect(err).ToNot(HaveOccurred()) 441 442 body, err := io.ReadAll(r.Body) 443 Expect(err).To(MatchError(os.ErrDeadlineExceeded)) 444 Expect(body).To(ContainSubstring("aa")) 445 446 w.Write([]byte("ok")) 447 }) 448 449 expectedEnd := time.Now().Add(deadlineDelay) 450 resp, err := client.Post( 451 fmt.Sprintf("https://localhost:%d/read-deadline", port), 452 "text/plain", 453 neverEnding('a'), 454 ) 455 Expect(err).ToNot(HaveOccurred()) 456 Expect(resp.StatusCode).To(Equal(200)) 457 458 body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 2*deadlineDelay)) 459 Expect(err).ToNot(HaveOccurred()) 460 Expect(time.Now().After(expectedEnd)).To(BeTrue()) 461 Expect(string(body)).To(Equal("ok")) 462 }) 463 464 It("supports write deadlines", func() { 465 mux.HandleFunc("/write-deadline", func(w http.ResponseWriter, r *http.Request) { 466 defer GinkgoRecover() 467 err := setWriteDeadline(w, time.Now().Add(deadlineDelay)) 468 Expect(err).ToNot(HaveOccurred()) 469 470 _, err = io.Copy(w, neverEnding('a')) 471 Expect(err).To(MatchError(os.ErrDeadlineExceeded)) 472 }) 473 474 expectedEnd := time.Now().Add(deadlineDelay) 475 476 resp, err := client.Get(fmt.Sprintf("https://localhost:%d/write-deadline", port)) 477 Expect(err).ToNot(HaveOccurred()) 478 Expect(resp.StatusCode).To(Equal(200)) 479 480 body, err := io.ReadAll(gbytes.TimeoutReader(resp.Body, 2*deadlineDelay)) 481 Expect(err).ToNot(HaveOccurred()) 482 Expect(time.Now().After(expectedEnd)).To(BeTrue()) 483 Expect(string(body)).To(ContainSubstring("aa")) 484 }) 485 } 486 })