Skip to content

Commit 4349ba7

Browse files
Add support for custom transformAndCheckStatus transform, and forcing json transforms
1 parent 1c55f18 commit 4349ba7

File tree

4 files changed

+171
-3
lines changed

4 files changed

+171
-3
lines changed

packages/kerosene/src/fetch/transformAndCheckStatus.spec.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import ClientError from "./clientError";
33
import HttpError from "./httpError";
44
import ServerError from "./serverError";
55
import _transform from "./transform";
6-
import transformAndCheckStatus from "./transformAndCheckStatus";
6+
import transformAndCheckStatus, {
7+
type TransformOptions,
8+
} from "./transformAndCheckStatus";
79

810
jest.mock("./transform");
911
const transform = _transform as unknown as jest.MockInstance<
@@ -23,6 +25,32 @@ describe("transformAndCheckStatus", () => {
2325
);
2426
});
2527

28+
it("should resolve as null for 204 response", async () => {
29+
const transformed = null;
30+
const response = {
31+
status: 204,
32+
} as Partial<Response> as Response;
33+
when(transform).calledWith(response).mockResolvedValue(transformed);
34+
await expect(transformAndCheckStatus(response)).resolves.toEqual(
35+
transformed,
36+
);
37+
});
38+
39+
it("should resolve a custom transformed response for 2xx if a transform is provided", async () => {
40+
const transformed = { key: "value" };
41+
const customTransform = { key: "transformedValue" };
42+
const response = {
43+
status: 200,
44+
} as Partial<Response> as Response;
45+
const options = {
46+
responseTransform: () => Promise.resolve(customTransform),
47+
} as const satisfies TransformOptions;
48+
when(transform).calledWith(response).mockResolvedValue(transformed);
49+
await expect(transformAndCheckStatus(response, options)).resolves.toEqual(
50+
customTransform,
51+
);
52+
});
53+
2654
it("should reject with a generic Error for a non-2xx response, but transform the response anyway", async () => {
2755
const transformed = { error: "An Error" };
2856
const response = {

packages/kerosene/src/fetch/transformAndCheckStatus.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,22 @@ import HttpError from "./httpError";
33
import ServerError from "./serverError";
44
import transform from "./transform";
55

6+
export type TransformOptions<T = unknown> = {
7+
responseTransform?: (response: Response) => Promise<T>;
8+
};
9+
610
/**
711
* Transforms the response, rejecting if the status is not 2xx
812
* @param response
13+
* @param options
914
*/
1015
export default function transformAndCheckStatus<T = unknown>(
1116
response: Response,
12-
): Promise<T> {
13-
return transform(response).then((transformed) => {
17+
options?: TransformOptions<T>,
18+
): Promise<T | null> {
19+
const transformHandler = options?.responseTransform || transform;
20+
21+
return transformHandler(response).then((transformed) => {
1422
if (response.status >= 200 && response.status < 300) return transformed;
1523

1624
if (response.status >= 400 && response.status < 500) {
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { when } from "jest-when";
2+
import ClientError from "./clientError";
3+
import HttpError from "./httpError";
4+
import ServerError from "./serverError";
5+
import _transform from "./transform";
6+
import transformJsonAndCheckStatus from "./transformJsonAndCheckStatus";
7+
8+
jest.mock("./transform");
9+
const transform = _transform as unknown as jest.MockInstance<
10+
ReturnType<typeof _transform>,
11+
Parameters<typeof _transform>
12+
>;
13+
14+
describe("transformJsonAndCheckStatus", () => {
15+
it("should resolve a transformed response for 2xx", async () => {
16+
const transformed = { key: "value" };
17+
const response = {
18+
status: 200,
19+
json: () => Promise.resolve(transformed),
20+
} as Partial<Response> as Response;
21+
when(transform).calledWith(response).mockResolvedValue(transformed);
22+
await expect(transformJsonAndCheckStatus(response)).resolves.toEqual(
23+
transformed,
24+
);
25+
});
26+
27+
it("should resolve as null for 204 response", async () => {
28+
const transformed = null;
29+
const response = {
30+
status: 204,
31+
} as Partial<Response> as Response;
32+
when(transform).calledWith(response).mockResolvedValue(transformed);
33+
await expect(transformJsonAndCheckStatus(response)).resolves.toEqual(
34+
transformed,
35+
);
36+
});
37+
38+
it("should reject with a generic Error for a non-2xx response, but transform the response anyway", async () => {
39+
const transformed = { error: "An Error" };
40+
const response = {
41+
status: 308,
42+
statusText: "Permanent Redirect",
43+
json: () => Promise.resolve(transformed),
44+
} as Partial<Response> as Response;
45+
when(transform).calledWith(response).mockResolvedValue(transformed);
46+
await transformJsonAndCheckStatus(response).then(
47+
() => {
48+
throw new Error("Expected transformJsonAndCheckStatus to be rejected");
49+
},
50+
(error) => {
51+
expect(error instanceof HttpError).toBe(true);
52+
expect(error instanceof ClientError).toBe(false);
53+
expect(error instanceof ServerError).toBe(false);
54+
expect(error.message).toBe(response.statusText);
55+
expect(error.status).toBe(response.status);
56+
expect(error.response).toBe(transformed);
57+
},
58+
);
59+
});
60+
61+
it("should reject with a ClientError for a 4xx response", async () => {
62+
const transformed = { error: "An Error" };
63+
const response = {
64+
status: 404,
65+
statusText: "Not Found",
66+
json: () => Promise.resolve(transformed),
67+
} as Partial<Response> as Response;
68+
when(transform).calledWith(response).mockResolvedValue(transformed);
69+
await transformJsonAndCheckStatus(response).then(
70+
() => {
71+
throw new Error("Expected transformJsonAndCheckStatus to be rejected");
72+
},
73+
(error) => {
74+
expect(error instanceof ClientError).toBe(true);
75+
expect(error.message).toBe(response.statusText);
76+
expect(error.status).toBe(response.status);
77+
expect(error.response).toBe(transformed);
78+
},
79+
);
80+
});
81+
82+
it("should reject with a ServerError for a 5xx response", async () => {
83+
const response = {
84+
status: 500,
85+
statusText: "Internal Server Error",
86+
json: () => Promise.resolve(undefined),
87+
} as Partial<Response> as Response;
88+
when(transform).calledWith(response).mockResolvedValue(undefined);
89+
await transformJsonAndCheckStatus(response).then(
90+
() => {
91+
throw new Error("Expected transformJsonAndCheckStatus to be rejected");
92+
},
93+
(error) => {
94+
expect(error instanceof ServerError).toBe(true);
95+
expect(error.message).toBe(response.statusText);
96+
expect(error.status).toBe(response.status);
97+
},
98+
);
99+
});
100+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import ClientError from "./clientError";
2+
import HttpError from "./httpError";
3+
import ServerError from "./serverError";
4+
import transform from "./transform";
5+
6+
/**
7+
* Transforms the response, rejecting if the status is not 2xx
8+
* @param response
9+
*/
10+
export default function transformAndCheckStatus<T = unknown>(
11+
response: Response,
12+
): Promise<T | null> {
13+
return transform(response).then(async (originalTransformed) => {
14+
// 204 statuses return null in the original transform, so keep that behaviour here
15+
if (originalTransformed === null) return originalTransformed;
16+
17+
// Otherwise we use .json directly, to avoid incorrect transformations when Content-Type is missing
18+
const transformed = await response.json();
19+
20+
if (response.status >= 200 && response.status < 300) return transformed;
21+
22+
if (response.status >= 400 && response.status < 500) {
23+
throw new ClientError(response.statusText, response.status, transformed);
24+
}
25+
26+
if (response.status >= 500 && response.status < 600) {
27+
throw new ServerError(response.statusText, response.status, transformed);
28+
}
29+
30+
throw new HttpError(response.statusText, response.status, transformed);
31+
});
32+
}

0 commit comments

Comments
 (0)