From 164805ca9ced54889d2d30ba06260e5e7801efef Mon Sep 17 00:00:00 2001 From: Waldemar Quevedo Date: Tue, 23 Aug 2022 10:16:28 -0700 Subject: [PATCH] js: Add JetStreamError with API response details (#1047) * js: Add JetStreamError with more API response details * js: Convert errors to JetStreamError and move to separate file Signed-off-by: Waldemar Quevedo Co-authored-by: Piotr Piotrowski --- js.go | 6 +- jserrors.go | 185 ++++++++++++++++++++++++++++++++++++++++++++++++ jsm.go | 54 ++++---------- nats.go | 134 +++++++++++++---------------------- test/js_test.go | 181 ++++++++++++++++++++++++++++++++++++++++------ 5 files changed, 408 insertions(+), 152 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 e7bf8a88f..c04308315 100644 --- a/jsm.go +++ b/jsm.go @@ -154,13 +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"` -} - // apiResponse is a standard response from the JetStream JSON API type apiResponse struct { Type string `json:"type"` @@ -219,27 +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 -) - -// 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) -} - // 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,12 +237,10 @@ func (js *js) AccountInfo(opts ...JSOpt) (*AccountInfo, error) { return nil, err } if info.Error != nil { - if info.Error.ErrorCode == JSErrCodeJetStreamNotEnabledForAccount { + // Internally checks based on error code instead of description match. + if errors.Is(info.Error, ErrJetStreamNotEnabledForAccount) { return nil, ErrJetStreamNotEnabledForAccount } - if info.Error.ErrorCode == JSErrCodeJetStreamNotEnabled { - return nil, ErrJetStreamNotEnabled - } return nil, info.Error } @@ -356,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 @@ -422,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 @@ -693,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 @@ -741,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 @@ -833,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 @@ -871,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 @@ -975,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 @@ -1189,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 f2317380b..3143b9ac3 100644 --- a/nats.go +++ b/nats.go @@ -90,91 +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") - 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") - - // DEPRECATED: ErrInvalidDurableName is no longer returned and will be removed in future releases - // Use ErrInvalidConsumerName instead - ErrInvalidDurableName = errors.New("nats: invalid durable name") + 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 423718538..46f644077 100644 --- a/test/js_test.go +++ b/test/js_test.go @@ -64,32 +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") + } + + // 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 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()) + } + + // 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 { + 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 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()) + } + }) + + t.Run("test non-api error", func(t *testing.T) { + s := RunBasicJetStreamServer() + defer shutdownJSServerAndRemoveStorage(t, s) + + 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") + } + }) - if _, err := js.AccountInfo(); err != nats.ErrJetStreamNotEnabledForAccount { - t.Fatalf("Did not get the proper error, got %v", err) - } } func TestJetStreamPublish(t *testing.T) {