Skip to content

Commit

Permalink
feat: export error handling function
Browse files Browse the repository at this point in the history
  • Loading branch information
makinwab committed Sep 13, 2021
1 parent d78b46f commit 613f4a3
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 1 deletion.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
},
"dependencies": {
"fast-copy": "^2.1.0",
"lodash-es": "^4.17.21",
"qs": "^6.9.4"
},
"devDependencies": {
Expand All @@ -59,12 +60,15 @@
"@semantic-release/github": "^7.2.3",
"@semantic-release/npm": "^7.1.3",
"@semantic-release/release-notes-generator": "^9.0.3",
"@types/chai": "^4.2.21",
"@types/lodash-es": "^4.17.5",
"@types/qs": "^6.9.5",
"@typescript-eslint/eslint-plugin": "4.31.0",
"@typescript-eslint/parser": "4.31.0",
"axios": "^0.21.0",
"axios-mock-adapter": "^1.15.0",
"babel-eslint": "^10.1.0",
"chai": "^4.3.4",
"core-js": "^3.8.0",
"cz-conventional-changelog": "^3.1.0",
"eslint": "^7.2.0",
Expand Down
77 changes: 77 additions & 0 deletions src/error-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { isPlainObject } from 'lodash-es'
import { AxiosError } from 'axios'

/**
* Handles errors received from the server. Parses the error into a more useful
* format, places it in an exception and throws it.
* See https://www.contentful.com/developers/docs/references/errors/
* for more details on the data received on the errorResponse.data property
* and the expected error codes.
* @private
*/
export default function errorHandler(errorResponse: AxiosError): never {
const { config, response } = errorResponse
let errorName

// Obscure the Management token
if (config && config.headers && config.headers['Authorization']) {
const token = `...${config.headers['Authorization'].substr(-5)}`
config.headers['Authorization'] = `Bearer ${token}`
}

if (!isPlainObject(response) || !isPlainObject(config)) {
throw errorResponse
}

const data = response?.data

const errorData: {
status?: number
statusText?: string
requestId?: string
message: string
details: Record<string, unknown>
request?: Record<string, unknown>
} = {
status: response?.status,
statusText: response?.statusText,
message: '',
details: {},
}

if (isPlainObject(config)) {
errorData.request = {
url: config.url,
headers: config.headers,
method: config.method,
payloadData: config.data,
}
}
if (data && isPlainObject(data)) {
if ('requestId' in data) {
errorData.requestId = data.requestId || 'UNKNOWN'
}
if ('message' in data) {
errorData.message = data.message || ''
}
if ('details' in data) {
errorData.details = data.details || {}
}
if ('sys' in data) {
if ('id' in data.sys) {
errorName = data.sys.id
}
}
}

const error = new Error()
error.name =
errorName && errorName !== 'Unknown' ? errorName : `${response?.status} ${response?.statusText}`

try {
error.message = JSON.stringify(errorData, null, ' ')
} catch {
error.message = errorData?.message ?? ''
}
throw error
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ export { default as enforceObjPath } from './enforce-obj-path'
export { default as freezeSys } from './freeze-sys'
export { default as getUserAgentHeader } from './get-user-agent'
export { default as toPlainObject } from './to-plain-object'
export { default as errorHandler } from './error-handler'

export type { AxiosInstance, CreateHttpClientParams } from './types'
115 changes: 115 additions & 0 deletions test/unit/error-handler-test.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import errorHandler from '../../src/error-handler'
import { errorMock } from './mocks'
import { expect } from 'chai'
import cloneDeep from 'lodash/cloneDeep'

const error: any = cloneDeep(errorMock)

describe('A errorHandler', () => {
// Best case scenario where an error is a known and expected situation and the
// server returns an error with a JSON payload with all the information possible
it('Throws well formed error with details from server', async () => {
error.response.data = {
sys: {
id: 'SpecificError',
type: 'Error',
},
message: 'datamessage',
requestId: 'requestid',
details: 'errordetails',
}

try {
errorHandler(error)
} catch (err) {
const parsedMessage = JSON.parse(err.message)
expect(err.name).equals('SpecificError', 'error name')
expect(parsedMessage.request.url).equals('requesturl', 'request url')
expect(parsedMessage.message).equals('datamessage', 'error payload message')
expect(parsedMessage.requestId).equals('requestid', 'request id')
expect(parsedMessage.details).equals('errordetails', 'error payload details')
}
})

// Second best case scenario, where we'll still get a JSON payload from the server
// but only with an Unknown error type and no additional details
it('Throws unknown error received from server', async () => {
error.response.data = {
sys: {
id: 'Unknown',
type: 'Error',
},
requestId: 'requestid',
}
error.response.status = 500
error.response.statusText = 'Internal'

try {
errorHandler(error)
} catch (err) {
const parsedMessage = JSON.parse(err.message)
expect(err.name).equals('500 Internal', 'error name defaults to status code and text')
expect(parsedMessage.request.url).equals('requesturl', 'request url')
expect(parsedMessage.requestId).equals('requestid', 'request id')
}
})

// Wurst case scenario, where we have no JSON payload and only HTTP status information
it('Throws error without additional detail', async () => {
error.response.status = 500
error.response.statusText = 'Everything is on fire'

try {
errorHandler(error)
} catch (err) {
const parsedMessage = JSON.parse(err.message)
expect(err.name).equals(
'500 Everything is on fire',
'error name defaults to status code and text'
)
expect(parsedMessage.request.url).equals('requesturl', 'request url')
}
})

it('Obscures management token in any error message', async () => {
const responseError: any = cloneDeep(errorMock)
responseError.config.headers = {
Authorization: 'Bearer secret-management-token',
}

try {
errorHandler(responseError)
} catch (err) {
const parsedMessage = JSON.parse(err.message)
expect(parsedMessage.request.headers.Authorization).equals(
'Bearer ...token',
'Obscures management token'
)
}

const requestError: any = {
config: {
url: 'requesturl',
headers: {},
},
data: {},
request: {
status: 404,
statusText: 'Not Found',
},
}

requestError.config.headers = {
Authorization: 'Bearer secret-management-token',
}

try {
errorHandler(requestError)
} catch (err) {
expect(err.config.headers.Authorization).equals(
'Bearer ...token',
'Obscures management token'
)
}
})
})
14 changes: 13 additions & 1 deletion test/unit/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,16 @@ const assetMock = {
},
}

export { linkMock, sysMock, contentTypeMock, entryMock, assetMock }
const errorMock = {
config: {
url: 'requesturl',
headers: {},
},
response: {
status: 404,
statusText: 'Not Found',
data: {},
},
}

export { linkMock, sysMock, contentTypeMock, entryMock, assetMock, errorMock }

0 comments on commit 613f4a3

Please sign in to comment.