Repository: kestra-io/kestra (java)

Issue:
 Image URL with HTTP Request type causes Kestra to crash
### Describe the issue

If you use `io.kestra.plugin.core.http.Request` and pass in a URL to a JPG, it causes the Kestra to crash. Works as expected for `io.kestra.plugin.core.http.Download`

```yaml
id: myflow2
namespace: company.myteam

tasks:

  - id: download
    type: io.kestra.plugin.core.http.Request
    uri: https://github.com/kestra-io.png

  - id: hello
    type: io.kestra.plugin.core.log.Log
    message: "{{ outputs.download.uri }}"
```

Error from Docker logs:
```
kestra-1    | 2024-06-24 11:01:58,983 INFO  worker_109   flow.myflow2.download [namespace: company.myteam] [flow: myflow2] [task: download] [execution: 3gfkcL9ocY0mS2x8QJwsLp] [taskrun: LohnZ3nVjVvAhbQ9Npd8d] Type Request started
postgres-1  | 2024-06-24 11:01:59.191 UTC [14315] ERROR:  unsupported Unicode escape sequence
postgres-1  | 2024-06-24 11:01:59.191 UTC [14315] DETAIL:  \u0000 cannot be converted to text.
postgres-1  | 2024-06-24 11:01:59.191 UTC [14315] CONTEXT:  JSON data, line 1: ...tream-id":["3"]},"body":"�PNG\r\n\u001A\n\u0000...
postgres-1  | 2024-06-24 11:01:59.191 UTC [14315] STATEMENT:  insert into queues ("value", "type", "key") values (cast($1 as jsonb), CAST($2 AS queue_type), $3)
kestra-1    | 2024-06-24 11:01:59,199 INFO  worker_109   flow.myflow2.download [namespace: company.myteam] [flow: myflow2] [task: download] [execution: 3gfkcL9ocY0mS2x8QJwsLp] [taskrun: LohnZ3nVjVvAhbQ9Npd8d] Type Request with state SUCCESS completed in 00:00:00.399
kestra-1    | 2024-06-24 11:01:59,200 ERROR worker_109   .c.u.ThreadUncaughtExceptionHandlers Caught an exception in Thread[#3945,worker_109,5,main]. Shutting down.
kestra-1    | org.jooq.exception.DataException: SQL [insert into queues ("value", "type", "key") values (cast(? as jsonb), CAST(? AS queue_type), ?)]; ERROR: unsupported Unicode escape sequence
kestra-1    |   Detail: \u0000 cannot be converted to text.
kestra-1    |   Where: JSON data, line 1: ...tream-id":["3"]},"body":"�PNG\r\n\u001A\n\u0000...
kestra-1    | 	at org.jooq_3.19.7.POSTGRES.debug(Unknown Source)
kestra-1    | 	at org.jooq.impl.Tools.translate(Tools.java:3568)
kestra-1    | 	at org.jooq.impl.Tools.translate(Tools.java:3560)
kestra-1    | 	at org.jooq.impl.DefaultExecuteContext.sqlException(DefaultExecuteContext.java:827)
kestra-1    | 	at org.jooq.impl.AbstractQuery.execute(AbstractQuery.java:362)
kestra-1    | 	at org.jooq.impl.AbstractDelegatingQuery.execute(AbstractDelegatingQuery.java:115)
kestra-1    | 	at io.kestra.jdbc.runner.JdbcQueue.lambda$produce$0(JdbcQueue.java:111)
kestra-1    | 	at org.jooq.impl.DefaultDSLContext.lambda$transaction$5(DefaultDSLContext.java:592)
kestra-1    | 	at org.jooq.impl.DefaultDSLContext.lambda$transactionResult0$3(DefaultDSLContext.java:530)
kestra-1    | 	at org.jooq.impl.Tools$3$1.block(Tools.java:6297)
kestra-1    | 	at java.base/java.util.concurrent.ForkJoinPool.unmanagedBlock(Unknown Source)
kestra-1    | 	at java.base/java.util.concurrent.ForkJoinPool.managedBlock(Unknown Source)
kestra-1    | 	at org.jooq.impl.Tools$3.get(Tools.java:6294)
kestra-1    | 	at org.jooq.impl.DefaultDSLContext.transactionResult0(DefaultDSLContext.java:578)
kestra-1    | 	at org.jooq.impl.DefaultDSLContext.transactionResult(DefaultDSLContext.java:502)
kestra-1    | 	at org.jooq.impl.DefaultDSLContext.transaction(DefaultDSLContext.java:591)
kestra-1    | 	at io.kestra.jdbc.JooqDSLContextWrapper.lambda$transaction$1(JooqDSLContextWrapper.java:58)
kestra-1    | 	at dev.failsafe.Functions.lambda$toCtxSupplier$11(Functions.java:243)
kestra-1    | 	at dev.failsafe.Functions.lambda$get$0(Functions.java:46)
kestra-1    | 	at dev.failsafe.internal.RetryPolicyExecutor.lambda$apply$0(RetryPolicyExecutor.java:74)
kestra-1    | 	at dev.failsafe.internal.FallbackExecutor.lambda$apply$0(FallbackExecutor.java:51)
kestra-1    | 	at dev.failsafe.SyncExecutionImpl.executeSync(SyncExecutionImpl.java:187)
kestra-1    | 	at dev.failsafe.FailsafeExecutor.call(FailsafeExecutor.java:376)
kestra-1    | 	at dev.failsafe.FailsafeExecutor.get(FailsafeExecutor.java:112)
kestra-1    | 	at io.kestra.core.utils.RetryUtils$Instance.wrap(RetryUtils.java:143)
kestra-1    | 	at io.kestra.core.utils.RetryUtils$Instance.runRetryIf(RetryUtils.java:102)
kestra-1    | 	at io.kestra.jdbc.JooqDSLContextWrapper.transaction(JooqDSLContextWrapper.java:55)
kestra-1    | 	at io.kestra.jdbc.runner.JdbcQueue.produce(JdbcQueue.java:101)
kestra-1    | 	at io.kestra.jdbc.runner.JdbcQueue.emit(JdbcQueue.java:121)
kestra-1    | 	at io.kestra.core.queues.QueueInterface.emit(QueueInterface.java:11)
kestra-1    | 	at io.kestra.core.runners.Worker.run(Worker.java:542)
kestra-1    | 	at io.kestra.core.runners.Worker.handleTask(Worker.java:264)
kestra-1    | 	at io.kestra.core.runners.Worker.lambda$run$7(Worker.java:223)
kestra-1    | 	at io.micrometer.core.instrument.internal.TimedRunnable.run(TimedRunnable.java:49)
kestra-1    | 	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
kestra-1    | 	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
kestra-1    | 	at java.base/java.lang.Thread.run(Unknown Source)
kestra-1    | Caused by: org.postgresql.util.PSQLException: ERROR: unsupported Unicode escape sequence
kestra-1    |   Detail: \u0000 cannot be converted to text.
kestra-1    |   Where: JSON data, line 1: ...tream-id":["3"]},"body":"�PNG\r\n\u001A\n\u0000...
kestra-1    | 	at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2725)
kestra-1    | 	at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2412)
kestra-1    | 	at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:371)
kestra-1    | 	at org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:502)
kestra-1    | 	at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:419)
kestra-1    | 	at org.postgresql.jdbc.PgPreparedStatement.executeWithFlags(PgPreparedStatement.java:194)
kestra-1    | 	at org.postgresql.jdbc.PgPreparedStatement.execute(PgPreparedStatement.java:180)
kestra-1    | 	at com.zaxxer.hikari.pool.ProxyPreparedStatement.execute(ProxyPreparedStatement.java:44)
kestra-1    | 	at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.execute(HikariProxyPreparedStatement.java)
kestra-1    | 	at org.jooq.tools.jdbc.DefaultPreparedStatement.execute(DefaultPreparedStatement.java:219)
kestra-1    | 	at org.jooq.impl.AbstractQuery.execute(AbstractQuery.java:458)
kestra-1    | 	at org.jooq.impl.AbstractDMLQuery.execute(AbstractDMLQuery.java:1068)
kestra-1    | 	at org.jooq.impl.AbstractQuery.execute(AbstractQuery.java:348)
kestra-1    | 	... 32 common frames omitted
kestra-1    | 2024-06-24 11:01:59,223 WARN  worker_109   io.kestra.core.runners.Worker Waiting for all worker threads to terminate (remaining: 0).
kestra-1    | Exception in thread "jdbc-delay-exception-watcher" java.util.concurrent.CancellationException
kestra-1    | 	at java.base/java.util.concurrent.FutureTask.report(Unknown Source)
kestra-1    | 	at java.base/java.util.concurrent.FutureTask.get(Unknown Source)
kestra-1    | 	at io.kestra.jdbc.runner.JdbcExecutor.lambda$run$2(JdbcExecutor.java:235)
kestra-1    | 	at java.base/java.lang.VirtualThread.run(Unknown Source)
kestra-1    | 2024-06-24 11:02:00,225 INFO  worker_109   io.kestra.core.runners.Worker All working threads are terminated.
kestra-1    | 2024-06-24 11:02:00,268 WARN  command-shutdown io.kestra.cli.AbstractCommand Receiving shutdown ! Try to graceful exit
kestra-1    | 2024-06-24 11:02:00,268 INFO  command-shutdown i.kestra.core.contexts.KestraContext Kestra server - Shutdown initiated
kestra-1    | 2024-06-24 11:02:00,268 INFO  command-shutdown i.kestra.core.contexts.KestraContext Kestra server - Shutdown completed
kestra-1 exited with code 1
Gracefully stopping... (press Ctrl+C again to force)
```

