Skip to content

Commit c440192

Browse files
committed
feat(courierhttp/client): h2c supported
1 parent aeb0b7b commit c440192

File tree

4 files changed

+131
-110
lines changed

4 files changed

+131
-110
lines changed

example/cmd/example/main_test.go

Lines changed: 66 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -3,127 +3,90 @@ package main_test
33
import (
44
"bytes"
55
"context"
6-
"crypto/tls"
76
"fmt"
87
"io"
9-
"net"
108
"net/http"
9+
"net/http/httptest"
10+
"net/http/httputil"
1111
"testing"
12-
"time"
1312

1413
"github.com/go-courier/logr"
1514
"github.com/go-courier/logr/slog"
1615
"github.com/octohelm/courier/example/apis"
1716
"github.com/octohelm/courier/example/client/example"
1817
domainorg "github.com/octohelm/courier/example/pkg/domain/org"
1918
"github.com/octohelm/courier/internal/testingutil"
20-
"github.com/octohelm/courier/pkg/courierhttp/client"
2119
"github.com/octohelm/courier/pkg/courierhttp/handler/httprouter"
2220
testingx "github.com/octohelm/x/testing"
23-
"golang.org/x/net/http2"
21+
"github.com/octohelm/x/testing/bdd"
2422
)
2523

26-
var htLogger = client.HttpTransportFunc(func(req *http.Request, next client.RoundTrip) (*http.Response, error) {
27-
startedAt := time.Now()
28-
29-
ctx, logger := logr.Start(req.Context(), "Request")
30-
defer logger.End()
31-
32-
resp, err := next(req.WithContext(ctx))
33-
34-
defer func() {
35-
cost := time.Since(startedAt)
36-
37-
logger := logger.WithValues(
38-
"cost", fmt.Sprintf("%0.3fms", float64(cost/time.Millisecond)),
39-
"method", req.Method,
40-
"url", req.URL.String(),
41-
"metadata", req.Header,
42-
"http.content-length", req.ContentLength,
43-
)
44-
45-
if err == nil {
46-
logger.WithValues("response.proto", resp.Proto).Info("success")
47-
} else {
48-
logger.Warn(fmt.Errorf("http request failed: %w", err))
49-
}
50-
}()
51-
52-
return resp, err
53-
})
54-
5524
func TestAll(t *testing.T) {
56-
h, err := httprouter.New(apis.R, "example")
57-
testingx.Expect(t, err, testingx.BeNil[error]())
58-
59-
srv := testingutil.Serve(t, h)
60-
61-
c := &example.Client{
62-
Endpoint: srv.URL,
63-
HttpTransports: []client.HttpTransport{htLogger},
64-
}
65-
ctx := c.InjectContext(context.Background())
66-
ctx = logr.WithLogger(ctx, slog.Logger(slog.Default()))
67-
68-
t.Run("Do Some Request", func(t *testing.T) {
69-
org := &example.GetOrg{}
70-
org.OrgName = "test"
71-
72-
resp, err := example.Do(ctx, org)
73-
testingx.Expect(t, err, testingx.BeNil[error]())
74-
testingx.Expect(t, resp.Name, testingx.Be(org.OrgName))
75-
testingx.Expect(t, resp.Type, testingx.Be(domainorg.TYPE__GOV))
76-
})
77-
78-
t.Run("Do Some Request with h2", func(t *testing.T) {
79-
org := &example.GetOrg{}
80-
org.OrgName = "test"
81-
82-
resp, err := example.Do(client.ContextWithRoundTripperCreator(ctx, func() http.RoundTripper {
83-
return &http2.Transport{
84-
AllowHTTP: true,
85-
DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
86-
return net.Dial(network, addr)
87-
},
88-
}
89-
}), org)
90-
testingx.Expect(t, err, testingx.BeNil[error]())
91-
testingx.Expect(t, resp.Name, testingx.Be(org.OrgName))
92-
})
93-
94-
t.Run("Upload", func(t *testing.T) {
95-
v := &example.UploadBlob{}
96-
v.RequestBody = io.NopCloser(bytes.NewBufferString("1234567"))
97-
98-
_, err := example.Do(ctx, v)
99-
testingx.Expect(t, err, testingx.BeNil[error]())
100-
})
25+
h := bdd.Must(httprouter.New(apis.R, "example"))
10126

