Repository: nats-io/nats.go (go)

Issue:
Casing issue in library prevents `ErrorIs` from working correctly
## Defect
There is a case-mismatch between nats.go and nats-server that breaks `ErrorIs` from working.
See more discussion at https://natsio.slack.com/archives/C069GSYFP/p1663083344148659

Make sure that these boxes are checked before submitting your issue -- thank you!

 - [x] Included nats.go version
 - [x] Included a [Minimal, Complete, and Verifiable example] (https://stackoverflow.com/help/mcve)


#### Versions of `nats.go` and the `nats-server` if one was involved:
nats.go: v1.16.1-0.20220810192301-fb5ca2cbc995
nats-server: v2.8.5-0.20220831210747-62f91c1dd2cf // 2.9.0-RC.16
#### OS/Container environment:
MacOS 12.5.1

#### Steps or code to reproduce the issue:
```
package mytest

import (
	"testing"
	"time"

	server "github.com/nats-io/nats-server/v2/server"
	nats "github.com/nats-io/nats.go"
	"github.com/stretchr/testify/require"
)

// Timeout for request/response.
const Timeout = 2 * time.Second

func startServer(t *testing.T) (s *server.Server) {
	t.Helper()

	s = server.New(&server.Options{
		Host:             "127.0.0.1",
		Port:             server.RANDOM_PORT,
		NoLog:            true,
		NoSigs:           true,
		Authorization: "abc",
		MaxControlLine:   2048,
	})

	go server.Run(s) // nolint:errcheck

	if !s.ReadyForConnections(Timeout) {
		t.Fatal("starting nats server: timeout")
	}
	return
}

func Test_requests(t *testing.T) {

	var server = startServer(t)
	defer server.Shutdown()

	var serverAddress = "nats://" + server.Addr().String() // use server address

	_, err := nats.Connect(serverAddress)
	require.Error(t, err)
	require.ErrorIs(t, err, nats.ErrAuthorization)
}```

#### Expected result:
test passes

#### Actual result:
test fails with 
        	Error:      	Target error should be in err chain:
        	            	expected: "nats: authorization violation"
        	            	in chain: "nats: Authorization Violation"


Pull request description:
Add natsProtoErr that can be used with errors.Is
The server can send both `-ERR 'Authorization Violation'` or `-ERR 'authorization violation'` depending on some conditions so in the client side we usually try to normalize the behavior by changing to lowercase when checking for an error.
One exception was during the initial connect where the client instead returned the raw error protocol message.

Since this behavior has been in the wild for many years, instead of returning the raw error or the proper error type like `ErrAuthorization` to keep things backwards compatible just in case, this adds a new `natsProtoErr` that can be used with `errors.Is` to check whether the raw protocol message is equivalent to other errors.

Fixes #1081

Signed-off-by: Waldemar Quevedo <wally@nats.io>

Patch:
diff --git a/nats.go b/nats.go
index 988924d..5edbdd3 100644
--- a/nats.go
+++ b/nats.go
@@ -2323,6 +2323,19 @@ func normalizeErr(line string) string {
 	return s
 }
 
+// natsProtoErr represents an -ERR protocol message sent by the server.
+type natsProtoErr struct {
+	description string
+}
+
+func (nerr *natsProtoErr) Error() string {
+	return fmt.Sprintf("nats: %s", nerr.description)
+}
+
+func (nerr *natsProtoErr) Is(err error) bool {
+	return strings.ToLower(nerr.Error()) == err.Error()
+}
+
 // Send a connect protocol message to the server, issue user/password if
 // applicable. Will wait for a flush to return from the server for error
 // processing.
@@ -2377,8 +2390,7 @@ func (nc *Conn) sendConnect() error {
 				// in doReconnect()).
 				nc.processAuthError(authErr)
 			}
-
-			return errors.New("nats: " + proto)
+			return &natsProtoErr{proto}
 		}
 
 		// Notify that we got an unexpected protocol.


Test patch:
diff --git a/test/auth_test.go b/test/auth_test.go
index 27559a7..32d80e5 100644
--- a/test/auth_test.go
+++ b/test/auth_test.go
@@ -14,6 +14,7 @@
 package test
 
 import (
+	"errors"
 	"fmt"
 	"strings"
 	"sync/atomic"
@@ -44,6 +45,10 @@ func TestAuth(t *testing.T) {
 		t.Fatalf("Expected error '%v', got '%v'", nats.ErrAuthorization, err)
 	}
 
+	if !errors.Is(err, nats.ErrAuthorization) {
+		t.Fatalf("Expected error '%v', got '%v'", nats.ErrAuthorization, err)
+	}
+
 	nc, err := nats.Connect("nats://derek:foo@127.0.0.1:8232")
 	if err != nil {
 		t.Fatal("Should have connected successfully with a token")
diff --git a/test/cluster_test.go b/test/cluster_test.go
index bbe3b90..5f7595f 100644
--- a/test/cluster_test.go
+++ b/test/cluster_test.go
@@ -14,6 +14,7 @@
 package test
 
 import (
+	"errors"
 	"fmt"
 	"math"
 	"net"
@@ -193,6 +194,10 @@ func TestAuthServers(t *testing.T) {
 		t.Fatalf("Wrong error, wanted Auth failure, got '%s'\n", err)
 	}
 
+	if !errors.Is(err, nats.ErrAuthorization) {
+		t.Fatalf("Expected error '%v', got '%v'", nats.ErrAuthorization, err)
+	}
+
 	// Test that we can connect to a subsequent correct server.
 	var authServers = []string{
 		"nats://127.0.0.1:1222",