Skip to content

Commit e0f5400

Browse files
committed
Use fetch
1 parent cd26658 commit e0f5400

File tree

6 files changed

+113
-81
lines changed

6 files changed

+113
-81
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
CHANGELOG
22
=========
33

4-
7.1.0
4+
8.0.0
55
-----------------
66

7+
* **Breaking** Internal webservice calls now use Node's built-in `fetch` instead of `http`. This
8+
will affect users who are on unsupported versions of Node, specifically Node 17 and below.
9+
* Two new error codes have been added: `NETWORK_TIMEOUT` and `FETCH_ERROR`, second of which is returned
10+
when there's a `fetch` related error that could not be handled by other errors.
711
* The minFraud Factors subscores have been deprecated. They will be removed
812
in March 2025. Please see [our release notes](https://dev.maxmind.com/minfraud/release-notes/2024/#deprecation-of-risk-factor-scoressubscores)
913
for more information.

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,16 @@ returned by the web API, we also return:
151151
code: 'INVALID_RESPONSE_BODY',
152152
error: <string>
153153
}
154+
155+
{
156+
code: 'NETWORK_TIMEOUT',
157+
error: <string>
158+
}
159+
160+
{
161+
code: 'FETCH_ERROR',
162+
error: <string>
163+
}
154164
```
155165

156166
## Example

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"gh-pages": "^6.0.0",
3333
"globals": "^15.9.0",
3434
"jest": "^29.5.0",
35-
"nock": "^14.0.0-beta.15",
35+
"nock": "^14.0.0-beta.16",
3636
"prettier": "^3.0.0",
3737
"ts-jest": "^29.1.0",
3838
"typedoc": "^0.26.3",

src/webServiceClient.spec.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -879,12 +879,33 @@ describe('WebServiceClient', () => {
879879
});
880880

881881
describe('error handling', () => {
882+
afterEach(() => {
883+
nock.cleanAll();
884+
});
885+
882886
const transaction = new Transaction({
883887
device: new Device({
884888
ipAddress: '1.1.1.1',
885889
}),
886890
});
887891

892+
it('handles timeouts', () => {
893+
const timeoutClient = new Client(auth.user, auth.pass, 10);
894+
expect.assertions(1);
895+
896+
nockInstance
897+
.post(fullPath('score'), score.request.basic)
898+
.basicAuth(auth)
899+
.delayConnection(100)
900+
.reply(200, score.response.full);
901+
902+
return expect(timeoutClient.score(transaction)).rejects.toEqual({
903+
code: 'NETWORK_TIMEOUT',
904+
error: 'The request timed out',
905+
url: baseUrl + fullPath('score'),
906+
});
907+
});
908+
888909
it('handles 5xx level errors', () => {
889910
expect.assertions(1);
890911

@@ -930,15 +951,12 @@ describe('WebServiceClient', () => {
930951
});
931952
});
932953

933-
it('handles general http.request errors', () => {
934-
const error = {
935-
code: 'FOO_ERR',
936-
message: 'some foo error',
937-
};
954+
it('handles general fetch errors', () => {
955+
const error = 'general error';
938956

939957
const expected = {
940-
code: error.code,
941-
error: error.message,
958+
code: 'FETCH_ERROR',
959+
error: `Error - ${error}`,
942960
url: baseUrl + fullPath('score'),
943961
};
944962

src/webServiceClient.ts

Lines changed: 71 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import * as http from 'http';
2-
import * as https from 'https';
31
import { version } from '../package.json';
42
import Transaction from './request/transaction';
53
import TransactionReport from './request/transaction-report';
@@ -13,6 +11,11 @@ interface ResponseError {
1311

1412
type servicePath = 'factors' | 'insights' | 'score' | 'transactions/report';
1513

14+
const invalidResponseBody = {
15+
code: 'INVALID_RESPONSE_BODY',
16+
error: 'Received an invalid or unparseable response body',
17+
};
18+
1619
export default class WebServiceClient {
1720
private accountID: string;
1821
private host: string;
@@ -65,7 +68,7 @@ export default class WebServiceClient {
6568
// eslint-disable-next-line @typescript-eslint/no-explicit-any
6669
modelClass?: any
6770
): Promise<T>;
68-
private responseFor(
71+
private async responseFor(
6972
path: servicePath,
7073
postData: string,
7174
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -74,97 +77,94 @@ export default class WebServiceClient {
7477
const parsedPath = `/minfraud/v2.0/${path}`;
7578
const url = `https://${this.host}${parsedPath}`;
7679

