Skip to content

Commit f8ef9dc

Browse files
committed
Add robust error handling and input validation for issue API
1 parent 497faf2 commit f8ef9dc

File tree

1 file changed

+232
-36
lines changed

1 file changed

+232
-36
lines changed

src/issue.ts

Lines changed: 232 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ type GithubEnv = {
1111
TURNSTILE_SECRET_KEY: string;
1212
};
1313

14+
type ErrorResponse = {
15+
error: string;
16+
code?: string;
17+
details?: string;
18+
};
19+
1420
type GithubIssue = {
1521
url: string;
1622
repository_url: string;
@@ -169,35 +175,129 @@ const app = new Hono()
169175
TURNSTILE_SECRET_KEY,
170176
);
171177
if (!isValid) {
172-
return c.json({ error: "Invalid Turnstile verification" }, 400);
178+
return c.json(
179+
{
180+
error: "Invalid Turnstile verification",
181+
code: "TURNSTILE_VERIFICATION_FAILED",
182+
details: "Please refresh the page and try again",
183+
} as ErrorResponse,
184+
400,
185+
);
173186
}
174187
}
175188

189+
// Validate input
190+
if (!title || title.length < 7) {
191+
return c.json(
192+
{
193+
error: "Title is required and must be at least 7 characters long",
194+
code: "INVALID_TITLE",
195+
} as ErrorResponse,
196+
400,
197+
);
198+
}
199+
200+
if (!body || body.trim().length < 10) {
201+
return c.json(
202+
{
203+
error:
204+
"Description is required and must be at least 10 characters long",
205+
code: "INVALID_DESCRIPTION",
206+
} as ErrorResponse,
207+
400,
208+
);
209+
}
210+
176211
// base64 encoded private key to utf8, not using buffer
177212
const privateKey = atob(GITHUB_APP_PRIVATE_KEY);
178213

179-
const accessToken = await getInstallationAccessToken(
180-
GITHUB_CLIENT_ID,
181-
privateKey,
182-
GITHUB_INSTALLATION_ID,
183-
);
214+
let accessToken: string;
215+
try {
216+
accessToken = await getInstallationAccessToken(
217+
GITHUB_CLIENT_ID,
218+
privateKey,
219+
GITHUB_INSTALLATION_ID,
220+
);
221+
} catch (error) {
222+
console.error("Failed to get GitHub access token:", error);
223+
return c.json(
224+
{
225+
error: "Failed to authenticate with GitHub",
226+
code: "GITHUB_AUTH_FAILED",
227+
details: "Please try again later",
228+
} as ErrorResponse,
229+
500,
230+
);
231+
}
184232
const repoOwner = "nthumodifications";
185233
const repoName = "courseweb";
186234