### Environment

- Kestra Version: 0.18 Snapshot
- Operating System (OS/Docker/Kubernetes):
- Java Version (if you don't run kestra in Docker):


Pull request:
fix(core): the Request task can crash the worker
Fixes #4115


Patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index f29d0d64d..a1acab0b2 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -16,9 +16,9 @@ on:
     inputs:
       skip-test:
         description: 'Skip test'
-        required: false
-        type: string
-        default: "false"
+        type: choice
+        required: true
+        default: 'false'
         options:
           - "true"
           - "false"
@@ -57,7 +57,7 @@ jobs:
       # Services
       - name: Build the docker-compose stack
         run: docker compose -f docker-compose-ci.yml up -d
-        if: ${{ github.event.inputs.skip-test == 'false' || github.event.inputs.skip-test == '' }}
+        if: ${{ github.event.inputs.skip-test == 'false' }}
 
       # Caches
       - name: Gradle cache
@@ -105,7 +105,7 @@ jobs:
 
       # Gradle check
       - name: Build with Gradle
-        if: ${{ github.event.inputs.skip-test == 'false' || github.event.inputs.skip-test == '' }}
+        if: ${{ github.event.inputs.skip-test == 'false' }}
         env:
           GOOGLE_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }}
         run: |
@@ -116,7 +116,7 @@ jobs:
 
       # Sonar
       - name: Analyze with Sonar
