From 1025172d823721990069362dde2b81971c51dba4 Mon Sep 17 00:00:00 2001 From: hadessunxy-code Date: Fri, 22 May 2026 13:43:11 +0100 Subject: [PATCH] Handle empty API responses gracefully --- package.json | 2 +- src/api-response.js | 65 +++++++++++++++++++++++++++++++++ test/api-response.test.js | 76 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 src/api-response.js create mode 100644 test/api-response.test.js diff --git a/package.json b/package.json index 6ec1d3f..4e7efa2 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "A simple CSV parser - BountyPay workflow demo", "main": "src/parser.js", "scripts": { - "test": "node test/parser.test.js" + "test": "node test/parser.test.js && node test/api-response.test.js" }, "license": "MIT" } diff --git a/src/api-response.js b/src/api-response.js new file mode 100644 index 0000000..24687bc --- /dev/null +++ b/src/api-response.js @@ -0,0 +1,65 @@ +const DEFAULT_EMPTY_MESSAGE = 'No data was returned by the server. Please try again.'; +const DEFAULT_NETWORK_MESSAGE = 'No response was received from the server. Please try again.'; +const DEFAULT_PARSE_MESSAGE = 'The server returned an invalid response. Please try again.'; + +function friendlyError(message, details = {}) { + return { + ok: false, + data: null, + error: { + message, + ...details, + }, + }; +} + +async function readBody(response) { + if (typeof response === 'string') { + return response; + } + + if (response && typeof response.text === 'function') { + return response.text(); + } + + if (response && 'body' in response) { + return response.body; + } + + return response; +} + +async function parseApiResponse(response) { + if (!response) { + return friendlyError(DEFAULT_NETWORK_MESSAGE); + } + + const status = typeof response.status === 'number' ? response.status : undefined; + const ok = typeof response.ok === 'boolean' ? response.ok : status === undefined || (status >= 200 && status < 300); + + if (status === 204 || status === 205) { + return friendlyError(DEFAULT_EMPTY_MESSAGE, { status }); + } + + const body = await readBody(response); + + if (body == null || (typeof body === 'string' && body.trim() === '')) { + return friendlyError(DEFAULT_EMPTY_MESSAGE, { status }); + } + + if (!ok) { + return friendlyError(`Request failed${status ? ` with status ${status}` : ''}. Please try again.`, { status }); + } + + if (typeof body !== 'string') { + return { ok: true, data: body, error: null }; + } + + try { + return { ok: true, data: JSON.parse(body), error: null }; + } catch (_error) { + return friendlyError(DEFAULT_PARSE_MESSAGE, { status }); + } +} + +module.exports = { parseApiResponse }; diff --git a/test/api-response.test.js b/test/api-response.test.js new file mode 100644 index 0000000..d7c2072 --- /dev/null +++ b/test/api-response.test.js @@ -0,0 +1,76 @@ +const { parseApiResponse } = require('../src/api-response'); + +let passed = 0; +let failed = 0; + +function assert(name, actual, expected) { + const a = JSON.stringify(actual); + const e = JSON.stringify(expected); + if (a === e) { + console.log(` OK ${name}`); + passed++; + } else { + console.log(` FAIL ${name}`); + console.log(` Expected: ${e}`); + console.log(` Actual: ${a}`); + failed++; + } +} + +async function run() { + console.log('\nAPI Response Tests\n'); + + assert( + 'returns parsed JSON for a valid response', + await parseApiResponse({ ok: true, status: 200, text: async () => '{"items":[1,2]}' }), + { ok: true, data: { items: [1, 2] }, error: null } + ); + + assert( + 'handles null response without throwing', + await parseApiResponse(null), + { + ok: false, + data: null, + error: { message: 'No response was received from the server. Please try again.' }, + } + ); + + assert( + 'handles empty response body without throwing', + await parseApiResponse({ ok: true, status: 200, text: async () => ' ' }), + { + ok: false, + data: null, + error: { message: 'No data was returned by the server. Please try again.', status: 200 }, + } + ); + + assert( + 'handles 204 responses as empty data', + await parseApiResponse({ ok: true, status: 204, text: async () => '' }), + { + ok: false, + data: null, + error: { message: 'No data was returned by the server. Please try again.', status: 204 }, + } + ); + + assert( + 'returns a friendly parse error for malformed JSON', + await parseApiResponse({ ok: true, status: 200, text: async () => '{bad json' }), + { + ok: false, + data: null, + error: { + message: 'The server returned an invalid response. Please try again.', + status: 200, + }, + } + ); + + console.log(`\nResults: ${passed} passed, ${failed} failed\n`); + process.exit(failed > 0 ? 1 : 0); +} + +run();