Skip to content

Commit dc60883

Browse files
committed
feat: Add new 'graphqlError' function that allows to create better error messages including http code
1 parent 59a55ad commit dc60883

File tree

4 files changed

+263
-15
lines changed

4 files changed

+263
-15
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"content-type": "^1.0.2",
3434
"graphql": "^0.12.0",
3535
"graphql-tools": "^2.21.0",
36-
"http-errors": "^1.6.1",
36+
"http-errors": "^1.6.2",
3737
"querystring": "^0.2.0",
3838
"raw-body": "^2.2.0",
3939
"url": "^0.11.0"

src/index.js

+25-4
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const accepts = require('accepts')
2121
const graphql = require('graphql')
2222
const httpError = require('http-errors')
2323
const url = require('url')
24+
const graphqlError = require('./utils')
2425

2526
const parseBody = require('./parseBody')
2627
const { renderGraphiQL } = require('./renderGraphiQL')
@@ -267,10 +268,29 @@ function graphqlHTTP(options) {
267268
}
268269

269270
// Format any encountered errors.
270-
if (result.errors) {
271-
result.errors = result.errors.map(formatErrorFn || graphql.formatError)
272-
if (allPropertiesFalsy(result.data))
271+
if (result.errors && result.errors.length > 0) {
272+
const formattingError = formatErrorFn || graphql.formatError
273+
let explicitHttpCode
274+
result.errors = result.errors.map(e => {
275+
let newError = formattingError(e)
276+
let errorMessage
277+
try {
278+
const errMsg = JSON.parse(newError.message) || {}
279+
if (errMsg.type == 'graphql') {
280+
explicitHttpCode = errMsg.httpCode || 500
281+
errorMessage = errMsg.hideErrors && errMsg.alternateMessage ? errMsg.alternateMessage : errMsg.message
282+
} else
283+
errorMessage = newError.message
284+
} catch (err) {
285+
errorMessage = newError.message
286+
}
287+
newError.message = errorMessage
288+
return newError
289+
})
290+
if (!explicitHttpCode && allPropertiesFalsy(result.data))
273291
httpCode = response.statusCode < 500 ? 500 : response.statusCode
292+
else if (explicitHttpCode)
293+
httpCode = explicitHttpCode
274294
}
275295
}
276296

