Skip to content

Commit 82c2952

Browse files
committed
Add custom error handler for missing default branch
When the GitHub API returns an error for a missing default branch, we will now show a custom error message. This custom error message includes a link to the page to create the branch. The error is detected using the `errors` field on the response that is now being returned.
1 parent abde8f3 commit 82c2952

File tree

3 files changed

+261
-4
lines changed

3 files changed

+261
-4
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { RequestError } from "@octokit/request-error";
2+
import { NotificationLogger, showAndLogErrorMessage } from "../common/logging";
3+
4+
type ApiError = {
5+
resource: string;
6+
field: string;
7+
code: string;
8+
};
9+
10+
type ErrorResponse = {
11+
message: string;
12+
errors?: ApiError[];
13+
};
14+
15+
export function handleRequestError(
16+
e: RequestError,
17+
logger: NotificationLogger,
18+
): boolean {
19+
if (e.status !== 422) {
20+
return false;
21+
}
22+
23+
if (!e.response?.data) {
24+
return false;
25+
}
26+
27+
const data = e.response.data;
28+
if (!isErrorResponse(data)) {
29+
return false;
30+
}
31+
32+
if (!data.errors) {
33+
return false;
34+
}
35+
36+
// This is the only custom error message we have
37+
const missingDefaultBranchError = data.errors.find(
38+
(error) =>
39+
error.resource === "Repository" &&
40+
error.field === "default_branch" &&
41+
error.code === "missing",
42+
);
43+
44+
if (!missingDefaultBranchError) {
45+
return false;
46+
}
47+
48+
if (
49+
!("repository" in missingDefaultBranchError) ||
50+
typeof missingDefaultBranchError.repository !== "string"
51+
) {
52+
return false;
53+
}
54+
55+
if (
56+
!("default_branch" in missingDefaultBranchError) ||
57+
typeof missingDefaultBranchError.default_branch !== "string"
58+
) {
59+
return false;
60+
}
61+
62+
const createBranchURL = `https://github.com/${
63+
missingDefaultBranchError.repository
64+
}/new/${encodeURIComponent(missingDefaultBranchError.default_branch)}`;
65+
66+
void showAndLogErrorMessage(
67+
logger,
68+
`Variant analysis failed because the controller repository ${missingDefaultBranchError.repository} does not have a branch '${missingDefaultBranchError.default_branch}'. ` +
69+
`Please create a '${missingDefaultBranchError.default_branch}' branch by clicking [here](${createBranchURL}) and re-run the variant analysis query.`,
70+
{
71+
fullMessage: e.message,
72+
},
73+
);
74+
75+
return true;
76+
}
77+
78+
function isErrorResponse(obj: unknown): obj is ErrorResponse {
79+
return (
80+
typeof obj === "object" &&
81+
obj !== null &&
82+
"message" in obj &&
83+
typeof obj.message === "string"
84+
);
85+
}

extensions/ql-vscode/src/variant-analysis/variant-analysis-manager.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
submitVariantAnalysis,
55
getVariantAnalysisRepo,
66
} from "./gh-api/gh-api-client";
7+
import { VariantAnalysis as ApiVariantAnalysis } from "./gh-api/variant-analysis";
78
import {
89
authentication,
910
AuthenticationSessionsChangeEvent,
@@ -76,6 +77,8 @@ import {
7677
showAndLogWarningMessage,
7778
} from "../common/logging";
7879
import { QueryTreeViewItem } from "../queries-panel/query-tree-view-item";
80+
import { RequestError } from "@octokit/request-error";
81+
import { handleRequestError } from "./custom-errors";
7982

8083
const maxRetryCount = 3;
8184

@@ -254,10 +257,20 @@ export class VariantAnalysisManager
254257
},
255258
};
256259

257-
const variantAnalysisResponse = await submitVariantAnalysis(
258-
this.app.credentials,
259-
variantAnalysisSubmission,
260-
);
260+
let variantAnalysisResponse: ApiVariantAnalysis;
261+
try {
262+
variantAnalysisResponse = await submitVariantAnalysis(
263+
this.app.credentials,
264+
variantAnalysisSubmission,
265+
);
266+
} catch (e: unknown) {
267+
// If the error is handled by the handleRequestError function, we don't need to throw
268+
if (e instanceof RequestError && handleRequestError(e, this.app.logger)) {
269+
return;
270+
}
271+
272+
throw e;
273+
}
261274

