Skip to content
Merged
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
76 changes: 76 additions & 0 deletions src/core/http/HttpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
private readonly logger: Logger | undefined;
private retryConfig: HttpClientRetryConfig = { maxRetries: 3, delayMs: 6000 };

// Error message formatting constants
private static readonly CAUSE_TRUNCATE_LENGTH = 200;
private static readonly CONTEXT_VALUE_TRUNCATE_LENGTH = 50;
private static readonly MAX_CONTEXT_KEYS = 3;

constructor(logger?: Logger) {
this.axiosInstance = axios.create();
this.logger = logger;
Expand Down Expand Up @@ -185,20 +190,91 @@
const data = (error.response?.data ?? {}) as Record<string, unknown>;
const code = typeof data['code'] === 'string' ? data['code'] : undefined;
const message = typeof data['message'] === 'string' ? data['message'] : undefined;
const cause = typeof data['cause'] === 'string' ? data['cause'] : undefined;
// Only accept plain objects for context, not arrays or null
const context =
typeof data['context'] === 'object' && data['context'] !== null && !Array.isArray(data['context'])
? data['context']
: undefined;

// Build error message with all available details
let msg = `HTTP ${status}`;
if (code) {
msg += `: ${code}`;
}
if (message) {
msg += ` - ${message}`;
}
// Include cause for DAML/Canton errors - this is the actionable info
if (cause) {
const truncatedCause =
cause.length > HttpClient.CAUSE_TRUNCATE_LENGTH
? `${cause.substring(0, HttpClient.CAUSE_TRUNCATE_LENGTH)}...`
: cause;
msg += ` (cause: ${truncatedCause})`;
}
// Include context summary if available
if (context) {
const contextSummary = this.formatContextSummary(context as Record<string, unknown>);
if (contextSummary) {
msg += ` [context: ${contextSummary}]`;
}
}

const err = new ApiError(msg, status, error.response?.statusText);
err.response = data;
return err;
}
return new NetworkError(`Request failed: ${error instanceof Error ? error.message : String(error)}`);
}

/**
* Formats a context object into a summary string for error messages. Shows up to MAX_CONTEXT_KEYS keys with truncated
* values.
*/
private formatContextSummary(contextObj: Record<string, unknown>): string | undefined {
const contextKeys = Object.keys(contextObj);
if (contextKeys.length === 0) {
return undefined;
}

return contextKeys
.slice(0, HttpClient.MAX_CONTEXT_KEYS)
.map((k) => {
const v = contextObj[k];
const vStr = this.stringifyContextValue(v);
const truncatedValue =
vStr.length > HttpClient.CONTEXT_VALUE_TRUNCATE_LENGTH
? `${vStr.substring(0, HttpClient.CONTEXT_VALUE_TRUNCATE_LENGTH)}...`
: vStr;
return `${k}=${truncatedValue}`;
})
.join(', ');
}

/**
* Safely converts a context value to a string representation. Handles null, undefined, strings, and objects (with
* circular reference protection).
*/
private stringifyContextValue(v: unknown): string {
if (typeof v === 'string') {
return v;
}
if (v === null) {
return 'null';
}
if (v === undefined) {
return 'undefined';
}
try {
// JSON.stringify can return undefined for functions, Symbols, or objects with toJSON() returning undefined
return JSON.stringify(v) ?? '[Object]';

Check warning on line 271 in src/core/http/HttpClient.ts

View workflow job for this annotation

GitHub Actions / test-cn-quickstart

Unnecessary conditional, expected left-hand side of `??` operator to be possibly null or undefined
} catch {
// Handle circular references, BigInt, or other non-serializable values
return '[Object]';
}
}

/**
* Determines whether a request error is retryable. Retries on:
*
Expand Down
Loading
Loading