Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion packages/core-internal/src/errors/sdkErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,12 +149,25 @@ export class SdkError extends Error {
message: string,
public readonly data?: unknown
) {
super(message);
// If a call site passes an underlying error via the `data` slot as
// `{ cause }`, forward it to `Error` so the standard `.cause` chain
// stays unbroken — loggers and error trackers (pino, Sentry, …) walk
// `.cause` to reach the root cause. The full `data` object is still
// retained on `this.data`. See modelcontextprotocol/typescript-sdk#2657.
super(message, dataHasCause(data) ? { cause: data.cause } : undefined);
this.name = 'SdkError';
stampErrorBrands(this, new.target);
}
}

/**
* Narrow an {@linkcode SdkError.data | data} value to one that carries a
* `cause`, so it can be forwarded to the `Error` constructor's `ErrorOptions`.
*/
function dataHasCause(data: unknown): data is { cause: unknown } {
return typeof data === 'object' && data !== null && 'cause' in data;
}

/**
* Typed shape for HTTP error data carried by {@linkcode SdkHttpError}.
*/
Expand Down
48 changes: 48 additions & 0 deletions packages/core-internal/test/errors/sdkErrorCause.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { describe, it, expect } from 'vitest';
import { SdkError, SdkErrorCode, SdkHttpError } from '../../src/index';

describe('SdkError cause forwarding (#2657)', () => {
it('forwards a `cause` from the data slot to Error.cause', () => {
const root = new Error('getaddrinfo ENOTFOUND does-not-resolve.invalid');
const error = new SdkError(SdkErrorCode.EraNegotiationFailed, 'Version negotiation probe failed: fetch failed', {
cause: root
});

// The standard `.cause` chain must reach the underlying error, so
// pino/Sentry-style walkers surface the real failure.
expect(error.cause).toBe(root);
});

it('still keeps the full object on `data` (does not move it out)', () => {
const root = new Error('boom');
const error = new SdkError(SdkErrorCode.EraNegotiationFailed, 'msg', { cause: root, extra: 1 });

expect(error.cause).toBe(root);
expect(error.data).toEqual({ cause: root, extra: 1 });
});

it('leaves cause undefined when data carries none', () => {
const error = new SdkError(SdkErrorCode.NotConnected, 'Transport is not connected', {
status: 401
});

expect(error.cause).toBeUndefined();
expect(error.data).toEqual({ status: 401 });
});

it('leaves cause undefined when there is no data', () => {
const error = new SdkError(SdkErrorCode.NotConnected, 'Transport is not connected');

expect(error.cause).toBeUndefined();
});

it('does not treat an HTTP data payload as a cause', () => {
const error = new SdkHttpError(SdkErrorCode.ClientHttpAuthentication, 'Unauthorized', {
status: 401,
statusText: 'Unauthorized'
});

expect(error.cause).toBeUndefined();
expect(error.status).toBe(401);
});
});
Loading