Repository: libp2p/go-libp2p
Pull request #2437 (2023-07-19 14:58:45)

swarm: fix Unwrap for DialError, implement Unwrap for TransportError
fixes: https://github.com/libp2p/go-libp2p/issues/2436

Depends on #2460  #2461 
Blocked on go1.21 release. 

sukunrt: I don't mind waiting. This is not blocking anything. 
marten-seemann: We can still get it into the next release. v0.30 won't happen before the Go 1.21 transition (quic-go forces us to do so, if we want to include v0.37).
marten-seemann: #2460 and #2461. @sukunrt can you rebase please?

Patch:
diff --git a/p2p/net/swarm/dial_error.go b/p2p/net/swarm/dial_error.go
index 711ee060..4de68220 100644
--- a/p2p/net/swarm/dial_error.go
+++ b/p2p/net/swarm/dial_error.go
@@ -30,10 +30,7 @@ func (e *DialError) recordErr(addr ma.Multiaddr, err error) {
 		e.Skipped++
 		return
 	}
-	e.DialErrors = append(e.DialErrors, TransportError{
-		Address: addr,
-		Cause:   err,
-	})
+	e.DialErrors = append(e.DialErrors, TransportError{Address: addr, Cause: err})
 }
 
 func (e *DialError) Error() string {
@@ -51,9 +48,19 @@ func (e *DialError) Error() string {
 	return builder.String()
 }
 
-// Unwrap implements https://godoc.org/golang.org/x/xerrors#Wrapper.
-func (e *DialError) Unwrap() error {
-	return e.Cause
+func (e *DialError) Unwrap() []error {
+	if e == nil {
+		return nil
+	}
+
+	errs := make([]error, len(e.DialErrors)+1)
+	if e.Cause != nil {
+		errs = append(errs, e.Cause)
+	}
+	for i := 0; i < len(e.DialErrors); i++ {
+		errs = append(errs, &e.DialErrors[i])
+	}
+	return errs
 }
 
 var _ error = (*DialError)(nil)
@@ -68,4 +75,8 @@ func (e *TransportError) Error() string {
 	return fmt.Sprintf("failed to dial %s: %s", e.Address, e.Cause)
 }
 
+func (e *TransportError) Unwrap() error {
+	return e.Cause
+}
+
 var _ error = (*TransportError)(nil)


Test patch:
diff --git a/p2p/net/swarm/dial_error_test.go b/p2p/net/swarm/dial_error_test.go
new file mode 100644
index 00000000..3231d6f0
--- /dev/null
+++ b/p2p/net/swarm/dial_error_test.go
@@ -0,0 +1,51 @@
+package swarm
+
+import (
+	"net"
+	"os"
+	"testing"
+
+	ma "github.com/multiformats/go-multiaddr"
+	"github.com/stretchr/testify/require"
+)
+
+func TestTransportError(t *testing.T) {
+	aa := ma.StringCast("/ip4/1.2.3.4/tcp/1234")
+	te := &TransportError{Address: aa, Cause: ErrDialBackoff}
+	require.ErrorIs(t, te, ErrDialBackoff, "TransportError should implement Unwrap")
+}
+
+func TestDialError(t *testing.T) {
+	de := &DialError{Peer: "pid", Cause: ErrGaterDisallowedConnection}
+	require.ErrorIs(t, de, ErrGaterDisallowedConnection,
+		"DialError Unwrap should handle DialError.Cause")
+	require.ErrorIs(t, de, de, "DialError Unwrap should handle match to self")
+
+	aa := ma.StringCast("/ip4/1.2.3.4/tcp/1234")
+	ab := ma.StringCast("/ip6/1::1/udp/1234/quic-v1")
+	de = &DialError{
+		Peer: "pid",
+		DialErrors: []TransportError{
+			{Address: aa, Cause: ErrDialBackoff}, {Address: ab, Cause: ErrNoTransport},
+		},
+	}
+	require.ErrorIs(t, de, ErrDialBackoff, "DialError.Unwrap should traverse TransportErrors")
+	require.ErrorIs(t, de, ErrNoTransport, "DialError.Unwrap should traverse TransportErrors")
+
+	de = &DialError{
+		Peer: "pid",
+		DialErrors: []TransportError{{Address: ab, Cause: ErrNoTransport},
+			// wrapped error 2 levels deep
+			{Address: aa, Cause: &net.OpError{
+				Op:  "write",
+				Net: "tcp",
+				Err: &os.SyscallError{
+					Syscall: "connect",
+					Err:     os.ErrPermission,
+				},
+			}},
+		},
+	}
+	require.ErrorIs(t, de, os.ErrPermission, "DialError.Unwrap should traverse TransportErrors")
+
+}
diff --git a/p2p/net/swarm/swarm_dial_test.go b/p2p/net/swarm/swarm_dial_test.go
index f3654441..47310978 100644
--- a/p2p/net/swarm/swarm_dial_test.go
+++ b/p2p/net/swarm/swarm_dial_test.go
@@ -372,5 +372,5 @@ func TestBlackHoledAddrBlocked(t *testing.T) {
 	if !errors.As(err, &de) {
 		t.Fatalf("expected to receive an error of type *DialError, got %s of type %T", err, err)
 	}
-	require.Contains(t, de.DialErrors, TransportError{Address: addr, Cause: ErrDialRefusedBlackHole})
+	require.ErrorIs(t, err, ErrDialRefusedBlackHole)
 }