-        if: ${{ env.SONAR_TOKEN != 0 && (github.event.inputs.skip-test == 'false' || github.event.inputs.skip-test == '') }}
+        if: ${{ env.SONAR_TOKEN != 0 && github.event.inputs.skip-test == 'false' }}
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
@@ -134,7 +134,7 @@ jobs:
 
       - name: Publish allure report
         uses: andrcuns/allure-publish-action@v2.6.0
-        if: ${{ always() && env.GOOGLE_SERVICE_ACCOUNT != 0 && (github.event.inputs.skip-test == 'false' || github.event.inputs.skip-test == '') }}
+        if: ${{ always() && env.GOOGLE_SERVICE_ACCOUNT != 0 && github.event.inputs.skip-test == 'false' }}
         env:
           GITHUB_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           JAVA_HOME: /usr/lib/jvm/default-jvm/
@@ -149,11 +149,11 @@ jobs:
 
       # Jacoco
       - name: 'Set up Cloud SDK'
-        if: ${{ env.GOOGLE_SERVICE_ACCOUNT != 0 }}
+        if: ${{ env.GOOGLE_SERVICE_ACCOUNT != 0 && github.event.inputs.skip-test == 'false' }}
         uses: 'google-github-actions/setup-gcloud@v2'
 
       - name: 'Copy jacoco files'
-        if: ${{ env.GOOGLE_SERVICE_ACCOUNT != 0 }}
+        if: ${{ env.GOOGLE_SERVICE_ACCOUNT != 0 && github.event.inputs.skip-test == 'false' }}
         run: |
           mv build/reports/jacoco/testCodeCoverageReport build/reports/jacoco/test/
           mv build/reports/jacoco/test/testCodeCoverageReport.xml build/reports/jacoco/jacocoTestReport.xml
@@ -168,7 +168,7 @@ jobs:
 
       # Codecov
       - uses: codecov/codecov-action@v4
-        if: ${{ github.event.inputs.skip-test == 'false' || github.event.inputs.skip-test == '' }}
+        if: ${{ github.event.inputs.skip-test == 'false' }}
         with:
           token: ${{ secrets.CODECOV_TOKEN }}
 
diff --git a/core/src/main/java/io/kestra/plugin/core/http/Request.java b/core/src/main/java/io/kestra/plugin/core/http/Request.java
index 476b07a48..ffc837c16 100644
--- a/core/src/main/java/io/kestra/plugin/core/http/Request.java
+++ b/core/src/main/java/io/kestra/plugin/core/http/Request.java
@@ -21,6 +21,7 @@ import java.net.URI;
 import java.security.GeneralSecurityException;
 import java.util.List;
 import java.util.Map;
+import java.util.OptionalInt;
 
 @SuperBuilder
 @ToString
@@ -122,6 +123,16 @@ public class Request extends AbstractHttp implements RunnableTask<Request.Output
                 response = client
                     .toBlocking()
                     .exchange(request, Argument.STRING, Argument.STRING);
