From 4a6b936277e854a8d7974afe7a1451268a98466e Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 09:17:32 +0000 Subject: [PATCH] Add input validation for empty event names in waitForExternalEvent() and sendEvent() waitForExternalEvent() and sendEvent() in RuntimeOrchestrationContext did not validate that event names are non-empty. Passing an empty string to waitForExternalEvent() creates a task that can never complete because handleEventRaised uses a truthy check that treats '' as falsy, skipping the pending task lookup entirely. Similarly, sendEvent() accepted empty instanceId and eventName without error. This adds validation consistent with the client-side raiseOrchestrationEvent() method, which already validates event names. Fixes #228 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../worker/runtime-orchestration-context.ts | 12 +++++ .../orchestration_context_methods.spec.ts | 52 +++++++++++++++++++ .../test/orchestration_executor.spec.ts | 24 +++++++++ 3 files changed, 88 insertions(+) diff --git a/packages/durabletask-js/src/worker/runtime-orchestration-context.ts b/packages/durabletask-js/src/worker/runtime-orchestration-context.ts index 2170af7..0c064fe 100644 --- a/packages/durabletask-js/src/worker/runtime-orchestration-context.ts +++ b/packages/durabletask-js/src/worker/runtime-orchestration-context.ts @@ -360,6 +360,10 @@ export class RuntimeOrchestrationContext extends OrchestrationContext { } waitForExternalEvent(name: string): Task { + if (!name) { + throw new Error("waitForExternalEvent: 'name' is required and cannot be empty."); + } + // Check to see if this event has already been received, in which case we // can return it immediately. Otherwise, record out intent to receive an // event with the given name so that we can resume the generator when it @@ -439,6 +443,14 @@ export class RuntimeOrchestrationContext extends OrchestrationContext { * Sends an event to another orchestration instance. */ sendEvent(instanceId: string, eventName: string, eventData?: any): void { + if (!instanceId) { + throw new Error("sendEvent: 'instanceId' is required and cannot be empty."); + } + + if (!eventName) { + throw new Error("sendEvent: 'eventName' is required and cannot be empty."); + } + const id = this.nextSequenceNumber(); const encodedData = eventData !== undefined ? JSON.stringify(eventData) : undefined; const action = ph.newSendEventAction(id, instanceId, eventName, encodedData); diff --git a/packages/durabletask-js/test/orchestration_context_methods.spec.ts b/packages/durabletask-js/test/orchestration_context_methods.spec.ts index e878e38..7e8a045 100644 --- a/packages/durabletask-js/test/orchestration_context_methods.spec.ts +++ b/packages/durabletask-js/test/orchestration_context_methods.spec.ts @@ -312,6 +312,58 @@ describe("OrchestrationContext.sendEvent", () => { // No data should be set (or empty) expect(sendEvent?.getData()?.getValue() ?? "").toEqual(""); }); + + it("should fail the orchestration when eventName is empty", async () => { + const orchestrator: TOrchestrator = async (ctx: OrchestrationContext) => { + ctx.sendEvent("target-instance-id", "", { data: "value" }); + return "done"; + }; + + const registry = new Registry(); + const name = registry.addOrchestrator(orchestrator); + const newEvents = [ + newOrchestratorStartedEvent(), + newExecutionStartedEvent(name, TEST_INSTANCE_ID), + ]; + const executor = new OrchestrationExecutor(registry); + const result = await executor.execute(TEST_INSTANCE_ID, [], newEvents); + + // The orchestration should fail because sendEvent throws on empty eventName + expect(result.actions.length).toEqual(1); + const completeAction = result.actions[0].getCompleteorchestration(); + expect(completeAction?.getOrchestrationstatus()).toEqual( + pb.OrchestrationStatus.ORCHESTRATION_STATUS_FAILED, + ); + expect(completeAction?.getFailuredetails()?.getErrormessage()).toContain( + "'eventName' is required and cannot be empty", + ); + }); + + it("should fail the orchestration when instanceId is empty", async () => { + const orchestrator: TOrchestrator = async (ctx: OrchestrationContext) => { + ctx.sendEvent("", "my-event", { data: "value" }); + return "done"; + }; + + const registry = new Registry(); + const name = registry.addOrchestrator(orchestrator); + const newEvents = [ + newOrchestratorStartedEvent(), + newExecutionStartedEvent(name, TEST_INSTANCE_ID), + ]; + const executor = new OrchestrationExecutor(registry); + const result = await executor.execute(TEST_INSTANCE_ID, [], newEvents); + + // The orchestration should fail because sendEvent throws on empty instanceId + expect(result.actions.length).toEqual(1); + const completeAction = result.actions[0].getCompleteorchestration(); + expect(completeAction?.getOrchestrationstatus()).toEqual( + pb.OrchestrationStatus.ORCHESTRATION_STATUS_FAILED, + ); + expect(completeAction?.getFailuredetails()?.getErrormessage()).toContain( + "'instanceId' is required and cannot be empty", + ); + }); }); describe("OrchestrationContext.newGuid", () => { diff --git a/packages/durabletask-js/test/orchestration_executor.spec.ts b/packages/durabletask-js/test/orchestration_executor.spec.ts index 599453f..66efb45 100644 --- a/packages/durabletask-js/test/orchestration_executor.spec.ts +++ b/packages/durabletask-js/test/orchestration_executor.spec.ts @@ -875,6 +875,30 @@ describe("Orchestration Executor", () => { } }); + it("should fail the orchestration when waitForExternalEvent is called with an empty name", async () => { + const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext, _: any): any { + const res = yield ctx.waitForExternalEvent(""); + return res; + }; + + const registry = new Registry(); + const orchestratorName = registry.addOrchestrator(orchestrator); + + const newEvents = [newOrchestratorStartedEvent(), newExecutionStartedEvent(orchestratorName, TEST_INSTANCE_ID)]; + + const executor = new OrchestrationExecutor(registry, testLogger); + const result = await executor.execute(TEST_INSTANCE_ID, [], newEvents); + + // The orchestration should fail because waitForExternalEvent throws on empty name + const completeAction = getAndValidateSingleCompleteOrchestrationAction(result); + expect(completeAction?.getOrchestrationstatus()).toEqual( + pb.OrchestrationStatus.ORCHESTRATION_STATUS_FAILED, + ); + expect(completeAction?.getFailuredetails()?.getErrormessage()).toContain( + "'name' is required and cannot be empty", + ); + }); + it("should be able to continue-as-new", async () => { for (const saveEvent of [true, false]) { const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext, input: number): any {