-
Notifications
You must be signed in to change notification settings - Fork 17.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
net/http/httptest: NewTLSServer doesn't support HTTP/2 requests #34939
Comments
@rhysh thank you for filing this issue and great to catch you here! --- before_test.go 2019-10-16 13:42:17.000000000 -0700
+++ fixed_test.go 2019-10-16 13:48:43.000000000 -0700
@@ -6,20 +6,34 @@
"net/http/httptest"
"sync/atomic"
"testing"
+
+ "golang.org/x/net/http2"
)
func TestHTTPTestHTTP2(t *testing.T) {
t.Run("basic", func(t *testing.T) {
var calls int64
- s := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ s := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&calls, 1)
if !r.ProtoAtLeast(2, 0) {
t.Errorf("Request is not http/2: %q", r.Proto)
}
}))
+ if err := http2.ConfigureServer(s.Config, new(http2.Server)); err != nil {
+ t.Fatalf("Failed to configure HTTP/2 server: %v", err)
+ }
+ s.TLS = s.Config.TLSConfig
+ s.StartTLS()
defer s.Close()
- resp, err := s.Client().Get(s.URL)
+ tr := &http.Transport{TLSClientConfig: s.Config.TLSConfig}
+ if err := http2.ConfigureTransport(tr); err != nil {
+ t.Fatalf("Failed to configure http2 transport: %v", err)
+ }
+ tr.TLSClientConfig.InsecureSkipVerify = true
+ client := &http.Client{Transport: tr}
+
+ resp, err := client.Get(s.URL)
if err != nil {
t.Fatalf("HTTP GET: %v", err)
}
@@ -37,13 +51,21 @@
t.Errorf("Request is not http/2: %q", r.Proto)
}
}))
- s.TLS = &tls.Config{
- NextProtos: []string{"h2"},
+ if err := http2.ConfigureServer(s.Config, new(http2.Server)); err != nil {
+ t.Fatalf("Failed to configure HTTP/2 server: %v", err)
}
+ s.TLS = s.Config.TLSConfig
s.StartTLS()
defer s.Close()
- resp, err := s.Client().Get(s.URL)
+ tr := &http.Transport{TLSClientConfig: s.Config.TLSConfig}
+ if err := http2.ConfigureTransport(tr); err != nil {
+ t.Fatalf("Failed to configure http2 transport: %v", err)
+ }
+ tr.TLSClientConfig.InsecureSkipVerify = true
+ client := &http.Client{Transport: tr}
+
+ resp, err := client.Get(s.URL)
if err != nil {
t.Fatalf("HTTP GET: %v", err)
} and I have encountered this is in the wild which is why I shared this gist https://gist.github.com/odeke-em/cacfe2ea5efc57c5a7a562aba1478378 to finally give you the file package repro
import (
"crypto/tls"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"golang.org/x/net/http2"
)
func TestHTTPTestHTTP2(t *testing.T) {
t.Run("basic", func(t *testing.T) {
var calls int64
s := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&calls, 1)
if !r.ProtoAtLeast(2, 0) {
t.Errorf("Request is not http/2: %q", r.Proto)
}
}))
if err := http2.ConfigureServer(s.Config, new(http2.Server)); err != nil {
t.Fatalf("Failed to configure HTTP/2 server: %v", err)
}
s.TLS = s.Config.TLSConfig
s.StartTLS()
defer s.Close()
tr := &http.Transport{TLSClientConfig: s.Config.TLSConfig}
if err := http2.ConfigureTransport(tr); err != nil {
t.Fatalf("Failed to configure http2 transport: %v", err)
}
tr.TLSClientConfig.InsecureSkipVerify = true
client := &http.Client{Transport: tr}
resp, err := client.Get(s.URL)
if err != nil {
t.Fatalf("HTTP GET: %v", err)
}
resp.Body.Close()
if have, want := atomic.LoadInt64(&calls), int64(1); have != want {
t.Errorf("HTTP handler called %d times, expected %d times", have, want)
}
})
t.Run("advertise-h2", func(t *testing.T) {
var calls int64
s := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&calls, 1)
if !r.ProtoAtLeast(2, 0) {
t.Errorf("Request is not http/2: %q", r.Proto)
}
}))
if err := http2.ConfigureServer(s.Config, new(http2.Server)); err != nil {
t.Fatalf("Failed to configure HTTP/2 server: %v", err)
}
s.TLS = s.Config.TLSConfig
s.StartTLS()
defer s.Close()
tr := &http.Transport{TLSClientConfig: s.Config.TLSConfig}
if err := http2.ConfigureTransport(tr); err != nil {
t.Fatalf("Failed to configure http2 transport: %v", err)
}
tr.TLSClientConfig.InsecureSkipVerify = true
client := &http.Client{Transport: tr}
resp, err := client.Get(s.URL)
if err != nil {
t.Fatalf("HTTP GET: %v", err)
}
resp.Body.Close()
if have, want := atomic.LoadInt64(&calls), int64(1); have != want {
t.Errorf("HTTP handler called %d times, expected %d times", have, want)
}
})
t.Run("transport-hacks", func(t *testing.T) {
var calls int64
s := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&calls, 1)
if !r.ProtoAtLeast(2, 0) {
t.Errorf("Request is not http/2: %q", r.Proto)
}
}))
s.TLS = &tls.Config{
NextProtos: []string{"h2"},
}
s.StartTLS()
defer s.Close()
transport := s.Client().Transport.(*http.Transport)
clientConfig := transport.TLSClientConfig
transport.TLSClientConfig = nil
// make a request to trigger HTTP/2 autoconfiguration
resp, err := s.Client().Get(s.URL)
if err == nil {
t.Errorf(`Expected harmless "certificate signed by unknown authority" error`)
resp.Body.Close()
}
// now allow the client to connect to the ad-hoc test server
transport.TLSClientConfig.RootCAs = clientConfig.RootCAs
resp, err = s.Client().Get(s.URL)
if err != nil {
t.Fatalf("HTTP GET: %v", err)
}
resp.Body.Close()
if have, want := atomic.LoadInt64(&calls), int64(1); have != want {
t.Errorf("HTTP handler called %d times, expected %d times", have, want)
}
})
} but anyways the crux of it is that you'll need to: What I think we can do though is for the net/http/httptest package is add a new helper for example |
Or perhaps an option for |
Howa about a new And an example, if docs aren't sufficient. So the flow would be NewUnstartedServer, set the bool to true, and Start. |
Perfect, SGTM, thank you @bradfitz! CL coming up shortly with an example too and tests :) |
Thanks @odeke-em and @bradfitz . A new boolean field sounds like the right level of complexity for the user. Do we need even that? Should http/2 be the default for httptest setups just as it is for production net/http clients and servers? When would a user want to opt out of http/2? httptest has a mechanism for that opt-out already: setting the server's NextProtos to not include "h2". Do we need a new specialized field? |
Change https://golang.org/cl/201557 mentions this issue: |
net/http/httptest has been around for ages before HTTP/2 and the norm when testing is to use HTTP/1.1, and as you can see configuring HTTP/2 on these servers is a little tricky. Changing it to always use HTTP/2 might break lots of people's code who've been using this package for years. I think we can perhaps schedule a gradual roll out to invert the behavior, and like you mention, instead make HTTP/1.1 toggleable by the opt-out but after a while of having EnableHTTP2 around. |
Change https://golang.org/cl/217131 mentions this issue: |
Updates #34939 Updates #36878 Change-Id: Ifa9a17b5b16bfcfbfe1d113a2b66a63ea3a6b59c Reviewed-on: https://go-review.googlesource.com/c/go/+/217131 Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
What version of Go are you using (
go version
)?Does this issue reproduce with the latest release?
Yes, go1.13.1 and tip are affected.
What operating system and processor architecture are you using (
go env
)?go env
OutputWhat did you do?
I called
httptest.NewTLSServer
and used the attachedhttp.Client
to make requests to it.What did you expect to see?
I expected the requests to the local test server to use HTTP/2. (This is important for me at the moment because I'm writing reproducers for bugs I've seen in HTTP/2 client requests.)
What did you see instead?
The requests are sent over HTTP/1.1.
It looks like this is because the httptest package's TLS setup sets a
tls.Config
on thehttp.Transport
before its first use, which defeats net/http's usual HTTP/2 autoconfiguration.The text was updated successfully, but these errors were encountered: