diff --git a/__tests__/commands/pages/__snapshots__/upload.test.ts.snap b/__tests__/commands/pages/__snapshots__/upload.test.ts.snap index 3985ae746..dd8a9c1c5 100644 --- a/__tests__/commands/pages/__snapshots__/upload.test.ts.snap +++ b/__tests__/commands/pages/__snapshots__/upload.test.ts.snap @@ -822,6 +822,20 @@ exports[`rdme docs upload > given that the file path is a single file > should e } `; +exports[`rdme docs upload > given that the file path is a single file > should gracefully handle errors that claim they are JSON but are not 1`] = ` +{ + "error": [Error: The ReadMe API responded with an unexpected error. Please try again and if this issue persists, get in touch with us at support@readme.io.], + "stderr": "- 🔬 Validating frontmatter data... +✔ 🔬 Validating frontmatter data... no issues found! +- 🚀 Uploading files to ReadMe... +✖ 🚀 Uploading files to ReadMe... 1 file(s) failed. +", + "stdout": "🚨 Received errors when attempting to upload 1 page(s): + - __tests__/__fixtures__/docs/new-docs/new-doc.md: The ReadMe API responded with an unexpected error. Please try again and if this issue persists, get in touch with us at support@readme.io. +", +} +`; + exports[`rdme docs upload > given that the file path is a single file > should not throw an error if \`max-errors\` flag is set to -1 1`] = ` { "result": { @@ -1672,6 +1686,20 @@ exports[`rdme reference upload > given that the file path is a single file > sho } `; +exports[`rdme reference upload > given that the file path is a single file > should gracefully handle errors that claim they are JSON but are not 1`] = ` +{ + "error": [Error: The ReadMe API responded with an unexpected error. Please try again and if this issue persists, get in touch with us at support@readme.io.], + "stderr": "- 🔬 Validating frontmatter data... +✔ 🔬 Validating frontmatter data... no issues found! +- 🚀 Uploading files to ReadMe... +✖ 🚀 Uploading files to ReadMe... 1 file(s) failed. +", + "stdout": "🚨 Received errors when attempting to upload 1 page(s): + - __tests__/__fixtures__/docs/new-docs/new-doc.md: The ReadMe API responded with an unexpected error. Please try again and if this issue persists, get in touch with us at support@readme.io. +", +} +`; + exports[`rdme reference upload > given that the file path is a single file > should not throw an error if \`max-errors\` flag is set to -1 1`] = ` { "result": { diff --git a/__tests__/commands/pages/upload.test.ts b/__tests__/commands/pages/upload.test.ts index d3a93fcdd..5b0dba6bb 100644 --- a/__tests__/commands/pages/upload.test.ts +++ b/__tests__/commands/pages/upload.test.ts @@ -104,6 +104,26 @@ describe.each([ mock.done(); }); + it('should gracefully handle errors that claim they are JSON but are not', async () => { + const mock = getAPIv2Mock({ authorization }) + .get(`/branches/stable/${route}/new-doc`) + .reply(404) + .post(`/branches/stable/${route}`, { + category: { uri: `/branches/stable/categories/${route}/category-slug` }, + slug: 'new-doc', + title: 'This is the document title', + content: { body: '\nBody\n' }, + }) + .reply(201, '<>yikes', { 'content-type': 'application/json' }); + + const result = await run(['__tests__/__fixtures__/docs/new-docs/new-doc.md', '--key', key]); + + expect(result).toMatchSnapshot(); + expect(fs.writeFileSync).not.toHaveBeenCalled(); + + mock.done(); + }); + describe('given that the --dry-run flag is passed', () => { it('should not create anything in ReadMe', async () => { const mock = getAPIv2Mock({ authorization }).get(`/branches/stable/${route}/new-doc`).reply(404); diff --git a/src/lib/readmeAPIFetch.ts b/src/lib/readmeAPIFetch.ts index 405e241e6..303ebf549 100644 --- a/src/lib/readmeAPIFetch.ts +++ b/src/lib/readmeAPIFetch.ts @@ -348,15 +348,27 @@ export async function handleAPIv1Res( ) { const contentType = res.headers.get('content-type') || ''; const extension = mime.extension(contentType); + debug(`received status code ${res.status} from ${res.url} with content type: ${contentType}`); if (extension === 'json') { - // TODO: type this better - // biome-ignore lint/suspicious/noExplicitAny: We do not have TS types for APIv1 responses. - const body = (await res.json()) as any; - debug(`received status code ${res.status} from ${res.url} with JSON response: ${JSON.stringify(body)}`); - if (body.error && rejectOnJsonError) { - return Promise.reject(new APIv1Error(body)); + // Just to be safe, we parse the response as text first in case the response is not valid JSON. + const text = await res.text(); + debug(`received status code ${res.status} from ${res.url} with JSON response: ${text}`); + try { + // biome-ignore lint/suspicious/noExplicitAny: We do not have TS types for APIv1 responses. + const body = JSON.parse(text) as any; + if (body.error && rejectOnJsonError) { + throw new APIv1Error(body); + } + return body; + } catch (e) { + if (e instanceof APIv1Error) { + throw e; + } + debug(`received the following error when attempting to parse JSON response: ${e.message}`); + throw new Error( + 'The ReadMe API responded with an unexpected error. Please try again and if this issue persists, get in touch with us at support@readme.io.', + ); } - return body; } if (res.status === SUCCESS_NO_CONTENT) { debug(`received status code ${res.status} from ${res.url} with no content`); @@ -386,6 +398,7 @@ export async function handleAPIv2Res { const contentType = res.headers.get('content-type') || ''; const extension = mime.extension(contentType) || contentType.includes('json') ? 'json' : false; + this.debug(`received status code ${res.status} from ${res.url} with content type: ${contentType}`); if (res.status === SUCCESS_NO_CONTENT) { // to prevent a memory leak, we should still consume the response body? even though we don't use it? // https://x.com/cramforce/status/1762142087930433999 @@ -393,12 +406,24 @@ export async function handleAPIv2Res