From dd95efc94a5a0c79a2bd7b33dd9a6054137d4437 Mon Sep 17 00:00:00 2001 From: fjtirado Date: Tue, 25 Nov 2025 12:48:22 +0100 Subject: [PATCH] [Fix #1014] Supporting PUT, PATCH, OPTIONS, HEAD, DELETE methods Signed-off-by: fjtirado --- .../impl/WorkflowError.java | 5 ++ .../http/AbstractHttpExecutorBuilder.java | 34 +++++----- .../http/AbstractRequestSupplier.java | 63 +++++++++++++++++++ .../http/CallableTaskHttpExecutorBuilder.java | 5 +- .../impl/executors/http/HttpExecutor.java | 14 +---- .../executors/http/HttpExecutorBuilder.java | 3 +- .../executors/http/HttpModelConverter.java | 18 ++++++ .../impl/executors/http/RequestSupplier.java | 2 +- .../http/WithBodyRequestSupplier.java | 53 ++++++++++++++++ .../http/WithoutBodyRequestSupplier.java | 46 ++++++++++++++ .../impl/test/OpenAPITest.java | 1 - .../impl/test/RetryTimeoutTest.java | 2 +- .../call-custom-function-inline.yaml | 2 +- 13 files changed, 211 insertions(+), 37 deletions(-) create mode 100644 impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/AbstractRequestSupplier.java create mode 100644 impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/WithBodyRequestSupplier.java create mode 100644 impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/WithoutBodyRequestSupplier.java diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowError.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowError.java index bb44f3e6..4e693bb5 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowError.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowError.java @@ -42,6 +42,11 @@ public static Builder communication(int status, TaskContext context, String titl .title(title); } + public static Builder communication(int status, TaskContext context) { + return new Builder(Errors.COMMUNICATION.toString(), status) + .instance(context.position().jsonPointer()); + } + public static Builder communication(TaskContext context, String title) { return communication(Errors.COMMUNICATION.status(), context, title); } diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/AbstractHttpExecutorBuilder.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/AbstractHttpExecutorBuilder.java index 78dc86e3..18eb64a2 100644 --- a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/AbstractHttpExecutorBuilder.java +++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/AbstractHttpExecutorBuilder.java @@ -16,10 +16,9 @@ package io.serverlessworkflow.impl.executors.http; import io.serverlessworkflow.impl.WorkflowApplication; -import io.serverlessworkflow.impl.WorkflowFilter; -import io.serverlessworkflow.impl.WorkflowUtils; import io.serverlessworkflow.impl.WorkflowValueResolver; import jakarta.ws.rs.HttpMethod; +import jakarta.ws.rs.client.Invocation; import jakarta.ws.rs.client.WebTarget; import java.net.URI; import java.util.Map; @@ -32,30 +31,27 @@ abstract class AbstractHttpExecutorBuilder { protected WorkflowValueResolver> queryMap; protected Optional authProvider = Optional.empty(); protected RequestSupplier requestFunction; - protected boolean redirect; protected static RequestSupplier buildRequestSupplier( - String method, Object body, WorkflowApplication application) { + String method, Object body, boolean redirect, WorkflowApplication application) { switch (method.toUpperCase()) { case HttpMethod.POST: - WorkflowFilter bodyFilter = WorkflowUtils.buildWorkflowFilter(application, body); - return (request, w, t, node) -> { - HttpModelConverter converter = HttpConverterResolver.converter(w, t); - return w.definition() - .application() - .modelFactory() - .fromAny( - request.post( - converter.toEntity(bodyFilter.apply(w, t, node)), converter.responseType())); - }; + return new WithBodyRequestSupplier(Invocation.Builder::post, application, body, redirect); + case HttpMethod.PUT: + return new WithBodyRequestSupplier(Invocation.Builder::put, application, body, redirect); + case HttpMethod.DELETE: + return new WithoutBodyRequestSupplier(Invocation.Builder::delete, application, redirect); + case HttpMethod.HEAD: + return new WithoutBodyRequestSupplier(Invocation.Builder::head, application, redirect); + case HttpMethod.OPTIONS: + return new WithoutBodyRequestSupplier(Invocation.Builder::options, application, redirect); + case HttpMethod.PATCH: + return new WithBodyRequestSupplier( + (request, entity) -> request.method("patch", entity), application, body, redirect); case HttpMethod.GET: default: - return (request, w, t, n) -> - w.definition() - .application() - .modelFactory() - .fromAny(request.get(HttpConverterResolver.converter(w, t).responseType())); + return new WithoutBodyRequestSupplier(Invocation.Builder::get, application, redirect); } } diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/AbstractRequestSupplier.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/AbstractRequestSupplier.java new file mode 100644 index 00000000..12a6c5a9 --- /dev/null +++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/AbstractRequestSupplier.java @@ -0,0 +1,63 @@ +/* + * 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.executors.http; + +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.WorkflowError; +import io.serverlessworkflow.impl.WorkflowException; +import io.serverlessworkflow.impl.WorkflowModel; +import jakarta.ws.rs.client.Invocation.Builder; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status.Family; + +abstract class AbstractRequestSupplier implements RequestSupplier { + + private final boolean redirect; + + public AbstractRequestSupplier(boolean redirect) { + this.redirect = redirect; + } + + @Override + public WorkflowModel apply( + Builder request, WorkflowContext workflow, TaskContext task, WorkflowModel model) { + HttpModelConverter converter = HttpConverterResolver.converter(workflow, task); + Response response = invokeRequest(request, converter, workflow, task, model); + validateStatus(task, response, converter); + return workflow + .definition() + .application() + .modelFactory() + .fromAny(response.readEntity(converter.responseType())); + } + + private void validateStatus(TaskContext task, Response response, HttpModelConverter converter) { + if (response.getStatusInfo().getFamily() != Family.SUCCESSFUL) { + throw new WorkflowException( + converter + .errorFromResponse(WorkflowError.communication(response.getStatus(), task), response) + .build()); + } + } + + protected abstract Response invokeRequest( + Builder request, + HttpModelConverter converter, + WorkflowContext workflow, + TaskContext task, + WorkflowModel model); +} diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/CallableTaskHttpExecutorBuilder.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/CallableTaskHttpExecutorBuilder.java index 52076b9e..ae57898e 100644 --- a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/CallableTaskHttpExecutorBuilder.java +++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/CallableTaskHttpExecutorBuilder.java @@ -63,7 +63,10 @@ public void init(CallHTTP task, WorkflowDefinition definition, WorkflowMutablePo } this.requestFunction = buildRequestSupplier( - httpArgs.getMethod().toUpperCase(), httpArgs.getBody(), definition.application()); + httpArgs.getMethod().toUpperCase(), + httpArgs.getBody(), + httpArgs.isRedirect(), + definition.application()); } @Override diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpExecutor.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpExecutor.java index 6331af99..184ff183 100644 --- a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpExecutor.java +++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpExecutor.java @@ -17,12 +17,9 @@ import io.serverlessworkflow.impl.TaskContext; import io.serverlessworkflow.impl.WorkflowContext; -import io.serverlessworkflow.impl.WorkflowError; -import io.serverlessworkflow.impl.WorkflowException; import io.serverlessworkflow.impl.WorkflowModel; import io.serverlessworkflow.impl.WorkflowValueResolver; import io.serverlessworkflow.impl.executors.CallableTask; -import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.client.Invocation.Builder; import jakarta.ws.rs.client.WebTarget; import java.util.Map; @@ -79,15 +76,8 @@ public CompletableFuture apply( h -> h.apply(workflow, taskContext, input).forEach((k, v) -> request.header(k, v))); return CompletableFuture.supplyAsync( () -> { - try { - authProvider.ifPresent(auth -> auth.build(request, workflow, taskContext, input)); - return requestFunction.apply(request, workflow, taskContext, input); - } catch (WebApplicationException exception) { - throw new WorkflowException( - WorkflowError.communication( - exception.getResponse().getStatus(), taskContext, exception) - .build()); - } + authProvider.ifPresent(auth -> auth.build(request, workflow, taskContext, input)); + return requestFunction.apply(request, workflow, taskContext, input); }, workflow.definition().application().executorService()); } diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpExecutorBuilder.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpExecutorBuilder.java index 31943409..814bb2bb 100644 --- a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpExecutorBuilder.java +++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpExecutorBuilder.java @@ -30,6 +30,7 @@ public class HttpExecutorBuilder extends AbstractHttpExecutorBuilder { private WorkflowValueResolver pathSupplier; private Object body; private String method = HttpMethod.GET; + private boolean redirect; private HttpExecutorBuilder(WorkflowDefinition definition) { this.definition = definition; @@ -83,7 +84,7 @@ public HttpExecutor build(String uri) { } public HttpExecutor build(WorkflowValueResolver uriSupplier) { - this.requestFunction = buildRequestSupplier(method, body, definition.application()); + this.requestFunction = buildRequestSupplier(method, body, redirect, definition.application()); this.targetSupplier = pathSupplier == null ? getTargetSupplier(uriSupplier) diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpModelConverter.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpModelConverter.java index d5bdded7..81f9d256 100644 --- a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpModelConverter.java +++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/HttpModelConverter.java @@ -15,8 +15,11 @@ */ package io.serverlessworkflow.impl.executors.http; +import io.serverlessworkflow.impl.WorkflowError; import io.serverlessworkflow.impl.WorkflowModel; import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Response; +import org.slf4j.LoggerFactory; public interface HttpModelConverter { @@ -25,4 +28,19 @@ default Entity toEntity(WorkflowModel model) { } Class responseType(); + + default WorkflowError.Builder errorFromResponse( + WorkflowError.Builder errorBuilder, Response response) { + try { + Object title = response.readEntity(responseType()); + if (title != null) { + errorBuilder.title(title.toString()); + } + } catch (Exception ex) { + LoggerFactory.getLogger(HttpModelConverter.class) + .warn("Problem extracting error from http response", ex); + } + + return errorBuilder; + } } diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/RequestSupplier.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/RequestSupplier.java index 5b498759..50155fff 100644 --- a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/RequestSupplier.java +++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/RequestSupplier.java @@ -23,5 +23,5 @@ @FunctionalInterface interface RequestSupplier { WorkflowModel apply( - Builder request, WorkflowContext workflow, TaskContext task, WorkflowModel node); + Builder request, WorkflowContext workflow, TaskContext task, WorkflowModel model); } diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/WithBodyRequestSupplier.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/WithBodyRequestSupplier.java new file mode 100644 index 00000000..949abf79 --- /dev/null +++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/WithBodyRequestSupplier.java @@ -0,0 +1,53 @@ +/* + * 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.executors.http; + +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowApplication; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.WorkflowFilter; +import io.serverlessworkflow.impl.WorkflowModel; +import io.serverlessworkflow.impl.WorkflowUtils; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.Invocation.Builder; +import jakarta.ws.rs.core.Response; +import java.util.function.BiFunction; + +class WithBodyRequestSupplier extends AbstractRequestSupplier { + private final WorkflowFilter bodyFilter; + private final BiFunction, Response> requestFunction; + + public WithBodyRequestSupplier( + BiFunction, Response> requestFunction, + WorkflowApplication application, + Object body, + boolean redirect) { + super(redirect); + this.requestFunction = requestFunction; + bodyFilter = WorkflowUtils.buildWorkflowFilter(application, body); + } + + @Override + protected Response invokeRequest( + Builder request, + HttpModelConverter converter, + WorkflowContext workflow, + TaskContext task, + WorkflowModel model) { + return requestFunction.apply( + request, converter.toEntity(bodyFilter.apply(workflow, task, model))); + } +} diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/WithoutBodyRequestSupplier.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/WithoutBodyRequestSupplier.java new file mode 100644 index 00000000..a9604005 --- /dev/null +++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/WithoutBodyRequestSupplier.java @@ -0,0 +1,46 @@ +/* + * 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.executors.http; + +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowApplication; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.WorkflowModel; +import jakarta.ws.rs.client.Invocation.Builder; +import jakarta.ws.rs.core.Response; +import java.util.function.Function; + +class WithoutBodyRequestSupplier extends AbstractRequestSupplier { + private final Function requestFunction; + + public WithoutBodyRequestSupplier( + Function requestFunction, + WorkflowApplication application, + boolean redirect) { + super(redirect); + this.requestFunction = requestFunction; + } + + @Override + protected Response invokeRequest( + Builder request, + HttpModelConverter converter, + WorkflowContext workflow, + TaskContext task, + WorkflowModel model) { + return requestFunction.apply(request); + } +} diff --git a/impl/test/src/test/java/io/serverlessworkflow/impl/test/OpenAPITest.java b/impl/test/src/test/java/io/serverlessworkflow/impl/test/OpenAPITest.java index f68d6b8e..b62d2e47 100644 --- a/impl/test/src/test/java/io/serverlessworkflow/impl/test/OpenAPITest.java +++ b/impl/test/src/test/java/io/serverlessworkflow/impl/test/OpenAPITest.java @@ -203,7 +203,6 @@ public void testOpenAPIBearerQueryInlinedBodyWithNegativeResponse() throws Excep .orElseThrow()); assertInstanceOf(WorkflowException.class, exception.getCause()); assertTrue(exception.getMessage().contains("status=409")); - assertTrue(exception.getMessage().contains("title=HTTP 409 Client Error")); RecordedRequest restRequest = restServer.takeRequest(); assertEquals("POST", restRequest.getMethod()); diff --git a/impl/test/src/test/java/io/serverlessworkflow/impl/test/RetryTimeoutTest.java b/impl/test/src/test/java/io/serverlessworkflow/impl/test/RetryTimeoutTest.java index 97682026..565cc24d 100644 --- a/impl/test/src/test/java/io/serverlessworkflow/impl/test/RetryTimeoutTest.java +++ b/impl/test/src/test/java/io/serverlessworkflow/impl/test/RetryTimeoutTest.java @@ -47,7 +47,7 @@ public class RetryTimeoutTest { @BeforeAll static void init() { - app = WorkflowApplication.builder().withListener(new TraceExecutionListener()).build(); + app = WorkflowApplication.builder().build(); } @AfterAll diff --git a/impl/test/src/test/resources/workflows-samples/call-custom-function-inline.yaml b/impl/test/src/test/resources/workflows-samples/call-custom-function-inline.yaml index 617fe6b0..23533935 100644 --- a/impl/test/src/test/resources/workflows-samples/call-custom-function-inline.yaml +++ b/impl/test/src/test/resources/workflows-samples/call-custom-function-inline.yaml @@ -22,4 +22,4 @@ do: - getPet: call: getPetById with: - petId: 69 \ No newline at end of file + petId: -1 \ No newline at end of file