diff --git a/spec/src/utils/helpers.js b/spec/src/utils/helpers.js index 2d7d1a7c..e8db295e 100644 --- a/spec/src/utils/helpers.js +++ b/spec/src/utils/helpers.js @@ -145,22 +145,51 @@ describe('ConstructorIO - Utils - Helpers', () => { }, }; - try { - await throwHttpErrorFromResponse(new Error(), { - json: () => new Promise((resolve) => { - resolve({ - message: errorMessage, - }); - }), - ...responseData, - }); - } catch (e) { - expect(e.message).to.equal(errorMessage); - expect(e.status).to.equal(responseData.status); - expect(e.statusText).to.equal(responseData.statusText); - expect(e.url).to.equal(responseData.url); - expect(e.headers).to.deep.equal(responseData.headers); - } + const error = await throwHttpErrorFromResponse(new Error(), { + text: () => Promise.resolve(JSON.stringify({ message: errorMessage })), + ...responseData, + }).catch((e) => e); + + expect(error.message).to.equal(errorMessage); + expect(error.status).to.equal(responseData.status); + expect(error.statusText).to.equal(responseData.statusText); + expect(error.url).to.equal(responseData.url); + expect(error.headers).to.deep.equal(responseData.headers); + }); + + it('Should throw an error with the raw body when the response is not JSON', async () => { + const responseData = { + status: 429, + statusText: 'Too Many Requests', + url: 'https://constructor.io', + headers: { + 'retry-after': '30', + }, + }; + + const error = await throwHttpErrorFromResponse(new Error(), { + text: () => Promise.resolve('Too many requests'), + ...responseData, + }).catch((e) => e); + + expect(error.message).to.equal('Too many requests'); + expect(error.status).to.equal(responseData.status); + expect(error.statusText).to.equal(responseData.statusText); + expect(error.url).to.equal(responseData.url); + expect(error.headers).to.deep.equal(responseData.headers); + }); + + it('Should throw an error with a status fallback when the response body is empty', async () => { + const error = await throwHttpErrorFromResponse(new Error(), { + text: () => Promise.resolve(''), + status: 502, + statusText: 'Bad Gateway', + url: 'https://constructor.io', + headers: {}, + }).catch((e) => e); + + expect(error.message).to.equal('HTTP 502'); + expect(error.status).to.equal(502); }); }); diff --git a/src/utils/helpers.js b/src/utils/helpers.js index 4428e06e..80f68976 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers.js @@ -48,15 +48,34 @@ const utils = { return snakeCasedObj; }, - throwHttpErrorFromResponse: (error, response) => response.json().then((json) => { - error.message = json.message; + // Attach the details of a non-2XX response to an error and throw it + // - Error bodies are not always JSON: rate limit and gateway responses are + // commonly plain text or HTML, so attempting to parse and falling back to + // the raw body keeps the real status and message instead of surfacing a + // SyntaxError from the parse itself + throwHttpErrorFromResponse: async (error, response) => { + let message = ''; + + try { + message = await response.text(); + + const parsed = JSON.parse(message); + + if (parsed && typeof parsed.message === 'string') { + message = parsed.message; + } + } catch (e) { + // Body is either unreadable or not JSON - keep whatever text we have + } + + error.message = message.trim() || `HTTP ${response.status}`; error.status = response.status; error.statusText = response.statusText; error.url = response.url; error.headers = response.headers; throw error; - }), + }, isNil: (value) => value == null,