@@ -400,5 +420,6 @@ const isGraphiQLRequest = (req) => getGraphQLParams(req).then(params => canDispl
400420

401421
module.exports = {
402422
graphqlHandler,
403-
isGraphiQLRequest
423+
isGraphiQLRequest,
424+
graphqlError
404425
}

src/utils.js

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* Returns a standard JS Error object where the message has been JSON serialized.
3+
* Possible signatures:
4+
* - (httpCode:Number)
5+
* - (httpCode:Number, message:String)
6+
* - (httpCode:Number, message:String, options:Object)
7+
* - (message:String)
8+
* - (message:String, options:Object)
9+
*
10+
* where options is { alternateMessage:String, hide:Boolean }
11+
* @return {Error} [description]
12+
*/
13+
const graphqlError = (...args) => {
14+
const codeDefined = typeof(args[0]) == 'number'
15+
const httpCode = codeDefined ? args[0] : 500
16+
const message = codeDefined ? args[1] || '' : args[0] || ''
17+
const options = codeDefined ? args[2] || {} : args[1] || {}
18+
const alternateMessage = options.alternateMessage || 'Internal Server Error'
19+
const hideErrors = options.hide
20+
return new Error(JSON.stringify({
21+
type: 'graphql',
22+
httpCode,
23+
message,
24+
alternateMessage,
25+
hideErrors
26+
}))
27+
}
28+
29+
module.exports = graphqlError

test/index.js

+208-10
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
const { assert } = require('chai')
99
const httpMocks = require('node-mocks-http')
1010
const { makeExecutableSchema } = require('graphql-tools')
11-
const { graphqlHandler } = require('../src/index')
1211
const { app } = require('webfunc')
12+
const { graphqlHandler, graphqlError } = require('../src/index')
1313

1414
/*eslint-disable */
1515
describe('index', () =>
@@ -73,7 +73,7 @@ describe('index', () =>
7373
if (results)
7474
return results
7575
else
76-
throw httpError(404, `Product with id ${id} does not exist.`)
76+
throw graphqlError(404, `Product with id ${id} does not exist.`)
7777
}
7878
}
7979
}
@@ -174,7 +174,7 @@ describe('index', () =>
174174
if (results)
175175
return results
176176
else
177-
throw httpError(404, `Product with id ${id} does not exist.`)
177+
throw graphqlError(404, `Product with id ${id} does not exist.`)
178178
}
179179
}
180180
}
@@ -264,7 +264,7 @@ describe('index', () =>
264264
if (results)
265265
return results
266266
else
267-
throw httpError(404, `Product with id ${id} does not exist.`)
267+
throw graphqlError(404, `Product with id ${id} does not exist.`)
268268
}
269269
}
270270
}
@@ -350,7 +350,7 @@ describe('index', () =>
350350
if (results)
351351
return results
352352
else
353-
throw httpError(404, `Product with id ${id} does not exist.`)
353+
throw graphqlError(404, `Product with id ${id} does not exist.`)
354354
}
355355
}
356356
}
@@ -450,7 +450,7 @@ describe('index', () =>
450450
if (results)
451451
return results
452452
else
453-
throw httpError(404, `Product with id ${id} does not exist.`)
453+
throw graphqlError(404, `Product with id ${id} does not exist.`)
454454
}
455455
}
456456
}
@@ -556,7 +556,7 @@ describe('index', () =>
556556
if (results)
557557
return results
558558
else
559-
throw httpError(404, `Product with id ${id} does not exist.`)
559+
throw graphqlError(404, `Product with id ${id} does not exist.`)
560560
}
561561
}
562562
}
@@ -641,7 +641,7 @@ describe('index', () =>
641641
if (results)
642642
return results
643643
else
644-
throw httpError(404, `Product with id ${id} does not exist.`)
644+
throw graphqlError(404, `Product with id ${id} does not exist.`)
645645
}
646646
}
647647
}
@@ -735,7 +735,7 @@ describe('index', () =>
735735
if (results)
736736
return results
737737
else
738-
throw httpError(404, `Product with id ${id} does not exist.`)
738+
throw graphqlError(404, `Product with id ${id} does not exist.`)
739739
}
740740
}
741741
}
@@ -828,7 +828,7 @@ describe('index', () =>
828828
if (results)
829829
return results
830830
else
831-
throw httpError(404, `Product with id ${id} does not exist.`)
831+
throw graphqlError(404, `Product with id ${id} does not exist.`)
832832
}
833833
}
834834
}
@@ -891,5 +891,203 @@ describe('index', () =>
891891
return Promise.all([result_01])
892892
})))
893893

