Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ CHANGELOG
* **Breaking** Dropped support for Node.js 18 and 20. The minimum supported version is
now 22.
* **Breaking** Dropped commonjs support. The package is now only available as an ES module.
* **Breaking** Errors from the web service client are now thrown as
`WebServiceError` instances, which extend `Error`, rather than as plain
objects. The `code`, `error`, `status`, and `url` properties are preserved,
so existing field access continues to work, but the thrown value is now an
`Error`. The original error is now preserved as the standard `cause`
property (for example, the network error behind a `FETCH_ERROR`). The
`WebServiceError` and `ArgumentError` classes and the `WebServiceClientError`
type are now exported from the package.
* Added the input `/device/tracking_token`. This is the token generated by
the [Device Tracking Add-on](https://dev.maxmind.com/minfraud/track-devices)
for explicit device linking. You may provide this by providing
Expand Down
34 changes: 21 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,17 +74,23 @@ client.insights(transaction).then(insightsResponse => ...);
client.factors(transaction).then(factorsResponse => ...);
```

If the request fails, an error object will be returned in the catch in the form
of:
If the request fails, the returned promise rejects with a `WebServiceError`.
This extends the built-in `Error` and has the following shape:

```js
{
code: string
error: string
status?: number
url: string
cause?: unknown // the underlying error, when one exists
}
```

`error` is also available as the standard `Error` `message`, and when the
failure was caused by another error (for example, a network failure), the
original error is available as `cause`.

### Reporting a transaction using the Report Transactions API

MaxMind encourages the use of this API, as data received through this channel
Expand Down Expand Up @@ -115,25 +121,23 @@ See the API documentation for more details.

If the request succeeds, no data is returned in the Promise.

If the request fails, an error object will be returned in the catch in the
form of:

```js
{
code: string
error: string
url: string
}
```
If the request fails, the returned promise rejects with a `WebServiceError`
(see above for its shape).

## Errors and Exceptions

Thrown by the request and transaction models:
* `ArgumentError` - Thrown when invalid data is passed to the Transaction
and Transaction property constructors.

Web service failures reject with a `WebServiceError`, which extends `Error`.
It exposes `code`, `error`, an optional `status`, `url`, and, when the failure
was caused by another error, the standard `cause` property. Both
`ArgumentError` and `WebServiceError` (along with the `WebServiceClientError`
type) are exported from the package.

In addition to the [response errors](https://dev.maxmind.com/minfraud/api-documentation/responses/?lang=en#errors)
returned by the web API, we also return:
returned by the web API, we also return these `code` values:

```js
{
Expand Down Expand Up @@ -162,6 +166,10 @@ returned by the web API, we also return:
}
```

For `FETCH_ERROR`, the `error` message includes the underlying failure reason
(for example, a DNS or connection error) when one is available, and the
original error is also attached as `cause`.

## Example

```js
Expand Down
105 changes: 105 additions & 0 deletions src/errors.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { ArgumentError, WebServiceError } from './errors.js';

describe('WebServiceError', () => {
it('is an Error instance', () => {
const err = new WebServiceError({
code: 'FETCH_ERROR',
error: 'something went wrong',
url: 'https://example.com',
});

expect(err).toBeInstanceOf(Error);
expect(err).toBeInstanceOf(WebServiceError);
expect(err.name).toBe('WebServiceError');
});

it('exposes code, error, status, and url', () => {
const err = new WebServiceError({
code: 'SERVER_ERROR',
error: 'boom',
status: 500,
url: 'https://example.com',
});

expect(err.code).toBe('SERVER_ERROR');
expect(err.error).toBe('boom');
expect(err.status).toBe(500);
expect(err.url).toBe('https://example.com');
});

it('uses the error string as the message', () => {
const err = new WebServiceError({
code: 'FETCH_ERROR',
error: 'the message',
url: 'https://example.com',
});

expect(err.message).toBe('the message');
expect(err.error).toBe(err.message);
});

it('preserves the underlying cause', () => {
const cause = new TypeError('fetch failed');
const err = new WebServiceError(
{
code: 'FETCH_ERROR',
error: 'TypeError - fetch failed',
url: 'https://example.com',
},
{ cause }
);

expect(err.cause).toBe(cause);
});

it('leaves cause undefined when not provided', () => {
const err = new WebServiceError({
code: 'FETCH_ERROR',
error: 'something went wrong',
url: 'https://example.com',
});

expect(err.cause).toBeUndefined();
expect(JSON.parse(JSON.stringify(err))).not.toHaveProperty('cause');
});

it('omits status when not provided', () => {
const err = new WebServiceError({
code: 'FETCH_ERROR',
error: 'something went wrong',
url: 'https://example.com',
});

expect(err.status).toBeUndefined();
expect(Object.prototype.hasOwnProperty.call(err, 'status')).toBe(false);
});

it('retains code, error, status, and url as enumerable properties', () => {
const err = new WebServiceError({
code: 'SERVER_ERROR',
error: 'boom',
status: 500,
url: 'https://example.com',
});

const serialized = JSON.parse(JSON.stringify(err));
expect(serialized).toMatchObject({
code: 'SERVER_ERROR',
error: 'boom',
status: 500,
url: 'https://example.com',
});
// `name` lives on the prototype, so it is not serialized.
expect(serialized).not.toHaveProperty('name');
});
});

describe('ArgumentError', () => {
it('is an Error instance', () => {
const err = new ArgumentError('bad input');

expect(err).toBeInstanceOf(Error);
expect(err.name).toBe('ArgumentError');
expect(err.message).toBe('bad input');
});
});
61 changes: 61 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,68 @@
import { WebServiceClientError } from './types.js';

/* tslint:disable:max-classes-per-file */
export class ArgumentError extends Error {
constructor(message: string) {
super(message);
this.name = this.constructor.name;
}
}
Comment on lines 4 to 9

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Expose structured fields on ArgumentError before exporting it publicly.

src/index.ts now re-exports this class, but it still only exposes name and message. That leaves library consumers without the code / error contract this file is supposed to provide for errors.

Proposed fix
 export class ArgumentError extends Error {
+  public readonly code: string;
+  public readonly error: string;
+
   constructor(message: string) {
     super(message);
     this.name = this.constructor.name;
+    this.code = 'INVALID_ARGUMENT';
+    this.error = message;
   }
 }

As per coding guidelines, src/errors.ts: Ensure error objects include code, error, and optional url properties for proper error handling by library consumers.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export class ArgumentError extends Error {
constructor(message: string) {
super(message);
this.name = this.constructor.name;
}
}
export class ArgumentError extends Error {
public readonly code: string;
public readonly error: string;
constructor(message: string) {
super(message);
this.name = this.constructor.name;
this.code = 'INVALID_ARGUMENT';
this.error = message;
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/errors.ts` around lines 4 - 9, Expose the structured error contract on
ArgumentError in src/errors.ts before it is re-exported through src/index.ts:
update the ArgumentError class so instances include the expected code and error
fields, and add an optional url field when applicable, alongside the existing
name/message setup. Make the constructor initialize these public properties
consistently so consumers can reliably inspect the error shape after importing
ArgumentError.

Source: Coding guidelines


/**
* An error returned by the minFraud web service or encountered while
* communicating with it.
*
* In addition to the standard `Error` properties (including `cause`, which
* holds the underlying error when one exists, such as the network error
* behind a `FETCH_ERROR`), it exposes the `code`, `status`, and `url`
* associated with the failure.
*/
export class WebServiceError extends Error implements WebServiceClientError {
/**
* The error code returned by the web service or generated by this client.
*/
public readonly code: string;
/**
* A human-readable description of the error. This is an alias of `message`,
* retained for backward compatibility.
*/
public readonly error: string;
/**
* The HTTP status code, when the error originated from an HTTP response.
*
* Declared with `declare` so that no class field is emitted: the property is
* absent (rather than set to `undefined`) when no status applies, e.g. on
* network-level errors such as `FETCH_ERROR` and `NETWORK_TIMEOUT`.
*/
declare public readonly status?: number;
/**
* The URL that was being requested when the error occurred.
*/
public readonly url: string;

constructor(
properties: {
code: string;
error: string;
status?: number;
url: string;
},
options?: { cause?: unknown }
) {
super(properties.error, options);
this.code = properties.code;
this.error = properties.error;
// Only assign `status` when present so it stays genuinely optional: on
// network-level errors (e.g. FETCH_ERROR, NETWORK_TIMEOUT) the instance
// carries no `status` property at all, rather than `status: undefined`.
if (properties.status !== undefined) {
this.status = properties.status;
}
this.url = properties.url;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}

// Set `name` on the prototype rather than in the constructor. Using a string
// literal keeps the name correct under minification, and keeping it off the
// instance means it stays out of `JSON.stringify` output.
WebServiceError.prototype.name = 'WebServiceError';
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as Constants from './constants.js';
import { ArgumentError, WebServiceError } from './errors.js';
import Account from './request/account.js';
import Billing from './request/billing.js';
import CreditCard from './request/credit-card.js';
Expand All @@ -12,10 +13,12 @@ import Shipping from './request/shipping.js';
import ShoppingCartItem from './request/shopping-cart-item.js';
import Transaction from './request/transaction.js';
import TransactionReport from './request/transaction-report.js';
import { WebServiceClientError } from './types.js';
import Client from './webServiceClient.js';

export {
Account,
ArgumentError,
Billing,
Client,
Constants,
Expand All @@ -30,4 +33,7 @@ export {
ShoppingCartItem,
Transaction,
TransactionReport,
WebServiceError,
};

export type { WebServiceClientError };
5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,9 @@ export interface WebServiceClientError {
error: string;
status?: number;
url: string;
/**
* The underlying error that caused this one, when available (for example,
* the network error behind a `FETCH_ERROR`).
*/
cause?: unknown;
}
Loading