Skip to content

Commit

Permalink
feat: better errors
Browse files Browse the repository at this point in the history
BREAKING CHANGE: No longer throwing a custom ClientError error type
Errors attempt to include a better message as the error.message
The thrown errors still have the strange amalgamation of request and response properties.
  • Loading branch information
bcomnes committed Nov 13, 2020
1 parent d29e801 commit 6121411
Show file tree
Hide file tree
Showing 6 changed files with 68 additions and 60 deletions.
46 changes: 32 additions & 14 deletions cjs/index.js
Original file line number Diff line number Diff line change
@@ -1,41 +1,40 @@
'use strict';
const fetch = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('node-fetch'))
const { ClientError } = require('./types.js')
const get = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('lodash.get'))

class GraphQLClient {
constructor (url, options = {}) {
this.url = url
this.options = options
}

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

const response = await fetch(this.url, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...headers },
body,
body: requestBody,
...others
})

const result = await getResult(response)
const responseBody = await getBody(response)

if (response.ok && !result.errors && result.data) {
if (response.ok && !responseBody.errors && responseBody.data) {
const { headers, status } = response
return { ...result, headers, status }
return { ...responseBody, headers, status }
} else {
const errorResult = typeof result === 'string' ? { error: result } : result
const errorResponseBody = typeof result === 'string' ? { error: responseBody } : responseBody

let bodyObj = body
let requestBodyObject = requestBody
try {
bodyObj = JSON.parse(body)
requestBodyObject = JSON.parse(requestBody)
} catch (e) { /* Swallow parsing errors */ }

throw new ClientError(
{ ...errorResult, status: response.status, headers: response.headers },
bodyObj
)
const error = generateError({ errorResponseBody, response, requestBodyObject })

throw error
}
}

Expand Down Expand Up @@ -102,11 +101,30 @@ function rawStringRequest (url, body, opts) {
}
exports.rawStringRequest = rawStringRequest

function getResult (response) {
function getBody (response) {
const contentType = response.headers.get('Content-Type')
if (contentType && contentType.startsWith('application/json')) {
return response.json()
} else {
return response.text()
}
}

function generateError ({ errorResponseBody, response, requestBodyObject }) {
// The goal is to capture a real error, with a halfway decent message.
// If there are additional object paths with good errors, we can add them here.
// This coveres apollo.
const message = get(errorResponseBody, 'errors[0].extensions.exception.data.message[0].messages[0].message') ||
get(errorResponseBody, 'errors[0].extensions.exception.data.data[0].messages[0].message') ||
get(errorResponseBody, 'errors[0].message') ||
get(errorResponseBody, 'errors.message') ||
get(response, 'statusText') ||
'There was an error with the request.'
const error = new Error(message)

error.response = { ...errorResponseBody, status: response.status, headers: response.headers }
error.request = requestBodyObject

return error
}
exports.generateError = generateError
4 changes: 2 additions & 2 deletions cjs/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ tap.test('basic error', async t => {

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

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"}}')
t.deepEqual(res.message, 'Syntax Error GraphQL request (1:1) Unexpected Name "x"\n\n1: x\n ^\n')
})

tap.test('basic error with raw request', async t => {
Expand All @@ -187,7 +187,7 @@ tap.test('basic error with raw request', async t => {
}
})
const res = await rawRequest(ctx.url, 'x').catch((x) => x)
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"}}')
t.deepEqual(res.message, 'Syntax Error GraphQL request (1:1) Unexpected Name "x"\n\n1: x\n ^\n')
})

tap.test('shut down test server', t => {
Expand Down
45 changes: 31 additions & 14 deletions esm/index.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,39 @@
import fetch from 'node-fetch'
import { ClientError } from './types.js'
import get from 'lodash.get'

export class GraphQLClient {
constructor (url, options = {}) {
this.url = url
this.options = options
}

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

const response = await fetch(this.url, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...headers },
body,
body: requestBody,
...others
})

const result = await getResult(response)
const responseBody = await getBody(response)

if (response.ok && !result.errors && result.data) {
if (response.ok && !responseBody.errors && responseBody.data) {
const { headers, status } = response
return { ...result, headers, status }
return { ...responseBody, headers, status }
} else {
const errorResult = typeof result === 'string' ? { error: result } : result
const errorResponseBody = typeof result === 'string' ? { error: responseBody } : responseBody

let bodyObj = body
let requestBodyObject = requestBody
try {
bodyObj = JSON.parse(body)
requestBodyObject = JSON.parse(requestBody)
} catch (e) { /* Swallow parsing errors */ }

throw new ClientError(
{ ...errorResult, status: response.status, headers: response.headers },
bodyObj
)
const error = generateError({ errorResponseBody, response, requestBodyObject })

throw error
}
}

Expand Down Expand Up @@ -96,11 +95,29 @@ export function rawStringRequest (url, body, opts) {
return client.rawStringRequest(body)
}

function getResult (response) {
function getBody (response) {
const contentType = response.headers.get('Content-Type')
if (contentType && contentType.startsWith('application/json')) {
return response.json()
} else {
return response.text()
}
}

export function generateError ({ errorResponseBody, response, requestBodyObject }) {
// The goal is to capture a real error, with a halfway decent message.
// If there are additional object paths with good errors, we can add them here.
// This coveres apollo.
const message = get(errorResponseBody, 'errors[0].extensions.exception.data.message[0].messages[0].message') ||
get(errorResponseBody, 'errors[0].extensions.exception.data.data[0].messages[0].message') ||
get(errorResponseBody, 'errors[0].message') ||
get(errorResponseBody, 'errors.message') ||
get(response, 'statusText') ||
'There was an error with the request.'
const error = new Error(message)

error.response = { ...errorResponseBody, status: response.status, headers: response.headers }
error.request = requestBodyObject

return error
}
4 changes: 2 additions & 2 deletions esm/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ tap.test('basic error', async t => {

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

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"}}')
t.deepEqual(res.message, 'Syntax Error GraphQL request (1:1) Unexpected Name "x"\n\n1: x\n ^\n')
})

tap.test('basic error with raw request', async t => {
Expand All @@ -186,7 +186,7 @@ tap.test('basic error with raw request', async t => {
}
})
const res = await rawRequest(ctx.url, 'x').catch((x) => x)
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"}}')
t.deepEqual(res.message, 'Syntax Error GraphQL request (1:1) Unexpected Name "x"\n\n1: x\n ^\n')
})

tap.test('shut down test server', t => {
Expand Down
28 changes: 0 additions & 28 deletions esm/types.js

This file was deleted.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
},
"homepage": "https://github.com/little-core-labs/gqlr#readme",
"dependencies": {
"lodash.get": "^4.4.2",
"node-fetch": "^2.6.0"
},
"devDependencies": {
Expand Down

0 comments on commit 6121411

Please sign in to comment.