187-
const response = await fetch(
188-
`https://api.github.com/repos/${repoOwner}/${repoName}/issues`,
189-
{
190-
method: "POST",
191-
headers: {
192-
"User-Agent": "nthumods-app",
193-
Authorization: `token ${accessToken}`,
194-
Accept: "application/vnd.github.v3+json",
235+
try {
236+
const response = await fetch(
237+
`https://api.github.com/repos/${repoOwner}/${repoName}/issues`,
238+
{
239+
method: "POST",
240+
headers: {
241+
"User-Agent": "nthumods-app",
242+
Authorization: `token ${accessToken}`,
243+
Accept: "application/vnd.github.v3+json",
244+
},
245+
body: JSON.stringify({ title, body, labels }),
195246
},
196-
body: JSON.stringify({ title, body, labels }),
197-
},
198-
);
199-
const data = (await response.json()) as GithubIssue;
200-
return c.json(data);
247+
);
248+
249+
if (!response.ok) {
250+
const errorText = await response.text();
251+
console.error(`GitHub API error: ${response.status} ${errorText}`);
252+
253+
let errorResponse: ErrorResponse;
254+
255+
switch (response.status) {
256+
case 401:
257+
errorResponse = {
258+
error: "Authentication failed with GitHub",
259+
code: "GITHUB_AUTH_INVALID",
260+
details: "Please refresh and try again",
261+
};
262+
break;
263+
case 403:
264+
errorResponse = {
265+
error: "Access denied by GitHub",
266+
code: "GITHUB_ACCESS_DENIED",
267+
details: "Rate limit exceeded or insufficient permissions",
268+
};
269+
break;
270+
case 422:
271+
errorResponse = {
272+
error: "Invalid issue data",
273+
code: "GITHUB_VALIDATION_ERROR",
274+
details: "The issue data was rejected by GitHub",
275+
};
276+
break;
277+
default:
278+
errorResponse = {
279+
error: "Failed to create GitHub issue",
280+
code: "GITHUB_API_ERROR",
281+
details: `HTTP ${response.status}: ${response.statusText}`,
282+
};
283+
}
284+
285+
return c.json(errorResponse, response.status as any);
286+
}
287+
288+
const data = (await response.json()) as GithubIssue;
289+
return c.json(data);
290+
} catch (error) {
291+
console.error("Error creating GitHub issue:", error);
292+
return c.json(
293+
{
294+
error: "Failed to submit issue",
295+
code: "NETWORK_ERROR",
296+
details: "Please check your connection and try again",
297+
} as ErrorResponse,
298+
500,
299+
);
300+
}
201301
},
202302
)
203303
.get(
@@ -210,34 +310,130 @@ const app = new Hono()
210310
),
211311
async (c) => {
212312
const { tag } = c.req.valid("query");
313+
314+
// Validate tag parameter
315+
if (!tag || tag.trim().length === 0) {
316+
return c.json(
317+
{
318+
error: "Tag parameter is required",
319+
code: "MISSING_TAG",
320+
} as ErrorResponse,
321+
400,
322+
);
323+
}
324+
213325
const {
214326
GITHUB_CLIENT_ID,
215327
GITHUB_APP_PRIVATE_KEY,
216328
GITHUB_INSTALLATION_ID,
217329
} = env<GithubEnv>(c);
218330
const privateKey = atob(GITHUB_APP_PRIVATE_KEY);
219331

220-
const accessToken = await getInstallationAccessToken(
221-
GITHUB_CLIENT_ID,
222-
privateKey,
223-
GITHUB_INSTALLATION_ID,
224-
);
332+
let accessToken: string;
333+
try {
334+
accessToken = await getInstallationAccessToken(
335+
GITHUB_CLIENT_ID,
336+
privateKey,
337+
GITHUB_INSTALLATION_ID,
338+
);
339+
} catch (error) {
340+
console.error("Failed to get GitHub access token for GET:", error);
341+
return c.json(
342+
{
343+
error: "Failed to authenticate with GitHub",
344+
code: "GITHUB_AUTH_FAILED",
345+
details: "Please try again later",
346+
} as ErrorResponse,
347+
500,
348+
);
349+
}
350+
225351
const repoOwner = "nthumodifications";
226352
const repoName = "courseweb";
227353

228-
const response = await fetch(
229-
`https://api.github.com/repos/${repoOwner}/${repoName}/issues?filter=all&labels=${tag}&state=open`,
230-
{
231-
method: "GET",
232-
headers: {
233-
"User-Agent": "nthumods-app",
234-
Authorization: `token ${accessToken}`,
235-
Accept: "application/vnd.github.v3+json",
354+
try {
355+
const response = await fetch(
356+
`https://api.github.com/repos/${repoOwner}/${repoName}/issues?filter=all&labels=${encodeURIComponent(tag)}&state=open`,
357+
{
358+
method: "GET",
359+
headers: {
360+
"User-Agent": "nthumods-app",
361+
Authorization: `token ${accessToken}`,
362+
Accept: "application/vnd.github.v3+json",
363+
},
236364
},
237-
},
238-
);
239-
const data = (await response.json()) as GithubIssue[];
240-
return c.json(data);
365+
);
366+
367+
if (!response.ok) {
368+
const errorText = await response.text();
369+
console.error(
370+
`GitHub API GET error: ${response.status} ${errorText}`,
371+
);
372+
373+
let errorResponse: ErrorResponse;
374+
375+
switch (response.status) {
376+
case 401:
377+
errorResponse = {
378+
error: "Authentication failed with GitHub",
379+
code: "GITHUB_AUTH_INVALID",
380+
details: "Please refresh and try again",
381+
};
382+
break;
383+
case 403:
384+
errorResponse = {
385+
error: "Access denied by GitHub",
386+
code: "GITHUB_ACCESS_DENIED",
387+
details: "Rate limit exceeded or insufficient permissions",
388+
};
389+
break;
390+
case 404:
391+
errorResponse = {
392+
error: "Repository not found",
393+
code: "GITHUB_REPO_NOT_FOUND",
394+
details: "The repository may have been moved or deleted",
395+
};
396+
break;
397+
default:
398+
errorResponse = {
399+
error: "Failed to fetch issues from GitHub",
400+
code: "GITHUB_API_ERROR",
401+
details: `HTTP ${response.status}: ${response.statusText}`,
402+
};
403+
}
404+
405+
return c.json(
406+
errorResponse,
407+
(response.status >= 500 ? 500 : response.status) as any,
408+
);
409+
}
410+
411+
const data = (await response.json()) as GithubIssue[];
412+
413+
// Validate response data
414+
if (!Array.isArray(data)) {
415+
return c.json(
416+
{
417+
error: "Invalid response from GitHub",
418+
code: "INVALID_GITHUB_RESPONSE",
419+
details: "Expected an array of issues",
420+
} as ErrorResponse,
421+
502,
422+
);
423+
}
424+
425+
return c.json(data);
426+
} catch (error) {
427+
console.error("Error fetching GitHub issues:", error);
428+
return c.json(
429+
{
430+
error: "Failed to fetch issues",
431+
code: "NETWORK_ERROR",
432+
details: "Please check your connection and try again",
433+
} as ErrorResponse,
434+
500,
435+
);
436+
}
241437
},
242438
);
243439

0 commit comments

Comments
 (0)