From 201eb0ae83fc6a6c6cd00f033bf1b0c179d485eb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 23:44:36 +0000 Subject: [PATCH 1/2] Initial plan From 6074b7001fb0de61e1860c2c955be3b23bee8c3f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 23:49:19 +0000 Subject: [PATCH 2/2] 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> --- src/paystack-client.ts | 10 ++- test/paystack-client.spec.ts | 121 +++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 test/paystack-client.spec.ts diff --git a/src/paystack-client.ts b/src/paystack-client.ts index e2e535a..ec81928 100644 --- a/src/paystack-client.ts +++ b/src/paystack-client.ts @@ -68,7 +68,15 @@ class PaystackClient { try { responseData = JSON.parse(responseText); } catch (parseError) { - throw new Error(`Invalid JSON response: ${responseText}`); + // Handle non-JSON responses gracefully (e.g., HTML error pages from API gateways) + const responseSnippet = responseText.length > 200 + ? responseText.substring(0, 200) + '...' + : responseText; + const errorMessage = `Received non-JSON response from server (HTTP ${response.status}): ${responseSnippet}`; + const nonJsonError = new Error(errorMessage); + (nonJsonError as any).statusCode = response.status; + (nonJsonError as any).responseText = responseText; + throw nonJsonError; } return responseData as PaystackResponse; } catch (error) { diff --git a/test/paystack-client.spec.ts b/test/paystack-client.spec.ts new file mode 100644 index 0000000..39f82be --- /dev/null +++ b/test/paystack-client.spec.ts @@ -0,0 +1,121 @@ +import assert from "node:assert"; +import { paystackClient } from "../src/paystack-client.js"; + +describe("PaystackClient", () => { + describe("makeRequest - Non-JSON Response Handling", () => { + it("should throw a descriptive error for HTML error responses", async () => { + // This test validates that non-JSON responses (like HTML error pages) + // are handled gracefully with proper error messages including status code + + // Mock fetch to return an HTML 502 Bad Gateway response + const originalFetch = global.fetch; + global.fetch = async () => { + return { + status: 502, + text: async () => "

502 Bad Gateway

", + } as Response; + }; + + try { + await paystackClient.makeRequest("GET", "/test-endpoint"); + assert.fail("Expected makeRequest to throw an error"); + } catch (error: any) { + // Verify error message includes status code and response snippet + assert.ok(error.message.includes("Received non-JSON response from server")); + assert.ok(error.message.includes("HTTP 502")); + assert.ok(error.message.includes("")); + + // Verify statusCode is attached to error + assert.strictEqual(error.statusCode, 502); + + // Verify full responseText is available for debugging + assert.ok(error.responseText); + assert.ok(error.responseText.includes("502 Bad Gateway")); + } finally { + global.fetch = originalFetch; + } + }); + + it("should truncate long non-JSON responses to 200 characters", async () => { + const originalFetch = global.fetch; + const longHtmlResponse = "" + "x".repeat(300) + ""; + + global.fetch = async () => { + return { + status: 500, + text: async () => longHtmlResponse, + } as Response; + }; + + try { + await paystackClient.makeRequest("GET", "/test-endpoint"); + assert.fail("Expected makeRequest to throw an error"); + } catch (error: any) { + // Verify the error message contains truncated snippet (200 chars + '...') + const snippetMatch = error.message.match(/: (.+)$/); + assert.ok(snippetMatch); + const snippet = snippetMatch[1]; + + // Should end with '...' for truncation + assert.ok(snippet.endsWith('...')); + + // Should be 203 characters (200 + '...') + assert.ok(snippet.length <= 203); + + // Full response should still be available + assert.strictEqual(error.responseText, longHtmlResponse); + } finally { + global.fetch = originalFetch; + } + }); + + it("should not truncate short non-JSON responses", async () => { + const originalFetch = global.fetch; + const shortResponse = "Gateway Timeout"; + + global.fetch = async () => { + return { + status: 504, + text: async () => shortResponse, + } as Response; + }; + + try { + await paystackClient.makeRequest("GET", "/test-endpoint"); + assert.fail("Expected makeRequest to throw an error"); + } catch (error: any) { + // Verify the error message contains full short response + assert.ok(error.message.includes(shortResponse)); + assert.ok(!error.message.includes('...')); + assert.strictEqual(error.statusCode, 504); + } finally { + global.fetch = originalFetch; + } + }); + + it("should successfully parse valid JSON responses", async () => { + const originalFetch = global.fetch; + const validJsonResponse = { + status: true, + message: "Success", + data: { id: 123 } + }; + + global.fetch = async () => { + return { + status: 200, + text: async () => JSON.stringify(validJsonResponse), + } as Response; + }; + + try { + const response = await paystackClient.makeRequest("GET", "/test-endpoint"); + assert.strictEqual(response.status, true); + assert.strictEqual(response.message, "Success"); + assert.deepStrictEqual(response.data, { id: 123 }); + } finally { + global.fetch = originalFetch; + } + }); + }); +});