Skip to content

Commit 24a96d8

Browse files
Fix HookConflictError web hydration (vercel#2249)
1 parent ddc8a79 commit 24a96d8

3 files changed

Lines changed: 41 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@workflow/web-shared': patch
3+
---
4+
5+
Hydrate serialized HookConflictError values in the web trace viewer.

packages/web-shared/src/lib/hydration.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,8 @@ export function getWebRevivers(): Revivers {
135135
// Error family. The reducer side (see
136136
// `packages/core/src/serialization/reducers/common.ts`) emits a tagged
137137
// entry for each built-in Error subclass plus the workflow-specific
138-
// `FatalError` / `RetryableError` and `AggregateError`. Without
138+
// `FatalError` / `RetryableError` / `HookConflictError` and
139+
// `AggregateError`. Without
139140
// matching revivers here, `devalue.unflatten` throws "Unknown type X"
140141
// — which surfaces in the web o11y UI as "Failed to load resource
141142
// details: Unknown type FatalError".
@@ -183,6 +184,20 @@ export function getWebRevivers(): Revivers {
183184
if (value.stack !== undefined) error.stack = value.stack;
184185
return error;
185186
},
187+
HookConflictError: (value) => {
188+
const opts = 'cause' in value ? { cause: value.cause } : undefined;
189+
const error = new Error(value.message, opts) as Error & {
190+
token?: string;
191+
conflictingRunId?: string;
192+
};
193+
error.name = 'HookConflictError';
194+
error.token = value.token;
195+
if (value.conflictingRunId !== undefined) {
196+
error.conflictingRunId = value.conflictingRunId;
197+
}
198+
if (value.stack !== undefined) error.stack = value.stack;
199+
return error;
200+
},
186201
RetryableError: (value) => {
187202
const opts = 'cause' in value ? { cause: value.cause } : undefined;
188203
const error = new Error(value.message, opts) as Error & {

packages/web-shared/test/hydration.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,26 @@ describe('getWebRevivers — error family', () => {
9191
expect(revived.message).toBe('cannot retry');
9292
});
9393

94+
it('hydrates a HookConflictError with token details preserved', () => {
95+
const revived = hydrateData(
96+
[
97+
['HookConflictError', 1],
98+
{ message: 2, stack: 3, token: 4, conflictingRunId: 5 },
99+
'Hook token "approval-token" is already in use by another workflow (run "wrun_conflicting")',
100+
'HookConflictError: Hook token "approval-token" is already in use by another workflow',
101+
'approval-token',
102+
'wrun_conflicting',
103+
],
104+
REVIVERS
105+
) as Error & { token?: string; conflictingRunId?: string };
106+
107+
expect(revived).toBeInstanceOf(Error);
108+
expect(revived.name).toBe('HookConflictError');
109+
expect(revived.message).toContain('already in use');
110+
expect(revived.token).toBe('approval-token');
111+
expect(revived.conflictingRunId).toBe('wrun_conflicting');
112+
});
113+
94114
it('hydrates a RetryableError with retryAfter as a Date', async () => {
95115
const retryAt = new Date('2025-01-01T00:00:00.000Z');
96116
const revived = await roundTrip<Error & { retryAfter: Date }>(

0 commit comments

Comments
 (0)