From ca5c45d0fbfda0fce266151a9b4360e70d5b5529 Mon Sep 17 00:00:00 2001 From: Bahman Asheghi Date: Fri, 19 Jun 2026 18:36:18 +0200 Subject: [PATCH] Improve Copilot token error diagnostics --- src/lib/error.ts | 19 +++++++++++- src/lib/token.ts | 39 ++++++++++++++++++++---- tests/http-error.test.ts | 65 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+), 7 deletions(-) create mode 100644 tests/http-error.test.ts diff --git a/src/lib/error.ts b/src/lib/error.ts index c39c22596..82a72468b 100644 --- a/src/lib/error.ts +++ b/src/lib/error.ts @@ -7,11 +7,28 @@ export class HTTPError extends Error { response: Response constructor(message: string, response: Response) { - super(message) + super(`${message} (${response.status} ${response.statusText})`) this.response = response } } +export async function getHTTPErrorDetails(error: HTTPError): Promise { + const errorText = await error.response.clone().text() + + if (!errorText) { + return { + status: error.response.status, + statusText: error.response.statusText, + } + } + + try { + return JSON.parse(errorText) as unknown + } catch { + return errorText + } +} + export async function forwardError(c: Context, error: unknown) { consola.error("Error occurred:", error) diff --git a/src/lib/token.ts b/src/lib/token.ts index fc8d2785f..a56aa89e7 100644 --- a/src/lib/token.ts +++ b/src/lib/token.ts @@ -7,7 +7,7 @@ import { getDeviceCode } from "~/services/github/get-device-code" import { getGitHubUser } from "~/services/github/get-user" import { pollAccessToken } from "~/services/github/poll-access-token" -import { HTTPError } from "./error" +import { HTTPError, getHTTPErrorDetails } from "./error" import { state } from "./state" const readGithubToken = () => fs.readFile(PATHS.GITHUB_TOKEN_PATH, "utf8") @@ -16,16 +16,32 @@ const writeGithubToken = (token: string) => fs.writeFile(PATHS.GITHUB_TOKEN_PATH, token) export const setupCopilotToken = async () => { - const { token, refresh_in } = await getCopilotToken() - state.copilotToken = token + let refreshIn: number + + try { + const { token, refresh_in } = await getCopilotToken() + state.copilotToken = token + refreshIn = refresh_in + } catch (error) { + if (error instanceof HTTPError) { + consola.error( + "Failed to get Copilot token:", + await getHTTPErrorDetails(error), + ) + throw error + } + + consola.error("Failed to get Copilot token:", error) + throw error + } // Display the Copilot token to the screen consola.debug("GitHub Copilot Token fetched successfully!") if (state.showToken) { - consola.info("Copilot token:", token) + consola.info("Copilot token:", state.copilotToken) } - const refreshInterval = (refresh_in - 60) * 1000 + const refreshInterval = (refreshIn - 60) * 1000 setInterval(async () => { consola.debug("Refreshing Copilot token") try { @@ -36,6 +52,14 @@ export const setupCopilotToken = async () => { consola.info("Refreshed Copilot token:", token) } } catch (error) { + if (error instanceof HTTPError) { + consola.error( + "Failed to refresh Copilot token:", + await getHTTPErrorDetails(error), + ) + throw error + } + consola.error("Failed to refresh Copilot token:", error) throw error } @@ -80,7 +104,10 @@ export async function setupGitHubToken( await logUser() } catch (error) { if (error instanceof HTTPError) { - consola.error("Failed to get GitHub token:", await error.response.json()) + consola.error( + "Failed to get GitHub token:", + await getHTTPErrorDetails(error), + ) throw error } diff --git a/tests/http-error.test.ts b/tests/http-error.test.ts new file mode 100644 index 000000000..1072e90fa --- /dev/null +++ b/tests/http-error.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, test } from "bun:test" + +import { HTTPError, getHTTPErrorDetails } from "~/lib/error" + +describe("HTTPError", () => { + test("includes response status in the error message", () => { + const error = new HTTPError( + "Failed to get Copilot token", + new Response("Forbidden", { status: 403, statusText: "Forbidden" }), + ) + + expect(error.message).toBe("Failed to get Copilot token (403 Forbidden)") + }) +}) + +describe("getHTTPErrorDetails", () => { + test("parses JSON response bodies", async () => { + const details = await getHTTPErrorDetails( + new HTTPError( + "Failed to get Copilot token", + Response.json( + { + error_details: { + message: "Your subscription has ended.", + notification_id: "subscription_ended", + }, + }, + { status: 403, statusText: "Forbidden" }, + ), + ), + ) + + expect(details).toEqual({ + error_details: { + message: "Your subscription has ended.", + notification_id: "subscription_ended", + }, + }) + }) + + test("returns text response bodies", async () => { + const details = await getHTTPErrorDetails( + new HTTPError( + "Failed to get Copilot token", + new Response("Forbidden", { status: 403, statusText: "Forbidden" }), + ), + ) + + expect(details).toBe("Forbidden") + }) + + test("returns status details for empty response bodies", async () => { + const details = await getHTTPErrorDetails( + new HTTPError( + "Failed to get Copilot token", + new Response(null, { status: 403, statusText: "Forbidden" }), + ), + ) + + expect(details).toEqual({ + status: 403, + statusText: "Forbidden", + }) + }) +})