102-
t.Run("UploadStoreBlob", func(t *testing.T) {
103-
v := &example.UploadStoreBlob{}
104-
v.Scope = "a/b/c"
105-
v.RequestBody = io.NopCloser(bytes.NewBufferString("1234567"))
27+
hh := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
28+
raw, _ := httputil.DumpRequest(request, true)
29+
fmt.Println(string(raw))
10630

107-
_, err := example.Do(ctx, v)
108-
testingx.Expect(t, err, testingx.BeNil[error]())
31+
h.ServeHTTP(writer, request)
10932
})
11033

111-
t.Run("GetStoreBlob", func(t *testing.T) {
112-
v := &example.GetStoreBlob{}
113-
v.Scope = "a/b/c"
114-
v.Digest = "xxx"
115-
116-
resp, err := example.Do(ctx, v)
117-
testingx.Expect(t, err, testingx.BeNil[error]())
118-
testingx.Expect(t, *resp, testingx.Be("a/b/c@xxx"))
119-
})
120-
121-
t.Run("GetFile", func(t *testing.T) {
122-
v := &example.GetFile{}
123-
v.Path = "a/b/c"
124-
125-
resp, err := example.Do(ctx, v)
126-
testingx.Expect(t, err, testingx.BeNil[error]())
127-
testingx.Expect(t, *resp, testingx.Be("a/b/c"))
128-
})
34+
for i, srv := range []*httptest.Server{
35+
testingutil.ServeWithH2C(t, hh),
36+
testingutil.Serve(t, hh),
37+
} {
38+
t.Run(fmt.Sprintf("serve http/%d", 2-i), func(t *testing.T) {
39+
c := &example.Client{Endpoint: srv.URL}
40+
41+
ctx := c.InjectContext(context.Background())
42+
ctx = logr.WithLogger(ctx, slog.Logger(slog.Default()))
43+
44+
t.Run("Do Some Request", func(t *testing.T) {
45+
org := &example.GetOrg{}
46+
org.OrgName = "test"
47+
48+
resp, err := example.Do(ctx, org)
49+
testingx.Expect(t, err, testingx.BeNil[error]())
50+
51+
testingx.Expect(t, resp.Name, testingx.Be(org.OrgName))
52+
testingx.Expect(t, resp.Type, testingx.Be(domainorg.TYPE__GOV))
53+
})
54+
55+
t.Run("Upload", func(t *testing.T) {
56+
v := &example.UploadBlob{}
57+
v.RequestBody = io.NopCloser(bytes.NewBufferString("1234567"))
58+
59+
_, err := example.Do(ctx, v)
60+
testingx.Expect(t, err, testingx.BeNil[error]())
61+
})
62+
63+
t.Run("UploadStoreBlob", func(t *testing.T) {
64+
v := &example.UploadStoreBlob{}
65+
v.Scope = "a/b/c"
66+
v.RequestBody = io.NopCloser(bytes.NewBufferString("1234567"))
67+
68+
_, err := example.Do(ctx, v)
69+
testingx.Expect(t, err, testingx.BeNil[error]())
70+
})
71+
72+
t.Run("GetStoreBlob", func(t *testing.T) {
73+
v := &example.GetStoreBlob{}
74+
v.Scope = "a/b/c"
75+
v.Digest = "xxx"
76+
77+
resp, err := example.Do(ctx, v)
78+
testingx.Expect(t, err, testingx.BeNil[error]())
79+
testingx.Expect(t, *resp, testingx.Be("a/b/c@xxx"))
80+
})
81+
82+
t.Run("GetFile", func(t *testing.T) {
83+
v := &example.GetFile{}
84+
v.Path = "a/b/c"
85+
86+
resp, err := example.Do(ctx, v)
87+
testingx.Expect(t, err, testingx.BeNil[error]())
88+
testingx.Expect(t, *resp, testingx.Be("a/b/c"))
89+
})
90+
})
91+
}
12992
}

internal/testingutil/serve.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,15 @@ import (
1010
)
1111

