diff --git a/README.md b/README.md index fa4def4..a898857 100644 --- a/README.md +++ b/README.md @@ -87,8 +87,8 @@ try { } } catch (err) { console.error('❌ Unable to process Assembly.', err) - if (err instanceof ApiError && err.response.assembly_id) { - console.error(`💡 More info: https://transloadit.com/assemblies/${err.response.assembly_id}`) + if (err instanceof ApiError && err.assemblyId) { + console.error(`💡 More info: https://transloadit.com/assemblies/${err.assemblyId}`) } } ``` @@ -417,11 +417,10 @@ const url = client.getSignedSmartCDNUrl({ Any errors originating from Node.js will be passed on and we use [GOT](https://github.com/sindresorhus/got) v11 for HTTP requests. [Errors from `got`](https://github.com/sindresorhus/got/tree/v11.8.6?tab=readme-ov-file#errors) will also be passed on, _except_ the `got.HTTPError` which will be replaced with a `transloadit.ApiError`, which will have its `cause` property set to the instance of the original `got.HTTPError`. `transloadit.ApiError` has these properties: -- `HTTPError.response` the JSON object returned by the server. It has these properties - - `error` (`string`) - [The Transloadit API error code](https://transloadit.com/docs/api/response-codes/#error-codes). - - `message` (`string`) - A textual representation of the Transloadit API error. - - `assembly_id`: (`string`) - If the request is related to an assembly, this will be the ID of the assembly. - - `assembly_ssl_url` (`string`) - If the request is related to an assembly, this will be the SSL URL to the assembly . +- `code` (`string`) - [The Transloadit API error code](https://transloadit.com/docs/api/response-codes/#error-codes). +- `rawMessage` (`string`) - A textual representation of the Transloadit API error. +- `assemblyId`: (`string`) - If the request is related to an assembly, this will be the ID of the assembly. +- `assemblySslUrl` (`string`) - If the request is related to an assembly, this will be the SSL URL to the assembly . To identify errors you can either check its props or use `instanceof`, e.g.: @@ -435,15 +434,15 @@ try { if (err.code === 'ENOENT') { return console.error('Cannot open file', err) } - if (err instanceof transloadit.ApiError && err.response.error === 'ASSEMBLY_INVALID_STEPS') { + if (err instanceof ApiError && err.code === 'ASSEMBLY_INVALID_STEPS') { return console.error('Invalid Assembly Steps', err) } } ``` -**Note:** Assemblies that have an error status (`assembly.error`) will only result in an error being thrown from `createAssembly` and `replayAssembly`. For other Assembly methods, no errors will be thrown, but any error can be found in the response's `error` property +**Note:** Assemblies that have an error status (`assembly.error`) will only result in an error being thrown from `createAssembly` and `replayAssembly`. For other Assembly methods, no errors will be thrown, but any error can be found in the response's `error` property (also `ApiError.code`). -- [More information on Transloadit errors (`ApiError.response.error`)](https://transloadit.com/docs/api/response-codes/#error-codes) +- [More information on Transloadit errors (`ApiError.code`)](https://transloadit.com/docs/api/response-codes/#error-codes) - [More information on request errors](https://github.com/sindresorhus/got#errors) ### Rate limiting & auto retry diff --git a/examples/retry.js b/examples/retry.js index e4295ac..963ec88 100644 --- a/examples/retry.js +++ b/examples/retry.js @@ -22,7 +22,7 @@ async function run() { const { items } = await transloadit.listTemplates({ sort: 'created', order: 'asc' }) return items } catch (err) { - if (err instanceof ApiError && err.response.error === 'INVALID_SIGNATURE') { + if (err instanceof ApiError && err.code === 'INVALID_SIGNATURE') { // This is an unrecoverable error, abort retry throw new pRetry.AbortError('INVALID_SIGNATURE') } diff --git a/src/ApiError.ts b/src/ApiError.ts index a2c3f08..49dde3e 100644 --- a/src/ApiError.ts +++ b/src/ApiError.ts @@ -3,7 +3,6 @@ import { HTTPError } from 'got' export interface TransloaditErrorResponseBody { error?: string message?: string - http_code?: string assembly_ssl_url?: string assembly_id?: string } @@ -11,40 +10,31 @@ export interface TransloaditErrorResponseBody { export class ApiError extends Error { override name = 'ApiError' - response: TransloaditErrorResponseBody + // there might not be an error code (or message) if the server didn't respond with any JSON response at all + // e.g. if there was a 500 in the HTTP reverse proxy + code?: string + rawMessage?: string + assemblySslUrl?: string + assemblyId?: string override cause?: HTTPError | undefined - constructor(params: { - cause?: HTTPError - appendStack?: string - body: TransloaditErrorResponseBody | undefined - }) { - const { cause, body, appendStack } = params + constructor(params: { cause?: HTTPError; body: TransloaditErrorResponseBody | undefined }) { + const { cause, body = {} } = params const parts = ['API error'] if (cause?.response.statusCode) parts.push(`(HTTP ${cause.response.statusCode})`) - if (body?.error) parts.push(`${body.error}:`) - if (body?.message) parts.push(body.message) - if (body?.assembly_ssl_url) parts.push(body.assembly_ssl_url) + if (body.error) parts.push(`${body.error}:`) + if (body.message) parts.push(body.message) + if (body.assembly_ssl_url) parts.push(body.assembly_ssl_url) const message = parts.join(' ') super(message) - - // if we have a cause, use the stack trace from it instead - if (cause != null && typeof cause.stack === 'string') { - const indexOfMessageEnd = cause.stack.indexOf(cause.message) + cause.message.length - const gotStacktrace = cause.stack.slice(indexOfMessageEnd) - this.stack = `${message}${gotStacktrace}` - } - - // If we have an original stack, append it to the bottom, because `got`s stack traces are not very good - if (this.stack != null && appendStack != null) { - this.stack += `\n${appendStack.replace(/^([^\n]+\n)/, '')}` - } - - this.response = body ?? {} + this.rawMessage = body.message + this.assemblyId = body.assembly_id + this.assemblySslUrl = body.assembly_ssl_url + this.code = body.error this.cause = cause } } diff --git a/src/Transloadit.ts b/src/Transloadit.ts index d206f66..da314a2 100644 --- a/src/Transloadit.ts +++ b/src/Transloadit.ts @@ -67,7 +67,7 @@ function checkResult(result: T | { error: string }): asserts result is T { 'error' in result && typeof result.error === 'string' ) { - throw new ApiError({ body: result }) // in this case there is no `cause` because we don't have a HTTPError + throw new ApiError({ body: result }) // in this case there is no `cause` because we don't have an HTTPError } } @@ -732,10 +732,6 @@ export class Transloadit { responseType: 'json', } - // `got` stacktraces are very lacking, so we capture our own - // https://github.com/sindresorhus/got/blob/main/documentation/async-stack-traces.md - const stack = new Error().stack - try { const request = got[method](url, requestOpts) const { body } = await request @@ -766,7 +762,6 @@ export class Transloadit { ) { throw new ApiError({ cause: err, - appendStack: stack, body: body as TransloaditErrorResponseBody, }) // todo don't assert type } diff --git a/test/integration/live-api.test.ts b/test/integration/live-api.test.ts index dd3a5d4..40ff690 100644 --- a/test/integration/live-api.test.ts +++ b/test/integration/live-api.test.ts @@ -398,10 +398,8 @@ describe('API integration', { timeout: 60000 }, () => { const promise = createAssembly(client, opts) await promise.catch((err) => { expect(err).toMatchObject({ - response: expect.objectContaining({ - error: 'INVALID_INPUT_ERROR', - assembly_id: expect.any(String), - }), + code: 'INVALID_INPUT_ERROR', + assemblyId: expect.any(String), }) }) await expect(promise).rejects.toThrow(Error) @@ -729,9 +727,7 @@ describe('API integration', { timeout: 60000 }, () => { expect(ok).toBe('TEMPLATE_DELETED') await expect(client.getTemplate(templId!)).rejects.toThrow( expect.objectContaining({ - response: expect.objectContaining({ - error: 'TEMPLATE_NOT_FOUND', - }), + code: 'TEMPLATE_NOT_FOUND', }) ) }) @@ -802,9 +798,7 @@ describe('API integration', { timeout: 60000 }, () => { expect(ok).toBe('TEMPLATE_CREDENTIALS_DELETED') await expect(client.getTemplateCredential(credId!)).rejects.toThrow( expect.objectContaining({ - response: expect.objectContaining({ - error: 'TEMPLATE_CREDENTIALS_NOT_READ', - }), + code: 'TEMPLATE_CREDENTIALS_NOT_READ', }) ) }) diff --git a/test/unit/mock-http.test.ts b/test/unit/mock-http.test.ts index 9e05203..be640cc 100644 --- a/test/unit/mock-http.test.ts +++ b/test/unit/mock-http.test.ts @@ -98,10 +98,8 @@ describe('Mocked API tests', () => { await expect(client.createAssembly()).rejects.toThrow( expect.objectContaining({ - response: { - error: 'INVALID_FILE_META_DATA', - message: 'Invalid file metadata', - }, + code: 'INVALID_FILE_META_DATA', + rawMessage: 'Invalid file metadata', message: 'API error (HTTP 400) INVALID_FILE_META_DATA: Invalid file metadata', }) ) @@ -122,64 +120,55 @@ describe('Mocked API tests', () => { expect.objectContaining({ message: 'API error (HTTP 400) INVALID_FILE_META_DATA: Invalid file metadata https://api2-oltu.transloadit.com/assemblies/foo', - response: expect.objectContaining({ assembly_id: '123' }), + assemblyId: '123', }) ) - try { - await promise - } catch (err) { - expect(inspect(err).split('\n')).toEqual([ - expect.stringMatching( - `API error \\(HTTP 400\\) INVALID_FILE_META_DATA: Invalid file metadata https://api2-oltu.transloadit.com/assemblies/foo` - ), - expect.stringMatching(` at .+`), - expect.stringMatching(` at .+`), - expect.stringMatching( - ` at Transloadit\\._remoteJson \\(.+\\/src\\/Transloadit\\.ts:\\d+:\\d+\\)` - ), - expect.stringMatching( - ` at createAssemblyAndUpload \\(.+\\/src\\/Transloadit\\.ts:\\d+:\\d+\\)` - ), - expect.stringMatching(` at .+\\/src\\/Transloadit\\.ts:\\d+:\\d+`), - expect.stringMatching(` at .+`), - expect.stringMatching(` at .+\\/test\\/unit\\/mock-http\\.test\\.ts:\\d+:\\d+`), - expect.stringMatching(` at .+`), - expect.stringMatching(` at .+`), - expect.stringMatching(` at .+`), - expect.stringMatching(` at .+`), - expect.stringMatching(` at .+`), - expect.stringMatching(` name: 'ApiError',`), - expect.stringMatching(` response: \\{`), - expect.stringMatching(` error: 'INVALID_FILE_META_DATA',`), - expect.stringMatching(` message: 'Invalid file metadata',`), - expect.stringMatching(` assembly_id: '123',`), - expect.stringMatching( - ` assembly_ssl_url: 'https:\\/\\/api2-oltu\\.transloadit\\.com\\/assemblies\\/foo'` - ), - expect.stringMatching(` \\},`), - expect.stringMatching(` cause: HTTPError: Response code 400 \\(Bad Request\\)`), - expect.stringMatching(` at .+`), - expect.stringMatching(` at .+`), - expect.stringMatching(` code: 'ERR_NON_2XX_3XX_RESPONSE',`), - // don't care about the rest: - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - expect.stringMatching(' }'), - expect.stringMatching(' }'), - expect.stringMatching('}'), - ]) - } + const errorString = await promise.catch(inspect) + expect(typeof errorString === 'string').toBeTruthy() + expect(inspect(errorString).split('\n')).toEqual([ + expect.stringMatching( + `API error \\(HTTP 400\\) INVALID_FILE_META_DATA: Invalid file metadata https://api2-oltu.transloadit.com/assemblies/foo` + ), + expect.stringMatching(` at .+`), + expect.stringMatching(` at .+`), + expect.stringMatching( + ` at createAssemblyAndUpload \\(.+\\/src\\/Transloadit\\.ts:\\d+:\\d+\\)` + ), + expect.stringMatching(` at .+\\/test\\/unit\\/mock-http\\.test\\.ts:\\d+:\\d+`), + expect.stringMatching(` at .+`), + expect.stringMatching(` at .+`), + expect.stringMatching(` at .+`), + expect.stringMatching(` at .+`), + expect.stringMatching(` at .+`), + expect.stringMatching(` at .+`), + expect.stringMatching(` rawMessage: 'Invalid file metadata',`), + expect.stringMatching(` assemblyId: '123',`), + expect.stringMatching( + ` assemblySslUrl: 'https:\\/\\/api2-oltu\\.transloadit\\.com\\/assemblies\\/foo'` + ), + expect.stringMatching(` code: 'INVALID_FILE_META_DATA',`), + expect.stringMatching(` cause: HTTPError: Response code 400 \\(Bad Request\\)`), + expect.stringMatching(` at .+`), + expect.stringMatching(` at .+`), + expect.stringMatching(` code: 'ERR_NON_2XX_3XX_RESPONSE',`), + // don't care about the rest: + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + expect.stringMatching(' }'), + expect.stringMatching(' }'), + expect.stringMatching('}'), + ]) }) it('should retry correctly on RATE_LIMIT_REACHED', async () => { @@ -211,9 +200,7 @@ describe('Mocked API tests', () => { await expect(client.createAssembly()).rejects.toThrow( expect.objectContaining({ message: 'API error (HTTP 413) RATE_LIMIT_REACHED: Request limit reached', - response: expect.objectContaining({ - error: 'RATE_LIMIT_REACHED', - }), + code: 'RATE_LIMIT_REACHED', }) ) scope.done() @@ -292,10 +279,8 @@ describe('Mocked API tests', () => { await expect(client.createAssembly()).rejects.toThrow( expect.objectContaining({ - response: expect.objectContaining({ - error: 'IMPORT_FILE_ERROR', - assembly_id: '1', - }), + code: 'IMPORT_FILE_ERROR', + assemblyId: '1', }) ) scope.done() @@ -310,9 +295,7 @@ describe('Mocked API tests', () => { await expect(client.replayAssembly('1')).rejects.toThrow( expect.objectContaining({ - response: expect.objectContaining({ - error: 'IMPORT_FILE_ERROR', - }), + code: 'IMPORT_FILE_ERROR', }) ) scope.done()