Skip to content

Commit 9ca7685

Browse files
committed
Handle errors during requests more intentionally
1 parent 9309043 commit 9ca7685

File tree

3 files changed

+114
-20
lines changed

3 files changed

+114
-20
lines changed

lib/ReplicateClient.js

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
import fetch from "node-fetch";
1+
import fetch, { FetchError } from "node-fetch";
22
import pkg from "../package.json" assert { type: "json" };
3-
import { ReplicateError } from "./errors.js";
3+
import {
4+
ReplicateError,
5+
ReplicateRequestError,
6+
ReplicateResponseError,
7+
} from "./errors.js";
48
import Prediction from "./Prediction.js";
59
import { sleep } from "./utils.js";
610

@@ -70,30 +74,43 @@ export default class ReplicateClient {
7074
async request(action, body) {
7175
const { method, path } = this.#parseAction(action);
7276

77+
const url = new URL(path, this.baseURL).href;
78+
const requestInit = {
79+
method,
80+
headers: this.#headers(),
81+
body: JSON.stringify(body),
82+
};
83+
7384
let resp;
7485
try {
75-
resp = await fetch(new URL(path, this.baseURL).href, {
76-
method,
77-
headers: this.#headers(),
78-
body: JSON.stringify(body),
79-
});
86+
resp = await fetch(url, requestInit);
8087
} catch (err) {
81-
// TODO: Better error handling.
82-
throw err;
88+
if (!err instanceof FetchError) {
89+
throw err;
90+
}
91+
92+
throw new ReplicateRequestError(url, requestInit);
8393
}
8494

95+
const respText = await resp.text();
96+
8597
if (!resp.ok) {
86-
// TODO: Better error handling.
87-
console.error(await resp.text());
88-
throw new ReplicateError(resp.statusText);
98+
let errorMessage;
99+
try {
100+
const respJSON = JSON.parse(respText);
101+
errorMessage = respJSON.details;
102+
} catch (err) {
103+
if (!err instanceof SyntaxError) {
104+
throw err;
105+
}
106+
107+
errorMessage = respText;
108+
}
109+
110+
throw new ReplicateResponseError(errorMessage, resp, action);
89111
}
90112

91-
try {
92-
return await resp.json();
93-
} catch (err) {
94-
// TODO: Better error handling.
95-
throw err;
96-
}
113+
return JSON.parse(respText);
97114
}
98115

99116
#headers() {

lib/ReplicateClient.test.js

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
11
import { jest } from "@jest/globals";
2-
import { ReplicateError } from "./errors.js";
2+
import { FetchError, Response } from "node-fetch";
3+
4+
jest.unstable_mockModule("node-fetch", () => ({
5+
default: jest.fn(),
6+
FetchError,
7+
}));
8+
9+
import {
10+
ReplicateError,
11+
ReplicateRequestError,
12+
ReplicateResponseError,
13+
} from "./errors.js";
314
import { PredictionStatus } from "./Prediction.js";
4-
import ReplicateClient from "./ReplicateClient.js";
15+
16+
const { default: fetch } = await import("node-fetch");
17+
const { default: ReplicateClient } = await import("./ReplicateClient.js");
518

619
let client;
720

@@ -161,3 +174,39 @@ describe("getPrediction()", () => {
161174
expect(client.request).toHaveBeenCalledWith("GET /v1/predictions/test-id");
162175
});
163176
});
177+
178+
describe("request()", () => {
179+
it("throws ReplicateRequestError on failed fetch", async () => {
180+
fetch.mockImplementation(async () => {
181+
throw new FetchError("Something went wrong");
182+
});
183+
184+
await expect(
185+
async () => await client.request("GET /v1/predictions/test-id")
186+
).rejects.toThrowError(ReplicateRequestError);
187+
await expect(
188+
async () => await client.request("GET /v1/predictions/test-id")
189+
).rejects.toThrowError(
190+
"Failed to make request: method=GET, url=https://api.replicate.com/v1/predictions/test-id, body=undefined"
191+
);
192+
});
193+
194+
it("throws ReplicateResponseError on error status", async () => {
195+
fetch.mockImplementation(
196+
async () =>
197+
new Response('{"status":403,"details":"Something went wrong"}', {
198+
status: 403,
199+
statusText: "Unauthorized",
200+
})
201+
);
202+
203+
await expect(
204+
async () => await client.request("GET /v1/predictions/test-id")
205+
).rejects.toThrowError(ReplicateResponseError);
206+
await expect(
207+
async () => await client.request("GET /v1/predictions/test-id")
208+
).rejects.toThrowError(
209+
"403 Unauthorized for GET /v1/predictions/test-id: Something went wrong"
210+
);
211+
});
212+
});

lib/errors.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,29 @@
11
export class ReplicateError extends Error {}
2+
3+
export class ReplicateRequestError extends ReplicateError {
4+
method;
5+
body;
6+
7+
constructor(url, requestInit) {
8+
super(
9+
`Failed to make request: method=${requestInit.method}, url=${url}, body=${requestInit.body}`
10+
);
11+
12+
this.method = requestInit.method;
13+
this.body = requestInit.body;
14+
}
15+
}
16+
17+
export class ReplicateResponseError extends ReplicateError {
18+
status;
19+
20+
constructor(message, response, action) {
21+
super(
22+
`${response.status} ${response.statusText}${
23+
action ? ` for ${action}` : ""
24+
}: ${message}`
25+
);
26+
27+
this.status = response.status;
28+
}
29+
}

0 commit comments

Comments
 (0)