From 862cf395817186c63c16a330b132cfcf8a37e7d4 Mon Sep 17 00:00:00 2001 From: Lewis Marshall Date: Thu, 30 Jul 2026 15:47:14 +0100 Subject: [PATCH] Check error codes against the ably-common registry at compile time `new ErrorInfo(message, code, statusCode)` takes two adjacent numbers the compiler accepted in either order, and codes were written as bare literals with nothing tying them to the registry in ably-common. Generating the set of registered codes from that registry and narrowing `code` to it closes both gaps: an unregistered code and a transposed `code`/`statusCode` pair now fail to compile, because registry codes are 5-6 digits and an HTTP status is 3, so a status can never be a member of the union. src/common/lib/types/errorcodes.ts is generated by the ably-common script `errors/scripts/generate-ts.js` via `npm run generate:errorcodes-ts`, and committed. The type erases at compile time, so this costs nothing in the bundle: `errorcodes` appears zero times in build/ably-node.js and build/ably.min.js. Errors this SDK raises are checked; errors decoded from the server are not, because the server chooses those codes and may use ones a given client version does not know. Rather than leave one signature to serve both, the two are separated by name: fromValues - checked, for errors this repository raises fromWireValues - unchecked, for a response body, a response header, or the `error` field of a ProtocolMessage `fromValues` was previously reached by both kinds of caller, so it could not be both checked and honest. Splitting it makes the safe path the default and leaves every unchecked site greppable; there are ten, all genuine server decodes apart from the application's authCallback in auth.ts, whose code originates in user code and so cannot be validated here. `ErrorInfoValues` and `PartialErrorInfoValues` are the checked shapes for the options-object constructor. `IConvertibleToErrorInfo` keeps `code: number` and is now reached only by the wire path. This exposed three defects: - Five sites had `code` and `statusCode` the wrong way round: src/common/types/http.ts (x2), nodejs and web crypto.ts, and web http.ts. Their `err.code` and `err.statusCode` were transposed and are now correct. The web crypto site had no valid pairing in either order (400/50000); it now matches its corrected Node sibling. - connectionerrors.ts `unknownChannelErr` used UNKNOWN_CONNECTION_ERR (50002), which is why UNKNOWN_CHANNEL_ERR (50001) was unused. The registry gives 50001 as `Internal channel error` and protocol.ts already uses it that way. Channel errors now report 50001. - normaliseAuthcallbackError in auth.ts took its code off an `any`, so neither the callback's value nor the SDK's own fallbacks were checked. The fallbacks are now annotated constants; the callback's value is an explicit cast. Note the first two change observable error codes. ConnectionErrorCodes uses `as const satisfies Record` so that keys stay literal and values are checked. A plain `Record` annotation would have kept accepting a misspelled key, which is the shape of the bug fixed above. Two CI steps in check.yml. The first typechecks src/, which nothing did before: the build is esbuild and the UTS suite runs under tsx, both of which strip types without checking them, so `tsc --noEmit ably.d.ts modular.d.ts` was only covering the two declaration files. Without it none of this is enforced anywhere but an editor. It passes with no other change. The second regenerates at the pinned submodule commit and fails on a diff, so a reference to a code that is not registered in ably-common cannot merge here first. The submodule pin is provisional: it points at the unmerged ably-common branch `error-code-typescript-gen`, needed for the generate and drift-check steps to run. Repoint it to main once that is merged. Only protocol/ differs between the old pin and this one; test-resources/, which is all ably-js reads from the submodule, is unchanged. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/check.yml | 7 + package.json | 3 +- src/common/lib/client/auth.ts | 17 +- src/common/lib/client/realtimechannel.ts | 4 +- src/common/lib/transport/connectionerrors.ts | 11 +- src/common/lib/types/devicedetails.ts | 2 +- src/common/lib/types/errorcodes.ts | 266 ++++++++++++++++++ src/common/lib/types/errorinfo.ts | 105 +++++-- src/common/lib/types/protocolmessage.ts | 6 +- src/common/types/http.ts | 4 +- .../lib/transport/nodecomettransport.js | 2 +- src/platform/nodejs/lib/util/crypto.ts | 2 +- src/platform/nodejs/lib/util/http.ts | 7 +- src/platform/web/lib/http/http.ts | 2 +- .../web/lib/http/request/fetchrequest.ts | 2 +- .../web/lib/http/request/xhrrequest.ts | 5 +- src/platform/web/lib/util/crypto.ts | 2 +- test/common/ably-common | 2 +- test/uts/rest/unit/types/error_types.test.ts | 2 +- 19 files changed, 394 insertions(+), 57 deletions(-) create mode 100644 src/common/lib/types/errorcodes.ts diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 7f5e3363bb..29abdc9113 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -28,6 +28,13 @@ jobs: - run: npm run lint - run: npm run format:check - run: npx tsc --noEmit ably.d.ts modular.d.ts + # src/ is compiled by esbuild, which strips types without checking them, so this is + # the only step that typechecks the library itself. + - run: npx tsc --noEmit -p tsconfig.json + # The error-code union is generated from the ably-common submodule at its pinned + # commit; fail if the committed copy has drifted from the registry. + - run: npm run generate:errorcodes-ts + - run: git diff --exit-code -- src/common/lib/types/errorcodes.ts # for some reason, this doesn't work in CI using `npx attw --pack .` - run: npm pack - run: npx attw ably-$(node -e "console.log(require('./package.json').version)").tgz --summary --exclude-entrypoints 'ably/modular' diff --git a/package.json b/package.json index 68a4bc2ba9..f5916a6f58 100644 --- a/package.json +++ b/package.json @@ -206,6 +206,7 @@ "modulereport": "tsc --noEmit --esModuleInterop scripts/moduleReport.ts && esr scripts/moduleReport.ts", "speccoveragereport": "tsc --noEmit --esModuleInterop --target ES2017 --moduleResolution node scripts/specCoverageReport.ts && esr scripts/specCoverageReport.ts", "process-private-api-data": "tsc --noEmit --esModuleInterop --strictNullChecks scripts/processPrivateApiData/run.ts && esr scripts/processPrivateApiData/run.ts", - "docs": "typedoc" + "docs": "typedoc", + "generate:errorcodes-ts": "node test/common/ably-common/errors/scripts/generate-ts.js --format=type --out src/common/lib/types/errorcodes.ts" } } diff --git a/src/common/lib/client/auth.ts b/src/common/lib/client/auth.ts index f8f65c938e..dd668c33f0 100644 --- a/src/common/lib/client/auth.ts +++ b/src/common/lib/client/auth.ts @@ -2,6 +2,7 @@ import Logger from '../util/logger'; import * as Utils from '../util/utils'; import Multicaster, { MulticasterInstance } from '../util/multicaster'; import ErrorInfo, { IPartialErrorInfo } from '../types/errorinfo'; +import type { ErrorCode } from '../types/errorcodes'; import { RequestResultError, RequestParams, RequestResult } from '../../types/http'; import * as API from '../../../../ably'; import BaseClient from './baseclient'; @@ -28,17 +29,27 @@ function isRealtime(client: BaseClient): client is BaseRealtime { return !!(client as BaseRealtime).connection; } +/* Fallbacks this SDK supplies when the callback's error carries no code of its own. Annotated + * so they are checked against the registry; the `err.code` below cannot be, because it comes + * from the application's authCallback rather than from this repository. */ +const AUTH_CALLBACK_ERROR_CODE: ErrorCode = 40170; +const AUTH_CALLBACK_FORBIDDEN_CODE: ErrorCode = 40300; + /* A client auth callback may give errors in any number of formats; normalise to an ErrorInfo or PartialErrorInfo */ function normaliseAuthcallbackError(err: any) { if (!Utils.isErrorInfoOrPartialErrorInfo(err)) { - return new ErrorInfo(Utils.inspectError(err), err.code || 40170, err.statusCode || 401); + return new ErrorInfo( + Utils.inspectError(err), + (err.code as ErrorCode) || AUTH_CALLBACK_ERROR_CODE, + err.statusCode || 401, + ); } /* network errors will not have an inherent error code */ if (!err.code) { if (err.statusCode === 403) { - err.code = 40300; + err.code = AUTH_CALLBACK_FORBIDDEN_CODE; } else { - err.code = 40170; + err.code = AUTH_CALLBACK_ERROR_CODE; /* normalise statusCode to 401 per RSA4e */ err.statusCode = 401; } diff --git a/src/common/lib/client/realtimechannel.ts b/src/common/lib/client/realtimechannel.ts index b4c42c8e6c..dcb389c02b 100644 --- a/src/common/lib/client/realtimechannel.ts +++ b/src/common/lib/client/realtimechannel.ts @@ -697,7 +697,7 @@ class RealtimeChannel extends EventEmitter { case actions.DETACHED: { const detachErr = message.error - ? ErrorInfo.fromValues(message.error) + ? ErrorInfo.fromWireValues(message.error) : new ErrorInfo('Channel detached', 90001, 404); if (this.state === 'detaching') { this.notifyState('detached', detachErr); @@ -858,7 +858,7 @@ class RealtimeChannel extends EventEmitter { /* attach/detach operation attempted on superseded transport handle */ this.checkPendingState(); } else { - this.notifyState('failed', ErrorInfo.fromValues(err)); + this.notifyState('failed', ErrorInfo.fromWireValues(err)); } break; } diff --git a/src/common/lib/transport/connectionerrors.ts b/src/common/lib/transport/connectionerrors.ts index fcd7b3c8af..175a81145c 100644 --- a/src/common/lib/transport/connectionerrors.ts +++ b/src/common/lib/transport/connectionerrors.ts @@ -1,5 +1,8 @@ import ErrorInfo from '../types/errorinfo'; +import type { ErrorCode } from '../types/errorcodes'; +// `satisfies` checks every code against the registry while keeping the literal types, so +// that the members stay usable as ErrorCode at the call sites below. const ConnectionErrorCodes = { DISCONNECTED: 80003, SUSPENDED: 80002, @@ -8,7 +11,7 @@ const ConnectionErrorCodes = { CLOSED: 80017, UNKNOWN_CONNECTION_ERR: 50002, UNKNOWN_CHANNEL_ERR: 50001, -}; +} as const satisfies Record; const ConnectionErrors = { disconnected: () => @@ -50,7 +53,7 @@ const ConnectionErrors = { unknownChannelErr: () => ErrorInfo.fromValues({ statusCode: 500, - code: ConnectionErrorCodes.UNKNOWN_CONNECTION_ERR, + code: ConnectionErrorCodes.UNKNOWN_CHANNEL_ERR, message: 'Internal channel error', }), }; @@ -59,7 +62,9 @@ export function isRetriable(err: ErrorInfo) { if (!err.statusCode || !err.code || err.statusCode >= 500) { return true; } - return Object.values(ConnectionErrorCodes).includes(err.code); + // Widened because `as const` gives the values their own narrow literal union, which + // `includes` will not accept an arbitrary ErrorCode against. + return (Object.values(ConnectionErrorCodes) as ErrorCode[]).includes(err.code); } export default ConnectionErrors; diff --git a/src/common/lib/types/devicedetails.ts b/src/common/lib/types/devicedetails.ts index 25b099d3d9..5119a263bc 100644 --- a/src/common/lib/types/devicedetails.ts +++ b/src/common/lib/types/devicedetails.ts @@ -124,7 +124,7 @@ class DeviceDetails { } static fromValues(values: Record): DeviceDetails { - values.error = values.error && ErrorInfo.fromValues(values.error as IConvertibleToErrorInfo); + values.error = values.error && ErrorInfo.fromWireValues(values.error as IConvertibleToErrorInfo); return Object.assign(new DeviceDetails(), values); } diff --git a/src/common/lib/types/errorcodes.ts b/src/common/lib/types/errorcodes.ts new file mode 100644 index 0000000000..25fca02f0b --- /dev/null +++ b/src/common/lib/types/errorcodes.ts @@ -0,0 +1,266 @@ +// GENERATED FROM ably-common/errors/codes — DO NOT EDIT. +// Regenerate with: npm run generate:errorcodes-ts + +/** A registered Ably error code. */ +export type ErrorCode = + | 10000 + | 20000 + | 40000 + | 40001 + | 40002 + | 40003 + | 40004 + | 40005 + | 40006 + | 40007 + | 40008 + | 40009 + | 40010 + | 40011 + | 40012 + | 40013 + | 40014 + | 40015 + | 40016 + | 40017 + | 40018 + | 40019 + | 40020 + | 40021 + | 40022 + | 40023 + | 40024 + | 40025 + | 40030 + | 40031 + | 40032 + | 40033 + | 40034 + | 40035 + | 40099 + | 40100 + | 40101 + | 40102 + | 40103 + | 40104 + | 40105 + | 40106 + | 40110 + | 40111 + | 40112 + | 40113 + | 40114 + | 40115 + | 40120 + | 40121 + | 40125 + | 40126 + | 40127 + | 40128 + | 40130 + | 40131 + | 40132 + | 40133 + | 40140 + | 40141 + | 40142 + | 40143 + | 40144 + | 40145 + | 40150 + | 40151 + | 40160 + | 40161 + | 40162 + | 40163 + | 40164 + | 40165 + | 40166 + | 40167 + | 40170 + | 40171 + | 40172 + | 40180 + | 40181 + | 40182 + | 40300 + | 40310 + | 40311 + | 40320 + | 40330 + | 40331 + | 40332 + | 40400 + | 40500 + | 40900 + | 41001 + | 42200 + | 42210 + | 42211 + | 42212 + | 42213 + | 42910 + | 42911 + | 42912 + | 42913 + | 42914 + | 42915 + | 42916 + | 42917 + | 42918 + | 42920 + | 42921 + | 42922 + | 42923 + | 42924 + | 42925 + | 42926 + | 50000 + | 50001 + | 50002 + | 50003 + | 50004 + | 50005 + | 50006 + | 50010 + | 50205 + | 50210 + | 50305 + | 50306 + | 50310 + | 50320 + | 50330 + | 50405 + | 50410 + | 61002 + | 70000 + | 70001 + | 70002 + | 70003 + | 70004 + | 70005 + | 70006 + | 71000 + | 71001 + | 71100 + | 71101 + | 71102 + | 71200 + | 71201 + | 71202 + | 71203 + | 71204 + | 71300 + | 71301 + | 71302 + | 71303 + | 72000 + | 72001 + | 72002 + | 72003 + | 72004 + | 72005 + | 72006 + | 72007 + | 72008 + | 80000 + | 80001 + | 80002 + | 80003 + | 80004 + | 80005 + | 80006 + | 80007 + | 80008 + | 80009 + | 80010 + | 80011 + | 80012 + | 80013 + | 80014 + | 80015 + | 80016 + | 80017 + | 80018 + | 80019 + | 80020 + | 80021 + | 80022 + | 80023 + | 80024 + | 80030 + | 90000 + | 90001 + | 90002 + | 90003 + | 90004 + | 90005 + | 90006 + | 90007 + | 90008 + | 90009 + | 90010 + | 90021 + | 91000 + | 91001 + | 91002 + | 91003 + | 91004 + | 91005 + | 91006 + | 91007 + | 91008 + | 91100 + | 92000 + | 92001 + | 92002 + | 92003 + | 92004 + | 92005 + | 92006 + | 92007 + | 92008 + | 93001 + | 93002 + | 101000 + | 101001 + | 101002 + | 101003 + | 101004 + | 102000 + | 102001 + | 102002 + | 102003 + | 102004 + | 102005 + | 102050 + | 102051 + | 102052 + | 102053 + | 102054 + | 102100 + | 102101 + | 102102 + | 102103 + | 102104 + | 102105 + | 102106 + | 102107 + | 102108 + | 102109 + | 102110 + | 102111 + | 102112 + | 102113 + | 102200 + | 102201 + | 102202 + | 103000 + | 103001 + | 103002 + | 103003 + | 103004 + | 103005 + | 103006 + | 103007 + | 103008; diff --git a/src/common/lib/types/errorinfo.ts b/src/common/lib/types/errorinfo.ts index 64523921dd..3080f7d2ee 100644 --- a/src/common/lib/types/errorinfo.ts +++ b/src/common/lib/types/errorinfo.ts @@ -1,9 +1,10 @@ import Platform from 'common/platform'; import * as Utils from '../util/utils'; import * as API from '../../../../ably'; +import type { ErrorCode } from './errorcodes'; export interface IPartialErrorInfo extends Error { - code: number | null; + code: ErrorCode | null; statusCode?: number; cause?: ErrorInfo | PartialErrorInfo; href?: string; @@ -24,6 +25,13 @@ function toString(err: ErrorInfo | PartialErrorInfo) { return result; } +/** + * The values of an error decoded from a server response body or a ProtocolMessage. `code` is + * a plain `number` rather than an {@link ErrorCode} because the server chose it, and may use + * codes this version of the client does not know about, so it cannot be checked against the + * registry. Reached only via {@link ErrorInfo.fromWireValues}; errors this SDK raises itself + * use {@link ErrorInfoValues}. + */ export interface IConvertibleToErrorInfo { message: string; code: number; @@ -34,6 +42,7 @@ export interface IConvertibleToErrorInfo { href?: string; } +/** As {@link IConvertibleToErrorInfo}, for the partial case. */ export interface IConvertibleToPartialErrorInfo { message: string; code: number | null; @@ -44,19 +53,45 @@ export interface IConvertibleToPartialErrorInfo { href?: string; } +/** + * The values an error raised by this SDK is constructed from. Identical to + * {@link IConvertibleToErrorInfo} except that `code` must be a registered Ably error code, + * so that an unregistered code, or a `code`/`statusCode` transposition, fails to compile. + * This is the shape to use for any error written in this repository. + */ +export interface ErrorInfoValues extends Omit { + code: ErrorCode; +} + +/** As {@link ErrorInfoValues}, for the partial case. */ +export interface PartialErrorInfoValues extends Omit { + code: ErrorCode | null; +} + +/** + * Apply the help.ably.io href default shared by the `fromValues` factories. Errors built + * with `new ErrorInfo(...)` deliberately do not get it. + */ +function withHelpHref(err: T): T { + if (err.code && !err.href) { + err.href = 'https://help.ably.io/error/' + err.code; + } + return err; +} + export default class ErrorInfo extends Error implements IPartialErrorInfo, API.ErrorInfo { - code: number; + code: ErrorCode; statusCode: number; cause?: ErrorInfo; href?: string; detail?: Record; remediation?: string; - constructor(message: string, code: number, statusCode: number, cause?: ErrorInfo, detail?: Record); - constructor(values: IConvertibleToErrorInfo); + constructor(message: string, code: ErrorCode, statusCode: number, cause?: ErrorInfo, detail?: Record); + constructor(values: ErrorInfoValues); constructor( - messageOrValues: string | IConvertibleToErrorInfo, - code?: number, + messageOrValues: string | ErrorInfoValues, + code?: ErrorCode, statusCode?: number, cause?: ErrorInfo, detail?: Record, @@ -84,7 +119,7 @@ export default class ErrorInfo extends Error implements IPartialErrorInfo, API.E if (typeof Object.setPrototypeOf !== 'undefined') { Object.setPrototypeOf(this, ErrorInfo.prototype); } - this.code = code as number; + this.code = code as ErrorCode; this.statusCode = statusCode as number; this.cause = cause; this.detail = detail; @@ -95,21 +130,29 @@ export default class ErrorInfo extends Error implements IPartialErrorInfo, API.E return toString(this); } - static fromValues(values: IConvertibleToErrorInfo): ErrorInfo { - // Delegate shape validation and field assignment to the options-object constructor; - // fromValues only adds the help.ably.io href default for server-decoded errors that - // arrive without one. SDK-thrown errors that use `new ErrorInfo({...})` directly do - // not get this default, by design. - const result = new ErrorInfo(values); - if (result.code && !result.href) { - result.href = 'https://help.ably.io/error/' + result.code; - } - return result; + /** + * Build an error this SDK is raising itself, adding the help.ably.io href default. `code` + * is checked against the registry. To build an error out of a server response body or a + * ProtocolMessage, use {@link ErrorInfo.fromWireValues} instead. + */ + static fromValues(values: ErrorInfoValues): ErrorInfo { + // Shape validation and field assignment are delegated to the options-object constructor. + return withHelpHref(new ErrorInfo(values)); + } + + /** + * Build an error out of data received from the server — a response body, or the `error` + * field of a ProtocolMessage. The cast is deliberate: the server chose this `code`, so + * unlike {@link ErrorInfo.fromValues} it is not checked against the registry. Prefer + * `fromValues` unless the code really did come from the server. + */ + static fromWireValues(values: IConvertibleToErrorInfo): ErrorInfo { + return withHelpHref(new ErrorInfo(values as ErrorInfoValues)); } } export class PartialErrorInfo extends Error implements IPartialErrorInfo { - code: number | null; + code: ErrorCode | null; statusCode?: number; cause?: ErrorInfo | PartialErrorInfo; href?: string; @@ -118,15 +161,15 @@ export class PartialErrorInfo extends Error implements IPartialErrorInfo { constructor( message: string, - code: number | null, + code: ErrorCode | null, statusCode?: number, cause?: ErrorInfo | PartialErrorInfo, detail?: Record, ); - constructor(values: IConvertibleToPartialErrorInfo); + constructor(values: PartialErrorInfoValues); constructor( - messageOrValues: string | IConvertibleToPartialErrorInfo, - code?: number | null, + messageOrValues: string | PartialErrorInfoValues, + code?: ErrorCode | null, statusCode?: number, cause?: ErrorInfo | PartialErrorInfo, detail?: Record, @@ -154,7 +197,7 @@ export class PartialErrorInfo extends Error implements IPartialErrorInfo { if (typeof Object.setPrototypeOf !== 'undefined') { Object.setPrototypeOf(this, PartialErrorInfo.prototype); } - this.code = code as number | null; + this.code = code as ErrorCode | null; this.statusCode = statusCode; this.cause = cause; this.detail = detail; @@ -165,13 +208,13 @@ export class PartialErrorInfo extends Error implements IPartialErrorInfo { return toString(this); } - static fromValues(values: IConvertibleToPartialErrorInfo): PartialErrorInfo { - // Same shape as ErrorInfo.fromValues - delegate validation/assignment to the - // options-object constructor; href default applies only to the server-decoded path. - const result = new PartialErrorInfo(values); - if (result.code && !result.href) { - result.href = 'https://help.ably.io/error/' + result.code; - } - return result; + /** As {@link ErrorInfo.fromValues}, for the partial case. */ + static fromValues(values: PartialErrorInfoValues): PartialErrorInfo { + return withHelpHref(new PartialErrorInfo(values)); + } + + /** As {@link ErrorInfo.fromWireValues}, for the partial case. */ + static fromWireValues(values: IConvertibleToPartialErrorInfo): PartialErrorInfo { + return withHelpHref(new PartialErrorInfo(values as PartialErrorInfoValues)); } } diff --git a/src/common/lib/types/protocolmessage.ts b/src/common/lib/types/protocolmessage.ts index 14ab7c9288..ba1b9ba4cd 100644 --- a/src/common/lib/types/protocolmessage.ts +++ b/src/common/lib/types/protocolmessage.ts @@ -3,7 +3,7 @@ import * as API from '../../../../ably'; import { PresenceMessagePlugin } from '../client/modularplugins'; import { AnnotationsPlugin } from '../client/modularplugins'; import * as Utils from '../util/utils'; -import ErrorInfo from './errorinfo'; +import ErrorInfo, { type IConvertibleToErrorInfo } from './errorinfo'; import { WireMessage } from './message'; import PresenceMessage, { WirePresenceMessage } from './presencemessage'; import Annotation, { WireAnnotation } from './annotation'; @@ -46,7 +46,7 @@ export function fromDeserialized( ): ProtocolMessage { let error: ErrorInfo | undefined; if (deserialized.error) { - error = ErrorInfo.fromValues(deserialized.error as ErrorInfo); + error = ErrorInfo.fromWireValues(deserialized.error as IConvertibleToErrorInfo); } let messages: WireMessage[] | undefined; @@ -132,7 +132,7 @@ export function stringify( result += '; state=' + toStringArray(objectsPlugin.WireObjectMessage.fromValuesArray(msg.state, Utils, MessageEncoding)); } - if (msg.error) result += '; error=' + ErrorInfo.fromValues(msg.error).toString(); + if (msg.error) result += '; error=' + ErrorInfo.fromWireValues(msg.error).toString(); if (msg.auth && msg.auth.accessToken) result += '; token=' + msg.auth.accessToken; if (msg.flags) result += '; flags=' + flagNames.filter(msg.hasFlag).join(','); if (msg.params) { diff --git a/src/common/types/http.ts b/src/common/types/http.ts index b135404adf..3a03df145d 100644 --- a/src/common/types/http.ts +++ b/src/common/types/http.ts @@ -237,7 +237,7 @@ export class Http { return tryAHost(hosts); } catch (err) { // Handle any unexpected error, to ensure we always meet our contract of not throwing any errors - return { error: new ErrorInfo(`Unexpected error in Http.do: ${Utils.inspectError(err)}`, 500, 50000) }; + return { error: new ErrorInfo(`Unexpected error in Http.do: ${Utils.inspectError(err)}`, 50000, 500) }; } } @@ -263,7 +263,7 @@ export class Http { return result; } catch (err) { // Handle any unexpected error, to ensure we always meet our contract of not throwing any errors - return { error: new ErrorInfo(`Unexpected error in Http.doUri: ${Utils.inspectError(err)}`, 500, 50000) }; + return { error: new ErrorInfo(`Unexpected error in Http.doUri: ${Utils.inspectError(err)}`, 50000, 500) }; } } } diff --git a/src/platform/nodejs/lib/transport/nodecomettransport.js b/src/platform/nodejs/lib/transport/nodecomettransport.js index 045a2d9f39..cd5ba2e8a6 100644 --- a/src/platform/nodejs/lib/transport/nodecomettransport.js +++ b/src/platform/nodejs/lib/transport/nodecomettransport.js @@ -270,7 +270,7 @@ class Request extends EventEmitter { return; } - var err = body.error && ErrorInfo.fromValues(body.error); + var err = body.error && ErrorInfo.fromWireValues(body.error); if (!err) { err = new PartialErrorInfo( 'Error response received from server: ' + statusCode + ', body was: ' + util.inspect(body), diff --git a/src/platform/nodejs/lib/util/crypto.ts b/src/platform/nodejs/lib/util/crypto.ts index 69353a1fb8..4eac767221 100644 --- a/src/platform/nodejs/lib/util/crypto.ts +++ b/src/platform/nodejs/lib/util/crypto.ts @@ -180,7 +180,7 @@ var createCryptoClass = function (bufferUtils: typeof BufferUtils) { try { return generateRandom((keyLength || DEFAULT_KEYLENGTH) / 8); } catch (err) { - throw new ErrorInfo('Failed to generate random key: ' + (err as Error).message, 500, 50000); + throw new ErrorInfo('Failed to generate random key: ' + (err as Error).message, 50000, 500); } } diff --git a/src/platform/nodejs/lib/util/http.ts b/src/platform/nodejs/lib/util/http.ts index b78f89c286..b0b8b70d1d 100644 --- a/src/platform/nodejs/lib/util/http.ts +++ b/src/platform/nodejs/lib/util/http.ts @@ -1,6 +1,7 @@ import Platform from 'common/platform'; import Defaults from 'common/lib/util/defaults'; import ErrorInfo from 'common/lib/types/errorinfo'; +import type { ErrorCode } from 'common/lib/types/errorcodes'; import { ErrnoException, RequestBody, @@ -164,11 +165,13 @@ const Http: IPlatformHttpStatic = class { } const error = (body as { error: ErrorInfo }).error - ? ErrorInfo.fromValues((body as { error: ErrorInfo }).error) + ? ErrorInfo.fromWireValues((body as { error: ErrorInfo }).error) : new ErrorInfo( (headers['x-ably-errormessage'] as string) || 'Error response received from server: ' + statusCode + ' body was: ' + Platform.Config.inspect(body), - Number(headers['x-ably-errorcode']), + // Read off a response header, so the server chose it and it can't be checked + // against the registry. + Number(headers['x-ably-errorcode']) as ErrorCode, statusCode, ); diff --git a/src/platform/web/lib/http/http.ts b/src/platform/web/lib/http/http.ts index 387ad2830d..12a89c02c8 100644 --- a/src/platform/web/lib/http/http.ts +++ b/src/platform/web/lib/http/http.ts @@ -15,8 +15,8 @@ export type HTTPRequestImplementations = Pick): re function getAblyError(responseBody: unknown, headers: Record) { if (isAblyError(responseBody, headers)) { - return responseBody.error && ErrorInfo.fromValues(responseBody.error); + return responseBody.error && ErrorInfo.fromWireValues(responseBody.error); } } @@ -175,7 +176,7 @@ class XHRRequest extends EventEmitter implements IXHRRequest { const errorHandler = ( errorEvent: ProgressEvent, message: string, - code: number | null, + code: ErrorCode | null, statusCode: number, ) => { let errorMessage = message + ' (event type: ' + errorEvent.type + ')'; diff --git a/src/platform/web/lib/util/crypto.ts b/src/platform/web/lib/util/crypto.ts index 6139cb19fc..6981e56a8f 100644 --- a/src/platform/web/lib/util/crypto.ts +++ b/src/platform/web/lib/util/crypto.ts @@ -143,7 +143,7 @@ var createCryptoClass = function (config: IPlatformConfig, bufferUtils: typeof B try { return config.getRandomArrayBuffer((keyLength || DEFAULT_KEYLENGTH) / 8); } catch (err) { - throw new ErrorInfo('Failed to generate random key: ' + (err as Error).message, 400, 50000); + throw new ErrorInfo('Failed to generate random key: ' + (err as Error).message, 50000, 500); } } diff --git a/test/common/ably-common b/test/common/ably-common index 496da5ead0..1c86dab126 160000 --- a/test/common/ably-common +++ b/test/common/ably-common @@ -1 +1 @@ -Subproject commit 496da5ead0fa9d6667be0422b5a7ffa62fa7366c +Subproject commit 1c86dab1261df7e9afc155d2fe23f55d51394937 diff --git a/test/uts/rest/unit/types/error_types.test.ts b/test/uts/rest/unit/types/error_types.test.ts index 8e415504ba..37ef3f03b5 100644 --- a/test/uts/rest/unit/types/error_types.test.ts +++ b/test/uts/rest/unit/types/error_types.test.ts @@ -106,7 +106,7 @@ describe('uts/rest/unit/types/error_types', function () { { code: 40400, status: 404, meaning: 'Not found' }, { code: 50000, status: 500, meaning: 'Internal server error' }, { code: 50003, status: 500, meaning: 'Timeout' }, - ]; + ] as const; for (const tc of cases) { const error = new Ably.ErrorInfo(tc.meaning, tc.code, tc.status);