UNPKG

9.62 kBPlain TextView Raw
1import {
2 ASTNode,
3 GraphQLError,
4 GraphQLFormattedError,
5 Source,
6 SourceLocation,
7 printError,
8 formatError,
9} from 'graphql';
10
11declare module 'graphql' {
12 export interface GraphQLErrorExtensions {
13 exception?: {
14 code?: string;
15 stacktrace?: ReadonlyArray<string>;
16 };
17 }
18}
19
20// Note: We'd like to switch to `extends GraphQLError` and look forward to doing so
21// as soon as we drop support for `graphql` bellow `v15.7.0`.
22export class ApolloError extends Error implements GraphQLError {
23 public extensions: Record<string, any>;
24 override readonly name!: string;
25 readonly locations: ReadonlyArray<SourceLocation> | undefined;
26 readonly path: ReadonlyArray<string | number> | undefined;
27 readonly source: Source | undefined;
28 readonly positions: ReadonlyArray<number> | undefined;
29 readonly nodes: ReadonlyArray<ASTNode> | undefined;
30 public originalError: Error | undefined;
31
32 [key: string]: any;
33
34 constructor(
35 message: string,
36 code?: string,
37 extensions?: Record<string, any>,
38 ) {
39 super(message);
40
41 // if no name provided, use the default. defineProperty ensures that it stays non-enumerable
42 if (!this.name) {
43 Object.defineProperty(this, 'name', { value: 'ApolloError' });
44 }
45
46 if (extensions?.extensions) {
47 throw Error(
48 'Pass extensions directly as the third argument of the ApolloError constructor: `new ' +
49 'ApolloError(message, code, {myExt: value})`, not `new ApolloError(message, code, ' +
50 '{extensions: {myExt: value}})`',
51 );
52 }
53
54 this.extensions = { ...extensions, code };
55 }
56
57 toJSON(): GraphQLFormattedError {
58 return formatError(toGraphQLError(this));
59 }
60
61 override toString(): string {
62 return printError(toGraphQLError(this));
63 }
64
65 get [Symbol.toStringTag](): string {
66 return this.name;
67 }
68}
69
70function toGraphQLError(error: ApolloError): GraphQLError {
71 return new GraphQLError(
72 error.message,
73 error.nodes,
74 error.source,
75 error.positions,
76 error.path,
77 error.originalError,
78 error.extensions,
79 );
80}
81
82function enrichError(error: Partial<GraphQLError>, debug: boolean = false) {
83 // follows similar structure to https://github.com/graphql/graphql-js/blob/main/src/error/GraphQLError.ts#L127-L176
84 // with the addition of name
85 const expanded = Object.create(Object.getPrototypeOf(error), {
86 name: {
87 value: error.name,
88 },
89 message: {
90 value: error.message,
91 enumerable: true,
92 writable: true,
93 },
94 locations: {
95 value: error.locations || undefined,
96 enumerable: true,
97 },
98 path: {
99 value: error.path || undefined,
100 enumerable: true,
101 },
102 nodes: {
103 value: error.nodes || undefined,
104 },
105 source: {
106 value: error.source || undefined,
107 },
108 positions: {
109 value: error.positions || undefined,
110 },
111 originalError: {
112 value: error.originalError,
113 },
114 });
115
116 expanded.extensions = {
117 ...error.extensions,
118 code: error.extensions?.code || 'INTERNAL_SERVER_ERROR',
119 exception: {
120 ...error.extensions?.exception,
121 ...(error.originalError as any),
122 },
123 };
124
125 // ensure that extensions is not taken from the originalError
126 // graphql-js ensures that the originalError's extensions are hoisted
127 // https://github.com/graphql/graphql-js/blob/0bb47b2/src/error/GraphQLError.js#L138
128 delete expanded.extensions.exception.extensions;
129 if (debug && !expanded.extensions.exception.stacktrace) {
130 const stack = error.originalError?.stack || error.stack;
131 expanded.extensions.exception.stacktrace = stack?.split('\n');
132 }
133
134 if (Object.keys(expanded.extensions.exception).length === 0) {
135 // remove from printing an empty object
136 delete expanded.extensions.exception;
137 }
138
139 return expanded as ApolloError;
140}
141
142export function toApolloError(
143 error: Error & { extensions?: Record<string, any> },
144 code: string = 'INTERNAL_SERVER_ERROR',
145): Error & { extensions: Record<string, any> } {
146 let err = error;
147 if (err.extensions) {
148 err.extensions.code = code;
149 } else {
150 err.extensions = { code };
151 }
152 return err as Error & { extensions: Record<string, any> };
153}
154
155export interface ErrorOptions {
156 code?: string;
157 // This declaration means it takes any "class" that has a constructor that
158 // takes a single string, and should be invoked via the `new` operator.
159 errorClass?: new (message: string) => ApolloError;
160}
161
162export function fromGraphQLError(error: GraphQLError, options?: ErrorOptions) {
163 const copy: ApolloError = options?.errorClass
164 ? new options.errorClass(error.message)
165 : new ApolloError(error.message);
166
167 // copy enumerable keys
168 Object.entries(error).forEach(([key, value]) => {
169 if (key === 'extensions') {
170 return; // extensions are handled bellow
171 }
172 copy[key] = value;
173 });
174
175 // merge extensions instead of just copying them
176 copy.extensions = {
177 ...copy.extensions,
178 ...error.extensions,
179 };
180
181 // Fallback on default for code
182 if (!copy.extensions.code) {
183 copy.extensions.code = options?.code || 'INTERNAL_SERVER_ERROR';
184 }
185
186 // copy the original error, while keeping all values non-enumerable, so they
187 // are not printed unless directly referenced
188 Object.defineProperty(copy, 'originalError', { value: {} });
189 Object.getOwnPropertyNames(error).forEach((key) => {
190 Object.defineProperty(copy.originalError, key, {
191 value: (error as any)[key],
192 });
193 });
194
195 return copy;
196}
197
198export class SyntaxError extends ApolloError {
199 constructor(message: string) {
200 super(message, 'GRAPHQL_PARSE_FAILED');
201
202 Object.defineProperty(this, 'name', { value: 'SyntaxError' });
203 }
204}
205
206export class ValidationError extends ApolloError {
207 constructor(message: string) {
208 super(message, 'GRAPHQL_VALIDATION_FAILED');
209
210 Object.defineProperty(this, 'name', { value: 'ValidationError' });
211 }
212}
213
214export class AuthenticationError extends ApolloError {
215 constructor(message: string, extensions?: Record<string, any>) {
216 super(message, 'UNAUTHENTICATED', extensions);
217
218 Object.defineProperty(this, 'name', { value: 'AuthenticationError' });
219 }
220}
221
222export class ForbiddenError extends ApolloError {
223 constructor(message: string, extensions?: Record<string, any>) {
224 super(message, 'FORBIDDEN', extensions);
225
226 Object.defineProperty(this, 'name', { value: 'ForbiddenError' });
227 }
228}
229
230export class PersistedQueryNotFoundError extends ApolloError {
231 constructor() {
232 super('PersistedQueryNotFound', 'PERSISTED_QUERY_NOT_FOUND');
233
234 Object.defineProperty(this, 'name', {
235 value: 'PersistedQueryNotFoundError',
236 });
237 }
238}
239
240export class PersistedQueryNotSupportedError extends ApolloError {
241 constructor() {
242 super('PersistedQueryNotSupported', 'PERSISTED_QUERY_NOT_SUPPORTED');
243
244 Object.defineProperty(this, 'name', {
245 value: 'PersistedQueryNotSupportedError',
246 });
247 }
248}
249
250export class UserInputError extends ApolloError {
251 constructor(message: string, extensions?: Record<string, any>) {
252 super(message, 'BAD_USER_INPUT', extensions);
253
254 Object.defineProperty(this, 'name', { value: 'UserInputError' });
255 }
256}
257
258export function formatApolloErrors(
259 errors: ReadonlyArray<Error>,
260 options?: {
261 formatter?: (error: GraphQLError) => GraphQLFormattedError;
262 debug?: boolean;
263 },
264): Array<ApolloError> {
265 if (!options) {
266 return errors.map((error) => enrichError(error));
267 }
268 const { formatter, debug } = options;
269
270 // Errors that occur in graphql-tools can contain an errors array that contains the errors thrown in a merged schema
271 // https://github.com/apollographql/graphql-tools/blob/3d53986ca/src/stitching/errors.ts#L104-L107
272 //
273 // They are are wrapped in an extra GraphQL error
274 // https://github.com/apollographql/graphql-tools/blob/3d53986ca/src/stitching/errors.ts#L109-L113
275 // which calls:
276 // https://github.com/graphql/graphql-js/blob/0a30b62964/src/error/locatedError.js#L18-L37
277 // Some processing for these nested errors could be done here:
278 //
279 // if (Array.isArray((error as any).errors)) {
280 // (error as any).errors.forEach(e => flattenedErrors.push(e));
281 // } else if (
282 // (error as any).originalError &&
283 // Array.isArray((error as any).originalError.errors)
284 // ) {
285 // (error as any).originalError.errors.forEach(e => flattenedErrors.push(e));
286 // } else {
287 // flattenedErrors.push(error);
288 // }
289
290 const enrichedErrors = errors.map((error) => enrichError(error, debug));
291 const makePrintable = (error: GraphQLFormattedError) => {
292 if (error instanceof Error) {
293 // Error defines its `message` and other fields as non-enumerable, meaning JSON.stringify does not print them.
294 const graphQLError = error as GraphQLFormattedError;
295 return {
296 message: graphQLError.message,
297 ...(graphQLError.locations && { locations: graphQLError.locations }),
298 ...(graphQLError.path && { path: graphQLError.path }),
299 ...(graphQLError.extensions && { extensions: graphQLError.extensions }),
300 };
301 }
302 return error;
303 };
304
305 if (!formatter) {
306 return enrichedErrors;
307 }
308
309 return enrichedErrors.map((error) => {
310 try {
311 return makePrintable(formatter(error));
312 } catch (err) {
313 if (debug) {
314 // XXX: This cast is pretty sketchy, as other error types can be thrown!
315 return enrichError(err as Partial<GraphQLError>, debug);
316 } else {
317 // obscure error
318 const newError = fromGraphQLError(
319 new GraphQLError('Internal server error'),
320 );
321 return enrichError(newError, debug);
322 }
323 }
324 }) as Array<ApolloError>;
325}