Skip to content

Commit ff2f2c5

Browse files
committed
feat: enhance GitHub request utilities with error handling
1 parent fb421b4 commit ff2f2c5

File tree

1 file changed

+107
-30
lines changed

1 file changed

+107
-30
lines changed

src/github/common/utils.ts

Lines changed: 107 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,123 @@
1-
import fetch from "node-fetch";
1+
import { createGitHubError } from "./errors.js";
22

3-
if (!process.env.GITHUB_PERSONAL_ACCESS_TOKEN) {
4-
console.error("GITHUB_PERSONAL_ACCESS_TOKEN environment variable is not set");
5-
process.exit(1);
3+
type RequestOptions = {
4+
method?: string;
5+
body?: unknown;
6+
headers?: Record<string, string>;
7+
};
8+
9+
async function parseResponseBody(response: Response): Promise<unknown> {
10+
const contentType = response.headers.get("content-type");
11+
if (contentType?.includes("application/json")) {
12+
return response.json();
13+
}
14+
return response.text();
615
}
716

8-
export const GITHUB_PERSONAL_ACCESS_TOKEN = process.env.GITHUB_PERSONAL_ACCESS_TOKEN;
17+
export async function githubRequest(
18+
url: string,
19+
options: RequestOptions = {}
20+
): Promise<unknown> {
21+
const headers = {
22+
"Accept": "application/vnd.github.v3+json",
23+
"Content-Type": "application/json",
24+
...options.headers,
25+
};
926

10-
interface GitHubRequestOptions {
11-
method?: string;
12-
body?: any;
13-
}
27+
if (process.env.GITHUB_TOKEN) {
28+
headers["Authorization"] = `Bearer ${process.env.GITHUB_TOKEN}`;
29+
}
1430

15-
export async function githubRequest(url: string, options: GitHubRequestOptions = {}) {
1631
const response = await fetch(url, {
1732
method: options.method || "GET",
18-
headers: {
19-
Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
20-
Accept: "application/vnd.github.v3+json",
21-
"User-Agent": "github-mcp-server",
22-
...(options.body ? { "Content-Type": "application/json" } : {}),
23-
},
24-
...(options.body ? { body: JSON.stringify(options.body) } : {}),
33+
headers,
34+
body: options.body ? JSON.stringify(options.body) : undefined,
2535
});
2636

37+
const responseBody = await parseResponseBody(response);
38+
2739
if (!response.ok) {
28-
throw new Error(`GitHub API error: ${response.statusText}`);
40+
throw createGitHubError(response.status, responseBody);
2941
}
3042

31-
return response.json();
43+
return responseBody;
3244
}
3345

34-
export function buildUrl(baseUrl: string, params: Record<string, any> = {}) {
35-
const url = new URL(baseUrl);
36-
Object.entries(params).forEach(([key, value]) => {
37-
if (value !== undefined && value !== null) {
38-
if (Array.isArray(value)) {
39-
url.searchParams.append(key, value.join(","));
40-
} else {
41-
url.searchParams.append(key, value.toString());
42-
}
46+
export function validateBranchName(branch: string): string {
47+
const sanitized = branch.trim();
48+
if (!sanitized) {
49+
throw new Error("Branch name cannot be empty");
50+
}
51+
if (sanitized.includes("..")) {
52+
throw new Error("Branch name cannot contain '..'");
53+
}
54+
if (/[\s~^:?*[\\\]]/.test(sanitized)) {
55+
throw new Error("Branch name contains invalid characters");
56+
}
57+
if (sanitized.startsWith("/") || sanitized.endsWith("/")) {
58+
throw new Error("Branch name cannot start or end with '/'");
59+
}
60+
if (sanitized.endsWith(".lock")) {
61+
throw new Error("Branch name cannot end with '.lock'");
62+
}
63+
return sanitized;
64+
}
65+
66+
export function validateRepositoryName(name: string): string {
67+
const sanitized = name.trim().toLowerCase();
68+
if (!sanitized) {
69+
throw new Error("Repository name cannot be empty");
70+
}
71+
if (!/^[a-z0-9_.-]+$/.test(sanitized)) {
72+
throw new Error(
73+
"Repository name can only contain lowercase letters, numbers, hyphens, periods, and underscores"
74+
);
75+
}
76+
if (sanitized.startsWith(".") || sanitized.endsWith(".")) {
77+
throw new Error("Repository name cannot start or end with a period");
78+
}
79+
return sanitized;
80+
}
81+
82+
export function validateOwnerName(owner: string): string {
83+
const sanitized = owner.trim().toLowerCase();
84+
if (!sanitized) {
85+
throw new Error("Owner name cannot be empty");
86+
}
87+
if (!/^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,38}$/.test(sanitized)) {
88+
throw new Error(
89+
"Owner name must start with a letter or number and can contain up to 39 characters"
90+
);
91+
}
92+
return sanitized;
93+
}
94+
95+
export async function checkBranchExists(
96+
owner: string,
97+
repo: string,
98+
branch: string
99+
): Promise<boolean> {
100+
try {
101+
await githubRequest(
102+
`https://api.github.com/repos/${owner}/${repo}/branches/${branch}`
103+
);
104+
return true;
105+
} catch (error) {
106+
if (error && typeof error === "object" && "status" in error && error.status === 404) {
107+
return false;
43108
}
44-
});
45-
return url.toString();
109+
throw error;
110+
}
111+
}
112+
113+
export async function checkUserExists(username: string): Promise<boolean> {
114+
try {
115+
await githubRequest(`https://api.github.com/users/${username}`);
116+
return true;
117+
} catch (error) {
118+
if (error && typeof error === "object" && "status" in error && error.status === 404) {
119+
return false;
120+
}
121+
throw error;
122+
}
46123
}

0 commit comments

Comments
 (0)