Repository: go-openapi/runtime
Pull request #243 (2022-05-05 16:34:58)

fix APIError to always report errors
Command `swagger generate client` is generating client code looking like this:

```
// ReadResponse reads a server response into the received o.
func (o *DownloadQPartReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) {


	readResponseError := func(err error) error {
		if response.Code()/100 == 2 {
			// that's a real error reading the response,
			// we might as well discard the response code ...
			return runtime.NewAPIError("read response", err, response.Code())
		}
		// now we're reading a potential error.. but if none don't say it's an error
		if err == io.EOF {
			err = nil
		}
		return runtime.NewAPIError("read response", err, response.Code())
	}

	switch response.Code() {

	case 200:
...
```

With errors that do not implement json marshalling the original error is wipped of the output:

```
cause [read response (status 200): {}]
```

After applying the patch:

```
cause [read response (status 200): 'write /path/to/my/file: no space left on device']
```



Patch:
diff --git a/client_response.go b/client_response.go
index 0b7e382..0d16911 100644
--- a/client_response.go
+++ b/client_response.go
@@ -15,10 +15,9 @@
 package runtime
 
 import (
+	"encoding/json"
 	"fmt"
 	"io"
-
-	"encoding/json"
 )
 
 // A ClientResponse represents a client response
@@ -61,13 +60,18 @@ type APIError struct {
 	Code          int
 }
 
-func (a *APIError) Error() string {
-	resp, _ := json.Marshal(a.Response)
-	return fmt.Sprintf("%s (status %d): %s", a.OperationName, a.Code, resp)
+func (o *APIError) Error() string {
+	var resp []byte
+	if err, ok := o.Response.(error); ok {
+		resp = []byte("'" + err.Error() + "'")
+	} else {
+		resp, _ = json.Marshal(o.Response)
+	}
+	return fmt.Sprintf("%s (status %d): %s", o.OperationName, o.Code, resp)
 }
 
-func (a *APIError) String() string {
-	return a.Error()
+func (o *APIError) String() string {
+	return o.Error()
 }
 
 // IsSuccess returns true when this elapse o k response returns a 2xx status code


Test patch:
diff --git a/client_response_test.go b/client_response_test.go
index ee13d9d..d84719b 100644
--- a/client_response_test.go
+++ b/client_response_test.go
@@ -16,8 +16,11 @@ package runtime
 
 import (
 	"bytes"
+	"errors"
 	"io"
+	"io/fs"
 	"io/ioutil"
+	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -61,3 +64,27 @@ func TestResponseReaderFunc(t *testing.T) {
 	assert.Equal(t, "the header", actual.Header)
 	assert.Equal(t, 490, actual.Code)
 }
+
+func TestResponseReaderFuncError(t *testing.T) {
+	reader := ClientResponseReaderFunc(func(r ClientResponse, _ Consumer) (interface{}, error) {
+		_, _ = ioutil.ReadAll(r.Body())
+		return nil, NewAPIError("fake", errors.New("writer closed"), 490)
+	})
+	_, err := reader.ReadResponse(response{}, nil)
+	assert.NotNil(t, err)
+	assert.True(t, strings.Contains(err.Error(), "writer closed"), err.Error())
+
+	reader = func(r ClientResponse, _ Consumer) (interface{}, error) {
+		_, _ = ioutil.ReadAll(r.Body())
+		err := &fs.PathError{
+			Op:   "write",
+			Path: "path/to/fake",
+			Err:  fs.ErrClosed,
+		}
+		return nil, NewAPIError("fake", err, 200)
+	}
+	_, err = reader.ReadResponse(response{}, nil)
+	assert.NotNil(t, err)
+	assert.True(t, strings.Contains(err.Error(), "file already closed"), err.Error())
+
+}