Skip to content

Commit 0e0e9e1

Browse files
authored
improve TransloaditError (#210)
- add `cause` to TranslodaitError which is the response body - deprecate `transloaditErrorCode` (use `cause.error`) - deprecate `response?.body` (use `cause`) - deprecate `assemblyId` (use `cause.assembly_id`) - make sure errors thrown when status 200 but with `body.error` are also TransloaditError
1 parent 3f1acb0 commit 0e0e9e1

File tree

6 files changed

+107
-28
lines changed

6 files changed

+107
-28
lines changed

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ const transloadit = new Transloadit({
9090
}
9191
} catch (err) {
9292
console.error('❌ Unable to process Assembly.', err)
93-
if (err.assemblyId) {
94-
console.error(`💡 More info: https://transloadit.com/assemblies/${err.assemblyId}`)
93+
if (err.cause?.assembly_id) {
94+
console.error(`💡 More info: https://transloadit.com/assemblies/${err.cause?.assembly_id}`)
9595
}
9696
}
9797
})()
@@ -421,8 +421,8 @@ const url = client.getSignedSmartCDNUrl({
421421
422422
Errors from Node.js will be passed on and we use [GOT](https://github.com/sindresorhus/got) for HTTP requests and errors from there will also be passed on. When the HTTP response code is not 200, the error will be an `HTTPError`, which is a [got.HTTPError](https://github.com/sindresorhus/got#errors)) with some additional properties:
423423
424-
- `HTTPError.response?.body` the JSON object returned by the server along with the error response (**note**: `HTTPError.response` will be `undefined` for non-server errors)
425-
- `HTTPError.transloaditErrorCode` alias for `HTTPError.response.body.error` ([View all error codes](https://transloadit.com/docs/api/response-codes/#error-codes))
424+
- **(deprecated: use `cause` instead)** `HTTPError.response?.body` the JSON object returned by the server along with the error response (**note**: `HTTPError.response` will be `undefined` for non-server errors)
425+
- **(deprecated)** `HTTPError.transloaditErrorCode` alias for `HTTPError.cause?.error` ([View all error codes](https://transloadit.com/docs/api/response-codes/#error-codes))
426426
- `HTTPError.assemblyId` (alias for `HTTPError.response.body.assembly_id`, if the request regards an [Assembly](https://transloadit.com/docs/api/assemblies-assembly-id-get/))
427427
428428
To identify errors you can either check its props or use `instanceof`, e.g.:
@@ -435,15 +435,15 @@ catch (err) {
435435
if (err.code === 'ENOENT') {
436436
return console.error('Cannot open file', err)
437437
}
438-
if (err.transloaditErrorCode === 'ASSEMBLY_INVALID_STEPS') {
438+
if (err.cause?.error === 'ASSEMBLY_INVALID_STEPS') {
439439
return console.error('Invalid Assembly Steps', err)
440440
}
441441
}
442442
```
443443
444444
**Note:** Assemblies that have an error status (`assembly.error`) will only result in an error 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
445445
446-
- [More information on Transloadit errors (`transloaditErrorCode`)](https://transloadit.com/docs/api/response-codes/#error-codes)
446+
- [More information on Transloadit errors (`cause.error`)](https://transloadit.com/docs/api/response-codes/#error-codes)
447447
- [More information on request errors](https://github.com/sindresorhus/got#errors)
448448
449449
### Rate limiting & auto retry

examples/retry.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ async function run() {
2222
const { items } = await transloadit.listTemplates({ sort: 'created', order: 'asc' })
2323
return items
2424
} catch (err) {
25-
if (err instanceof TransloaditError && err.transloaditErrorCode === 'INVALID_SIGNATURE') {
25+
if (err instanceof TransloaditError && err.cause?.error === 'INVALID_SIGNATURE') {
2626
// This is an unrecoverable error, abort retry
2727
throw new pRetry.AbortError('INVALID_SIGNATURE')
2828
}

src/Transloadit.ts

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createHmac, randomUUID } from 'crypto'
2-
import got, { RequiredRetryOptions, Headers, OptionsOfJSONResponseBody } from 'got'
2+
import got, { RequiredRetryOptions, Headers, OptionsOfJSONResponseBody, HTTPError } from 'got'
33
import FormData from 'form-data'
44
import { constants, createReadStream } from 'fs'
55
import { access } from 'fs/promises'
@@ -11,7 +11,7 @@ import pMap from 'p-map'
1111
import { InconsistentResponseError } from './InconsistentResponseError'
1212
import { PaginationStream } from './PaginationStream'
1313
import { PollingTimeoutError } from './PollingTimeoutError'
14-
import { TransloaditError } from './TransloaditError'
14+
import { TransloaditResponseBody, TransloaditError } from './TransloaditError'
1515
import { version } from '../package.json'
1616
import { sendTusRequest, Stream } from './tus'
1717

@@ -47,32 +47,58 @@ interface CreateAssemblyPromise extends Promise<Assembly> {
4747
assemblyId: string
4848
}
4949

50-
function decorateHttpError(err: TransloaditError, body: any): TransloaditError {
51-
if (!body) return err
52-
50+
function getTransloaditErrorPropsFromBody(err: Error, body: TransloaditResponseBody) {
5351
let newMessage = err.message
5452
let newStack = err.stack
5553

5654
// Provide a more useful message if there is one
57-
if (body.message && body.error) newMessage += ` ${body.error}: ${body.message}`
58-
else if (body.error) newMessage += ` ${body.error}`
55+
if (body?.message && body?.error) newMessage += ` ${body.error}: ${body.message}`
56+
else if (body?.error) newMessage += ` ${body.error}`
5957

60-
if (body.assembly_ssl_url) newMessage += ` - ${body.assembly_ssl_url}`
58+
if (body?.assembly_ssl_url) newMessage += ` - ${body.assembly_ssl_url}`
6159

6260
if (typeof err.stack === 'string') {
6361
const indexOfMessageEnd = err.stack.indexOf(err.message) + err.message.length
6462
const stacktrace = err.stack.slice(indexOfMessageEnd)
6563
newStack = `${newMessage}${stacktrace}`
6664
}
6765

66+
return {
67+
message: newMessage,
68+
...(newStack != null && { stack: newStack }),
69+
...(body?.assembly_id && { assemblyId: body.assembly_id }),
70+
...(body?.error && { transloaditErrorCode: body.error }),
71+
}
72+
}
73+
74+
function decorateTransloaditError(err: HTTPError, body: TransloaditResponseBody): TransloaditError {
75+
// todo improve this
76+
const transloaditErr = err as HTTPError & TransloaditError
77+
/* eslint-disable no-param-reassign */
78+
if (body) transloaditErr.cause = body
79+
const props = getTransloaditErrorPropsFromBody(err, body)
80+
transloaditErr.message = props.message
81+
if (props.stack != null) transloaditErr.stack = props.stack
82+
if (props.assemblyId) transloaditErr.assemblyId = props.assemblyId
83+
if (props.transloaditErrorCode) transloaditErr.transloaditErrorCode = props.transloaditErrorCode
84+
/* eslint-enable no-param-reassign */
85+
86+
return transloaditErr
87+
}
88+
89+
function makeTransloaditError(err: Error, body: TransloaditResponseBody): TransloaditError {
90+
const transloaditErr = new TransloaditError(err.message, body)
91+
// todo improve this
6892
/* eslint-disable no-param-reassign */
69-
err.message = newMessage
70-
if (newStack != null) err.stack = newStack
71-
if (body.assembly_id) err.assemblyId = body.assembly_id
72-
if (body.error) err.transloaditErrorCode = body.error
93+
if (body) transloaditErr.cause = body
94+
const props = getTransloaditErrorPropsFromBody(err, body)
95+
transloaditErr.message = props.message
96+
if (props.stack != null) transloaditErr.stack = props.stack
97+
if (props.assemblyId) transloaditErr.assemblyId = props.assemblyId
98+
if (props.transloaditErrorCode) transloaditErr.transloaditErrorCode = props.transloaditErrorCode
7399
/* eslint-enable no-param-reassign */
74100

75-
return err
101+
return transloaditErr
76102
}
77103

78104
// Not sure if this is still a problem with the API, but throw a special error type so the user can retry if needed
@@ -95,7 +121,7 @@ function checkResult<T>(result: T | { error: string }): asserts result is T {
95121
'error' in result &&
96122
typeof result.error === 'string'
97123
) {
98-
throw decorateHttpError(new TransloaditError('Error in response', result), result)
124+
throw makeTransloaditError(new Error('Error in response'), result)
99125
}
100126
}
101127

@@ -738,7 +764,7 @@ export class Transloadit {
738764

739765
log('Sending request', method, url)
740766

741-
// Cannot use got.retry because we are using FormData which is a stream and can only be used once
767+
// todo use got.retry instead because we are no longer using FormData (which is a stream and can only be used once)
742768
// https://github.com/sindresorhus/got/issues/1282
743769
for (let retryCount = 0; ; retryCount++) {
744770
let form
@@ -788,7 +814,7 @@ export class Transloadit {
788814
retryCount < this._maxRetries
789815
)
790816
) {
791-
throw decorateHttpError(err, body)
817+
throw decorateTransloaditError(err, body as TransloaditResponseBody) // todo improve
792818
}
793819

794820
const { retryIn: retryInSec } = body.info

src/TransloaditError.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,34 @@
1+
export type TransloaditResponseBody =
2+
| {
3+
error?: string
4+
message?: string
5+
http_code?: string
6+
assembly_ssl_url?: string
7+
assembly_id?: string
8+
}
9+
| undefined
10+
111
export class TransloaditError extends Error {
212
override name = 'TransloaditError'
3-
response: { body: unknown }
13+
14+
/**
15+
* @deprecated use `cause` instead.
16+
*/
17+
response: { body: TransloaditResponseBody }
18+
19+
/**
20+
* @deprecated use `cause.assembly_id` instead.
21+
*/
422
assemblyId?: string
23+
24+
/**
25+
* @deprecated use `cause?.error` instead.
26+
*/
527
transloaditErrorCode?: string
628

7-
constructor(message: string, body: unknown) {
29+
override cause?: TransloaditResponseBody
30+
31+
constructor(message: string, body: TransloaditResponseBody) {
832
super(message)
933
this.response = { body }
1034
}

test/integration/live-api.test.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,10 @@ describe('API integration', { timeout: 60000 }, () => {
399399
await promise.catch((err) => {
400400
expect(err).toMatchObject({
401401
transloaditErrorCode: 'INVALID_INPUT_ERROR',
402+
cause: expect.objectContaining({
403+
error: 'INVALID_INPUT_ERROR',
404+
assembly_id: expect.any(String),
405+
}),
402406
assemblyId: expect.any(String),
403407
})
404408
})
@@ -726,7 +730,12 @@ describe('API integration', { timeout: 60000 }, () => {
726730
const { ok } = template
727731
expect(ok).toBe('TEMPLATE_DELETED')
728732
await expect(client.getTemplate(templId!)).rejects.toThrow(
729-
expect.objectContaining({ transloaditErrorCode: 'TEMPLATE_NOT_FOUND' })
733+
expect.objectContaining({
734+
transloaditErrorCode: 'TEMPLATE_NOT_FOUND',
735+
cause: expect.objectContaining({
736+
error: 'TEMPLATE_NOT_FOUND',
737+
}),
738+
})
730739
)
731740
})
732741
})
@@ -795,7 +804,12 @@ describe('API integration', { timeout: 60000 }, () => {
795804
const { ok } = credential
796805
expect(ok).toBe('TEMPLATE_CREDENTIALS_DELETED')
797806
await expect(client.getTemplateCredential(credId!)).rejects.toThrow(
798-
expect.objectContaining({ transloaditErrorCode: 'TEMPLATE_CREDENTIALS_NOT_READ' })
807+
expect.objectContaining({
808+
transloaditErrorCode: 'TEMPLATE_CREDENTIALS_NOT_READ',
809+
cause: expect.objectContaining({
810+
error: 'TEMPLATE_CREDENTIALS_NOT_READ',
811+
}),
812+
})
799813
)
800814
})
801815
})

test/unit/mock-http.test.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ describe('Mocked API tests', () => {
9797
await expect(client.createAssembly()).rejects.toThrow(
9898
expect.objectContaining({
9999
transloaditErrorCode: 'INVALID_FILE_META_DATA',
100+
cause: {
101+
error: 'INVALID_FILE_META_DATA',
102+
},
100103
message: 'Response code 400 (Bad Request) INVALID_FILE_META_DATA',
101104
})
102105
)
@@ -119,6 +122,7 @@ describe('Mocked API tests', () => {
119122
response: expect.objectContaining({
120123
body: expect.objectContaining({ assembly_id: '123' }),
121124
}),
125+
cause: expect.objectContaining({ assembly_id: '123' }),
122126
})
123127
)
124128
})
@@ -152,6 +156,9 @@ describe('Mocked API tests', () => {
152156
await expect(client.createAssembly()).rejects.toThrow(
153157
expect.objectContaining({
154158
transloaditErrorCode: 'RATE_LIMIT_REACHED',
159+
cause: expect.objectContaining({
160+
error: 'RATE_LIMIT_REACHED',
161+
}),
155162
message: 'Response code 413 (Payload Too Large) RATE_LIMIT_REACHED: Request limit reached',
156163
})
157164
)
@@ -234,6 +241,9 @@ describe('Mocked API tests', () => {
234241
await expect(client.createAssembly()).rejects.toThrow(
235242
expect.objectContaining({
236243
transloaditErrorCode: 'IMPORT_FILE_ERROR',
244+
cause: expect.objectContaining({
245+
error: 'IMPORT_FILE_ERROR',
246+
}),
237247
response: expect.objectContaining({ body: expect.objectContaining({ assembly_id: '1' }) }),
238248
})
239249
)
@@ -248,7 +258,12 @@ describe('Mocked API tests', () => {
248258
.reply(200, { error: 'IMPORT_FILE_ERROR' })
249259

250260
await expect(client.replayAssembly('1')).rejects.toThrow(
251-
expect.objectContaining({ transloaditErrorCode: 'IMPORT_FILE_ERROR' })
261+
expect.objectContaining({
262+
transloaditErrorCode: 'IMPORT_FILE_ERROR',
263+
cause: expect.objectContaining({
264+
error: 'IMPORT_FILE_ERROR',
265+
}),
266+
})
252267
)
253268
scope.done()
254269
})

0 commit comments

Comments
 (0)