894+
/*eslint-disable */
895+
describe('index', () =>
896+
describe('#graphqlError: 01', () =>
897+
it(`Should control the HTTP code being sent when some errors happen.`, () => {
898+
/*eslint-enable */
899+
900+
const schema = `
901+
type Product {
902+
id: ID!
903+
name: String!
904+
shortDescription: String
905+
}
906+
907+
type Query {
908+
# ### GET products
909+
#
910+
# _Arguments_
911+
# - **id**: Product's id (optional)
912+
products(id: Int): [Product]
913+
}
914+
915+
schema {
916+
query: Query
917+
}
918+
`
919+
const productMocks = [{ id: 1, name: 'Product A', shortDescription: 'First product.' }, { id: 2, name: 'Product B', shortDescription: 'Second product.' }]
920+
const productResolver = {
921+
Query: {
922+
products(root, { id }, context) {
923+
const results = id ? productMocks.filter(p => p.id == id) : productMocks
924+
if (results.length > 0)
925+
return results
926+
else
927+
throw graphqlError(404, `Product with id ${id} does not exist.`)
928+
}
929+
}
930+
}
931+
932+
const executableSchema = makeExecutableSchema({
933+
typeDefs: schema,
934+
resolvers: productResolver
935+
})
936+
937+
const graphqlOptions = {
938+
schema: executableSchema,
939+
graphiql: true,
940+
endpointURL: "/graphiql"
941+
}
942+
943+
const uri = 'users/graphiql'
944+
const req_01 = httpMocks.createRequest({
945+
method: 'GET',
946+
headers: {
947+
origin: 'http://localhost:8080',
948+
referer: 'http://localhost:8080',
949+
accept: 'application/json'
950+
},
951+
_parsedUrl: {
952+
pathname: uri
953+
},
954+
url: uri,
955+
body: {
956+
query: `
957+
query {
958+
products(id: 10) {
959+
name
960+
}
961+
}`,
962+
variables: null
963+
}
964+
})
965+
const res_01 = httpMocks.createResponse()
966+
967+
app.reset()
968+
app.all(['/users', '/users/graphiql'], graphqlHandler(graphqlOptions), () => null)
969+
const fn = app.handleEvent()
970+
971+
const result_01 = fn(req_01, res_01).then(() => {
972+
assert.equal(res_01.statusCode, 404)
973+
const html = res_01._getData()
974+
assert.isOk(html)
975+
assert.equal(typeof(html), 'string')
976+
let htmlJson
977+
try {
978+
htmlJson = JSON.parse(html)
979+
}
980+
catch(err) {
981+
assert.isOk(err)
982+
htmlJson = null
983+
}
984+
assert.isOk(htmlJson, `Response should be a json object.`)
985+
assert.isOk(htmlJson.errors, `Response should be a json object with a defined 'errors' property.`)
986+
assert.isOk(htmlJson.errors.length > 0, `Response's 'errors' must be an array with at least one element.`)
987+
assert.equal(htmlJson.errors[0].message, 'Product with id 10 does not exist.')
988+
})
989+
990+
return Promise.all([result_01])
991+
})))
992+
993+
/*eslint-disable */
994+
describe('index', () =>
995+
describe('#graphqlError: 02', () =>
996+
it(`Should obfuscate the error message in prod.`, () => {
997+
/*eslint-enable */
998+
999+
const schema = `
1000+
type Product {
1001+
id: ID!
1002+
name: String!
1003+
shortDescription: String
1004+
}
1005+
1006+
type Query {
1007+
# ### GET products
1008+
#
1009+
# _Arguments_
1010+
# - **id**: Product's id (optional)
1011+
products(id: Int): [Product]
1012+
}
1013+
1014+
schema {
1015+
query: Query
1016+
}
1017+
`
1018+
const productMocks = [{ id: 1, name: 'Product A', shortDescription: 'First product.' }, { id: 2, name: 'Product B', shortDescription: 'Second product.' }]
1019+
const productResolver = {
1020+
Query: {
1021+
products(root, { id }, context) {
1022+
const results = id ? productMocks.filter(p => p.id == id) : productMocks
1023+
if (results.length > 0)
1024+
return results
1025+
else
1026+
throw graphqlError(404, `Product with id ${id} does not exist.`, { hide: true })
1027+
}
1028+
}
1029+
}
1030+
1031+
const executableSchema = makeExecutableSchema({
1032+
typeDefs: schema,
1033+
resolvers: productResolver
1034+
})
1035+
1036+
const graphqlOptions = {
1037+
schema: executableSchema,
1038+
graphiql: true,
1039+
endpointURL: "/graphiql"
1040+
}
1041+
1042+
const uri = 'users/graphiql'
1043+
const req_01 = httpMocks.createRequest({
1044+
method: 'GET',
1045+
headers: {
1046+
origin: 'http://localhost:8080',
1047+
referer: 'http://localhost:8080',
1048+
accept: 'application/json'
1049+
},
1050+
_parsedUrl: {
1051+
pathname: uri
1052+
},
1053+
url: uri,
1054+
body: {
1055+
query: `
1056+
query {
1057+
products(id: 10) {
1058+
name
1059+
}
1060+
}`,
1061+
variables: null
1062+
}
1063+
})
1064+
const res_01 = httpMocks.createResponse()
1065+
1066+
app.reset()
1067+
app.all(['/users', '/users/graphiql'], graphqlHandler(graphqlOptions), () => null)
1068+
const fn = app.handleEvent()
1069+
1070+
const result_01 = fn(req_01, res_01).then(() => {
1071+
assert.equal(res_01.statusCode, 404)
1072+
const html = res_01._getData()
1073+
assert.isOk(html)
1074+
assert.equal(typeof(html), 'string')
1075+
let htmlJson
1076+
try {
1077+
htmlJson = JSON.parse(html)
1078+
}
1079+
catch(err) {
1080+
assert.isOk(err)
1081+
htmlJson = null
1082+
}
1083+
assert.isOk(htmlJson, `Response should be a json object.`)
1084+
assert.isOk(htmlJson.errors, `Response should be a json object with a defined 'errors' property.`)
1085+
assert.isOk(htmlJson.errors.length > 0, `Response's 'errors' must be an array with at least one element.`)
1086+
assert.equal(htmlJson.errors[0].message, 'Internal Server Error')
1087+
})
1088+
1089+
return Promise.all([result_01])
1090+
})))
1091+
8941092

8951093

0 commit comments

Comments
 (0)