Publishes localhost services to the agentic web through self-hostable, trustless relays with x402 payments.
Improve resource cleanup and error logging
Adds explicit error logging for resource closures (streams, connections, response bodies) throughout relaydns and example clients. Refactors shutdown sequences in example_chat and example_http_client to ensure proper context cancellation and orderly shutdown. Also fixes minor error handling in websocket closure.
cognitive committed
Oct 21, 2025 at 22:51 UTC
144367abf0ad951f77928ccb5e9b9cd4a2032546
6 files changed
+83
-32
cmd/example_chat/main.go
+10
-10
@@ -41,6 +41,7 @@ func main() {
41
42
func runChat(cmd *cobra.Command, args []string) error {
43
ctx, cancel := context.WithCancel(context.Background())
44
+ defer cancel() // Ensure context is cancelled on all exit paths
45
46
// 1) start local chat HTTP backend
47
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", flagPort))
@@ -70,27 +71,26 @@ func runChat(cmd *cobra.Command, args []string) error {
71
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
72
<-sig
73
log.Info().Msg("[chat] shutting down...")
73
-
74
+
75
// Shutdown sequence:
75
- // 1. Cancel context to stop client advertising/refresh loops
76
- cancel()
77
-
78
- // 2. Close client (waits for goroutines, closes libp2p host)
76
+ // Note: defer cancel() at function start stops client advertising/refresh loops
77
+
78
+ // 1. Close client (waits for goroutines, closes libp2p host)
79
if err := client.Close(); err != nil {
80
log.Warn().Err(err).Msg("[chat] client close error")
81
}
82
-
83
- // 3. Shutdown HTTP server with a fresh context (with timeout)
82
+
83
+ // 2. Shutdown HTTP server with a fresh context (with timeout)
84
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
85
defer shutdownCancel()
86
if err := srv.Shutdown(shutdownCtx); err != nil {
87
log.Error().Err(err).Msg("[chat] http server shutdown error")
88
}
89
-
90
- // 4. Close all websocket connections and wait for handlers to finish
89
+
90
+ // 3. Close all websocket connections and wait for handlers to finish
91
hub.closeAll()
92
hub.wait()
93
-
93
+
94
log.Info().Msg("[chat] shutdown complete")
95
return nil
96
}
cmd/example_chat/view.go
+1
-1
@@ -100,7 +100,7 @@ func handleWS(w http.ResponseWriter, r *http.Request, h *hub) {
100
if leftUser != "" {
101
h.broadcast(message{TS: time.Now().UTC(), User: leftUser, Event: "left"})
102
}
103
- conn.Close(websocket.StatusNormalClosure, "")
103
+ _ = conn.Close(websocket.StatusNormalClosure, "")
104
cancelConn()
105
h.wg.Done()
106
}()
cmd/example_http_client/main.go
+18
-6
@@ -8,6 +8,7 @@ import (
8
"os"
9
"os/signal"
10
"syscall"
11
+ "time"
12
13
"github.com/gosuda/relaydns/relaydns"
14
"github.com/rs/zerolog/log"
@@ -41,7 +42,7 @@ func main() {
42
43
func runClient(cmd *cobra.Command, args []string) error {
44
ctx, cancel := context.WithCancel(context.Background())
44
- defer cancel()
45
+ defer cancel() // Ensure context is cancelled on all exit paths
46
47
if flagName == "" {
48
hn, err := os.Hostname()
@@ -83,17 +84,28 @@ func runClient(cmd *cobra.Command, args []string) error {
84
return fmt.Errorf("start client: %w", err)
85
}
86
clientRef = client
86
- defer client.Close()
87
88
// wait for termination
89
sig := make(chan os.Signal, 1)
90
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
91
<-sig
92
- log.Info().Msg("[client] shutting down client...")
93
- if err := srv.Shutdown(ctx); err != nil {
94
- log.Error().Err(err).Msg("[client] server forced to shutdown")
92
+ log.Info().Msg("[client] shutting down...")
93
+
94
+ // Shutdown sequence:
95
+ // Note: defer cancel() at function start stops client advertising/refresh loops
96
+
97
+ // 1. Close client (waits for goroutines, closes libp2p host)
98
+ if err := client.Close(); err != nil {
99
+ log.Warn().Err(err).Msg("[client] client close error")
100
+ }
101
+
102
+ // 2. Shutdown HTTP server with a fresh context (with timeout)
103
+ shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
104
+ defer shutdownCancel()
105
+ if err := srv.Shutdown(shutdownCtx); err != nil {
106
+ log.Error().Err(err).Msg("[client] http server shutdown error")
107
}
96
- log.Info().Msg("[client] http server stopped")
108
109
+ log.Info().Msg("[client] shutdown complete")
110
return nil
111
}
relaydns/client.go
+15
-5
@@ -207,16 +207,26 @@ func (b *RelayClient) setupStreamHandler() error {
207
b.h.SetStreamHandler(b.protoID, b.cfg.Handler)
208
case b.cfg.TargetTCP != "":
209
b.h.SetStreamHandler(b.protoID, func(s network.Stream) {
210
- defer s.Close()
210
+ defer func() {
211
+ if err := s.Close(); err != nil {
212
+ log.Debug().Err(err).Msg("relaydns: stream close")
213
+ }
214
+ }()
215
c, err := net.Dial("tcp", b.cfg.TargetTCP)
216
if err != nil {
217
log.Error().Err(err).Msgf("relaydns: dial %s", b.cfg.TargetTCP)
218
return
219
}
216
- defer c.Close()
217
- // raw byte pipe
218
- go io.Copy(c, s)
219
- io.Copy(s, c)
220
+ defer func() {
221
+ if err := c.Close(); err != nil {
222
+ log.Debug().Err(err).Msg("relaydns: conn close")
223
+ }
224
+ }()
225
+ // raw byte pipe (bidirectional)
226
+ go func() {
227
+ _, _ = io.Copy(c, s)
228
+ }()
229
+ _, _ = io.Copy(s, c)
230
})
231
default:
232
return fmt.Errorf("relaydns: either Handler or TargetTCP must be set")
relaydns/server.go
+33
-9
@@ -193,7 +193,11 @@ func (d *RelayServer) ProxyHTTP(w http.ResponseWriter, r *http.Request, peerID,
193
http.Error(w, "open stream failed", http.StatusBadGateway)
194
return
195
}
196
- defer s.Close()
196
+ defer func() {
197
+ if err := s.Close(); err != nil {
198
+ log.Debug().Err(err).Msg("stream close")
199
+ }
200
+ }()
201
outReq := r.Clone(d.ctx)
202
outReq.URL = &url.URL{Path: pathSuffix, RawQuery: r.URL.RawQuery}
203
outReq.RequestURI = ""
@@ -209,7 +213,11 @@ func (d *RelayServer) ProxyHTTP(w http.ResponseWriter, r *http.Request, peerID,
213
http.Error(w, "bad upstream response", http.StatusBadGateway)
214
return
215
}
212
- defer resp.Body.Close()
216
+ defer func() {
217
+ if err := resp.Body.Close(); err != nil {
218
+ log.Debug().Err(err).Msg("response body close")
219
+ }
220
+ }()
221
222
// Handle WebSocket Upgrade: write 101 response and then raw-tunnel bytes
223
if resp.StatusCode == http.StatusSwitchingProtocols && strings.Contains(strings.ToLower(resp.Header.Get("Upgrade")), "websocket") {
@@ -219,7 +227,11 @@ func (d *RelayServer) ProxyHTTP(w http.ResponseWriter, r *http.Request, peerID,
227
log.Error().Err(err).Msg("hijack client conn")
228
return
229
}
222
- defer clientConn.Close()
230
+ defer func() {
231
+ if err := clientConn.Close(); err != nil {
232
+ log.Debug().Err(err).Msg("client conn close")
233
+ }
234
+ }()
235
// Write upstream 101 response (headers)
236
if err := resp.Write(clientBuf); err == nil {
237
_ = clientBuf.Flush()
@@ -231,9 +243,11 @@ func (d *RelayServer) ProxyHTTP(w http.ResponseWriter, r *http.Request, peerID,
243
_, _ = clientConn.Write(tmp)
244
}
245
}
234
- // Raw byte tunnel between client and upstream stream
235
- go io.Copy(s, clientConn)
236
- io.Copy(clientConn, s)
246
+ // Raw byte tunnel between client and upstream stream (bidirectional)
247
+ go func() {
248
+ _, _ = io.Copy(s, clientConn)
249
+ }()
250
+ _, _ = io.Copy(clientConn, s)
251
return
252
}
253
}
@@ -267,7 +281,11 @@ func (d *RelayServer) ProxyHTTP(w http.ResponseWriter, r *http.Request, peerID,
281
// ProxyTCP opens a libp2p stream to peerID using the Director protocol and
282
// pipes raw bytes between the accepted TCP connection and the libp2p stream.
283
func (d *RelayServer) ProxyTCP(c net.Conn, peerID string) error {
270
- defer c.Close()
284
+ defer func() {
285
+ if err := c.Close(); err != nil {
286
+ log.Debug().Err(err).Msg("tcp conn close")
287
+ }
288
+ }()
289
d.storeMu.Lock()
290
entry, ok := d.store[peerID]
291
d.storeMu.Unlock()
@@ -281,9 +299,15 @@ func (d *RelayServer) ProxyTCP(c net.Conn, peerID string) error {
299
if err != nil {
300
return err
301
}
284
- defer s.Close()
302
+ defer func() {
303
+ if err := s.Close(); err != nil {
304
+ log.Debug().Err(err).Msg("stream close")
305
+ }
306
+ }()
307
// bidirectional copy
286
- go io.Copy(s, c)
308
+ go func() {
309
+ _, _ = io.Copy(s, c)
310
+ }()
311
_, _ = io.Copy(c, s)
312
return nil
313
}
relaydns/utils.go
+6
-1
@@ -10,6 +10,7 @@ import (
10
"time"
11
12
"github.com/libp2p/go-libp2p/core/host"
13
+ "github.com/rs/zerolog/log"
14
)
15
16
func AddrToTarget(listen string) string {
@@ -73,7 +74,11 @@ func fetchMultiaddrsFromHosts(base string, timeout time.Duration) ([]string, err
74
if err != nil {
75
return nil, err
76
}
76
- defer resp.Body.Close()
77
+ defer func() {
78
+ if err := resp.Body.Close(); err != nil {
79
+ log.Debug().Err(err).Msg("response body close")
80
+ }
81
+ }()
82
83
var payload Hosts
84
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {