diff --git a/packages/core/src/runtime.ts b/packages/core/src/runtime.ts index 66a5b5616..469f593a8 100644 --- a/packages/core/src/runtime.ts +++ b/packages/core/src/runtime.ts @@ -1,12 +1,9 @@ import { waitUntil } from '@vercel/functions'; import { - FatalError, - RetryableError, WorkflowAPIError, WorkflowRunCancelledError, WorkflowRunFailedError, WorkflowRunNotCompletedError, - WorkflowRuntimeError, } from '@workflow/errors'; import type { Event, @@ -15,25 +12,17 @@ import type { World, } from '@workflow/world'; import { WorkflowSuspension } from './global.js'; -import { runtimeLogger } from './logger.js'; -import { getStepFunction } from './private.js'; import { getWorld, getWorldHandlers } from './runtime/world.js'; import { type Serializable, type StepInvokePayload, - StepInvokePayloadSchema, - type WorkflowInvokePayload, WorkflowInvokePayloadSchema, } from './schemas.js'; import { dehydrateStepArguments, - dehydrateStepReturnValue, getExternalRevivers, - hydrateStepArguments, hydrateWorkflowReturnValue, } from './serialization.js'; -// TODO: move step handler out to a separate file -import { contextStorage } from './step/context-storage.js'; import * as Attribute from './telemetry/semantic-conventions.js'; import { serializeTraceCarrier, trace, withTraceContext } from './telemetry.js'; import { getErrorName, getErrorStack } from './types.js'; @@ -44,6 +33,7 @@ import { import { runWorkflow } from './workflow.js'; export type { Event, WorkflowRun }; +export { WorkflowAPIError } from '@workflow/errors'; export { WorkflowSuspension } from './global.js'; export { getHookByToken, @@ -51,6 +41,7 @@ export { resumeWebhook, } from './runtime/resume-hook.js'; export { type StartOptions, start } from './runtime/start.js'; +export { runStep, stepEntrypoint } from './runtime/step-entrypoint.js'; export { createWorld, @@ -539,299 +530,3 @@ export function workflowEntrypoint(workflowCode: string) { } ); } - -/** - * A single route that handles any step execution request and routes to the - * appropriate step function. We may eventually want to create different bundles - * for each step, this is temporary. - */ -export const stepEntrypoint = - /* @__PURE__ */ getWorldHandlers().createQueueHandler( - '__wkf_step_', - async (message_, metadata) => { - const { - workflowName, - workflowRunId, - workflowStartedAt, - stepId, - traceCarrier: traceContext, - } = StepInvokePayloadSchema.parse(message_); - // Execute step within the propagated trace context - return await withTraceContext(traceContext, async () => { - // Extract the step name from the topic name - const stepName = metadata.queueName.slice('__wkf_step_'.length); - const world = getWorld(); - - return trace(`STEP ${stepName}`, async (span) => { - span?.setAttributes({ - ...Attribute.StepName(stepName), - ...Attribute.StepAttempt(metadata.attempt), - ...Attribute.QueueName(metadata.queueName), - }); - - const stepFn = getStepFunction(stepName); - if (!stepFn) { - throw new Error(`Step "${stepName}" not found`); - } - if (typeof stepFn !== 'function') { - throw new Error( - `Step "${stepName}" is not a function (got ${typeof stepFn})` - ); - } - - span?.setAttributes({ - ...Attribute.WorkflowName(workflowName), - ...Attribute.WorkflowRunId(workflowRunId), - ...Attribute.StepId(stepId), - ...Attribute.StepMaxRetries(stepFn.maxRetries ?? 3), - ...Attribute.StepTracePropagated(!!traceContext), - }); - - let step = await world.steps.get(workflowRunId, stepId); - - runtimeLogger.debug('Step execution details', { - stepName, - stepId: step.stepId, - status: step.status, - attempt: step.attempt, - }); - - span?.setAttributes({ - ...Attribute.StepStatus(step.status), - }); - - // Check if the step has a `retryAfter` timestamp that hasn't been reached yet - const now = Date.now(); - if (step.retryAfter && step.retryAfter.getTime() > now) { - const timeoutSeconds = Math.ceil( - (step.retryAfter.getTime() - now) / 1000 - ); - span?.setAttributes({ - ...Attribute.StepRetryTimeoutSeconds(timeoutSeconds), - }); - runtimeLogger.debug('Step retryAfter timestamp not yet reached', { - stepName, - stepId: step.stepId, - retryAfter: step.retryAfter, - timeoutSeconds, - }); - return { timeoutSeconds }; - } - - let result: unknown; - const attempt = step.attempt + 1; - try { - if (step.status !== 'pending') { - // We should only be running the step if it's pending - // (initial state, or state set on re-try), so the step has been - // invoked erroneously. - console.error( - `[Workflows] "${workflowRunId}" - Step invoked erroneously, expected status "pending", got "${step.status}" instead, skipping execution` - ); - span?.setAttributes({ - ...Attribute.StepSkipped(true), - ...Attribute.StepSkipReason(step.status), - }); - return; - } - - await world.events.create(workflowRunId, { - eventType: 'step_started', // TODO: Replace with 'step_retrying' - correlationId: stepId, - }); - - step = await world.steps.update(workflowRunId, stepId, { - attempt, - status: 'running', - }); - - if (!step.startedAt) { - throw new WorkflowRuntimeError( - `Step "${stepId}" has no "startedAt" timestamp` - ); - } - // Hydrate the step input arguments - const ops: Promise[] = []; - const args = hydrateStepArguments(step.input, ops); - - span?.setAttributes({ - ...Attribute.StepArgumentsCount(args.length), - }); - - result = await contextStorage.run( - { - stepMetadata: { - stepId, - stepStartedAt: new Date(+step.startedAt), - attempt, - }, - workflowMetadata: { - workflowRunId, - workflowStartedAt: new Date(+workflowStartedAt), - // TODO: there should be a getUrl method on the world interface itself. This - // solution only works for vercel + embedded worlds. - url: process.env.VERCEL_URL - ? `https://${process.env.VERCEL_URL}` - : `http://localhost:${process.env.PORT || 3000}`, - }, - ops, - }, - () => stepFn(...args) - ); - - result = dehydrateStepReturnValue(result, ops); - - waitUntil(Promise.all(ops)); - - // Update the event log with the step result - await world.events.create(workflowRunId, { - eventType: 'step_completed', - correlationId: stepId, - eventData: { - result: result as Serializable, - }, - }); - - await world.steps.update(workflowRunId, stepId, { - status: 'completed', - output: result as Serializable, - }); - - span?.setAttributes({ - ...Attribute.StepStatus('completed'), - ...Attribute.StepResultType(typeof result), - }); - } catch (err: unknown) { - span?.setAttributes({ - ...Attribute.StepErrorName(getErrorName(err)), - ...Attribute.StepErrorMessage(String(err)), - }); - - if (WorkflowAPIError.is(err)) { - if (err.status === 410) { - // Workflow has already completed, so no-op - console.warn( - `Workflow run "${workflowRunId}" has already completed, skipping step "${stepId}": ${err.message}` - ); - return; - } - } - - if (FatalError.is(err)) { - const stackLines = getErrorStack(err).split('\n').slice(0, 4); - console.error( - `[Workflows] "${workflowRunId}" - Encountered \`FatalError\` while executing step "${stepName}":\n > ${stackLines.join('\n > ')}\n\nBubbling up error to parent workflow` - ); - // Fatal error - store the error in the event log and re-invoke the workflow - await world.events.create(workflowRunId, { - eventType: 'step_failed', - correlationId: stepId, - eventData: { - error: String(err), - stack: err.stack, - fatal: true, - }, - }); - await world.steps.update(workflowRunId, stepId, { - status: 'failed', - error: String(err), - // TODO: include error codes when we define them - // TODO: serialize/include the error name and stack? - }); - - span?.setAttributes({ - ...Attribute.StepStatus('failed'), - ...Attribute.StepFatalError(true), - }); - } else { - const maxRetries = stepFn.maxRetries ?? 3; - - span?.setAttributes({ - ...Attribute.StepAttempt(attempt), - ...Attribute.StepMaxRetries(maxRetries), - }); - - if (attempt >= maxRetries) { - // Max retries reached - const stackLines = getErrorStack(err).split('\n').slice(0, 4); - console.error( - `[Workflows] "${workflowRunId}" - Encountered \`Error\` while executing step "${stepName}" (attempt ${attempt}):\n > ${stackLines.join('\n > ')}\n\n Max retries reached\n Bubbling error to parent workflow` - ); - const errorMessage = `Step "${stepName}" failed after max retries: ${String(err)}`; - await world.events.create(workflowRunId, { - eventType: 'step_failed', - correlationId: stepId, - eventData: { - error: errorMessage, - stack: getErrorStack(err), - fatal: true, - }, - }); - await world.steps.update(workflowRunId, stepId, { - status: 'failed', - error: errorMessage, - }); - - span?.setAttributes({ - ...Attribute.StepStatus('failed'), - ...Attribute.StepRetryExhausted(true), - }); - } else { - // Not at max retries yet - log as a retryable error - if (RetryableError.is(err)) { - console.warn( - `[Workflows] "${workflowRunId}" - Encountered \`RetryableError\` while executing step "${stepName}" (attempt ${attempt}):\n > ${String(err.message)}\n\n This step has failed but will be retried` - ); - } else { - const stackLines = getErrorStack(err).split('\n').slice(0, 4); - console.error( - `[Workflows] "${workflowRunId}" - Encountered \`Error\` while executing step "${stepName}" (attempt ${attempt}):\n > ${stackLines.join('\n > ')}\n\n This step has failed but will be retried` - ); - } - await world.events.create(workflowRunId, { - eventType: 'step_failed', - correlationId: stepId, - eventData: { - error: String(err), - stack: getErrorStack(err), - }, - }); - - await world.steps.update(workflowRunId, stepId, { - status: 'pending', // TODO: Should be "retrying" once we have that status - ...(RetryableError.is(err) && { - retryAfter: err.retryAfter, - }), - }); - - const timeoutSeconds = Math.max( - 1, - RetryableError.is(err) - ? Math.ceil((+err.retryAfter.getTime() - Date.now()) / 1000) - : 1 - ); - - span?.setAttributes({ - ...Attribute.StepRetryTimeoutSeconds(timeoutSeconds), - ...Attribute.StepRetryWillRetry(true), - }); - - // It's a retryable error - so have the queue keep the message visible - // so that it gets retried. - return { timeoutSeconds }; - } - } - } - - await world.queue(`__wkf_workflow_${workflowName}`, { - runId: workflowRunId, - traceCarrier: await serializeTraceCarrier(), - } satisfies WorkflowInvokePayload); - }); - }); - } - ); - -// this is a no-op placeholder as the client is -// expecting this to be present but we aren't actually using it -export function runStep() {} diff --git a/packages/core/src/runtime/step-entrypoint.ts b/packages/core/src/runtime/step-entrypoint.ts new file mode 100644 index 000000000..bdde43373 --- /dev/null +++ b/packages/core/src/runtime/step-entrypoint.ts @@ -0,0 +1,323 @@ +import { waitUntil } from '@vercel/functions'; +import { + FatalError, + RetryableError, + WorkflowAPIError, + WorkflowRuntimeError, +} from '@workflow/errors'; +import { runtimeLogger } from '../logger.js'; +import { getStepFunction } from '../private.js'; +import { + type Serializable, + StepInvokePayloadSchema, + type WorkflowInvokePayload, +} from '../schemas.js'; +import { + dehydrateStepReturnValue, + hydrateStepArguments, +} from '../serialization.js'; +import { contextStorage } from '../step/context-storage.js'; +import * as Attribute from '../telemetry/semantic-conventions.js'; +import { + serializeTraceCarrier, + trace, + withTraceContext, +} from '../telemetry.js'; +import { getErrorName, getErrorStack } from '../types.js'; +import { getWorld, getWorldHandlers } from './world.js'; + +/** + * A single route that handles any step execution request and routes to the + * appropriate step function. We may eventually want to create different bundles + * for each step, this is temporary. + */ +export const stepEntrypoint = + /* @__PURE__ */ getWorldHandlers().createQueueHandler( + '__wkf_step_', + async (message_, metadata) => { + const { + workflowName, + workflowRunId, + workflowStartedAt, + stepId, + traceCarrier: traceContext, + } = StepInvokePayloadSchema.parse(message_); + // Execute step within the propagated trace context + return await withTraceContext(traceContext, async () => { + // Extract the step name from the topic name + const stepName = metadata.queueName.slice('__wkf_step_'.length); + const world = getWorld(); + + return trace(`STEP ${stepName}`, async (span) => { + span?.setAttributes({ + ...Attribute.StepName(stepName), + ...Attribute.StepAttempt(metadata.attempt), + ...Attribute.QueueName(metadata.queueName), + }); + + const stepFn = getStepFunction(stepName); + if (!stepFn) { + throw new Error(`Step "${stepName}" not found`); + } + if (typeof stepFn !== 'function') { + throw new Error( + `Step "${stepName}" is not a function (got ${typeof stepFn})` + ); + } + + span?.setAttributes({ + ...Attribute.WorkflowName(workflowName), + ...Attribute.WorkflowRunId(workflowRunId), + ...Attribute.StepId(stepId), + ...Attribute.StepMaxRetries(stepFn.maxRetries ?? 3), + ...Attribute.StepTracePropagated(!!traceContext), + }); + + let step = await world.steps.get(workflowRunId, stepId); + + runtimeLogger.debug('Step execution details', { + stepName, + stepId: step.stepId, + status: step.status, + attempt: step.attempt, + }); + + span?.setAttributes({ + ...Attribute.StepStatus(step.status), + }); + + // Check if the step has a `retryAfter` timestamp that hasn't been reached yet + const now = Date.now(); + if (step.retryAfter && step.retryAfter.getTime() > now) { + const timeoutSeconds = Math.ceil( + (step.retryAfter.getTime() - now) / 1000 + ); + span?.setAttributes({ + ...Attribute.StepRetryTimeoutSeconds(timeoutSeconds), + }); + runtimeLogger.debug('Step retryAfter timestamp not yet reached', { + stepName, + stepId: step.stepId, + retryAfter: step.retryAfter, + timeoutSeconds, + }); + return { timeoutSeconds }; + } + + let result: unknown; + const attempt = step.attempt + 1; + try { + if (step.status !== 'pending') { + // We should only be running the step if it's pending + // (initial state, or state set on re-try), so the step has been + // invoked erroneously. + console.error( + `[Workflows] "${workflowRunId}" - Step invoked erroneously, expected status "pending", got "${step.status}" instead, skipping execution` + ); + span?.setAttributes({ + ...Attribute.StepSkipped(true), + ...Attribute.StepSkipReason(step.status), + }); + return; + } + + await world.events.create(workflowRunId, { + eventType: 'step_started', // TODO: Replace with 'step_retrying' + correlationId: stepId, + }); + + step = await world.steps.update(workflowRunId, stepId, { + attempt, + status: 'running', + }); + + if (!step.startedAt) { + throw new WorkflowRuntimeError( + `Step "${stepId}" has no "startedAt" timestamp` + ); + } + // Hydrate the step input arguments + const ops: Promise[] = []; + const args = hydrateStepArguments(step.input, ops); + + span?.setAttributes({ + ...Attribute.StepArgumentsCount(args.length), + }); + + result = await contextStorage.run( + { + stepMetadata: { + stepId, + stepStartedAt: new Date(+step.startedAt), + attempt, + }, + workflowMetadata: { + workflowRunId, + workflowStartedAt: new Date(+workflowStartedAt), + // TODO: there should be a getUrl method on the world interface itself. This + // solution only works for vercel + embedded worlds. + url: process.env.VERCEL_URL + ? `https://${process.env.VERCEL_URL}` + : `http://localhost:${process.env.PORT || 3000}`, + }, + ops, + }, + () => stepFn(...args) + ); + + result = dehydrateStepReturnValue(result, ops); + + waitUntil(Promise.all(ops)); + + // Update the event log with the step result + await world.events.create(workflowRunId, { + eventType: 'step_completed', + correlationId: stepId, + eventData: { + result: result as Serializable, + }, + }); + + await world.steps.update(workflowRunId, stepId, { + status: 'completed', + output: result as Serializable, + }); + + span?.setAttributes({ + ...Attribute.StepStatus('completed'), + ...Attribute.StepResultType(typeof result), + }); + } catch (err: unknown) { + span?.setAttributes({ + ...Attribute.StepErrorName(getErrorName(err)), + ...Attribute.StepErrorMessage(String(err)), + }); + + if (WorkflowAPIError.is(err)) { + if (err.status === 410) { + // Workflow has already completed, so no-op + console.warn( + `Workflow run "${workflowRunId}" has already completed, skipping step "${stepId}": ${err.message}` + ); + return; + } + } + + if (FatalError.is(err)) { + const stackLines = getErrorStack(err).split('\n').slice(0, 4); + console.error( + `[Workflows] "${workflowRunId}" - Encountered \`FatalError\` while executing step "${stepName}":\n > ${stackLines.join('\n > ')}\n\nBubbling up error to parent workflow` + ); + // Fatal error - store the error in the event log and re-invoke the workflow + await world.events.create(workflowRunId, { + eventType: 'step_failed', + correlationId: stepId, + eventData: { + error: String(err), + stack: err.stack, + fatal: true, + }, + }); + await world.steps.update(workflowRunId, stepId, { + status: 'failed', + error: String(err), + // TODO: include error codes when we define them + // TODO: serialize/include the error name and stack? + }); + + span?.setAttributes({ + ...Attribute.StepStatus('failed'), + ...Attribute.StepFatalError(true), + }); + } else { + const maxRetries = stepFn.maxRetries ?? 3; + + span?.setAttributes({ + ...Attribute.StepAttempt(attempt), + ...Attribute.StepMaxRetries(maxRetries), + }); + + if (attempt >= maxRetries) { + // Max retries reached + const stackLines = getErrorStack(err).split('\n').slice(0, 4); + console.error( + `[Workflows] "${workflowRunId}" - Encountered \`Error\` while executing step "${stepName}" (attempt ${attempt}):\n > ${stackLines.join('\n > ')}\n\n Max retries reached\n Bubbling error to parent workflow` + ); + const errorMessage = `Step "${stepName}" failed after max retries: ${String(err)}`; + await world.events.create(workflowRunId, { + eventType: 'step_failed', + correlationId: stepId, + eventData: { + error: errorMessage, + stack: getErrorStack(err), + fatal: true, + }, + }); + await world.steps.update(workflowRunId, stepId, { + status: 'failed', + error: errorMessage, + }); + + span?.setAttributes({ + ...Attribute.StepStatus('failed'), + ...Attribute.StepRetryExhausted(true), + }); + } else { + // Not at max retries yet - log as a retryable error + if (RetryableError.is(err)) { + console.warn( + `[Workflows] "${workflowRunId}" - Encountered \`RetryableError\` while executing step "${stepName}" (attempt ${attempt}):\n > ${String(err.message)}\n\n This step has failed but will be retried` + ); + } else { + const stackLines = getErrorStack(err).split('\n').slice(0, 4); + console.error( + `[Workflows] "${workflowRunId}" - Encountered \`Error\` while executing step "${stepName}" (attempt ${attempt}):\n > ${stackLines.join('\n > ')}\n\n This step has failed but will be retried` + ); + } + await world.events.create(workflowRunId, { + eventType: 'step_failed', + correlationId: stepId, + eventData: { + error: String(err), + stack: getErrorStack(err), + }, + }); + + await world.steps.update(workflowRunId, stepId, { + status: 'pending', // TODO: Should be "retrying" once we have that status + ...(RetryableError.is(err) && { + retryAfter: err.retryAfter, + }), + }); + + const timeoutSeconds = Math.max( + 1, + RetryableError.is(err) + ? Math.ceil((+err.retryAfter.getTime() - Date.now()) / 1000) + : 1 + ); + + span?.setAttributes({ + ...Attribute.StepRetryTimeoutSeconds(timeoutSeconds), + ...Attribute.StepRetryWillRetry(true), + }); + + // It's a retryable error - so have the queue keep the message visible + // so that it gets retried. + return { timeoutSeconds }; + } + } + } + + await world.queue(`__wkf_workflow_${workflowName}`, { + runId: workflowRunId, + traceCarrier: await serializeTraceCarrier(), + } satisfies WorkflowInvokePayload); + }); + }); + } + ); + +// this is a no-op placeholder as the client is +// expecting this to be present but we aren't actually using it +export function runStep() {} diff --git a/packages/workflow/src/api.ts b/packages/workflow/src/api.ts index 81baccd20..73de501ba 100644 --- a/packages/workflow/src/api.ts +++ b/packages/workflow/src/api.ts @@ -8,6 +8,7 @@ export { runStep, type StartOptions, start, + WorkflowAPIError, type WorkflowReadableStreamOptions, type WorkflowRun, } from '@workflow/core/runtime'; diff --git a/workbench/example/api/hook.ts b/workbench/example/api/hook.ts index 4a28822c6..7addb0851 100644 --- a/workbench/example/api/hook.ts +++ b/workbench/example/api/hook.ts @@ -1,4 +1,4 @@ -import { getHookByToken, resumeHook } from 'workflow/api'; +import { getHookByToken, resumeHook, WorkflowAPIError } from 'workflow/api'; export const POST = async (request: Request) => { const { token, data } = await request.json(); @@ -9,9 +9,10 @@ export const POST = async (request: Request) => { console.log('hook', hook); } catch (error) { console.log('error during getHookByToken', error); - // TODO: `WorkflowAPIError` is not exported, so for now - // we'll return 404 assuming it's the "invalid" token test case - return Response.json(null, { status: 404 }); + if (error instanceof WorkflowAPIError && error.status === 404) { + return Response.json(null, { status: 404 }); + } + throw error; } await resumeHook(hook.token, { diff --git a/workbench/hono/server.ts b/workbench/hono/server.ts index adf73ef0a..7182a530b 100644 --- a/workbench/hono/server.ts +++ b/workbench/hono/server.ts @@ -1,5 +1,11 @@ import { Hono } from 'hono'; -import { getHookByToken, getRun, resumeHook, start } from 'workflow/api'; +import { + getHookByToken, + getRun, + resumeHook, + start, + WorkflowAPIError, +} from 'workflow/api'; import { hydrateWorkflowArguments } from 'workflow/internal/serialization'; import { allWorkflows } from './_workflows.js'; @@ -152,9 +158,10 @@ app.post('/api/hook', async ({ req }) => { console.log('hook', hook); } catch (error) { console.log('error during getHookByToken', error); - // TODO: `WorkflowAPIError` is not exported, so for now - // we'll return 404 assuming it's the "invalid" token test case - return Response.json(null, { status: 404 }); + if (error instanceof WorkflowAPIError && error.status === 404) { + return Response.json(null, { status: 404 }); + } + throw error; } await resumeHook(hook.token, { diff --git a/workbench/nitro-v2/server/api/hook.post.ts b/workbench/nitro-v2/server/api/hook.post.ts index 22c8c9496..d0f98b401 100644 --- a/workbench/nitro-v2/server/api/hook.post.ts +++ b/workbench/nitro-v2/server/api/hook.post.ts @@ -1,5 +1,5 @@ import { defineEventHandler, readBody } from 'h3'; -import { getHookByToken, resumeHook } from 'workflow/api'; +import { getHookByToken, resumeHook, WorkflowAPIError } from 'workflow/api'; export default defineEventHandler(async (event) => { const { token, data } = await readBody(event); @@ -10,9 +10,10 @@ export default defineEventHandler(async (event) => { console.log('hook', hook); } catch (error) { console.log('error during getHookByToken', error); - // TODO: `WorkflowAPIError` is not exported, so for now - // we'll return 404 assuming it's the "invalid" token test case - return Response.json(null, { status: 404 }); + if (error instanceof WorkflowAPIError && error.status === 404) { + return Response.json(null, { status: 404 }); + } + throw error; } await resumeHook(hook.token, { diff --git a/workbench/nitro-v3/routes/api/hook.post.ts b/workbench/nitro-v3/routes/api/hook.post.ts index 6578a4af1..684b376ca 100644 --- a/workbench/nitro-v3/routes/api/hook.post.ts +++ b/workbench/nitro-v3/routes/api/hook.post.ts @@ -1,4 +1,4 @@ -import { getHookByToken, resumeHook } from 'workflow/api'; +import { getHookByToken, resumeHook, WorkflowAPIError } from 'workflow/api'; export default async ({ req }: { req: Request }) => { const { token, data } = await req.json(); @@ -9,9 +9,10 @@ export default async ({ req }: { req: Request }) => { console.log('hook', hook); } catch (error) { console.log('error during getHookByToken', error); - // TODO: `WorkflowAPIError` is not exported, so for now - // we'll return 404 assuming it's the "invalid" token test case - return Response.json(null, { status: 404 }); + if (error instanceof WorkflowAPIError && error.status === 404) { + return Response.json(null, { status: 404 }); + } + throw error; } await resumeHook(hook.token, { diff --git a/workbench/sveltekit/src/routes/api/hook/+server.ts b/workbench/sveltekit/src/routes/api/hook/+server.ts index a0254e560..c954a94e7 100644 --- a/workbench/sveltekit/src/routes/api/hook/+server.ts +++ b/workbench/sveltekit/src/routes/api/hook/+server.ts @@ -1,5 +1,5 @@ import { json, type RequestHandler } from '@sveltejs/kit'; -import { getHookByToken, resumeHook } from 'workflow/api'; +import { getHookByToken, resumeHook, WorkflowAPIError } from 'workflow/api'; export const POST: RequestHandler = async ({ request, @@ -14,9 +14,10 @@ export const POST: RequestHandler = async ({ console.log('hook', hook); } catch (error) { console.log('error during getHookByToken', error); - // TODO: `WorkflowAPIError` is not exported, so for now - // we'll return 404 assuming it's the "invalid" token test case - return json(null, { status: 404 }); + if (error instanceof WorkflowAPIError && error.status === 404) { + return json(null, { status: 404 }); + } + throw error; } await resumeHook(hook.token, {