1212
func Serve(t testing.TB, handler http.Handler) *httptest.Server {
13-
srv := httptest.NewServer(h2c.NewHandler(handler, &http2.Server{}))
14-
t.Cleanup(func() {
15-
srv.Close()
16-
})
13+
srv := httptest.NewUnstartedServer(handler)
14+
srv.Start()
15+
t.Cleanup(srv.Close)
16+
return srv
17+
}
18+
19+
func ServeWithH2C(t testing.TB, handler http.Handler) *httptest.Server {
20+
srv := httptest.NewUnstartedServer(h2c.NewHandler(handler, &http2.Server{}))
21+
srv.Start()
22+
t.Cleanup(srv.Close)
1723
return srv
1824
}

pkg/courierhttp/client/client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func (c *Client) Do(ctx context.Context, req any, metas ...courier.Metadata) cou
7373
httpClient = GetReasonableClientContext(ctx, c.HttpTransports...)
7474
} else {
7575
if httpClient.Transport == nil {
76-
httpClient.Transport = http.DefaultTransport
76+
httpClient.Transport = reasonableRoundTripper
7777
}
7878
httpClient.Transport = WithHttpTransports(c.HttpTransports...)(httpClient.Transport)
7979
}

pkg/courierhttp/client/http_default.go

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ package client
33
import (
44
"context"
55
"crypto/tls"
6+
"errors"
67
"net"
78
"net/http"
89
"time"
10+
11+
"golang.org/x/net/http2"
912
)
1013

1114
var reasonableRoundTripper = &http.Transport{
@@ -24,13 +27,13 @@ var reasonableRoundTripper = &http.Transport{
2427
ExpectContinueTimeout: 1 * time.Second,
2528

2629
ResponseHeaderTimeout: 60 * time.Second,
27-
30+
2831
ForceAttemptHTTP2: true,
2932
}
3033

3134
var defaultTlsConfig = &tls.Config{}
32-
3335
var defaultHosts = Hosts{}
36+
var useHttp2Transport = true
3437

3538
func AddHostAlias(hostAliases ...HostAlias) {
3639
for _, hostAlias := range hostAliases {
@@ -45,6 +48,10 @@ func SetDefaultTLSClientConfig(tlsConfig *tls.Config) {
4548
}
4649
}
4750

51+
func UseHttp2Transport(enabled bool) {
52+
useHttp2Transport = enabled
53+
}
54+
4855
func GetReasonableClientContext(ctx context.Context, httpTransports ...HttpTransport) *http.Client {
4956
t := http.RoundTripper(reasonableRoundTripper)
5057

@@ -53,6 +60,8 @@ func GetReasonableClientContext(ctx context.Context, httpTransports ...HttpTrans
5360
t = tc()
5461
}
5562

63+
t = upgradeToSupportH2c(t)
64+
5665
return &http.Client{Transport: WithHttpTransports(httpTransports...)(t)}
5766
}
5867

@@ -87,5 +96,48 @@ func GetShortConnClientContext(ctx context.Context, httpTransports ...HttpTransp
8796
t = tc()
8897
}
8998

99+
t = upgradeToSupportH2c(t)
100+
90101
return &http.Client{Transport: WithHttpTransports(httpTransports...)(t)}
91102
}
103+
104+
func upgradeToSupportH2c(t http.RoundTripper) http.RoundTripper {
105+
if !useHttp2Transport {
106+
return t
107+
}
108+
109+
if t1, ok := t.(*http.Transport); ok {
110+
if !t1.DisableKeepAlives {
111+
if t2, err := http2.ConfigureTransports(t1); err == nil {
112+
t2.AllowHTTP = true
113+
t2.DialTLSContext = func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
114+
return t1.DialContext(ctx, network, addr)
115+
}
116+
t2.ConnPool = nil
117+
118+
return &fallbackTransport{
119+
http2Transport: t2,
120+
httpTransport: t1,
121+
}
122+
}
123+
}
124+
}
125+
126+
return t
127+
}
128+
129+
type fallbackTransport struct {
130+
http2Transport *http2.Transport
131+
httpTransport *http.Transport
132+
}
133+
134+
func (t *fallbackTransport) RoundTrip(req *http.Request) (*http.Response, error) {
135+
resp, err := t.http2Transport.RoundTrip(req)
136+
if err != nil {
137+
if errors.Is(err, http2.ErrFrameTooLarge) {
138+
return t.httpTransport.RoundTrip(req)
139+
}
140+
return nil, err
141+
}
142+
return resp, nil
143+
}

0 commit comments

Comments
 (0)