fix(client): surface underlying network error via Error.cause on probe failures (#2657) (#2726)

Co-authored-by: Konstantin Konstantinov <konstantin@mach5technology.com>
Co-authored-by: Konstantin Konstantinov <KKonstantinov@users.noreply.github.com>
This commit is contained in:
LuckTerence 2026-09-03 22:32:08 +08:00 • committed by GitHub
commit 6fa42279fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 97 additions and 7 deletions

View file

@ -283,6 +283,9 @@ describe('stamped-seam fault injection (identity-preserving auth outcomes, never
expect(out.settled).toBe('rejected');
expect(out.error).toBeInstanceOf(SdkError);
expect((out.error as SdkError).code).toBe(SdkErrorCode.EraNegotiationFailed);
// The failure rides the standard cause chain (#2657); the legacy data.cause
// slot is kept populated for compatibility until it is removed.
expect((out.error as SdkError).cause).toBe(netError);
expect(((out.error as SdkError).data as { cause?: unknown }).cause).toBe(netError);
});
});

View file

@ -270,6 +270,25 @@ describe('row: network outage → typed connect error (Node)', () => {
const verdict = classify({ kind: 'network-error', error: new TypeError('fetch failed') }, { environment: 'node' });
expect(verdict.kind).toBe('error');
});
test('the underlying network error is reachable via Error.cause (#2657)', () => {
// Node's fetch wraps the socket/DNS failure: `TypeError: fetch failed` with
// the error that actually names the failure (ENOTFOUND / ECONNREFUSED /
// ETIMEDOUT) on its own `cause`.
const dnsError = Object.assign(new Error('getaddrinfo ENOTFOUND unreachable.invalid'), { code: 'ENOTFOUND' });
const fetchError = new TypeError('fetch failed', { cause: dnsError });
const verdict = classify({ kind: 'network-error', error: fetchError });
expect(verdict.kind).toBe('error');
if (verdict.kind === 'error') {
// Walking `.cause` (what loggers and error reporters do) must reach the
// error that names the failure instead of dead-ending on the SdkError.
expect(verdict.error.cause).toBe(fetchError);
expect((verdict.error.cause as Error).cause).toBe(dnsError);
// The legacy data.cause slot stays populated too (kept for compatibility,
// slated for removal).
expect(((verdict.error as SdkError).data as { cause?: unknown }).cause).toBe(fetchError);
}
});
});
describe('row: timeout — transport-aware verdict', () => {