From 0d391b02ebc1131944ebfac39ab83a2d0cf1e6f9 Mon Sep 17 00:00:00 2001 From: "R.I.Pienaar" Date: Fri, 30 Apr 2021 15:03:51 +0200 Subject: [PATCH] richer api errors proposal Signed-off-by: R.I.Pienaar --- .github/workflows/go-test.yaml | 6 +- doc/README.md | 2 +- doc/adr/0001-jetstream-json-api-design.md | 51 +-- doc/adr/0007-error-codes.md | 142 +++++++ main.go | 2 + server/errors.go | 44 +-- server/errors.json | 460 ++++++++++++++++++++++ server/errors_gen.go | 116 ++++++ server/jetstream.go | 40 +- server/jetstream_api.go | 439 +++++++++------------ server/jetstream_cluster.go | 128 +++--- server/jetstream_cluster_test.go | 18 +- server/jetstream_errors.go | 105 +++++ server/jetstream_errors_generated.go | 306 ++++++++++++++ server/jetstream_errors_test.go | 98 +++++ server/jetstream_test.go | 41 +- server/mqtt.go | 58 ++- server/stream.go | 68 ++-- server/test_test.go | 12 + 19 files changed, 1622 insertions(+), 514 deletions(-) create mode 100644 doc/adr/0007-error-codes.md create mode 100644 server/errors.json create mode 100644 server/errors_gen.go create mode 100644 server/jetstream_errors.go create mode 100644 server/jetstream_errors_generated.go create mode 100644 server/jetstream_errors_test.go diff --git a/.github/workflows/go-test.yaml b/.github/workflows/go-test.yaml index b2149622..1b1df712 100644 --- a/.github/workflows/go-test.yaml +++ b/.github/workflows/go-test.yaml @@ -43,8 +43,8 @@ jobs: shell: bash --noprofile --norc -x -eo pipefail {0} run: | set -e - go test -i ./... - go test -v -run=TestNoRace --failfast -p=1 ./... + go test -vet=off -i ./... + go test -vet=off -v -run=TestNoRace --failfast -p=1 ./... # coverage via cov.sh disabled while just testing the waters - go test -v -race -p=1 --failfast ./... + go test -vet=off -v -race -p=1 --failfast ./... set +e diff --git a/doc/README.md b/doc/README.md index e65e7b83..87e45cdc 100644 --- a/doc/README.md +++ b/doc/README.md @@ -18,4 +18,4 @@ good starting point. We do not have a fixed format at present. ## ADR Statuses -Each ADR has a status, let's try to use `Proposed`, `Approved` and `Rejected`. +Each ADR has a status, let's try to use `Proposed`, `Approved` `Partially Implemented`, `Implemented` and `Rejected`. diff --git a/doc/adr/0001-jetstream-json-api-design.md b/doc/adr/0001-jetstream-json-api-design.md index 458420b3..1a88e828 100644 --- a/doc/adr/0001-jetstream-json-api-design.md +++ b/doc/adr/0001-jetstream-json-api-design.md @@ -7,25 +7,10 @@ Author: @ripienaar Partially Implemented -The JSON API has been updated, however the urlencoded replies have not been adopted. - ## Context At present, the API encoding consists of mixed text and JSON, we should improve consistency and error handling. -## Decision - -We anticipate many types of consumer for JetStream including very small devices via MQTT and those written in -languages without easy to use JSON support like C. - -We will make it easy for light-weight clients by using `+OK` and `-ERR` style prefixes for client interactions, -but the server management API will be JSON all the time. The rationale being that such small devices will most -likely have their Consumers and Streams provisioned as part of the platform and often might not care for the -additional context. - -In client responses we will use URL Encoding to provide additional context so multiple fields worth of -context can be provided. URL Encoding has [broad language support](https://www.rosettacode.org/wiki/URL_decoding). - ### Admin APIs #### Requests @@ -72,14 +57,15 @@ Successful Stream Info: } ``` -Stream Info Error: +Consumer Info Error: ```json { - "type": "io.nats.jetstream.api.v1.stream_info", + "type": "io.nats.jetstream.api.v1.consumer_info", "error": { - "description": "unknown stream bob", - "code": 400 + "description": "consumer not found", + "code": 404, + "error_code": 10059 } } ``` @@ -88,7 +74,12 @@ Here we have a minimally correct response with the additional error object. In the `error` struct we have `description` as a short human friendly explanation that should include enough context to identify what Stream or Consumer acted on and whatever else we feel will help the user while not sharing privileged account -information. `code` will be HTTP like, 400 human error, 500 server error etc. +information. These strings are not part of the API promises, we can update and re-word or translate them at any time. Programmatic +error handling should look at the `code` which will be HTTP like, 4xx human error, 5xx server error etc. Finally, the `error_code` +indicates the specific reason for the 404 - here `10059` means the stream did not exist, helping developers identify the +real underlying cause. + +More information about the `error_code` system can be found in [ADR-7](0007-error-codes.md). Ideally the error response includes a minimally valid body of what was requested but this can be very hard to implement correctly. @@ -106,17 +97,6 @@ Today the list API's just return `["ORDERS"]`, these will become: With the same `error` treatment when some error happens. -### Client Interactions - -To keep clients on small devices viable we will standardise our responses as in the examples below. - - * `+OK` everything is fine no additional context - * `+OK "stream=ORDERS&sequence=10"` everything is fine, we have additional data to pass using standard URL encoding - * `-ERR` something failed, we have no reason - * `-ERR "reason=reason%20for%20failure&stream=STREAM"` something failed, we have additional context to provide using standard URL encoding - -Additional information will be needed for example when there are overlapping Streams and one of them fail. - ## Implementation While implementing this in JetStream the following pattern emerged: @@ -128,8 +108,11 @@ type JSApiResponse struct { } type ApiError struct { - Code int `json:"code"` - Description string `json:"description,omitempty"` + Code int `json:"code"` + ErrCode int `json:"err_code,omitempty"` + Description string `json:"description,omitempty"` + URL string `json:"-"` + Help string `json:"-"` } type JSApiConsumerCreateResponse struct { @@ -169,4 +152,4 @@ Validating this in JSON Schema draft 7 is a bit awkward, not impossible and spec ## Consequences -URL Encoding does not carry data types and the response fields will need documenting. +URL Encoding does not carry data types, and the response fields will need documenting. diff --git a/doc/adr/0007-error-codes.md b/doc/adr/0007-error-codes.md new file mode 100644 index 00000000..40cf48c2 --- /dev/null +++ b/doc/adr/0007-error-codes.md @@ -0,0 +1,142 @@ +# 7. NATS Server Error Codes + +Date: 2021-05-12 +Author: @ripienaar + +## Status + +Partially Implemented in [#1811](https://github.com/nats-io/nats-server/issues/1811) + +The current focus is JetStream APIs, we will as a followup do a refactor and generalization and move onto other +areas of the server. + +## Context + +When a developer performs a Consumer Info API request she might get a 404 error, there is no way to know if this is +a 404 due to the Stream not existing or the Consumer not existing. The only way is to parse the returned error description +like `consumer not found`. Further several other error situations might arise which due to our code design would be surfaced +as a 404 when in fact it's more like a 5xx error - I/O errors and such. + +If users are parsing our error strings it means our error text form part of the Public API, we can never improve errors, +fix spelling errors or translate errors into other languages. + +This ADR describes an additional `error_code` that provides deeper context into the underlying cause of the 404. + +## Design + +We will adopt a numbering system for our errors where every error has a unique number within a range that indicates the +subsystem it belongs to. + +|Range|Description| +|-----|-----------| +|1xxxx|JetStream related errors| +|2xxxx|MQTT related errors| + +The JetStream API error will be adjusted like this initially with later work turning this into a more generic error +usable in other parts of the NATS Server code base. + +```go +// ApiError is included in all responses if there was an error. +type ApiError struct { + Code int `json:"code"` + ErrCode int `json:"err_code,omitempty"` + Description string `json:"description,omitempty"` + URL string `json:"-"` + Help string `json:"-"` +} +``` + +Here the `code` and `error_code` is what we'll consider part of the Public API with `description` being specifically +out of scope for SemVer protection and changes to these will not be considered a breaking change. + +The `ApiError` type will implement `error` and whenever it will be logged will append the code to the log line, for example: + +``` +stream not found (10059) +``` + +The `nats` CLI will have a lookup system like `nats error 10059` that will show details of this error including help, +urls and such. It will also assist in listing and searching errors. The same could be surfaced later in documentation +and other areas. + +## Using in code + +### Raising an error + +Here we raise a `stream not found` error without providing any additional context to the user, the constant is +`JSStreamNotFoundErr` from which you can guess it takes no printf style interpolations vs one that does which would +end in `...ErrF`: + +The go doc for this constant would also include the content of the error to assist via intellisense in your IDE. + +```go +err = doThing() +if err != nil { + return ApiErrors[JSStreamNotFoundErr] +} +``` + +If we have to do string interpolation of the error body, here the `JSStreamRestoreErrF` has the body +`"restore failed: {err}"`, the `NewT()` will simply use `strings.Replaces()` to create a new `ApiError` with the full string: + +```go +err = doRestore() +if err != nil { + return ApiErrors[JSStreamRestoreErrF].NewT("{err}", err) +} +``` + +If we had to handle an error that may be an `ApiError` or a traditional go error we can use the `ErrOrNew` function, +this will look at the result from `lookupConsumer()`, if it's an `ApiError` that error will be set else a `JSConsumerNotFoundErr` +will be created. Essentially the `lookupConsumer()` would return a `JSStreamNotFoundErr` if the stream does not exist else +a `JSConsumerNotFoundErr` or go error on I/O failure for example. + +```go +var resp = JSApiConsumerCreateResponse{ApiResponse: ApiResponse{Type: JSApiStreamCreateResponseType}} + +_, err = lookupConsumer(stream, consumer) +if err != nil { + resp.Error = ApiErrors[JSConsumerNotFoundErr].ErrOrNew(err) +} +``` + +### Testing Errors + +Should you need to determine if a error is of a specific kind (error code) this can be done using the `IsNatsErr()` function: + +```go +err = doThing() +if IsNatsErr(err, JSStreamNotFoundErr, JSConsumerNotFoundErr) { + // create the stream and consumer +} else if err !=nil{ + // other critical failure +} +``` + +## Maintaining the errors + +The file `errors.json` holds the data used to generate the error constants, lists etc + +```json +[ + { + "constant": "JSClusterPeerNotMemberErr", + "code": 400, + "error_code": 10040, + "description": "peer not a member" + }, + { + "constant": "JSNotEnabledErr", + "code": 503, + "error_code": 10039, + "description": "JetStream not enabled for account", + "help": "This error indicates that JetStream is not enabled either at a global level or at global and account level", + "url": "https://docs.nats.io/jetstream" + } +] +``` + +After editing this file run `go generate` in the top of the `nats-server` repo, and it will update the needed files. Check +in the result. + +When run this will verify that the `error_code` and `constant` is unique in each error diff --git a/main.go b/main.go index d1e005cf..f762b183 100644 --- a/main.go +++ b/main.go @@ -13,6 +13,8 @@ package main +//go:generate go run server/errors_gen.go + import ( "flag" "fmt" diff --git a/server/errors.go b/server/errors.go index ddad6423..f5db0c48 100644 --- a/server/errors.go +++ b/server/errors.go @@ -171,47 +171,11 @@ var ( ErrMalformedSubject = errors.New("malformed subject") // ErrSubscribePermissionViolation is returned when processing of a subscription fails due to permissions. - ErrSubscribePermissionViolation = errors.New("subscribe permission viloation") + ErrSubscribePermissionViolation = errors.New("subscribe permission violation") // ErrNoTransforms signals no subject transforms are available to map this subject. ErrNoTransforms = errors.New("no matching transforms available") - // ErrJetStreamNotEnabled is returned when JetStream is not enabled. - ErrJetStreamNotEnabled = errors.New("jetstream not enabled") - - // ErrJetStreamStreamNotFound is returned when a stream can not be found. - ErrJetStreamStreamNotFound = errors.New("stream not found") - - // ErrJetStreamStreamAlreadyUsed is returned when a stream name has already been taken. - ErrJetStreamStreamAlreadyUsed = errors.New("stream name already in use") - - // ErrJetStreamConsumerAlreadyUsed is returned when a consumer name has already been taken. - ErrJetStreamConsumerAlreadyUsed = errors.New("consumer name already in use") - - // ErrJetStreamNotEnabledForAccount is returned JetStream is not enabled for this account. - ErrJetStreamNotEnabledForAccount = errors.New("jetstream not enabled for account") - - // ErrJetStreamNotLeader is returned when issuing commands to a cluster on the wrong server. - ErrJetStreamNotLeader = errors.New("jetstream cluster can not handle request") - - // ErrJetStreamNotAssigned is returned when the resource (stream or consumer) is not assigned. - ErrJetStreamNotAssigned = errors.New("jetstream cluster not assigned to this server") - - // ErrJetStreamNotClustered is returned when a call requires clustering and we are not. - ErrJetStreamNotClustered = errors.New("jetstream not in clustered mode") - - // ErrJetStreamResourcesExceeded is returned when a call would exceed internal resource limits. - ErrJetStreamResourcesExceeded = errors.New("jetstream resources exceeded for server") - - // ErrStorageResourcesExceeded is returned when storage resources would be exceeded. - ErrStorageResourcesExceeded = errors.New("insufficient storage resources available") - - // ErrMemoryResourcesExceeded is returned when memory resources would be exceeded. - ErrMemoryResourcesExceeded = errors.New("insufficient memory resources available") - - // ErrReplicasNotSupported is returned when a stream with replicas > 1 in non-clustered mode. - ErrReplicasNotSupported = errors.New("replicas > 1 not supported in non-clustered mode") - // ErrCertNotPinned is returned when pinned certs are set and the certificate is not in it ErrCertNotPinned = errors.New("certificate not pinned") ) @@ -297,7 +261,7 @@ func NewErrorCtx(err error, format string, args ...interface{}) error { return &errCtx{err, fmt.Sprintf(format, args...)} } -// implement to work with errors.Is and errors.As +// Unwrap implement to work with errors.Is and errors.As func (e *errCtx) Unwrap() error { if e == nil { return nil @@ -313,7 +277,7 @@ func (e *errCtx) Context() string { return e.ctx } -// Return Error or, if type is right error and context +// UnpackIfErrorCtx return Error or, if type is right error and context func UnpackIfErrorCtx(err error) string { if e, ok := err.(*errCtx); ok { if _, ok := e.error.(*errCtx); ok { @@ -336,7 +300,7 @@ func errorsUnwrap(err error) error { return u.Unwrap() } -// implements: go 1.13 errors.Is(err, target error) bool +// ErrorIs implements: go 1.13 errors.Is(err, target error) bool // TODO replace with native code once we no longer support go1.12 func ErrorIs(err, target error) bool { // this is an outright copy of go 1.13 errors.Is(err, target error) bool diff --git a/server/errors.json b/server/errors.json new file mode 100644 index 00000000..c621f833 --- /dev/null +++ b/server/errors.json @@ -0,0 +1,460 @@ +[ + { + "constant": "JSClusterPeerNotMemberErr", + "code": 400, + "error_code": 10040, + "description": "peer not a member" + }, + { + "constant": "JSConsumerEphemeralWithDurableInSubjectErr", + "code": 400, + "error_code": 10019, + "description": "consumer expected to be ephemeral but detected a durable name set in subject" + }, + { + "constant": "JSStreamExternalDelPrefixOverlapsErrF", + "code": 400, + "error_code": 10022, + "description": "stream external delivery prefix {prefix} overlaps with stream subject {subject}" + }, + { + "constant": "JSAccountResourcesExceededErr", + "code": 400, + "error_code": 10002, + "description": "resource limits exceeded for account" + }, + { + "constant": "JSClusterNotAvailErr", + "code": 503, + "error_code": 10008, + "description": "JetStream system temporarily unavailable" + }, + { + "constant": "JSStreamSubjectOverlapErr", + "code": 500, + "error_code": 10065, + "description": "subjects overlap with an existing stream" + }, + { + "constant": "JSStreamWrongLastSequenceErrF", + "code": 400, + "error_code": 10071, + "description": "wrong last sequence: {seq}" + }, + { + "constant": "JSTemplateNameNotMatchSubjectErr", + "code": 400, + "error_code": 10073, + "description": "template name in subject does not match request" + }, + { + "constant": "JSClusterNoPeersErr", + "code": 400, + "error_code": 10005, + "description": "no suitable peers for placement" + }, + { + "constant": "JSConsumerEphemeralWithDurableNameErr", + "code": 400, + "error_code": 10020, + "description": "consumer expected to be ephemeral but a durable name was set in request" + }, + { + "constant": "JSInsufficientResourcesErr", + "code": 503, + "error_code": 10023, + "description": "insufficient resources" + }, + { + "constant": "JSMirrorMaxMessageSizeTooBigErr", + "code": 400, + "error_code": 10030, + "description": "stream mirror must have max message size >= source" + }, + { + "constant": "JSStreamTemplateDeleteErrF", + "code": 500, + "error_code": 10067, + "description": "{err}", + "comment": "Generic stream template deletion failed error string" + }, + { + "constant": "JSBadRequestErr", + "code": 400, + "error_code": 10003, + "description": "bad request" + }, + { + "constant": "JSClusterUnSupportFeatureErr", + "code": 503, + "error_code": 10036, + "description": "not currently supported in clustered mode" + }, + { + "constant": "JSConsumerNotFoundErr", + "code": 404, + "error_code": 10014, + "description": "consumer not found" + }, + { + "constant": "JSSourceMaxMessageSizeTooBigErr", + "code": 400, + "error_code": 10046, + "description": "stream source must have max message size >= target" + }, + { + "constant": "JSStreamAssignmentErrF", + "code": 500, + "error_code": 10048, + "description": "{err}", + "commend": "Generic stream assignment error string" + }, + { + "constant": "JSStreamMessageExceedsMaximumErr", + "code": 400, + "error_code": 10054, + "description": "message size exceeds maximum allowed" + }, + { + "constant": "JSStreamTemplateCreateErrF", + "code": 500, + "error_code": 10066, + "description": "{err}", + "comment": "Generic template creation failed string" + }, + { + "constant": "JSInvalidJSONErr", + "code": 400, + "error_code": 10025, + "description": "invalid JSON" + }, + { + "constant": "JSStreamInvalidExternalDeliverySubjErrF", + "code": 400, + "error_code": 10024, + "description": "stream external delivery prefix {prefix} must not contain wildcards" + }, + { + "constant": "JSStreamRestoreErrF", + "code": 500, + "error_code": 10062, + "description": "restore failed: {err}" + }, + { + "constant": "JSClusterIncompleteErr", + "code": 503, + "error_code": 10004, + "description": "incomplete results" + }, + { + "constant": "JSNoAccountErr", + "code": 503, + "error_code": 10035, + "description": "account not found" + }, + { + "constant": "JSRaftGeneralErrF", + "code": 500, + "error_code": 10041, + "description": "{err}", + "comment": "General RAFT error string" + }, + { + "constant": "JSRestoreSubscribeFailedErrF", + "code": 500, + "error_code": 10042, + "description": "JetStream unable to subscribe to restore snapshot {subject}: {err}" + }, + { + "constant": "JSStreamDeleteErrF", + "code": 500, + "error_code": 10050, + "description": "{err}", + "comment": "General stream deletion error string" + }, + { + "constant": "JSStreamExternalApiOverlapErrF", + "code": 400, + "error_code": 10021, + "description": "stream external api prefix {prefix} must not overlap with {subject}" + }, + { + "constant": "JSMirrorWithSubjectsErr", + "code": 400, + "error_code": 10034, + "description": "stream mirrors can not also contain subjects" + }, + { + "constant": "JSNotEnabledErr", + "code": 503, + "error_code": 10039, + "description": "JetStream not enabled for account", + "help": "This error indicates that JetStream is not enabled either at a global level or at global and account level" + }, + { + "constant": "JSSequenceNotFoundErrF", + "code": 400, + "error_code": 10043, + "description": "sequence {seq} not found" + }, + { + "constant": "JSStreamMirrorNotUpdatableErr", + "code": 400, + "error_code": 10055, + "description": "Mirror configuration can not be updated" + }, + { + "constant": "JSStreamSequenceNotMatchErr", + "code": 503, + "error_code": 10063, + "description": "expected stream sequence does not match" + }, + { + "constant": "JSStreamWrongLastMsgIDErrF", + "code": 400, + "error_code": 10070, + "description": "wrong last msg ID: {id}" + }, + { + "constant": "JSTempStorageFailedErr", + "code": 500, + "error_code": 10072, + "description": "JetStream unable to open temp storage for restore" + }, + { + "constant": "JSStorageResourcesExceededErr", + "code": 500, + "error_code": 10047, + "description": "insufficient storage resources available" + }, + { + "constant": "JSStreamMismatchErr", + "code": 400, + "error_code": 10056, + "description": "stream name in subject does not match request" + }, + { + "constant": "JSStreamNotMatchErr", + "code": 400, + "error_code": 10060, + "description": "expected stream does not match" + }, + { + "constant": "JSMirrorConsumerSetupFailedErrF", + "code": 500, + "error_code": 10029, + "description": "{err}", + "comment": "Generic mirror consumer setup failure string" + }, + { + "constant": "JSNotEmptyRequestErr", + "code": 400, + "error_code": 10038, + "description": "expected an empty request payload" + }, + { + "constant": "JSStreamNameExistErr", + "code": 400, + "error_code": 10058, + "description": "stream name already in use" + }, + { + "constant": "JSClusterTagsErr", + "code": 400, + "error_code": 10011, + "description": "tags placement not supported for operation" + }, + { + "constant": "JSMaximumConsumersLimitErr", + "code": 400, + "error_code": 10026, + "description": "maximum consumers exceeds account limit" + }, + { + "constant": "JSSourceConsumerSetupFailedErrF", + "code": 500, + "error_code": 10045, + "description": "{err}", + "comment": "General source consumer setup failure string" + }, + { + "constant": "JSConsumerCreateErrF", + "code": 500, + "error_code": 10012, + "description": "{err}", + "comment": "General consumer creation failure string" + }, + { + "constant": "JSConsumerDurableNameNotInSubjectErr", + "code": 400, + "error_code": 10016, + "description": "consumer expected to be durable but no durable name set in subject" + }, + { + "constant": "JSStreamLimitsErrF", + "code": 500, + "error_code": 10053, + "description": "{err}", + "comment": "General stream limits exceeded error string" + }, + { + "constant": "JSStreamReplicasNotUpdatableErr", + "code": 400, + "error_code": 10061, + "description": "Replicas configuration can not be updated" + }, + { + "constant": "JSStreamTemplateNotFoundErr", + "code": 404, + "error_code": 10068, + "description": "template not found" + }, + { + "constant": "JSClusterNotAssignedErr", + "code": 500, + "error_code": 10007, + "description": "JetStream cluster not assigned to this server" + }, + { + "constant": "JSClusterNotLeaderErr", + "code": 500, + "error_code": 10009, + "description": "JetStream cluster can not handle request" + }, + { + "constant": "JSConsumerNameExistErr", + "code": 400, + "error_code": 10013, + "description": "consumer name already in use" + }, + { + "constant": "JSMirrorWithSourcesErr", + "code": 400, + "error_code": 10031, + "description": "stream mirrors can not also contain other sources" + }, + { + "constant": "JSStreamNotFoundErr", + "code": 404, + "error_code": 10059, + "description": "stream not found" + }, + { + "constant": "JSClusterRequiredErr", + "code": 503, + "error_code": 10010, + "description": "JetStream clustering support required" + }, + { + "constant": "JSConsumerDurableNameNotSetErr", + "code": 400, + "error_code": 10018, + "description": "consumer expected to be durable but a durable name was not set" + }, + { + "constant": "JSMaximumStreamsLimitErr", + "code": 400, + "error_code": 10027, + "description": "maximum number of streams reached" + }, + { + "constant": "JSMirrorWithStartSeqAndTimeErr", + "code": 400, + "error_code": 10032, + "description": "stream mirrors can not have both start seq and start time configured" + }, + { + "constant": "JSStreamSnapshotErrF", + "code": 500, + "error_code": 10064, + "description": "snapshot failed: {err}" + }, + { + "constant": "JSStreamUpdateErrF", + "code": 500, + "error_code": 10069, + "description": "{err}", + "comment": "Generic stream update error string" + }, + { + "constant": "JSClusterNotActiveErr", + "code": 500, + "error_code": 10006, + "description": "JetStream not in clustered mode" + }, + { + "constant": "JSConsumerDurableNameNotMatchSubjectErr", + "code": 400, + "error_code": 10017, + "description": "consumer name in subject does not match durable name in request" + }, + { + "constant": "JSMemoryResourcesExceededErr", + "code": 500, + "error_code": 10028, + "description": "insufficient memory resources available" + }, + { + "constant": "JSMirrorWithSubjectFiltersErr", + "code": 400, + "error_code": 10033, + "description": "stream mirrors can not contain filtered subjects" + }, + { + "constant": "JSStreamCreateErrF", + "code": 500, + "error_code": 10049, + "description": "{err}", + "comment": "Generic stream creation error string" + }, + { + "constant": "JSClusterServerNotMemberErr", + "code": 400, + "error_code": 10044, + "description": "server is not a member of the cluster" + }, + { + "constant": "JSNoMessageFoundErr", + "code": 404, + "error_code": 10037, + "description": "no message found" + }, + { + "constant": "JSSnapshotDeliverSubjectInvalidErr", + "code": 400, + "error_code": 10015, + "description": "deliver subject not valid" + }, + { + "constant": "JSStreamGeneralErrorF", + "code": 500, + "error_code": 10051, + "description": "General stream failure string" + }, + { + "constant": "JSStreamInvalidConfigF", + "code": 500, + "error_code": 10052, + "description": "{err}", + "comment": "Stream configuration validation error string" + }, + { + "constant": "JSStreamReplicasNotSupportedErr", + "code": 500, + "error_code": 10074, + "description": "replicas > 1 not supported in non-clustered mode" + }, + { + "constant": "JSStreamMsgDeleteFailedF", + "code": 500, + "error_code": 10057, + "description": "%s", + "comment": "Generic message deletion failure error string" + }, + { + "constant": "JSPeerRemapErr", + "code": 503, + "error_code": 10075, + "description": "peer remap failed" + } +] diff --git a/server/errors_gen.go b/server/errors_gen.go new file mode 100644 index 00000000..d88ccaad --- /dev/null +++ b/server/errors_gen.go @@ -0,0 +1,116 @@ +// +build ignore + +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "os" + "os/exec" + "sort" + "text/template" +) + +var templ = ` +// Generated code, do not edit. See errors.json and run go generate to update + +package server + +const ( +{{- range $i, $error := . }} +{{- if .Comment }} + // {{ .Constant }} {{ .Comment }} ({{ .Description | print }}) +{{- else }} + // {{ .Constant }} {{ .Description | print }} +{{- end }} + {{ .Constant }} ErrorIdentifier = {{ .ErrCode }} +{{ end }} +) + +var ( + ApiErrors = map[ErrorIdentifier]*ApiError{ +{{- range $i, $error := . }} + {{ .Constant }}: {Code: {{ .Code }},ErrCode: {{ .ErrCode }},Description: {{ .Description | printf "%q" }},{{- if .Help }}Help: {{ .Help | printf "%q" }},{{- end }}{{- if .URL }}URL: {{ .URL | printf "%q" }},{{- end }} },{{- end }} + } +) +` + +func panicIfErr(err error) { + if err == nil { + return + } + panic(err) +} + +type Errors struct { + Constant string `json:"constant"` + Code int `json:"code"` + ErrCode int `json:"error_code"` + Description string `json:"description"` + Comment string `json:"comment"` + Help string `json:"help"` + URL string `json:"url"` +} + +func goFmt(file string) error { + c := exec.Command("go", "fmt", file) + out, err := c.CombinedOutput() + if err != nil { + log.Printf("go fmt failed: %s", string(out)) + } + + return err +} + +func checkDupes(errs []Errors) error { + codes := []int{} + for _, err := range errs { + codes = append(codes, err.ErrCode) + } + + codeKeys := make(map[int]bool) + constKeys := make(map[string]bool) + + for _, entry := range errs { + if _, found := codeKeys[entry.ErrCode]; found { + return fmt.Errorf("duplicate error code %+v", entry) + } + + if _, found := constKeys[entry.Constant]; found { + return fmt.Errorf("duplicate error constant %+v", entry) + } + + codeKeys[entry.ErrCode] = true + constKeys[entry.Constant] = true + } + + return nil +} + +func main() { + ej, err := os.ReadFile("server/errors.json") + panicIfErr(err) + + errs := []Errors{} + panicIfErr(json.Unmarshal(ej, &errs)) + panicIfErr(checkDupes(errs)) + + sort.Slice(errs, func(i, j int) bool { + return errs[i].Constant < errs[j].Constant + }) + + t := template.New("errors").Funcs(template.FuncMap{"inc": func(i int) int { return i + 1 }}) + p, err := t.Parse(templ) + panicIfErr(err) + + tf, err := ioutil.TempFile("", "") + panicIfErr(err) + defer tf.Close() + + panicIfErr(p.Execute(tf, errs)) + + panicIfErr(os.Rename(tf.Name(), "server/jetstream_errors_generated.go")) + panicIfErr(goFmt("server/jetstream_errors_generated.go")) +} diff --git a/server/jetstream.go b/server/jetstream.go index ff132ce6..b5de3888 100644 --- a/server/jetstream.go +++ b/server/jetstream.go @@ -775,7 +775,7 @@ func (s *Server) JetStreamNumAccounts() int { func (s *Server) JetStreamReservedResources() (int64, int64, error) { js := s.getJetStream() if js == nil { - return -1, -1, ErrJetStreamNotEnabled + return -1, -1, ApiErrors[JSNotEnabledErr] } js.mu.RLock() defer js.mu.RUnlock() @@ -818,7 +818,7 @@ func (a *Account) EnableJetStream(limits *JetStreamAccountLimits) error { limits = dynamicJSAccountLimits } a.assignJetStreamLimits(limits) - return ErrJetStreamNotEnabled + return ApiErrors[JSNotEnabledErr] } if s.SystemAccount() == a { @@ -1107,14 +1107,14 @@ func (a *Account) lookupStream(name string) (*stream, error) { a.mu.RUnlock() if jsa == nil { - return nil, ErrJetStreamNotEnabled + return nil, ApiErrors[JSNotEnabledErr] } jsa.mu.Lock() defer jsa.mu.Unlock() mset, ok := jsa.streams[name] if !ok { - return nil, ErrJetStreamStreamNotFound + return nil, ApiErrors[JSStreamNotFoundErr] } return mset, nil } @@ -1131,10 +1131,10 @@ func (a *Account) UpdateJetStreamLimits(limits *JetStreamAccountLimits) error { } js := s.getJetStream() if js == nil { - return ErrJetStreamNotEnabled + return ApiErrors[JSNotEnabledErr] } if jsa == nil { - return ErrJetStreamNotEnabledForAccount + return ApiErrors[JSNotEnabledErr] } if limits == nil { @@ -1223,7 +1223,7 @@ func (a *Account) DisableJetStream() error { js := s.getJetStream() if js == nil { - return ErrJetStreamNotEnabled + return ApiErrors[JSNotEnabledErr] } // Remove service imports. @@ -1248,7 +1248,7 @@ func (a *Account) removeJetStream() error { js := s.getJetStream() if js == nil { - return ErrJetStreamNotEnabled + return ApiErrors[JSNotEnabledErr] } return js.disableJetStream(js.lookupAccount(a)) @@ -1257,7 +1257,7 @@ func (a *Account) removeJetStream() error { // Disable JetStream for the account. func (js *jetStream) disableJetStream(jsa *jsAccount) error { if jsa == nil || jsa.account == nil { - return ErrJetStreamNotEnabledForAccount + return ApiErrors[JSNotEnabledErr] } js.mu.Lock() @@ -1428,11 +1428,11 @@ func (jsa *jsAccount) limitsExceeded(storeType StorageType) bool { // Lock should be held. func (jsa *jsAccount) checkLimits(config *StreamConfig) error { if jsa.limits.MaxStreams > 0 && len(jsa.streams) >= jsa.limits.MaxStreams { - return fmt.Errorf("maximum number of streams reached") + return ApiErrors[JSMaximumStreamsLimitErr] } // Check MaxConsumers if config.MaxConsumers > 0 && jsa.limits.MaxConsumers > 0 && config.MaxConsumers > jsa.limits.MaxConsumers { - return fmt.Errorf("maximum consumers exceeds account limit") + return ApiErrors[JSMaximumConsumersLimitErr] } // Check storage, memory or disk. @@ -1451,26 +1451,26 @@ func (jsa *jsAccount) checkBytesLimits(addBytes int64, storage StorageType) erro // Account limits defined. if jsa.limits.MaxMemory > 0 { if jsa.memReserved+addBytes > jsa.limits.MaxMemory { - return ErrMemoryResourcesExceeded + return ApiErrors[JSMemoryResourcesExceededErr] } } else { // Account is unlimited, check if this server can handle request. js := jsa.js if js.memReserved+addBytes > js.config.MaxMemory { - return ErrMemoryResourcesExceeded + return ApiErrors[JSMemoryResourcesExceededErr] } } case FileStorage: // Account limits defined. if jsa.limits.MaxStore > 0 { if jsa.storeReserved+addBytes > jsa.limits.MaxStore { - return ErrStorageResourcesExceeded + return ApiErrors[JSStorageResourcesExceededErr] } } else { // Account is unlimited, check if this server can handle request. js := jsa.js if js.storeReserved+addBytes > js.config.MaxStore { - return ErrStorageResourcesExceeded + return ApiErrors[JSStorageResourcesExceededErr] } } } @@ -1573,10 +1573,10 @@ func (js *jetStream) sufficientResources(limits *JetStreamAccountLimits) error { } if js.memReserved+limits.MaxMemory > js.config.MaxMemory { - return ErrMemoryResourcesExceeded + return ApiErrors[JSMemoryResourcesExceededErr] } if js.storeReserved+limits.MaxStore > js.config.MaxStore { - return ErrStorageResourcesExceeded + return ApiErrors[JSStorageResourcesExceededErr] } return nil } @@ -1664,7 +1664,7 @@ func (a *Account) checkForJetStream() (*Server, *jsAccount, error) { a.mu.RUnlock() if s == nil || jsa == nil { - return nil, nil, ErrJetStreamNotEnabledForAccount + return nil, nil, ApiErrors[JSNotEnabledErr] } return s, jsa, nil @@ -1875,7 +1875,7 @@ func (t *streamTemplate) delete() error { t.mu.Unlock() if jsa == nil { - return ErrJetStreamNotEnabled + return ApiErrors[JSNotEnabledErr] } jsa.mu.Lock() @@ -1919,7 +1919,7 @@ func (t *streamTemplate) delete() error { func (a *Account) deleteStreamTemplate(name string) error { t, err := a.lookupStreamTemplate(name) if err != nil { - return err + return ApiErrors[JSStreamTemplateNotFoundErr] } return t.delete() } diff --git a/server/jetstream_api.go b/server/jetstream_api.go index c1cce472..42d396db 100644 --- a/server/jetstream_api.go +++ b/server/jetstream_api.go @@ -39,10 +39,9 @@ const ( // For constructing JetStream domain prefixes. jsDomainAPI = "$JS.%s.API.>" - // JSApiPrefix JSApiPrefix = "$JS.API" - // JSApiInfo is for obtaining general information about JetStream for this account. + // JSApiAccountInfo is for obtaining general information about JetStream for this account. // Will return JSON response. JSApiAccountInfo = "$JS.API.INFO" @@ -91,7 +90,7 @@ const ( JSApiStreamDelete = "$JS.API.STREAM.DELETE.*" JSApiStreamDeleteT = "$JS.API.STREAM.DELETE.%s" - // JSApiPurgeStream is the endpoint to purge streams. + // JSApiStreamPurge is the endpoint to purge streams. // Will return JSON response. JSApiStreamPurge = "$JS.API.STREAM.PURGE.*" JSApiStreamPurgeT = "$JS.API.STREAM.PURGE.%s" @@ -104,11 +103,11 @@ const ( JSApiStreamSnapshotT = "$JS.API.STREAM.SNAPSHOT.%s" // JSApiStreamRestore is the endpoint to restore a stream from a snapshot. - // Caller should resond to each chunk with a nil body response. + // Caller should respond to each chunk with a nil body response. JSApiStreamRestore = "$JS.API.STREAM.RESTORE.*" JSApiStreamRestoreT = "$JS.API.STREAM.RESTORE.%s" - // JSApiDeleteMsg is the endpoint to delete messages from a stream. + // JSApiMsgDelete is the endpoint to delete messages from a stream. // Will return JSON response. JSApiMsgDelete = "$JS.API.STREAM.MSG.DELETE.*" JSApiMsgDeleteT = "$JS.API.STREAM.MSG.DELETE.%s" @@ -134,15 +133,14 @@ const ( JSApiConsumersT = "$JS.API.CONSUMER.NAMES.%s" // JSApiConsumerList is the endpoint that will return all detailed consumer information - JSApiConsumerList = "$JS.API.CONSUMER.LIST.*" - JSApiConsumerListT = "$JS.API.CONSUMER.LIST.%s" + JSApiConsumerList = "$JS.API.CONSUMER.LIST.*" // JSApiConsumerInfo is for obtaining general information about a consumer. // Will return JSON response. JSApiConsumerInfo = "$JS.API.CONSUMER.INFO.*.*" JSApiConsumerInfoT = "$JS.API.CONSUMER.INFO.%s.%s" - // JSApiDeleteConsumer is the endpoint to delete consumers. + // JSApiConsumerDelete is the endpoint to delete consumers. // Will return JSON response. JSApiConsumerDelete = "$JS.API.CONSUMER.DELETE.*.*" JSApiConsumerDeleteT = "$JS.API.CONSUMER.DELETE.%s.%s" @@ -231,13 +229,13 @@ const ( // JSAdvisoryStreamRestoreCompletePre notification that a restore was completed. JSAdvisoryStreamRestoreCompletePre = "$JS.EVENT.ADVISORY.STREAM.RESTORE_COMPLETE" - // JSAdvisoryStreamLeaderElectPre notification that a replicated stream has elected a leader. + // JSAdvisoryStreamLeaderElectedPre notification that a replicated stream has elected a leader. JSAdvisoryStreamLeaderElectedPre = "$JS.EVENT.ADVISORY.STREAM.LEADER_ELECTED" // JSAdvisoryStreamQuorumLostPre notification that a stream and its consumers are stalled. JSAdvisoryStreamQuorumLostPre = "$JS.EVENT.ADVISORY.STREAM.QUORUM_LOST" - // JSAdvisoryConsumerLeaderElectPre notification that a replicated consumer has elected a leader. + // JSAdvisoryConsumerLeaderElectedPre notification that a replicated consumer has elected a leader. JSAdvisoryConsumerLeaderElectedPre = "$JS.EVENT.ADVISORY.CONSUMER.LEADER_ELECTED" // JSAdvisoryConsumerQuorumLostPre notification that a consumer is stalled. @@ -254,24 +252,26 @@ const ( JSAuditAdvisory = "$JS.EVENT.ADVISORY.API" ) -// Maximum name lengths for streams, consumers and templates. +// JSMaxNameLen is the maximum name lengths for streams, consumers and templates. const JSMaxNameLen = 256 // Responses for API calls. -// ApiError is included in all responses if there was an error. -// TODO(dlc) - Move to more generic location. -type ApiError struct { - Code int `json:"code"` - Description string `json:"description,omitempty"` -} - // ApiResponse is a standard response from the JetStream JSON API type ApiResponse struct { Type string `json:"type"` Error *ApiError `json:"error,omitempty"` } +// ToError checks if the response has a error and if it does converts it to an error avoiding the pitfalls described by https://yourbasic.org/golang/gotcha-why-nil-error-not-equal-nil/ +func (r *ApiResponse) ToError() error { + if r.Error == nil { + return nil + } + + return r.Error +} + const JSApiOverloadedType = "io.nats.jetstream.api.v1.system_overloaded" // ApiPaged includes variables used to create paged responses from the JSON API @@ -310,12 +310,10 @@ type JSApiStreamDeleteResponse struct { const JSApiStreamDeleteResponseType = "io.nats.jetstream.api.v1.stream_delete_response" -// JSApiStreamInfoRequest. type JSApiStreamInfoRequest struct { DeletedDetails bool `json:"deleted_details,omitempty"` } -// JSApiStreamInfoResponse. type JSApiStreamInfoResponse struct { ApiResponse *StreamInfo @@ -323,7 +321,7 @@ type JSApiStreamInfoResponse struct { const JSApiStreamInfoResponseType = "io.nats.jetstream.api.v1.stream_info_response" -// Maximum entries we will return for streams or consumers lists. +// JSApiNamesLimit is the maximum entries we will return for streams or consumers lists. // TODO(dlc) - with header or request support could request chunked response. const JSApiNamesLimit = 1024 const JSApiListLimit = 256 @@ -354,7 +352,6 @@ type JSApiStreamListResponse struct { const JSApiStreamListResponseType = "io.nats.jetstream.api.v1.stream_list_response" -// JSApiStreamPurgeResponse. type JSApiStreamPurgeResponse struct { ApiResponse Success bool `json:"success,omitempty"` @@ -377,7 +374,6 @@ type JSApiMsgDeleteRequest struct { NoErase bool `json:"no_erase,omitempty"` } -// JSApiMsgDeleteResponse. type JSApiMsgDeleteResponse struct { ApiResponse Success bool `json:"success,omitempty"` @@ -416,8 +412,6 @@ type JSApiStreamRestoreRequest struct { State StreamState `json:"state"` } -const JSApiStreamRestoreRequestType = "io.nats.jetstream.api.v1.stream_restore_request" - // JSApiStreamRestoreResponse is the direct response to the restore request. type JSApiStreamRestoreResponse struct { ApiResponse @@ -489,7 +483,6 @@ type JSApiMsgGetRequest struct { Seq uint64 `json:"seq"` } -// JSApiMsgGetResponse. type JSApiMsgGetResponse struct { ApiResponse Message *StoredMsg `json:"message,omitempty"` @@ -500,7 +493,6 @@ const JSApiMsgGetResponseType = "io.nats.jetstream.api.v1.stream_msg_get_respons // JSWaitQueueDefaultMax is the default max number of outstanding requests for pull consumers. const JSWaitQueueDefaultMax = 512 -// JSApiConsumerCreateResponse. type JSApiConsumerCreateResponse struct { ApiResponse *ConsumerInfo @@ -508,7 +500,6 @@ type JSApiConsumerCreateResponse struct { const JSApiConsumerCreateResponseType = "io.nats.jetstream.api.v1.consumer_create_response" -// JSApiConsumerDeleteResponse. type JSApiConsumerDeleteResponse struct { ApiResponse Success bool `json:"success,omitempty"` @@ -516,7 +507,6 @@ type JSApiConsumerDeleteResponse struct { const JSApiConsumerDeleteResponseType = "io.nats.jetstream.api.v1.consumer_delete_response" -// JSApiConsumerInfoResponse. type JSApiConsumerInfoResponse struct { ApiResponse *ConsumerInfo @@ -524,12 +514,10 @@ type JSApiConsumerInfoResponse struct { const JSApiConsumerInfoResponseType = "io.nats.jetstream.api.v1.consumer_info_response" -// JSApiConsumersRequest type JSApiConsumersRequest struct { ApiPagedRequest } -// JSApiConsumerNamesResponse. type JSApiConsumerNamesResponse struct { ApiResponse ApiPaged @@ -538,7 +526,6 @@ type JSApiConsumerNamesResponse struct { const JSApiConsumerNamesResponseType = "io.nats.jetstream.api.v1.consumer_names_response" -// JSApiConsumerListResponse. type JSApiConsumerListResponse struct { ApiResponse ApiPaged @@ -562,7 +549,6 @@ type JSApiStreamTemplateCreateResponse struct { const JSApiStreamTemplateCreateResponseType = "io.nats.jetstream.api.v1.stream_template_create_response" -// JSApiStreamTemplateDeleteResponse type JSApiStreamTemplateDeleteResponse struct { ApiResponse Success bool `json:"success,omitempty"` @@ -578,7 +564,6 @@ type JSApiStreamTemplateInfoResponse struct { const JSApiStreamTemplateInfoResponseType = "io.nats.jetstream.api.v1.stream_template_info_response" -// JSApiStreamTemplatesRequest type JSApiStreamTemplatesRequest struct { ApiPagedRequest } @@ -592,27 +577,6 @@ type JSApiStreamTemplateNamesResponse struct { const JSApiStreamTemplateNamesResponseType = "io.nats.jetstream.api.v1.stream_template_names_response" -var ( - jsNotEnabledErr = &ApiError{Code: 503, Description: "JetStream not enabled for account"} - jsBadRequestErr = &ApiError{Code: 400, Description: "bad request"} - jsNotEmptyRequestErr = &ApiError{Code: 400, Description: "expected an empty request payload"} - jsInvalidJSONErr = &ApiError{Code: 400, Description: "invalid JSON"} - jsInsufficientErr = &ApiError{Code: 503, Description: "insufficient resources"} - jsNoConsumerErr = &ApiError{Code: 404, Description: "consumer not found"} - jsStreamMismatchErr = &ApiError{Code: 400, Description: "stream name in subject does not match request"} - jsNoClusterSupportErr = &ApiError{Code: 503, Description: "not currently supported in clustered mode"} - jsClusterNotAvailErr = &ApiError{Code: 503, Description: "JetStream system temporarily unavailable"} - jsClusterRequiredErr = &ApiError{Code: 503, Description: "JetStream clustering support required"} - jsPeerNotMemberErr = &ApiError{Code: 400, Description: "peer not a member"} - jsPeerRemapErr = &ApiError{Code: 503, Description: "peer remap failed"} - jsClusterIncompleteErr = &ApiError{Code: 503, Description: "incomplete results"} - jsClusterTagsErr = &ApiError{Code: 400, Description: "tags placement not supported for operation"} - jsClusterNoPeersErr = &ApiError{Code: 400, Description: "no suitable peers for placement"} - jsServerNotMemberErr = &ApiError{Code: 400, Description: "server is not a member of the cluster"} - jsNoMessageFoundErr = &ApiError{Code: 404, Description: "no message found"} - jsNoAccountErr = &ApiError{Code: 503, Description: "account not found"} -) - // For easier handling of exports and imports. var allJsExports = []string{ JSApiAccountInfo, @@ -678,7 +642,7 @@ func (js *jetStream) apiDispatch(sub *subscription, c *client, subject, reply st atomic.AddInt64(&js.apiCalls, -1) ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) if err == nil { - resp := &ApiResponse{Type: JSApiOverloadedType, Error: jsInsufficientErr} + resp := &ApiResponse{Type: JSApiOverloadedType, Error: ApiErrors[JSInsufficientResourcesErr]} s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) } else { s.Warnf(badAPIRequestT, rmsg) @@ -707,7 +671,7 @@ func (js *jetStream) apiDispatch(sub *subscription, c *client, subject, reply st func (s *Server) setJetStreamExportSubs() error { js := s.getJetStream() if js == nil { - return ErrJetStreamNotEnabled + return ApiErrors[JSNotEnabledErr] } // This is the catch all now for all JetStream API calls. @@ -889,7 +853,7 @@ func (s *Server) jsAccountInfoRequest(sub *subscription, c *client, subject, rep return } if js.isLeaderless() { - resp.Error = jsClusterNotAvailErr + resp.Error = ApiErrors[JSClusterNotAvailErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -903,7 +867,7 @@ func (s *Server) jsAccountInfoRequest(sub *subscription, c *client, subject, rep if !doErr { return } - resp.Error = jsNotEnabledErr + resp.Error = ApiErrors[JSNotEnabledErr] } else { stats := acc.JetStreamUsage() resp.JetStreamAccountStats = &stats @@ -941,34 +905,34 @@ func (s *Server) jsTemplateCreateRequest(sub *subscription, c *client, subject, var resp = JSApiStreamTemplateCreateResponse{ApiResponse: ApiResponse{Type: JSApiStreamTemplateCreateResponseType}} if !acc.JetStreamEnabled() { - resp.Error = jsNotEnabledErr + resp.Error = ApiErrors[JSNotEnabledErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } // Not supported for now. if s.JetStreamIsClustered() { - resp.Error = jsNoClusterSupportErr + resp.Error = ApiErrors[JSClusterUnSupportFeatureErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } var cfg StreamTemplateConfig if err := json.Unmarshal(msg, &cfg); err != nil { - resp.Error = jsInvalidJSONErr + resp.Error = ApiErrors[JSInvalidJSONErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } templateName := templateNameFromSubject(subject) if templateName != cfg.Name { - resp.Error = &ApiError{Code: 400, Description: "template name in subject does not match request"} + resp.Error = ApiErrors[JSTemplateNameNotMatchSubjectErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } t, err := acc.addStreamTemplate(&cfg) if err != nil { - resp.Error = jsError(err) + resp.Error = ApiErrors[JSStreamTemplateCreateErrF].ErrOrNew(err) s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -996,14 +960,14 @@ func (s *Server) jsTemplateNamesRequest(sub *subscription, c *client, subject, r var resp = JSApiStreamTemplateNamesResponse{ApiResponse: ApiResponse{Type: JSApiStreamTemplateNamesResponseType}} if !acc.JetStreamEnabled() { - resp.Error = jsNotEnabledErr + resp.Error = ApiErrors[JSNotEnabledErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } // Not supported for now. if s.JetStreamIsClustered() { - resp.Error = jsNoClusterSupportErr + resp.Error = ApiErrors[JSClusterUnSupportFeatureErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -1012,7 +976,7 @@ func (s *Server) jsTemplateNamesRequest(sub *subscription, c *client, subject, r if !isEmptyRequest(msg) { var req JSApiStreamTemplatesRequest if err := json.Unmarshal(msg, &req); err != nil { - resp.Error = jsInvalidJSONErr + resp.Error = ApiErrors[JSInvalidJSONErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -1060,19 +1024,19 @@ func (s *Server) jsTemplateInfoRequest(sub *subscription, c *client, subject, re var resp = JSApiStreamTemplateInfoResponse{ApiResponse: ApiResponse{Type: JSApiStreamTemplateInfoResponseType}} if !acc.JetStreamEnabled() { - resp.Error = jsNotEnabledErr + resp.Error = ApiErrors[JSNotEnabledErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } if !isEmptyRequest(msg) { - resp.Error = jsNotEmptyRequestErr + resp.Error = ApiErrors[JSNotEmptyRequestErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } name := templateNameFromSubject(subject) t, err := acc.lookupStreamTemplate(name) if err != nil { - resp.Error = jsNotFoundError(err) + resp.Error = ApiErrors[JSStreamTemplateNotFoundErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -1101,19 +1065,19 @@ func (s *Server) jsTemplateDeleteRequest(sub *subscription, c *client, subject, var resp = JSApiStreamTemplateDeleteResponse{ApiResponse: ApiResponse{Type: JSApiStreamTemplateDeleteResponseType}} if !acc.JetStreamEnabled() { - resp.Error = jsNotEnabledErr + resp.Error = ApiErrors[JSNotEnabledErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } if !isEmptyRequest(msg) { - resp.Error = jsNotEmptyRequestErr + resp.Error = ApiErrors[JSNotEmptyRequestErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } name := templateNameFromSubject(subject) err = acc.deleteStreamTemplate(name) if err != nil { - resp.Error = jsError(err) + resp.Error = ApiErrors[JSStreamTemplateDeleteErrF].ErrOrNew(err) s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -1130,20 +1094,6 @@ func (s *Server) jsonResponse(v interface{}) string { return string(b) } -func jsError(err error) *ApiError { - return &ApiError{ - Code: 500, - Description: err.Error(), - } -} - -func jsNotFoundError(err error) *ApiError { - return &ApiError{ - Code: 404, - Description: err.Error(), - } -} - // Request to create a stream. func (s *Server) jsStreamCreateRequest(sub *subscription, c *client, subject, reply string, rmsg []byte) { if c == nil || !s.JetStreamEnabled() { @@ -1164,7 +1114,7 @@ func (s *Server) jsStreamCreateRequest(sub *subscription, c *client, subject, re return } if js.isLeaderless() { - resp.Error = jsClusterNotAvailErr + resp.Error = ApiErrors[JSClusterNotAvailErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -1176,20 +1126,20 @@ func (s *Server) jsStreamCreateRequest(sub *subscription, c *client, subject, re if hasJS, doErr := acc.checkJetStream(); !hasJS { if doErr { - resp.Error = jsNotEnabledErr + resp.Error = ApiErrors[JSNotEnabledErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) } return } var cfg StreamConfig if err := json.Unmarshal(msg, &cfg); err != nil { - resp.Error = jsInvalidJSONErr + resp.Error = ApiErrors[JSInvalidJSONErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } streamName := streamNameFromSubject(subject) if streamName != cfg.Name { - resp.Error = jsStreamMismatchErr + resp.Error = ApiErrors[JSStreamMismatchErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -1223,22 +1173,22 @@ func (s *Server) jsStreamCreateRequest(sub *subscription, c *client, subject, re // Do some pre-checking for mirror config to avoid cycles in clustered mode. if cfg.Mirror != nil { if len(cfg.Subjects) > 0 { - resp.Error = &ApiError{Code: 400, Description: "stream mirrors can not also contain subjects"} + resp.Error = ApiErrors[JSMirrorWithSubjectsErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } if len(cfg.Sources) > 0 { - resp.Error = &ApiError{Code: 400, Description: "stream mirrors can not also contain other sources"} + resp.Error = ApiErrors[JSMirrorWithSourcesErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } if cfg.Mirror.FilterSubject != _EMPTY_ { - resp.Error = &ApiError{Code: 400, Description: "stream mirrors can not contain filtered subjects"} + resp.Error = ApiErrors[JSMirrorWithSubjectFiltersErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } if cfg.Mirror.OptStartSeq > 0 && cfg.Mirror.OptStartTime != nil { - resp.Error = &ApiError{Code: 400, Description: "stream mirrors can not have both start seq and start time configured"} + resp.Error = ApiErrors[JSMirrorWithStartSeqAndTimeErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -1253,7 +1203,7 @@ func (s *Server) jsStreamCreateRequest(sub *subscription, c *client, subject, re streamSubs = append(streamSubs, subs...) } if exists && cfg.MaxMsgSize > 0 && maxMsgSize > 0 && cfg.MaxMsgSize < maxMsgSize { - resp.Error = &ApiError{Code: 400, Description: "stream mirror must have max message size >= source"} + resp.Error = ApiErrors[JSMirrorMaxMessageSizeTooBigErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -1282,7 +1232,7 @@ func (s *Server) jsStreamCreateRequest(sub *subscription, c *client, subject, re apiPrefixes = append(apiPrefixes, src.External.ApiPrefix) } if exists && cfg.MaxMsgSize > 0 && maxMsgSize > 0 && cfg.MaxMsgSize < maxMsgSize { - resp.Error = &ApiError{Code: 400, Description: "stream source must have max message size >= target"} + resp.Error = ApiErrors[JSSourceMaxMessageSizeTooBigErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -1291,13 +1241,13 @@ func (s *Server) jsStreamCreateRequest(sub *subscription, c *client, subject, re // check prefix overlap with subjects for _, pfx := range deliveryPrefixes { if !IsValidPublishSubject(pfx) { - resp.Error = &ApiError{Code: 400, Description: fmt.Sprintf("stream external delivery prefix %q must be a valid subject without wildcards", pfx)} + resp.Error = ApiErrors[JSStreamInvalidExternalDeliverySubjErrF].NewT("{prefix}", pfx) s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } for _, sub := range streamSubs { if SubjectsCollide(sub, fmt.Sprintf("%s.%s", pfx, sub)) { - resp.Error = &ApiError{Code: 400, Description: fmt.Sprintf("stream external delivery prefix %q overlaps with stream subject %q", pfx, sub)} + resp.Error = ApiErrors[JSStreamExternalDelPrefixOverlapsErrF].NewT("{prefix}", pfx, "{subject}", sub) s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -1311,7 +1261,7 @@ func (s *Server) jsStreamCreateRequest(sub *subscription, c *client, subject, re return } if SubjectsCollide(apiPfx, JSApiPrefix) { - resp.Error = &ApiError{Code: 400, Description: fmt.Sprintf("stream external api prefix %q must not overlap with %s", apiPfx, JSApiPrefix)} + resp.Error = ApiErrors[JSStreamExternalApiOverlapErrF].NewT("{prefix}", apiPfx, "{subject}", JSApiPrefix) s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -1324,7 +1274,7 @@ func (s *Server) jsStreamCreateRequest(sub *subscription, c *client, subject, re mset, err := acc.addStream(&cfg) if err != nil { - resp.Error = jsError(err) + resp.Error = ApiErrors[JSStreamCreateErrF].NewT("{err}", err) s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -1353,7 +1303,7 @@ func (s *Server) jsStreamUpdateRequest(sub *subscription, c *client, subject, re return } if js.isLeaderless() { - resp.Error = jsClusterNotAvailErr + resp.Error = ApiErrors[JSClusterNotAvailErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -1365,28 +1315,28 @@ func (s *Server) jsStreamUpdateRequest(sub *subscription, c *client, subject, re if hasJS, doErr := acc.checkJetStream(); !hasJS { if doErr { - resp.Error = jsNotEnabledErr + resp.Error = ApiErrors[JSNotEnabledErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) } return } var ncfg StreamConfig if err := json.Unmarshal(msg, &ncfg); err != nil { - resp.Error = jsInvalidJSONErr + resp.Error = ApiErrors[JSInvalidJSONErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } cfg, err := checkStreamCfg(&ncfg) if err != nil { - resp.Error = jsError(err) + resp.Error = ApiErrors[JSStreamInvalidConfigF].NewT("{err}", err) s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } streamName := streamNameFromSubject(subject) if streamName != cfg.Name { - resp.Error = jsStreamMismatchErr + resp.Error = ApiErrors[JSStreamMismatchErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -1398,13 +1348,13 @@ func (s *Server) jsStreamUpdateRequest(sub *subscription, c *client, subject, re mset, err := acc.lookupStream(streamName) if err != nil { - resp.Error = jsNotFoundError(err) + resp.Error = ApiErrors[JSStreamNotFoundErr].ErrOrNew(err) s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } if err := mset.update(&cfg); err != nil { - resp.Error = jsError(err) + resp.Error = ApiErrors[JSStreamUpdateErrF].ErrOrNew(err) s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -1435,7 +1385,7 @@ func (s *Server) jsStreamNamesRequest(sub *subscription, c *client, subject, rep return } if js.isLeaderless() { - resp.Error = jsClusterNotAvailErr + resp.Error = ApiErrors[JSClusterNotAvailErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -1447,7 +1397,7 @@ func (s *Server) jsStreamNamesRequest(sub *subscription, c *client, subject, rep if hasJS, doErr := acc.checkJetStream(); !hasJS { if doErr { - resp.Error = jsNotEnabledErr + resp.Error = ApiErrors[JSNotEnabledErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) } return @@ -1459,7 +1409,7 @@ func (s *Server) jsStreamNamesRequest(sub *subscription, c *client, subject, rep if !isEmptyRequest(msg) { var req JSApiStreamNamesRequest if err := json.Unmarshal(msg, &req); err != nil { - resp.Error = jsInvalidJSONErr + resp.Error = ApiErrors[JSInvalidJSONErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -1480,7 +1430,7 @@ func (s *Server) jsStreamNamesRequest(sub *subscription, c *client, subject, rep } js.mu.RLock() for stream, sa := range cc.streams[acc.Name] { - if sa.err == ErrJetStreamNotAssigned { + if sa.err == ApiErrors[JSClusterNotAssignedErr] { continue } if filter != _EMPTY_ { @@ -1562,7 +1512,7 @@ func (s *Server) jsStreamListRequest(sub *subscription, c *client, subject, repl return } if js.isLeaderless() { - resp.Error = jsClusterNotAvailErr + resp.Error = ApiErrors[JSClusterNotAvailErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -1574,7 +1524,7 @@ func (s *Server) jsStreamListRequest(sub *subscription, c *client, subject, repl if hasJS, doErr := acc.checkJetStream(); !hasJS { if doErr { - resp.Error = jsNotEnabledErr + resp.Error = ApiErrors[JSNotEnabledErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) } return @@ -1584,7 +1534,7 @@ func (s *Server) jsStreamListRequest(sub *subscription, c *client, subject, repl if !isEmptyRequest(msg) { var req JSApiStreamNamesRequest if err := json.Unmarshal(msg, &req); err != nil { - resp.Error = jsInvalidJSONErr + resp.Error = ApiErrors[JSInvalidJSONErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -1660,18 +1610,18 @@ func (s *Server) jsStreamInfoRequest(sub *subscription, c *client, subject, repl // We can't find the stream, so mimic what would be the errors below. if hasJS, doErr := acc.checkJetStream(); !hasJS { if doErr { - resp.Error = jsNotEnabledErr + resp.Error = ApiErrors[JSNotEnabledErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) } return } // No stream present. - resp.Error = jsNotFoundError(ErrJetStreamStreamNotFound) + resp.Error = ApiErrors[JSStreamNotFoundErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } else if sa == nil { if js.isLeaderless() { - resp.Error = jsClusterNotAvailErr + resp.Error = ApiErrors[JSClusterNotAvailErr] // Delaying an error response gives the leader a chance to respond before us s.sendDelayedAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp), nil) } @@ -1684,7 +1634,7 @@ func (s *Server) jsStreamInfoRequest(sub *subscription, c *client, subject, repl // We have the stream assigned and a leader, so only the stream leader should answer. if !acc.JetStreamIsStreamLeader(streamName) && !isLeaderless { if js.isLeaderless() { - resp.Error = jsClusterNotAvailErr + resp.Error = ApiErrors[JSClusterNotAvailErr] // Delaying an error response gives the leader a chance to respond before us s.sendDelayedAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp), sa.Group) } @@ -1694,7 +1644,7 @@ func (s *Server) jsStreamInfoRequest(sub *subscription, c *client, subject, repl if hasJS, doErr := acc.checkJetStream(); !hasJS { if doErr { - resp.Error = jsNotEnabledErr + resp.Error = ApiErrors[JSNotEnabledErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) } return @@ -1704,7 +1654,7 @@ func (s *Server) jsStreamInfoRequest(sub *subscription, c *client, subject, repl if !isEmptyRequest(msg) { var req JSApiStreamInfoRequest if err := json.Unmarshal(msg, &req); err != nil { - resp.Error = jsInvalidJSONErr + resp.Error = ApiErrors[JSInvalidJSONErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -1713,7 +1663,7 @@ func (s *Server) jsStreamInfoRequest(sub *subscription, c *client, subject, repl mset, err := acc.lookupStream(streamName) if err != nil { - resp.Error = jsNotFoundError(err) + resp.Error = ApiErrors[JSStreamNotFoundErr].ErrOrNew(err) s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -1759,7 +1709,7 @@ func (s *Server) jsStreamLeaderStepDownRequest(sub *subscription, c *client, sub // If we are not in clustered mode this is a failed request. if !s.JetStreamIsClustered() { - resp.Error = jsClusterRequiredErr + resp.Error = ApiErrors[JSClusterRequiredErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -1770,7 +1720,7 @@ func (s *Server) jsStreamLeaderStepDownRequest(sub *subscription, c *client, sub return } if js.isLeaderless() { - resp.Error = jsClusterNotAvailErr + resp.Error = ApiErrors[JSClusterNotAvailErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -1780,7 +1730,7 @@ func (s *Server) jsStreamLeaderStepDownRequest(sub *subscription, c *client, sub js.mu.RUnlock() if isLeader && sa == nil { - resp.Error = jsNotFoundError(ErrJetStreamStreamNotFound) + resp.Error = ApiErrors[JSStreamNotFoundErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } else if sa == nil { @@ -1789,20 +1739,20 @@ func (s *Server) jsStreamLeaderStepDownRequest(sub *subscription, c *client, sub if hasJS, doErr := acc.checkJetStream(); !hasJS { if doErr { - resp.Error = jsNotEnabledErr + resp.Error = ApiErrors[JSNotEnabledErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) } return } if !isEmptyRequest(msg) { - resp.Error = jsBadRequestErr + resp.Error = ApiErrors[JSBadRequestErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } // Check to see if we are a member of the group and if the group has no leader. if js.isGroupLeaderless(sa.Group) { - resp.Error = jsClusterNotAvailErr + resp.Error = ApiErrors[JSClusterNotAvailErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -1814,7 +1764,7 @@ func (s *Server) jsStreamLeaderStepDownRequest(sub *subscription, c *client, sub mset, err := acc.lookupStream(name) if err != nil { - resp.Error = jsNotFoundError(err) + resp.Error = ApiErrors[JSStreamNotFoundErr].ErrOrNew(err) s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -1845,7 +1795,7 @@ func (s *Server) jsConsumerLeaderStepDownRequest(sub *subscription, c *client, s // If we are not in clustered mode this is a failed request. if !s.JetStreamIsClustered() { - resp.Error = jsClusterRequiredErr + resp.Error = ApiErrors[JSClusterRequiredErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -1856,7 +1806,7 @@ func (s *Server) jsConsumerLeaderStepDownRequest(sub *subscription, c *client, s return } if js.isLeaderless() { - resp.Error = jsClusterNotAvailErr + resp.Error = ApiErrors[JSClusterNotAvailErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -1870,7 +1820,7 @@ func (s *Server) jsConsumerLeaderStepDownRequest(sub *subscription, c *client, s js.mu.RUnlock() if isLeader && sa == nil { - resp.Error = jsNotFoundError(ErrJetStreamStreamNotFound) + resp.Error = ApiErrors[JSStreamNotFoundErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } else if sa == nil { @@ -1881,13 +1831,13 @@ func (s *Server) jsConsumerLeaderStepDownRequest(sub *subscription, c *client, s ca = sa.consumers[consumer] } if ca == nil { - resp.Error = jsNoConsumerErr + resp.Error = ApiErrors[JSConsumerNotFoundErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } // Check to see if we are a member of the group and if the group has no leader. if js.isGroupLeaderless(ca.Group) { - resp.Error = jsClusterNotAvailErr + resp.Error = ApiErrors[JSClusterNotAvailErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -1898,26 +1848,26 @@ func (s *Server) jsConsumerLeaderStepDownRequest(sub *subscription, c *client, s if hasJS, doErr := acc.checkJetStream(); !hasJS { if doErr { - resp.Error = jsNotEnabledErr + resp.Error = ApiErrors[JSNotEnabledErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) } return } if !isEmptyRequest(msg) { - resp.Error = jsBadRequestErr + resp.Error = ApiErrors[JSBadRequestErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } mset, err := acc.lookupStream(stream) if err != nil { - resp.Error = jsNotFoundError(ErrJetStreamStreamNotFound) + resp.Error = ApiErrors[JSStreamNotFoundErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } o := mset.lookupConsumer(consumer) if o == nil { - resp.Error = jsNoConsumerErr + resp.Error = ApiErrors[JSConsumerNotFoundErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -1947,7 +1897,7 @@ func (s *Server) jsStreamRemovePeerRequest(sub *subscription, c *client, subject // If we are not in clustered mode this is a failed request. if !s.JetStreamIsClustered() { - resp.Error = jsClusterRequiredErr + resp.Error = ApiErrors[JSClusterRequiredErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -1958,7 +1908,7 @@ func (s *Server) jsStreamRemovePeerRequest(sub *subscription, c *client, subject return } if js.isLeaderless() { - resp.Error = jsClusterNotAvailErr + resp.Error = ApiErrors[JSClusterNotAvailErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -1974,32 +1924,32 @@ func (s *Server) jsStreamRemovePeerRequest(sub *subscription, c *client, subject if hasJS, doErr := acc.checkJetStream(); !hasJS { if doErr { - resp.Error = jsNotEnabledErr + resp.Error = ApiErrors[JSNotEnabledErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) } return } if isEmptyRequest(msg) { - resp.Error = jsBadRequestErr + resp.Error = ApiErrors[JSBadRequestErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } var req JSApiStreamRemovePeerRequest if err := json.Unmarshal(msg, &req); err != nil { - resp.Error = jsInvalidJSONErr + resp.Error = ApiErrors[JSInvalidJSONErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } if req.Peer == _EMPTY_ { - resp.Error = jsBadRequestErr + resp.Error = ApiErrors[JSBadRequestErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } if sa == nil { // No stream present. - resp.Error = jsNotFoundError(ErrJetStreamStreamNotFound) + resp.Error = ApiErrors[JSStreamNotFoundErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -2015,14 +1965,14 @@ func (s *Server) jsStreamRemovePeerRequest(sub *subscription, c *client, subject // Make sure we are a member. if !isMember { - resp.Error = jsPeerNotMemberErr + resp.Error = ApiErrors[JSClusterPeerNotMemberErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } // If we are here we have a valid peer member set for removal. if !js.removePeerFromStream(sa, nodeName) { - resp.Error = jsPeerRemapErr + resp.Error = ApiErrors[JSPeerRemapErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -2060,14 +2010,14 @@ func (s *Server) jsLeaderServerRemoveRequest(sub *subscription, c *client, subje var resp = JSApiMetaServerRemoveResponse{ApiResponse: ApiResponse{Type: JSApiMetaServerRemoveResponseType}} if isEmptyRequest(msg) { - resp.Error = jsBadRequestErr + resp.Error = ApiErrors[JSBadRequestErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } var req JSApiMetaServerRemoveRequest if err := json.Unmarshal(msg, &req); err != nil { - resp.Error = jsInvalidJSONErr + resp.Error = ApiErrors[JSInvalidJSONErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -2084,7 +2034,7 @@ func (s *Server) jsLeaderServerRemoveRequest(sub *subscription, c *client, subje js.mu.RUnlock() if found == _EMPTY_ { - resp.Error = jsServerNotMemberErr + resp.Error = ApiErrors[JSClusterServerNotMemberErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -2131,13 +2081,13 @@ func (s *Server) jsLeaderStepDownRequest(sub *subscription, c *client, subject, if !isEmptyRequest(msg) { var req JSApiLeaderStepdownRequest if err := json.Unmarshal(msg, &req); err != nil { - resp.Error = jsInvalidJSONErr + resp.Error = ApiErrors[JSInvalidJSONErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } if len(req.Placement.Tags) > 0 { // Tags currently not supported. - resp.Error = jsClusterTagsErr + resp.Error = ApiErrors[JSClusterTagsErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -2153,7 +2103,7 @@ func (s *Server) jsLeaderStepDownRequest(sub *subscription, c *client, subject, } } if len(peers) == 0 { - resp.Error = jsClusterNoPeersErr + resp.Error = ApiErrors[JSClusterNoPeersErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -2166,9 +2116,8 @@ func (s *Server) jsLeaderStepDownRequest(sub *subscription, c *client, subject, // Call actual stepdown. err = cc.meta.StepDown(preferredLeader) - if err != nil { - resp.Error = jsError(err) + resp.Error = ApiErrors[JSRaftGeneralErrF].ErrOrNew(err) } else { resp.Success = true } @@ -2214,7 +2163,7 @@ func (s *Server) jsStreamDeleteRequest(sub *subscription, c *client, subject, re return } if js.isLeaderless() { - resp.Error = jsClusterNotAvailErr + resp.Error = ApiErrors[JSClusterNotAvailErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -2226,14 +2175,14 @@ func (s *Server) jsStreamDeleteRequest(sub *subscription, c *client, subject, re if hasJS, doErr := acc.checkJetStream(); !hasJS { if doErr { - resp.Error = jsNotEnabledErr + resp.Error = ApiErrors[JSNotEnabledErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) } return } if !isEmptyRequest(msg) { - resp.Error = jsNotEmptyRequestErr + resp.Error = ApiErrors[JSNotEmptyRequestErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -2247,13 +2196,13 @@ func (s *Server) jsStreamDeleteRequest(sub *subscription, c *client, subject, re mset, err := acc.lookupStream(stream) if err != nil { - resp.Error = jsNotFoundError(err) + resp.Error = ApiErrors[JSStreamNotFoundErr].ErrOrNew(err) s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } if err := mset.delete(); err != nil { - resp.Error = jsError(err) + resp.Error = ApiErrors[JSStreamDeleteErrF].ErrOrNew(err) s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -2285,7 +2234,7 @@ func (s *Server) jsMsgDeleteRequest(sub *subscription, c *client, subject, reply return } if js.isLeaderless() { - resp.Error = jsClusterNotAvailErr + resp.Error = ApiErrors[JSClusterNotAvailErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -2298,13 +2247,13 @@ func (s *Server) jsMsgDeleteRequest(sub *subscription, c *client, subject, reply // We can't find the stream, so mimic what would be the errors below. if hasJS, doErr := acc.checkJetStream(); !hasJS { if doErr { - resp.Error = jsNotEnabledErr + resp.Error = ApiErrors[JSNotEnabledErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) } return } // No stream present. - resp.Error = jsNotFoundError(ErrJetStreamStreamNotFound) + resp.Error = ApiErrors[JSStreamNotFoundErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } else if sa == nil { @@ -2313,7 +2262,7 @@ func (s *Server) jsMsgDeleteRequest(sub *subscription, c *client, subject, reply // Check to see if we are a member of the group and if the group has no leader. if js.isGroupLeaderless(sa.Group) { - resp.Error = jsClusterNotAvailErr + resp.Error = ApiErrors[JSClusterNotAvailErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -2326,26 +2275,26 @@ func (s *Server) jsMsgDeleteRequest(sub *subscription, c *client, subject, reply if hasJS, doErr := acc.checkJetStream(); !hasJS { if doErr { - resp.Error = jsNotEnabledErr + resp.Error = ApiErrors[JSNotEnabledErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) } return } if isEmptyRequest(msg) { - resp.Error = jsBadRequestErr + resp.Error = ApiErrors[JSBadRequestErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } var req JSApiMsgDeleteRequest if err := json.Unmarshal(msg, &req); err != nil { - resp.Error = jsInvalidJSONErr + resp.Error = ApiErrors[JSInvalidJSONErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } mset, err := acc.lookupStream(stream) if err != nil { - resp.Error = jsNotFoundError(err) + resp.Error = ApiErrors[JSStreamNotFoundErr].ErrOrNew(err) s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -2362,9 +2311,9 @@ func (s *Server) jsMsgDeleteRequest(sub *subscription, c *client, subject, reply removed, err = mset.eraseMsg(req.Seq) } if err != nil { - resp.Error = jsError(err) + resp.Error = ApiErrors[JSStreamMsgDeleteFailedF].ErrOrNew(err) } else if !removed { - resp.Error = &ApiError{Code: 400, Description: fmt.Sprintf("sequence [%d] not found", req.Seq)} + resp.Error = ApiErrors[JSSequenceNotFoundErrF].NewT("{seq}", req.Seq) } else { resp.Success = true } @@ -2394,7 +2343,7 @@ func (s *Server) jsMsgGetRequest(sub *subscription, c *client, subject, reply st return } if js.isLeaderless() { - resp.Error = jsClusterNotAvailErr + resp.Error = ApiErrors[JSClusterNotAvailErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -2407,13 +2356,13 @@ func (s *Server) jsMsgGetRequest(sub *subscription, c *client, subject, reply st // We can't find the stream, so mimic what would be the errors below. if hasJS, doErr := acc.checkJetStream(); !hasJS { if doErr { - resp.Error = jsNotEnabledErr + resp.Error = ApiErrors[JSNotEnabledErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) } return } // No stream present. - resp.Error = jsNotFoundError(ErrJetStreamStreamNotFound) + resp.Error = ApiErrors[JSStreamNotFoundErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } else if sa == nil { @@ -2422,7 +2371,7 @@ func (s *Server) jsMsgGetRequest(sub *subscription, c *client, subject, reply st // Check to see if we are a member of the group and if the group has no leader. if js.isGroupLeaderless(sa.Group) { - resp.Error = jsClusterNotAvailErr + resp.Error = ApiErrors[JSClusterNotAvailErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -2435,33 +2384,33 @@ func (s *Server) jsMsgGetRequest(sub *subscription, c *client, subject, reply st if hasJS, doErr := acc.checkJetStream(); !hasJS { if doErr { - resp.Error = jsNotEnabledErr + resp.Error = ApiErrors[JSNotEnabledErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) } return } if isEmptyRequest(msg) { - resp.Error = jsBadRequestErr + resp.Error = ApiErrors[JSBadRequestErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } var req JSApiMsgGetRequest if err := json.Unmarshal(msg, &req); err != nil { - resp.Error = jsInvalidJSONErr + resp.Error = ApiErrors[JSInvalidJSONErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } mset, err := acc.lookupStream(stream) if err != nil { - resp.Error = jsNotFoundError(err) + resp.Error = ApiErrors[JSStreamNotFoundErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } subj, hdr, msg, ts, err := mset.store.LoadMsg(req.Seq) if err != nil { - resp.Error = jsNoMessageFoundErr + resp.Error = ApiErrors[JSNoMessageFoundErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -2506,18 +2455,18 @@ func (s *Server) jsStreamPurgeRequest(sub *subscription, c *client, subject, rep // We can't find the stream, so mimic what would be the errors below. if hasJS, doErr := acc.checkJetStream(); !hasJS { if doErr { - resp.Error = jsNotEnabledErr + resp.Error = ApiErrors[JSNotEnabledErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) } return } // No stream present. - resp.Error = jsNotFoundError(ErrJetStreamStreamNotFound) + resp.Error = ApiErrors[JSStreamNotFoundErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } else if sa == nil { if js.isLeaderless() { - resp.Error = jsClusterNotAvailErr + resp.Error = ApiErrors[JSClusterNotAvailErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) } return @@ -2525,7 +2474,7 @@ func (s *Server) jsStreamPurgeRequest(sub *subscription, c *client, subject, rep // Check to see if we are a member of the group and if the group has no leader. if js.isGroupLeaderless(sa.Group) { - resp.Error = jsClusterNotAvailErr + resp.Error = ApiErrors[JSClusterNotAvailErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -2533,7 +2482,7 @@ func (s *Server) jsStreamPurgeRequest(sub *subscription, c *client, subject, rep // We have the stream assigned and a leader, so only the stream leader should answer. if !acc.JetStreamIsStreamLeader(stream) { if js.isLeaderless() { - resp.Error = jsClusterNotAvailErr + resp.Error = ApiErrors[JSClusterNotAvailErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) } return @@ -2542,20 +2491,20 @@ func (s *Server) jsStreamPurgeRequest(sub *subscription, c *client, subject, rep if hasJS, doErr := acc.checkJetStream(); !hasJS { if doErr { - resp.Error = jsNotEnabledErr + resp.Error = ApiErrors[JSNotEnabledErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) } return } if !isEmptyRequest(msg) { - resp.Error = jsNotEmptyRequestErr + resp.Error = ApiErrors[JSNotEmptyRequestErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } mset, err := acc.lookupStream(stream) if err != nil { - resp.Error = jsNotFoundError(err) + resp.Error = ApiErrors[JSStreamNotFoundErr].ErrOrNew(err) s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -2567,7 +2516,7 @@ func (s *Server) jsStreamPurgeRequest(sub *subscription, c *client, subject, rep purged, err := mset.purge() if err != nil { - resp.Error = jsError(err) + resp.Error = ApiErrors[JSStreamGeneralErrorF].ErrOrNew(err) } else { resp.Purged = purged resp.Success = true @@ -2588,19 +2537,19 @@ func (s *Server) jsStreamRestoreRequest(sub *subscription, c *client, subject, r var resp = JSApiStreamRestoreResponse{ApiResponse: ApiResponse{Type: JSApiStreamRestoreResponseType}} if !acc.JetStreamEnabled() { - resp.Error = jsNotEnabledErr + resp.Error = ApiErrors[JSNotEnabledErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } if isEmptyRequest(msg) { - resp.Error = jsBadRequestErr + resp.Error = ApiErrors[JSBadRequestErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } var req JSApiStreamRestoreRequest if err := json.Unmarshal(msg, &req); err != nil { - resp.Error = jsInvalidJSONErr + resp.Error = ApiErrors[JSInvalidJSONErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -2617,14 +2566,14 @@ func (s *Server) jsStreamRestoreRequest(sub *subscription, c *client, subject, r } if _, err := acc.lookupStream(stream); err == nil { - resp.Error = jsError(ErrJetStreamStreamAlreadyUsed) + resp.Error = ApiErrors[JSStreamNameExistErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } if hasJS, doErr := acc.checkJetStream(); !hasJS { if doErr { - resp.Error = jsNotEnabledErr + resp.Error = ApiErrors[JSNotEnabledErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) } return @@ -2649,8 +2598,8 @@ func (s *Server) processStreamRestore(ci *ClientInfo, acc *Account, cfg *StreamC tfile, err := ioutil.TempFile(snapDir, "js-restore-") if err != nil { - resp.Error = &ApiError{Code: 500, Description: "JetStream unable to open temp storage for restore"} - s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + resp.Error = ApiErrors[JSTempStorageFailedErr] + s.sendAPIErrResponse(ci, acc, subject, reply, msg, s.jsonResponse(&resp)) return nil } @@ -2719,7 +2668,7 @@ func (s *Server) processStreamRestore(ci *ClientInfo, acc *Account, cfg *StreamC total += len(msg) if js.wouldExceedLimits(FileStorage, total) { s.resourcesExeededError() - resultCh <- result{ErrJetStreamResourcesExceeded, reply} + resultCh <- result{ApiErrors[JSInsufficientResourcesErr], reply} return } @@ -2741,14 +2690,14 @@ func (s *Server) processStreamRestore(ci *ClientInfo, acc *Account, cfg *StreamC if err != nil { tfile.Close() os.Remove(tfile.Name()) - resp.Error = &ApiError{Code: 500, Description: "JetStream unable to subscribe to restore snapshot " + restoreSubj + ": " + err.Error()} - s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + resp.Error = ApiErrors[JSRestoreSubscribeFailedErrF].NewT("{subject}", restoreSubj, "{err}", err) + s.sendAPIErrResponse(ci, acc, subject, reply, msg, s.jsonResponse(&resp)) return nil } // Mark the subject so the end user knows where to send the snapshot chunks. resp.DeliverSubject = restoreSubj - s.sendAPIResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) + s.sendAPIResponse(ci, acc, subject, reply, msg, s.jsonResponse(resp)) doneCh := make(chan error, 1) @@ -2804,7 +2753,7 @@ func (s *Server) processStreamRestore(ci *ClientInfo, acc *Account, cfg *StreamC var resp = JSApiStreamCreateResponse{ApiResponse: ApiResponse{Type: JSApiStreamCreateResponseType}} if err != nil { - resp.Error = jsError(err) + resp.Error = ApiErrors[JSStreamRestoreErrF].ErrOrNewT(err, "{err}", err) s.Warnf("Restore failed for %s for stream '%s > %s' in %v", friendlyBytes(int64(total)), streamName, acc.Name, end.Sub(start)) } else { @@ -2853,26 +2802,26 @@ func (s *Server) jsStreamSnapshotRequest(sub *subscription, c *client, subject, var resp = JSApiStreamSnapshotResponse{ApiResponse: ApiResponse{Type: JSApiStreamSnapshotResponseType}} if !acc.JetStreamEnabled() { - resp.Error = jsNotEnabledErr + resp.Error = ApiErrors[JSNotEnabledErr] s.sendAPIErrResponse(ci, acc, subject, reply, smsg, s.jsonResponse(&resp)) return } if isEmptyRequest(msg) { - resp.Error = jsBadRequestErr + resp.Error = ApiErrors[JSBadRequestErr] s.sendAPIErrResponse(ci, acc, subject, reply, smsg, s.jsonResponse(&resp)) return } mset, err := acc.lookupStream(stream) if err != nil { - resp.Error = jsNotFoundError(err) + resp.Error = ApiErrors[JSStreamNotFoundErr].ErrOrNew(err) s.sendAPIErrResponse(ci, acc, subject, reply, smsg, s.jsonResponse(&resp)) return } if hasJS, doErr := acc.checkJetStream(); !hasJS { if doErr { - resp.Error = jsNotEnabledErr + resp.Error = ApiErrors[JSNotEnabledErr] s.sendAPIErrResponse(ci, acc, subject, reply, smsg, s.jsonResponse(&resp)) } return @@ -2880,12 +2829,12 @@ func (s *Server) jsStreamSnapshotRequest(sub *subscription, c *client, subject, var req JSApiStreamSnapshotRequest if err := json.Unmarshal(msg, &req); err != nil { - resp.Error = jsInvalidJSONErr + resp.Error = ApiErrors[JSInvalidJSONErr] s.sendAPIErrResponse(ci, acc, subject, reply, smsg, s.jsonResponse(&resp)) return } if !IsValidSubject(req.DeliverSubject) { - resp.Error = &ApiError{Code: 400, Description: "deliver subject not valid"} + resp.Error = ApiErrors[JSSnapshotDeliverSubjectInvalidErr] s.sendAPIErrResponse(ci, acc, subject, reply, smsg, s.jsonResponse(&resp)) return } @@ -2904,7 +2853,7 @@ func (s *Server) jsStreamSnapshotRequest(sub *subscription, c *client, subject, sr, err := mset.snapshot(0, req.CheckMsgs, !req.NoConsumers) if err != nil { s.Warnf("Snapshot of stream '%s > %s' failed: %v", mset.jsa.account.Name, mset.name(), err) - resp.Error = jsError(err) + resp.Error = ApiErrors[JSStreamSnapshotErrF].ErrOrNew(err) s.sendAPIErrResponse(ci, acc, subject, reply, smsg, s.jsonResponse(&resp)) return } @@ -3069,7 +3018,7 @@ func (s *Server) jsConsumerCreate(sub *subscription, c *client, subject, reply s var req CreateConsumerRequest if err := json.Unmarshal(msg, &req); err != nil { - resp.Error = jsInvalidJSONErr + resp.Error = ApiErrors[JSInvalidJSONErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -3087,7 +3036,7 @@ func (s *Server) jsConsumerCreate(sub *subscription, c *client, subject, reply s return } if js.isLeaderless() { - resp.Error = jsClusterNotAvailErr + resp.Error = ApiErrors[JSClusterNotAvailErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -3100,43 +3049,43 @@ func (s *Server) jsConsumerCreate(sub *subscription, c *client, subject, reply s if hasJS, doErr := acc.checkJetStream(); !hasJS { if doErr { - resp.Error = jsNotEnabledErr + resp.Error = ApiErrors[JSNotEnabledErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) } return } if streamName != req.Stream { - resp.Error = jsStreamMismatchErr + resp.Error = ApiErrors[JSStreamMismatchErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } if expectDurable { if numTokens(subject) != 7 { - resp.Error = &ApiError{Code: 400, Description: "consumer expected to be durable but no durable name set in subject"} + resp.Error = ApiErrors[JSConsumerDurableNameNotInSubjectErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } // Now check on requirements for durable request. if req.Config.Durable == _EMPTY_ { - resp.Error = &ApiError{Code: 400, Description: "consumer expected to be durable but a durable name was not set"} + resp.Error = ApiErrors[JSConsumerDurableNameNotSetErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } consumerName := tokenAt(subject, 7) if consumerName != req.Config.Durable { - resp.Error = &ApiError{Code: 400, Description: "consumer name in subject does not match durable name in request"} + resp.Error = ApiErrors[JSConsumerDurableNameNotMatchSubjectErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } } else { if numTokens(subject) != 5 { - resp.Error = &ApiError{Code: 400, Description: "consumer expected to be ephemeral but detected a durable name set in subject"} + resp.Error = ApiErrors[JSConsumerEphemeralWithDurableInSubjectErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } if req.Config.Durable != _EMPTY_ { - resp.Error = &ApiError{Code: 400, Description: "consumer expected to be ephemeral but a durable name was set in request"} + resp.Error = ApiErrors[JSConsumerEphemeralWithDurableNameErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -3149,14 +3098,14 @@ func (s *Server) jsConsumerCreate(sub *subscription, c *client, subject, reply s stream, err := acc.lookupStream(req.Stream) if err != nil { - resp.Error = jsNotFoundError(err) + resp.Error = ApiErrors[JSStreamNotFoundErr].ErrOrNew(err) s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } o, err := stream.addConsumer(&req.Config) if err != nil { - resp.Error = jsError(err) + resp.Error = ApiErrors[JSConsumerCreateErrF].ErrOrNew(err) s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -3187,7 +3136,7 @@ func (s *Server) jsConsumerNamesRequest(sub *subscription, c *client, subject, r return } if js.isLeaderless() { - resp.Error = jsClusterNotAvailErr + resp.Error = ApiErrors[JSClusterNotAvailErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -3199,7 +3148,7 @@ func (s *Server) jsConsumerNamesRequest(sub *subscription, c *client, subject, r if hasJS, doErr := acc.checkJetStream(); !hasJS { if doErr { - resp.Error = jsNotEnabledErr + resp.Error = ApiErrors[JSNotEnabledErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) } return @@ -3209,7 +3158,7 @@ func (s *Server) jsConsumerNamesRequest(sub *subscription, c *client, subject, r if !isEmptyRequest(msg) { var req JSApiConsumersRequest if err := json.Unmarshal(msg, &req); err != nil { - resp.Error = jsInvalidJSONErr + resp.Error = ApiErrors[JSInvalidJSONErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -3229,14 +3178,14 @@ func (s *Server) jsConsumerNamesRequest(sub *subscription, c *client, subject, r sas := cc.streams[acc.Name] if sas == nil { js.mu.RUnlock() - resp.Error = jsNotFoundError(ErrJetStreamNotEnabled) + resp.Error = ApiErrors[JSStreamNotFoundErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } sa := sas[streamName] if sa == nil || sa.err != nil { js.mu.RUnlock() - resp.Error = jsNotFoundError(ErrJetStreamStreamNotFound) + resp.Error = ApiErrors[JSStreamNotFoundErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -3256,7 +3205,7 @@ func (s *Server) jsConsumerNamesRequest(sub *subscription, c *client, subject, r } else { mset, err := acc.lookupStream(streamName) if err != nil { - resp.Error = jsNotFoundError(err) + resp.Error = ApiErrors[JSStreamNotFoundErr].ErrOrNew(err) s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -3308,7 +3257,7 @@ func (s *Server) jsConsumerListRequest(sub *subscription, c *client, subject, re return } if js.isLeaderless() { - resp.Error = jsClusterNotAvailErr + resp.Error = ApiErrors[JSClusterNotAvailErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -3320,7 +3269,7 @@ func (s *Server) jsConsumerListRequest(sub *subscription, c *client, subject, re if hasJS, doErr := acc.checkJetStream(); !hasJS { if doErr { - resp.Error = jsNotEnabledErr + resp.Error = ApiErrors[JSNotEnabledErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) } return @@ -3330,7 +3279,7 @@ func (s *Server) jsConsumerListRequest(sub *subscription, c *client, subject, re if !isEmptyRequest(msg) { var req JSApiConsumersRequest if err := json.Unmarshal(msg, &req); err != nil { - resp.Error = jsInvalidJSONErr + resp.Error = ApiErrors[JSInvalidJSONErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -3350,7 +3299,7 @@ func (s *Server) jsConsumerListRequest(sub *subscription, c *client, subject, re mset, err := acc.lookupStream(streamName) if err != nil { - resp.Error = jsNotFoundError(err) + resp.Error = ApiErrors[JSStreamNotFoundErr].ErrOrNew(err) s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -3413,23 +3362,23 @@ func (s *Server) jsConsumerInfoRequest(sub *subscription, c *client, subject, re // We can't find the consumer, so mimic what would be the errors below. if hasJS, doErr := acc.checkJetStream(); !hasJS { if doErr { - resp.Error = jsNotEnabledErr + resp.Error = ApiErrors[JSNotEnabledErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) } return } if sa == nil { - resp.Error = jsNotFoundError(ErrJetStreamStreamNotFound) + resp.Error = ApiErrors[JSStreamNotFoundErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } // If we are here the consumer is not present. - resp.Error = jsNoConsumerErr + resp.Error = ApiErrors[JSConsumerNotFoundErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } else if ca == nil { if js.isLeaderless() { - resp.Error = jsClusterNotAvailErr + resp.Error = ApiErrors[JSClusterNotAvailErr] // Delaying an error response gives the leader a chance to respond before us s.sendDelayedAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp), nil) } @@ -3438,7 +3387,7 @@ func (s *Server) jsConsumerInfoRequest(sub *subscription, c *client, subject, re // Check to see if we are a member of the group and if the group has no leader. if js.isGroupLeaderless(ca.Group) { - resp.Error = jsClusterNotAvailErr + resp.Error = ApiErrors[JSClusterNotAvailErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -3446,13 +3395,13 @@ func (s *Server) jsConsumerInfoRequest(sub *subscription, c *client, subject, re // We have the consumer assigned and a leader, so only the consumer leader should answer. if !acc.JetStreamIsConsumerLeader(streamName, consumerName) { if js.isLeaderless() { - resp.Error = jsClusterNotAvailErr + resp.Error = ApiErrors[JSClusterNotAvailErr] // Delaying an error response gives the leader a chance to respond before us s.sendDelayedAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp), ca.Group) } else if rg := ca.Group; rg != nil && rg.node != nil && rg.isMember(cc.meta.ID()) { // Check here if we are a member and this is just a new consumer that does not have a leader yet. if rg.node.GroupLeader() == _EMPTY_ && !rg.node.HadPreviousLeader() { - resp.Error = jsNoConsumerErr + resp.Error = ApiErrors[JSConsumerNotFoundErr] s.sendDelayedAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp), nil) } } @@ -3461,26 +3410,26 @@ func (s *Server) jsConsumerInfoRequest(sub *subscription, c *client, subject, re } if !acc.JetStreamEnabled() { - resp.Error = jsNotEnabledErr + resp.Error = ApiErrors[JSNotEnabledErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } if !isEmptyRequest(msg) { - resp.Error = jsNotEmptyRequestErr + resp.Error = ApiErrors[JSNotEmptyRequestErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } mset, err := acc.lookupStream(streamName) if err != nil { - resp.Error = jsNotFoundError(err) + resp.Error = ApiErrors[JSStreamNotFoundErr].ErrOrNew(err) s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } obs := mset.lookupConsumer(consumerName) if obs == nil { - resp.Error = jsNoConsumerErr + resp.Error = ApiErrors[JSConsumerNotFoundErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -3508,7 +3457,7 @@ func (s *Server) jsConsumerDeleteRequest(sub *subscription, c *client, subject, return } if js.isLeaderless() { - resp.Error = jsClusterNotAvailErr + resp.Error = ApiErrors[JSClusterNotAvailErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -3520,13 +3469,13 @@ func (s *Server) jsConsumerDeleteRequest(sub *subscription, c *client, subject, if hasJS, doErr := acc.checkJetStream(); !hasJS { if doErr { - resp.Error = jsNotEnabledErr + resp.Error = ApiErrors[JSNotEnabledErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) } return } if !isEmptyRequest(msg) { - resp.Error = jsNotEmptyRequestErr + resp.Error = ApiErrors[JSNotEmptyRequestErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } @@ -3540,19 +3489,19 @@ func (s *Server) jsConsumerDeleteRequest(sub *subscription, c *client, subject, mset, err := acc.lookupStream(stream) if err != nil { - resp.Error = jsNotFoundError(err) + resp.Error = ApiErrors[JSStreamNotFoundErr].ErrOrNew(err) s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } obs := mset.lookupConsumer(consumer) if obs == nil { - resp.Error = jsNoConsumerErr + resp.Error = ApiErrors[JSConsumerNotFoundErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } if err := obs.delete(); err != nil { - resp.Error = jsError(err) + resp.Error = ApiErrors[JSStreamGeneralErrorF].ErrOrNew(err) s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) return } diff --git a/server/jetstream_cluster.go b/server/jetstream_cluster.go index 1f715187..0392f2d8 100644 --- a/server/jetstream_cluster.go +++ b/server/jetstream_cluster.go @@ -204,7 +204,7 @@ func (s *Server) JetStreamIsCurrent() bool { func (s *Server) JetStreamSnapshotMeta() error { js := s.getJetStream() if js == nil { - return ErrJetStreamNotEnabled + return ApiErrors[JSNotEnabledErr] } js.mu.RLock() cc := js.cluster @@ -222,10 +222,10 @@ func (s *Server) JetStreamSnapshotMeta() error { func (s *Server) JetStreamStepdownStream(account, stream string) error { js, cc := s.getJetStreamCluster() if js == nil { - return ErrJetStreamNotEnabled + return ApiErrors[JSNotEnabledErr] } if cc == nil { - return ErrJetStreamNotClustered + return ApiErrors[JSClusterNotActiveErr] } // Grab account acc, err := s.LookupAccount(account) @@ -248,10 +248,10 @@ func (s *Server) JetStreamStepdownStream(account, stream string) error { func (s *Server) JetStreamSnapshotStream(account, stream string) error { js, cc := s.getJetStreamCluster() if js == nil { - return ErrJetStreamNotEnabled + return ApiErrors[JSNotEnabledErr] } if cc == nil { - return ErrJetStreamNotClustered + return ApiErrors[JSClusterNotActiveErr] } // Grab account acc, err := s.LookupAccount(account) @@ -267,7 +267,7 @@ func (s *Server) JetStreamSnapshotStream(account, stream string) error { mset.mu.RLock() if !mset.node.Leader() { mset.mu.RUnlock() - return ErrJetStreamNotLeader + return ApiErrors[JSNotEnabledErr] } n := mset.node mset.mu.RUnlock() @@ -435,7 +435,7 @@ func (s *Server) enableJetStreamClustering() error { } js := s.getJetStream() if js == nil { - return ErrJetStreamNotEnabled + return ApiErrors[JSNotEnabledErr] } // Already set. if js.cluster != nil { @@ -1191,7 +1191,7 @@ func (js *jetStream) createRaftGroup(rg *raftGroup, storage StorageType) error { s, cc := js.srv, js.cluster if cc == nil || cc.meta == nil { - return ErrJetStreamNotClustered + return ApiErrors[JSClusterNotActiveErr] } // If this is a single peer raft group or we are not a member return. @@ -1435,7 +1435,7 @@ func (js *jetStream) monitorStream(mset *stream, sa *streamAssignment) { Stream: sa.Config.Name, Restore: &JSApiStreamRestoreResponse{ApiResponse: ApiResponse{Type: JSApiStreamRestoreResponseType}}, } - result.Restore.Error = jsError(sa.err) + result.Restore.Error = ApiErrors[JSStreamAssignmentErrF].ErrOrNewT(sa.err, "{err}", sa.err) js.mu.Unlock() // Send response to the metadata leader. They will forward to the user as needed. s.sendInternalMsgLocked(streamAssignmentSubj, _EMPTY_, nil, result) @@ -1630,10 +1630,10 @@ func (js *jetStream) applyStreamEntries(mset *stream, ce *CommittedEntry, isReco if isLeader && !isRecovering { var resp = JSApiMsgDeleteResponse{ApiResponse: ApiResponse{Type: JSApiMsgDeleteResponseType}} if err != nil { - resp.Error = jsError(err) + resp.Error = ApiErrors[JSStreamMsgDeleteFailedF].ErrOrNewT(err, "{err}", err) s.sendAPIErrResponse(md.Client, mset.account(), md.Subject, md.Reply, _EMPTY_, s.jsonResponse(resp)) } else if !removed { - resp.Error = &ApiError{Code: 400, Description: fmt.Sprintf("sequence [%d] not found", md.Seq)} + resp.Error = ApiErrors[JSSequenceNotFoundErrF].NewT("{seq}", md.Seq) s.sendAPIErrResponse(md.Client, mset.account(), md.Subject, md.Reply, _EMPTY_, s.jsonResponse(resp)) } else { resp.Success = true @@ -1667,7 +1667,7 @@ func (js *jetStream) applyStreamEntries(mset *stream, ce *CommittedEntry, isReco if isLeader && !isRecovering { var resp = JSApiStreamPurgeResponse{ApiResponse: ApiResponse{Type: JSApiStreamPurgeResponseType}} if err != nil { - resp.Error = jsError(err) + resp.Error = ApiErrors[JSStreamGeneralErrorF].ErrOrNewT(err, "{err}", err) s.sendAPIErrResponse(sp.Client, mset.account(), sp.Subject, sp.Reply, _EMPTY_, s.jsonResponse(resp)) } else { resp.Purged = purged @@ -1779,7 +1779,7 @@ func (js *jetStream) processStreamLeaderChange(mset *stream, isLeader bool) { // Send our response. var resp = JSApiStreamCreateResponse{ApiResponse: ApiResponse{Type: JSApiStreamCreateResponseType}} if err != nil { - resp.Error = jsError(err) + resp.Error = ApiErrors[JSStreamCreateErrF].ErrOrNewT(err, "{err}", err) s.sendAPIErrResponse(client, acc, subject, reply, _EMPTY_, s.jsonResponse(&resp)) } else { resp.StreamInfo = &StreamInfo{ @@ -1920,7 +1920,7 @@ func (js *jetStream) processStreamAssignment(sa *streamAssignment) bool { Stream: stream, Response: &JSApiStreamCreateResponse{ApiResponse: ApiResponse{Type: JSApiStreamCreateResponseType}}, } - result.Response.Error = jsNoAccountErr + result.Response.Error = ApiErrors[JSNoAccountErr] s.sendInternalMsgLocked(streamAssignmentSubj, _EMPTY_, nil, result) s.Warnf(ll) } else { @@ -2071,7 +2071,7 @@ func (js *jetStream) processClusterUpdateStream(acc *Account, sa *streamAssignme Response: &JSApiStreamCreateResponse{ApiResponse: ApiResponse{Type: JSApiStreamCreateResponseType}}, Update: true, } - result.Response.Error = jsError(err) + result.Response.Error = ApiErrors[JSStreamGeneralErrorF].ErrOrNewT(err, "{err}", err) js.mu.Unlock() // Send response to the metadata leader. They will forward to the user as needed. @@ -2141,7 +2141,7 @@ func (js *jetStream) processClusterCreateStream(acc *Account, sa *streamAssignme s.Warnf("JetStream cluster error updating stream %q for account %q: %v", sa.Config.Name, acc.Name, err) mset.setStreamAssignment(osa) } - } else if err == ErrJetStreamStreamNotFound { + } else if err == ApiErrors[JSStreamNotFoundErr] { // Add in the stream here. mset, err = acc.addStreamWithAssignment(sa.Config, nil, sa) } @@ -2174,7 +2174,7 @@ func (js *jetStream) processClusterCreateStream(acc *Account, sa *streamAssignme Stream: sa.Config.Name, Response: &JSApiStreamCreateResponse{ApiResponse: ApiResponse{Type: JSApiStreamCreateResponseType}}, } - result.Response.Error = jsError(err) + result.Response.Error = ApiErrors[JSStreamCreateErrF].ErrOrNewT(err, "{err}", err) } js.mu.Unlock() @@ -2218,7 +2218,7 @@ func (js *jetStream) processClusterCreateStream(acc *Account, sa *streamAssignme Stream: sa.Config.Name, Restore: &JSApiStreamRestoreResponse{ApiResponse: ApiResponse{Type: JSApiStreamRestoreResponseType}}, } - result.Restore.Error = jsError(sa.err) + result.Restore.Error = ApiErrors[JSStreamRestoreErrF].ErrOrNewT(sa.err, "{err}", sa.err) js.mu.Unlock() // Send response to the metadata leader. They will forward to the user as needed. b, _ := json.Marshal(result) // Avoids auto-processing and doing fancy json with newlines. @@ -2331,7 +2331,7 @@ func (js *jetStream) processClusterDeleteStream(sa *streamAssignment, isMember, // Go ahead and delete the stream. mset, err := acc.lookupStream(sa.Config.Name) if err != nil { - resp.Error = jsNotFoundError(err) + resp.Error = ApiErrors[JSStreamNotFoundErr].ErrOrNewT(err, "{err}", err) } else if mset != nil { err = mset.stop(true, wasLeader) } @@ -2346,7 +2346,7 @@ func (js *jetStream) processClusterDeleteStream(sa *streamAssignment, isMember, if err != nil { if resp.Error == nil { - resp.Error = jsError(err) + resp.Error = ApiErrors[JSStreamGeneralErrorF].ErrOrNewT(err, "{err}", err) } s.sendAPIErrResponse(sa.Client, acc, sa.Subject, sa.Reply, _EMPTY_, s.jsonResponse(resp)) } else { @@ -2386,7 +2386,7 @@ func (js *jetStream) processConsumerAssignment(ca *consumerAssignment) { Consumer: consumer, Response: &JSApiConsumerCreateResponse{ApiResponse: ApiResponse{Type: JSApiConsumerCreateResponseType}}, } - result.Response.Error = jsNoAccountErr + result.Response.Error = ApiErrors[JSNoAccountErr] s.sendInternalMsgLocked(consumerAssignmentSubj, _EMPTY_, nil, result) s.Warnf(ll) } else { @@ -2492,14 +2492,14 @@ func (js *jetStream) processClusterCreateConsumer(ca *consumerAssignment, state if err != nil { js.mu.Lock() s.Debugf("Consumer create failed, could not locate stream '%s > %s'", ca.Client.serviceAccount(), ca.Stream) - ca.err = ErrJetStreamStreamNotFound + ca.err = ApiErrors[JSStreamNotFoundErr] result := &consumerAssignmentResult{ Account: ca.Client.serviceAccount(), Stream: ca.Stream, Consumer: ca.Name, Response: &JSApiConsumerCreateResponse{ApiResponse: ApiResponse{Type: JSApiConsumerCreateResponseType}}, } - result.Response.Error = jsNotFoundError(ErrJetStreamStreamNotFound) + result.Response.Error = ApiErrors[JSStreamNotFoundErr] s.sendInternalMsgLocked(consumerAssignmentSubj, _EMPTY_, nil, result) js.mu.Unlock() return @@ -2524,7 +2524,7 @@ func (js *jetStream) processClusterCreateConsumer(ca *consumerAssignment, state Consumer: ca.Name, Response: &JSApiConsumerCreateResponse{ApiResponse: ApiResponse{Type: JSApiConsumerCreateResponseType}}, } - result.Response.Error = jsNotFoundError(ErrJetStreamConsumerAlreadyUsed) + result.Response.Error = ApiErrors[JSConsumerNameExistErr] s.sendInternalMsgLocked(consumerAssignmentSubj, _EMPTY_, nil, result) js.mu.Unlock() return @@ -2568,7 +2568,7 @@ func (js *jetStream) processClusterCreateConsumer(ca *consumerAssignment, state Consumer: ca.Name, Response: &JSApiConsumerCreateResponse{ApiResponse: ApiResponse{Type: JSApiConsumerCreateResponseType}}, } - result.Response.Error = jsError(err) + result.Response.Error = ApiErrors[JSConsumerCreateErrF].ErrOrNewT(err, "{err}", err) } else if err == errNoInterest { // This is a stranded ephemeral, let's clean this one up. subject := fmt.Sprintf(JSApiConsumerDeleteT, ca.Stream, ca.Name) @@ -2614,12 +2614,12 @@ func (js *jetStream) processClusterDeleteConsumer(ca *consumerAssignment, isMemb // Go ahead and delete the consumer. mset, err := acc.lookupStream(ca.Stream) if err != nil { - resp.Error = jsNotFoundError(err) + resp.Error = ApiErrors[JSStreamNotFoundErr].ErrOrNewT(err, "{err}", err) } else if mset != nil { if o := mset.lookupConsumer(ca.Name); o != nil { err = o.stopWithFlags(true, true, wasLeader) } else { - resp.Error = jsNoConsumerErr + resp.Error = ApiErrors[JSConsumerNotFoundErr] } } @@ -2633,7 +2633,7 @@ func (js *jetStream) processClusterDeleteConsumer(ca *consumerAssignment, isMemb if err != nil { if resp.Error == nil { - resp.Error = jsError(err) + resp.Error = ApiErrors[JSStreamNotFoundErr].ErrOrNewT(err, "{err}", err) } s.sendAPIErrResponse(ca.Client, acc, ca.Subject, ca.Reply, _EMPTY_, s.jsonResponse(resp)) } else { @@ -2968,7 +2968,7 @@ func (js *jetStream) processConsumerLeaderChange(o *consumer, isLeader bool) { var resp = JSApiConsumerCreateResponse{ApiResponse: ApiResponse{Type: JSApiConsumerCreateResponseType}} if err != nil { - resp.Error = jsError(err) + resp.Error = ApiErrors[JSConsumerCreateErrF].ErrOrNewT(err, "{err}", err) s.sendAPIErrResponse(client, acc, subject, reply, _EMPTY_, s.jsonResponse(&resp)) } else { resp.ConsumerInfo = o.info() @@ -3102,7 +3102,7 @@ func (js *jetStream) processStreamAssignmentResults(sub *subscription, c *client // TODO(dlc) - Could have mixed results, should track per peer. // Set sa.err while we are deleting so we will not respond to list/names requests. if !result.Update && time.Since(sa.Created) < 5*time.Second { - sa.err = ErrJetStreamNotAssigned + sa.err = ApiErrors[JSClusterNotAssignedErr] cc.meta.Propose(encodeDeleteStreamAssignment(sa)) } } @@ -3133,7 +3133,7 @@ func (js *jetStream) processConsumerAssignmentResults(sub *subscription, c *clie // TODO(dlc) - Could have mixed results, should track per peer. if result.Response.Error != nil { // So while we are deleting we will not respond to list/names requests. - ca.err = ErrJetStreamNotAssigned + ca.err = ApiErrors[JSClusterNotAssignedErr] cc.meta.Propose(encodeDeleteConsumerAssignment(ca)) } } @@ -3319,14 +3319,14 @@ func (s *Server) jsClusteredStreamRequest(ci *ClientInfo, acc *Account, subject, acc.mu.RUnlock() if jsa == nil { - resp.Error = jsNotEnabledErr + resp.Error = ApiErrors[JSNotEnabledErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) return } ccfg, err := checkStreamCfg(config) if err != nil { - resp.Error = jsError(err) + resp.Error = ApiErrors[JSStreamInvalidConfigF].ErrOrNewT(err, "{err}", err) s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) return } @@ -3343,14 +3343,14 @@ func (s *Server) jsClusteredStreamRequest(ci *ClientInfo, acc *Account, subject, jsa.mu.RUnlock() if exceeded { - resp.Error = jsError(fmt.Errorf("maximum number of streams reached")) + resp.Error = ApiErrors[JSMaximumStreamsLimitErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) return } // Check for stream limits here before proposing. if err := jsa.checkLimits(cfg); err != nil { - resp.Error = jsError(err) + resp.Error = ApiErrors[JSStreamLimitsErrF].ErrOrNewT(err, "{err}", err) s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) return } @@ -3360,7 +3360,7 @@ func (s *Server) jsClusteredStreamRequest(ci *ClientInfo, acc *Account, subject, defer js.mu.Unlock() if sa := js.streamAssignment(acc.Name, cfg.Name); sa != nil { - resp.Error = jsError(ErrJetStreamStreamAlreadyUsed) + resp.Error = ApiErrors[JSStreamNameExistErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) return } @@ -3370,7 +3370,7 @@ func (s *Server) jsClusteredStreamRequest(ci *ClientInfo, acc *Account, subject, for _, subj := range sa.Config.Subjects { for _, tsubj := range cfg.Subjects { if SubjectsCollide(tsubj, subj) { - resp.Error = jsError(fmt.Errorf("subjects overlap with an existing stream")) + resp.Error = ApiErrors[JSStreamSubjectOverlapErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) return } @@ -3381,7 +3381,7 @@ func (s *Server) jsClusteredStreamRequest(ci *ClientInfo, acc *Account, subject, // Raft group selection and placement. rg := cc.createGroupForStream(ci, cfg) if rg == nil { - resp.Error = jsInsufficientErr + resp.Error = ApiErrors[JSInsufficientResourcesErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) return } @@ -3407,32 +3407,32 @@ func (s *Server) jsClusteredStreamUpdateRequest(ci *ClientInfo, acc *Account, su osa := js.streamAssignment(acc.Name, cfg.Name) if osa == nil { - resp.Error = jsNotFoundError(ErrJetStreamStreamNotFound) + resp.Error = ApiErrors[JSStreamNotFoundErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) return } var newCfg *StreamConfig if jsa := js.accounts[acc.Name]; jsa != nil { if ncfg, err := jsa.configUpdateCheck(osa.Config, cfg); err != nil { - resp.Error = jsError(err) + resp.Error = ApiErrors[JSStreamUpdateErrF].ErrOrNewT(err, "{err}", err) s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) return } else { newCfg = ncfg } } else { - resp.Error = jsNotEnabledErr + resp.Error = ApiErrors[JSNotEnabledErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) return } // Check for cluster changes that we want to error on. if newCfg.Replicas != len(osa.Group.Peers) { - resp.Error = &ApiError{Code: 400, Description: "Replicas configuration can not be updated"} + resp.Error = ApiErrors[JSStreamReplicasNotUpdatableErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) return } if !reflect.DeepEqual(newCfg.Mirror, osa.Config.Mirror) { - resp.Error = &ApiError{Code: 400, Description: "Mirror configuration can not be updated"} + resp.Error = ApiErrors[JSStreamMirrorNotUpdatableErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) return } @@ -3445,7 +3445,7 @@ func (s *Server) jsClusteredStreamUpdateRequest(ci *ClientInfo, acc *Account, su for _, subj := range sa.Config.Subjects { for _, tsubj := range newCfg.Subjects { if SubjectsCollide(tsubj, subj) { - resp.Error = jsError(fmt.Errorf("subjects overlap with an existing stream")) + resp.Error = ApiErrors[JSStreamSubjectOverlapErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) return } @@ -3469,7 +3469,7 @@ func (s *Server) jsClusteredStreamDeleteRequest(ci *ClientInfo, acc *Account, st osa := js.streamAssignment(acc.Name, stream) if osa == nil { var resp = JSApiStreamDeleteResponse{ApiResponse: ApiResponse{Type: JSApiStreamDeleteResponseType}} - resp.Error = jsNotFoundError(ErrJetStreamStreamNotFound) + resp.Error = ApiErrors[JSStreamNotFoundErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) return } @@ -3495,7 +3495,7 @@ func (s *Server) jsClusteredStreamPurgeRequest(ci *ClientInfo, acc *Account, mse sa := js.streamAssignment(acc.Name, stream) if sa == nil { resp := JSApiStreamPurgeResponse{ApiResponse: ApiResponse{Type: JSApiStreamPurgeResponseType}} - resp.Error = jsNotFoundError(ErrJetStreamStreamNotFound) + resp.Error = ApiErrors[JSStreamNotFoundErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) return } @@ -3507,7 +3507,7 @@ func (s *Server) jsClusteredStreamPurgeRequest(ci *ClientInfo, acc *Account, mse var resp = JSApiStreamPurgeResponse{ApiResponse: ApiResponse{Type: JSApiStreamPurgeResponseType}} purged, err := mset.purge() if err != nil { - resp.Error = jsError(err) + resp.Error = ApiErrors[JSStreamGeneralErrorF].ErrOrNewT(err, "{err}", err) } else { resp.Purged = purged resp.Success = true @@ -3529,7 +3529,7 @@ func (s *Server) jsClusteredStreamRestoreRequest(ci *ClientInfo, acc *Account, r resp := JSApiStreamRestoreResponse{ApiResponse: ApiResponse{Type: JSApiStreamRestoreResponseType}} if sa := js.streamAssignment(ci.serviceAccount(), cfg.Name); sa != nil { - resp.Error = jsError(ErrJetStreamStreamAlreadyUsed) + resp.Error = ApiErrors[JSStreamNameExistErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) return } @@ -3537,7 +3537,7 @@ func (s *Server) jsClusteredStreamRestoreRequest(ci *ClientInfo, acc *Account, r // Raft group selection and placement. rg := cc.createGroupForStream(ci, cfg) if rg == nil { - resp.Error = jsInsufficientErr + resp.Error = ApiErrors[JSInsufficientResourcesErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) return } @@ -3665,7 +3665,7 @@ LOOP: return case <-notActive.C: s.Warnf("Did not receive all stream info results for %q", acc) - resp.Error = jsClusterIncompleteErr + resp.Error = ApiErrors[JSClusterIncompleteErr] break LOOP case si := <-rc: resp.Streams = append(resp.Streams, si) @@ -3843,19 +3843,19 @@ func (s *Server) jsClusteredConsumerDeleteRequest(ci *ClientInfo, acc *Account, sa := js.streamAssignment(acc.Name, stream) if sa == nil { - resp.Error = jsNotFoundError(ErrJetStreamStreamNotFound) + resp.Error = ApiErrors[JSStreamNotFoundErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) return } if sa.consumers == nil { - resp.Error = jsNoConsumerErr + resp.Error = ApiErrors[JSConsumerNotFoundErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) return } oca := sa.consumers[consumer] if oca == nil { - resp.Error = jsNoConsumerErr + resp.Error = ApiErrors[JSConsumerNotFoundErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) return } @@ -3905,9 +3905,9 @@ func (s *Server) jsClusteredMsgDeleteRequest(ci *ClientInfo, acc *Account, mset } var resp = JSApiMsgDeleteResponse{ApiResponse: ApiResponse{Type: JSApiMsgDeleteResponseType}} if err != nil { - resp.Error = jsError(err) + resp.Error = ApiErrors[JSStreamMsgDeleteFailedF].ErrOrNewT(err, "{err}", err) } else if !removed { - resp.Error = &ApiError{Code: 400, Description: fmt.Sprintf("sequence [%d] not found", req.Seq)} + resp.Error = ApiErrors[JSSequenceNotFoundErrF].NewT("{seq}", req.Seq) } else { resp.Success = true } @@ -3966,7 +3966,7 @@ func (s *Server) jsClusteredConsumerRequest(ci *ClientInfo, acc *Account, subjec // Lookup the stream assignment. sa := js.streamAssignment(acc.Name, stream) if sa == nil { - resp.Error = jsError(ErrJetStreamStreamNotFound) + resp.Error = ApiErrors[JSStreamNotFoundErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) return } @@ -3986,7 +3986,7 @@ func (s *Server) jsClusteredConsumerRequest(ci *ClientInfo, acc *Account, subjec rg := cc.createGroupForConsumer(sa) if rg == nil { - resp.Error = jsInsufficientErr + resp.Error = ApiErrors[JSInsufficientResourcesErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) return } @@ -4021,7 +4021,7 @@ func (s *Server) jsClusteredConsumerRequest(ci *ClientInfo, acc *Account, subjec shouldErr = len(rr.psubs)+len(rr.qsubs) != 0 } if shouldErr { - resp.Error = jsError(ErrJetStreamConsumerAlreadyUsed) + resp.Error = ApiErrors[JSConsumerNameExistErr] s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) return } @@ -4215,14 +4215,14 @@ func (mset *stream) processClusteredInboundMsg(subject, reply string, hdr, msg [ if js.limitsExceeded(stype) { s.resourcesExeededError() if canRespond { - b, _ := json.Marshal(&JSPubAckResponse{PubAck: &PubAck{Stream: name}, Error: jsInsufficientErr}) + b, _ := json.Marshal(&JSPubAckResponse{PubAck: &PubAck{Stream: name}, Error: ApiErrors[JSInsufficientResourcesErr]}) outq.send(&jsPubMsg{reply, _EMPTY_, _EMPTY_, nil, b, nil, 0, nil}) } // Stepdown regardless. if node := mset.raftNode(); node != nil { node.StepDown() } - return 0, ErrJetStreamResourcesExceeded + return 0, ApiErrors[JSInsufficientResourcesErr] } // Check here pre-emptively if we have exceeded our account limits. @@ -4247,7 +4247,7 @@ func (mset *stream) processClusteredInboundMsg(subject, reply string, hdr, msg [ s.Warnf(err.Error()) if canRespond { var resp = &JSPubAckResponse{PubAck: &PubAck{Stream: name}} - resp.Error = &ApiError{Code: 400, Description: "resource limits exceeded for account"} + resp.Error = ApiErrors[JSAccountResourcesExceededErr] response, _ = json.Marshal(resp) outq.send(&jsPubMsg{reply, _EMPTY_, _EMPTY_, nil, response, nil, 0, nil}) } @@ -4260,7 +4260,7 @@ func (mset *stream) processClusteredInboundMsg(subject, reply string, hdr, msg [ s.Warnf(err.Error()) if canRespond { var resp = &JSPubAckResponse{PubAck: &PubAck{Stream: name}} - resp.Error = &ApiError{Code: 400, Description: "message size exceeds maximum allowed"} + resp.Error = ApiErrors[JSStreamMessageExceedsMaximumErr] response, _ = json.Marshal(resp) outq.send(&jsPubMsg{reply, _EMPTY_, _EMPTY_, nil, response, nil, 0, nil}) } @@ -4533,7 +4533,7 @@ RETRY: } else if isOutOfSpaceErr(err) { s.handleOutOfSpace(msetName) return - } else if err == ErrJetStreamResourcesExceeded { + } else if err == ApiErrors[JSInsufficientResourcesErr] { s.resourcesExeededError() return } else { @@ -4569,7 +4569,7 @@ func (mset *stream) processCatchupMsg(msg []byte) (uint64, error) { } if mset.js.limitsExceeded(mset.cfg.Storage) { - return 0, ErrJetStreamResourcesExceeded + return 0, ApiErrors[JSInsufficientResourcesErr] } // Put into our store diff --git a/server/jetstream_cluster_test.go b/server/jetstream_cluster_test.go index b580f972..378c2c09 100644 --- a/server/jetstream_cluster_test.go +++ b/server/jetstream_cluster_test.go @@ -140,7 +140,7 @@ func TestJetStreamClusterStreamLimitWithAccountDefaults(t *testing.T) { MaxBytes: 15 * 1024 * 1024, }) if err == nil || !strings.Contains(err.Error(), "insufficient storage") { - t.Fatalf("Expected %v but got %v", ErrStorageResourcesExceeded, err) + t.Fatalf("Expected %v but got %v", ApiErrors[JSStorageResourcesExceededErr], err) } } @@ -2559,7 +2559,7 @@ func TestJetStreamClusterUserSnapshotAndRestore(t *testing.T) { t.Fatalf("Unexpected error: %v", err) } json.Unmarshal(rmsg.Data, &rresp) - if rresp.Error == nil || rresp.Error.Code != 500 || !strings.Contains(rresp.Error.Description, "already in use") { + if !IsNatsErr(rresp.Error, JSStreamNameExistErr) { t.Fatalf("Did not get correct error response: %+v", rresp.Error) } @@ -4914,7 +4914,7 @@ func TestJetStreamFailMirrorsAndSources(t *testing.T) { nc2, _ := jsClientConnect(t, s, nats.UserInfo("rip", "pass")) defer nc2.Close() - testPrefix := func(testName, error string, cfg StreamConfig) { + testPrefix := func(testName string, id ErrorIdentifier, cfg StreamConfig) { t.Run(testName, func(t *testing.T) { req, err := json.Marshal(cfg) if err != nil { @@ -4930,13 +4930,13 @@ func TestJetStreamFailMirrorsAndSources(t *testing.T) { } if scResp.Error == nil { t.Fatalf("Did expect an error but got none") - } else if !strings.Contains(scResp.Error.Description, error) { + } else if !IsNatsErr(scResp.Error, id) { t.Fatalf("Expected different error: %s", scResp.Error.Description) } }) } - testPrefix("mirror-bad-deliverprefix", `prefix "test" overlaps with stream subject "test.>"`, StreamConfig{ + testPrefix("mirror-bad-deliverprefix", JSStreamExternalDelPrefixOverlapsErrF, StreamConfig{ Name: "MY_MIRROR_TEST", Storage: FileStorage, Mirror: &StreamSource{ @@ -4948,7 +4948,7 @@ func TestJetStreamFailMirrorsAndSources(t *testing.T) { }, }, }) - testPrefix("mirror-bad-apiprefix", `api prefix "$JS.API" must not overlap`, StreamConfig{ + testPrefix("mirror-bad-apiprefix", JSStreamExternalApiOverlapErrF, StreamConfig{ Name: "MY_MIRROR_TEST", Storage: FileStorage, Mirror: &StreamSource{ @@ -4959,7 +4959,7 @@ func TestJetStreamFailMirrorsAndSources(t *testing.T) { }, }, }) - testPrefix("source-bad-deliverprefix", `prefix "test" overlaps with stream subject "test.>"`, StreamConfig{ + testPrefix("source-bad-deliverprefix", JSStreamExternalDelPrefixOverlapsErrF, StreamConfig{ Name: "MY_SOURCE_TEST", Storage: FileStorage, Sources: []*StreamSource{{ @@ -4971,7 +4971,7 @@ func TestJetStreamFailMirrorsAndSources(t *testing.T) { }, }, }) - testPrefix("source-bad-apiprefix", `api prefix "$JS.API" must not overlap`, StreamConfig{ + testPrefix("source-bad-apiprefix", JSStreamExternalApiOverlapErrF, StreamConfig{ Name: "MY_SOURCE_TEST", Storage: FileStorage, Sources: []*StreamSource{{ @@ -6112,7 +6112,7 @@ func TestJetStreamClusterDomains(t *testing.T) { // The domain signals to the system that we are our own JetStream domain and should not extend CORE. // We want to check to make sure we have all the deny properly setup. spoke.mu.Lock() - //var hasDE, hasDI bool + // var hasDE, hasDI bool for _, ln := range spoke.leafs { ln.mu.Lock() remote := ln.leaf.remote diff --git a/server/jetstream_errors.go b/server/jetstream_errors.go new file mode 100644 index 00000000..a0568677 --- /dev/null +++ b/server/jetstream_errors.go @@ -0,0 +1,105 @@ +package server + +import ( + "fmt" + "strings" +) + +type ErrorIdentifier uint16 + +// IsNatsErr determines if a error matches ID, if multiple IDs are given if the error matches any of these the function will be true +func IsNatsErr(err error, ids ...ErrorIdentifier) bool { + if err == nil { + return false + } + + ce, ok := err.(*ApiError) + if !ok || ce == nil { + return false + } + + found := false + for _, id := range ids { + ae, ok := ApiErrors[id] + if !ok || ae == nil { + continue + } + + if ce.ErrCode == ae.ErrCode { + found = true + } + } + + return found +} + +// ApiError is included in all responses if there was an error. +type ApiError struct { + Code int `json:"code"` + ErrCode uint16 `json:"err_code,omitempty"` + Description string `json:"description,omitempty"` + URL string `json:"-"` + Help string `json:"-"` +} + +func (e *ApiError) Error() string { + return fmt.Sprintf("%s (%d)", e.Description, e.ErrCode) +} + +// ErrOrNewT returns err if it's an ApiError else creates a new error using NewT() +func (e *ApiError) ErrOrNewT(err error, replacements ...interface{}) *ApiError { + if ae, ok := err.(*ApiError); ok { + return ae + } + + return e.NewT(replacements...) +} + +// ErrOrNew returns err if it's an ApiError else creates a new error +func (e *ApiError) ErrOrNew(err error) *ApiError { + if ae, ok := err.(*ApiError); ok { + return ae + } + + return e.NewT() +} + +// NewT creates a new error using strings.Replacer on the Description field, arguments must be an even number like NewT("{err}", err) +func (e *ApiError) NewT(replacements ...interface{}) *ApiError { + ne := &ApiError{ + Code: e.Code, + ErrCode: e.ErrCode, + Description: e.Description, + } + + if len(replacements) == 0 { + return ne + } + + if len(replacements)%2 != 0 { + panic("invalid error replacement") + } + + var ra []string + + var key string + for i, replacement := range replacements { + if i%2 == 0 { + key = replacement.(string) + continue + } + + switch v := replacement.(type) { + case string: + ra = append(ra, key, v) + case error: + ra = append(ra, key, v.Error()) + default: + ra = append(ra, key, fmt.Sprintf("%v", v)) + } + } + + ne.Description = strings.NewReplacer(ra...).Replace(e.Description) + + return ne +} diff --git a/server/jetstream_errors_generated.go b/server/jetstream_errors_generated.go new file mode 100644 index 00000000..892cbb6c --- /dev/null +++ b/server/jetstream_errors_generated.go @@ -0,0 +1,306 @@ +// Generated code, do not edit. See errors.json and run go generate to update + +package server + +const ( + // JSAccountResourcesExceededErr resource limits exceeded for account + JSAccountResourcesExceededErr ErrorIdentifier = 10002 + + // JSBadRequestErr bad request + JSBadRequestErr ErrorIdentifier = 10003 + + // JSClusterIncompleteErr incomplete results + JSClusterIncompleteErr ErrorIdentifier = 10004 + + // JSClusterNoPeersErr no suitable peers for placement + JSClusterNoPeersErr ErrorIdentifier = 10005 + + // JSClusterNotActiveErr JetStream not in clustered mode + JSClusterNotActiveErr ErrorIdentifier = 10006 + + // JSClusterNotAssignedErr JetStream cluster not assigned to this server + JSClusterNotAssignedErr ErrorIdentifier = 10007 + + // JSClusterNotAvailErr JetStream system temporarily unavailable + JSClusterNotAvailErr ErrorIdentifier = 10008 + + // JSClusterNotLeaderErr JetStream cluster can not handle request + JSClusterNotLeaderErr ErrorIdentifier = 10009 + + // JSClusterPeerNotMemberErr peer not a member + JSClusterPeerNotMemberErr ErrorIdentifier = 10040 + + // JSClusterRequiredErr JetStream clustering support required + JSClusterRequiredErr ErrorIdentifier = 10010 + + // JSClusterServerNotMemberErr server is not a member of the cluster + JSClusterServerNotMemberErr ErrorIdentifier = 10044 + + // JSClusterTagsErr tags placement not supported for operation + JSClusterTagsErr ErrorIdentifier = 10011 + + // JSClusterUnSupportFeatureErr not currently supported in clustered mode + JSClusterUnSupportFeatureErr ErrorIdentifier = 10036 + + // JSConsumerCreateErrF General consumer creation failure string ({err}) + JSConsumerCreateErrF ErrorIdentifier = 10012 + + // JSConsumerDurableNameNotInSubjectErr consumer expected to be durable but no durable name set in subject + JSConsumerDurableNameNotInSubjectErr ErrorIdentifier = 10016 + + // JSConsumerDurableNameNotMatchSubjectErr consumer name in subject does not match durable name in request + JSConsumerDurableNameNotMatchSubjectErr ErrorIdentifier = 10017 + + // JSConsumerDurableNameNotSetErr consumer expected to be durable but a durable name was not set + JSConsumerDurableNameNotSetErr ErrorIdentifier = 10018 + + // JSConsumerEphemeralWithDurableInSubjectErr consumer expected to be ephemeral but detected a durable name set in subject + JSConsumerEphemeralWithDurableInSubjectErr ErrorIdentifier = 10019 + + // JSConsumerEphemeralWithDurableNameErr consumer expected to be ephemeral but a durable name was set in request + JSConsumerEphemeralWithDurableNameErr ErrorIdentifier = 10020 + + // JSConsumerNameExistErr consumer name already in use + JSConsumerNameExistErr ErrorIdentifier = 10013 + + // JSConsumerNotFoundErr consumer not found + JSConsumerNotFoundErr ErrorIdentifier = 10014 + + // JSInsufficientResourcesErr insufficient resources + JSInsufficientResourcesErr ErrorIdentifier = 10023 + + // JSInvalidJSONErr invalid JSON + JSInvalidJSONErr ErrorIdentifier = 10025 + + // JSMaximumConsumersLimitErr maximum consumers exceeds account limit + JSMaximumConsumersLimitErr ErrorIdentifier = 10026 + + // JSMaximumStreamsLimitErr maximum number of streams reached + JSMaximumStreamsLimitErr ErrorIdentifier = 10027 + + // JSMemoryResourcesExceededErr insufficient memory resources available + JSMemoryResourcesExceededErr ErrorIdentifier = 10028 + + // JSMirrorConsumerSetupFailedErrF Generic mirror consumer setup failure string ({err}) + JSMirrorConsumerSetupFailedErrF ErrorIdentifier = 10029 + + // JSMirrorMaxMessageSizeTooBigErr stream mirror must have max message size >= source + JSMirrorMaxMessageSizeTooBigErr ErrorIdentifier = 10030 + + // JSMirrorWithSourcesErr stream mirrors can not also contain other sources + JSMirrorWithSourcesErr ErrorIdentifier = 10031 + + // JSMirrorWithStartSeqAndTimeErr stream mirrors can not have both start seq and start time configured + JSMirrorWithStartSeqAndTimeErr ErrorIdentifier = 10032 + + // JSMirrorWithSubjectFiltersErr stream mirrors can not contain filtered subjects + JSMirrorWithSubjectFiltersErr ErrorIdentifier = 10033 + + // JSMirrorWithSubjectsErr stream mirrors can not also contain subjects + JSMirrorWithSubjectsErr ErrorIdentifier = 10034 + + // JSNoAccountErr account not found + JSNoAccountErr ErrorIdentifier = 10035 + + // JSNoMessageFoundErr no message found + JSNoMessageFoundErr ErrorIdentifier = 10037 + + // JSNotEmptyRequestErr expected an empty request payload + JSNotEmptyRequestErr ErrorIdentifier = 10038 + + // JSNotEnabledErr JetStream not enabled for account + JSNotEnabledErr ErrorIdentifier = 10039 + + // JSPeerRemapErr peer remap failed + JSPeerRemapErr ErrorIdentifier = 10075 + + // JSRaftGeneralErrF General RAFT error string ({err}) + JSRaftGeneralErrF ErrorIdentifier = 10041 + + // JSRestoreSubscribeFailedErrF JetStream unable to subscribe to restore snapshot {subject}: {err} + JSRestoreSubscribeFailedErrF ErrorIdentifier = 10042 + + // JSSequenceNotFoundErrF sequence {seq} not found + JSSequenceNotFoundErrF ErrorIdentifier = 10043 + + // JSSnapshotDeliverSubjectInvalidErr deliver subject not valid + JSSnapshotDeliverSubjectInvalidErr ErrorIdentifier = 10015 + + // JSSourceConsumerSetupFailedErrF General source consumer setup failure string ({err}) + JSSourceConsumerSetupFailedErrF ErrorIdentifier = 10045 + + // JSSourceMaxMessageSizeTooBigErr stream source must have max message size >= target + JSSourceMaxMessageSizeTooBigErr ErrorIdentifier = 10046 + + // JSStorageResourcesExceededErr insufficient storage resources available + JSStorageResourcesExceededErr ErrorIdentifier = 10047 + + // JSStreamAssignmentErrF {err} + JSStreamAssignmentErrF ErrorIdentifier = 10048 + + // JSStreamCreateErrF Generic stream creation error string ({err}) + JSStreamCreateErrF ErrorIdentifier = 10049 + + // JSStreamDeleteErrF General stream deletion error string ({err}) + JSStreamDeleteErrF ErrorIdentifier = 10050 + + // JSStreamExternalApiOverlapErrF stream external api prefix {prefix} must not overlap with {subject} + JSStreamExternalApiOverlapErrF ErrorIdentifier = 10021 + + // JSStreamExternalDelPrefixOverlapsErrF stream external delivery prefix {prefix} overlaps with stream subject {subject} + JSStreamExternalDelPrefixOverlapsErrF ErrorIdentifier = 10022 + + // JSStreamGeneralErrorF General stream failure string + JSStreamGeneralErrorF ErrorIdentifier = 10051 + + // JSStreamInvalidConfigF Stream configuration validation error string ({err}) + JSStreamInvalidConfigF ErrorIdentifier = 10052 + + // JSStreamInvalidExternalDeliverySubjErrF stream external delivery prefix {prefix} must not contain wildcards + JSStreamInvalidExternalDeliverySubjErrF ErrorIdentifier = 10024 + + // JSStreamLimitsErrF General stream limits exceeded error string ({err}) + JSStreamLimitsErrF ErrorIdentifier = 10053 + + // JSStreamMessageExceedsMaximumErr message size exceeds maximum allowed + JSStreamMessageExceedsMaximumErr ErrorIdentifier = 10054 + + // JSStreamMirrorNotUpdatableErr Mirror configuration can not be updated + JSStreamMirrorNotUpdatableErr ErrorIdentifier = 10055 + + // JSStreamMismatchErr stream name in subject does not match request + JSStreamMismatchErr ErrorIdentifier = 10056 + + // JSStreamMsgDeleteFailedF Generic message deletion failure error string (%s) + JSStreamMsgDeleteFailedF ErrorIdentifier = 10057 + + // JSStreamNameExistErr stream name already in use + JSStreamNameExistErr ErrorIdentifier = 10058 + + // JSStreamNotFoundErr stream not found + JSStreamNotFoundErr ErrorIdentifier = 10059 + + // JSStreamNotMatchErr expected stream does not match + JSStreamNotMatchErr ErrorIdentifier = 10060 + + // JSStreamReplicasNotSupportedErr replicas > 1 not supported in non-clustered mode + JSStreamReplicasNotSupportedErr ErrorIdentifier = 10074 + + // JSStreamReplicasNotUpdatableErr Replicas configuration can not be updated + JSStreamReplicasNotUpdatableErr ErrorIdentifier = 10061 + + // JSStreamRestoreErrF restore failed: {err} + JSStreamRestoreErrF ErrorIdentifier = 10062 + + // JSStreamSequenceNotMatchErr expected stream sequence does not match + JSStreamSequenceNotMatchErr ErrorIdentifier = 10063 + + // JSStreamSnapshotErrF snapshot failed: {err} + JSStreamSnapshotErrF ErrorIdentifier = 10064 + + // JSStreamSubjectOverlapErr subjects overlap with an existing stream + JSStreamSubjectOverlapErr ErrorIdentifier = 10065 + + // JSStreamTemplateCreateErrF Generic template creation failed string ({err}) + JSStreamTemplateCreateErrF ErrorIdentifier = 10066 + + // JSStreamTemplateDeleteErrF Generic stream template deletion failed error string ({err}) + JSStreamTemplateDeleteErrF ErrorIdentifier = 10067 + + // JSStreamTemplateNotFoundErr template not found + JSStreamTemplateNotFoundErr ErrorIdentifier = 10068 + + // JSStreamUpdateErrF Generic stream update error string ({err}) + JSStreamUpdateErrF ErrorIdentifier = 10069 + + // JSStreamWrongLastMsgIDErrF wrong last msg ID: {id} + JSStreamWrongLastMsgIDErrF ErrorIdentifier = 10070 + + // JSStreamWrongLastSequenceErrF wrong last sequence: {seq} + JSStreamWrongLastSequenceErrF ErrorIdentifier = 10071 + + // JSTempStorageFailedErr JetStream unable to open temp storage for restore + JSTempStorageFailedErr ErrorIdentifier = 10072 + + // JSTemplateNameNotMatchSubjectErr template name in subject does not match request + JSTemplateNameNotMatchSubjectErr ErrorIdentifier = 10073 +) + +var ( + ApiErrors = map[ErrorIdentifier]*ApiError{ + JSAccountResourcesExceededErr: {Code: 400, ErrCode: 10002, Description: "resource limits exceeded for account"}, + JSBadRequestErr: {Code: 400, ErrCode: 10003, Description: "bad request"}, + JSClusterIncompleteErr: {Code: 503, ErrCode: 10004, Description: "incomplete results"}, + JSClusterNoPeersErr: {Code: 400, ErrCode: 10005, Description: "no suitable peers for placement"}, + JSClusterNotActiveErr: {Code: 500, ErrCode: 10006, Description: "JetStream not in clustered mode"}, + JSClusterNotAssignedErr: {Code: 500, ErrCode: 10007, Description: "JetStream cluster not assigned to this server"}, + JSClusterNotAvailErr: {Code: 503, ErrCode: 10008, Description: "JetStream system temporarily unavailable"}, + JSClusterNotLeaderErr: {Code: 500, ErrCode: 10009, Description: "JetStream cluster can not handle request"}, + JSClusterPeerNotMemberErr: {Code: 400, ErrCode: 10040, Description: "peer not a member"}, + JSClusterRequiredErr: {Code: 503, ErrCode: 10010, Description: "JetStream clustering support required"}, + JSClusterServerNotMemberErr: {Code: 400, ErrCode: 10044, Description: "server is not a member of the cluster"}, + JSClusterTagsErr: {Code: 400, ErrCode: 10011, Description: "tags placement not supported for operation"}, + JSClusterUnSupportFeatureErr: {Code: 503, ErrCode: 10036, Description: "not currently supported in clustered mode"}, + JSConsumerCreateErrF: {Code: 500, ErrCode: 10012, Description: "{err}"}, + JSConsumerDurableNameNotInSubjectErr: {Code: 400, ErrCode: 10016, Description: "consumer expected to be durable but no durable name set in subject"}, + JSConsumerDurableNameNotMatchSubjectErr: {Code: 400, ErrCode: 10017, Description: "consumer name in subject does not match durable name in request"}, + JSConsumerDurableNameNotSetErr: {Code: 400, ErrCode: 10018, Description: "consumer expected to be durable but a durable name was not set"}, + JSConsumerEphemeralWithDurableInSubjectErr: {Code: 400, ErrCode: 10019, Description: "consumer expected to be ephemeral but detected a durable name set in subject"}, + JSConsumerEphemeralWithDurableNameErr: {Code: 400, ErrCode: 10020, Description: "consumer expected to be ephemeral but a durable name was set in request"}, + JSConsumerNameExistErr: {Code: 400, ErrCode: 10013, Description: "consumer name already in use"}, + JSConsumerNotFoundErr: {Code: 404, ErrCode: 10014, Description: "consumer not found"}, + JSInsufficientResourcesErr: {Code: 503, ErrCode: 10023, Description: "insufficient resources"}, + JSInvalidJSONErr: {Code: 400, ErrCode: 10025, Description: "invalid JSON"}, + JSMaximumConsumersLimitErr: {Code: 400, ErrCode: 10026, Description: "maximum consumers exceeds account limit"}, + JSMaximumStreamsLimitErr: {Code: 400, ErrCode: 10027, Description: "maximum number of streams reached"}, + JSMemoryResourcesExceededErr: {Code: 500, ErrCode: 10028, Description: "insufficient memory resources available"}, + JSMirrorConsumerSetupFailedErrF: {Code: 500, ErrCode: 10029, Description: "{err}"}, + JSMirrorMaxMessageSizeTooBigErr: {Code: 400, ErrCode: 10030, Description: "stream mirror must have max message size >= source"}, + JSMirrorWithSourcesErr: {Code: 400, ErrCode: 10031, Description: "stream mirrors can not also contain other sources"}, + JSMirrorWithStartSeqAndTimeErr: {Code: 400, ErrCode: 10032, Description: "stream mirrors can not have both start seq and start time configured"}, + JSMirrorWithSubjectFiltersErr: {Code: 400, ErrCode: 10033, Description: "stream mirrors can not contain filtered subjects"}, + JSMirrorWithSubjectsErr: {Code: 400, ErrCode: 10034, Description: "stream mirrors can not also contain subjects"}, + JSNoAccountErr: {Code: 503, ErrCode: 10035, Description: "account not found"}, + JSNoMessageFoundErr: {Code: 404, ErrCode: 10037, Description: "no message found"}, + JSNotEmptyRequestErr: {Code: 400, ErrCode: 10038, Description: "expected an empty request payload"}, + JSNotEnabledErr: {Code: 503, ErrCode: 10039, Description: "JetStream not enabled for account", Help: "This error indicates that JetStream is not enabled either at a global level or at global and account level"}, + JSPeerRemapErr: {Code: 503, ErrCode: 10075, Description: "peer remap failed"}, + JSRaftGeneralErrF: {Code: 500, ErrCode: 10041, Description: "{err}"}, + JSRestoreSubscribeFailedErrF: {Code: 500, ErrCode: 10042, Description: "JetStream unable to subscribe to restore snapshot {subject}: {err}"}, + JSSequenceNotFoundErrF: {Code: 400, ErrCode: 10043, Description: "sequence {seq} not found"}, + JSSnapshotDeliverSubjectInvalidErr: {Code: 400, ErrCode: 10015, Description: "deliver subject not valid"}, + JSSourceConsumerSetupFailedErrF: {Code: 500, ErrCode: 10045, Description: "{err}"}, + JSSourceMaxMessageSizeTooBigErr: {Code: 400, ErrCode: 10046, Description: "stream source must have max message size >= target"}, + JSStorageResourcesExceededErr: {Code: 500, ErrCode: 10047, Description: "insufficient storage resources available"}, + JSStreamAssignmentErrF: {Code: 500, ErrCode: 10048, Description: "{err}"}, + JSStreamCreateErrF: {Code: 500, ErrCode: 10049, Description: "{err}"}, + JSStreamDeleteErrF: {Code: 500, ErrCode: 10050, Description: "{err}"}, + JSStreamExternalApiOverlapErrF: {Code: 400, ErrCode: 10021, Description: "stream external api prefix {prefix} must not overlap with {subject}"}, + JSStreamExternalDelPrefixOverlapsErrF: {Code: 400, ErrCode: 10022, Description: "stream external delivery prefix {prefix} overlaps with stream subject {subject}"}, + JSStreamGeneralErrorF: {Code: 500, ErrCode: 10051, Description: "General stream failure string"}, + JSStreamInvalidConfigF: {Code: 500, ErrCode: 10052, Description: "{err}"}, + JSStreamInvalidExternalDeliverySubjErrF: {Code: 400, ErrCode: 10024, Description: "stream external delivery prefix {prefix} must not contain wildcards"}, + JSStreamLimitsErrF: {Code: 500, ErrCode: 10053, Description: "{err}"}, + JSStreamMessageExceedsMaximumErr: {Code: 400, ErrCode: 10054, Description: "message size exceeds maximum allowed"}, + JSStreamMirrorNotUpdatableErr: {Code: 400, ErrCode: 10055, Description: "Mirror configuration can not be updated"}, + JSStreamMismatchErr: {Code: 400, ErrCode: 10056, Description: "stream name in subject does not match request"}, + JSStreamMsgDeleteFailedF: {Code: 500, ErrCode: 10057, Description: "%s"}, + JSStreamNameExistErr: {Code: 400, ErrCode: 10058, Description: "stream name already in use"}, + JSStreamNotFoundErr: {Code: 404, ErrCode: 10059, Description: "stream not found"}, + JSStreamNotMatchErr: {Code: 400, ErrCode: 10060, Description: "expected stream does not match"}, + JSStreamReplicasNotSupportedErr: {Code: 500, ErrCode: 10074, Description: "replicas > 1 not supported in non-clustered mode"}, + JSStreamReplicasNotUpdatableErr: {Code: 400, ErrCode: 10061, Description: "Replicas configuration can not be updated"}, + JSStreamRestoreErrF: {Code: 500, ErrCode: 10062, Description: "restore failed: {err}"}, + JSStreamSequenceNotMatchErr: {Code: 503, ErrCode: 10063, Description: "expected stream sequence does not match"}, + JSStreamSnapshotErrF: {Code: 500, ErrCode: 10064, Description: "snapshot failed: {err}"}, + JSStreamSubjectOverlapErr: {Code: 500, ErrCode: 10065, Description: "subjects overlap with an existing stream"}, + JSStreamTemplateCreateErrF: {Code: 500, ErrCode: 10066, Description: "{err}"}, + JSStreamTemplateDeleteErrF: {Code: 500, ErrCode: 10067, Description: "{err}"}, + JSStreamTemplateNotFoundErr: {Code: 404, ErrCode: 10068, Description: "template not found"}, + JSStreamUpdateErrF: {Code: 500, ErrCode: 10069, Description: "{err}"}, + JSStreamWrongLastMsgIDErrF: {Code: 400, ErrCode: 10070, Description: "wrong last msg ID: {id}"}, + JSStreamWrongLastSequenceErrF: {Code: 400, ErrCode: 10071, Description: "wrong last sequence: {seq}"}, + JSTempStorageFailedErr: {Code: 500, ErrCode: 10072, Description: "JetStream unable to open temp storage for restore"}, + JSTemplateNameNotMatchSubjectErr: {Code: 400, ErrCode: 10073, Description: "template name in subject does not match request"}, + } +) diff --git a/server/jetstream_errors_test.go b/server/jetstream_errors_test.go new file mode 100644 index 00000000..99cd9143 --- /dev/null +++ b/server/jetstream_errors_test.go @@ -0,0 +1,98 @@ +package server + +import ( + "errors" + "fmt" + "testing" + "time" +) + +func TestIsNatsErr(t *testing.T) { + if !IsNatsErr(ApiErrors[JSNotEnabledErr], JSNotEnabledErr) { + t.Fatalf("Expected error match") + } + + if IsNatsErr(ApiErrors[JSNotEnabledErr], JSClusterNotActiveErr) { + t.Fatalf("Expected error mismatch") + } + + if IsNatsErr(ApiErrors[JSNotEnabledErr], JSClusterNotActiveErr, JSClusterNotAvailErr) { + t.Fatalf("Expected error mismatch") + } + + if !IsNatsErr(ApiErrors[JSNotEnabledErr], JSClusterNotActiveErr, JSNotEnabledErr) { + t.Fatalf("Expected error match") + } + + if !IsNatsErr(&ApiError{ErrCode: 10039}, 1, JSClusterNotActiveErr, JSNotEnabledErr) { + t.Fatalf("Expected error match") + } + + if IsNatsErr(&ApiError{ErrCode: 10039}, 1, 2, JSClusterNotActiveErr) { + t.Fatalf("Expected error mismatch") + } + + if IsNatsErr(nil, JSClusterNotActiveErr) { + t.Fatalf("Expected error mismatch") + } + + if IsNatsErr(errors.New("x"), JSClusterNotActiveErr) { + t.Fatalf("Expected error mismatch") + } +} + +func TestApiError_Error(t *testing.T) { + if es := ApiErrors[JSClusterNotActiveErr].Error(); es != "JetStream not in clustered mode (10006)" { + t.Fatalf("Expected 'JetStream not in clustered mode (10006)', got %q", es) + } +} + +func TestApiError_NewF(t *testing.T) { + ne := ApiErrors[JSRestoreSubscribeFailedErrF].NewT("{subject}", "the.subject", "{err}", errors.New("failed error")) + if ne.Description != "JetStream unable to subscribe to restore snapshot the.subject: failed error" { + t.Fatalf("Expected 'JetStream unable to subscribe to restore snapshot the.subject: failed error' got %q", ne.Description) + } + + if ne == ApiErrors[JSRestoreSubscribeFailedErrF] { + t.Fatalf("Expected a new instance") + } +} + +func TestApiError_ErrOrNewF(t *testing.T) { + if ne := ApiErrors[JSStreamRestoreErrF].ErrOrNewT(ApiErrors[JSNotEnabledErr], "{err}", errors.New("failed error")); !IsNatsErr(ne, JSNotEnabledErr) { + t.Fatalf("Expected JSNotEnabledErr got %s", ne) + } + + if ne := ApiErrors[JSStreamRestoreErrF].ErrOrNewT(nil, "{err}", errors.New("failed error")); !IsNatsErr(ne, JSStreamRestoreErrF) { + t.Fatalf("Expected JSStreamRestoreErrF got %s", ne) + } + + if ne := ApiErrors[JSStreamRestoreErrF].ErrOrNewT(errors.New("other error"), "{err}", errors.New("failed error")); !IsNatsErr(ne, JSStreamRestoreErrF) { + t.Fatalf("Expected JSStreamRestoreErrF got %s", ne) + } +} + +func TestApiError_ErrOrNew(t *testing.T) { + if ne := ApiErrors[JSPeerRemapErr].ErrOrNew(ApiErrors[JSNotEnabledErr]); !IsNatsErr(ne, JSNotEnabledErr) { + t.Fatalf("Expected JSNotEnabledErr got %s", ne) + } + + if ne := ApiErrors[JSPeerRemapErr].ErrOrNew(nil); !IsNatsErr(ne, JSPeerRemapErr) { + t.Fatalf("Expected JSPeerRemapErr got %s", ne) + } + + if ne := ApiErrors[JSPeerRemapErr].ErrOrNew(errors.New("other error")); !IsNatsErr(ne, JSPeerRemapErr) { + t.Fatalf("Expected JSPeerRemapErr got %s", ne) + } +} + +func TestApiError_NewT(t *testing.T) { + aerr := ApiError{ + Code: 999, + Description: "thing {string} failed on attempt {int} after {duration} with {float}: {err}", + } + + if ne := aerr.NewT("{float}", 1.1, "{err}", fmt.Errorf("simulated error"), "{string}", "hello world", "{int}", 10, "{duration}", 456*time.Millisecond); ne.Description != "thing hello world failed on attempt 10 after 456ms with 1.1: simulated error" { + t.Fatalf("Expected formatted error, got: %q", ne.Description) + } +} diff --git a/server/jetstream_test.go b/server/jetstream_test.go index 57a5b46f..be8d1f25 100644 --- a/server/jetstream_test.go +++ b/server/jetstream_test.go @@ -827,7 +827,7 @@ func TestJetStreamAddStreamCanonicalNames(t *testing.T) { expectErr := func(_ *stream, err error) { t.Helper() - if err == nil || !strings.Contains(err.Error(), "can not contain") { + if !IsNatsErr(err, JSStreamInvalidConfigF) { t.Fatalf("Expected error but got none") } } @@ -4292,7 +4292,7 @@ func TestJetStreamSnapshotsAPI(t *testing.T) { t.Fatalf("Unexpected error on snapshot request: %v", err) } json.Unmarshal(rmsg.Data, &rresp) - if rresp.Error == nil || rresp.Error.Code != 500 || !strings.Contains(rresp.Error.Description, "already in use") { + if !IsNatsErr(rresp.Error, JSStreamNameExistErr) { t.Fatalf("Did not get correct error response: %+v", rresp.Error) } @@ -7417,34 +7417,13 @@ func TestJetStreamRequestAPI(t *testing.T) { t.Fatalf("Created time seems wrong: %v\n", scResp.Created) } - checkBadRequest := func(e *ApiError, description string) { - t.Helper() - if e == nil || e.Code != 400 || e.Description != description { - t.Fatalf("Did not get proper error: %+v", e) - } - } - - checkServerError := func(e *ApiError, description string) { - t.Helper() - if e == nil || e.Code != 500 || e.Description != description { - t.Fatalf("Did not get proper server error: %+v\n", e) - } - } - - checkNotFound := func(e *ApiError, description string) { - t.Helper() - if e == nil || e.Code != 404 || e.Description != description { - t.Fatalf("Did not get proper server error: %+v\n", e) - } - } - // Check that the name in config has to match the name in the subject resp, _ = nc.Request(fmt.Sprintf(JSApiStreamCreateT, "BOB"), req, time.Second) scResp.Error, scResp.StreamInfo = nil, nil if err := json.Unmarshal(resp.Data, &scResp); err != nil { t.Fatalf("Unexpected error: %v", err) } - checkBadRequest(scResp.Error, "stream name in subject does not match request") + checkNatsError(t, scResp.Error, JSStreamMismatchErr) // Check that update works. msetCfg.Subjects = []string{"foo", "bar", "baz"} @@ -7569,7 +7548,7 @@ func TestJetStreamRequestAPI(t *testing.T) { if err = json.Unmarshal(resp.Data, &bResp); err != nil { t.Fatalf("Unexpected error: %v", err) } - checkNotFound(bResp.Error, "stream not found") + checkNatsError(t, bResp.Error, JSStreamNotFoundErr) // Now create a consumer. delivery := nats.NewInbox() @@ -7589,7 +7568,7 @@ func TestJetStreamRequestAPI(t *testing.T) { if err = json.Unmarshal(resp.Data, &ccResp); err != nil { t.Fatalf("Unexpected error: %v", err) } - checkServerError(ccResp.Error, "consumer requires interest for delivery subject when ephemeral") + checkNatsError(t, ccResp.Error, JSConsumerCreateErrF) // Now create subscription and make sure we get proper response. sub, _ := nc.SubscribeSync(delivery) @@ -7627,7 +7606,7 @@ func TestJetStreamRequestAPI(t *testing.T) { t.Fatalf("Unexpected error: %v", err) } // Since we do not have interest this should have failed. - checkBadRequest(ccResp.Error, "stream name in subject does not match request") + checkNatsError(t, ccResp.Error, JSStreamMismatchErr) // Get the list of all of the consumers for our stream. resp, err = nc.Request(fmt.Sprintf(JSApiConsumersT, msetCfg.Name), nil, time.Second) @@ -7697,7 +7676,7 @@ func TestJetStreamRequestAPI(t *testing.T) { if err = json.Unmarshal(resp.Data, &ccResp); err != nil { t.Fatalf("Unexpected error: %v", err) } - checkBadRequest(ccResp.Error, "consumer expected to be ephemeral but a durable name was set in request") + checkNatsError(t, ccResp.Error, JSConsumerEphemeralWithDurableNameErr) // Now make sure we can create a durable on the subject with the proper name. resp, err = nc.Request(fmt.Sprintf(JSApiDurableCreateT, msetCfg.Name, obsReq.Config.Durable), req, time.Second) @@ -7726,7 +7705,7 @@ func TestJetStreamRequestAPI(t *testing.T) { if err = json.Unmarshal(resp.Data, &ccResp); err != nil { t.Fatalf("Unexpected error: %v", err) } - checkBadRequest(ccResp.Error, "consumer expected to be durable but a durable name was not set") + checkNatsError(t, ccResp.Error, JSConsumerDurableNameNotSetErr) // Now delete a msg. dreq := JSApiMsgDeleteRequest{Seq: 2} @@ -7804,7 +7783,7 @@ func TestJetStreamRequestAPI(t *testing.T) { if err = json.Unmarshal(resp.Data, &stResp); err != nil { t.Fatalf("Unexpected error: %v", err) } - checkBadRequest(stResp.Error, "template name in subject does not match request") + checkNatsError(t, stResp.Error, JSTemplateNameNotMatchSubjectErr) resp, _ = nc.Request(fmt.Sprintf(JSApiTemplateCreateT, template.Name), req, time.Second) stResp.Error, stResp.StreamTemplateInfo = nil, nil @@ -7857,7 +7836,7 @@ func TestJetStreamRequestAPI(t *testing.T) { if err = json.Unmarshal(resp.Data, &tDeleteResp); err != nil { t.Fatalf("Unexpected error: %v", err) } - checkServerError(tDeleteResp.Error, "template not found") + checkNatsError(t, tDeleteResp.Error, JSStreamTemplateNotFoundErr) resp, _ = nc.Request(fmt.Sprintf(JSApiTemplateDeleteT, "ss"), nil, time.Second) tDeleteResp.Error = nil diff --git a/server/mqtt.go b/server/mqtt.go index bc109c7f..279b0d13 100644 --- a/server/mqtt.go +++ b/server/mqtt.go @@ -962,7 +962,7 @@ func (s *Server) mqttCreateAccountSessionManager(acc *Account, quitCh chan struc Retention: InterestPolicy, Replicas: as.replicas, } - if _, err := jsa.createStream(cfg); isErrorOtherThan(err, ErrJetStreamStreamAlreadyUsed) { + if _, err := jsa.createStream(cfg); isErrorOtherThan(err, JSStreamNameExistErr) { return nil, fmt.Errorf("create messages stream for account %q: %v", acc.GetName(), err) } @@ -975,7 +975,7 @@ func (s *Server) mqttCreateAccountSessionManager(acc *Account, quitCh chan struc Replicas: as.replicas, } si, err := jsa.createStream(cfg) - if isErrorOtherThan(err, ErrJetStreamStreamAlreadyUsed) { + if isErrorOtherThan(err, JSStreamNameExistErr) { return nil, fmt.Errorf("create retained messages stream for account %q: %v", acc.GetName(), err) } if err != nil { @@ -1110,18 +1110,6 @@ func (jsa *mqttJSA) newRequestEx(kind, subject string, hdr int, msg []byte, time return i, nil } -// If `e` is not nil, returns an error corresponding to e.Description, if not empty, -// or an error of the form: "code %d". -func convertApiErrorToError(e *ApiError) error { - if e == nil { - return nil - } - if e.Description == _EMPTY_ { - return fmt.Errorf("code %d", e.Code) - } - return errors.New(e.Description) -} - func (jsa *mqttJSA) createConsumer(cfg *CreateConsumerRequest) (*JSApiConsumerCreateResponse, error) { cfgb, err := json.Marshal(cfg) if err != nil { @@ -1138,7 +1126,7 @@ func (jsa *mqttJSA) createConsumer(cfg *CreateConsumerRequest) (*JSApiConsumerCr return nil, err } ccr := ccri.(*JSApiConsumerCreateResponse) - return ccr, convertApiErrorToError(ccr.Error) + return ccr, ccr.ToError() } func (jsa *mqttJSA) deleteConsumer(streamName, consName string) (*JSApiConsumerDeleteResponse, error) { @@ -1148,7 +1136,7 @@ func (jsa *mqttJSA) deleteConsumer(streamName, consName string) (*JSApiConsumerD return nil, err } cdr := cdri.(*JSApiConsumerDeleteResponse) - return cdr, convertApiErrorToError(cdr.Error) + return cdr, cdr.ToError() } func (jsa *mqttJSA) createStream(cfg *StreamConfig) (*StreamInfo, error) { @@ -1161,7 +1149,7 @@ func (jsa *mqttJSA) createStream(cfg *StreamConfig) (*StreamInfo, error) { return nil, err } scr := scri.(*JSApiStreamCreateResponse) - return scr.StreamInfo, convertApiErrorToError(scr.Error) + return scr.StreamInfo, scr.ToError() } func (jsa *mqttJSA) lookupStream(name string) (*StreamInfo, error) { @@ -1170,7 +1158,7 @@ func (jsa *mqttJSA) lookupStream(name string) (*StreamInfo, error) { return nil, err } slr := slri.(*JSApiStreamInfoResponse) - return slr.StreamInfo, convertApiErrorToError(slr.Error) + return slr.StreamInfo, slr.ToError() } func (jsa *mqttJSA) deleteStream(name string) (bool, error) { @@ -1179,7 +1167,7 @@ func (jsa *mqttJSA) deleteStream(name string) (bool, error) { return false, err } sdr := sdri.(*JSApiStreamDeleteResponse) - return sdr.Success, convertApiErrorToError(sdr.Error) + return sdr.Success, sdr.ToError() } func (jsa *mqttJSA) loadMsg(streamName string, seq uint64) (*StoredMsg, error) { @@ -1193,7 +1181,7 @@ func (jsa *mqttJSA) loadMsg(streamName string, seq uint64) (*StoredMsg, error) { return nil, err } lmr := lmri.(*JSApiMsgGetResponse) - return lmr.Message, convertApiErrorToError(lmr.Error) + return lmr.Message, lmr.ToError() } func (jsa *mqttJSA) storeMsg(subject string, headers int, msg []byte) (*JSPubAckResponse, error) { @@ -1206,7 +1194,7 @@ func (jsa *mqttJSA) storeMsgWithKind(kind, subject string, headers int, msg []by return nil, err } smr := smri.(*JSPubAckResponse) - return smr, convertApiErrorToError(smr.Error) + return smr, smr.ToError() } func (jsa *mqttJSA) deleteMsg(stream string, seq uint64) { @@ -1224,11 +1212,9 @@ func (jsa *mqttJSA) deleteMsg(stream string, seq uint64) { // ////////////////////////////////////////////////////////////////////////////// -// Returns true if `err1` is not nil and does not match `err2`, that is -// their error strings are different. -// Assumes that `err2` is never nil. -func isErrorOtherThan(err1, err2 error) bool { - return err1 != nil && err1.Error() != err2.Error() +// Returns true if `err` is not nil and does not match the api error with ErrorIdentifier id +func isErrorOtherThan(err error, id ErrorIdentifier) bool { + return err != nil && !IsNatsErr(err, id) } // Process JS API replies. @@ -1250,43 +1236,43 @@ func (as *mqttAccountSessionManager) processJSAPIReplies(_ *subscription, pc *cl case mqttJSAStreamCreate: var resp = &JSApiStreamCreateResponse{} if err := json.Unmarshal(msg, resp); err != nil { - resp.Error = jsInvalidJSONErr + resp.Error = ApiErrors[JSInvalidJSONErr] } ch <- resp case mqttJSAStreamLookup: var resp = &JSApiStreamInfoResponse{} if err := json.Unmarshal(msg, &resp); err != nil { - resp.Error = jsInvalidJSONErr + resp.Error = ApiErrors[JSInvalidJSONErr] } ch <- resp case mqttJSAStreamDel: var resp = &JSApiStreamDeleteResponse{} if err := json.Unmarshal(msg, &resp); err != nil { - resp.Error = jsInvalidJSONErr + resp.Error = ApiErrors[JSInvalidJSONErr] } ch <- resp case mqttJSAConsumerCreate: var resp = &JSApiConsumerCreateResponse{} if err := json.Unmarshal(msg, resp); err != nil { - resp.Error = jsInvalidJSONErr + resp.Error = ApiErrors[JSInvalidJSONErr] } ch <- resp case mqttJSAConsumerDel: var resp = &JSApiConsumerDeleteResponse{} if err := json.Unmarshal(msg, resp); err != nil { - resp.Error = jsInvalidJSONErr + resp.Error = ApiErrors[JSInvalidJSONErr] } ch <- resp case mqttJSAMsgStore, mqttJSASessPersist: var resp = &JSPubAckResponse{} if err := json.Unmarshal(msg, resp); err != nil { - resp.Error = jsInvalidJSONErr + resp.Error = ApiErrors[JSInvalidJSONErr] } ch <- resp case mqttJSAMsgLoad: var resp = &JSApiMsgGetResponse{} if err := json.Unmarshal(msg, resp); err != nil { - resp.Error = jsInvalidJSONErr + resp.Error = ApiErrors[JSInvalidJSONErr] } ch <- resp default: @@ -1361,7 +1347,7 @@ func (as *mqttAccountSessionManager) processSessionPersist(_ *subscription, pc * if err := json.Unmarshal(msg, par); err != nil { return } - if err := convertApiErrorToError(par.Error); err != nil { + if err := par.Error; err != nil { return } cIDHash := strings.TrimPrefix(par.Stream, mqttSessionsStreamNamePrefix) @@ -1889,13 +1875,13 @@ CREATE_STREAM: if err != nil { // Check for insufficient resources. If that is the case, and if possible, try // again with a lower replicas value. - if cfg.Replicas > 1 && err.Error() == jsInsufficientErr.Description { + if cfg.Replicas > 1 && IsNatsErr(err, JSInsufficientResourcesErr) { cfg.Replicas-- goto CREATE_STREAM } // If there is an error and not simply "already used" (which means that the // stream already exists) then we fail. - if isErrorOtherThan(err, ErrJetStreamStreamAlreadyUsed) { + if isErrorOtherThan(err, JSStreamNameExistErr) { return formatError("create session stream", err) } } diff --git a/server/stream.go b/server/stream.go index 208c6463..2f026ad2 100644 --- a/server/stream.go +++ b/server/stream.go @@ -57,14 +57,21 @@ type StreamConfig struct { Sources []*StreamSource `json:"sources,omitempty"` } -const JSApiPubAckResponseType = "io.nats.jetstream.api.v1.pub_ack_response" - // JSPubAckResponse is a formal response to a publish operation. type JSPubAckResponse struct { Error *ApiError `json:"error,omitempty"` *PubAck } +// ToError checks if the response has a error and if it does converts it to an error avoiding the pitfalls described by https://yourbasic.org/golang/gotcha-why-nil-error-not-equal-nil/ +func (r *JSPubAckResponse) ToError() error { + if r.Error == nil { + return nil + } + + return r.Error +} + // PubAck is the detail you get back from a publish to a stream that was successful. // e.g. +OK {"stream": "Orders", "seq": 22} type PubAck struct { @@ -220,8 +227,7 @@ type ddentry struct { // Replicas Range const ( - StreamDefaultReplicas = 1 - StreamMaxReplicas = 5 + StreamMaxReplicas = 5 ) // AddStream adds a stream for the given account. @@ -250,12 +256,12 @@ func (a *Account) addStreamWithAssignment(config *StreamConfig, fsConfig *FileSt // Sensible defaults. cfg, err := checkStreamCfg(config) if err != nil { - return nil, err + return nil, ApiErrors[JSStreamInvalidConfigF].ErrOrNewT(err, "{err}", err) } singleServerMode := !s.JetStreamIsClustered() && s.standAloneMode() if singleServerMode && cfg.Replicas > 1 { - return nil, ErrReplicasNotSupported + return nil, ApiErrors[JSStreamReplicasNotSupportedErr] } jsa.mu.Lock() @@ -270,7 +276,7 @@ func (a *Account) addStreamWithAssignment(config *StreamConfig, fsConfig *FileSt } return mset, nil } else { - return nil, ErrJetStreamStreamAlreadyUsed + return nil, ApiErrors[JSStreamNameExistErr] } } // Check for limits. @@ -731,7 +737,7 @@ func (jsa *jsAccount) subjectsOverlap(subjects []string) bool { return false } -// Default duplicates window. +// StreamDefaultDuplicatesWindow default duplicates window. const StreamDefaultDuplicatesWindow = 2 * time.Minute func checkStreamCfg(config *StreamConfig) (StreamConfig, error) { @@ -824,31 +830,31 @@ func (mset *stream) fileStoreConfig() (FileStoreConfig, error) { func (jsa *jsAccount) configUpdateCheck(old, new *StreamConfig) (*StreamConfig, error) { cfg, err := checkStreamCfg(new) if err != nil { - return nil, err + return nil, ApiErrors[JSStreamInvalidConfigF].ErrOrNewT(err, "{err}", err) } // Name must match. if cfg.Name != old.Name { - return nil, fmt.Errorf("stream configuration name must match original") + return nil, ApiErrors[JSStreamInvalidConfigF].NewT("{err}", "stream configuration name must match original") } // Can't change MaxConsumers for now. if cfg.MaxConsumers != old.MaxConsumers { - return nil, fmt.Errorf("stream configuration update can not change MaxConsumers") + return nil, ApiErrors[JSStreamInvalidConfigF].NewT("{err}", "stream configuration update can not change MaxConsumers") } // Can't change storage types. if cfg.Storage != old.Storage { - return nil, fmt.Errorf("stream configuration update can not change storage type") + return nil, ApiErrors[JSStreamInvalidConfigF].NewT("{err}", "stream configuration update can not change storage type") } // Can't change retention. if cfg.Retention != old.Retention { - return nil, fmt.Errorf("stream configuration update can not change retention policy") + return nil, ApiErrors[JSStreamInvalidConfigF].NewT("{err}", "stream configuration update can not change retention policy") } // Can not have a template owner for now. if old.Template != _EMPTY_ { - return nil, fmt.Errorf("stream configuration update not allowed on template owned stream") + return nil, ApiErrors[JSStreamInvalidConfigF].NewT("{err}", "stream configuration update not allowed on template owned stream") } if cfg.Template != _EMPTY_ { - return nil, fmt.Errorf("stream configuration update can not be owned by a template") + return nil, ApiErrors[JSStreamInvalidConfigF].NewT("{err}", "stream configuration update can not be owned by a template") } // Check limits. @@ -863,7 +869,7 @@ func (mset *stream) update(config *StreamConfig) error { ocfg := mset.config() cfg, err := mset.jsa.configUpdateCheck(&ocfg, config) if err != nil { - return err + return ApiErrors[JSStreamInvalidConfigF].ErrOrNewT(err, "{err}", err) } mset.mu.Lock() @@ -1323,7 +1329,7 @@ func (mset *stream) processInboundMirrorMsg(m *inMsg) bool { if node != nil { if js.limitsExceeded(stype) { s.resourcesExeededError() - err = ErrJetStreamResourcesExceeded + err = ApiErrors[JSInsufficientResourcesErr] } else { err = node.Propose(encodeStreamMsg(m.subj, _EMPTY_, m.hdr, m.msg, sseq-1, ts)) } @@ -1506,7 +1512,7 @@ func (mset *stream) setupMirrorConsumer() error { if err := json.Unmarshal(msg, &ccr); err != nil { c.Warnf("JetStream bad mirror consumer create response: %q", msg) mset.cancelMirrorConsumer() - mset.setMirrorErr(jsInvalidJSONErr) + mset.setMirrorErr(ApiErrors[JSInvalidJSONErr]) return } respCh <- &ccr @@ -1555,7 +1561,7 @@ func (mset *stream) setupMirrorConsumer() error { mset.queueInbound(msgs, subject, reply, hdr, msg) }) if err != nil { - mset.mirror.err = jsError(err) + mset.mirror.err = ApiErrors[JSMirrorConsumerSetupFailedErrF].ErrOrNewT(err, "{err}", err) mset.mirror.sub = nil mset.mirror.cname = _EMPTY_ } else { @@ -1739,7 +1745,7 @@ func (mset *stream) setSourceConsumer(iname string, seq uint64) { mset.queueInbound(si.msgs, subject, reply, hdr, msg) }) if err != nil { - si.err = jsError(err) + si.err = ApiErrors[JSSourceConsumerSetupFailedErrF].ErrOrNewT(err, "{err}", err) si.sub = nil } else { si.err = nil @@ -2531,7 +2537,7 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte, mset.mu.Unlock() if canRespond && outq != nil { resp.PubAck = &PubAck{Stream: name} - resp.Error = &ApiError{Code: 503, Description: "expected stream sequence does not match"} + resp.Error = ApiErrors[JSStreamSequenceNotMatchErr] b, _ := json.Marshal(resp) outq.send(&jsPubMsg{reply, _EMPTY_, _EMPTY_, nil, b, nil, 0, nil}) } @@ -2567,7 +2573,7 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte, mset.mu.Unlock() if canRespond { resp.PubAck = &PubAck{Stream: name} - resp.Error = &ApiError{Code: 400, Description: "expected stream does not match"} + resp.Error = ApiErrors[JSStreamNotMatchErr] b, _ := json.Marshal(resp) outq.send(&jsPubMsg{reply, _EMPTY_, _EMPTY_, nil, b, nil, 0, nil}) } @@ -2580,7 +2586,7 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte, mset.mu.Unlock() if canRespond { resp.PubAck = &PubAck{Stream: name} - resp.Error = &ApiError{Code: 400, Description: fmt.Sprintf("wrong last sequence: %d", mlseq)} + resp.Error = ApiErrors[JSStreamWrongLastSequenceErrF].NewT("{seq}", mlseq) b, _ := json.Marshal(resp) outq.send(&jsPubMsg{reply, _EMPTY_, _EMPTY_, nil, b, nil, 0, nil}) } @@ -2593,7 +2599,7 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte, mset.mu.Unlock() if canRespond { resp.PubAck = &PubAck{Stream: name} - resp.Error = &ApiError{Code: 400, Description: fmt.Sprintf("wrong last msg ID: %s", last)} + resp.Error = ApiErrors[JSStreamWrongLastMsgIDErrF].NewT("{id}", last) b, _ := json.Marshal(resp) outq.send(&jsPubMsg{reply, _EMPTY_, _EMPTY_, nil, b, nil, 0, nil}) } @@ -2614,7 +2620,7 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte, mset.mu.Unlock() if canRespond { resp.PubAck = &PubAck{Stream: name} - resp.Error = &ApiError{Code: 400, Description: "message size exceeds maximum allowed"} + resp.Error = ApiErrors[JSStreamMessageExceedsMaximumErr] b, _ := json.Marshal(resp) mset.outq.send(&jsPubMsg{reply, _EMPTY_, _EMPTY_, nil, b, nil, 0, nil}) } @@ -2628,7 +2634,7 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte, mset.mu.Unlock() if canRespond { resp.PubAck = &PubAck{Stream: name} - resp.Error = jsInsufficientErr + resp.Error = ApiErrors[JSInsufficientResourcesErr] b, _ := json.Marshal(resp) mset.outq.send(&jsPubMsg{reply, _EMPTY_, _EMPTY_, nil, b, nil, 0, nil}) } @@ -2636,7 +2642,7 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte, if node := mset.raftNode(); node != nil { node.StepDown() } - return ErrJetStreamResourcesExceeded + return ApiErrors[JSInsufficientResourcesErr] } var noInterest bool @@ -2722,7 +2728,7 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte, s.Warnf("JetStream resource limits exceeded for account: %q", accName) if canRespond { resp.PubAck = &PubAck{Stream: name} - resp.Error = &ApiError{Code: 400, Description: "resource limits exceeded for account"} + resp.Error = ApiErrors[JSAccountResourcesExceededErr] response, _ = json.Marshal(resp) } // If we did not succeed put those values back. @@ -2943,7 +2949,7 @@ func (mset *stream) stop(deleteFlag, advisory bool) error { mset.mu.RUnlock() if jsa == nil { - return ErrJetStreamNotEnabledForAccount + return ApiErrors[JSNotEnabledErr] } // Remove from our account map. @@ -3235,7 +3241,7 @@ func (a *Account) RestoreStream(ncfg *StreamConfig, r io.Reader) (*stream, error cfg, err := checkStreamCfg(ncfg) if err != nil { - return nil, err + return nil, ApiErrors[JSStreamNotFoundErr].ErrOrNewT(err, "{err}", err) } _, jsa, err := a.checkForJetStream() @@ -3297,7 +3303,7 @@ func (a *Account) RestoreStream(ncfg *StreamConfig, r io.Reader) (*stream, error // See if this stream already exists. if _, err := a.lookupStream(cfg.Name); err == nil { - return nil, ErrJetStreamStreamAlreadyUsed + return nil, ApiErrors[JSStreamNameExistErr] } // Move into the correct place here. ndir := path.Join(jsa.storeDir, streamsDir, cfg.Name) diff --git a/server/test_test.go b/server/test_test.go index c7cc3004..6bdaf905 100644 --- a/server/test_test.go +++ b/server/test_test.go @@ -58,6 +58,18 @@ type cluster struct { t *testing.T } +func checkNatsError(t *testing.T, e *ApiError, id ErrorIdentifier) { + t.Helper() + ae, ok := ApiErrors[id] + if !ok { + t.Fatalf("Unknown error ID identifier: %d", id) + } + + if e.ErrCode != ae.ErrCode { + t.Fatalf("Did not get NATS Error %d: %+v", e.ErrCode, e) + } +} + // Creates a full cluster with numServers and given name and makes sure its well formed. // Will have Gateways and Leaf Node connections active. func createClusterWithName(t *testing.T, clusterName string, numServers int, connectTo ...*cluster) *cluster {