Skip to content

Commit 7b98eb0

Browse files
🕵️ strengthen error propagation + add tests (#23)
This change adds the following features: * ability to get either an `ArrayBuffer` or a `Uint8Array` response for developer ease * `ProdiaCapacityError` gives a more descriptive error asking you to check your token & job type * `ProdiaBadResponseError` shows the raw response text to give you more information e.g. "invalid token" * last item in state history is checked for failure error messages This change adds the following tests: * a bad job config must return a `ProdiaUserError` referencing that the json schema validation failed, the name of the field, and the criteria for it's failure (i.e. beyond a maximum) * a bad job type returns a `ProdiaCapacityError`. we plan on changing this in the future but the current correct response is a 429 `ProdiaCapacityError` * a bad token must return a `ProdiaBadResponseError` referencing "invalid token" * `image/png` accept type must return a real png image
1 parent 22d9e75 commit 7b98eb0

File tree

5 files changed

+214
-48
lines changed

5 files changed

+214
-48
lines changed

.github/workflows/validate.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ jobs:
1111

1212
- uses: denoland/setup-deno@v1
1313
with:
14-
deno-version: vx.x.x
14+
deno-version: v2.x.x
1515

1616
- run: deno fmt --check
1717

1818
- run: deno check prodia.ts
1919

20+
- run: deno check v2/index.ts
21+
2022
- run: deno test --allow-env --allow-net
2123
env:
2224
PRODIA_TOKEN: ${{ secrets.PRODIA_TOKEN }}

test/v2.errors.test.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { assert, assertStringIncludes } from "jsr:@std/assert";
2+
import {
3+
createProdia,
4+
ProdiaBadResponseError,
5+
ProdiaCapacityError,
6+
ProdiaUserError,
7+
} from "../v2/index.ts";
8+
9+
const token = Deno.env.get("PRODIA_TOKEN");
10+
11+
if (typeof token !== "string") {
12+
throw new Error("PRODIA_TOKEN is not set");
13+
}
14+
15+
await Deno.test("Error Propagation: Bad Job Config returns ProdiaUserError", {
16+
sanitizeResources: false,
17+
}, async () => {
18+
const client = createProdia({
19+
token,
20+
});
21+
22+
try {
23+
const job = await client.job({
24+
type: "inference.flux.schnell.txt2img.v1",
25+
config: {
26+
prompt: "puppies in a cloud, 4k",
27+
steps: 1000,
28+
},
29+
});
30+
31+
throw new Error("Job should not succeed");
32+
} catch (err) {
33+
assert(
34+
err instanceof ProdiaUserError,
35+
"Error should be a ProdiaUserError",
36+
);
37+
assertStringIncludes(err.message, "jsonschema validation failed");
38+
assertStringIncludes(err.message, "steps/maximum");
39+
assertStringIncludes(err.message, "maximum: got 1,000");
40+
}
41+
});
42+
43+
await Deno.test("Error Propagation: No Job Type returns ProdiaCapacityError", {
44+
sanitizeResources: false,
45+
}, async () => {
46+
const client = createProdia({
47+
token,
48+
maxRetries: 2,
49+
});
50+
51+
try {
52+
await client.job({
53+
config: {
54+
prompt: "puppies in a cloud, 4k",
55+
},
56+
});
57+
58+
throw new Error("Job should not succeed");
59+
} catch (err) {
60+
assert(
61+
err instanceof ProdiaCapacityError,
62+
"Error should be a ProdiaCapacityError",
63+
);
64+
65+
assertStringIncludes(
66+
err.message,
67+
"Are your sure your token and job type are correct?",
68+
);
69+
}
70+
});
71+
72+
await Deno.test(
73+
'Error Propagation: Bad Token returns ProdiaBadResponseError "invalid token"',
74+
{ sanitizeResources: false },
75+
async () => {
76+
const client = createProdia({
77+
token: "bad-token-xxx",
78+
});
79+
80+
try {
81+
await client.job({
82+
type: "inference.flux.schnell.txt2img.v1",
83+
config: {
84+
prompt: "puppies in a cloud, 4k",
85+
},
86+
});
87+
88+
throw new Error("Job should not succeed");
89+
} catch (err) {
90+
assert(
91+
err instanceof ProdiaBadResponseError,
92+
"Error should be a ProdiaBadResponseError",
93+
);
94+
95+
assertStringIncludes(err.message, "401 token is invalid");
96+
}
97+
},
98+
);

test/v2.examples.test.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { assert, assertEquals } from "jsr:@std/assert";
2+
import { createProdia } from "../v2/index.ts";
3+
4+
const token = Deno.env.get("PRODIA_TOKEN");
5+
6+
if (typeof token !== "string") {
7+
throw new Error("PRODIA_TOKEN is not set");
8+
}
9+
10+
const isJpeg = (image: ArrayBuffer): boolean => {
11+
const view = new Uint8Array(image);
12+
13+
return view[0] === 0xff && view[1] === 0xd8;
14+
};
15+
16+
const isPng = (image: ArrayBuffer): boolean => {
17+
const view = new Uint8Array(image);
18+
19+
return [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A].every((byte, i) =>
20+
byte === view[i]
21+
);
22+
};
23+
24+
await Deno.test("Example Job: JPEG Output (ArrayBuffer)", async () => {
25+
const client = createProdia({
26+
token,
27+
});
28+
29+
const job = await client.job({
30+
type: "inference.flux.schnell.txt2img.v1",
31+
config: {
32+
prompt: "puppies in a cloud, 4k",
33+
steps: 1,
34+
width: 1024,
35+
height: 1024,
36+
},
37+
});
38+
39+
const image = await job.arrayBuffer();
40+
41+
assert(image instanceof ArrayBuffer, "Image should be an ArrayBuffer");
42+
assert(!(image instanceof Uint8Array), "Image should not be a Uint8Array");
43+
assertEquals(isJpeg(image), true, "Image should be a JPEG");
44+
});
45+
46+
await Deno.test("Example Job: JPEG Output (Uint8Array)", async () => {
47+
const client = createProdia({
48+
token,
49+
});
50+
51+
const job = await client.job({
52+
type: "inference.flux.schnell.txt2img.v1",
53+
config: {
54+
prompt: "puppies in a cloud, 4k",
55+
steps: 1,
56+
width: 1024,
57+
height: 1024,
58+
},
59+
});
60+
61+
const image = await job.uint8Array();
62+
63+
assert(image instanceof Uint8Array, "Image should be a Uint8Array");
64+
assertEquals(isJpeg(image), true, "Image should be a JPEG");
65+
});
66+
67+
await Deno.test("Example Job: PNG Output (Uint8Array)", async () => {
68+
const client = createProdia({
69+
token,
70+
});
71+
72+
const job = await client.job({
73+
type: "inference.flux.schnell.txt2img.v1",
74+
config: {
75+
prompt: "puppies in a cloud, 4k",
76+
steps: 1,
77+
width: 1024,
78+
height: 1024,
79+
},
80+
}, {
81+
accept: "image/png",
82+
});
83+
84+
const image = await job.uint8Array();
85+
86+
assert(image instanceof Uint8Array, "Image should be a Uint8Array");
87+
assertEquals(isPng(image), true, "Image should be a PNG");
88+
});

test/v2.test.ts

Lines changed: 0 additions & 34 deletions
This file was deleted.

v2/index.ts

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,10 @@ const defaultJobOptions: ProdiaJobOptions = {
4444
export type ProdiaJobResponse = {
4545
job: ProdiaJob;
4646

47-
// Currently only one output field is expected for all job types.
48-
//This will return the raw bytes for that output.
47+
// currently these are the only output field for all job types.
48+
// they will return the raw bytes for that output.
4949
arrayBuffer: () => Promise<ArrayBuffer>;
50+
uint8Array: () => Promise<Uint8Array>;
5051
};
5152

5253
/* client & client configuration*/
@@ -75,7 +76,7 @@ export const createProdia = ({
7576
token,
7677
baseUrl = "https://inference.prodia.com/v2",
7778
maxErrors = 1,
78-
maxRetries = Infinity,
79+
maxRetries = 10,
7980
}: CreateProdiaOptions): Prodia => {
8081
const job = async (
8182
params: ProdiaJob,
@@ -164,7 +165,7 @@ export const createProdia = ({
164165

165166
if (response.status === 429) {
166167
throw new ProdiaCapacityError(
167-
"Unable to schedule the job with current token.",
168+
"Unable to schedule the job. Are your sure your token and job type are correct?",
168169
);
169170
}
170171

@@ -175,30 +176,41 @@ export const createProdia = ({
175176
throw new ProdiaUserError(body.error);
176177
}
177178

179+
const lastStateHistory = body.state.history.slice(-1)[0];
180+
181+
if (lastStateHistory && "message" in lastStateHistory) {
182+
throw new ProdiaUserError(lastStateHistory.message);
183+
}
184+
178185
throw new Error("Job Failed: Bad Content-Type: application/json");
179186
}
180187

188+
if (response.status < 200 || response.status > 299) {
189+
throw new ProdiaBadResponseError(
190+
`${response.status} ${await response.text()}`,
191+
);
192+
}
193+
181194
const body = await response.formData();
195+
182196
const job = JSON.parse(
183197
new TextDecoder().decode(
184198
await (body.get("job") as Blob).arrayBuffer(),
185199
),
186200
) as ProdiaJob;
201+
187202
if ("error" in job && typeof job.error === "string") {
188203
throw new ProdiaUserError(job.error);
189204
}
190205

191-
if (response.status < 200 || response.status > 299) {
192-
throw new ProdiaBadResponseError(
193-
`${response.status} ${response.statusText}`,
194-
);
195-
}
196-
197-
const buffer = await (body.get("output") as Blob).arrayBuffer();
198-
199206
return {
200207
job: job,
201-
arrayBuffer: () => Promise.resolve(buffer),
208+
arrayBuffer: async () =>
209+
await (body.get("output") as Blob).arrayBuffer(),
210+
uint8Array: async () =>
211+
new Uint8Array(
212+
await (body.get("output") as Blob).arrayBuffer(),
213+
),
202214
};
203215
};
204216

0 commit comments

Comments
 (0)