From 4a1de73f5dc55748ae10287ffa5ccb3240b3fdf6 Mon Sep 17 00:00:00 2001 From: qer Date: Fri, 17 Jul 2026 12:44:21 +0800 Subject: [PATCH 1/2] fix(oauth): include the transport root cause in connection error messages --- packages/oauth/src/errors.ts | 8 ++++---- packages/oauth/src/oauth.ts | 20 +++++++++++++++++++- packages/oauth/test/oauth.test.ts | 21 +++++++++++++++++++++ 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/packages/oauth/src/errors.ts b/packages/oauth/src/errors.ts index bab0eccd8b..cba91a2ad5 100644 --- a/packages/oauth/src/errors.ts +++ b/packages/oauth/src/errors.ts @@ -16,8 +16,8 @@ */ export class OAuthError extends Error { - constructor(message: string) { - super(message); + constructor(message: string, options?: ErrorOptions) { + super(message, options); this.name = 'OAuthError'; } } @@ -30,8 +30,8 @@ export class OAuthUnauthorizedError extends OAuthError { } export class OAuthConnectionError extends OAuthError { - constructor(message: string) { - super(message); + constructor(message: string, options?: ErrorOptions) { + super(message, options); this.name = 'OAuthConnectionError'; } } diff --git a/packages/oauth/src/oauth.ts b/packages/oauth/src/oauth.ts index 64d86d86ac..f55d22c2e3 100644 --- a/packages/oauth/src/oauth.ts +++ b/packages/oauth/src/oauth.ts @@ -82,7 +82,8 @@ async function postForm( }); } catch (error) { throw new OAuthConnectionError( - `OAuth request to ${url} failed: ${error instanceof Error ? error.message : String(error)}`, + `OAuth request to ${url} failed: ${describeFetchFailure(error)}`, + { cause: error }, ); } const status = response.status; @@ -96,6 +97,23 @@ async function postForm( return { status, data }; } +/** + * Flatten a fetch rejection into a readable message. undici throws a generic + * `TypeError: fetch failed` and hides the real reason (DNS, refused, TLS, + * timeout) in a nested `cause` chain — walk it so the surfaced message names + * the actual failure instead of just "fetch failed". + */ +function describeFetchFailure(error: unknown): string { + if (!(error instanceof Error)) return String(error); + const messages = new Set(); + let current: Error | undefined = error; + while (current !== undefined) { + messages.add(current.message); + current = current.cause instanceof Error ? current.cause : undefined; + } + return [...messages].join(': '); +} + // ── requestDeviceAuthorization ──────────────────────────────────────── export async function requestDeviceAuthorization( diff --git a/packages/oauth/test/oauth.test.ts b/packages/oauth/test/oauth.test.ts index 15a2c5cd41..2ea145631e 100644 --- a/packages/oauth/test/oauth.test.ts +++ b/packages/oauth/test/oauth.test.ts @@ -575,6 +575,27 @@ describe('refreshAccessToken', () => { ).rejects.toBeInstanceOf(OAuthConnectionError); }); + it('names the transport root cause in the connection error message', async () => { + const badConfig: OAuthFlowConfig = { + ...flowConfig(), + oauthHost: 'http://127.0.0.1:1', // reserved port, ECONNREFUSED + }; + + const failure = await refreshToken(badConfig, 'rt', { + maxRetries: 1, + backoffMs: () => 0, + }).catch((error: unknown) => error); + + expect(failure).toBeInstanceOf(OAuthConnectionError); + const error = failure as OAuthConnectionError; + expect(error.cause).toBeInstanceOf(Error); + // The undici root cause (e.g. `connect ECONNREFUSED 127.0.0.1:1`) must be + // visible in the surfaced message, not just the generic "fetch failed". + const rootCause = error.cause instanceof Error ? error.cause : undefined; + expect(rootCause?.message).toBeTruthy(); + expect(error.message).toContain(rootCause?.message); + }); + it('sends grant_type=refresh_token + refresh_token', async () => { server.enqueue('/api/oauth/token', { status: 200, From 37082dbdc8703b74166178d8659e909921de6f51 Mon Sep 17 00:00:00 2001 From: qer Date: Fri, 17 Jul 2026 12:44:30 +0800 Subject: [PATCH 2/2] chore: add changeset for oauth connection error diagnostics --- .changeset/oauth-connection-error-cause.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/oauth-connection-error-cause.md diff --git a/.changeset/oauth-connection-error-cause.md b/.changeset/oauth-connection-error-cause.md new file mode 100644 index 0000000000..6765491599 --- /dev/null +++ b/.changeset/oauth-connection-error-cause.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Include the underlying network cause (DNS failure, refused connection, TLS or timeout errors) in OAuth connection error messages instead of a bare "fetch failed".