Skip to content

Commit b54bea0

Browse files
authoredAug 27, 2021
feat(typescript): export GraphqlResponseError (#312)
1 parent 485cbcd commit b54bea0

File tree

5 files changed

+110
-38
lines changed

5 files changed

+110
-38
lines changed
 

‎README.md

+27-16
Original file line numberDiff line numberDiff line change
@@ -263,10 +263,11 @@ import type { GraphQlQueryResponseData } from "@octokit/graphql";
263263

264264
## Errors
265265

266-
In case of a GraphQL error, `error.message` is set to the first error from the response’s `errors` array. All errors can be accessed at `error.errors`. `error.request` has the request options such as query, variables and headers set for easier debugging.
266+
In case of a GraphQL error, `error.message` is set to a combined message describing all errors returned by the endpoint.
267+
All errors can be accessed at `error.errors`. `error.request` has the request options such as query, variables and headers set for easier debugging.
267268

268269
```js
269-
let { graphql } = require("@octokit/graphql");
270+
let { graphql, GraphqlResponseError } = require("@octokit/graphql");
270271
graphqlt = graphql.defaults({
271272
headers: {
272273
authorization: `token secret123`,
@@ -281,20 +282,30 @@ const query = `{
281282
try {
282283
const result = await graphql(query);
283284
} catch (error) {
284-
// server responds with
285-
// {
286-
// "data": null,
287-
// "errors": [{
288-
// "message": "Field 'bioHtml' doesn't exist on type 'User'",
289-
// "locations": [{
290-
// "line": 3,
291-
// "column": 5
292-
// }]
293-
// }]
294-
// }
295-
296-
console.log("Request failed:", error.request); // { query, variables: {}, headers: { authorization: 'token secret123' } }
297-
console.log(error.message); // Field 'bioHtml' doesn't exist on type 'User'
285+
if (error instanceof GraphqlResponseError) {
286+
// do something with the error, allowing you to detect a graphql response error,
287+
// compared to accidentally catching unrelated errors.
288+
289+
// server responds with an object like the following (as an example)
290+
// class GraphqlResponseError {
291+
// "headers": {
292+
// "status": "403",
293+
// },
294+
// "data": null,
295+
// "errors": [{
296+
// "message": "Field 'bioHtml' doesn't exist on type 'User'",
297+
// "locations": [{
298+
// "line": 3,
299+
// "column": 5
300+
// }]
301+
// }]
302+
// }
303+
304+
console.log("Request failed:", error.request); // { query, variables: {}, headers: { authorization: 'token secret123' } }
305+
console.log(error.message); // Field 'bioHtml' doesn't exist on type 'User'
306+
} else {
307+
// handle non-GraphQL error
308+
}
298309
}
299310
```
300311

‎src/error.ts

+30-14
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,38 @@
11
import { ResponseHeaders } from "@octokit/types";
2-
import { GraphQlEndpointOptions, GraphQlQueryResponse } from "./types";
2+
import {
3+
GraphQlEndpointOptions,
4+
GraphQlQueryResponse,
5+
GraphQlQueryResponseData,
6+
GraphQlResponse,
7+
} from "./types";
8+
9+
type ServerResponseData<T> = Required<GraphQlQueryResponse<T>>;
10+
11+
function _buildMessageForResponseErrors(
12+
data: ServerResponseData<unknown>
13+
): string {
14+
return (
15+
`Request failed due to following response errors:\n` +
16+
data.errors.map((e) => ` - ${e.message}`).join("\n")
17+
);
18+
}
19+
20+
export class GraphqlResponseError<ResponseData> extends Error {
21+
override name = "GraphqlResponseError";
22+
23+
readonly errors: GraphQlQueryResponse<never>["errors"];
24+
readonly data: ResponseData;
325

4-
export class GraphqlError<ResponseData> extends Error {
5-
public request: GraphQlEndpointOptions;
626
constructor(
7-
request: GraphQlEndpointOptions,
8-
response: {
9-
headers: ResponseHeaders;
10-
data: Required<GraphQlQueryResponse<ResponseData>>;
11-
}
27+
readonly request: GraphQlEndpointOptions,
28+
readonly headers: ResponseHeaders,
29+
readonly response: ServerResponseData<ResponseData>
1230
) {
13-
const message = response.data.errors[0].message;
14-
super(message);
31+
super(_buildMessageForResponseErrors(response));
1532

16-
Object.assign(this, response.data);
17-
Object.assign(this, { headers: response.headers });
18-
this.name = "GraphqlError";
19-
this.request = request;
33+
// Expose the errors and response data in their shorthand properties.
34+
this.errors = response.errors;
35+
this.data = response.data;
2036

2137
// Maintains proper stack trace (only available on V8)
2238
/* istanbul ignore next */

‎src/graphql.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { request as Request } from "@octokit/request";
22
import { ResponseHeaders } from "@octokit/types";
3-
import { GraphqlError } from "./error";
3+
import { GraphqlResponseError } from "./error";
44
import {
55
GraphQlEndpointOptions,
66
RequestParameters,
@@ -76,10 +76,11 @@ export function graphql<ResponseData = GraphQlQueryResponseData>(
7676
headers[key] = response.headers[key];
7777
}
7878

79-
throw new GraphqlError(requestOptions, {
79+
throw new GraphqlResponseError(
80+
requestOptions,
8081
headers,
81-
data: response.data as Required<GraphQlQueryResponse<ResponseData>>,
82-
});
82+
response.data as Required<GraphQlQueryResponse<ResponseData>>
83+
);
8384
}
8485

8586
return response.data.data;

‎src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const graphql = withDefaults(request, {
1414
});
1515

1616
export { GraphQlQueryResponseData } from "./types";
17+
export { GraphqlResponseError } from "./error";
1718

1819
export function withCustomRequest(customRequest: typeof request) {
1920
return withDefaults(customRequest, {

‎test/error.test.ts

+47-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import fetchMock from "fetch-mock";
22

3-
import { graphql } from "../src";
3+
import { graphql, GraphqlResponseError } from "../src";
44

55
describe("errors", () => {
66
it("Invalid query", () => {
@@ -40,13 +40,55 @@ describe("errors", () => {
4040

4141
.catch((error) => {
4242
expect(error.message).toEqual(
43-
"Field 'bioHtml' doesn't exist on type 'User'"
43+
"Request failed due to following response errors:\n" +
44+
" - Field 'bioHtml' doesn't exist on type 'User'"
4445
);
4546
expect(error.errors).toStrictEqual(mockResponse.errors);
4647
expect(error.request.query).toEqual(query);
4748
});
4849
});
4950

51+
it("Should be able check if an error is instance of a GraphQL response error", () => {
52+
const query = `{
53+
repository {
54+
name
55+
}
56+
}`;
57+
58+
const mockResponse = {
59+
data: null,
60+
errors: [
61+
{
62+
locations: [
63+
{
64+
column: 5,
65+
line: 3,
66+
},
67+
],
68+
message: "Some error message",
69+
},
70+
],
71+
};
72+
73+
return graphql(query, {
74+
headers: {
75+
authorization: `token secret123`,
76+
},
77+
request: {
78+
fetch: fetchMock
79+
.sandbox()
80+
.post("https://api.github.com/graphql", mockResponse),
81+
},
82+
})
83+
.then((result) => {
84+
throw new Error("Should not resolve");
85+
})
86+
87+
.catch((error) => {
88+
expect(error instanceof GraphqlResponseError).toBe(true);
89+
});
90+
});
91+
5092
it("Should throw an error for a partial response accompanied by errors", () => {
5193
const query = `{
5294
repository(name: "probot", owner: "probot") {
@@ -105,7 +147,8 @@ describe("errors", () => {
105147
})
106148
.catch((error) => {
107149
expect(error.message).toEqual(
108-
"`invalid cursor` does not appear to be a valid cursor."
150+
"Request failed due to following response errors:\n" +
151+
" - `invalid cursor` does not appear to be a valid cursor."
109152
);
110153
expect(error.errors).toStrictEqual(mockResponse.errors);
111154
expect(error.request.query).toEqual(query);
@@ -119,7 +162,7 @@ describe("errors", () => {
119162

120163
it("Should throw for server error", () => {
121164
const query = `{
122-
viewer {
165+
viewer {
123166
login
124167
}
125168
}`;

0 commit comments

Comments
 (0)
Please sign in to comment.