Compare commits

...

1 Commits

Author SHA1 Message Date
Florian Hussonnois
655de98781 fix(core): allow null val for optional input with default (#11819)
Handles multipart 'null' string as null (no value) for the input

Fixes: #11819
2025-10-13 11:17:25 +02:00
3 changed files with 54 additions and 7 deletions

View File

@@ -130,7 +130,7 @@ public class FlowInputOutput {
private Mono<Map<String, Object>> readData(List<Input<?>> inputs, Execution execution, Publisher<CompletedPart> data, boolean uploadFiles) {
return Flux.from(data)
.publishOn(Schedulers.boundedElastic())
.<AbstractMap.SimpleEntry<String, String>>handle((input, sink) -> {
.<Map.Entry<String, String>>handle((input, sink) -> {
if (input instanceof CompletedFileUpload fileUpload) {
boolean oldStyleInput = false;
if ("files".equals(fileUpload.getName())) {
@@ -150,7 +150,7 @@ public class FlowInputOutput {
.getContextStorageURI()
);
fileUpload.discard();
sink.next(new AbstractMap.SimpleEntry<>(inputId, from.toString()));
sink.next(Map.entry(inputId, from.toString()));
} else {
try {
final String fileExtension = FileInput.findFileInputExtension(inputs, fileName);
@@ -165,7 +165,7 @@ public class FlowInputOutput {
return;
}
URI from = storageInterface.from(execution, inputId, fileName, tempFile);
sink.next(new AbstractMap.SimpleEntry<>(inputId, from.toString()));
sink.next(Map.entry(inputId, from.toString()));
} finally {
if (!tempFile.delete()) {
tempFile.deleteOnExit();
@@ -178,13 +178,13 @@ public class FlowInputOutput {
}
} else {
try {
sink.next(new AbstractMap.SimpleEntry<>(input.getName(), new String(input.getBytes())));
sink.next(Map.entry(input.getName(), new String(input.getBytes())));
} catch (IOException e) {
sink.error(e);
}
}
})
.collectMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue);
.collectMap(Map.Entry::getKey, Map.Entry::getValue);
}
/**
@@ -334,7 +334,7 @@ public class FlowInputOutput {
}
// validate and parse input value
if (value == null) {
if (value == null || "NULL".equals(value.toString().toUpperCase(Locale.ROOT))) {
if (input.getRequired()) {
resolvable.resolveWithError(input.toConstraintViolationException("missing required input", null));
} else {

View File

@@ -19,6 +19,7 @@ import io.micronaut.http.multipart.CompletedFileUpload;
import io.micronaut.http.multipart.CompletedPart;
import io.micronaut.test.annotation.MockBean;
import jakarta.inject.Inject;
import jakarta.validation.ConstraintViolationException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.reactivestreams.Publisher;
@@ -34,6 +35,7 @@ import java.util.Map;
import java.util.Optional;
import static io.kestra.core.tenant.TenantService.MAIN_TENANT;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
@KestraTest
class FlowInputOutputTest {
@@ -335,6 +337,44 @@ class FlowInputOutputTest {
Assertions.assertEquals(TEST_SECRET_VALUE, results.get("input"));
}
@Test
void shouldNotGetDefaultWhenPassingNullForNotRequiredInput() {
// Given
StringInput input = StringInput.builder()
.id("input")
.type(Type.STRING)
.defaults(Property.ofValue("default"))
.required(false)
.build();
Mono<CompletedPart> data = Mono.just(new MemoryCompletedPart("input", "null".getBytes(StandardCharsets.UTF_8)));
// When
Map<String, Object> results = flowInputOutput.readExecutionInputs(List.of(input), null, DEFAULT_TEST_EXECUTION, data).block();
// Then
Assertions.assertNull(results.get("input"));
}
@Test
void shouldNotGetErrorWhenPassingNullForRequiredInput() {
// Given
StringInput input = StringInput.builder()
.id("input")
.type(Type.STRING)
.defaults(Property.ofValue("default"))
.required(true)
.build();
Mono<CompletedPart> data = Mono.just(new MemoryCompletedPart("input", "null".getBytes(StandardCharsets.UTF_8)));
// When
ConstraintViolationException exception = Assertions.assertThrows(ConstraintViolationException.class, () ->
flowInputOutput.readExecutionInputs(List.of(input), null, DEFAULT_TEST_EXECUTION, data).block()
);
assertThat(exception.getMessage()).isEqualTo("input: Invalid input for `input`, missing required input, but received `null`");
}
private static class MemoryCompletedPart implements CompletedPart {
protected final String name;

View File

@@ -5,9 +5,14 @@ export const inputsToFormData = (submitor, inputsList, values) => {
let inputValuesCloned = _cloneDeep(values)
for (const input of inputsList || []) {
if (inputValuesCloned[input.id] === undefined || inputValuesCloned[input.id] === "") {
if (inputValuesCloned[input.id] === undefined) {
delete inputValuesCloned[input.id];
}
// Input was explicitly filled with empty string
if (inputValuesCloned[input.id] === "") {
inputValuesCloned[input.id] = undefined;
}
// Required to have "undefined" value for boolean
if (input.type === "BOOLEAN" && inputValuesCloned[input.id] === "undefined") {
@@ -34,6 +39,8 @@ export const inputsToFormData = (submitor, inputsList, values) => {
} else {
formData.append(inputName, inputValue);
}
} else if (input.type !== "BOOLEAN") {
formData.append(inputName, null);
}
}