77-
const options = {
78-
auth: `${this.accountID}:${this.licenseKey}`,
80+
const controller = new AbortController();
81+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
82+
83+
const options: RequestInit = {
84+
body: postData,
7985
headers: {
8086
Accept: 'application/json',
81-
'Content-Length': Buffer.byteLength(postData),
87+
Authorization:
88+
'Basic ' +
89+
Buffer.from(`${this.accountID}:${this.licenseKey}`).toString(
90+
'base64'
91+
),
92+
'Content-Length': Buffer.byteLength(postData).toString(),
8293
'Content-Type': 'application/json',
8394
'User-Agent': `minfraud-api-node/${version}`,
8495
},
85-
host: this.host,
8696
method: 'POST',
87-
path: parsedPath,
88-
timeout: this.timeout,
97+
signal: controller.signal,
8998
};
9099

91-
return new Promise((resolve, reject) => {
92-
const req = https.request(options, (response) => {
93-
let data = '';
94-
95-
response.on('data', (chunk) => {
96-
data += chunk;
97-
});
98-
99-
response.on('end', () => {
100-
if (response.statusCode && response.statusCode === 204) {
101-
return resolve();
102-
}
103-
104-
try {
105-
data = JSON.parse(data);
106-
} catch {
107-
return reject(this.handleError({}, response, url));
108-
}
109-
110-
if (response.statusCode && response.statusCode !== 200) {
111-
return reject(
112-
this.handleError(data as ResponseError, response, url)
113-
);
114-
}
115-
116-
return resolve(new modelClass(data));
117-
});
118-
});
119-
120-
req.on('error', (err: NodeJS.ErrnoException) => {
121-
return reject({
122-
code: err.code,
123-
error: err.message,
124-
url,
125-
} as WebServiceClientError);
126-
});
127-
128-
req.write(postData);
129-
130-
req.end();
131-
});
100+
let data;
101+
try {
102+
const response = await fetch(url, options);
103+
104+
if (!response.ok) {
105+
return Promise.reject(await this.handleError(response, url));
106+
}
107+
108+
if (response.status === 204) {
109+
return Promise.resolve();
110+
}
111+
data = await response.json();
112+
} catch (err) {
113+
const error = err as TypeError;
114+
switch (error.name) {
115+
case 'AbortError':
116+
return Promise.reject({
117+
code: 'NETWORK_TIMEOUT',
118+
error: 'The request timed out',
119+
url,
120+
});
121+
case 'SyntaxError':
122+
return Promise.reject({
123+
...invalidResponseBody,
124+
url,
125+
});
126+
default:
127+
return Promise.reject({
128+
code: 'FETCH_ERROR',
129+
error: `${error.name} - ${error.message}`,
130+
url,
131+
});
132+
}
133+
} finally {
134+
clearTimeout(timeoutId);
135+
}
136+
return new modelClass(data);
132137
}
133138

134-
private handleError(
135-
data: ResponseError,
136-
response: http.IncomingMessage,
139+
private async handleError(
140+
response: Response,
137141
url: string
138-
): WebServiceClientError {
139-
if (
140-
response.statusCode &&
141-
response.statusCode >= 500 &&
142-
response.statusCode < 600
143-
) {
142+
): Promise<WebServiceClientError> {
143+
if (response.status && response.status >= 500 && response.status < 600) {
144144
return {
145145
code: 'SERVER_ERROR',
146-
error: `Received a server error with HTTP status code: ${response.statusCode}`,
146+
error: `Received a server error with HTTP status code: ${response.status}`,
147147
url,
148148
};
149149
}
150150

151-
if (
152-
response.statusCode &&
153-
(response.statusCode < 400 || response.statusCode >= 600)
154-
) {
151+
if (response.status && (response.status < 400 || response.status >= 600)) {
155152
return {
156153
code: 'HTTP_STATUS_CODE_ERROR',
157-
error: `Received an unexpected HTTP status code: ${response.statusCode}`,
154+
error: `Received an unexpected HTTP status code: ${response.status}`,
158155
url,
159156
};
160157
}
161158

162-
if (!data.code || !data.error) {
163-
return {
164-
code: 'INVALID_RESPONSE_BODY',
165-
error: 'Received an invalid or unparseable response body',
166-
url,
167-
};
159+
let data;
160+
try {
161+
data = (await response.json()) as ResponseError;
162+
163+
if (!data.code || !data.error) {
164+
return { ...invalidResponseBody, url };
165+
}
166+
} catch {
167+
return { ...invalidResponseBody, url };
168168
}
169169

170170
return { ...data, url } as WebServiceClientError;

0 commit comments

Comments
 (0)