Skip to content

Commit 1d851f8

Browse files
authored
Merge pull request #17 from little-core-labs/better-errors
feat: better errors
2 parents d29e801 + 6121411 commit 1d851f8

File tree

6 files changed

+68
-60
lines changed

6 files changed

+68
-60
lines changed

cjs/index.js

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,40 @@
11
'use strict';
22
const fetch = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('node-fetch'))
3-
const { ClientError } = require('./types.js')
3+
const get = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('lodash.get'))
44

55
class GraphQLClient {
66
constructor (url, options = {}) {
77
this.url = url
88
this.options = options
99
}
1010

11-
async rawStringRequest (body) {
11+
async rawStringRequest (requestBody) {
1212
// If you need to generate your gql body elsewhere, you can still utilize errors and options.
1313
const { headers, ...others } = this.options
1414

1515
const response = await fetch(this.url, {
1616
method: 'POST',
1717
headers: { 'Content-Type': 'application/json', ...headers },
18-
body,
18+
body: requestBody,
1919
...others
2020
})
2121

22-
const result = await getResult(response)
22+
const responseBody = await getBody(response)
2323

24-
if (response.ok && !result.errors && result.data) {
24+
if (response.ok && !responseBody.errors && responseBody.data) {
2525
const { headers, status } = response
26-
return { ...result, headers, status }
26+
return { ...responseBody, headers, status }
2727
} else {
28-
const errorResult = typeof result === 'string' ? { error: result } : result
28+
const errorResponseBody = typeof result === 'string' ? { error: responseBody } : responseBody
2929

30-
let bodyObj = body
30+
let requestBodyObject = requestBody
3131
try {
32-
bodyObj = JSON.parse(body)
32+
requestBodyObject = JSON.parse(requestBody)
3333
} catch (e) { /* Swallow parsing errors */ }
3434

35-
throw new ClientError(
36-
{ ...errorResult, status: response.status, headers: response.headers },
37-
bodyObj
38-
)
35+
const error = generateError({ errorResponseBody, response, requestBodyObject })
36+
37+
throw error
3938
}
4039
}
4140

@@ -102,11 +101,30 @@ function rawStringRequest (url, body, opts) {
102101
}
103102
exports.rawStringRequest = rawStringRequest
104103

105-
function getResult (response) {
104+
function getBody (response) {
106105
const contentType = response.headers.get('Content-Type')
107106
if (contentType && contentType.startsWith('application/json')) {
108107
return response.json()
109108
} else {
110109
return response.text()
111110
}
112111
}
112+
113+
function generateError ({ errorResponseBody, response, requestBodyObject }) {
114+
// The goal is to capture a real error, with a halfway decent message.
115+
// If there are additional object paths with good errors, we can add them here.
116+
// This coveres apollo.
117+
const message = get(errorResponseBody, 'errors[0].extensions.exception.data.message[0].messages[0].message') ||
118+
get(errorResponseBody, 'errors[0].extensions.exception.data.data[0].messages[0].message') ||
119+
get(errorResponseBody, 'errors[0].message') ||
120+
get(errorResponseBody, 'errors.message') ||
121+
get(response, 'statusText') ||
122+
'There was an error with the request.'
123+
const error = new Error(message)
124+
125+
error.response = { ...errorResponseBody, status: response.status, headers: response.headers }
126+
error.request = requestBodyObject
127+
128+
return error
129+
}
130+
exports.generateError = generateError

cjs/index.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ tap.test('basic error', async t => {
169169

170170
const res = await request(ctx.url, 'x').catch((x) => x)
171171

172-
t.deepEqual(res.message, 'GraphQL Error (Code: 200): {"response":{"errors":{"message":"Syntax Error GraphQL request (1:1) Unexpected Name \\"x\\"\\n\\n1: x\\n ^\\n","locations":[{"line":1,"column":1}]},"status":200,"headers":{}},"request":{"query":"x"}}')
172+
t.deepEqual(res.message, 'Syntax Error GraphQL request (1:1) Unexpected Name "x"\n\n1: x\n ^\n')
173173
})
174174

175175
tap.test('basic error with raw request', async t => {
@@ -187,7 +187,7 @@ tap.test('basic error with raw request', async t => {
187187
}
188188
})
189189
const res = await rawRequest(ctx.url, 'x').catch((x) => x)
190-
t.deepEqual(res.message, 'GraphQL Error (Code: 200): {"response":{"errors":{"message":"Syntax Error GraphQL request (1:1) Unexpected Name \\"x\\"\\n\\n1: x\\n ^\\n","locations":[{"line":1,"column":1}]},"status":200,"headers":{}},"request":{"query":"x"}}')
190+
t.deepEqual(res.message, 'Syntax Error GraphQL request (1:1) Unexpected Name "x"\n\n1: x\n ^\n')
191191
})
192192

193193
tap.test('shut down test server', t => {

esm/index.js

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,39 @@
11
import fetch from 'node-fetch'
2-
import { ClientError } from './types.js'
2+
import get from 'lodash.get'
33

44
export class GraphQLClient {
55
constructor (url, options = {}) {
66
this.url = url
77
this.options = options
88
}
99

10-
async rawStringRequest (body) {
10+
async rawStringRequest (requestBody) {
1111
// If you need to generate your gql body elsewhere, you can still utilize errors and options.
1212
const { headers, ...others } = this.options
1313

1414
const response = await fetch(this.url, {
1515
method: 'POST',
1616
headers: { 'Content-Type': 'application/json', ...headers },
17-
body,
17+
body: requestBody,
1818
...others
1919
})
2020

21-
const result = await getResult(response)
21+
const responseBody = await getBody(response)
2222

23-
if (response.ok && !result.errors && result.data) {
23+
if (response.ok && !responseBody.errors && responseBody.data) {
2424
const { headers, status } = response
25-
return { ...result, headers, status }
25+
return { ...responseBody, headers, status }
2626
} else {
27-
const errorResult = typeof result === 'string' ? { error: result } : result
27+
const errorResponseBody = typeof result === 'string' ? { error: responseBody } : responseBody
2828

29-
let bodyObj = body
29+
let requestBodyObject = requestBody
3030
try {
31-
bodyObj = JSON.parse(body)
31+
requestBodyObject = JSON.parse(requestBody)
3232
} catch (e) { /* Swallow parsing errors */ }
3333

34-
throw new ClientError(
35-
{ ...errorResult, status: response.status, headers: response.headers },
36-
bodyObj
37-
)
34+
const error = generateError({ errorResponseBody, response, requestBodyObject })
35+
36+
throw error
3837
}
3938
}
4039

@@ -96,11 +95,29 @@ export function rawStringRequest (url, body, opts) {
9695
return client.rawStringRequest(body)
9796
}
9897

99-
function getResult (response) {
98+
function getBody (response) {
10099
const contentType = response.headers.get('Content-Type')
101100
if (contentType && contentType.startsWith('application/json')) {
102101
return response.json()
103102
} else {
104103
return response.text()
105104
}
106105
}
106+
107+
export function generateError ({ errorResponseBody, response, requestBodyObject }) {
108+
// The goal is to capture a real error, with a halfway decent message.
109+
// If there are additional object paths with good errors, we can add them here.
110+
// This coveres apollo.
111+
const message = get(errorResponseBody, 'errors[0].extensions.exception.data.message[0].messages[0].message') ||
112+
get(errorResponseBody, 'errors[0].extensions.exception.data.data[0].messages[0].message') ||
113+
get(errorResponseBody, 'errors[0].message') ||
114+
get(errorResponseBody, 'errors.message') ||
115+
get(response, 'statusText') ||
116+
'There was an error with the request.'
117+
const error = new Error(message)
118+
119+
error.response = { ...errorResponseBody, status: response.status, headers: response.headers }
120+
error.request = requestBodyObject
121+
122+
return error
123+
}

esm/index.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ tap.test('basic error', async t => {
168168

169169
const res = await request(ctx.url, 'x').catch((x) => x)
170170

171-
t.deepEqual(res.message, 'GraphQL Error (Code: 200): {"response":{"errors":{"message":"Syntax Error GraphQL request (1:1) Unexpected Name \\"x\\"\\n\\n1: x\\n ^\\n","locations":[{"line":1,"column":1}]},"status":200,"headers":{}},"request":{"query":"x"}}')
171+
t.deepEqual(res.message, 'Syntax Error GraphQL request (1:1) Unexpected Name "x"\n\n1: x\n ^\n')
172172
})
173173

174174
tap.test('basic error with raw request', async t => {
@@ -186,7 +186,7 @@ tap.test('basic error with raw request', async t => {
186186
}
187187
})
188188
const res = await rawRequest(ctx.url, 'x').catch((x) => x)
189-
t.deepEqual(res.message, 'GraphQL Error (Code: 200): {"response":{"errors":{"message":"Syntax Error GraphQL request (1:1) Unexpected Name \\"x\\"\\n\\n1: x\\n ^\\n","locations":[{"line":1,"column":1}]},"status":200,"headers":{}},"request":{"query":"x"}}')
189+
t.deepEqual(res.message, 'Syntax Error GraphQL request (1:1) Unexpected Name "x"\n\n1: x\n ^\n')
190190
})
191191

192192
tap.test('shut down test server', t => {

esm/types.js

Lines changed: 0 additions & 28 deletions
This file was deleted.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
},
3939
"homepage": "https://github.com/little-core-labs/gqlr#readme",
4040
"dependencies": {
41+
"lodash.get": "^4.4.2",
4142
"node-fetch": "^2.6.0"
4243
},
4344
"devDependencies": {

0 commit comments

Comments
 (0)