Repository: kestra-io/kestra (java)

Issue:
fail to get response from HttpRequest
### Describe the issue

### Environment:
Instance: local standalone docker
Reproduction: Occurs consistently when receiving HTTP responses with status code > 400
Version: Latest

### Issue Description
encounters IllegalArgumentException: Null charset name during error response handling.

2025-03-26 15:02:39.182 java.lang.IllegalArgumentException: Null charset name
	at java.base/java.nio.charset.Charset.lookup(Unknown Source)
	at java.base/java.nio.charset.Charset.forName(Unknown Source)
	at io.kestra.core.http.HttpRequest$RequestBody.from(HttpRequest.java:150)
	at io.kestra.core.http.HttpRequest.from(HttpRequest.java:62)
	at io.kestra.core.http.HttpResponse.from(HttpResponse.java:53)
	at io.kestra.core.http.client.apache.FailedResponseInterceptor.process(FailedResponseInterceptor.java:30)
	at org.apache.hc.core5.http.protocol.DefaultHttpProcessor.process(DefaultHttpProcessor.java:117)
	at org.apache.hc.client5.http.impl.classic.MainClientExec.execute(MainClientExec.java:119)
	at org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51)
	at org.apache.hc.client5.http.impl.classic.ConnectExec.execute(ConnectExec.java:188)
	at org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51)
	at org.apache.hc.client5.http.impl.classic.ProtocolExec.execute(ProtocolExec.java:192)
	at org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51)
	at org.apache.hc.client5.http.impl.classic.HttpRequestRetryExec.execute(HttpRequestRetryExec.java:113)
	at org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51)
	at org.apache.hc.client5.http.impl.classic.ContentCompressionExec.execute(ContentCompressionExec.java:152)
	at org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51)
	at org.apache.hc.client5.http.impl.classic.RedirectExec.execute(RedirectExec.java:116)
	at org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51)
	at org.apache.hc.client5.http.impl.classic.InternalHttpClient.doExecute(InternalHttpClient.java:170)
	at org.apache.hc.client5.http.impl.classic.CloseableHttpClient.execute(CloseableHttpClient.java:245)
	at org.apache.hc.client5.http.impl.classic.CloseableHttpClient.execute(CloseableHttpClient.java:188)
	at io.kestra.core.http.client.HttpClient.request(HttpClient.java:251)
	at io.kestra.core.http.client.HttpClient.request(HttpClient.java:191)
	at io.kestra.plugin.core.http.Request.run(Request.java:291)
	at io.kestra.plugin.core.http.Request.run(Request.java:29)
	at io.kestra.core.runners.WorkerTaskCallable.lambda$doCall$1(WorkerTaskCallable.java:76)
	at dev.failsafe.Functions.lambda$toCtxSupplier$9(Functions.java:228)
	at dev.failsafe.Functions.lambda$get$0(Functions.java:46)
	at dev.failsafe.internal.TimeoutExecutor.lambda$apply$1(TimeoutExecutor.java:93)
	at dev.failsafe.SyncExecutionImpl.executeSync(SyncExecutionImpl.java:187)
	at dev.failsafe.FailsafeExecutor.call(FailsafeExecutor.java:376)
	at dev.failsafe.FailsafeExecutor.run(FailsafeExecutor.java:220)
	at io.kestra.core.runners.WorkerTaskCallable.doCall(WorkerTaskCallable.java:76)
	at io.kestra.core.runners.AbstractWorkerCallable.call(AbstractWorkerCallable.java:62)
	at io.kestra.core.runners.WorkerSecurityService.callInSecurityContext(WorkerSecurityService.java:10)
	at io.kestra.core.runners.Worker.lambda$callJob$19(Worker.java:837)
	at io.kestra.core.trace.NoopTracer.inCurrentContext(NoopTracer.java:15)
	at io.kestra.core.runners.Worker.callJob(Worker.java:833)
	at io.kestra.core.runners.Worker.runAttempt(Worker.java:792)
	at io.kestra.core.runners.Worker.run(Worker.java:640)
	at io.kestra.core.runners.Worker.handleTask(Worker.java:332)
	at io.kestra.core.runners.Worker.lambda$run$8(Worker.java:261)
	at io.micrometer.core.instrument.internal.TimedRunnable.run(TimedRunnable.java:49)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
	at java.base/java.lang.Thread.run(Unknown Source)

at io.kestra.core.http.HttpRequest$RequestBody.from(HttpRequest.java:150):
Charset charset = entity.getContentEncoding() != null ? Charset.forName(entity.getContentEncoding()) : StandardCharsets.UTF_8;

Is there a specific reason for using Content-Encoding  (e.g., gzip/deflate) for charset detection?


### Environment

- Kestra Version: develop


Pull request:
fix(core): charset should not be taken from
Fixes #8072

Patch:
diff --git a/core/src/main/java/io/kestra/core/http/HttpRequest.java b/core/src/main/java/io/kestra/core/http/HttpRequest.java
index 4c41b8e01..123dc5fba 100644
--- a/core/src/main/java/io/kestra/core/http/HttpRequest.java
+++ b/core/src/main/java/io/kestra/core/http/HttpRequest.java
@@ -157,25 +157,32 @@ public class HttpRequest {
                 return null;
             }
 
