Skip to content

Commit

Permalink
wip - removing NatsError in favor of easier more generic APIs and tes…
Browse files Browse the repository at this point in the history
…ting with instanceof.

Signed-off-by: Alberto Ricart <[email protected]>
  • Loading branch information
aricart committed Oct 25, 2024
1 parent 54c5dc8 commit 2ac7ea9
Show file tree
Hide file tree
Showing 52 changed files with 1,254 additions and 1,043 deletions.
2 changes: 2 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@
- headers_only is needed in Consumer

- add a test for next/fetch/consume where message size smaller than availablle

- doc
2 changes: 1 addition & 1 deletion core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -701,7 +701,7 @@ const sub = nc.subscribe("hello", { timeout: 1000 });
// handle the messages
}
})().catch((err) => {
if (err.code === ErrorCode.Timeout) {
if (err instanceof TimeoutError) {
console.log(`sub timed out!`);
} else {
console.log(`sub iterator got an error!`);
Expand Down
8 changes: 2 additions & 6 deletions core/src/authenticator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import type {
TokenAuth,
UserPass,
} from "./core.ts";
import { ErrorCode, NatsError } from "./core.ts";

export function multiAuthenticator(authenticators: Authenticator[]) {
return (nonce?: string): Auth => {
Expand Down Expand Up @@ -134,16 +133,13 @@ export function credsAuthenticator(
// get the JWT
let m = CREDS.exec(s);
if (!m) {
throw NatsError.errorForCode(ErrorCode.BadCreds);
throw new Error("unable to parse credentials");
}
const jwt = m[1].trim();
// get the nkey
m = CREDS.exec(s);
if (!m) {
throw NatsError.errorForCode(ErrorCode.BadCreds);
}
if (!m) {
throw NatsError.errorForCode(ErrorCode.BadCreds);
throw new Error("unable to parse credentials");
}
const seed = TE.encode(m[1].trim());

Expand Down
8 changes: 3 additions & 5 deletions core/src/bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import { Empty } from "./types.ts";
import { nuid } from "./nuid.ts";
import { deferred, Perf } from "./util.ts";
import type { NatsConnectionImpl } from "./nats.ts";
import { ErrorCode, NatsError } from "./core.ts";
import type { NatsConnection } from "./core.ts";

export class Metric {
Expand Down Expand Up @@ -132,10 +131,9 @@ export class Bench {
this.nc.closed()
.then((err) => {
if (err) {
throw new NatsError(
`bench closed with an error: ${err.message}`,
ErrorCode.Unknown,
err,
throw new Error(
`bench closed with an error`,
{ cause: err },
);
}
});
Expand Down
43 changes: 24 additions & 19 deletions core/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
*/

import { nuid } from "./nuid.ts";
import { InvalidOptionError } from "./errors.ts";

/**
* Events reported by the {@link NatsConnection#status} iterator.
Expand Down Expand Up @@ -45,7 +46,7 @@ export enum DebugEvents {

export enum ErrorCode {
// emitted by the client
ApiError = "BAD API",
// ApiError = "BAD API",
BadAuthentication = "BAD_AUTHENTICATION",
BadCreds = "BAD_CREDS",
BadHeader = "BAD_HEADER",
Expand Down Expand Up @@ -95,21 +96,6 @@ export function isNatsError(err: NatsError | Error): err is NatsError {
return typeof (err as NatsError).code === "string";
}

export interface ApiError {
/**
* HTTP like error code in the 300 to 500 range
*/
code: number;
/**
* A human friendly description of the error
*/
description: string;
/**
* The NATS error code unique to each kind of error
*/
err_code?: number;
}

export class Messages {
messages: Map<string, string>;

Expand Down Expand Up @@ -138,6 +124,21 @@ export class Messages {
// safari doesn't support static class members
const messages: Messages = new Messages();

export type ApiError = {
/**
* Status code
*/
code: number;
/**
* JetStream Error Code
*/
err_code: number;
/**
* Error description
*/
description: string;
};

export class NatsError extends Error {
// TODO: on major version this should change to a number/enum
code: string;
Expand Down Expand Up @@ -193,7 +194,7 @@ export class NatsError extends Error {
}
}

export type MsgCallback<T> = (err: NatsError | null, msg: T) => void;
export type MsgCallback<T> = (err: Error | null, msg: T) => void;

/**
* Subscription Options
Expand Down Expand Up @@ -232,6 +233,7 @@ export interface DnsResolveFn {
export interface Status {
type: Events | DebugEvents;
data: string | ServersChanged | number;
error?: Error;
permissionContext?: { operation: string; subject: string };
}

Expand Down Expand Up @@ -811,7 +813,10 @@ export function createInbox(prefix = ""): string {
prefix.split(".")
.forEach((v) => {
if (v === "*" || v === ">") {
throw new Error(`inbox prefixes cannot have wildcards '${prefix}'`);
throw InvalidOptionError.illegalArgument(
"prefix",
`cannot have wildcards ('${prefix}')`,
);
}
});
return `${prefix}.${nuid.next()}`;
Expand All @@ -824,7 +829,7 @@ export interface Request {

resolver(err: Error | null, msg: Msg): void;

cancel(err?: NatsError): void;
cancel(err?: Error): void;
}

/**
Expand Down
195 changes: 195 additions & 0 deletions core/src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/*
* Copyright 2024 Synadia Communications, Inc
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

export class InvalidSubjectError extends Error {
constructor(subject: string, options?: ErrorOptions) {
super(`illegal subject: '${subject}'`, options);
this.name = "InvalidSubjectError";
}
}

export class InvalidHeaderError extends Error {
constructor(message: string, options?: ErrorOptions) {
super(`invalid header: ${message}`, options);
this.name = "InvalidHeaderError";
}
}

export class InvalidOptionError extends Error {
constructor(message: string, options?: ErrorOptions) {
super(message, options);
this.name = "InvalidOptionError";
}

static exclusiveOptions(opts: string[]): InvalidOptionError {
const names = opts.map((o) => `'${o}'`).join(",");
return new InvalidOptionError(`options ${names} are mutually exclusive.`);
}

static illegalArgument(name: string, message: string): InvalidOptionError {
return new InvalidOptionError(`argument '${name}' ${message}`);
}

static illegalOption(prop: string, message: string): InvalidOptionError {
return new InvalidOptionError(`option '${prop}' ${message}`);
}
}

export class InvalidOperationError extends Error {
constructor(message: string, options?: ErrorOptions) {
super(message, options);
this.name = "InvalidOperationError";
}
}

export class UserAuthenticationExpiredError extends Error {
constructor(message: string, options?: ErrorOptions) {
super(message, options);
this.name = "UserAuthenticationExpiredError";
}

static parse(s: string): UserAuthenticationExpiredError | null {
const ss = s.toLowerCase();
if (ss.indexOf("user authentication expired") !== -1) {
return new UserAuthenticationExpiredError(s);
}
return null;
}
}

export class AuthorizationError extends Error {
constructor(message: string, options?: ErrorOptions) {
super(message, options);
this.name = "AuthorizationError";
}

static parse(s: string): AuthorizationError | null {
const messages = [
"authorization violation",
"account authentication expired",
"authentication timeout",
];

const ss = s.toLowerCase();

for (let i = 0; i < messages.length; i++) {
if (ss.indexOf(messages[i]) !== -1) {
return new AuthorizationError(s);
}
}

return null;
}
}

export class ClosedConnectionError extends Error {
constructor() {
super("closed connection");
this.name = "ClosedConnectionError";
}
}

export class ConnectionDrainingError extends Error {
constructor() {
super("connection draining");
this.name = "DrainingConnectionError";
}
}

export class ConnectionError extends Error {
constructor(message: string, options?: ErrorOptions) {
super(message, options);
this.name = "ConnectionError";
}
}

export class ProtocolError extends Error {
constructor(message: string, options?: ErrorOptions) {
super(message, options);
this.name = "ProtocolError";
}
}

export class RequestError extends Error {
constructor(message = "", options?: ErrorOptions) {
super(message, options);
this.name = "RequestError";
}
}

export class TimeoutError extends Error {
constructor(options?: ErrorOptions) {
super("timeout", options);
this.name = "TimeoutError";
}
}

export class NoRespondersError extends Error {
subject: string;
constructor(subject: string, options?: ErrorOptions) {
super(`no responders: '${subject}'`, options);
this.subject = subject;
this.name = "NoResponders";
}
}

export class ServerError extends Error {
constructor(message: string, options?: ErrorOptions) {
super(message, options);
this.name = "ServerError";
}
}

export class PermissionViolationError extends Error {
operation: "publish" | "subscription";
subject: string;
queue?: string;

constructor(
message: string,
operation: "publish" | "subscription",
subject: string,
queue?: string,
options?: ErrorOptions,
) {
super(message, options);
this.name = "PermissionViolationError";
this.operation = operation;
this.subject = subject;
this.queue = queue;
}

static parse(s: string): PermissionViolationError | null {
const t = s ? s.toLowerCase() : "";
if (t.indexOf("permissions violation") === -1) {
return null;
}
let operation: "publish" | "subscription" = "publish";
let subject = "";
let queue: string | undefined = undefined;
const m = s.match(/(Publish|Subscription) to "(\S+)"/);
if (m) {
operation = m[1].toLowerCase() as "publish" | "subscription";
subject = m[2];
if (operation === "subscription") {
const qm = s.match(/using queue "(\S+)"/);
if (qm) {
queue = qm[1];
}
}
}
return new PermissionViolationError(s, operation, subject, queue);
}
}
13 changes: 6 additions & 7 deletions core/src/headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@

import { TD, TE } from "./encoders.ts";
import type { MsgHdrs } from "./core.ts";
import { ErrorCode, Match, NatsError } from "./core.ts";
import { Match } from "./core.ts";
import { InvalidHeaderError } from "./errors.ts";

// https://www.ietf.org/rfc/rfc822.txt
// 3.1.2. STRUCTURE OF HEADER FIELDS
Expand Down Expand Up @@ -47,9 +48,8 @@ export function canonicalMIMEHeaderKey(k: string): string {
for (let i = 0; i < k.length; i++) {
let c = k.charCodeAt(i);
if (c === colon || c < start || c > end) {
throw new NatsError(
`'${k[i]}' is not a valid character for a header key`,
ErrorCode.BadHeader,
throw new InvalidHeaderError(
`'${k[i]}' is not a valid character in a header name`,
);
}
if (upper && a <= c && c <= z) {
Expand Down Expand Up @@ -170,9 +170,8 @@ export class MsgHdrsImpl implements MsgHdrs {
static validHeaderValue(k: string): string {
const inv = /[\r\n]/;
if (inv.test(k)) {
throw new NatsError(
"invalid header value - \\r and \\n are not allowed.",
ErrorCode.BadHeader,
throw new InvalidHeaderError(
"values cannot contain \\r or \\n",
);
}
return k.trim();
Expand Down
Loading

0 comments on commit 2ac7ea9

Please sign in to comment.