Skip to content

net/http/httptest: NewTLSServer doesn't support HTTP/2 requests #34939

Closed
@rhysh

Description

@rhysh

What version of Go are you using (go version)?

$ go1.13 version
go version go1.13.1 darwin/amd64
$ go-tip version
go version devel +f6c624a22a Wed Oct 16 15:58:33 2019 +0000 darwin/amd64

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 Output
$ go1.13 env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/Users/rhys/Library/Caches/go-build"
GOENV="/Users/rhys/Library/Application Support/go/env"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GONOPROXY="*"
GONOSUMDB="*"
GOOS="darwin"
GOPATH="/Users/rhys/go"
GOPRIVATE="*"
GOPROXY="direct"
GOROOT="/Users/rhys/go/version/go1.13"
GOSUMDB="off"
GOTMPDIR=""
GOTOOLDIR="/Users/rhys/go/version/go1.13/pkg/tool/darwin_amd64"
GCCGO="gccgo"
AR="ar"
CC="clang"
CXX="clang++"
CGO_ENABLED="1"
GOMOD=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/49/zmds5zsn75z1283vtzxyfr5hj7yjq4/T/go-build721053994=/tmp/go-build -gno-record-gcc-switches -fno-common"

What did you do?

I called httptest.NewTLSServer and used the attached http.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 the http.Transport before its first use, which defeats net/http's usual HTTP/2 autoconfiguration.

$  go1.13 test -v ./repro_test.go
=== RUN   TestHTTPTestHTTP2
=== RUN   TestHTTPTestHTTP2/basic
=== RUN   TestHTTPTestHTTP2/advertise-h2
=== RUN   TestHTTPTestHTTP2/transport-hacks
2019/10/16 13:23:20 http: TLS handshake error from 127.0.0.1:64474: remote error: tls: bad certificate
--- FAIL: TestHTTPTestHTTP2 (0.14s)
    --- FAIL: TestHTTPTestHTTP2/basic (0.00s)
        repro_test.go:17: Request is not http/2: "HTTP/1.1"
    --- FAIL: TestHTTPTestHTTP2/advertise-h2 (0.00s)
        repro_test.go:37: Request is not http/2: "HTTP/1.1"
    --- PASS: TestHTTPTestHTTP2/transport-hacks (0.14s)
FAIL
FAIL    command-line-arguments  0.193s
FAIL
$ go-tip test -v ./repro_test.go
=== RUN   TestHTTPTestHTTP2
=== RUN   TestHTTPTestHTTP2/basic
=== RUN   TestHTTPTestHTTP2/advertise-h2
=== RUN   TestHTTPTestHTTP2/transport-hacks
2019/10/16 13:23:55 http: TLS handshake error from 127.0.0.1:64490: remote error: tls: bad certificate
--- FAIL: TestHTTPTestHTTP2 (0.14s)
    --- FAIL: TestHTTPTestHTTP2/basic (0.00s)
        repro_test.go:17: Request is not http/2: "HTTP/1.1"
    --- FAIL: TestHTTPTestHTTP2/advertise-h2 (0.00s)
        repro_test.go:37: Request is not http/2: "HTTP/1.1"
    --- PASS: TestHTTPTestHTTP2/transport-hacks (0.14s)
FAIL
FAIL    command-line-arguments  1.254s
FAIL
package repro

import (
	"crypto/tls"
	"net/http"
	"net/http/httptest"
	"sync/atomic"
	"testing"
)

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) {
			atomic.AddInt64(&calls, 1)
			if !r.ProtoAtLeast(2, 0) {
				t.Errorf("Request is not http/2: %q", r.Proto)
			}
		}))
		defer s.Close()

		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)
		}
	})

	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)
			}
		}))
		s.TLS = &tls.Config{
			NextProtos: []string{"h2"},
		}
		s.StartTLS()
		defer s.Close()

		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)
		}
	})

	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)
		}
	})
}

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions