Skip to content

Commit 3ec8a85

Browse files
authored
fix: display stack traces on graphql errors (#1193)
1 parent a972cec commit 3ec8a85

File tree

5 files changed

+161
-35
lines changed

5 files changed

+161
-35
lines changed

src/runtime/server/error-formatter.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { GraphQLError, Source, SourceLocation } from 'graphql'
2+
import stripAnsi from 'strip-ansi'
3+
import { log } from './logger'
4+
5+
const resolverLogger = log.child('graphql')
6+
7+
export function errorFormatter(graphQLError: GraphQLError) {
8+
const colorlessMessage = stripAnsi(graphQLError.message)
9+
10+
if (process.env.NEXUS_STAGE === 'dev') {
11+
resolverLogger.error(graphQLError.message)
12+
13+
if (graphQLError.source && graphQLError.locations) {
14+
console.log(indent(printSourceLocation(graphQLError.source, graphQLError.locations[0]), 5))
15+
if (graphQLError.stack) {
16+
console.log(graphQLError.stack)
17+
}
18+
}
19+
} else {
20+
graphQLError.message = colorlessMessage
21+
resolverLogger.error('An exception occurred in one of your resolver', {
22+
error: {
23+
...graphQLError,
24+
stack: graphQLError.stack
25+
},
26+
})
27+
}
28+
29+
graphQLError.message = colorlessMessage
30+
31+
return graphQLError
32+
}
33+
34+
/**
35+
* Render a helpful description of the location in the GraphQL Source document.
36+
* Modified version from graphql-js
37+
*/
38+
export function printSourceLocation(source: Source, sourceLocation: SourceLocation): string {
39+
const firstLineColumnOffset = source.locationOffset.column - 1
40+
const body = whitespace(firstLineColumnOffset) + source.body
41+
42+
const lineIndex = sourceLocation.line - 1
43+
const lineOffset = source.locationOffset.line - 1
44+
const lineNum = sourceLocation.line + lineOffset
45+
46+
const columnOffset = sourceLocation.line === 1 ? firstLineColumnOffset : 0
47+
const columnNum = sourceLocation.column + columnOffset
48+
49+
const lines = body.split(/\r\n|[\n\r]/g)
50+
const locationLine = lines[lineIndex]
51+
52+
// Special case for minified documents
53+
if (locationLine.length > 120) {
54+
const subLineIndex = Math.floor(columnNum / 80)
55+
const subLineColumnNum = columnNum % 80
56+
const subLines = []
57+
for (let i = 0; i < locationLine.length; i += 80) {
58+
subLines.push(locationLine.slice(i, i + 80))
59+
}
60+
61+
return printPrefixedLines([
62+
[`${lineNum}`, subLines[0]],
63+
...subLines.slice(1, subLineIndex + 1).map((subLine) => ['', subLine]),
64+
[' ', whitespace(subLineColumnNum - 1) + '^'],
65+
['', subLines[subLineIndex + 1]],
66+
])
67+
}
68+
69+
return printPrefixedLines([
70+
// Lines specified like this: ["prefix", "string"],
71+
[`${lineNum - 1}`, lines[lineIndex - 1]],
72+
[`${lineNum}`, locationLine],
73+
['', whitespace(columnNum - 1) + '^'],
74+
[`${lineNum + 1}`, lines[lineIndex + 1]],
75+
])
76+
}
77+
78+
function printPrefixedLines(lines: Array<string[]>): string {
79+
const existingLines = lines.filter(([_, line]) => line !== undefined)
80+
81+
const padLen = Math.max(...existingLines.map(([prefix]) => prefix.length))
82+
return existingLines
83+
.map(([prefix, line]) => leftPad(padLen, prefix) + (line ? ' | ' + line : ' |'))
84+
.join('\n')
85+
}
86+
87+
function whitespace(len: number): string {
88+
return Array(len + 1).join(' ')
89+
}
90+
91+
function leftPad(len: number, str: string): string {
92+
return whitespace(len - str.length) + str
93+
}
94+
95+
function indent(str: string, len: number, char: string = ' ') {
96+
return str
97+
.split('\n')
98+
.map((s) => char.repeat(len) + s)
99+
.join('\n')
100+
}

src/runtime/server/handler-graphql.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { IncomingMessage, ServerResponse } from 'http'
33
import { Socket } from 'net'
44
import { createRequestHandlerGraphQL } from './handler-graphql'
55
import { NexusRequestHandler } from './server'
6+
import { errorFormatter } from './error-formatter'
67

78
let handler: NexusRequestHandler
89
let socket: Socket
@@ -165,6 +166,7 @@ function createHandler(...types: any) {
165166
},
166167
{
167168
introspection: true,
169+
errorFormatterFn: errorFormatter,
168170
}
169171
)
170172
}

