Skip to content

Commit 60b7f8f

Browse files
kanadguptaerunion
andauthored
feat: better error-handling for non-JSON responses (#538)
* chore(deps): bring mime-types into the mix We already have this package installed as part of `form-data`, but now we're going to use it to parse content-type headers. * feat: add non-JSON handling to `handleRes` This is mostly functionally equivalent, but now we check the Content-Type of the response before parsing its body as JSON. That way we can inspect the body properly. * refactor(openapi): use handleRes for error handling * test: update tests to reflect new error conditions * Update __tests__/cmds/openapi.test.js Co-authored-by: Jon Ursenbach <erunion@users.noreply.github.com> Co-authored-by: Jon Ursenbach <erunion@users.noreply.github.com>
1 parent dbd6930 commit 60b7f8f

File tree

5 files changed

+64
-39
lines changed

5 files changed

+64
-39
lines changed

__tests__/cmds/openapi.test.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,11 @@ describe('rdme openapi', () => {
535535

536536
await expect(
537537
openapi.run({ spec: require.resolve('@readme/oas-examples/2.0/json/petstore.json'), key, version })
538-
).rejects.toStrictEqual(new Error('There was an error uploading!'));
538+
).rejects.toStrictEqual(
539+
new Error(
540+
'Yikes, something went wrong! Please try uploading your spec again and if the problem persists, get in touch with our support team at support@readme.io.'
541+
)
542+
);
539543

540544
return mock.done();
541545
});
@@ -555,7 +559,7 @@ describe('rdme openapi', () => {
555559
.post('/api/v1/api-specification', { registryUUID })
556560
.delayConnection(1000)
557561
.basicAuth({ user: key })
558-
.reply(400, '<html></html>');
562+
.reply(500, '<title>Application Error</title>');
559563

560564
await expect(
561565
openapi.run({ spec: require.resolve('@readme/oas-examples/2.0/json/petstore.json'), key, version })

package-lock.json

Lines changed: 15 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"form-data": "^4.0.0",
4848
"gray-matter": "^4.0.1",
4949
"isemail": "^3.1.3",
50+
"mime-types": "^2.1.35",
5051
"node-fetch": "^2.6.1",
5152
"oas-normalize": "^6.0.0",
5253
"open": "^8.2.1",

src/cmds/openapi.js

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const config = require('config');
55
const fs = require('fs');
66
const { debug, oraOptions } = require('../lib/logger');
77
const fetch = require('../lib/fetch');
8+
const { handleRes } = require('../lib/fetch');
89
const { getProjectVersion } = require('../lib/versionSelect');
910
const ora = require('ora');
1011
const parse = require('parse-link-header');
@@ -108,24 +109,29 @@ module.exports = class OpenAPICommand {
108109
);
109110
}
110111

111-
async function error(err) {
112-
debug(`error response received with status code ${err.status}`);
113-
try {
114-
const parsedError = await err.json();
115-
debug(`full error response: ${JSON.stringify(parsedError)}`);
116-
return Promise.reject(new APIError(parsedError));
117-
} catch (e) {
118-
debug(`error parsing JSON with message: ${e.message}`);
119-
if (e.message.includes('Unexpected token < in JSON')) {
120-
return Promise.reject(
121-
new Error(
122-
"We're sorry, your upload request timed out. Please try again or split your file up into smaller chunks."
123-
)
112+
async function error(res) {
113+
return handleRes(res).catch(err => {
114+
// If we receive an APIError, no changes needed! Throw it as is.
115+
if (err instanceof APIError) {
116+
throw err;
117+
}
118+
// If we receive certain text responses, it's likely a 5xx error from our server.
119+
if (
120+
typeof err === 'string' &&
121+
(err.includes('<title>Application Error</title>') || // Heroku error
122+
err.includes('520: Web server is returning an unknown error</title>')) // Cloudflare error
123+
) {
124+
throw new Error(
125+
"We're sorry, your upload request timed out. Please try again or split your file up into smaller chunks."
124126
);
125127
}
126-
127-
return Promise.reject(new Error('There was an error uploading!'));
128-
}
128+
// As a fallback, we throw a more generic error.
129+
throw new Error(
130+
`Yikes, something went wrong! Please try uploading your spec again and if the problem persists, get in touch with our support team at ${chalk.underline(
131+
'support@readme.io'
132+
)}.`
133+
);
134+
});
129135
}
130136

131137
const registryUUID = await streamSpecToRegistry(bundledSpec);

src/lib/fetch.js

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
const { debug } = require('./logger');
33
const fetch = require('node-fetch');
44
const isGHA = require('./isGitHub');
5+
const mime = require('mime-types');
56
const pkg = require('../../package.json');
67
const APIError = require('./apiError');
78

@@ -41,18 +42,30 @@ module.exports.getUserAgent = function getUserAgent() {
4142
};
4243

4344
/**
44-
* Small handler for transforming responses from our API into JSON and if there's errors, throwing
45-
* an APIError exception.
45+
* Small handler for handling responses from our API.
46+
*
47+
* If we receive JSON errors, we throw an APIError exception.
48+
*
49+
* If we receive non-JSON responses, we consider them errors and throw them.
4650
*
4751
* @param {Response} res
4852
*/
4953
module.exports.handleRes = async function handleRes(res) {
50-
const body = await res.json();
51-
debug(`received status code ${res.status} with response body: ${JSON.stringify(body)}`);
52-
if (body.error) {
53-
return Promise.reject(new APIError(body));
54+
const contentType = res.headers.get('content-type');
55+
const extension = mime.extension(contentType);
56+
if (extension === 'json') {
57+
const body = await res.json();
58+
debug(`received status code ${res.status} with JSON response: ${JSON.stringify(body)}`);
59+
if (body.error) {
60+
return Promise.reject(new APIError(body));
61+
}
62+
return body;
5463
}
55-
return body;
64+
// If we receive a non-JSON response, it's likely an error.
65+
// Let's debug the raw response body and throw it.
66+
const body = await res.text();
67+
debug(`received status code ${res.status} with non-JSON response: ${body}`);
68+
return Promise.reject(body);
5669
};
5770

5871
/**

0 commit comments

Comments
 (0)