Repository: kubernetes/kubernetes (language: go)

Issue:
`watchErrorStream` doesn't wrap the upstream error
### What happened?

`watchErrorStream` function doesn't wrap the upstream error emitted during reading from the websocket, instead it transforms the error into a simple string making it impossible to use errors.Is/As with the underlying error

```
func watchErrorStream(errorStream io.Reader, d errorStreamDecoder) chan error {
	errorChan := make(chan error)

	go func() {
		defer runtime.HandleCrash()

		message, err := io.ReadAll(errorStream)
		switch {
		case err != nil && err != io.EOF:
			errorChan <- fmt.Errorf("error reading from error stream: %s", err)
		case len(message) > 0:
			errorChan <- d.decode(message)
		default:
			errorChan <- nil
		}
		close(errorChan)
	}()

	return errorChan
}


```

https://github.com/kubernetes/kubernetes/blob/42c4ddbe2d8efe95481d10ccfc9ca098a71e83e9/staging/src/k8s.io/client-go/tools/remotecommand/errorstream.go#L31-L54

### What did you expect to happen?

`watchErrorStream` should wrap the underlying error so clients can inspect using `errors.As/Is` instead of transforming the error into a string.

### How can we reproduce it (as minimally and precisely as possible)?

Try to use client-go and use errors.Is with the output of StreamWithContext

### Anything else we need to know?

_No response_

### Kubernetes version

<details>

```console
$ kubectl version
# paste output here
```

</details>


### Cloud provider

<details>

</details>


### OS version

<details>

```console
# On Linux:
$ cat /etc/os-release
# paste output here
$ uname -a
# paste output here

# On Windows:
C:\> wmic os get Caption, Version, BuildNumber, OSArchitecture
# paste output here
```

</details>


### Install tools

<details>

</details>


### Container runtime (CRI) and version (if applicable)

<details>

</details>


### Related plugins (CNI, CSI, ...) and versions (if applicable)

<details>

</details>



Discussion / hints:

Fix patch:
diff --git a/staging/src/k8s.io/client-go/tools/remotecommand/errorstream.go b/staging/src/k8s.io/client-go/tools/remotecommand/errorstream.go
index e60dd7cdc7ccd..90bb39b4a444f 100644
--- a/staging/src/k8s.io/client-go/tools/remotecommand/errorstream.go
+++ b/staging/src/k8s.io/client-go/tools/remotecommand/errorstream.go
@@ -41,7 +41,7 @@ func watchErrorStream(errorStream io.Reader, d errorStreamDecoder) chan error {
 		message, err := io.ReadAll(errorStream)
 		switch {
 		case err != nil && err != io.EOF:
-			errorChan <- fmt.Errorf("error reading from error stream: %s", err)
+			errorChan <- fmt.Errorf("error reading from error stream: %w", err)
 		case len(message) > 0:
 			errorChan <- d.decode(message)
 		default:


Test patch:
diff --git a/staging/src/k8s.io/client-go/tools/remotecommand/v2_test.go b/staging/src/k8s.io/client-go/tools/remotecommand/v2_test.go
index e303f57a9e0f7..412cee8d2f4f2 100644
--- a/staging/src/k8s.io/client-go/tools/remotecommand/v2_test.go
+++ b/staging/src/k8s.io/client-go/tools/remotecommand/v2_test.go
@@ -19,12 +19,13 @@ package remotecommand
 import (
 	"errors"
 	"io"
+	"net"
 	"net/http"
 	"strings"
 	"testing"
 	"time"
 
-	"k8s.io/api/core/v1"
+	v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/util/httpstream"
 	"k8s.io/apimachinery/pkg/util/wait"
 )
@@ -181,17 +182,34 @@ func TestV2ErrorStreamReading(t *testing.T) {
 	tests := []struct {
 		name          string
 		stream        io.Reader
-		expectedError error
+		expectedError func(*testing.T, error)
 	}{
 		{
-			name:          "error reading from stream",
-			stream:        &fakeReader{errors.New("foo")},
-			expectedError: errors.New("error reading from error stream: foo"),
+			name:   "error reading from stream",
+			stream: &fakeReader{errors.New("foo")},
+			expectedError: func(t *testing.T, err error) {
+				if e, a := "error reading from error stream: foo", err.Error(); e != a {
+					t.Errorf("expected '%s', got '%s'", e, a)
+				}
+			},
 		},
 		{
-			name:          "stream returns an error",
-			stream:        strings.NewReader("some error"),
-			expectedError: errors.New("error executing remote command: some error"),
+			name:   "stream returns an error",
+			stream: strings.NewReader("some error"),
+			expectedError: func(t *testing.T, err error) {
+				if e, a := "error executing remote command: some error", err.Error(); e != a {
+					t.Errorf("expected '%s', got '%s'", e, a)
+				}
+			},
+		},
+		{
+			name:   "typed error",
+			stream: &fakeReader{net.ErrClosed},
+			expectedError: func(t *testing.T, err error) {
+				if !errors.Is(err, net.ErrClosed) {
+					t.Errorf("expected errors.Is(err, net.ErrClosed), failed on %#v", err)
+				}
+			},
 		},
 	}
 
@@ -214,8 +232,8 @@ func TestV2ErrorStreamReading(t *testing.T) {
 		if test.expectedError != nil {
 			if err == nil {
 				t.Errorf("%s: expected an error", test.name)
-			} else if e, a := test.expectedError, err; e.Error() != a.Error() {
-				t.Errorf("%s: expected %q, got %q", test.name, e, a)
+			} else {
+				test.expectedError(t, err)
 			}
 			continue
 		}