Skip to content

Commit

Permalink
feat(typescript): export GraphqlResponseError (#312)
Browse files Browse the repository at this point in the history
  • Loading branch information
devversion authored Aug 27, 2021
1 parent 485cbcd commit b54bea0
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 38 deletions.
43 changes: 27 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,10 +263,11 @@ import type { GraphQlQueryResponseData } from "@octokit/graphql";

## Errors

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.
In case of a GraphQL error, `error.message` is set to a combined message describing all errors returned by the endpoint.
All errors can be accessed at `error.errors`. `error.request` has the request options such as query, variables and headers set for easier debugging.

```js
let { graphql } = require("@octokit/graphql");
let { graphql, GraphqlResponseError } = require("@octokit/graphql");
graphqlt = graphql.defaults({
headers: {
authorization: `token secret123`,
Expand All @@ -281,20 +282,30 @@ const query = `{
try {
const result = await graphql(query);
} catch (error) {
// server responds with
// {
// "data": null,
// "errors": [{
// "message": "Field 'bioHtml' doesn't exist on type 'User'",
// "locations": [{
// "line": 3,
// "column": 5
// }]
// }]
// }

console.log("Request failed:", error.request); // { query, variables: {}, headers: { authorization: 'token secret123' } }
console.log(error.message); // Field 'bioHtml' doesn't exist on type 'User'
if (error instanceof GraphqlResponseError) {
// do something with the error, allowing you to detect a graphql response error,
// compared to accidentally catching unrelated errors.

// server responds with an object like the following (as an example)
// class GraphqlResponseError {
// "headers": {
// "status": "403",
// },
// "data": null,
// "errors": [{
// "message": "Field 'bioHtml' doesn't exist on type 'User'",
// "locations": [{
// "line": 3,
// "column": 5
// }]
// }]
// }

console.log("Request failed:", error.request); // { query, variables: {}, headers: { authorization: 'token secret123' } }
console.log(error.message); // Field 'bioHtml' doesn't exist on type 'User'
} else {
// handle non-GraphQL error
}
}
```

Expand Down
44 changes: 30 additions & 14 deletions src/error.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,38 @@
import { ResponseHeaders } from "@octokit/types";
import { GraphQlEndpointOptions, GraphQlQueryResponse } from "./types";
import {
GraphQlEndpointOptions,
GraphQlQueryResponse,
GraphQlQueryResponseData,
GraphQlResponse,
} from "./types";

type ServerResponseData<T> = Required<GraphQlQueryResponse<T>>;

function _buildMessageForResponseErrors(
data: ServerResponseData<unknown>
): string {
return (
`Request failed due to following response errors:\n` +
data.errors.map((e) => ` - ${e.message}`).join("\n")
);
}

export class GraphqlResponseError<ResponseData> extends Error {
override name = "GraphqlResponseError";

readonly errors: GraphQlQueryResponse<never>["errors"];
readonly data: ResponseData;

export class GraphqlError<ResponseData> extends Error {
public request: GraphQlEndpointOptions;
constructor(
request: GraphQlEndpointOptions,
response: {
headers: ResponseHeaders;
data: Required<GraphQlQueryResponse<ResponseData>>;
}
readonly request: GraphQlEndpointOptions,
readonly headers: ResponseHeaders,
readonly response: ServerResponseData<ResponseData>
) {
const message = response.data.errors[0].message;
super(message);
super(_buildMessageForResponseErrors(response));

Object.assign(this, response.data);
Object.assign(this, { headers: response.headers });
this.name = "GraphqlError";
this.request = request;
// Expose the errors and response data in their shorthand properties.
this.errors = response.errors;
this.data = response.data;

// Maintains proper stack trace (only available on V8)
/* istanbul ignore next */
Expand Down
9 changes: 5 additions & 4 deletions src/graphql.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { request as Request } from "@octokit/request";
import { ResponseHeaders } from "@octokit/types";
import { GraphqlError } from "./error";
import { GraphqlResponseError } from "./error";
import {
GraphQlEndpointOptions,
RequestParameters,
Expand Down Expand Up @@ -76,10 +76,11 @@ export function graphql<ResponseData = GraphQlQueryResponseData>(
headers[key] = response.headers[key];
}

throw new GraphqlError(requestOptions, {
throw new GraphqlResponseError(
requestOptions,
headers,
data: response.data as Required<GraphQlQueryResponse<ResponseData>>,
});
response.data as Required<GraphQlQueryResponse<ResponseData>>
);
}

return response.data.data;
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const graphql = withDefaults(request, {
});

export { GraphQlQueryResponseData } from "./types";
export { GraphqlResponseError } from "./error";

export function withCustomRequest(customRequest: typeof request) {
return withDefaults(customRequest, {
Expand Down
51 changes: 47 additions & 4 deletions test/error.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import fetchMock from "fetch-mock";

import { graphql } from "../src";
import { graphql, GraphqlResponseError } from "../src";

describe("errors", () => {
it("Invalid query", () => {
Expand Down Expand Up @@ -40,13 +40,55 @@ describe("errors", () => {

.catch((error) => {
expect(error.message).toEqual(
"Field 'bioHtml' doesn't exist on type 'User'"
"Request failed due to following response errors:\n" +
" - Field 'bioHtml' doesn't exist on type 'User'"
);
expect(error.errors).toStrictEqual(mockResponse.errors);
expect(error.request.query).toEqual(query);
});
});

it("Should be able check if an error is instance of a GraphQL response error", () => {
const query = `{
repository {
name
}
}`;

const mockResponse = {
data: null,
errors: [
{
locations: [
{
column: 5,
line: 3,
},
],
message: "Some error message",
},
],
};

return graphql(query, {
headers: {
authorization: `token secret123`,
},
request: {
fetch: fetchMock
.sandbox()
.post("https://api.github.com/graphql", mockResponse),
},
})
.then((result) => {
throw new Error("Should not resolve");
})

.catch((error) => {
expect(error instanceof GraphqlResponseError).toBe(true);
});
});

it("Should throw an error for a partial response accompanied by errors", () => {
const query = `{
repository(name: "probot", owner: "probot") {
Expand Down Expand Up @@ -105,7 +147,8 @@ describe("errors", () => {
})
.catch((error) => {
expect(error.message).toEqual(
"`invalid cursor` does not appear to be a valid cursor."
"Request failed due to following response errors:\n" +
" - `invalid cursor` does not appear to be a valid cursor."
);
expect(error.errors).toStrictEqual(mockResponse.errors);
expect(error.request.query).toEqual(query);
Expand All @@ -119,7 +162,7 @@ describe("errors", () => {

it("Should throw for server error", () => {
const query = `{
viewer {
viewer {
login
}
}`;
Expand Down

0 comments on commit b54bea0

Please sign in to comment.