Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 45 additions & 16 deletions spec/src/utils/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(), {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The test pattern await fn().catch((e) => e) silently swallows any unexpected resolution — if throwHttpErrorFromResponse were accidentally changed to resolve instead of reject, the test would still pass (all the expect assertions would simply run against undefined). A more defensive pattern would be:

let error;
try {
  await throwHttpErrorFromResponse(new Error(), { ... });
  throw new Error('Expected throwHttpErrorFromResponse to throw');
} catch (e) {
  error = e;
}

Alternatively, with chai-as-promised: await expect(throwHttpErrorFromResponse(...)).to.be.rejectedWith(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);
});
});

Expand Down
25 changes: 22 additions & 3 deletions src/utils/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down
Loading