From e3147fa05438ef9f21429dbb6d4441d758e72d40 Mon Sep 17 00:00:00 2001 From: Waldemar Quevedo Date: Fri, 12 Aug 2022 16:09:07 -0700 Subject: [PATCH 1/7] js: Add JetStreamAPIError with more api response details Signed-off-by: Waldemar Quevedo --- jsm.go | 55 +++++++++++++++-- nats.go | 160 ++++++++++++++++++++++++------------------------ test/js_test.go | 11 +++- 3 files changed, 139 insertions(+), 87 deletions(-) diff --git a/jsm.go b/jsm.go index e7bf8a88f..14e61b40c 100644 --- a/jsm.go +++ b/jsm.go @@ -161,6 +161,37 @@ type APIError struct { Description string `json:"description,omitempty"` } +// JetStreamAPIError is an error result from making a request to the +// JetStream API. +type JetStreamAPIError interface { + Code() int + ErrorCode() ErrorCode + Description() string + Error() string +} + +type jsAPIError struct { + code int + errorCode ErrorCode + description string +} + +func (err *jsAPIError) Code() int { + return err.code +} + +func (err *jsAPIError) ErrorCode() ErrorCode { + return err.errorCode +} + +func (err *jsAPIError) Description() string { + return err.description +} + +func (err *jsAPIError) Error() string { + return err.description +} + // apiResponse is a standard response from the JetStream JSON API type apiResponse struct { Type string `json:"type"` @@ -240,6 +271,14 @@ func (e *APIError) Error() string { return fmt.Sprintf("nats: API error %d: %s", e.ErrorCode, e.Description) } +var ( + // ErrJetStreamNotEnabled is an error returned when JetStream is not enabled. + ErrJetStreamNotEnabledForAccount JetStreamAPIError = &jsAPIError{errorCode: JSErrCodeJetStreamNotEnabledForAccount, description: "nats: jetstream not enabled for account"} + + // ErrJetStreamNotEnabledForAccount is an error returned when JetStream is not enabled for an account. + ErrJetStreamNotEnabled JetStreamAPIError = &jsAPIError{errorCode: JSErrCodeJetStreamNotEnabled, description: "nats: jetstream not enabled"} +) + // AccountInfo retrieves info about the JetStream usage from the current account. // If JetStream is not enabled, this will return ErrJetStreamNotEnabled // Other errors can happen but are generally considered retryable @@ -265,13 +304,19 @@ func (js *js) AccountInfo(opts ...JSOpt) (*AccountInfo, error) { return nil, err } if info.Error != nil { + // Check based on error code instead of description match. if info.Error.ErrorCode == JSErrCodeJetStreamNotEnabledForAccount { - return nil, ErrJetStreamNotEnabledForAccount - } - if info.Error.ErrorCode == JSErrCodeJetStreamNotEnabled { - return nil, ErrJetStreamNotEnabled + err = ErrJetStreamNotEnabledForAccount + } else if info.Error.ErrorCode == JSErrCodeJetStreamNotEnabled { + err = ErrJetStreamNotEnabled + } else { + err = &jsAPIError{ + code: info.Error.Code, + errorCode: info.Error.ErrorCode, + description: info.Error.Description, + } } - return nil, info.Error + return nil, err } return &info.AccountInfo, nil diff --git a/nats.go b/nats.go index f2317380b..15e29ca2f 100644 --- a/nats.go +++ b/nats.go @@ -90,87 +90,85 @@ const ( // Errors var ( - ErrConnectionClosed = errors.New("nats: connection closed") - ErrConnectionDraining = errors.New("nats: connection draining") - ErrDrainTimeout = errors.New("nats: draining connection timed out") - ErrConnectionReconnecting = errors.New("nats: connection reconnecting") - ErrSecureConnRequired = errors.New("nats: secure connection required") - ErrSecureConnWanted = errors.New("nats: secure connection not available") - ErrBadSubscription = errors.New("nats: invalid subscription") - ErrTypeSubscription = errors.New("nats: invalid subscription type") - ErrBadSubject = errors.New("nats: invalid subject") - ErrBadQueueName = errors.New("nats: invalid queue name") - ErrSlowConsumer = errors.New("nats: slow consumer, messages dropped") - ErrTimeout = errors.New("nats: timeout") - ErrBadTimeout = errors.New("nats: timeout invalid") - ErrAuthorization = errors.New("nats: authorization violation") - ErrAuthExpired = errors.New("nats: authentication expired") - ErrAuthRevoked = errors.New("nats: authentication revoked") - ErrAccountAuthExpired = errors.New("nats: account authentication expired") - ErrNoServers = errors.New("nats: no servers available for connection") - ErrJsonParse = errors.New("nats: connect message, json parse error") - ErrChanArg = errors.New("nats: argument needs to be a channel type") - ErrMaxPayload = errors.New("nats: maximum payload exceeded") - ErrMaxMessages = errors.New("nats: maximum messages delivered") - ErrSyncSubRequired = errors.New("nats: illegal call on an async subscription") - ErrMultipleTLSConfigs = errors.New("nats: multiple tls.Configs not allowed") - ErrNoInfoReceived = errors.New("nats: protocol exception, INFO not received") - ErrReconnectBufExceeded = errors.New("nats: outbound buffer limit exceeded") - ErrInvalidConnection = errors.New("nats: invalid connection") - ErrInvalidMsg = errors.New("nats: invalid message or message nil") - ErrInvalidArg = errors.New("nats: invalid argument") - ErrInvalidContext = errors.New("nats: invalid context") - ErrNoDeadlineContext = errors.New("nats: context requires a deadline") - ErrNoEchoNotSupported = errors.New("nats: no echo option not supported by this server") - ErrClientIDNotSupported = errors.New("nats: client ID not supported by this server") - ErrUserButNoSigCB = errors.New("nats: user callback defined without a signature handler") - ErrNkeyButNoSigCB = errors.New("nats: nkey defined without a signature handler") - ErrNoUserCB = errors.New("nats: user callback not defined") - ErrNkeyAndUser = errors.New("nats: user callback and nkey defined") - ErrNkeysNotSupported = errors.New("nats: nkeys not supported by the server") - ErrStaleConnection = errors.New("nats: " + STALE_CONNECTION) - ErrTokenAlreadySet = errors.New("nats: token and token handler both set") - ErrMsgNotBound = errors.New("nats: message is not bound to subscription/connection") - ErrMsgNoReply = errors.New("nats: message does not have a reply") - ErrClientIPNotSupported = errors.New("nats: client IP not supported by this server") - ErrDisconnected = errors.New("nats: server is disconnected") - ErrHeadersNotSupported = errors.New("nats: headers not supported by this server") - ErrBadHeaderMsg = errors.New("nats: message could not decode headers") - ErrNoResponders = errors.New("nats: no responders available for request") - ErrNoContextOrTimeout = errors.New("nats: no context or timeout given") - ErrPullModeNotAllowed = errors.New("nats: pull based not supported") - ErrJetStreamNotEnabledForAccount = errors.New("nats: jetstream not enabled for this account") - ErrJetStreamNotEnabled = errors.New("nats: jetstream not enabled") - ErrJetStreamBadPre = errors.New("nats: jetstream api prefix not valid") - ErrNoStreamResponse = errors.New("nats: no response from stream") - ErrNotJSMessage = errors.New("nats: not a jetstream message") - ErrInvalidStreamName = errors.New("nats: invalid stream name") - ErrInvalidConsumerName = errors.New("nats: invalid consumer name") - ErrNoMatchingStream = errors.New("nats: no stream matches subject") - ErrSubjectMismatch = errors.New("nats: subject does not match consumer") - ErrContextAndTimeout = errors.New("nats: context and timeout can not both be set") - ErrInvalidJSAck = errors.New("nats: invalid jetstream publish response") - ErrMultiStreamUnsupported = errors.New("nats: multiple streams are not supported") - ErrStreamConfigRequired = errors.New("nats: stream configuration is required") - ErrStreamNameRequired = errors.New("nats: stream name is required") - ErrStreamNotFound = errors.New("nats: stream not found") - ErrConsumerNotFound = errors.New("nats: consumer not found") - ErrConsumerNameRequired = errors.New("nats: consumer name is required") - ErrConsumerConfigRequired = errors.New("nats: consumer configuration is required") - ErrStreamSnapshotConfigRequired = errors.New("nats: stream snapshot configuration is required") - ErrDeliverSubjectRequired = errors.New("nats: deliver subject is required") - ErrPullSubscribeToPushConsumer = errors.New("nats: cannot pull subscribe to push based consumer") - ErrPullSubscribeRequired = errors.New("nats: must use pull subscribe to bind to pull based consumer") - ErrConsumerNotActive = errors.New("nats: consumer not active") - ErrConsumerNameAlreadyInUse = errors.New("nats: consumer name already in use") - ErrMsgNotFound = errors.New("nats: message not found") - ErrMsgAlreadyAckd = errors.New("nats: message was already acknowledged") - ErrCantAckIfConsumerAckNone = errors.New("nats: cannot acknowledge a message for a consumer with AckNone policy") - ErrStreamInfoMaxSubjects = errors.New("nats: subject details would exceed maximum allowed") - ErrStreamNameAlreadyInUse = errors.New("nats: stream name already in use") - ErrMaxConnectionsExceeded = errors.New("nats: server maximum connections exceeded") - ErrBadRequest = errors.New("nats: bad request") - ErrConnectionNotTLS = errors.New("nats: connection is not tls") + ErrConnectionClosed = errors.New("nats: connection closed") + ErrConnectionDraining = errors.New("nats: connection draining") + ErrDrainTimeout = errors.New("nats: draining connection timed out") + ErrConnectionReconnecting = errors.New("nats: connection reconnecting") + ErrSecureConnRequired = errors.New("nats: secure connection required") + ErrSecureConnWanted = errors.New("nats: secure connection not available") + ErrBadSubscription = errors.New("nats: invalid subscription") + ErrTypeSubscription = errors.New("nats: invalid subscription type") + ErrBadSubject = errors.New("nats: invalid subject") + ErrBadQueueName = errors.New("nats: invalid queue name") + ErrSlowConsumer = errors.New("nats: slow consumer, messages dropped") + ErrTimeout = errors.New("nats: timeout") + ErrBadTimeout = errors.New("nats: timeout invalid") + ErrAuthorization = errors.New("nats: authorization violation") + ErrAuthExpired = errors.New("nats: authentication expired") + ErrAuthRevoked = errors.New("nats: authentication revoked") + ErrAccountAuthExpired = errors.New("nats: account authentication expired") + ErrNoServers = errors.New("nats: no servers available for connection") + ErrJsonParse = errors.New("nats: connect message, json parse error") + ErrChanArg = errors.New("nats: argument needs to be a channel type") + ErrMaxPayload = errors.New("nats: maximum payload exceeded") + ErrMaxMessages = errors.New("nats: maximum messages delivered") + ErrSyncSubRequired = errors.New("nats: illegal call on an async subscription") + ErrMultipleTLSConfigs = errors.New("nats: multiple tls.Configs not allowed") + ErrNoInfoReceived = errors.New("nats: protocol exception, INFO not received") + ErrReconnectBufExceeded = errors.New("nats: outbound buffer limit exceeded") + ErrInvalidConnection = errors.New("nats: invalid connection") + ErrInvalidMsg = errors.New("nats: invalid message or message nil") + ErrInvalidArg = errors.New("nats: invalid argument") + ErrInvalidContext = errors.New("nats: invalid context") + ErrNoDeadlineContext = errors.New("nats: context requires a deadline") + ErrNoEchoNotSupported = errors.New("nats: no echo option not supported by this server") + ErrClientIDNotSupported = errors.New("nats: client ID not supported by this server") + ErrUserButNoSigCB = errors.New("nats: user callback defined without a signature handler") + ErrNkeyButNoSigCB = errors.New("nats: nkey defined without a signature handler") + ErrNoUserCB = errors.New("nats: user callback not defined") + ErrNkeyAndUser = errors.New("nats: user callback and nkey defined") + ErrNkeysNotSupported = errors.New("nats: nkeys not supported by the server") + ErrStaleConnection = errors.New("nats: " + STALE_CONNECTION) + ErrTokenAlreadySet = errors.New("nats: token and token handler both set") + ErrMsgNotBound = errors.New("nats: message is not bound to subscription/connection") + ErrMsgNoReply = errors.New("nats: message does not have a reply") + ErrClientIPNotSupported = errors.New("nats: client IP not supported by this server") + ErrDisconnected = errors.New("nats: server is disconnected") + ErrHeadersNotSupported = errors.New("nats: headers not supported by this server") + ErrBadHeaderMsg = errors.New("nats: message could not decode headers") + ErrNoResponders = errors.New("nats: no responders available for request") + ErrNoContextOrTimeout = errors.New("nats: no context or timeout given") + ErrPullModeNotAllowed = errors.New("nats: pull based not supported") + ErrJetStreamBadPre = errors.New("nats: jetstream api prefix not valid") + ErrNoStreamResponse = errors.New("nats: no response from stream") + ErrNotJSMessage = errors.New("nats: not a jetstream message") + ErrInvalidStreamName = errors.New("nats: invalid stream name") + ErrInvalidConsumerName = errors.New("nats: invalid consumer name") + ErrNoMatchingStream = errors.New("nats: no stream matches subject") + ErrSubjectMismatch = errors.New("nats: subject does not match consumer") + ErrContextAndTimeout = errors.New("nats: context and timeout can not both be set") + ErrInvalidJSAck = errors.New("nats: invalid jetstream publish response") + ErrMultiStreamUnsupported = errors.New("nats: multiple streams are not supported") + ErrStreamConfigRequired = errors.New("nats: stream configuration is required") + ErrStreamNameRequired = errors.New("nats: stream name is required") + ErrStreamNotFound = errors.New("nats: stream not found") + ErrConsumerNotFound = errors.New("nats: consumer not found") + ErrConsumerNameRequired = errors.New("nats: consumer name is required") + ErrConsumerConfigRequired = errors.New("nats: consumer configuration is required") + ErrStreamSnapshotConfigRequired = errors.New("nats: stream snapshot configuration is required") + ErrDeliverSubjectRequired = errors.New("nats: deliver subject is required") + ErrPullSubscribeToPushConsumer = errors.New("nats: cannot pull subscribe to push based consumer") + ErrPullSubscribeRequired = errors.New("nats: must use pull subscribe to bind to pull based consumer") + ErrConsumerNameAlreadyInUse = errors.New("nats: consumer name already in use") + ErrConsumerNotActive = errors.New("nats: consumer not active") + ErrMsgNotFound = errors.New("nats: message not found") + ErrMsgAlreadyAckd = errors.New("nats: message was already acknowledged") + ErrCantAckIfConsumerAckNone = errors.New("nats: cannot acknowledge a message for a consumer with AckNone policy") + ErrStreamInfoMaxSubjects = errors.New("nats: subject details would exceed maximum allowed") + ErrStreamNameAlreadyInUse = errors.New("nats: stream name already in use") + ErrMaxConnectionsExceeded = errors.New("nats: server maximum connections exceeded") + ErrBadRequest = errors.New("nats: bad request") + ErrConnectionNotTLS = errors.New("nats: connection is not tls") // DEPRECATED: ErrInvalidDurableName is no longer returned and will be removed in future releases // Use ErrInvalidConsumerName instead diff --git a/test/js_test.go b/test/js_test.go index 423718538..8687c8af0 100644 --- a/test/js_test.go +++ b/test/js_test.go @@ -87,9 +87,18 @@ func TestJetStreamNotAccountEnabled(t *testing.T) { nc, js := jsClient(t, s) defer nc.Close() - if _, err := js.AccountInfo(); err != nats.ErrJetStreamNotEnabledForAccount { + _, err := js.AccountInfo() + if err != nats.ErrJetStreamNotEnabledForAccount { t.Fatalf("Did not get the proper error, got %v", err) } + jserr, ok := err.(nats.JetStreamAPIError) + if !ok { + t.Fatal("Expected a JetStreamAPIError") + } + expected := nats.JSErrCodeJetStreamNotEnabledForAccount + if jserr.ErrorCode() != nats.ErrorCode(expected) { + t.Fatalf("Expected: %v, got: %v", expected, jserr.ErrorCode()) + } } func TestJetStreamPublish(t *testing.T) { From aad6326c8a6d9ae2f0062552faa6b6898771de44 Mon Sep 17 00:00:00 2001 From: Waldemar Quevedo Date: Sun, 14 Aug 2022 00:21:29 -0700 Subject: [PATCH 2/7] js: leverage more errors package internals Signed-off-by: Waldemar Quevedo --- jsm.go | 37 ++++++++++++++++++++++++++++------- nats.go | 4 ---- test/js_test.go | 52 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 11 deletions(-) diff --git a/jsm.go b/jsm.go index 14e61b40c..ca203b9bc 100644 --- a/jsm.go +++ b/jsm.go @@ -161,19 +161,36 @@ type APIError struct { Description string `json:"description,omitempty"` } +// Error prints the JetStream API error code and description +func (e *APIError) Error() string { + return fmt.Sprintf("nats: API error %d: %s", e.ErrorCode, e.Description) +} + +// Is matches against an APIError. +func (e *APIError) Is(err error) bool { + // Extract internal APIError to match against. + var aerr *APIError + ok := errors.As(err, &aerr) + if !ok { + return ok + } + return e.ErrorCode == aerr.ErrorCode +} + // JetStreamAPIError is an error result from making a request to the // JetStream API. type JetStreamAPIError interface { Code() int ErrorCode() ErrorCode Description() string - Error() string + error } type jsAPIError struct { code int errorCode ErrorCode description string + message string } func (err *jsAPIError) Code() int { @@ -185,11 +202,22 @@ func (err *jsAPIError) ErrorCode() ErrorCode { } func (err *jsAPIError) Description() string { + if err.description == "" { + return err.message + } return err.description } func (err *jsAPIError) Error() string { - return err.description + return fmt.Sprintf("nats: %v", err.message) +} + +func (err *jsAPIError) Unwrap() error { + return &APIError{ + Code: err.Code(), + ErrorCode: err.ErrorCode(), + Description: err.Description(), + } } // apiResponse is a standard response from the JetStream JSON API @@ -266,11 +294,6 @@ const ( JSErrCodeMessageNotFound ErrorCode = 10037 ) -// Error prints the JetStream API error code and description -func (e *APIError) Error() string { - return fmt.Sprintf("nats: API error %d: %s", e.ErrorCode, e.Description) -} - var ( // ErrJetStreamNotEnabled is an error returned when JetStream is not enabled. ErrJetStreamNotEnabledForAccount JetStreamAPIError = &jsAPIError{errorCode: JSErrCodeJetStreamNotEnabledForAccount, description: "nats: jetstream not enabled for account"} diff --git a/nats.go b/nats.go index 15e29ca2f..30dff008d 100644 --- a/nats.go +++ b/nats.go @@ -169,10 +169,6 @@ var ( ErrMaxConnectionsExceeded = errors.New("nats: server maximum connections exceeded") ErrBadRequest = errors.New("nats: bad request") ErrConnectionNotTLS = errors.New("nats: connection is not tls") - - // DEPRECATED: ErrInvalidDurableName is no longer returned and will be removed in future releases - // Use ErrInvalidConsumerName instead - ErrInvalidDurableName = errors.New("nats: invalid durable name") ) func init() { diff --git a/test/js_test.go b/test/js_test.go index 8687c8af0..10614ccf8 100644 --- a/test/js_test.go +++ b/test/js_test.go @@ -88,9 +88,23 @@ func TestJetStreamNotAccountEnabled(t *testing.T) { defer nc.Close() _, err := js.AccountInfo() + // check directly to var (backwards compatible) if err != nats.ErrJetStreamNotEnabledForAccount { t.Fatalf("Did not get the proper error, got %v", err) } + + // matching via errors.Is + if ok := errors.Is(err, nats.ErrJetStreamNotEnabled); !ok { + t.Fatal("Expected ErrJetStreamNotEnabled") + } + + // matching wrapped via error.Is + err2 := fmt.Errorf("custom error: %w", nats.ErrJetStreamNotEnabled) + if ok := errors.Is(err2, nats.ErrJetStreamNotEnabled); !ok { + t.Fatal("Expected wrapped ErrJetStreamNotEnabled") + } + + // via classic type assertion. jserr, ok := err.(nats.JetStreamAPIError) if !ok { t.Fatal("Expected a JetStreamAPIError") @@ -99,6 +113,44 @@ func TestJetStreamNotAccountEnabled(t *testing.T) { if jserr.ErrorCode() != nats.ErrorCode(expected) { t.Fatalf("Expected: %v, got: %v", expected, jserr.ErrorCode()) } + + // matching to interface via errors.As(...) + var apierr nats.JetStreamAPIError + ok = errors.As(err, &apierr) + if !ok { + t.Fatal("Expected a JetStreamAPIError") + } + if apierr.ErrorCode() != expected { + t.Fatalf("Expected: %v, got: %v", expected, apierr.ErrorCode()) + } + expectedMessage := "nats: jetstream not enabled" + if apierr.Error() != expectedMessage { + t.Fatalf("Expected: %v, got: %v", expectedMessage, apierr.Error()) + } + + // matching arbitrary custom error via errors.Is(...) + customErr := &nats.APIError{ErrorCode: expected} + if ok := errors.Is(customErr, nats.ErrJetStreamNotEnabled); !ok { + t.Fatal("Expected wrapped ErrJetStreamNotEnabled") + } + customErr = &nats.APIError{ErrorCode: 1} + if ok := errors.Is(customErr, nats.ErrJetStreamNotEnabled); ok { + t.Fatal("Expected to not match ErrJetStreamNotEnabled") + } + + // matching to concrete type via errors.As(...) + var aerr *nats.APIError + ok = errors.As(err, &aerr) + if !ok { + t.Fatal("Expected an APIError") + } + if aerr.ErrorCode != expected { + t.Fatalf("Expected: %v, got: %v", expected, aerr.ErrorCode) + } + expectedMessage = "nats: API error 10039: jetstream not enabled" + if aerr.Error() != expectedMessage { + t.Fatalf("Expected: %v, got: %v", expectedMessage, apierr.Error()) + } } func TestJetStreamPublish(t *testing.T) { From a89ee0023eed8f11ea0c184d76c2e130f2f27647 Mon Sep 17 00:00:00 2001 From: Waldemar Quevedo Date: Sun, 14 Aug 2022 09:35:48 -0700 Subject: [PATCH 3/7] js: change to use errors.Is Signed-off-by: Waldemar Quevedo --- jsm.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/jsm.go b/jsm.go index ca203b9bc..82cb5d348 100644 --- a/jsm.go +++ b/jsm.go @@ -327,10 +327,9 @@ func (js *js) AccountInfo(opts ...JSOpt) (*AccountInfo, error) { return nil, err } if info.Error != nil { - // Check based on error code instead of description match. - if info.Error.ErrorCode == JSErrCodeJetStreamNotEnabledForAccount { - err = ErrJetStreamNotEnabledForAccount - } else if info.Error.ErrorCode == JSErrCodeJetStreamNotEnabled { + var err error + // Internally checks based on error code instead of description match. + if errors.Is(info.Error, ErrJetStreamNotEnabledForAccount) { err = ErrJetStreamNotEnabled } else { err = &jsAPIError{ From 21ea50a4e0ff1f8fbb1cdf4dd69d10d0d43f2658 Mon Sep 17 00:00:00 2001 From: Waldemar Quevedo Date: Sun, 14 Aug 2022 11:19:13 -0700 Subject: [PATCH 4/7] js: smaller JetStreamError interface, make JetStreamAPIError concrete type Signed-off-by: Waldemar Quevedo --- jsm.go | 86 +++++++++++++++++-------------------------------- nats.go | 14 ++++++-- test/js_test.go | 40 +++++++++++++---------- 3 files changed, 65 insertions(+), 75 deletions(-) diff --git a/jsm.go b/jsm.go index 82cb5d348..9bb7a7909 100644 --- a/jsm.go +++ b/jsm.go @@ -154,22 +154,22 @@ type ExternalStream struct { DeliverPrefix string `json:"deliver"` } -// APIError is included in all API responses if there was an error. -type APIError struct { +// JetStreamAPIError is included in all API responses if there was an error. +type JetStreamAPIError struct { Code int `json:"code"` ErrorCode ErrorCode `json:"err_code"` Description string `json:"description,omitempty"` } // Error prints the JetStream API error code and description -func (e *APIError) Error() string { +func (e *JetStreamAPIError) Error() string { return fmt.Sprintf("nats: API error %d: %s", e.ErrorCode, e.Description) } -// Is matches against an APIError. -func (e *APIError) Is(err error) bool { +// Is matches against an JetStreamAPIError. +func (e *JetStreamAPIError) Is(err error) bool { // Extract internal APIError to match against. - var aerr *APIError + var aerr *JetStreamAPIError ok := errors.As(err, &aerr) if !ok { return ok @@ -177,53 +177,37 @@ func (e *APIError) Is(err error) bool { return e.ErrorCode == aerr.ErrorCode } -// JetStreamAPIError is an error result from making a request to the -// JetStream API. -type JetStreamAPIError interface { - Code() int - ErrorCode() ErrorCode - Description() string +// JetStreamError is an error result that happens when using JetStream. +type JetStreamError interface { + APIError() *JetStreamAPIError error } -type jsAPIError struct { - code int - errorCode ErrorCode - description string - message string +type jsError struct { + apiErr *JetStreamAPIError + message string } -func (err *jsAPIError) Code() int { - return err.code +func (err *jsError) APIError() *JetStreamAPIError { + return err.apiErr } -func (err *jsAPIError) ErrorCode() ErrorCode { - return err.errorCode -} - -func (err *jsAPIError) Description() string { - if err.description == "" { - return err.message +func (err *jsError) Error() string { + if err.apiErr != nil && err.apiErr.Description != "" { + return fmt.Sprintf("nats: %v", err.apiErr.Description) } - return err.description -} - -func (err *jsAPIError) Error() string { return fmt.Sprintf("nats: %v", err.message) } -func (err *jsAPIError) Unwrap() error { - return &APIError{ - Code: err.Code(), - ErrorCode: err.ErrorCode(), - Description: err.Description(), - } +func (err *jsError) Unwrap() error { + // Allow matching to embedded APIError in case there is one. + return err.apiErr } // apiResponse is a standard response from the JetStream JSON API type apiResponse struct { - Type string `json:"type"` - Error *APIError `json:"error,omitempty"` + Type string `json:"type"` + Error *JetStreamAPIError `json:"error,omitempty"` } // apiPaged includes variables used to create paged responses from the JSON API @@ -294,14 +278,6 @@ const ( JSErrCodeMessageNotFound ErrorCode = 10037 ) -var ( - // ErrJetStreamNotEnabled is an error returned when JetStream is not enabled. - ErrJetStreamNotEnabledForAccount JetStreamAPIError = &jsAPIError{errorCode: JSErrCodeJetStreamNotEnabledForAccount, description: "nats: jetstream not enabled for account"} - - // ErrJetStreamNotEnabledForAccount is an error returned when JetStream is not enabled for an account. - ErrJetStreamNotEnabled JetStreamAPIError = &jsAPIError{errorCode: JSErrCodeJetStreamNotEnabled, description: "nats: jetstream not enabled"} -) - // AccountInfo retrieves info about the JetStream usage from the current account. // If JetStream is not enabled, this will return ErrJetStreamNotEnabled // Other errors can happen but are generally considered retryable @@ -330,12 +306,10 @@ func (js *js) AccountInfo(opts ...JSOpt) (*AccountInfo, error) { var err error // Internally checks based on error code instead of description match. if errors.Is(info.Error, ErrJetStreamNotEnabledForAccount) { - err = ErrJetStreamNotEnabled + err = ErrJetStreamNotEnabledForAccount } else { - err = &jsAPIError{ - code: info.Error.Code, - errorCode: info.Error.ErrorCode, - description: info.Error.Description, + err = &jsError{ + apiErr: info.Error, } } return nil, err @@ -829,11 +803,11 @@ type StreamInfo struct { // StreamSourceInfo shows information about an upstream stream source. type StreamSourceInfo struct { - Name string `json:"name"` - Lag uint64 `json:"lag"` - Active time.Duration `json:"active"` - External *ExternalStream `json:"external"` - Error *APIError `json:"error"` + Name string `json:"name"` + Lag uint64 `json:"lag"` + Active time.Duration `json:"active"` + External *ExternalStream `json:"external"` + Error *JetStreamAPIError `json:"error"` } // StreamState is information about the given stream. diff --git a/nats.go b/nats.go index 30dff008d..7c9264c1f 100644 --- a/nats.go +++ b/nats.go @@ -153,14 +153,13 @@ var ( ErrStreamNameRequired = errors.New("nats: stream name is required") ErrStreamNotFound = errors.New("nats: stream not found") ErrConsumerNotFound = errors.New("nats: consumer not found") + ErrConsumerNameAlreadyInUse = errors.New("nats: consumer name already in use") ErrConsumerNameRequired = errors.New("nats: consumer name is required") ErrConsumerConfigRequired = errors.New("nats: consumer configuration is required") ErrStreamSnapshotConfigRequired = errors.New("nats: stream snapshot configuration is required") ErrDeliverSubjectRequired = errors.New("nats: deliver subject is required") ErrPullSubscribeToPushConsumer = errors.New("nats: cannot pull subscribe to push based consumer") ErrPullSubscribeRequired = errors.New("nats: must use pull subscribe to bind to pull based consumer") - ErrConsumerNameAlreadyInUse = errors.New("nats: consumer name already in use") - ErrConsumerNotActive = errors.New("nats: consumer not active") ErrMsgNotFound = errors.New("nats: message not found") ErrMsgAlreadyAckd = errors.New("nats: message was already acknowledged") ErrCantAckIfConsumerAckNone = errors.New("nats: cannot acknowledge a message for a consumer with AckNone policy") @@ -171,6 +170,17 @@ var ( ErrConnectionNotTLS = errors.New("nats: connection is not tls") ) +var ( + // ErrJetStreamNotEnabled is an error returned when JetStream is not enabled for an account. + ErrJetStreamNotEnabled JetStreamError = &jsError{apiErr: &JetStreamAPIError{ErrorCode: JSErrCodeJetStreamNotEnabled, Description: "jetstream not enabled"}} + + // ErrJetStreamNotEnabledForAccount is an error returned when JetStream is not enabled for an account. + ErrJetStreamNotEnabledForAccount JetStreamError = &jsError{apiErr: &JetStreamAPIError{ErrorCode: JSErrCodeJetStreamNotEnabledForAccount, Description: "jetstream not enabled"}} + + // ErrConsumerNotActive is an error returned when consumer is not active. + ErrConsumerNotActive JetStreamError = &jsError{message: "consumer not active"} +) + func init() { rand.Seed(time.Now().UnixNano()) } diff --git a/test/js_test.go b/test/js_test.go index 10614ccf8..df42a155c 100644 --- a/test/js_test.go +++ b/test/js_test.go @@ -94,8 +94,8 @@ func TestJetStreamNotAccountEnabled(t *testing.T) { } // matching via errors.Is - if ok := errors.Is(err, nats.ErrJetStreamNotEnabled); !ok { - t.Fatal("Expected ErrJetStreamNotEnabled") + if ok := errors.Is(err, nats.ErrJetStreamNotEnabledForAccount); !ok { + t.Fatal("Expected ErrJetStreamNotEnabledForAccount") } // matching wrapped via error.Is @@ -105,23 +105,29 @@ func TestJetStreamNotAccountEnabled(t *testing.T) { } // via classic type assertion. - jserr, ok := err.(nats.JetStreamAPIError) + jserr, ok := err.(nats.JetStreamError) if !ok { - t.Fatal("Expected a JetStreamAPIError") + t.Fatal("Expected a JetStreamError") } expected := nats.JSErrCodeJetStreamNotEnabledForAccount - if jserr.ErrorCode() != nats.ErrorCode(expected) { - t.Fatalf("Expected: %v, got: %v", expected, jserr.ErrorCode()) + if jserr.APIError().ErrorCode != expected { + t.Fatalf("Expected: %v, got: %v", expected, jserr.APIError().ErrorCode) + } + if jserr.APIError() == nil { + t.Fatal("Expected APIError") } // matching to interface via errors.As(...) - var apierr nats.JetStreamAPIError + var apierr nats.JetStreamError ok = errors.As(err, &apierr) if !ok { - t.Fatal("Expected a JetStreamAPIError") + t.Fatal("Expected a JetStreamError") + } + if apierr.APIError() == nil { + t.Fatal("Expected APIError") } - if apierr.ErrorCode() != expected { - t.Fatalf("Expected: %v, got: %v", expected, apierr.ErrorCode()) + if apierr.APIError().ErrorCode != expected { + t.Fatalf("Expected: %v, got: %v", expected, apierr.APIError().ErrorCode) } expectedMessage := "nats: jetstream not enabled" if apierr.Error() != expectedMessage { @@ -129,17 +135,17 @@ func TestJetStreamNotAccountEnabled(t *testing.T) { } // matching arbitrary custom error via errors.Is(...) - customErr := &nats.APIError{ErrorCode: expected} - if ok := errors.Is(customErr, nats.ErrJetStreamNotEnabled); !ok { - t.Fatal("Expected wrapped ErrJetStreamNotEnabled") + customErr := &nats.JetStreamAPIError{ErrorCode: expected} + if ok := errors.Is(customErr, nats.ErrJetStreamNotEnabledForAccount); !ok { + t.Fatal("Expected wrapped ErrJetStreamNotEnabledForAccount") } - customErr = &nats.APIError{ErrorCode: 1} - if ok := errors.Is(customErr, nats.ErrJetStreamNotEnabled); ok { + customErr = &nats.JetStreamAPIError{ErrorCode: 1} + if ok := errors.Is(customErr, nats.ErrJetStreamNotEnabledForAccount); ok { t.Fatal("Expected to not match ErrJetStreamNotEnabled") } // matching to concrete type via errors.As(...) - var aerr *nats.APIError + var aerr *nats.JetStreamAPIError ok = errors.As(err, &aerr) if !ok { t.Fatal("Expected an APIError") @@ -4894,7 +4900,7 @@ func testJetStreamMirror_Source(t *testing.T, nodes ...*jsServer) { if err == nil { t.Fatal("Unexpected success") } - apiErr := &nats.APIError{} + apiErr := &nats.JetStreamAPIError{} if !errors.As(err, &apiErr) || apiErr.ErrorCode != 10093 { t.Fatalf("Expected API error 10093; got: %v", err) } From 36b86626f100b4bcf16203e1e6e034f4f945d716 Mon Sep 17 00:00:00 2001 From: Waldemar Quevedo Date: Mon, 22 Aug 2022 11:14:19 -0700 Subject: [PATCH 5/7] js: switch to APIError Signed-off-by: Waldemar Quevedo --- jsm.go | 32 ++++++++++++++++---------------- nats.go | 4 ++-- test/js_test.go | 8 ++++---- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/jsm.go b/jsm.go index 9bb7a7909..b4b29c92b 100644 --- a/jsm.go +++ b/jsm.go @@ -154,22 +154,22 @@ type ExternalStream struct { DeliverPrefix string `json:"deliver"` } -// JetStreamAPIError is included in all API responses if there was an error. -type JetStreamAPIError struct { +// APIError is included in all API responses if there was an error. +type APIError struct { Code int `json:"code"` ErrorCode ErrorCode `json:"err_code"` Description string `json:"description,omitempty"` } // Error prints the JetStream API error code and description -func (e *JetStreamAPIError) Error() string { +func (e *APIError) Error() string { return fmt.Sprintf("nats: API error %d: %s", e.ErrorCode, e.Description) } -// Is matches against an JetStreamAPIError. -func (e *JetStreamAPIError) Is(err error) bool { +// Is matches against an APIError. +func (e *APIError) Is(err error) bool { // Extract internal APIError to match against. - var aerr *JetStreamAPIError + var aerr *APIError ok := errors.As(err, &aerr) if !ok { return ok @@ -179,16 +179,16 @@ func (e *JetStreamAPIError) Is(err error) bool { // JetStreamError is an error result that happens when using JetStream. type JetStreamError interface { - APIError() *JetStreamAPIError + APIError() *APIError error } type jsError struct { - apiErr *JetStreamAPIError + apiErr *APIError message string } -func (err *jsError) APIError() *JetStreamAPIError { +func (err *jsError) APIError() *APIError { return err.apiErr } @@ -206,8 +206,8 @@ func (err *jsError) Unwrap() error { // apiResponse is a standard response from the JetStream JSON API type apiResponse struct { - Type string `json:"type"` - Error *JetStreamAPIError `json:"error,omitempty"` + Type string `json:"type"` + Error *APIError `json:"error,omitempty"` } // apiPaged includes variables used to create paged responses from the JSON API @@ -803,11 +803,11 @@ type StreamInfo struct { // StreamSourceInfo shows information about an upstream stream source. type StreamSourceInfo struct { - Name string `json:"name"` - Lag uint64 `json:"lag"` - Active time.Duration `json:"active"` - External *ExternalStream `json:"external"` - Error *JetStreamAPIError `json:"error"` + Name string `json:"name"` + Lag uint64 `json:"lag"` + Active time.Duration `json:"active"` + External *ExternalStream `json:"external"` + Error *APIError `json:"error"` } // StreamState is information about the given stream. diff --git a/nats.go b/nats.go index 7c9264c1f..f3ecdc4e8 100644 --- a/nats.go +++ b/nats.go @@ -172,10 +172,10 @@ var ( var ( // ErrJetStreamNotEnabled is an error returned when JetStream is not enabled for an account. - ErrJetStreamNotEnabled JetStreamError = &jsError{apiErr: &JetStreamAPIError{ErrorCode: JSErrCodeJetStreamNotEnabled, Description: "jetstream not enabled"}} + ErrJetStreamNotEnabled JetStreamError = &jsError{apiErr: &APIError{ErrorCode: JSErrCodeJetStreamNotEnabled, Description: "jetstream not enabled"}} // ErrJetStreamNotEnabledForAccount is an error returned when JetStream is not enabled for an account. - ErrJetStreamNotEnabledForAccount JetStreamError = &jsError{apiErr: &JetStreamAPIError{ErrorCode: JSErrCodeJetStreamNotEnabledForAccount, Description: "jetstream not enabled"}} + ErrJetStreamNotEnabledForAccount JetStreamError = &jsError{apiErr: &APIError{ErrorCode: JSErrCodeJetStreamNotEnabledForAccount, Description: "jetstream not enabled"}} // ErrConsumerNotActive is an error returned when consumer is not active. ErrConsumerNotActive JetStreamError = &jsError{message: "consumer not active"} diff --git a/test/js_test.go b/test/js_test.go index df42a155c..86babe57d 100644 --- a/test/js_test.go +++ b/test/js_test.go @@ -135,17 +135,17 @@ func TestJetStreamNotAccountEnabled(t *testing.T) { } // matching arbitrary custom error via errors.Is(...) - customErr := &nats.JetStreamAPIError{ErrorCode: expected} + customErr := &nats.APIError{ErrorCode: expected} if ok := errors.Is(customErr, nats.ErrJetStreamNotEnabledForAccount); !ok { t.Fatal("Expected wrapped ErrJetStreamNotEnabledForAccount") } - customErr = &nats.JetStreamAPIError{ErrorCode: 1} + customErr = &nats.APIError{ErrorCode: 1} if ok := errors.Is(customErr, nats.ErrJetStreamNotEnabledForAccount); ok { t.Fatal("Expected to not match ErrJetStreamNotEnabled") } // matching to concrete type via errors.As(...) - var aerr *nats.JetStreamAPIError + var aerr *nats.APIError ok = errors.As(err, &aerr) if !ok { t.Fatal("Expected an APIError") @@ -4900,7 +4900,7 @@ func testJetStreamMirror_Source(t *testing.T, nodes ...*jsServer) { if err == nil { t.Fatal("Unexpected success") } - apiErr := &nats.JetStreamAPIError{} + apiErr := &nats.APIError{} if !errors.As(err, &apiErr) || apiErr.ErrorCode != 10093 { t.Fatalf("Expected API error 10093; got: %v", err) } From 8fee091099a0aa8672701df247841780db965079 Mon Sep 17 00:00:00 2001 From: Waldemar Quevedo Date: Mon, 22 Aug 2022 11:28:05 -0700 Subject: [PATCH 6/7] js: make an APIError implement the interface Signed-off-by: Waldemar Quevedo --- jsm.go | 12 +++++++----- test/js_test.go | 7 +++++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/jsm.go b/jsm.go index b4b29c92b..a64c8572e 100644 --- a/jsm.go +++ b/jsm.go @@ -166,6 +166,11 @@ func (e *APIError) Error() string { return fmt.Sprintf("nats: API error %d: %s", e.ErrorCode, e.Description) } +// APIError implements the JetStreamError interface. +func (e *APIError) APIError() *APIError { + return e +} + // Is matches against an APIError. func (e *APIError) Is(err error) bool { // Extract internal APIError to match against. @@ -303,14 +308,11 @@ func (js *js) AccountInfo(opts ...JSOpt) (*AccountInfo, error) { return nil, err } if info.Error != nil { - var err error + var err JetStreamError = info.Error + // Internally checks based on error code instead of description match. if errors.Is(info.Error, ErrJetStreamNotEnabledForAccount) { err = ErrJetStreamNotEnabledForAccount - } else { - err = &jsError{ - apiErr: info.Error, - } } return nil, err } diff --git a/test/js_test.go b/test/js_test.go index 86babe57d..2d6180d13 100644 --- a/test/js_test.go +++ b/test/js_test.go @@ -134,6 +134,9 @@ func TestJetStreamNotAccountEnabled(t *testing.T) { t.Fatalf("Expected: %v, got: %v", expectedMessage, apierr.Error()) } + // an APIError also implements the JetStreamError interface. + var _ nats.JetStreamError = &nats.APIError{} + // matching arbitrary custom error via errors.Is(...) customErr := &nats.APIError{ErrorCode: expected} if ok := errors.Is(customErr, nats.ErrJetStreamNotEnabledForAccount); !ok { @@ -143,6 +146,10 @@ func TestJetStreamNotAccountEnabled(t *testing.T) { if ok := errors.Is(customErr, nats.ErrJetStreamNotEnabledForAccount); ok { t.Fatal("Expected to not match ErrJetStreamNotEnabled") } + var cerr nats.JetStreamError + if ok := errors.As(customErr, &cerr); !ok { + t.Fatal("Expected custom error to be a JetStreamError") + } // matching to concrete type via errors.As(...) var aerr *nats.APIError From f7eaca6f25d2984b582d8ccba825f8169b659970 Mon Sep 17 00:00:00 2001 From: Piotr Piotrowski Date: Tue, 23 Aug 2022 13:11:59 +0200 Subject: [PATCH 7/7] Convert errors to JetStreamError and move to separate file --- js.go | 6 +- jserrors.go | 185 +++++++++++++++++++++++++++++++++++++ jsm.go | 97 +++---------------- nats.go | 138 ++++++++++----------------- test/js_test.go | 241 ++++++++++++++++++++++++++++++------------------ 5 files changed, 401 insertions(+), 266 deletions(-) create mode 100644 jserrors.go diff --git a/js.go b/js.go index c596fd132..bb827f9ea 100644 --- a/js.go +++ b/js.go @@ -1693,7 +1693,7 @@ func (js *js) subscribe(subj, queue string, cb MsgHandler, ch chan *Msg, isSync, hasHeartbeats = info.Config.Heartbeat > 0 } } else { - if cinfo.Error.ErrorCode == JSErrCodeStreamNotFound { + if errors.Is(cinfo.Error, ErrStreamNotFound) { return nil, ErrStreamNotFound } return nil, cinfo.Error @@ -2772,10 +2772,10 @@ func (js *js) getConsumerInfoContext(ctx context.Context, stream, consumer strin return nil, err } if info.Error != nil { - if info.Error.ErrorCode == JSErrCodeConsumerNotFound { + if errors.Is(info.Error, ErrConsumerNotFound) { return nil, ErrConsumerNotFound } - if info.Error.ErrorCode == JSErrCodeStreamNotFound { + if errors.Is(info.Error, ErrStreamNotFound) { return nil, ErrStreamNotFound } return nil, info.Error diff --git a/jserrors.go b/jserrors.go new file mode 100644 index 000000000..a6f2dea49 --- /dev/null +++ b/jserrors.go @@ -0,0 +1,185 @@ +// Copyright 2020-2022 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package nats + +import ( + "errors" + "fmt" +) + +var ( + // API errors + + // ErrJetStreamNotEnabled is an error returned when JetStream is not enabled for an account. + ErrJetStreamNotEnabled JetStreamError = &jsError{apiErr: &APIError{ErrorCode: JSErrCodeJetStreamNotEnabled, Description: "jetstream not enabled", Code: 503}} + + // ErrJetStreamNotEnabledForAccount is an error returned when JetStream is not enabled for an account. + ErrJetStreamNotEnabledForAccount JetStreamError = &jsError{apiErr: &APIError{ErrorCode: JSErrCodeJetStreamNotEnabledForAccount, Description: "jetstream not enabled for account", Code: 503}} + + // ErrStreamNotFound is an error returned when stream with given name does not exist. + ErrStreamNotFound JetStreamError = &jsError{apiErr: &APIError{ErrorCode: JSErrCodeStreamNotFound, Description: "stream not found", Code: 404}} + + // ErrStreamNameAlreadyInUse is returned when a stream with given name already exists and has a different configuration + ErrStreamNameAlreadyInUse JetStreamError = &jsError{apiErr: &APIError{ErrorCode: JSErrCodeStreamNameInUse, Description: "stream name already in use", Code: 400}} + + // ErrConsumerNotFound is an error returned when consumer with given name does not exist. + ErrConsumerNotFound JetStreamError = &jsError{apiErr: &APIError{ErrorCode: JSErrCodeConsumerNotFound, Description: "consumer not found", Code: 404}} + + // ErrMsgNotFound is returned when message with provided sequence number does npt exist. + ErrMsgNotFound JetStreamError = &jsError{apiErr: &APIError{ErrorCode: JSErrCodeMessageNotFound, Description: "message not found", Code: 404}} + + // ErrBadRequest is returned when invalid request is sent to JetStream API. + ErrBadRequest JetStreamError = &jsError{apiErr: &APIError{ErrorCode: JSErrCodeBadRequest, Description: "bad request", Code: 400}} + + // Client errors + + // ErrConsumerNotFound is an error returned when consumer with given name does not exist. + ErrConsumerNameAlreadyInUse JetStreamError = &jsError{message: "consumer name already in use"} + + // ErrConsumerNotActive is an error returned when consumer is not active. + ErrConsumerNotActive JetStreamError = &jsError{message: "consumer not active"} + + // ErrInvalidJSAck is returned when JetStream ack from message publish is invalid. + ErrInvalidJSAck JetStreamError = &jsError{message: "invalid jetstream publish response"} + + // ErrStreamConfigRequired is returned when empty stream configuration is supplied to add/update stream. + ErrStreamConfigRequired JetStreamError = &jsError{message: "stream configuration is required"} + + // ErrStreamNameRequired is returned when the provided stream name is empty. + ErrStreamNameRequired JetStreamError = &jsError{message: "stream name is required"} + + // ErrConsumerNameRequired is returned when the provided consumer durable name is empty, + ErrConsumerNameRequired JetStreamError = &jsError{message: "consumer name is required"} + + // ErrConsumerConfigRequired is returned when empty consumer consuguration is supplied to add/update consumer. + ErrConsumerConfigRequired JetStreamError = &jsError{message: "consumer configuration is required"} + + // ErrPullSubscribeToPushConsumer is returned when attempting to use PullSubscribe on push consumer. + ErrPullSubscribeToPushConsumer JetStreamError = &jsError{message: "cannot pull subscribe to push based consumer"} + + // ErrPullSubscribeRequired is returned when attempting to use subscribe methods not suitable for pull consumers for pull consumers. + ErrPullSubscribeRequired JetStreamError = &jsError{message: "must use pull subscribe to bind to pull based consumer"} + + // ErrMsgAlreadyAckd is returned when attempting to acknowledge message more than once. + ErrMsgAlreadyAckd JetStreamError = &jsError{message: "message was already acknowledged"} + + // ErrNoStreamResponse is returned when there is no response from stream (e.g. no responders error). + ErrNoStreamResponse JetStreamError = &jsError{message: "no response from stream"} + + // ErrNotJSMessage is returned when attempting to get metadata from non JetStream message . + ErrNotJSMessage JetStreamError = &jsError{message: "not a jetstream message"} + + // ErrInvalidStreamName is returned when the provided stream name is invalid (contains '.'). + ErrInvalidStreamName JetStreamError = &jsError{message: "invalid stream name"} + + // ErrInvalidConsumerName is returned when the provided consumer name is invalid (contains '.'). + ErrInvalidConsumerName JetStreamError = &jsError{message: "invalid consumer name"} + + // ErrNoMatchingStream is returned when stream lookup by subject is unsuccessful. + ErrNoMatchingStream JetStreamError = &jsError{message: "no stream matches subject"} + + // ErrSubjectMismatch is returned when the provided subject does not match consumer's filter subject. + ErrSubjectMismatch JetStreamError = &jsError{message: "subject does not match consumer"} + + // ErrContextAndTimeout is returned when attempting to use both context and timeout. + ErrContextAndTimeout JetStreamError = &jsError{message: "context and timeout can not both be set"} + + // ErrCantAckIfConsumerAckNone is returned when attempting to ack a message for consumer with AckNone policy set. + ErrCantAckIfConsumerAckNone JetStreamError = &jsError{message: "cannot acknowledge a message for a consumer with AckNone policy"} + + // DEPRECATED: ErrInvalidDurableName is no longer returned and will be removed in future releases + // Use ErrInvalidConsumerName instead + ErrInvalidDurableName = errors.New("nats: invalid durable name") +) + +// Error code represents JetStream error codes returned by the API +type ErrorCode uint16 + +const ( + JSErrCodeJetStreamNotEnabledForAccount ErrorCode = 10039 + JSErrCodeJetStreamNotEnabled ErrorCode = 10076 + + JSErrCodeStreamNotFound ErrorCode = 10059 + JSErrCodeStreamNameInUse ErrorCode = 10058 + + JSErrCodeConsumerNotFound ErrorCode = 10014 + JSErrCodeConsumerNameExists ErrorCode = 10013 + JSErrCodeConsumerAlreadyExists ErrorCode = 10105 + + JSErrCodeMessageNotFound ErrorCode = 10037 + + JSErrCodeBadRequest ErrorCode = 10003 +) + +// APIError is included in all API responses if there was an error. +type APIError struct { + Code int `json:"code"` + ErrorCode ErrorCode `json:"err_code"` + Description string `json:"description,omitempty"` +} + +// Error prints the JetStream API error code and description +func (e *APIError) Error() string { + return fmt.Sprintf("nats: API error %d: %s", e.ErrorCode, e.Description) +} + +// APIError implements the JetStreamError interface. +func (e *APIError) APIError() *APIError { + return e +} + +// Is matches against an APIError. +func (e *APIError) Is(err error) bool { + if e == nil { + return false + } + // Extract internal APIError to match against. + var aerr *APIError + ok := errors.As(err, &aerr) + if !ok { + return ok + } + return e.ErrorCode == aerr.ErrorCode +} + +// JetStreamError is an error result that happens when using JetStream. +// In case of client-side error, `APIError()` returns nil +type JetStreamError interface { + APIError() *APIError + error +} + +type jsError struct { + apiErr *APIError + message string +} + +func (err *jsError) APIError() *APIError { + return err.apiErr +} + +func (err *jsError) Error() string { + if err.apiErr != nil && err.apiErr.Description != "" { + return err.apiErr.Error() + } + return fmt.Sprintf("nats: %s", err.message) +} + +func (err *jsError) Unwrap() error { + // Allow matching to embedded APIError in case there is one. + if err.apiErr == nil { + return nil + } + return err.apiErr +} diff --git a/jsm.go b/jsm.go index a64c8572e..c04308315 100644 --- a/jsm.go +++ b/jsm.go @@ -154,61 +154,6 @@ type ExternalStream struct { DeliverPrefix string `json:"deliver"` } -// APIError is included in all API responses if there was an error. -type APIError struct { - Code int `json:"code"` - ErrorCode ErrorCode `json:"err_code"` - Description string `json:"description,omitempty"` -} - -// Error prints the JetStream API error code and description -func (e *APIError) Error() string { - return fmt.Sprintf("nats: API error %d: %s", e.ErrorCode, e.Description) -} - -// APIError implements the JetStreamError interface. -func (e *APIError) APIError() *APIError { - return e -} - -// Is matches against an APIError. -func (e *APIError) Is(err error) bool { - // Extract internal APIError to match against. - var aerr *APIError - ok := errors.As(err, &aerr) - if !ok { - return ok - } - return e.ErrorCode == aerr.ErrorCode -} - -// JetStreamError is an error result that happens when using JetStream. -type JetStreamError interface { - APIError() *APIError - error -} - -type jsError struct { - apiErr *APIError - message string -} - -func (err *jsError) APIError() *APIError { - return err.apiErr -} - -func (err *jsError) Error() string { - if err.apiErr != nil && err.apiErr.Description != "" { - return fmt.Sprintf("nats: %v", err.apiErr.Description) - } - return fmt.Sprintf("nats: %v", err.message) -} - -func (err *jsError) Unwrap() error { - // Allow matching to embedded APIError in case there is one. - return err.apiErr -} - // apiResponse is a standard response from the JetStream JSON API type apiResponse struct { Type string `json:"type"` @@ -267,22 +212,6 @@ type accountInfoResponse struct { AccountInfo } -type ErrorCode uint16 - -const ( - JSErrCodeJetStreamNotEnabledForAccount ErrorCode = 10039 - JSErrCodeJetStreamNotEnabled ErrorCode = 10076 - - JSErrCodeStreamNotFound ErrorCode = 10059 - JSErrCodeStreamNameInUse ErrorCode = 10058 - - JSErrCodeConsumerNotFound ErrorCode = 10014 - JSErrCodeConsumerNameExists ErrorCode = 10013 - JSErrCodeConsumerAlreadyExists ErrorCode = 10105 - - JSErrCodeMessageNotFound ErrorCode = 10037 -) - // AccountInfo retrieves info about the JetStream usage from the current account. // If JetStream is not enabled, this will return ErrJetStreamNotEnabled // Other errors can happen but are generally considered retryable @@ -308,13 +237,11 @@ func (js *js) AccountInfo(opts ...JSOpt) (*AccountInfo, error) { return nil, err } if info.Error != nil { - var err JetStreamError = info.Error - // Internally checks based on error code instead of description match. if errors.Is(info.Error, ErrJetStreamNotEnabledForAccount) { - err = ErrJetStreamNotEnabledForAccount + return nil, ErrJetStreamNotEnabledForAccount } - return nil, err + return nil, info.Error } return &info.AccountInfo, nil @@ -399,10 +326,10 @@ func (js *js) upsertConsumer(stream string, cfg *ConsumerConfig, opts ...JSOpt) return nil, err } if info.Error != nil { - if info.Error.ErrorCode == JSErrCodeStreamNotFound { + if errors.Is(info.Error, ErrStreamNotFound) { return nil, ErrStreamNotFound } - if info.Error.ErrorCode == JSErrCodeConsumerNotFound { + if errors.Is(info.Error, ErrConsumerNotFound) { return nil, ErrConsumerNotFound } return nil, info.Error @@ -465,7 +392,7 @@ func (js *js) DeleteConsumer(stream, consumer string, opts ...JSOpt) error { } if resp.Error != nil { - if resp.Error.ErrorCode == JSErrCodeConsumerNotFound { + if errors.Is(resp.Error, ErrConsumerNotFound) { return ErrConsumerNotFound } return resp.Error @@ -736,7 +663,7 @@ func (js *js) AddStream(cfg *StreamConfig, opts ...JSOpt) (*StreamInfo, error) { return nil, err } if resp.Error != nil { - if resp.Error.ErrorCode == JSErrCodeStreamNameInUse { + if errors.Is(resp.Error, ErrStreamNameAlreadyInUse) { return nil, ErrStreamNameAlreadyInUse } return nil, resp.Error @@ -784,7 +711,7 @@ func (js *js) StreamInfo(stream string, opts ...JSOpt) (*StreamInfo, error) { return nil, err } if resp.Error != nil { - if resp.Error.ErrorCode == JSErrCodeStreamNotFound { + if errors.Is(resp.Error, ErrStreamNotFound) { return nil, ErrStreamNotFound } return nil, resp.Error @@ -876,7 +803,7 @@ func (js *js) UpdateStream(cfg *StreamConfig, opts ...JSOpt) (*StreamInfo, error return nil, err } if resp.Error != nil { - if resp.Error.ErrorCode == JSErrCodeStreamNotFound { + if errors.Is(resp.Error, ErrStreamNotFound) { return nil, ErrStreamNotFound } return nil, resp.Error @@ -914,7 +841,7 @@ func (js *js) DeleteStream(name string, opts ...JSOpt) error { } if resp.Error != nil { - if resp.Error.ErrorCode == JSErrCodeStreamNotFound { + if errors.Is(resp.Error, ErrStreamNotFound) { return ErrStreamNotFound } return resp.Error @@ -1018,10 +945,10 @@ func (js *js) getMsg(name string, mreq *apiMsgGetRequest, opts ...JSOpt) (*RawSt return nil, err } if resp.Error != nil { - if resp.Error.ErrorCode == JSErrCodeMessageNotFound { + if errors.Is(resp.Error, ErrMsgNotFound) { return nil, ErrMsgNotFound } - if resp.Error.ErrorCode == JSErrCodeStreamNotFound { + if errors.Is(resp.Error, ErrStreamNotFound) { return nil, ErrStreamNotFound } return nil, resp.Error @@ -1232,7 +1159,7 @@ func (js *js) purgeStream(stream string, req *StreamPurgeRequest, opts ...JSOpt) return err } if resp.Error != nil { - if resp.Error.Code == 400 { + if errors.Is(resp.Error, ErrBadRequest) { return fmt.Errorf("%w: %s", ErrBadRequest, "invalid purge request body") } return resp.Error diff --git a/nats.go b/nats.go index f3ecdc4e8..3143b9ac3 100644 --- a/nats.go +++ b/nats.go @@ -90,95 +90,55 @@ const ( // Errors var ( - ErrConnectionClosed = errors.New("nats: connection closed") - ErrConnectionDraining = errors.New("nats: connection draining") - ErrDrainTimeout = errors.New("nats: draining connection timed out") - ErrConnectionReconnecting = errors.New("nats: connection reconnecting") - ErrSecureConnRequired = errors.New("nats: secure connection required") - ErrSecureConnWanted = errors.New("nats: secure connection not available") - ErrBadSubscription = errors.New("nats: invalid subscription") - ErrTypeSubscription = errors.New("nats: invalid subscription type") - ErrBadSubject = errors.New("nats: invalid subject") - ErrBadQueueName = errors.New("nats: invalid queue name") - ErrSlowConsumer = errors.New("nats: slow consumer, messages dropped") - ErrTimeout = errors.New("nats: timeout") - ErrBadTimeout = errors.New("nats: timeout invalid") - ErrAuthorization = errors.New("nats: authorization violation") - ErrAuthExpired = errors.New("nats: authentication expired") - ErrAuthRevoked = errors.New("nats: authentication revoked") - ErrAccountAuthExpired = errors.New("nats: account authentication expired") - ErrNoServers = errors.New("nats: no servers available for connection") - ErrJsonParse = errors.New("nats: connect message, json parse error") - ErrChanArg = errors.New("nats: argument needs to be a channel type") - ErrMaxPayload = errors.New("nats: maximum payload exceeded") - ErrMaxMessages = errors.New("nats: maximum messages delivered") - ErrSyncSubRequired = errors.New("nats: illegal call on an async subscription") - ErrMultipleTLSConfigs = errors.New("nats: multiple tls.Configs not allowed") - ErrNoInfoReceived = errors.New("nats: protocol exception, INFO not received") - ErrReconnectBufExceeded = errors.New("nats: outbound buffer limit exceeded") - ErrInvalidConnection = errors.New("nats: invalid connection") - ErrInvalidMsg = errors.New("nats: invalid message or message nil") - ErrInvalidArg = errors.New("nats: invalid argument") - ErrInvalidContext = errors.New("nats: invalid context") - ErrNoDeadlineContext = errors.New("nats: context requires a deadline") - ErrNoEchoNotSupported = errors.New("nats: no echo option not supported by this server") - ErrClientIDNotSupported = errors.New("nats: client ID not supported by this server") - ErrUserButNoSigCB = errors.New("nats: user callback defined without a signature handler") - ErrNkeyButNoSigCB = errors.New("nats: nkey defined without a signature handler") - ErrNoUserCB = errors.New("nats: user callback not defined") - ErrNkeyAndUser = errors.New("nats: user callback and nkey defined") - ErrNkeysNotSupported = errors.New("nats: nkeys not supported by the server") - ErrStaleConnection = errors.New("nats: " + STALE_CONNECTION) - ErrTokenAlreadySet = errors.New("nats: token and token handler both set") - ErrMsgNotBound = errors.New("nats: message is not bound to subscription/connection") - ErrMsgNoReply = errors.New("nats: message does not have a reply") - ErrClientIPNotSupported = errors.New("nats: client IP not supported by this server") - ErrDisconnected = errors.New("nats: server is disconnected") - ErrHeadersNotSupported = errors.New("nats: headers not supported by this server") - ErrBadHeaderMsg = errors.New("nats: message could not decode headers") - ErrNoResponders = errors.New("nats: no responders available for request") - ErrNoContextOrTimeout = errors.New("nats: no context or timeout given") - ErrPullModeNotAllowed = errors.New("nats: pull based not supported") - ErrJetStreamBadPre = errors.New("nats: jetstream api prefix not valid") - ErrNoStreamResponse = errors.New("nats: no response from stream") - ErrNotJSMessage = errors.New("nats: not a jetstream message") - ErrInvalidStreamName = errors.New("nats: invalid stream name") - ErrInvalidConsumerName = errors.New("nats: invalid consumer name") - ErrNoMatchingStream = errors.New("nats: no stream matches subject") - ErrSubjectMismatch = errors.New("nats: subject does not match consumer") - ErrContextAndTimeout = errors.New("nats: context and timeout can not both be set") - ErrInvalidJSAck = errors.New("nats: invalid jetstream publish response") - ErrMultiStreamUnsupported = errors.New("nats: multiple streams are not supported") - ErrStreamConfigRequired = errors.New("nats: stream configuration is required") - ErrStreamNameRequired = errors.New("nats: stream name is required") - ErrStreamNotFound = errors.New("nats: stream not found") - ErrConsumerNotFound = errors.New("nats: consumer not found") - ErrConsumerNameAlreadyInUse = errors.New("nats: consumer name already in use") - ErrConsumerNameRequired = errors.New("nats: consumer name is required") - ErrConsumerConfigRequired = errors.New("nats: consumer configuration is required") - ErrStreamSnapshotConfigRequired = errors.New("nats: stream snapshot configuration is required") - ErrDeliverSubjectRequired = errors.New("nats: deliver subject is required") - ErrPullSubscribeToPushConsumer = errors.New("nats: cannot pull subscribe to push based consumer") - ErrPullSubscribeRequired = errors.New("nats: must use pull subscribe to bind to pull based consumer") - ErrMsgNotFound = errors.New("nats: message not found") - ErrMsgAlreadyAckd = errors.New("nats: message was already acknowledged") - ErrCantAckIfConsumerAckNone = errors.New("nats: cannot acknowledge a message for a consumer with AckNone policy") - ErrStreamInfoMaxSubjects = errors.New("nats: subject details would exceed maximum allowed") - ErrStreamNameAlreadyInUse = errors.New("nats: stream name already in use") - ErrMaxConnectionsExceeded = errors.New("nats: server maximum connections exceeded") - ErrBadRequest = errors.New("nats: bad request") - ErrConnectionNotTLS = errors.New("nats: connection is not tls") -) - -var ( - // ErrJetStreamNotEnabled is an error returned when JetStream is not enabled for an account. - ErrJetStreamNotEnabled JetStreamError = &jsError{apiErr: &APIError{ErrorCode: JSErrCodeJetStreamNotEnabled, Description: "jetstream not enabled"}} - - // ErrJetStreamNotEnabledForAccount is an error returned when JetStream is not enabled for an account. - ErrJetStreamNotEnabledForAccount JetStreamError = &jsError{apiErr: &APIError{ErrorCode: JSErrCodeJetStreamNotEnabledForAccount, Description: "jetstream not enabled"}} - - // ErrConsumerNotActive is an error returned when consumer is not active. - ErrConsumerNotActive JetStreamError = &jsError{message: "consumer not active"} + ErrConnectionClosed = errors.New("nats: connection closed") + ErrConnectionDraining = errors.New("nats: connection draining") + ErrDrainTimeout = errors.New("nats: draining connection timed out") + ErrConnectionReconnecting = errors.New("nats: connection reconnecting") + ErrSecureConnRequired = errors.New("nats: secure connection required") + ErrSecureConnWanted = errors.New("nats: secure connection not available") + ErrBadSubscription = errors.New("nats: invalid subscription") + ErrTypeSubscription = errors.New("nats: invalid subscription type") + ErrBadSubject = errors.New("nats: invalid subject") + ErrBadQueueName = errors.New("nats: invalid queue name") + ErrSlowConsumer = errors.New("nats: slow consumer, messages dropped") + ErrTimeout = errors.New("nats: timeout") + ErrBadTimeout = errors.New("nats: timeout invalid") + ErrAuthorization = errors.New("nats: authorization violation") + ErrAuthExpired = errors.New("nats: authentication expired") + ErrAuthRevoked = errors.New("nats: authentication revoked") + ErrAccountAuthExpired = errors.New("nats: account authentication expired") + ErrNoServers = errors.New("nats: no servers available for connection") + ErrJsonParse = errors.New("nats: connect message, json parse error") + ErrChanArg = errors.New("nats: argument needs to be a channel type") + ErrMaxPayload = errors.New("nats: maximum payload exceeded") + ErrMaxMessages = errors.New("nats: maximum messages delivered") + ErrSyncSubRequired = errors.New("nats: illegal call on an async subscription") + ErrMultipleTLSConfigs = errors.New("nats: multiple tls.Configs not allowed") + ErrNoInfoReceived = errors.New("nats: protocol exception, INFO not received") + ErrReconnectBufExceeded = errors.New("nats: outbound buffer limit exceeded") + ErrInvalidConnection = errors.New("nats: invalid connection") + ErrInvalidMsg = errors.New("nats: invalid message or message nil") + ErrInvalidArg = errors.New("nats: invalid argument") + ErrInvalidContext = errors.New("nats: invalid context") + ErrNoDeadlineContext = errors.New("nats: context requires a deadline") + ErrNoEchoNotSupported = errors.New("nats: no echo option not supported by this server") + ErrClientIDNotSupported = errors.New("nats: client ID not supported by this server") + ErrUserButNoSigCB = errors.New("nats: user callback defined without a signature handler") + ErrNkeyButNoSigCB = errors.New("nats: nkey defined without a signature handler") + ErrNoUserCB = errors.New("nats: user callback not defined") + ErrNkeyAndUser = errors.New("nats: user callback and nkey defined") + ErrNkeysNotSupported = errors.New("nats: nkeys not supported by the server") + ErrStaleConnection = errors.New("nats: " + STALE_CONNECTION) + ErrTokenAlreadySet = errors.New("nats: token and token handler both set") + ErrMsgNotBound = errors.New("nats: message is not bound to subscription/connection") + ErrMsgNoReply = errors.New("nats: message does not have a reply") + ErrClientIPNotSupported = errors.New("nats: client IP not supported by this server") + ErrDisconnected = errors.New("nats: server is disconnected") + ErrHeadersNotSupported = errors.New("nats: headers not supported by this server") + ErrBadHeaderMsg = errors.New("nats: message could not decode headers") + ErrNoResponders = errors.New("nats: no responders available for request") + ErrMaxConnectionsExceeded = errors.New("nats: server maximum connections exceeded") + ErrConnectionNotTLS = errors.New("nats: connection is not tls") ) func init() { diff --git a/test/js_test.go b/test/js_test.go index 2d6180d13..46f644077 100644 --- a/test/js_test.go +++ b/test/js_test.go @@ -64,106 +64,169 @@ func TestJetStreamNotEnabled(t *testing.T) { } } -func TestJetStreamNotAccountEnabled(t *testing.T) { - conf := createConfFile(t, []byte(` - listen: 127.0.0.1:-1 - no_auth_user: rip - jetstream: {max_mem_store: 64GB, max_file_store: 10TB} - accounts: { - JS: { - jetstream: enabled - users: [ {user: dlc, password: foo} ] - }, - IU: { - users: [ {user: rip, password: bar} ] - }, +func TestJetStreamErrors(t *testing.T) { + t.Run("API error", func(t *testing.T) { + conf := createConfFile(t, []byte(` + listen: 127.0.0.1:-1 + no_auth_user: rip + jetstream: {max_mem_store: 64GB, max_file_store: 10TB} + accounts: { + JS: { + jetstream: enabled + users: [ {user: dlc, password: foo} ] + }, + IU: { + users: [ {user: rip, password: bar} ] + }, + } + `)) + defer os.Remove(conf) + + s, _ := RunServerWithConfig(conf) + defer shutdownJSServerAndRemoveStorage(t, s) + + nc, js := jsClient(t, s) + defer nc.Close() + + _, err := js.AccountInfo() + // check directly to var (backwards compatible) + if err != nats.ErrJetStreamNotEnabledForAccount { + t.Fatalf("Did not get the proper error, got %v", err) } - `)) - defer os.Remove(conf) - s, _ := RunServerWithConfig(conf) - defer shutdownJSServerAndRemoveStorage(t, s) + // matching via errors.Is + if ok := errors.Is(err, nats.ErrJetStreamNotEnabledForAccount); !ok { + t.Fatal("Expected ErrJetStreamNotEnabledForAccount") + } - nc, js := jsClient(t, s) - defer nc.Close() + // matching wrapped via error.Is + err2 := fmt.Errorf("custom error: %w", nats.ErrJetStreamNotEnabledForAccount) + if ok := errors.Is(err2, nats.ErrJetStreamNotEnabledForAccount); !ok { + t.Fatal("Expected wrapped ErrJetStreamNotEnabled") + } - _, err := js.AccountInfo() - // check directly to var (backwards compatible) - if err != nats.ErrJetStreamNotEnabledForAccount { - t.Fatalf("Did not get the proper error, got %v", err) - } + // via classic type assertion. + jserr, ok := err.(nats.JetStreamError) + if !ok { + t.Fatal("Expected a JetStreamError") + } + expected := nats.JSErrCodeJetStreamNotEnabledForAccount + if jserr.APIError().ErrorCode != expected { + t.Fatalf("Expected: %v, got: %v", expected, jserr.APIError().ErrorCode) + } + if jserr.APIError() == nil { + t.Fatal("Expected APIError") + } - // matching via errors.Is - if ok := errors.Is(err, nats.ErrJetStreamNotEnabledForAccount); !ok { - t.Fatal("Expected ErrJetStreamNotEnabledForAccount") - } + // matching to interface via errors.As(...) + var apierr nats.JetStreamError + ok = errors.As(err, &apierr) + if !ok { + t.Fatal("Expected a JetStreamError") + } + if apierr.APIError() == nil { + t.Fatal("Expected APIError") + } + if apierr.APIError().ErrorCode != expected { + t.Fatalf("Expected: %v, got: %v", expected, apierr.APIError().ErrorCode) + } + expectedMessage := "nats: API error 10039: jetstream not enabled for account" + if apierr.Error() != expectedMessage { + t.Fatalf("Expected: %v, got: %v", expectedMessage, apierr.Error()) + } - // matching wrapped via error.Is - err2 := fmt.Errorf("custom error: %w", nats.ErrJetStreamNotEnabled) - if ok := errors.Is(err2, nats.ErrJetStreamNotEnabled); !ok { - t.Fatal("Expected wrapped ErrJetStreamNotEnabled") - } + // an APIError also implements the JetStreamError interface. + var _ nats.JetStreamError = &nats.APIError{} - // via classic type assertion. - jserr, ok := err.(nats.JetStreamError) - if !ok { - t.Fatal("Expected a JetStreamError") - } - expected := nats.JSErrCodeJetStreamNotEnabledForAccount - if jserr.APIError().ErrorCode != expected { - t.Fatalf("Expected: %v, got: %v", expected, jserr.APIError().ErrorCode) - } - if jserr.APIError() == nil { - t.Fatal("Expected APIError") - } + // matching arbitrary custom error via errors.Is(...) + customErr := &nats.APIError{ErrorCode: expected} + if ok := errors.Is(customErr, nats.ErrJetStreamNotEnabledForAccount); !ok { + t.Fatal("Expected wrapped ErrJetStreamNotEnabledForAccount") + } + customErr = &nats.APIError{ErrorCode: 1} + if ok := errors.Is(customErr, nats.ErrJetStreamNotEnabledForAccount); ok { + t.Fatal("Expected to not match ErrJetStreamNotEnabled") + } + var cerr nats.JetStreamError + if ok := errors.As(customErr, &cerr); !ok { + t.Fatal("Expected custom error to be a JetStreamError") + } - // matching to interface via errors.As(...) - var apierr nats.JetStreamError - ok = errors.As(err, &apierr) - if !ok { - t.Fatal("Expected a JetStreamError") - } - if apierr.APIError() == nil { - t.Fatal("Expected APIError") - } - if apierr.APIError().ErrorCode != expected { - t.Fatalf("Expected: %v, got: %v", expected, apierr.APIError().ErrorCode) - } - expectedMessage := "nats: jetstream not enabled" - if apierr.Error() != expectedMessage { - t.Fatalf("Expected: %v, got: %v", expectedMessage, apierr.Error()) - } + // matching to concrete type via errors.As(...) + var aerr *nats.APIError + ok = errors.As(err, &aerr) + if !ok { + t.Fatal("Expected an APIError") + } + if aerr.ErrorCode != expected { + t.Fatalf("Expected: %v, got: %v", expected, aerr.ErrorCode) + } + expectedMessage = "nats: API error 10039: jetstream not enabled for account" + if aerr.Error() != expectedMessage { + t.Fatalf("Expected: %v, got: %v", expectedMessage, apierr.Error()) + } + }) - // an APIError also implements the JetStreamError interface. - var _ nats.JetStreamError = &nats.APIError{} + t.Run("test non-api error", func(t *testing.T) { + s := RunBasicJetStreamServer() + defer shutdownJSServerAndRemoveStorage(t, s) - // matching arbitrary custom error via errors.Is(...) - customErr := &nats.APIError{ErrorCode: expected} - if ok := errors.Is(customErr, nats.ErrJetStreamNotEnabledForAccount); !ok { - t.Fatal("Expected wrapped ErrJetStreamNotEnabledForAccount") - } - customErr = &nats.APIError{ErrorCode: 1} - if ok := errors.Is(customErr, nats.ErrJetStreamNotEnabledForAccount); ok { - t.Fatal("Expected to not match ErrJetStreamNotEnabled") - } - var cerr nats.JetStreamError - if ok := errors.As(customErr, &cerr); !ok { - t.Fatal("Expected custom error to be a JetStreamError") - } + nc, js := jsClient(t, s) + defer nc.Close() + + // stream with empty name + _, err := js.AddStream(&nats.StreamConfig{}) + if err == nil { + t.Fatalf("Expected error, got nil") + } + + // check directly to var (backwards compatible) + if err != nats.ErrStreamNameRequired { + t.Fatalf("Expected: %v; got: %v", nats.ErrInvalidStreamName, err) + } + + // matching via errors.Is + if ok := errors.Is(err, nats.ErrStreamNameRequired); !ok { + t.Fatalf("Expected: %v; got: %v", nats.ErrStreamNameRequired, err) + } + + // matching wrapped via error.Is + err2 := fmt.Errorf("custom error: %w", nats.ErrStreamNameRequired) + if ok := errors.Is(err2, nats.ErrStreamNameRequired); !ok { + t.Fatal("Expected wrapped ErrStreamNameRequired") + } + + // via classic type assertion. + jserr, ok := err.(nats.JetStreamError) + if !ok { + t.Fatal("Expected a JetStreamError") + } + if jserr.APIError() != nil { + t.Fatalf("Expected: empty APIError; got: %v", jserr.APIError()) + } + + // matching to interface via errors.As(...) + var jserr2 nats.JetStreamError + ok = errors.As(err, &jserr2) + if !ok { + t.Fatal("Expected a JetStreamError") + } + if jserr2.APIError() != nil { + t.Fatalf("Expected: empty APIError; got: %v", jserr2.APIError()) + } + expectedMessage := "nats: stream name is required" + if jserr2.Error() != expectedMessage { + t.Fatalf("Expected: %v, got: %v", expectedMessage, jserr2.Error()) + } + + // matching to concrete type via errors.As(...) + var aerr *nats.APIError + ok = errors.As(err, &aerr) + if ok { + t.Fatal("Expected ErrStreamNameRequired not to map to APIError") + } + }) - // matching to concrete type via errors.As(...) - var aerr *nats.APIError - ok = errors.As(err, &aerr) - if !ok { - t.Fatal("Expected an APIError") - } - if aerr.ErrorCode != expected { - t.Fatalf("Expected: %v, got: %v", expected, aerr.ErrorCode) - } - expectedMessage = "nats: API error 10039: jetstream not enabled" - if aerr.Error() != expectedMessage { - t.Fatalf("Expected: %v, got: %v", expectedMessage, apierr.Error()) - } } func TestJetStreamPublish(t *testing.T) {