+
+                // check that the string is a valid Unicode string
+                if (response.getBody().isPresent()) {
+                    OptionalInt illegalChar = response.body().chars().filter(c -> !Character.isDefined(c)).findFirst();
+                    if (illegalChar.isPresent()) {
+                        throw new IllegalArgumentException("Illegal unicode code point in request body: " + illegalChar.getAsInt() +
+                            ", the Request task only support valid Unicode strings as body.\n" +
+                            "You can try using the Download task instead.");
+                    }
+                }
             } catch (HttpClientResponseException e) {
                 if (!allowFailed) {
                     throw e;
diff --git a/ui/src/components/inputs/EditorSidebar.vue b/ui/src/components/inputs/EditorSidebar.vue
index c2081ebd0..d4c37f412 100644
--- a/ui/src/components/inputs/EditorSidebar.vue
+++ b/ui/src/components/inputs/EditorSidebar.vue
@@ -61,7 +61,6 @@
                 <input
                     ref="folderPicker"
                     type="file"
-                    multiple
                     webkitdirectory
                     mozdirectory
                     msdirectory
@@ -77,7 +76,7 @@
                     <template #dropdown>
                         <el-dropdown-menu>
                             <el-dropdown-item @click="$refs.filePicker.click()">
-                                {{ $t("namespace files.import.file") }}
+                                {{ $t("namespace files.import.files") }}
                             </el-dropdown-item>
                             <el-dropdown-item
                                 @click="$refs.folderPicker.click()"
diff --git a/ui/src/translations/en.json b/ui/src/translations/en.json
index 4f850ac39..c49b7aa20 100644
--- a/ui/src/translations/en.json
+++ b/ui/src/translations/en.json
@@ -644,7 +644,7 @@
       },
       "import": {
         "import": "Import",
-        "file": "Import file",
+        "files": "Import files",
         "folder": "Import folder",
         "success": "File(s) successfully imported",
         "error": "Error(s) occurred while importing the file(s)"
diff --git a/ui/src/translations/fr.json b/ui/src/translations/fr.json
index d6cffb533..c4ae040e5 100644
--- a/ui/src/translations/fr.json
+++ b/ui/src/translations/fr.json
@@ -616,7 +616,7 @@
       },
       "import": {
           "import": "Importer",
-          "file": "Importer un fichier",
+          "files": "Importer des fichiers",
           "folder": "Importer un dossier",
           "success": "Fichier(s) importé(s) avec succès",
           "error": "Des erreurs se sont produites lors de l'importation du(des) fichier(s)"


Test patch:
diff --git a/core/src/test/java/io/kestra/core/runners/pebble/filters/ValuesFilterTest.java b/core/src/test/java/io/kestra/core/runners/pebble/filters/ValuesFilterTest.java
index 32b113291..5e1a64a9d 100644
--- a/core/src/test/java/io/kestra/core/runners/pebble/filters/ValuesFilterTest.java
+++ b/core/src/test/java/io/kestra/core/runners/pebble/filters/ValuesFilterTest.java
@@ -1,20 +1,23 @@
 package io.kestra.core.runners.pebble.filters;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
 import com.google.common.collect.ImmutableMap;
 import io.kestra.core.exceptions.IllegalVariableEvaluationException;
 import io.kestra.core.junit.annotations.KestraTest;
 import io.kestra.core.runners.VariableRenderer;
+import io.kestra.core.serializers.JacksonMapper;
 import jakarta.inject.Inject;
 import org.junit.jupiter.api.Test;
 
 import java.time.ZoneId;
 import java.time.ZonedDateTime;
 import java.util.Arrays;
+import java.util.List;
 import java.util.Map;
 
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.containsString;
-import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.*;
 
 @KestraTest
 class ValuesFilterTest {
@@ -22,7 +25,7 @@ class ValuesFilterTest {
     VariableRenderer variableRenderer;
 
     @Test
-    void map() throws IllegalVariableEvaluationException {
+    void map() throws IllegalVariableEvaluationException, JsonProcessingException {
         ZonedDateTime date = ZonedDateTime.parse("2013-09-08T16:19:00+02").withZoneSameLocal(ZoneId.systemDefault());
 
         ImmutableMap<String, Object> vars = ImmutableMap.of(
@@ -39,11 +42,13 @@ class ValuesFilterTest {
         );
 
         String render = variableRenderer.render("{{ vars.second.map | values }}", vars);
-        assertThat(render, containsString("\"string\""));
-        assertThat(render, containsString("123"));
+        List<Object> list = JacksonMapper.ofJson().readValue(render, new TypeReference<>() {});
+        assertThat(list, hasItems("string"));
+        assertThat(list, hasItems(1.123));
 
         render = variableRenderer.render("{{ vars.second | values }}", vars);
-        assertThat(render, containsString("\"string\""));
-        assertThat(render, containsString("true,"));
+        list = JacksonMapper.ofJson().readValue(render, new TypeReference<>() {});
+        assertThat(list, hasItems("string"));
+        assertThat(list, hasItems(true));
     }
 }