mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(post card): clarify full error copying
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { formatErrorForDisplay } from '../error-utils';
|
||||
import { formatErrorForDisplay, serializeErrorForClipboard } from '../error-utils';
|
||||
|
||||
describe('error utils', () => {
|
||||
it('returns plain string errors unchanged', () => {
|
||||
@@ -29,4 +29,55 @@ describe('error utils', () => {
|
||||
}),
|
||||
).toBe('publish failed: provider: plebpubsub; reason: timeout');
|
||||
});
|
||||
|
||||
it('formats nested Error details without dropping the nested message', () => {
|
||||
expect(
|
||||
formatErrorForDisplay({
|
||||
details: {
|
||||
reason: new Error('provider timeout'),
|
||||
},
|
||||
message: 'publish failed',
|
||||
}),
|
||||
).toBe('publish failed: reason: provider timeout');
|
||||
});
|
||||
|
||||
it('formats cyclic errors without recursing forever', () => {
|
||||
const error = Object.assign(new Error('publish failed'), {
|
||||
details: {
|
||||
elapsedMs: BigInt(5000),
|
||||
},
|
||||
});
|
||||
Object.assign(error.details, { self: error.details });
|
||||
|
||||
expect(formatErrorForDisplay(error)).toBe('publish failed: elapsedMs: 5000; self: [Circular]');
|
||||
});
|
||||
|
||||
it('serializes cyclic errors as valid JSON for copying', () => {
|
||||
const cause = new Error('provider timeout');
|
||||
const error = Object.assign(new Error('publish failed'), {
|
||||
attempts: [{ elapsedMs: BigInt(5000), provider: 'pubsub', reason: cause }],
|
||||
});
|
||||
Object.assign(error, { self: error });
|
||||
|
||||
expect(JSON.parse(serializeErrorForClipboard(error))).toMatchObject({
|
||||
attempts: [
|
||||
{
|
||||
elapsedMs: '5000',
|
||||
provider: 'pubsub',
|
||||
reason: {
|
||||
message: 'provider timeout',
|
||||
name: 'Error',
|
||||
},
|
||||
},
|
||||
],
|
||||
message: 'publish failed',
|
||||
name: 'Error',
|
||||
self: '[Circular]',
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps string errors unchanged and empty objects as JSON', () => {
|
||||
expect(serializeErrorForClipboard('plain failure')).toBe('plain failure');
|
||||
expect(serializeErrorForClipboard({})).toBe('{}');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,27 +4,160 @@ type ErrorLike = {
|
||||
message?: unknown;
|
||||
};
|
||||
|
||||
const normalizeUnknownErrorPart = (value: unknown): string | undefined => {
|
||||
const CIRCULAR_REFERENCE_LABEL = '[Circular]';
|
||||
const UNDEFINED_LABEL = '[undefined]';
|
||||
|
||||
const normalizeErrorForClipboard = (value: unknown, seen = new WeakSet<object>()): unknown => {
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
if (value === undefined) {
|
||||
return UNDEFINED_LABEL;
|
||||
}
|
||||
if (typeof value === 'bigint') {
|
||||
return value.toString();
|
||||
}
|
||||
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'symbol' || typeof value === 'function') {
|
||||
return String(value);
|
||||
}
|
||||
if (value instanceof Date) {
|
||||
return Number.isNaN(value.getTime()) ? String(value) : value.toISOString();
|
||||
}
|
||||
if (value instanceof Error) {
|
||||
if (seen.has(value)) {
|
||||
return CIRCULAR_REFERENCE_LABEL;
|
||||
}
|
||||
seen.add(value);
|
||||
const normalized: Record<string, unknown> = {
|
||||
name: value.name,
|
||||
message: value.message,
|
||||
};
|
||||
if (value.stack) {
|
||||
normalized.stack = value.stack;
|
||||
}
|
||||
for (const [key, entryValue] of Object.entries(value)) {
|
||||
normalized[key] = normalizeErrorForClipboard(entryValue, seen);
|
||||
}
|
||||
if ('cause' in value && value.cause !== undefined) {
|
||||
normalized.cause = normalizeErrorForClipboard(value.cause, seen);
|
||||
}
|
||||
seen.delete(value);
|
||||
return normalized;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
if (seen.has(value)) {
|
||||
return CIRCULAR_REFERENCE_LABEL;
|
||||
}
|
||||
seen.add(value);
|
||||
const normalized = value.map((entry) => normalizeErrorForClipboard(entry, seen));
|
||||
seen.delete(value);
|
||||
return normalized;
|
||||
}
|
||||
if (value instanceof Map) {
|
||||
if (seen.has(value)) {
|
||||
return CIRCULAR_REFERENCE_LABEL;
|
||||
}
|
||||
seen.add(value);
|
||||
const normalized = Object.fromEntries([...value.entries()].map(([key, entryValue]) => [String(key), normalizeErrorForClipboard(entryValue, seen)]));
|
||||
seen.delete(value);
|
||||
return normalized;
|
||||
}
|
||||
if (value instanceof Set) {
|
||||
if (seen.has(value)) {
|
||||
return CIRCULAR_REFERENCE_LABEL;
|
||||
}
|
||||
seen.add(value);
|
||||
const normalized = [...value].map((entry) => normalizeErrorForClipboard(entry, seen));
|
||||
seen.delete(value);
|
||||
return normalized;
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
if (seen.has(value)) {
|
||||
return CIRCULAR_REFERENCE_LABEL;
|
||||
}
|
||||
seen.add(value);
|
||||
const normalized: Record<string, unknown> = {};
|
||||
for (const [key, entryValue] of Object.entries(value as Record<string, unknown>)) {
|
||||
normalized[key] = normalizeErrorForClipboard(entryValue, seen);
|
||||
}
|
||||
seen.delete(value);
|
||||
return normalized;
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
|
||||
export const serializeErrorForClipboard = (error: unknown): string => {
|
||||
if (typeof error === 'string') {
|
||||
return error;
|
||||
}
|
||||
|
||||
const serializableError = normalizeErrorForClipboard(error);
|
||||
|
||||
try {
|
||||
return JSON.stringify(serializableError, null, 2) ?? String(error);
|
||||
} catch {
|
||||
return String(error);
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeUnknownErrorPart = (value: unknown, seen = new WeakSet<object>()): string | undefined => {
|
||||
if (value === null || value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return value.trim() || undefined;
|
||||
}
|
||||
if (typeof value === 'number' || typeof value === 'boolean') {
|
||||
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
|
||||
return String(value);
|
||||
}
|
||||
if (typeof value === 'symbol' || typeof value === 'function') {
|
||||
return String(value);
|
||||
}
|
||||
if (value instanceof Error) {
|
||||
if (seen.has(value)) {
|
||||
return CIRCULAR_REFERENCE_LABEL;
|
||||
}
|
||||
seen.add(value);
|
||||
const parts = [normalizeUnknownErrorPart(value.message, seen)];
|
||||
for (const [key, entryValue] of Object.entries(value)) {
|
||||
const normalizedValue = normalizeUnknownErrorPart(entryValue, seen);
|
||||
if (normalizedValue) {
|
||||
parts.push(`${key}: ${normalizedValue}`);
|
||||
}
|
||||
}
|
||||
if ('cause' in value) {
|
||||
const normalizedCause = normalizeUnknownErrorPart(value.cause, seen);
|
||||
if (normalizedCause) {
|
||||
parts.push(`cause: ${normalizedCause}`);
|
||||
}
|
||||
}
|
||||
seen.delete(value);
|
||||
return parts.filter(Boolean).join('; ') || value.name;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
const parts = value.map(normalizeUnknownErrorPart).filter(Boolean);
|
||||
if (seen.has(value)) {
|
||||
return CIRCULAR_REFERENCE_LABEL;
|
||||
}
|
||||
seen.add(value);
|
||||
const parts = value.map((entryValue) => normalizeUnknownErrorPart(entryValue, seen)).filter(Boolean);
|
||||
seen.delete(value);
|
||||
return parts.length ? parts.join('; ') : undefined;
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
if (seen.has(value)) {
|
||||
return CIRCULAR_REFERENCE_LABEL;
|
||||
}
|
||||
seen.add(value);
|
||||
const entries = Object.entries(value as Record<string, unknown>)
|
||||
.map(([key, entryValue]) => {
|
||||
const normalizedValue = normalizeUnknownErrorPart(entryValue);
|
||||
const normalizedValue = normalizeUnknownErrorPart(entryValue, seen);
|
||||
return normalizedValue ? `${key}: ${normalizedValue}` : undefined;
|
||||
})
|
||||
.filter(Boolean);
|
||||
seen.delete(value);
|
||||
|
||||
if (entries.length) {
|
||||
return entries.join('; ');
|
||||
|
||||
Reference in New Issue
Block a user