src/runtime/server/handler-graphql.ts

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import { Either, isLeft, left, right, toError, tryCatch } from 'fp-ts/lib/Either'
22
import {
33
execute,
4+
FieldNode,
5+
formatError,
46
getOperationAST,
7+
GraphQLError,
8+
GraphQLFormattedError,
59
GraphQLSchema,
610
parse,
711
Source,
12+
specifiedRules,
813
validate,
914
ValidationContext,
10-
specifiedRules,
11-
FieldNode,
12-
GraphQLError,
1315
} from 'graphql'
1416
import { IncomingMessage } from 'http'
1517
import createError, { HttpError } from 'http-errors'
@@ -20,6 +22,7 @@ import { sendError, sendErrorData, sendSuccess } from './utils'
2022

2123
type Settings = {
2224
introspection: boolean
25+
errorFormatterFn(graphqlError: GraphQLError): GraphQLFormattedError
2326
}
2427

2528
type CreateHandler = (
@@ -56,6 +59,7 @@ export const createRequestHandlerGraphQL: CreateHandler = (schema, createContext
5659
res
5760
) => {
5861
const errParams = await getGraphQLParams(req)
62+
const errorFormatter = settings.errorFormatterFn ?? formatError
5963

6064
if (isLeft(errParams)) {
6165
return sendError(res, errParams.left)
@@ -87,7 +91,9 @@ export const createRequestHandlerGraphQL: CreateHandler = (schema, createContext
8791
// todo lots of rich info for clients in here, expose it to them
8892
return sendErrorData(
8993
res,
90-
createError(400, 'GraphQL operation validation failed', { data: validationFailures })
94+
createError(400, 'GraphQL operation validation failed', {
95+
graphqlErrors: validationFailures.map(errorFormatter),
96+
})
9197
)
9298
}
9399

@@ -105,19 +111,36 @@ export const createRequestHandlerGraphQL: CreateHandler = (schema, createContext
105111

106112
const context = await createContext(req)
107113

108-
const result = await execute({
109-
schema: schema,
110-
document: documentAST,
111-
contextValue: context,
112-
variableValues: params.variables,
113-
operationName: params.operationName,
114-
})
114+
try {
115+
const result = await execute({
116+
schema: schema,
117+
document: documentAST,
118+
contextValue: context,
119+
variableValues: params.variables,
120+
operationName: params.operationName,
121+
})
122+
123+
if (result.errors) {
124+
const formattedResult = {
125+
...result,
126+
errors: result.errors?.map(errorFormatter),
127+
}
128+
129+
return sendErrorData(
130+
res,
131+
createError(500, 'failed while resolving client request', { graphqlErrors: formattedResult })
132+
)
133+
}
115134

116-
if (result.errors) {
117-
return sendErrorData(res, createError(500, 'failed while resolving client request', { data: result }))
135+
return sendSuccess(res, result)
136+
} catch (contextError) {
137+
return sendErrorData(
138+
res,
139+
createError(400, 'GraphQL execution context error.', {
140+
graphqlErrors: [errorFormatter(contextError)],
141+
})
142+
)
118143
}
119-
120-
return sendSuccess(res, result)
121144
}
122145

123146
/**

src/runtime/server/server.ts

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import createExpress, { Express } from 'express'
2-
import { GraphQLSchema } from 'graphql'
2+
import { GraphQLError, GraphQLSchema } from 'graphql'
33
import * as HTTP from 'http'
44
import { HttpError } from 'http-errors'
55
import * as Net from 'net'
6-
import stripAnsi from 'strip-ansi'
76
import * as Plugin from '../../lib/plugin'
87
import { httpClose, httpListen, MaybePromise, noop } from '../../lib/utils'
98
import { AppState } from '../app'
109
import * as DevMode from '../dev-mode'
1110
import { ContextContributor } from '../schema/schema'
1211
import { assembledGuard } from '../utils'
12+
import { errorFormatter } from './error-formatter'
1313
import { createRequestHandlerGraphQL } from './handler-graphql'
1414
import { createRequestHandlerPlayground } from './handler-playground'
1515
import { log } from './logger'
@@ -73,12 +73,13 @@ export function create(appState: AppState) {
7373
get graphql() {
7474
return (
7575
assembledGuard(appState, 'app.server.handlers.graphql', () => {
76-
return wrapHandlerWithErrorHandling(
77-
createRequestHandlerGraphQL(
78-
appState.assembled!.schema,
79-
appState.assembled!.createContext,
80-
settings.data.graphql
81-
)
76+
return createRequestHandlerGraphQL(
77+
appState.assembled!.schema,
78+
appState.assembled!.createContext,
79+
{
80+
...settings.data.graphql,
81+
errorFormatterFn: errorFormatter,
82+
}
8283
)
8384
}) ?? noop
8485
)
@@ -110,10 +111,13 @@ export function create(appState: AppState) {
110111
loadedRuntimePlugins
111112
)
112113

113-
const graphqlHandler = createRequestHandlerGraphQL(schema, createContext, settings.data.graphql)
114+
const graphqlHandler = createRequestHandlerGraphQL(schema, createContext, {
115+
...settings.data.graphql,
116+
errorFormatterFn: errorFormatter,
117+
})
114118

115-
express.post(settings.data.path, wrapHandlerWithErrorHandling(graphqlHandler))
116-
express.get(settings.data.path, wrapHandlerWithErrorHandling(graphqlHandler))
119+
express.post(settings.data.path, graphqlHandler)
120+
express.get(settings.data.path, graphqlHandler)
117121

118122
return { createContext }
119123
},
@@ -159,18 +163,15 @@ const wrapHandlerWithErrorHandling = (handler: NexusRequestHandler): NexusReques
159163
await handler(req, res)
160164
if (res.statusCode !== 200 && (res as any).error) {
161165
const error: HttpError = (res as any).error
162-
const colorlessMessage = stripAnsi(error.message)
166+
const graphqlErrors: GraphQLError[] = error.graphqlErrors
163167

164-
if (process.env.NEXUS_STAGE === 'dev') {
165-
resolverLogger.error(error.stack ?? error.message)
168+
if (graphqlErrors.length > 0) {
169+
graphqlErrors.forEach(errorFormatter)
166170
} else {
167-
resolverLogger.error('An exception occured in one of your resolver', {
168-
error: error.stack ? stripAnsi(error.stack) : colorlessMessage,
171+
log.error(error.message, {
172+
error,
169173
})
170174
}
171-
172-
// todo bring back payload sanitization for data sent to clients
173-
// error.message = colorlessMessage
174175
}
175176
}
176177
}

src/runtime/server/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export function sendSuccess(res: ServerResponse, data: object): void {
99

1010
export function sendErrorData(res: ServerResponse, e: HttpError): void {
1111
;(res as any).error = e
12-
sendJSON(res, e.status, e.name, e.headers ?? {}, e.data)
12+
sendJSON(res, e.status, e.name, e.headers ?? {}, e.graphqlErrors)
1313
}
1414

1515
export function sendError(res: ServerResponse, e: HttpError): void {

0 commit comments

Comments
 (0)