Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: export error handling function #206

Merged
merged 1 commit into from
Sep 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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": "^4.17.21",
makinwab marked this conversation as resolved.
Show resolved Hide resolved
"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": "^4.14.172",
"@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'
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']) {
makinwab marked this conversation as resolved.
Show resolved Hide resolved
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'
makinwab marked this conversation as resolved.
Show resolved Hide resolved

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 }