@samitouri / QOSAMI-X402Tunnel / commits / 144367ab
Fork 0

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 {