262275
const processedVariantAnalysis = processVariantAnalysis(
263276
variantAnalysisSubmission,
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { RequestError } from "@octokit/request-error";
2+
import { createMockLogger } from "../../__mocks__/loggerMock";
3+
import { handleRequestError } from "../../../src/variant-analysis/custom-errors";
4+
import { faker } from "@faker-js/faker";
5+
6+
describe("handleRequestError", () => {
7+
const logger = createMockLogger();
8+
9+
it("returns false when handling a non-422 error", () => {
10+
const e = mockRequestError(404, {
11+
message: "Not Found",
12+
});
13+
expect(handleRequestError(e, logger)).toBe(false);
14+
expect(logger.showErrorMessage).not.toHaveBeenCalled();
15+
});
16+
17+
it("returns false when handling a different error without errors", () => {
18+
const e = mockRequestError(422, {
19+
message:
20+
"Unable to trigger a variant analysis. None of the requested repositories could be found.",
21+
});
22+
expect(handleRequestError(e, logger)).toBe(false);
23+
expect(logger.showErrorMessage).not.toHaveBeenCalled();
24+
});
25+
26+
it("returns false when handling an error without response body", () => {
27+
const e = mockRequestError(422, undefined);
28+
expect(handleRequestError(e, logger)).toBe(false);
29+
expect(logger.showErrorMessage).not.toHaveBeenCalled();
30+
});
31+
32+
it("returns false when handling an error without response", () => {
33+
const e = new RequestError("Timeout", 500, {
34+
headers: {
35+
"Content-Type": "application/json",
36+
},
37+
request: {
38+
method: "POST",
39+
url: faker.internet.url(),
40+
headers: {
41+
"Content-Type": "application/json",
42+
},
43+
},
44+
});
45+
expect(handleRequestError(e, logger)).toBe(false);
46+
expect(logger.showErrorMessage).not.toHaveBeenCalled();
47+
});
48+
49+
it("returns false when handling a different error with errors", () => {
50+
const e = mockRequestError(422, {
51+
message:
52+
"Unable to trigger a variant analysis. None of the requested repositories could be found.",
53+
errors: [
54+
{
55+
resource: "VariantAnalysis",
56+
field: "repositories",
57+
code: "not_found",
58+
},
59+
],
60+
});
61+
expect(handleRequestError(e, logger)).toBe(false);
62+
expect(logger.showErrorMessage).not.toHaveBeenCalled();
63+
});
64+
65+
it("returns false when handling without repository field", () => {
66+
const e = mockRequestError(422, {
67+
message:
68+
"Variant analysis failed because controller repository github/pickles does not have a branch 'main'. Please create a 'main' branch in the repository and re-run the variant analysis.",
69+
errors: [
70+
{
71+
resource: "Repository",
72+
field: "default_branch",
73+
code: "missing",
74+
default_branch: "main",
75+
},
76+
],
77+
});
78+
expect(handleRequestError(e, logger)).toBe(false);
79+
expect(logger.showErrorMessage).not.toHaveBeenCalled();
80+
});
81+
82+
it("returns false when handling without default_branch field", () => {
83+
const e = mockRequestError(422, {
84+
message:
85+
"Variant analysis failed because controller repository github/pickles does not have a branch 'main'. Please create a 'main' branch in the repository and re-run the variant analysis.",
86+
errors: [
87+
{
88+
resource: "Repository",
89+
field: "default_branch",
90+
code: "missing",
91+
repository: "github/pickles",
92+
},
93+
],
94+
});
95+
expect(handleRequestError(e, logger)).toBe(false);
96+
expect(logger.showErrorMessage).not.toHaveBeenCalled();
97+
});
98+
99+
it("shows notification when handling a missing default branch error", () => {
100+
const e = mockRequestError(422, {
101+
message:
102+
"Variant analysis failed because controller repository github/pickles does not have a branch 'main'. Please create a 'main' branch in the repository and re-run the variant analysis.",
103+
errors: [
104+
{
105+
resource: "Repository",
106+
field: "default_branch",
107+
code: "missing",
108+
repository: "github/pickles",
109+
default_branch: "main",
110+
},
111+
],
112+
});
113+
expect(handleRequestError(e, logger)).toBe(true);
114+
expect(logger.showErrorMessage).toHaveBeenCalledWith(
115+
"Variant analysis failed because the controller repository github/pickles does not have a branch 'main'. Please create a 'main' branch by clicking [here](https://github.com/github/pickles/new/main) and re-run the variant analysis query.",
116+
);
117+
});
118+
});
119+
120+
function mockRequestError(status: number, body: any): RequestError {
121+
return new RequestError(
122+
body ? toErrorMessage(body) : "Unknown error",
123+
status,
124+
{
125+
request: {
126+
method: "POST",
127+
url: faker.internet.url(),
128+
headers: {
129+
"Content-Type": "application/json",
130+
},
131+
},
132+
response: {
133+
url: faker.internet.url(),
134+
status,
135+
headers: {
136+
"Content-Type": "application/json",
137+
},
138+
data: body,
139+
},
140+
},
141+
);
142+
}
143+
144+
// Copied from https://github.com/octokit/request.js/blob/c67f902350384846f88d91196e7066daadc08357/src/fetch-wrapper.ts#L166 to have a
145+
// somewhat realistic error message
146+
function toErrorMessage(data: any) {
147+
if (typeof data === "string") return data;
148+
149+
if ("message" in data) {
150+
if (Array.isArray(data.errors)) {
151+
return `${data.message}: ${data.errors.map(JSON.stringify).join(", ")}`;
152+
}
153+
154+
return data.message;
155+
}
156+
157+
// istanbul ignore next - just in case
158+
return `Unknown error: ${JSON.stringify(data)}`;
159+
}

0 commit comments

Comments
 (0)