-            Charset charset = entity.getContentEncoding() != null ? Charset.forName(entity.getContentEncoding()) : StandardCharsets.UTF_8;
-
-            if (entity.getContentType().equals(ContentType.APPLICATION_OCTET_STREAM.getMimeType())) {
+            String[] parts = entity.getContentType().split(";");
+            String mimeType = parts[0];
+            Charset charset = StandardCharsets.UTF_8;
+            for (String part : parts) {
+                String stripped = part.strip();
+                if (stripped.startsWith("charset")) {
+                    charset = Charset.forName(stripped.substring(stripped.lastIndexOf('=') + 1));
+                }
+            }
+            if (mimeType.equals(ContentType.APPLICATION_OCTET_STREAM.getMimeType())) {
                 return ByteArrayRequestBody.builder()
-                    .contentType(entity.getContentType())
+                    .contentType(mimeType)
                     .charset(charset)
                     .content(IOUtils.toByteArray(entity.getContent()))
                     .build();
             }
 
-            if (entity.getContentType().equals(ContentType.TEXT_PLAIN.getMimeType())) {
+            if (mimeType.equals(ContentType.TEXT_PLAIN.getMimeType())) {
                 return StringRequestBody.builder()
-                    .contentType(entity.getContentType())
+                    .contentType(mimeType)
                     .charset(charset)
                     .content(IOUtils.toString(entity.getContent(), charset))
                     .build();
             }
 
-            if (entity.getContentType().equals(ContentType.APPLICATION_JSON.getMimeType())) {
+            if (mimeType.equals(ContentType.APPLICATION_JSON.getMimeType())) {
                 return JsonRequestBody.builder()
                     .charset(charset)
                     .content(JacksonMapper.toObject(IOUtils.toString(entity.getContent(), charset)))
@@ -184,7 +191,7 @@ public class HttpRequest {
 
             return ByteArrayRequestBody.builder()
                 .charset(charset)
-                .contentType(entity.getContentType())
+                .contentType(mimeType)
                 .content(entity.getContent().readAllBytes())
                 .build();
         }
diff --git a/ui/src/assets/docs/basic.md b/ui/src/assets/docs/basic.md
index c9fa86eae..ab7e72dff 100644
--- a/ui/src/assets/docs/basic.md
+++ b/ui/src/assets/docs/basic.md
@@ -205,6 +205,8 @@ The table below lists common Pebble expressions and functions.
 | `{{ trigger.flowId }}`                                                                             | The ID of the flow that triggers the current flow.                                                                              |
 | `{{ trigger.flowRevision }}`                                                                       | The revision of the flow that triggers the current flow.                                                                        |
 | `{{ envs.foo }}`                                                                                   | Accesses environment variable `KESTRA_FOO`.                                                                                     |
+| `{{ kestra.environment }}`                                                                                   | Accesses Environment variables such as `kestra.environment.name.`                                                                                     |
+| `{{ kestra.url }}`                                                                                   | Accesses Environment URL variable.                                                                                     |
 | `{{ globals.foo }}`                                                                                | Accesses global variable `foo`.                                                                                                 |
 | `{{ vars.my_variable }}`                                                                           | Accesses flow variable `my_variable`.                                                                                           |
 | `{{ inputs.myInput }}`                                                                             | Accesses flow input `myInput`.                                                                                                  |


Test patch:
diff --git a/core/src/test/java/io/kestra/core/http/client/HttpClientTest.java b/core/src/test/java/io/kestra/core/http/client/HttpClientTest.java
index 2bd7383ca..264720f4c 100644
--- a/core/src/test/java/io/kestra/core/http/client/HttpClientTest.java
+++ b/core/src/test/java/io/kestra/core/http/client/HttpClientTest.java
@@ -351,6 +351,17 @@ class HttpClientTest {
         }
     }
 
+    @Test
+    void noErrorPost404() throws IOException, IllegalVariableEvaluationException, HttpClientException {
+        try (HttpClient client = client(b -> b.configuration(HttpConfiguration.builder().allowFailed(Property.of(true)).build()))) {
+            URI uri = URI.create(embeddedServerUri + "/http/post-error");
+
+            HttpResponse<Map<String, String>> response = client.request(HttpRequest.builder().uri(uri).method("POST").body(HttpRequest.StringRequestBody.builder().content("OK").build()).build());
+
+            assertThat(response.getStatus().getCode(), is(404));
+        }
+    }
+
     @Test
     void specialContentType() throws IllegalVariableEvaluationException, HttpClientException, IOException {
         try (HttpClient client = client()) {
@@ -431,6 +442,13 @@ class HttpClientTest {
                 .body(Map.of("status", status));
         }
 
+        @Post("error")
+        public io.micronaut.http.HttpResponse<Object> postErrors(@QueryValue int status, @Body String body) {
+            return io.micronaut.http.HttpResponse
+                .status(HttpStatus.valueOf(status))
+                .body(Map.of("status", status));
+        }
+
         @ExecuteOn(TaskExecutors.IO)
         @Post(uri = "multipart", consumes = MediaType.MULTIPART_FORM_DATA)
         public io.micronaut.http.HttpResponse<Object> multipartPost(@Body MultipartBody body) {