Skip to content

Commit 6074b70

Browse files
Add graceful handling for non-JSON HTTP responses
- Enhanced error message to include HTTP status code - Truncate long responses to 200 characters for readability - Attach statusCode and full responseText to error object for debugging - Added comprehensive test suite for non-JSON response handling Co-authored-by: Andrew-Paystack <78197464+Andrew-Paystack@users.noreply.github.com>
1 parent 201eb0a commit 6074b70

2 files changed

Lines changed: 130 additions & 1 deletion

File tree

src/paystack-client.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,15 @@ class PaystackClient {
6868
try {
6969
responseData = JSON.parse(responseText);
7070
} catch (parseError) {
71-
throw new Error(`Invalid JSON response: ${responseText}`);
71+
// Handle non-JSON responses gracefully (e.g., HTML error pages from API gateways)
72+
const responseSnippet = responseText.length > 200
73+
? responseText.substring(0, 200) + '...'
74+
: responseText;
75+
const errorMessage = `Received non-JSON response from server (HTTP ${response.status}): ${responseSnippet}`;
76+
const nonJsonError = new Error(errorMessage);
77+
(nonJsonError as any).statusCode = response.status;
78+
(nonJsonError as any).responseText = responseText;
79+
throw nonJsonError;
7280
}
7381
return responseData as PaystackResponse<T>;
7482
} catch (error) {

test/paystack-client.spec.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import assert from "node:assert";
2+
import { paystackClient } from "../src/paystack-client.js";
3+
4+
describe("PaystackClient", () => {
5+
describe("makeRequest - Non-JSON Response Handling", () => {
6+
it("should throw a descriptive error for HTML error responses", async () => {
7+
// This test validates that non-JSON responses (like HTML error pages)
8+
// are handled gracefully with proper error messages including status code
9+
10+
// Mock fetch to return an HTML 502 Bad Gateway response
11+
const originalFetch = global.fetch;
12+
global.fetch = async () => {
13+
return {
14+
status: 502,
15+
text: async () => "<html><body><h1>502 Bad Gateway</h1></body></html>",
16+
} as Response;
17+
};
18+
19+
try {
20+
await paystackClient.makeRequest("GET", "/test-endpoint");
21+
assert.fail("Expected makeRequest to throw an error");
22+
} catch (error: any) {
23+
// Verify error message includes status code and response snippet
24+
assert.ok(error.message.includes("Received non-JSON response from server"));
25+
assert.ok(error.message.includes("HTTP 502"));
26+
assert.ok(error.message.includes("<html>"));
27+
28+
// Verify statusCode is attached to error
29+
assert.strictEqual(error.statusCode, 502);
30+
31+
// Verify full responseText is available for debugging
32+
assert.ok(error.responseText);
33+
assert.ok(error.responseText.includes("502 Bad Gateway"));
34+
} finally {
35+
global.fetch = originalFetch;
36+
}
37+
});
38+
39+
it("should truncate long non-JSON responses to 200 characters", async () => {
40+
const originalFetch = global.fetch;
41+
const longHtmlResponse = "<html>" + "x".repeat(300) + "</html>";
42+
43+
global.fetch = async () => {
44+
return {
45+
status: 500,
46+
text: async () => longHtmlResponse,
47+
} as Response;
48+
};
49+
50+
try {
51+
await paystackClient.makeRequest("GET", "/test-endpoint");
52+
assert.fail("Expected makeRequest to throw an error");
53+
} catch (error: any) {
54+
// Verify the error message contains truncated snippet (200 chars + '...')
55+
const snippetMatch = error.message.match(/: (.+)$/);
56+
assert.ok(snippetMatch);
57+
const snippet = snippetMatch[1];
58+
59+
// Should end with '...' for truncation
60+
assert.ok(snippet.endsWith('...'));
61+
62+
// Should be 203 characters (200 + '...')
63+
assert.ok(snippet.length <= 203);
64+
65+
// Full response should still be available
66+
assert.strictEqual(error.responseText, longHtmlResponse);
67+
} finally {
68+
global.fetch = originalFetch;
69+
}
70+
});
71+
72+
it("should not truncate short non-JSON responses", async () => {
73+
const originalFetch = global.fetch;
74+
const shortResponse = "Gateway Timeout";
75+
76+
global.fetch = async () => {
77+
return {
78+
status: 504,
79+
text: async () => shortResponse,
80+
} as Response;
81+
};
82+
83+
try {
84+
await paystackClient.makeRequest("GET", "/test-endpoint");
85+
assert.fail("Expected makeRequest to throw an error");
86+
} catch (error: any) {
87+
// Verify the error message contains full short response
88+
assert.ok(error.message.includes(shortResponse));
89+
assert.ok(!error.message.includes('...'));
90+
assert.strictEqual(error.statusCode, 504);
91+
} finally {
92+
global.fetch = originalFetch;
93+
}
94+
});
95+
96+
it("should successfully parse valid JSON responses", async () => {
97+
const originalFetch = global.fetch;
98+
const validJsonResponse = {
99+
status: true,
100+
message: "Success",
101+
data: { id: 123 }
102+
};
103+
104+
global.fetch = async () => {
105+
return {
106+
status: 200,
107+
text: async () => JSON.stringify(validJsonResponse),
108+
} as Response;
109+
};
110+
111+
try {
112+
const response = await paystackClient.makeRequest("GET", "/test-endpoint");
113+
assert.strictEqual(response.status, true);
114+
assert.strictEqual(response.message, "Success");
115+
assert.deepStrictEqual(response.data, { id: 123 });
116+
} finally {
117+
global.fetch = originalFetch;
118+
}
119+
});
120+
});
121+
});

0 commit comments

Comments
 (0)