Repository: goadesign/goa
Pull request #3232 (2023-01-23 09:23:08)

ClientError: Add original error
Storing the original error on `ClientError` struct enables checking the root cause:  for example
```golang
errors.Is(err, syscall.ECONNREFUSED)
```


Patch:
diff --git a/Makefile b/Makefile
index 3ff51804..bdff24f9 100644
--- a/Makefile
+++ b/Makefile
@@ -13,8 +13,8 @@
 # - "all" is the default target, it runs "lint" and "test"
 #
 MAJOR=3
-MINOR=10
-BUILD=2
+MINOR=11
+BUILD=0
 
 GOOS=$(shell go env GOOS)
 GO_FILES=$(shell find . -type f -name '*.go')
diff --git a/README.md b/README.md
index f7fa06c8..5cb7fbea 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
 
 ## Overview
 
-[![GoDev](https://img.shields.io/badge/go-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/goa.design/goa/v3@v3.10.2/dsl?tab=doc)
+[![GoDev](https://img.shields.io/badge/go-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/goa.design/goa/v3@v3.11.0/dsl?tab=doc)
 [![Foresight](https://api-public.service.runforesight.com/api/v1/badge/test?repoId=67defc2e-b3f8-4043-bac7-a4e215518ed3)](http://goa.app.runforesight.com/)
 [![Go Report Card](https://goreportcard.com/badge/goadesign/goa)](https://goreportcard.com/report/goadesign/goa)
 [![Slack](https://img.shields.io/badge/Slack-gray.svg?longCache=true&logo=slack&colorB=red)](https://gophers.slack.com/messages/goa)
@@ -86,7 +86,7 @@ invoking the client code.
 go install goa.design/goa/v3/cmd/goa@v3
 ```
 
-Current Release: `v3.10.2`
+Current Release: `v3.11.0`
 
 ## Teaser
 
@@ -352,7 +352,7 @@ In particular the page
 explains how to leverage the generated code to implement an HTTP or gRPC
 service.
 
-The [![DSL GoDoc](https://img.shields.io/badge/godoc-DSL-blue)](https://pkg.go.dev/goa.design/goa/v3@v3.10.2/dsl?tab=doc)
+The [![DSL GoDoc](https://img.shields.io/badge/godoc-DSL-blue)](https://pkg.go.dev/goa.design/goa/v3@v3.11.0/dsl?tab=doc)
 contains a fully documented reference of all the DSL functions.
 
 ### Instrumentation and System Example
diff --git a/codegen/validation.go b/codegen/validation.go
index bb7b74e0..6f215571 100644
--- a/codegen/validation.go
+++ b/codegen/validation.go
@@ -152,11 +152,7 @@ func recurseValidationCode(att *expr.AttributeExpr, put expr.UserType, attCtx *A
 		// validating a proto-generated type since the HTTP
 		// serialization transforms unions into objects.
 		u := expr.AsUnion(att.Type)
-		tref := Goify(put.Name(), true)
-		if attCtx.DefaultPkg != "" {
-			tref = attCtx.DefaultPkg + "." + tref
-		}
-		tref = "*" + tref
+		tref := attCtx.Scope.Ref(&expr.AttributeExpr{Type: put}, attCtx.DefaultPkg)
 		var vals []string
 		var types []string
 		for _, v := range u.Values {
diff --git a/go.mod b/go.mod
index 1bf41237..8c8ed3ed 100644
--- a/go.mod
+++ b/go.mod
@@ -14,7 +14,7 @@ require (
 	github.com/sergi/go-diff v1.3.1
 	github.com/zach-klippenstein/goregen v0.0.0-20160303162051-795b5e3961ea
 	golang.org/x/text v0.6.0
-	golang.org/x/tools v0.4.0
+	golang.org/x/tools v0.5.0
 	google.golang.org/grpc v1.52.0
 	google.golang.org/protobuf v1.28.1
 	gopkg.in/yaml.v3 v3.0.1
@@ -33,8 +33,8 @@ require (
 	github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
 	github.com/smartystreets/goconvey v1.7.2 // indirect
 	golang.org/x/mod v0.7.0 // indirect
-	golang.org/x/net v0.4.0 // indirect
-	golang.org/x/sys v0.3.0 // indirect
+	golang.org/x/net v0.5.0 // indirect
+	golang.org/x/sys v0.4.0 // indirect
 	google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 )
diff --git a/go.sum b/go.sum
index fba9b425..cc67783f 100644
--- a/go.sum
+++ b/go.sum
@@ -81,18 +81,18 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
 golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
 golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
-golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
+golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
+golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
 golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
-golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
+golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
 golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.4.0 h1:7mTAgkunk3fr4GAloyyCasadO6h9zSsQZbwvcaIciV4=
-golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
+golang.org/x/tools v0.5.0 h1:+bSpV5HIeWkuvgaMfI3UmKRThoTA5ODJTUd8T17NO+4=
+golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6 h1:a2S6M0+660BgMNl++4JPlcAO/CjkqYItDEZwkoDQK7c=
 google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
diff --git a/http/client.go b/http/client.go
index e3f73d1e..aa2285c3 100644
--- a/http/client.go
+++ b/http/client.go
@@ -49,9 +49,15 @@ type (
 		Timeout bool
 		// Is the error a server-side fault?
 		Fault bool
+		// The original error if any
+		Err error
 	}
 )
 
+func (c ClientError) Unwrap() error {
+	return c.Err
+}
+
 // NewDebugDoer wraps the given doer and captures the request and response so
 // they can be printed.
 func NewDebugDoer(d Doer) DebugDoer {
@@ -157,28 +163,28 @@ func ErrInvalidType(svc, m, expected string, actual interface{}) error {
 // request body.
 func ErrEncodingError(svc, m string, err error) error {
 	msg := fmt.Sprintf("failed to encode request body: %s", err)
-	return &ClientError{Name: "encoding_error", Message: msg, Service: svc, Method: m}
+	return &ClientError{Name: "encoding_error", Message: msg, Service: svc, Method: m, Err: err}
 }
 
 // ErrInvalidURL is the error returned when the URL computed for an method is
 // invalid.
 func ErrInvalidURL(svc, m, u string, err error) error {
 	msg := fmt.Sprintf("invalid URL %s: %s", u, err)
-	return &ClientError{Name: "invalid_url", Message: msg, Service: svc, Method: m}
+	return &ClientError{Name: "invalid_url", Message: msg, Service: svc, Method: m, Err: err}
 }
 
 // ErrDecodingError is the error returned when the decoder fails to decode the
 // response body.
 func ErrDecodingError(svc, m string, err error) error {
 	msg := fmt.Sprintf("failed to decode response body: %s", err)
-	return &ClientError{Name: "decoding_error", Message: msg, Service: svc, Method: m}
+	return &ClientError{Name: "decoding_error", Message: msg, Service: svc, Method: m, Err: err}
 }
 
 // ErrValidationError is the error returned when the response body is properly
 // received and decoded but fails validation.
 func ErrValidationError(svc, m string, err error) error {
 	msg := fmt.Sprintf("invalid response: %s", err)
-	return &ClientError{Name: "validation_error", Message: msg, Service: svc, Method: m}
+	return &ClientError{Name: "validation_error", Message: msg, Service: svc, Method: m, Err: err}
 }
 
 // ErrInvalidResponse is the error returned when the service responded with an
@@ -214,5 +220,5 @@ func ErrRequestError(svc, m string, err error) error {
 		timeout = nerr.Timeout()
 	}
 	return &ClientError{Name: "request_error", Message: err.Error(), Service: svc, Method: m,
-		Temporary: temporary, Timeout: timeout}
+		Temporary: temporary, Timeout: timeout, Err: err}
 }
diff --git a/pkg/version.go b/pkg/version.go
index 11e834aa..3f08c93a 100644
--- a/pkg/version.go
+++ b/pkg/version.go
@@ -10,9 +10,9 @@ const (
 	// Major version number
 	Major = 3
 	// Minor version number
-	Minor = 10
+	Minor = 11
 	// Build number
-	Build = 2
+	Build = 0
 	// Suffix - set to empty string in release tag commits.
 	Suffix = ""
 )


Test patch:
diff --git a/http/client_test.go b/http/client_test.go
new file mode 100644
index 00000000..29153cdf
--- /dev/null
+++ b/http/client_test.go
@@ -0,0 +1,55 @@
+package http
+
+import (
+	"errors"
+	"testing"
+)
+
+func TestClientError_Unwrap(t *testing.T) {
+	sentinelError := errors.New("this is na error")
+	alternateSentinelError := errors.New("another error")
+
+	tests := []struct {
+		name             string
+		err              error
+		checkedSentinel  error
+		expectedCausedBy bool
+	}{
+		{
+			name: "caused by sentinel",
+			err: ErrRequestError(
+				"AService",
+				"Something went wrong",
+				sentinelError,
+			),
+			checkedSentinel:  sentinelError,
+			expectedCausedBy: true,
+		},
+		{
+			name: "null cause hypothesis",
+			err: ErrRequestError(
+				"AService",
+				"Something went wrong",
+				sentinelError,
+			),
+			checkedSentinel:  alternateSentinelError,
+			expectedCausedBy: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(
+			tt.name, func(t *testing.T) {
+				isCausedBy := errors.Is(tt.err, tt.checkedSentinel)
+
+				if isCausedBy != tt.expectedCausedBy {
+					if tt.expectedCausedBy {
+						t.Errorf("got error %#v, should be caused by %#v", tt.err, tt.checkedSentinel)
+					} else {
+						t.Errorf("got error %#v, must NOT be caused by %#v", tt.err, tt.checkedSentinel)
+					}
+				}
+			},
+		)
+	}
+}