From a266a55918f28575652b0c7a6575e6322c3109c7 Mon Sep 17 00:00:00 2001 From: CahidArda Date: Wed, 10 Jun 2026 15:09:26 +0300 Subject: [PATCH 1/9] feat: report failing step name via Upstash-Error-Step-Name header When a step throws during execution, attach the step name to the error and surface it on the 500 error response via the Upstash-Error-Step-Name header so Workflow Logs can show which step is being retried. --- src/constants.ts | 1 + src/context/auto-executor.ts | 8 +++++-- src/error.ts | 37 +++++++++++++++++++++++++++++++ src/qstash/submit-steps.ts | 12 ++++++++-- src/serve/index.ts | 6 +++++ src/serve/serve.test.ts | 43 ++++++++++++++++++++++++++++++++++++ 6 files changed, 103 insertions(+), 4 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index cbd07cb5..947f0eec 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -12,6 +12,7 @@ export const WORKFLOW_RETRIED_HEADER = "Upstash-Retried"; export const WORKFLOW_LABEL_HEADER = "Upstash-Label"; export const WORKFLOW_UNKOWN_SDK_VERSION_HEADER = "Upstash-Workflow-Unknown-Sdk"; export const WORKFLOW_UNKOWN_SDK_TRIGGER_HEADER = "upstash-workflow-trigger-by-sdk"; +export const WORKFLOW_ERROR_STEP_NAME_HEADER = "Upstash-Error-Step-Name"; export const WORKFLOW_PROTOCOL_VERSION = "1"; export const WORKFLOW_PROTOCOL_VERSION_HEADER = "Upstash-Workflow-Sdk-Version"; diff --git a/src/context/auto-executor.ts b/src/context/auto-executor.ts index dd09e3e9..d84ea7c3 100644 --- a/src/context/auto-executor.ts +++ b/src/context/auto-executor.ts @@ -1,4 +1,4 @@ -import { isInstanceOf, WorkflowAbort, WorkflowError } from "../error"; +import { attachStepNameToError, isInstanceOf, WorkflowAbort, WorkflowError } from "../error"; import type { WorkflowContext } from "./context"; import type { StepFunction, ParallelCallState, Step, Telemetry } from "../types"; import { type BaseLazyStep } from "./steps"; @@ -265,9 +265,13 @@ export class AutoExecutor { ) { throw error; } - throw new WorkflowError( + const wrappedError = new WorkflowError( `Error submitting steps to QStash in partial parallel step execution: ${error}` ); + // preserve the failing step name through the re-wrap so it still + // reaches the error response header + attachStepNameToError(wrappedError, parallelSteps[stepIndex].stepName); + throw wrappedError; } break; } diff --git a/src/error.ts b/src/error.ts index 517f629a..7cefd290 100644 --- a/src/error.ts +++ b/src/error.ts @@ -111,6 +111,43 @@ export const formatWorkflowError = (error: unknown): FailureFunctionPayload => { }; }; +/** + * An error that may carry the name of the step that was executing when it was + * thrown. The serve handler reads this to report the failing step name to QStash + * via the `Upstash-Error-Step-Name` response header (so it shows up in Workflow Logs). + */ +type ErrorWithStepName = { + errorStepName?: string; +}; + +/** + * Records the name of the currently executing step on an error so it can be + * surfaced later when building the error response. The first recorded name wins, + * so the step closest to where the error originated is preserved. + * + * @param error error thrown while executing a step + * @param stepName name of the step that was executing + */ +export const attachStepNameToError = (error: unknown, stepName: string): void => { + if (typeof error === "object" && error !== null && !("errorStepName" in error)) { + (error as ErrorWithStepName).errorStepName = stepName; + } +}; + +/** + * Reads the step name recorded by `attachStepNameToError`. + * + * @param error error to read the step name from + * @returns the recorded step name, or undefined if none was recorded + */ +export const getStepNameFromError = (error: unknown): string | undefined => { + if (typeof error === "object" && error !== null) { + const stepName = (error as ErrorWithStepName).errorStepName; + return typeof stepName === "string" ? stepName : undefined; + } + return undefined; +}; + /** * Gets the constructor name of an object. * diff --git a/src/qstash/submit-steps.ts b/src/qstash/submit-steps.ts index 786f847d..c48a1e0d 100644 --- a/src/qstash/submit-steps.ts +++ b/src/qstash/submit-steps.ts @@ -1,5 +1,5 @@ import { NO_CONCURRENCY } from "../constants"; -import { WorkflowAbort } from "../error"; +import { attachStepNameToError, WorkflowAbort } from "../error"; import { Telemetry } from "../types"; import { WorkflowContext } from "../context"; import { BaseLazyStep } from "../context/steps"; @@ -109,7 +109,15 @@ export const submitSingleStep = async ({ stepName: lazyStep.stepName, }); - const resultStep = await lazyStep.getResultStep(concurrency, stepId); + let resultStep; + try { + resultStep = await lazyStep.getResultStep(concurrency, stepId); + } catch (error) { + // The step function threw. Remember which step failed so the serve handler + // can report it to QStash via the `Upstash-Error-Step-Name` header. + attachStepNameToError(error, lazyStep.stepName); + throw error; + } const { headers } = lazyStep.getHeaders({ context, diff --git a/src/serve/index.ts b/src/serve/index.ts index 60a0a68b..5ca2f266 100644 --- a/src/serve/index.ts +++ b/src/serve/index.ts @@ -4,6 +4,7 @@ import { WORKFLOW_CREATED_AT_HEADER, WORKFLOW_INVOKE_COUNT_HEADER, WORKFLOW_LABEL_HEADER, + WORKFLOW_ERROR_STEP_NAME_HEADER, WORKFLOW_PROTOCOL_VERSION, WORKFLOW_PROTOCOL_VERSION_HEADER, WORKFLOW_RETRIED_HEADER, @@ -11,6 +12,7 @@ import { import { WorkflowContext } from "../context"; import { formatWorkflowError, + getStepNameFromError, isInstanceOf, WorkflowNonRetryableError, WorkflowRetryAfterError, @@ -335,10 +337,14 @@ export const serveBase = < await middlewareManager.dispatchDebug("onError", { error: isInstanceOf(error, Error) ? error : new Error(formattedError.message), }); + // if the error happened while executing a known step, report its name so + // it can be shown in Workflow Logs when the step is retried + const stepName = getStepNameFromError(error); return new Response(JSON.stringify(formattedError), { status: 500, headers: { [WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION, + ...(stepName ? { [WORKFLOW_ERROR_STEP_NAME_HEADER]: stepName } : {}), }, }) as TResponse; } diff --git a/src/serve/serve.test.ts b/src/serve/serve.test.ts index 93a026f3..d25f899a 100644 --- a/src/serve/serve.test.ts +++ b/src/serve/serve.test.ts @@ -15,6 +15,7 @@ import { Client as WorkflowClient } from "../client"; import type { FinishCondition, RouteFunction, Step, WorkflowServeOptions } from "../types"; import { WORKFLOW_FAILURE_HEADER, + WORKFLOW_ERROR_STEP_NAME_HEADER, WORKFLOW_ID_HEADER, WORKFLOW_INIT_HEADER, WORKFLOW_INVOKE_COUNT_HEADER, @@ -381,6 +382,8 @@ describe("serve", () => { const response = await endpoint(request); expect(response.status).toBe(500); expect(response.statusText).toBe(""); + // failing step name is reported so Workflow Logs can show it on retry + expect(response.headers.get(WORKFLOW_ERROR_STEP_NAME_HEADER)).toBe("wrong step"); const result = await response.json(); expect(result).toEqual({ error: "Error", @@ -396,6 +399,46 @@ describe("serve", () => { expect(onErrorCalled).toBeTrue(); }); + test("should report the failing step name when a parallel step throws", async () => { + const { handler: endpoint } = serve( + async (context) => { + await Promise.all([ + context.run("parallel step 1", () => "result 1"), + context.run("parallel step 2", () => { + throw new Error("parallel-error"); + }), + ]); + }, + { + qstashClient, + receiver: undefined, + } + ); + + // partial parallel execution: QStash is calling back to run the second + // parallel step (targetStep 2), which is the one that throws. + // prettier-ignore + const planSteps: Step[] = [ + { stepId: 0, stepName: "parallel step 1", stepType: "Run", concurrent: 2, targetStep: 1 }, + { stepId: 0, stepName: "parallel step 2", stepType: "Run", concurrent: 2, targetStep: 2 }, + ]; + const request = getRequest(WORKFLOW_ENDPOINT, "wfr-bar", "my-payload", planSteps); + let called = false; + await mockQStashServer({ + execute: async () => { + const response = await endpoint(request); + expect(response.status).toBe(500); + // the failing parallel step's name is reported, even though the error is + // re-wrapped on the partial parallel execution path + expect(response.headers.get(WORKFLOW_ERROR_STEP_NAME_HEADER)).toBe("parallel step 2"); + called = true; + }, + responseFields: { body: { messageId: "some-message-id" }, status: 200 }, + receivesRequest: false, + }); + expect(called).toBeTrue(); + }); + describe("duplicate checks", () => { const { handler: endpoint } = serve( async (context) => { From bb57ce5fa21f6bd58a373b577561f3d2e0a53a41 Mon Sep 17 00:00:00 2001 From: CahidArda Date: Wed, 10 Jun 2026 15:43:23 +0300 Subject: [PATCH 2/9] feat: expose stepName on failed next-step logs The server now reports the failing step name (from the Upstash-Error-Step-Name header) on the "next" step log group. Surface it as an optional stepName field in the logs response type. --- src/client/types.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/client/types.ts b/src/client/types.ts index d6af42eb..890779a0 100644 --- a/src/client/types.ts +++ b/src/client/types.ts @@ -192,6 +192,13 @@ type StepLogGroup = */ steps: { messageId: string; + /** + * name of the step being run. + * + * only available when the SDK reported it while the step was failing + * (via the `Upstash-Error-Step-Name` response header). + */ + stepName?: string; state: "STEP_PROGRESS" | "STEP_RETRY" | "STEP_FAILED" | "STEP_CANCELED"; /** * retries From 10abd9ad21d4898229d86c576f59e0834af9cdb6 Mon Sep 17 00:00:00 2001 From: CahidArda Date: Wed, 10 Jun 2026 15:49:29 +0300 Subject: [PATCH 3/9] fix: change requestcatcher to httpstatus --- src/test-utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test-utils.ts b/src/test-utils.ts index b986d5c6..ad8d1e48 100644 --- a/src/test-utils.ts +++ b/src/test-utils.ts @@ -10,8 +10,8 @@ import { export const MOCK_QSTASH_SERVER_PORT = 8080; export const MOCK_QSTASH_SERVER_URL = `http://localhost:${MOCK_QSTASH_SERVER_PORT}`; -export const MOCK_SERVER_URL = "https://requestcatcher.com/"; -export const WORKFLOW_ENDPOINT = "https://requestcatcher.com/api"; +export const MOCK_SERVER_URL = "https://mock.httpstatus.io/200/"; +export const WORKFLOW_ENDPOINT = "https://mock.httpstatus.io/200/api"; export type ResponseFields = { body: unknown; From 642a39dbfc41e08bc36d834a79b8948e683027b9 Mon Sep 17 00:00:00 2001 From: CahidArda Date: Wed, 10 Jun 2026 15:53:26 +0300 Subject: [PATCH 4/9] fix: update mock server URLs to use requestcatcher --- src/test-utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test-utils.ts b/src/test-utils.ts index ad8d1e48..764e910d 100644 --- a/src/test-utils.ts +++ b/src/test-utils.ts @@ -10,8 +10,8 @@ import { export const MOCK_QSTASH_SERVER_PORT = 8080; export const MOCK_QSTASH_SERVER_URL = `http://localhost:${MOCK_QSTASH_SERVER_PORT}`; -export const MOCK_SERVER_URL = "https://mock.httpstatus.io/200/"; -export const WORKFLOW_ENDPOINT = "https://mock.httpstatus.io/200/api"; +export const MOCK_SERVER_URL = "https://wf-test.requestcatcher.com/"; +export const WORKFLOW_ENDPOINT = "https://wf-test.requestcatcher.com/api"; export type ResponseFields = { body: unknown; From 928a6fe959fa41d7c250c15b1acfbaec601206d0 Mon Sep 17 00:00:00 2001 From: CahidArda Date: Wed, 10 Jun 2026 16:16:42 +0300 Subject: [PATCH 5/9] feat: expose labels array on DLQ messages DLQMessage only surfaced the first label via the deprecated `label` field, unlike WorkflowRunLog which already exposes the full `labels` array. Add `labels: string[]` to DLQMessage (and PublicDLQMessage) and mark `label` deprecated, matching the run-log type. Adds a mocked test asserting both fields round-trip through dlq.list() and a live test confirming a run triggered with label: [a, b] surfaces label === a and labels === [a, b] on its DLQ message. --- src/client/dlq.test.ts | 65 ++++++++++++++++++++++++++++++++++++++++++ src/client/dlq.ts | 12 +++++++- 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/src/client/dlq.test.ts b/src/client/dlq.test.ts index 79b506e9..c91c496b 100644 --- a/src/client/dlq.test.ts +++ b/src/client/dlq.test.ts @@ -115,6 +115,35 @@ describe("DLQ", () => { }); }); + test("should surface both label and labels from the response", async () => { + const labelOne = "label-one"; + const labelTwo = "label-two"; + const message = { + ...MOCK_DLQ_MESSAGES[0], + label: labelOne, + labels: [labelOne, labelTwo], + }; + + await mockQStashServer({ + execute: async () => { + const result = await client.dlq.list(); + // legacy `label` only carries the first label + expect(result.messages[0].label).toBe(labelOne); + // new `labels` carries all of them + expect(result.messages[0].labels).toEqual([labelOne, labelTwo]); + }, + responseFields: { + status: 200, + body: { messages: [message], cursor: undefined }, + }, + receivesRequest: { + method: "GET", + url: `${MOCK_QSTASH_SERVER_URL}/v2/dlq?source=workflow`, + token, + }, + }); + }); + test("should list DLQ messages with filter options", async () => { const filter = { fromDate: 1640995200000, // 2022-01-01 @@ -1067,6 +1096,42 @@ describe("DLQ", () => { { timeout: 180000 } ); + test( + "should return both label and labels on a DLQ message", + async () => { + const labelOne = `dlq-labels-1-${nanoid()}`; + const labelTwo = `dlq-labels-2-${nanoid()}`; + + const { workflowRunId } = await liveClient.trigger({ + url: "https://mock.httpstatus.io/500", + label: [labelOne, labelTwo], + retries: 0, + }); + + try { + await eventually( + async () => { + const { messages } = await liveClient.dlq.list({ + count: 100, + filter: { label: [labelOne, labelTwo] }, + }); + const message = messages.find((m) => m.workflowRunId === workflowRunId); + expect(message).toBeDefined(); + // legacy `label` only carries the first label + expect(message!.label).toBe(labelOne); + // new `labels` carries all of them + expect(message!.labels).toEqual([labelOne, labelTwo]); + }, + { timeout: 120000, interval: 2000 } + ); + } finally { + // clean up so we don't leave noise in the DLQ + await liveClient.dlq.delete({ filter: { label: [labelOne, labelTwo] } }).catch(() => {}); + } + }, + { timeout: 180000 } + ); + test( "should retry failure function of a DLQ message", async () => { diff --git a/src/client/dlq.ts b/src/client/dlq.ts index c605d66b..fa0ae4bf 100644 --- a/src/client/dlq.ts +++ b/src/client/dlq.ts @@ -45,9 +45,18 @@ type DLQMessage = { */ failureCallbackInfo?: FailureCallbackInfo; /** - * label passed when triggering workflow + * Label passed when triggering the workflow run. + * + * @deprecated Use `labels` instead. When a run has multiple labels, this + * field only contains the first one. */ label?: string; + /** + * Labels attached to the workflow run. + * + * A run can have multiple labels when triggered with `label: string[]`. + */ + labels?: string[]; }; type PublicDLQMessage = Pick< @@ -68,6 +77,7 @@ type PublicDLQMessage = Pick< | "failureCallback" | "failureCallbackInfo" | "label" + | "labels" >; function buildResumeRestartHeaders(options?: ResumeRestartOptions): Record { From 5d70caa422cb22d79badd77452cba21bcaa917fd Mon Sep 17 00:00:00 2001 From: CahidArda Date: Wed, 10 Jun 2026 16:22:25 +0300 Subject: [PATCH 6/9] fix: bump version --- src/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants.ts b/src/constants.ts index 947f0eec..bb11d7e0 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -23,7 +23,7 @@ export const NO_CONCURRENCY = 1; export const NOT_SET = "not-set"; export const DEFAULT_RETRIES = 3; -export const VERSION = "v1.2.1"; +export const VERSION = "v1.3.2"; export const SDK_TELEMETRY = `@upstash/workflow@${VERSION}`; export const TELEMETRY_HEADER_SDK = "Upstash-Telemetry-Sdk" as const; From de4598de7f72396b313e33b5ff4f0307a0955c4d Mon Sep 17 00:00:00 2001 From: CahidArda Date: Wed, 10 Jun 2026 16:24:45 +0300 Subject: [PATCH 7/9] fix: urls in the tests --- src/client/index.test.ts | 34 +++++++------- src/middleware/middleware.test.ts | 6 +-- src/serve/multi-region-integration.test.ts | 28 ++++++------ src/serve/serve-many.test.ts | 52 +++++++++++----------- src/serve/serve.test.ts | 10 ++--- src/workflow-requests.test.ts | 6 +-- 6 files changed, 68 insertions(+), 68 deletions(-) diff --git a/src/client/index.test.ts b/src/client/index.test.ts index 242b683b..2ac3a449 100644 --- a/src/client/index.test.ts +++ b/src/client/index.test.ts @@ -685,7 +685,7 @@ describe("workflow client", () => { "upstash-retry-delay": "1000", "upstash-workflow-init": "true", "upstash-workflow-runid": `wfr_${myWorkflowRunId}`, - "upstash-workflow-url": "https://requestcatcher.com/api", + "upstash-workflow-url": "https://wf-test.requestcatcher.com/api", "upstash-delay": "1s", "content-type": "application/json", "upstash-feature-set": "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig", @@ -696,7 +696,7 @@ describe("workflow client", () => { "upstash-telemetry-sdk": expect.stringContaining("@upstash/workflow"), "upstash-workflow-sdk-version": "1", "upstash-failure-callback-forward-upstash-label": "test-label", - "upstash-failure-callback": "https://requestcatcher.com/api", + "upstash-failure-callback": "https://wf-test.requestcatcher.com/api", "upstash-failure-callback-feature-set": "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig", "upstash-failure-callback-forward-upstash-workflow-failure-callback": "true", @@ -707,7 +707,7 @@ describe("workflow client", () => { "upstash-failure-callback-workflow-calltype": "failureCall", "upstash-failure-callback-workflow-init": "false", "upstash-failure-callback-workflow-runid": `wfr_${myWorkflowRunId}`, - "upstash-failure-callback-workflow-url": "https://requestcatcher.com/api", + "upstash-failure-callback-workflow-url": "https://wf-test.requestcatcher.com/api", }, body, }, @@ -767,7 +767,7 @@ describe("workflow client", () => { "upstash-retry-delay": "1000", "upstash-workflow-init": "true", "upstash-workflow-runid": `wfr_${myWorkflowRunId}`, - "upstash-workflow-url": "https://requestcatcher.com/api", + "upstash-workflow-url": "https://wf-test.requestcatcher.com/api", "upstash-delay": "1s", "content-type": "application/json", "upstash-feature-set": "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig", @@ -775,7 +775,7 @@ describe("workflow client", () => { "upstash-telemetry-runtime": expect.stringMatching(/bun@/), "upstash-telemetry-sdk": expect.stringContaining("@upstash/workflow"), "upstash-workflow-sdk-version": "1", - "upstash-failure-callback": "https://requestcatcher.com/api", + "upstash-failure-callback": "https://wf-test.requestcatcher.com/api", "upstash-failure-callback-feature-set": "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig", "upstash-failure-callback-forward-upstash-workflow-failure-callback": "true", @@ -786,7 +786,7 @@ describe("workflow client", () => { "upstash-failure-callback-workflow-calltype": "failureCall", "upstash-failure-callback-workflow-init": "false", "upstash-failure-callback-workflow-runid": `wfr_${myWorkflowRunId}`, - "upstash-failure-callback-workflow-url": "https://requestcatcher.com/api", + "upstash-failure-callback-workflow-url": "https://wf-test.requestcatcher.com/api", }, body, }, @@ -800,7 +800,7 @@ describe("workflow client", () => { "upstash-retry-delay": "2000", "upstash-workflow-init": "true", "upstash-workflow-runid": `wfr_${myWorkflowRunId2}`, - "upstash-workflow-url": "https://requestcatcher.com/api", + "upstash-workflow-url": "https://wf-test.requestcatcher.com/api", "upstash-not-before": "4102444800", "content-type": "application/json", "upstash-feature-set": "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig", @@ -808,7 +808,7 @@ describe("workflow client", () => { "upstash-telemetry-runtime": expect.stringMatching(/bun@/), "upstash-telemetry-sdk": expect.stringContaining("@upstash/workflow"), "upstash-workflow-sdk-version": "1", - "upstash-failure-callback": "https://requestcatcher.com/api", + "upstash-failure-callback": "https://wf-test.requestcatcher.com/api", "upstash-failure-callback-feature-set": "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig", "upstash-failure-callback-forward-upstash-workflow-failure-callback": "true", @@ -819,7 +819,7 @@ describe("workflow client", () => { "upstash-failure-callback-workflow-calltype": "failureCall", "upstash-failure-callback-workflow-init": "false", "upstash-failure-callback-workflow-runid": `wfr_${myWorkflowRunId2}`, - "upstash-failure-callback-workflow-url": "https://requestcatcher.com/api", + "upstash-failure-callback-workflow-url": "https://wf-test.requestcatcher.com/api", }, body: body2, }, @@ -841,7 +841,7 @@ describe("workflow client", () => { retries: 15, retryDelay: "1000", delay: 1, - failureUrl: "https://requestcatcher.com/some-failure-callback", + failureUrl: "https://wf-test.requestcatcher.com/some-failure-callback", flowControl: { key: "failure-flow-key", rate: 5, @@ -869,9 +869,9 @@ describe("workflow client", () => { "upstash-retry-delay": "1000", "upstash-workflow-init": "true", "upstash-workflow-runid": `wfr_${myWorkflowRunId}`, - "upstash-workflow-url": "https://requestcatcher.com/api", + "upstash-workflow-url": "https://wf-test.requestcatcher.com/api", "upstash-delay": "1s", - "upstash-failure-callback": "https://requestcatcher.com/some-failure-callback", + "upstash-failure-callback": "https://wf-test.requestcatcher.com/some-failure-callback", "content-type": "application/json", "upstash-feature-set": "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig", "upstash-failure-callback-feature-set": @@ -884,7 +884,7 @@ describe("workflow client", () => { "upstash-failure-callback-workflow-calltype": "failureCall", "upstash-failure-callback-workflow-init": "false", "upstash-failure-callback-workflow-runid": `wfr_${myWorkflowRunId}`, - "upstash-failure-callback-workflow-url": "https://requestcatcher.com/api", + "upstash-failure-callback-workflow-url": "https://wf-test.requestcatcher.com/api", "upstash-telemetry-framework": "unknown", "upstash-telemetry-runtime": expect.stringMatching(/bun@/), "upstash-telemetry-sdk": expect.stringContaining("@upstash/workflow"), @@ -944,7 +944,7 @@ describe("workflow client", () => { body, workflowRunId: myWorkflowRunId, redact: { body: true, header: true }, - failureUrl: "https://requestcatcher.com/some-failure-callback", + failureUrl: "https://wf-test.requestcatcher.com/some-failure-callback", }); }, responseFields: { @@ -1484,7 +1484,7 @@ describe("workflow client", () => { "upstash-method": "POST", "upstash-workflow-init": "true", "upstash-workflow-runid": `wfr_${myWorkflowRunId}`, - "upstash-workflow-url": "https://requestcatcher.com/api", + "upstash-workflow-url": "https://wf-test.requestcatcher.com/api", "content-type": "application/json", "upstash-feature-set": "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig", "upstash-forward-upstash-label": "valid_label.1", @@ -1496,7 +1496,7 @@ describe("workflow client", () => { "upstash-telemetry-sdk": expect.stringContaining("@upstash/workflow"), "upstash-workflow-sdk-version": "1", "upstash-failure-callback-forward-upstash-label": "valid_label.1", - "upstash-failure-callback": "https://requestcatcher.com/api", + "upstash-failure-callback": "https://wf-test.requestcatcher.com/api", "upstash-failure-callback-feature-set": "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig", "upstash-failure-callback-flow-control-key": "valid-key_1.0", @@ -1506,7 +1506,7 @@ describe("workflow client", () => { "upstash-failure-callback-workflow-calltype": "failureCall", "upstash-failure-callback-workflow-init": "false", "upstash-failure-callback-workflow-runid": `wfr_${myWorkflowRunId}`, - "upstash-failure-callback-workflow-url": "https://requestcatcher.com/api", + "upstash-failure-callback-workflow-url": "https://wf-test.requestcatcher.com/api", }, }, ], diff --git a/src/middleware/middleware.test.ts b/src/middleware/middleware.test.ts index efa3ba8f..ec351e4e 100644 --- a/src/middleware/middleware.test.ts +++ b/src/middleware/middleware.test.ts @@ -248,7 +248,7 @@ describe("middleware", () => { await context.sleep(stepThreeName, 10); }; - const qstashClient = new Client({ baseUrl: "https://requestcatcher.com", token: "token" }); + const qstashClient = new Client({ baseUrl: "https://wf-test.requestcatcher.com", token: "token" }); qstashClient.http.request = jest.fn(); const runMiddlewareTest = async ( @@ -257,11 +257,11 @@ describe("middleware", () => { ) => { const { middleware, accumulator } = createLoggingMiddleware(); - const request = getRequest("https://requestcatcher.com", "wfr-id", undefined, steps); + const request = getRequest("https://wf-test.requestcatcher.com", "wfr-id", undefined, steps); const { POST: handler } = serve(routeFunction, { middlewares: [middleware], - url: "https://requestcatcher.com", + url: "https://wf-test.requestcatcher.com", receiver: undefined, qstashClient, }); diff --git a/src/serve/multi-region-integration.test.ts b/src/serve/multi-region-integration.test.ts index d1b0cc7e..86a52a80 100644 --- a/src/serve/multi-region-integration.test.ts +++ b/src/serve/multi-region-integration.test.ts @@ -128,7 +128,7 @@ describe("Multi-Region Integration Tests", () => { token: "default-token", body: [ { - destination: "https://requestcatcher.com/api", + destination: "https://wf-test.requestcatcher.com/api", headers: { "content-type": "application/json", "upstash-feature-set": "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig", @@ -140,7 +140,7 @@ describe("Multi-Region Integration Tests", () => { "upstash-workflow-init": "false", "upstash-workflow-runid": "wfr_123", "upstash-workflow-sdk-version": "1", - "upstash-workflow-url": "https://requestcatcher.com/api", + "upstash-workflow-url": "https://wf-test.requestcatcher.com/api", }, body: `{"stepId":1,"stepName":"step1","stepType":"Run","out":"\\"workflow result\\"","concurrent":1}`, }, @@ -203,7 +203,7 @@ describe("Multi-Region Integration Tests", () => { token: "us-token", // Should use US token body: [ { - destination: "https://requestcatcher.com/api", + destination: "https://wf-test.requestcatcher.com/api", headers: { "content-type": "application/json", "upstash-feature-set": "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig", @@ -215,7 +215,7 @@ describe("Multi-Region Integration Tests", () => { "upstash-workflow-init": "false", "upstash-workflow-runid": "wfr_us_123", "upstash-workflow-sdk-version": "1", - "upstash-workflow-url": "https://requestcatcher.com/api", + "upstash-workflow-url": "https://wf-test.requestcatcher.com/api", }, body: `{"stepId":1,"stepName":"step1","stepType":"Run","out":"\\"us workflow result\\"","concurrent":1}`, }, @@ -287,7 +287,7 @@ describe("Multi-Region Integration Tests", () => { token: "us-token", // Should use US token based on region header body: [ { - destination: "https://requestcatcher.com/api", + destination: "https://wf-test.requestcatcher.com/api", headers: { "content-type": "application/json", "upstash-feature-set": "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig", @@ -299,7 +299,7 @@ describe("Multi-Region Integration Tests", () => { "upstash-workflow-init": "false", "upstash-workflow-runid": "wfr_us_123", "upstash-workflow-sdk-version": "1", - "upstash-workflow-url": "https://requestcatcher.com/api", + "upstash-workflow-url": "https://wf-test.requestcatcher.com/api", }, body: `{"stepId":2,"stepName":"step2","stepType":"Run","out":"\\"step 2 result\\"","concurrent":1}`, }, @@ -362,7 +362,7 @@ describe("Multi-Region Integration Tests", () => { token: "eu-token", // Should use EU token body: [ { - destination: "https://requestcatcher.com/api", + destination: "https://wf-test.requestcatcher.com/api", headers: { "content-type": "application/json", "upstash-feature-set": "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig", @@ -374,7 +374,7 @@ describe("Multi-Region Integration Tests", () => { "upstash-workflow-init": "false", "upstash-workflow-runid": "wfr_eu_123", "upstash-workflow-sdk-version": "1", - "upstash-workflow-url": "https://requestcatcher.com/api", + "upstash-workflow-url": "https://wf-test.requestcatcher.com/api", }, body: `{"stepId":1,"stepName":"step1","stepType":"Run","out":"\\"eu workflow result\\"","concurrent":1}`, }, @@ -445,7 +445,7 @@ describe("Multi-Region Integration Tests", () => { token: "eu-token", // Should use EU token based on region header body: [ { - destination: "https://requestcatcher.com/api", + destination: "https://wf-test.requestcatcher.com/api", headers: { "content-type": "application/json", "upstash-feature-set": "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig", @@ -457,7 +457,7 @@ describe("Multi-Region Integration Tests", () => { "upstash-workflow-init": "false", "upstash-workflow-runid": "wfr_eu_123", "upstash-workflow-sdk-version": "1", - "upstash-workflow-url": "https://requestcatcher.com/api", + "upstash-workflow-url": "https://wf-test.requestcatcher.com/api", }, body: `{"stepId":2,"stepName":"step2","stepType":"Run","out":"\\"step 2 result\\"","concurrent":1}`, }, @@ -577,7 +577,7 @@ describe("Multi-Region Integration Tests", () => { token: "us-token", body: [ { - destination: "https://requestcatcher.com/api", + destination: "https://wf-test.requestcatcher.com/api", headers: { "content-type": "application/json", "upstash-feature-set": "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig", @@ -589,7 +589,7 @@ describe("Multi-Region Integration Tests", () => { "upstash-workflow-init": "false", "upstash-workflow-runid": "wfr_config_123", "upstash-workflow-sdk-version": "1", - "upstash-workflow-url": "https://requestcatcher.com/api", + "upstash-workflow-url": "https://wf-test.requestcatcher.com/api", }, body: `{"stepId":1,"stepName":"step1","stepType":"Run","out":"\\"result\\"","concurrent":1}`, }, @@ -764,7 +764,7 @@ describe("Multi-Region Integration Tests", () => { token: "custom-token", // Should use custom client token, not region tokens body: [ { - destination: "https://requestcatcher.com/api", + destination: "https://wf-test.requestcatcher.com/api", headers: { "content-type": "application/json", "upstash-feature-set": "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig", @@ -776,7 +776,7 @@ describe("Multi-Region Integration Tests", () => { "upstash-workflow-init": "false", "upstash-workflow-runid": "wfr_custom_123", "upstash-workflow-sdk-version": "1", - "upstash-workflow-url": "https://requestcatcher.com/api", + "upstash-workflow-url": "https://wf-test.requestcatcher.com/api", }, body: `{"stepId":1,"stepName":"step1","stepType":"Run","out":"\\"result\\"","concurrent":1}`, }, diff --git a/src/serve/serve-many.test.ts b/src/serve/serve-many.test.ts index a346d04e..48a5c57c 100644 --- a/src/serve/serve-many.test.ts +++ b/src/serve/serve-many.test.ts @@ -195,13 +195,13 @@ describe("serveMany", () => { "Upstash-Workflow-RunId": ["wfr_id"], "Upstash-Workflow-Runid": ["wfr_id"], "Upstash-Workflow-Sdk-Version": ["1"], - "Upstash-Workflow-Url": ["https://requestcatcher.com/api/workflowTwo"], + "Upstash-Workflow-Url": ["https://wf-test.requestcatcher.com/api/workflowTwo"], "content-type": ["application/json"], "upstash-forward-x-vercel-protection-bypass": ["testing"], }, workflowRunId: "wfr_id", workflowRunCreatedAt: Number(workflowCreatedAt), - workflowUrl: "https://requestcatcher.com/api/workflowTwo", + workflowUrl: "https://wf-test.requestcatcher.com/api/workflowTwo", step: { stepId: 1, concurrent: 1, @@ -245,10 +245,10 @@ describe("serveMany", () => { body: [ { body: '{"count":99}', - destination: "https://requestcatcher.com/api/workflowOne", + destination: "https://wf-test.requestcatcher.com/api/workflowOne", headers: { "content-type": "application/json", - "upstash-callback": "https://requestcatcher.com/api/workflowThree", + "upstash-callback": "https://wf-test.requestcatcher.com/api/workflowThree", "upstash-callback-feature-set": "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig", "upstash-callback-forward-upstash-workflow-callback": "true", @@ -261,7 +261,7 @@ describe("serveMany", () => { "upstash-callback-workflow-calltype": "fromCallback", "upstash-callback-workflow-init": "false", "upstash-callback-workflow-runid": "wfr_id", - "upstash-callback-workflow-url": "https://requestcatcher.com/api/workflowThree", + "upstash-callback-workflow-url": "https://wf-test.requestcatcher.com/api/workflowThree", "upstash-forward-upstash-workflow-invoke-count": "1", "upstash-feature-set": "WF_NoDelete,InitialBody", "upstash-method": "POST", @@ -273,7 +273,7 @@ describe("serveMany", () => { "upstash-workflow-init": "false", "upstash-workflow-runid": "wfr_id", "upstash-workflow-sdk-version": "1", - "upstash-workflow-url": "https://requestcatcher.com/api/workflowThree", + "upstash-workflow-url": "https://wf-test.requestcatcher.com/api/workflowThree", }, }, ], @@ -302,12 +302,12 @@ describe("serveMany", () => { body: [ { body: "hello world", - destination: "https://requestcatcher.com/api/workflowTwo", + destination: "https://wf-test.requestcatcher.com/api/workflowTwo", headers: { "upstash-callback-feature-set": "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig", "content-type": "application/json", - "upstash-callback": "https://requestcatcher.com/api/workflowFour", + "upstash-callback": "https://wf-test.requestcatcher.com/api/workflowFour", "upstash-callback-forward-upstash-workflow-callback": "true", "upstash-callback-forward-upstash-workflow-concurrent": "1", "upstash-callback-forward-upstash-workflow-contenttype": "application/json", @@ -317,7 +317,7 @@ describe("serveMany", () => { "upstash-callback-workflow-calltype": "fromCallback", "upstash-callback-workflow-init": "false", "upstash-callback-workflow-runid": "wfr_id", - "upstash-callback-workflow-url": "https://requestcatcher.com/api/workflowFour", + "upstash-callback-workflow-url": "https://wf-test.requestcatcher.com/api/workflowFour", "upstash-feature-set": "WF_NoDelete,InitialBody", "upstash-method": "POST", "upstash-retries": "0", @@ -328,7 +328,7 @@ describe("serveMany", () => { "upstash-workflow-init": "false", "upstash-workflow-runid": "wfr_id", "upstash-workflow-sdk-version": "1", - "upstash-workflow-url": "https://requestcatcher.com/api/workflowFour", + "upstash-workflow-url": "https://wf-test.requestcatcher.com/api/workflowFour", }, }, ], @@ -370,12 +370,12 @@ describe("serveMany", () => { "Upstash-Workflow-RunId": ["wfr_id"], "Upstash-Workflow-Runid": ["wfr_id"], "Upstash-Workflow-Sdk-Version": ["1"], - "Upstash-Workflow-Url": ["https://requestcatcher.com/api/workflowFive"], + "Upstash-Workflow-Url": ["https://wf-test.requestcatcher.com/api/workflowFive"], "content-type": ["application/json"], }, workflowRunId: "wfr_id", workflowRunCreatedAt: Number(workflowCreatedAt), - workflowUrl: "https://requestcatcher.com/api/workflowFive", + workflowUrl: "https://wf-test.requestcatcher.com/api/workflowFive", step: { stepId: 1, concurrent: 1, @@ -420,7 +420,7 @@ describe("serveMany", () => { "Upstash-Workflow-RunId": ["wfr_id"], "Upstash-Workflow-Runid": ["wfr_id"], "Upstash-Workflow-Sdk-Version": ["1"], - "Upstash-Workflow-Url": ["https://requestcatcher.com/api/workflowSix"], + "Upstash-Workflow-Url": ["https://wf-test.requestcatcher.com/api/workflowSix"], "content-type": ["application/json"], }, step: { @@ -430,7 +430,7 @@ describe("serveMany", () => { stepType: "Invoke", }, workflowRunId: "wfr_id", - workflowUrl: "https://requestcatcher.com/api/workflowSix", + workflowUrl: "https://wf-test.requestcatcher.com/api/workflowSix", workflowRunCreatedAt: Number(workflowCreatedAt), }, }, @@ -457,10 +457,10 @@ describe("serveMany", () => { token, body: [ { - destination: "https://requestcatcher.com/api/workflowFour", + destination: "https://wf-test.requestcatcher.com/api/workflowFour", headers: { "content-type": "application/json", - "upstash-callback": "https://requestcatcher.com/api/workflowSeven", + "upstash-callback": "https://wf-test.requestcatcher.com/api/workflowSeven", "upstash-callback-feature-set": "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig", "upstash-callback-forward-upstash-workflow-callback": "true", @@ -472,7 +472,7 @@ describe("serveMany", () => { "upstash-callback-workflow-calltype": "fromCallback", "upstash-callback-workflow-init": "false", "upstash-callback-workflow-runid": "wfr_id", - "upstash-callback-workflow-url": "https://requestcatcher.com/api/workflowSeven", + "upstash-callback-workflow-url": "https://wf-test.requestcatcher.com/api/workflowSeven", "upstash-feature-set": "WF_NoDelete,InitialBody", "upstash-method": "POST", "upstash-retries": "0", @@ -483,7 +483,7 @@ describe("serveMany", () => { "upstash-workflow-init": "false", "upstash-workflow-runid": "wfr_id", "upstash-workflow-sdk-version": "1", - "upstash-workflow-url": "https://requestcatcher.com/api/workflowSeven", + "upstash-workflow-url": "https://wf-test.requestcatcher.com/api/workflowSeven", }, }, ], @@ -525,12 +525,12 @@ describe("serveMany", () => { "Upstash-Workflow-RunId": ["wfr_id"], "Upstash-Workflow-Runid": ["wfr_id"], "Upstash-Workflow-Sdk-Version": ["1"], - "Upstash-Workflow-Url": ["https://requestcatcher.com/api/workflowEight"], + "Upstash-Workflow-Url": ["https://wf-test.requestcatcher.com/api/workflowEight"], "content-type": ["application/json"], }, workflowRunId: "wfr_id", workflowRunCreatedAt: Number(workflowCreatedAt), - workflowUrl: "https://requestcatcher.com/api/workflowEight", + workflowUrl: "https://wf-test.requestcatcher.com/api/workflowEight", step: { stepId: 1, concurrent: 1, @@ -553,27 +553,27 @@ describe("serveMany", () => { describe("getNewUrlFromWorkflowId", () => { test("should return new url", () => { - const url = "https://requestcatcher.com/api/original_workflow"; + const url = "https://wf-test.requestcatcher.com/api/original_workflow"; const workflowId = "workflowId"; const newUrl = getNewUrlFromWorkflowId(url, workflowId); - expect(newUrl).toBe("https://requestcatcher.com/api/workflowId"); + expect(newUrl).toBe("https://wf-test.requestcatcher.com/api/workflowId"); }); test("should ignore query parameters", () => { - const url = "https://requestcatcher.com/api/original_workflow?query=param"; + const url = "https://wf-test.requestcatcher.com/api/original_workflow?query=param"; const workflowId = "workflowId"; const newUrl = getNewUrlFromWorkflowId(url, workflowId); - expect(newUrl).toBe("https://requestcatcher.com/api/workflowId"); + expect(newUrl).toBe("https://wf-test.requestcatcher.com/api/workflowId"); }); test("shuold ignore hash parameters", () => { - const url = "https://requestcatcher.com/api/original_workflow#hash"; + const url = "https://wf-test.requestcatcher.com/api/original_workflow#hash"; const workflowId = "workflowId"; const newUrl = getNewUrlFromWorkflowId(url, workflowId); - expect(newUrl).toBe("https://requestcatcher.com/api/workflowId"); + expect(newUrl).toBe("https://wf-test.requestcatcher.com/api/workflowId"); }); }); }); diff --git a/src/serve/serve.test.ts b/src/serve/serve.test.ts index d25f899a..0c0673be 100644 --- a/src/serve/serve.test.ts +++ b/src/serve/serve.test.ts @@ -715,7 +715,7 @@ describe("serve", () => { destination: "some-url", headers: { "content-type": "application/json", - "upstash-callback": "https://requestcatcher.com/api", + "upstash-callback": "https://wf-test.requestcatcher.com/api", "upstash-callback-forward-upstash-workflow-callback": "true", "upstash-callback-feature-set": "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig", @@ -730,7 +730,7 @@ describe("serve", () => { "upstash-callback-workflow-calltype": "fromCallback", "upstash-callback-workflow-init": "false", "upstash-callback-workflow-runid": "wfr-foo", - "upstash-callback-workflow-url": "https://requestcatcher.com/api", + "upstash-callback-workflow-url": "https://wf-test.requestcatcher.com/api", "upstash-feature-set": "WF_NoDelete,InitialBody", "upstash-forward-test": "headers", "upstash-method": "PATCH", @@ -744,7 +744,7 @@ describe("serve", () => { "upstash-workflow-init": "false", "upstash-workflow-runid": "wfr-foo", "upstash-workflow-sdk-version": "1", - "upstash-workflow-url": "https://requestcatcher.com/api", + "upstash-workflow-url": "https://wf-test.requestcatcher.com/api", }, body: "body", }, @@ -1156,7 +1156,7 @@ describe("serve", () => { }); test("allow https://", async () => { - const url = "https://requestcatcher.com"; + const url = "https://wf-test.requestcatcher.com"; const { handler } = serve( async (context) => { await context.sleep("sleeping", 1); @@ -1195,7 +1195,7 @@ describe("serve", () => { } ); - const response = await handler(new Request("https://requestcatcher.com", { method: "POST" })); + const response = await handler(new Request("https://wf-test.requestcatcher.com", { method: "POST" })); expect(response.status).toBe(500); const content = await response.json(); expect(content).toEqual({ diff --git a/src/workflow-requests.test.ts b/src/workflow-requests.test.ts index 3a0c9e6b..e57e3963 100644 --- a/src/workflow-requests.test.ts +++ b/src/workflow-requests.test.ts @@ -708,7 +708,7 @@ describe("Workflow Requests", () => { "Upstash-Failure-Callback-Workflow-Calltype": "failureCall", "Upstash-Failure-Callback-Workflow-Init": "false", "Upstash-Failure-Callback-Workflow-Runid": workflowRunId, - "Upstash-Failure-Callback-Workflow-Url": "https://requestcatcher.com/api", + "Upstash-Failure-Callback-Workflow-Url": "https://wf-test.requestcatcher.com/api", "Upstash-Failure-Callback": failureUrl, "content-type": "application/json", "Upstash-Failure-Callback-Flow-Control-Key": "failure-key", @@ -763,9 +763,9 @@ describe("Workflow Requests", () => { }); expect(typeof body).toBe("string"); expect(JSON.parse(body)).toEqual({ - url: "https://requestcatcher.com/api", + url: "https://wf-test.requestcatcher.com/api", timeout: "20s", - timeoutUrl: "https://requestcatcher.com/api", + timeoutUrl: "https://wf-test.requestcatcher.com/api", timeoutHeaders: { "Upstash-Workflow-Init": ["false"], "Upstash-Workflow-RunId": [workflowRunId], From a3bb5dc76f1d34cc60503dcb987e78bd336abbb5 Mon Sep 17 00:00:00 2001 From: CahidArda Date: Wed, 10 Jun 2026 16:31:03 +0300 Subject: [PATCH 8/9] fix: harden step-name error annotation and header sanitization - attachStepNameToError no longer throws on non-extensible/frozen errors, so it can never mask the original failure - sanitize the step name (strip control chars like CR/LF) before putting it in the Upstash-Error-Step-Name header so an invalid value can't break the 500 response --- src/error.ts | 7 ++++++- src/serve/index.ts | 9 +++++++-- src/serve/serve.test.ts | 33 ++++++++++++++++++++++++++++++++- 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/error.ts b/src/error.ts index 7cefd290..4ce6f392 100644 --- a/src/error.ts +++ b/src/error.ts @@ -129,8 +129,13 @@ type ErrorWithStepName = { * @param stepName name of the step that was executing */ export const attachStepNameToError = (error: unknown, stepName: string): void => { - if (typeof error === "object" && error !== null && !("errorStepName" in error)) { + if (typeof error !== "object" || error === null) return; + if ("errorStepName" in error) return; + if (!Object.isExtensible(error)) return; + try { (error as ErrorWithStepName).errorStepName = stepName; + } catch { + // best-effort metadata only; never mask the original error } }; diff --git a/src/serve/index.ts b/src/serve/index.ts index 5ca2f266..8bbeee45 100644 --- a/src/serve/index.ts +++ b/src/serve/index.ts @@ -338,8 +338,13 @@ export const serveBase = < error: isInstanceOf(error, Error) ? error : new Error(formattedError.message), }); // if the error happened while executing a known step, report its name so - // it can be shown in Workflow Logs when the step is retried - const stepName = getStepNameFromError(error); + // it can be shown in Workflow Logs when the step is retried. + const stepName = getStepNameFromError(error) + // strip control characters (e.g. CR/LF) so an invalid step name + // can't produce an invalid header value and break the 500 response + // eslint-disable-next-line no-control-regex + ?.replace(/[\u0000-\u001F\u007F]+/g, " ") + .trim(); return new Response(JSON.stringify(formattedError), { status: 500, headers: { diff --git a/src/serve/serve.test.ts b/src/serve/serve.test.ts index 0c0673be..57649a79 100644 --- a/src/serve/serve.test.ts +++ b/src/serve/serve.test.ts @@ -399,6 +399,35 @@ describe("serve", () => { expect(onErrorCalled).toBeTrue(); }); + test("should strip control characters from the reported step name", async () => { + const { handler: endpoint } = serve( + async (context) => { + await context.run("wrong\r\nstep", async () => { + throw new Error("some-error"); + }); + }, + { + qstashClient, + receiver: undefined, + } + ); + + const request = getRequest(WORKFLOW_ENDPOINT, "wfr-bar", "my-payload", []); + let called = false; + await mockQStashServer({ + execute: async () => { + const response = await endpoint(request); + expect(response.status).toBe(500); + // CR/LF replaced so the header value stays valid + expect(response.headers.get(WORKFLOW_ERROR_STEP_NAME_HEADER)).toBe("wrong step"); + called = true; + }, + responseFields: { body: { messageId: "some-message-id" }, status: 200 }, + receivesRequest: false, + }); + expect(called).toBeTrue(); + }); + test("should report the failing step name when a parallel step throws", async () => { const { handler: endpoint } = serve( async (context) => { @@ -1195,7 +1224,9 @@ describe("serve", () => { } ); - const response = await handler(new Request("https://wf-test.requestcatcher.com", { method: "POST" })); + const response = await handler( + new Request("https://wf-test.requestcatcher.com", { method: "POST" }) + ); expect(response.status).toBe(500); const content = await response.json(); expect(content).toEqual({ From e2d6a2cd4c32ac8a371378dc4d339fc94cab51e2 Mon Sep 17 00:00:00 2001 From: CahidArda Date: Wed, 10 Jun 2026 16:55:26 +0300 Subject: [PATCH 9/9] style: apply prettier formatting fixes to test files --- src/client/index.test.ts | 3 ++- src/middleware/middleware.test.ts | 12 ++++++++++-- src/serve/serve-many.test.ts | 9 ++++++--- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/client/index.test.ts b/src/client/index.test.ts index 2ac3a449..b459be04 100644 --- a/src/client/index.test.ts +++ b/src/client/index.test.ts @@ -871,7 +871,8 @@ describe("workflow client", () => { "upstash-workflow-runid": `wfr_${myWorkflowRunId}`, "upstash-workflow-url": "https://wf-test.requestcatcher.com/api", "upstash-delay": "1s", - "upstash-failure-callback": "https://wf-test.requestcatcher.com/some-failure-callback", + "upstash-failure-callback": + "https://wf-test.requestcatcher.com/some-failure-callback", "content-type": "application/json", "upstash-feature-set": "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig", "upstash-failure-callback-feature-set": diff --git a/src/middleware/middleware.test.ts b/src/middleware/middleware.test.ts index ec351e4e..817cfa74 100644 --- a/src/middleware/middleware.test.ts +++ b/src/middleware/middleware.test.ts @@ -248,7 +248,10 @@ describe("middleware", () => { await context.sleep(stepThreeName, 10); }; - const qstashClient = new Client({ baseUrl: "https://wf-test.requestcatcher.com", token: "token" }); + const qstashClient = new Client({ + baseUrl: "https://wf-test.requestcatcher.com", + token: "token", + }); qstashClient.http.request = jest.fn(); const runMiddlewareTest = async ( @@ -257,7 +260,12 @@ describe("middleware", () => { ) => { const { middleware, accumulator } = createLoggingMiddleware(); - const request = getRequest("https://wf-test.requestcatcher.com", "wfr-id", undefined, steps); + const request = getRequest( + "https://wf-test.requestcatcher.com", + "wfr-id", + undefined, + steps + ); const { POST: handler } = serve(routeFunction, { middlewares: [middleware], diff --git a/src/serve/serve-many.test.ts b/src/serve/serve-many.test.ts index 48a5c57c..61700128 100644 --- a/src/serve/serve-many.test.ts +++ b/src/serve/serve-many.test.ts @@ -261,7 +261,8 @@ describe("serveMany", () => { "upstash-callback-workflow-calltype": "fromCallback", "upstash-callback-workflow-init": "false", "upstash-callback-workflow-runid": "wfr_id", - "upstash-callback-workflow-url": "https://wf-test.requestcatcher.com/api/workflowThree", + "upstash-callback-workflow-url": + "https://wf-test.requestcatcher.com/api/workflowThree", "upstash-forward-upstash-workflow-invoke-count": "1", "upstash-feature-set": "WF_NoDelete,InitialBody", "upstash-method": "POST", @@ -317,7 +318,8 @@ describe("serveMany", () => { "upstash-callback-workflow-calltype": "fromCallback", "upstash-callback-workflow-init": "false", "upstash-callback-workflow-runid": "wfr_id", - "upstash-callback-workflow-url": "https://wf-test.requestcatcher.com/api/workflowFour", + "upstash-callback-workflow-url": + "https://wf-test.requestcatcher.com/api/workflowFour", "upstash-feature-set": "WF_NoDelete,InitialBody", "upstash-method": "POST", "upstash-retries": "0", @@ -472,7 +474,8 @@ describe("serveMany", () => { "upstash-callback-workflow-calltype": "fromCallback", "upstash-callback-workflow-init": "false", "upstash-callback-workflow-runid": "wfr_id", - "upstash-callback-workflow-url": "https://wf-test.requestcatcher.com/api/workflowSeven", + "upstash-callback-workflow-url": + "https://wf-test.requestcatcher.com/api/workflowSeven", "upstash-feature-set": "WF_NoDelete,InitialBody", "upstash-method": "POST", "upstash-retries": "0",