From 87d767a4462808539d5a7132bffd7c3ca3965496 Mon Sep 17 00:00:00 2001 From: Matheus Cruz Date: Thu, 13 Aug 2026 21:25:44 -0300 Subject: [PATCH 1/2] [Fix #1625] Add null check for required 'in' property in ForExecutorBuilder ForExecutorBuilder.buildCollectionFilter() dereferences task.getFor().getIn() without a null check. Programmatically-built workflows bypass schema validation, so a ForTask without 'in' set causes a raw NullPointerException with no context. Added Objects.requireNonNull with a descriptive message and a test that verifies the error when 'in' is missing. Signed-off-by: Matheus Cruz --- .../impl/executors/ForExecutor.java | 4 +- .../impl/test/ForTaskMissingInTest.java | 44 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 impl/test/src/test/java/io/serverlessworkflow/impl/test/ForTaskMissingInTest.java diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForExecutor.java index 4eeb69ff7..d19b9bf90 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForExecutor.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForExecutor.java @@ -28,6 +28,7 @@ import io.serverlessworkflow.impl.expressions.ExpressionDescriptor; import java.util.Collection; import java.util.Iterator; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -51,7 +52,8 @@ protected Optional buildWhileFilter() { } protected WorkflowValueResolver> buildCollectionFilter() { - In in = task.getFor().getIn(); + In in = + Objects.requireNonNull(task.getFor().getIn(), "'in' is a required property for ForTask"); return application .expressionFactory() .resolveCollection( diff --git a/impl/test/src/test/java/io/serverlessworkflow/impl/test/ForTaskMissingInTest.java b/impl/test/src/test/java/io/serverlessworkflow/impl/test/ForTaskMissingInTest.java new file mode 100644 index 000000000..dd8ee757b --- /dev/null +++ b/impl/test/src/test/java/io/serverlessworkflow/impl/test/ForTaskMissingInTest.java @@ -0,0 +1,44 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.impl.test; + +import static io.serverlessworkflow.fluent.spec.dsl.DSL.*; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.fluent.spec.WorkflowBuilder; +import io.serverlessworkflow.impl.WorkflowApplication; +import org.junit.jupiter.api.Test; + +class ForTaskMissingInTest { + + @Test + void forTaskWithoutInShouldFailWithDescriptiveMessage() { + Workflow workflow = + WorkflowBuilder.workflow("for-missing-in", "test", "0.1.0") + .tasks( + doTasks( + forEach( + "loopWithoutIn", + f -> f.each("item").tasks(t -> t.set("noop", s -> s.put("done", true)))))) + .build(); + try (WorkflowApplication app = WorkflowApplication.builder().build()) { + assertThatThrownBy(() -> app.workflowDefinition(workflow)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("'in' is a required property for ForTask"); + } + } +} From fa84da7397aa2cab6d86d90fcdef52a6d1920a45 Mon Sep 17 00:00:00 2001 From: Matheus Cruz Date: Mon, 31 Aug 2026 10:09:44 -0300 Subject: [PATCH 2/2] Do a early validation (during workflow build) Signed-off-by: Matheus Cruz --- .../fluent/spec/ForEachTaskBuilder.java | 3 ++ .../fluent/spec/WorkflowBuilderTest.java | 2 +- .../impl/executors/ForExecutor.java | 4 +-- .../impl/test/ForTaskMissingInTest.java | 30 ++++++++++--------- 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/ForEachTaskBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/ForEachTaskBuilder.java index 196ed888e..21ef0e9bc 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/ForEachTaskBuilder.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/ForEachTaskBuilder.java @@ -21,6 +21,7 @@ import io.serverlessworkflow.api.types.TaskItem; import io.serverlessworkflow.fluent.spec.spi.ForEachTaskFluent; import java.util.List; +import java.util.Objects; import java.util.function.Consumer; public class ForEachTaskBuilder> @@ -85,6 +86,8 @@ public ForEachTaskBuilder tasks(Consumer doBuilderConsumer) { public ForTask build() { this.forTask.setFor(this.forTaskConfiguration); + Objects.requireNonNull( + this.forTask.getFor().getIn(), "'in' is a required property for ForTask"); return this.forTask; } } diff --git a/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/WorkflowBuilderTest.java b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/WorkflowBuilderTest.java index 4ba41c203..57af6a590 100644 --- a/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/WorkflowBuilderTest.java +++ b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/WorkflowBuilderTest.java @@ -119,7 +119,7 @@ void testDoTaskSetAndForEach() { .tasks( d -> d.set("initCtx", "$.foo = 'bar'") - .forEach("item", f -> f.each("item").at("$.list"))) + .forEach("item", f -> f.each("item").at("index").in("$.list"))) .build(); List items = wf.getDo(); diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForExecutor.java index d19b9bf90..4eeb69ff7 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForExecutor.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForExecutor.java @@ -28,7 +28,6 @@ import io.serverlessworkflow.impl.expressions.ExpressionDescriptor; import java.util.Collection; import java.util.Iterator; -import java.util.Objects; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -52,8 +51,7 @@ protected Optional buildWhileFilter() { } protected WorkflowValueResolver> buildCollectionFilter() { - In in = - Objects.requireNonNull(task.getFor().getIn(), "'in' is a required property for ForTask"); + In in = task.getFor().getIn(); return application .expressionFactory() .resolveCollection( diff --git a/impl/test/src/test/java/io/serverlessworkflow/impl/test/ForTaskMissingInTest.java b/impl/test/src/test/java/io/serverlessworkflow/impl/test/ForTaskMissingInTest.java index dd8ee757b..88c3a7a9b 100644 --- a/impl/test/src/test/java/io/serverlessworkflow/impl/test/ForTaskMissingInTest.java +++ b/impl/test/src/test/java/io/serverlessworkflow/impl/test/ForTaskMissingInTest.java @@ -20,25 +20,27 @@ import io.serverlessworkflow.api.types.Workflow; import io.serverlessworkflow.fluent.spec.WorkflowBuilder; -import io.serverlessworkflow.impl.WorkflowApplication; import org.junit.jupiter.api.Test; class ForTaskMissingInTest { @Test void forTaskWithoutInShouldFailWithDescriptiveMessage() { - Workflow workflow = - WorkflowBuilder.workflow("for-missing-in", "test", "0.1.0") - .tasks( - doTasks( - forEach( - "loopWithoutIn", - f -> f.each("item").tasks(t -> t.set("noop", s -> s.put("done", true)))))) - .build(); - try (WorkflowApplication app = WorkflowApplication.builder().build()) { - assertThatThrownBy(() -> app.workflowDefinition(workflow)) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("'in' is a required property for ForTask"); - } + + assertThatThrownBy( + () -> { + Workflow ignored = + WorkflowBuilder.workflow("for-missing-in", "test", "0.1.0") + .tasks( + doTasks( + forEach( + "loopWithoutIn", + f -> + f.each("item") + .tasks(t -> t.set("noop", s -> s.put("done", true)))))) + .build(); + }) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("'in' is a required property for ForTask"); } }