Skip to content

Commit fb24b8b

Browse files
authored
Merge branch 'main' into dependabot/go_modules/internal/conformance/github.com/quic-go/quic-go-0.49.1
2 parents c96da72 + cb2e11f commit fb24b8b

File tree

3 files changed

+138
-4
lines changed

3 files changed

+138
-4
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/.tmp/
22
*.pprof
33
*.svg
4+
.idea
45
cover.out
56
connect.test

internal/memhttp/memhttp.go

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ type Server struct {
3333

3434
serverWG sync.WaitGroup
3535
serverErr error
36+
37+
// client is configured for use with the server.
38+
// Its transport is automatically closed when Close is called.
39+
client *http.Client
40+
clientMu sync.Mutex
3641
}
3742

3843
// NewServer creates a new Server that uses the given handler. Configuration
@@ -94,12 +99,18 @@ func (s *Server) TransportHTTP1() *http.Transport {
9499
}
95100

96101
// Client returns an [http.Client] configured to use in-memory pipes rather
97-
// than TCP and speak HTTP/2. It is configured to use the same
98-
// [http2.Transport] as [Transport].
102+
// than TCP and speak HTTP/2.
99103
//
100-
// Callers may reconfigure the returned client without affecting other clients.
104+
// Client is configured to use the same transport for the lifetime of the
105+
// server, and its idle connections are automatically closed when the
106+
// server is closed.
101107
func (s *Server) Client() *http.Client {
102-
return &http.Client{Transport: s.Transport()}
108+
s.clientMu.Lock()
109+
defer s.clientMu.Unlock()
110+
if s.client == nil {
111+
s.client = &http.Client{Transport: s.Transport()}
112+
}
113+
return s.client
103114
}
104115

105116
// URL returns the server's URL.
@@ -110,6 +121,11 @@ func (s *Server) URL() string {
110121
// Shutdown gracefully shuts down the server, without interrupting any active
111122
// connections. See [http.Server.Shutdown] for details.
112123
func (s *Server) Shutdown(ctx context.Context) error {
124+
s.clientMu.Lock()
125+
if s.client != nil {
126+
s.client.CloseIdleConnections()
127+
}
128+
s.clientMu.Unlock()
113129
if err := s.server.Shutdown(ctx); err != nil {
114130
return err
115131
}
@@ -128,6 +144,11 @@ func (s *Server) Cleanup() error {
128144
// Close closes the server's listener. It does not wait for connections to
129145
// finish.
130146
func (s *Server) Close() error {
147+
s.clientMu.Lock()
148+
if s.client != nil {
149+
s.client.CloseIdleConnections()
150+
}
151+
s.clientMu.Unlock()
131152
return s.server.Close()
132153
}
133154

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Copyright 2021-2025 The Connect Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
//go:build go1.25
16+
17+
// Copyright 2021-2025 The Connect Authors
18+
//
19+
// Licensed under the Apache License, Version 2.0 (the "License");
20+
// you may not use this file except in compliance with the License.
21+
// You may obtain a copy of the License at
22+
//
23+
// http://www.apache.org/licenses/LICENSE-2.0
24+
//
25+
// Unless required by applicable law or agreed to in writing, software
26+
// distributed under the License is distributed on an "AS IS" BASIS,
27+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
28+
// See the License for the specific language governing permissions and
29+
// limitations under the License.
30+
31+
package memhttptest_test
32+
33+
import (
34+
"bytes"
35+
"io"
36+
"net/http"
37+
"strings"
38+
"testing"
39+
"testing/synctest"
40+
41+
"connectrpc.com/connect/internal/assert"
42+
"connectrpc.com/connect/internal/memhttp"
43+
"connectrpc.com/connect/internal/memhttp/memhttptest"
44+
)
45+
46+
// TestMemhttpWithSynctest verifies that memhttp works correctly with synctest.
47+
func TestMemhttpWithSynctest(t *testing.T) {
48+
t.Parallel()
49+
body := "request body"
50+
51+
handler := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
52+
buf := &bytes.Buffer{}
53+
_, err := io.Copy(buf, request.Body)
54+
if err != nil {
55+
t.Errorf("failed to copy body: %v", err)
56+
}
57+
if buf.String() != body {
58+
t.Errorf("got body %q, want %q", buf.String(), body)
59+
}
60+
writer.WriteHeader(http.StatusOK)
61+
})
62+
63+
tests := []struct {
64+
name string
65+
client func(*testing.T, *memhttp.Server) *http.Client
66+
}{
67+
{
68+
name: "server.Client()",
69+
client: func(t *testing.T, s *memhttp.Server) *http.Client {
70+
t.Helper()
71+
return s.Client()
72+
},
73+
},
74+
{
75+
name: "Custom Client HTTP/1",
76+
client: func(t *testing.T, s *memhttp.Server) *http.Client {
77+
t.Helper()
78+
// HTTP/1.1's is a per-request closure, so nothing leaks outside the bubble.
79+
return &http.Client{Transport: s.TransportHTTP1()}
80+
},
81+
},
82+
{
83+
name: "Custom Client HTTP/2",
84+
client: func(t *testing.T, s *memhttp.Server) *http.Client {
85+
t.Helper()
86+
// HTTP/2 a goroutine running for future connections, which leaks outside the bubble.
87+
client := &http.Client{Transport: s.Transport()}
88+
// Closing idle connections here ensures synctest doesn't panic.
89+
t.Cleanup(client.CloseIdleConnections)
90+
return client
91+
},
92+
},
93+
}
94+
95+
for _, test := range tests {
96+
t.Run(test.name, func(t *testing.T) {
97+
t.Parallel()
98+
synctest.Test(t, func(t *testing.T) {
99+
t.Helper()
100+
server := memhttptest.NewServer(t, handler)
101+
102+
req, err := http.NewRequestWithContext(t.Context(), http.MethodPut, server.URL(), strings.NewReader(body))
103+
assert.Nil(t, err)
104+
105+
client := test.client(t, server)
106+
resp, err := client.Do(req)
107+
assert.Nil(t, err)
108+
resp.Body.Close()
109+
})
110+
})
111+
}
112+
}

0 commit comments

Comments
 (0)