Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,6 @@ protected static RequestSupplier buildRequestSupplier(
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);
Comment on lines -49 to -51
Copy link
Contributor Author

@mcruzdev mcruzdev Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to discuss about PATCH, I will write some options tomorrow 🛌🏼

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HI folks, I did not have time to solve this one. My new changes:

  • Add tests for DELETE, HEAD, PUT and OPTIONS
  • Use one method by CNCF Specification file
  • Remove for now the support for PATCH

I removed the support for PATCH just to find a better solution for it, actually the HTTP client lib we are using, uses the java.net.HttpURLConnection that does not support PATCH methods. To achieve it, we have some workarounds:

  • To use a property HttpUrlConnectorProvider.SET_METHOD_WORKAROUND but it uses reflection behind the scenes and it is necessary to use --add-opens. (it need an extra configuration)
  • Use X-HTTP-Method-Override header (in my opnion, it is a bad idea, it is not guaranteed solution)
  • Add a new library that supports PATCH with no friction.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ricardozanini @fjtirado do you have another solution in mind?

case HttpMethod.GET:
default:
return new WithoutBodyRequestSupplier(Invocation.Builder::get, application, redirect);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
*/
package io.serverlessworkflow.impl.executors.http;

import static jakarta.ws.rs.core.Response.Status.Family.REDIRECTION;
import static jakarta.ws.rs.core.Response.Status.Family.SUCCESSFUL;

import io.serverlessworkflow.impl.TaskContext;
import io.serverlessworkflow.impl.WorkflowContext;
import io.serverlessworkflow.impl.WorkflowError;
Expand All @@ -36,20 +39,39 @@ public AbstractRequestSupplier(boolean redirect) {
public WorkflowModel apply(
Builder request, WorkflowContext workflow, TaskContext task, WorkflowModel model) {
HttpModelConverter converter = HttpConverterResolver.converter(workflow, task);

if (!redirect) {
// disable automatic redirects handling from Jersey client
request.property("jersey.config.client.followRedirects", false);
}

Response response = invokeRequest(request, converter, workflow, task, model);
validateStatus(task, response, converter);
validateStatus(task, response);
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) {
private void validateStatus(TaskContext task, Response response) {
Family statusFamily = response.getStatusInfo().getFamily();
boolean isSuccess = statusFamily.equals(SUCCESSFUL);
boolean isRedirect = statusFamily.equals(REDIRECTION);
boolean valid = redirect ? (isSuccess || isRedirect) : isSuccess;

if (!valid) {
String expectedRange = redirect ? "200-399" : "200-299";
throw new WorkflowException(
converter
.errorFromResponse(WorkflowError.communication(response.getStatus(), task), response)
WorkflowError.communication(
response.getStatus(),
task,
String.format(
"The property 'redirect' is set to %s but received status %d (%s); expected status in the %s range",
redirect,
response.getStatus(),
response.getStatusInfo().getReasonPhrase(),
expectedRange))
.build());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,27 @@
import static io.serverlessworkflow.api.WorkflowReader.readWorkflowFromClasspath;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowableOfType;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;

import io.serverlessworkflow.impl.WorkflowApplication;
import io.serverlessworkflow.impl.WorkflowModel;
import java.io.IOException;
import java.util.Map;
import java.util.stream.Stream;
import java.util.concurrent.CompletionException;
import mockwebserver3.MockResponse;
import mockwebserver3.MockWebServer;
import okhttp3.Headers;
import org.assertj.core.api.Condition;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class HTTPWorkflowDefinitionTest {

private static WorkflowApplication appl;
private static MockWebServer mockServer;

@BeforeAll
static void init() {
Expand All @@ -46,75 +50,205 @@ static void cleanup() {
appl.close();
}

@ParameterizedTest
@MethodSource("provideParameters")
void testWorkflowExecution(String fileName, Object input, Condition<Object> condition)
throws IOException {
assertThat(
appl.workflowDefinition(readWorkflowFromClasspath(fileName))
.instance(input)
.start()
.join())
.is(condition);
}

@ParameterizedTest
@ValueSource(
strings = {
"workflows-samples/call-http-query-parameters.yaml",
"workflows-samples/call-http-query-parameters-external-schema.yaml"
})
void testWrongSchema(String fileName) {
@BeforeEach
void setup() throws IOException {
mockServer = new MockWebServer();
mockServer.start(9876);
}

@AfterEach
void shutdownServer() {
mockServer.close();
}

private static boolean httpCondition(WorkflowModel obj) {
Map<String, Object> map = obj.asMap().orElseThrow();
return map.containsKey("photoUrls") || map.containsKey("petId");
}

@Test
void callHttpGet_should_return_pet_data() throws Exception {
WorkflowModel result =
appl.workflowDefinition(readWorkflowFromClasspath("workflows-samples/call-http-get.yaml"))
.instance(Map.of("petId", 10))
.start()
.join();
assertThat(result)
.has(new Condition<>(HTTPWorkflowDefinitionTest::httpCondition, "callHttpCondition"));
}

@Test
void callHttpGet_with_not_found_petId_should_keep_input_petId() throws Exception {
WorkflowModel result =
appl.workflowDefinition(readWorkflowFromClasspath("workflows-samples/call-http-get.yaml"))
.instance(Map.of("petId", "-1"))
.start()
.join();
assertThat(result.asMap().orElseThrow()).containsKey("petId");
}

@Test
void callHttpEndpointInterpolation_should_work() throws Exception {
WorkflowModel result =
appl.workflowDefinition(
readWorkflowFromClasspath(
"workflows-samples/call-http-endpoint-interpolation.yaml"))
.instance(Map.of("petId", 10))
.start()
.join();
assertThat(result)
.has(new Condition<>(HTTPWorkflowDefinitionTest::httpCondition, "callHttpCondition"));
}

@Test
void callHttpQueryParameters_should_find_star_trek_movie() throws Exception {
WorkflowModel result =
appl.workflowDefinition(
readWorkflowFromClasspath("workflows-samples/call-http-query-parameters.yaml"))
.instance(Map.of("uid", "MOMA0000092393"))
.start()
.join();
assertThat(((Map<String, Object>) result.asMap().orElseThrow().get("movie")).get("title"))
.isEqualTo("Star Trek");
}

@Test
void callHttpFindByStatus_should_return_non_empty_collection() throws Exception {

WorkflowModel result =
appl.workflowDefinition(
readWorkflowFromClasspath("workflows-samples/call-http-find-by-status.yaml"))
.instance(Map.of())
.start()
.join();
assertThat(result.asCollection()).isNotEmpty();
}

@Test
void callHttpQueryParameters_external_schema_should_find_star_trek() throws Exception {
WorkflowModel result =
appl.workflowDefinition(
readWorkflowFromClasspath(
"workflows-samples/call-http-query-parameters-external-schema.yaml"))
.instance(Map.of("uid", "MOMA0000092393"))
.start()
.join();
assertThat(((Map<String, Object>) result.asMap().orElseThrow().get("movie")).get("title"))
.isEqualTo("Star Trek");
}

@Test
void callHttpPost_should_return_created_firstName() throws Exception {
mockServer.enqueue(
new MockResponse(
200,
Headers.of("Content-Type", "application/json"),
"""
{
"firstName": "Javierito"
}
"""));
WorkflowModel result =
appl.workflowDefinition(readWorkflowFromClasspath("workflows-samples/call-http-post.yaml"))
.instance(Map.of("name", "Javierito", "surname", "Unknown"))
.start()
.join();
assertThat(result.asText().orElseThrow()).isEqualTo("Javierito");
}

@Test
void testCallHttpDelete() {
assertDoesNotThrow(
() -> {
appl.workflowDefinition(
readWorkflowFromClasspath("workflows-samples/call-http-delete.yaml"))
.instance(Map.of())
.start()
.join();
});
}

@Test
void callHttpPut_should_contain_firstName_with_john() throws Exception {
WorkflowModel result =
appl.workflowDefinition(readWorkflowFromClasspath("workflows-samples/call-http-put.yaml"))
.instance(Map.of("firstName", "John"))
.start()
.join();
assertThat(result.asText().orElseThrow()).contains("John");
}

@Test
void testWrongSchema_should_throw_illegal_argument() {
IllegalArgumentException exception =
catchThrowableOfType(
IllegalArgumentException.class,
() -> appl.workflowDefinition(readWorkflowFromClasspath(fileName)).instance(Map.of()));
() ->
appl.workflowDefinition(
readWorkflowFromClasspath(
"workflows-samples/call-http-query-parameters.yaml"))
.instance(Map.of()));
assertThat(exception)
.isNotNull()
.hasMessageContaining("There are JsonSchema validation errors");
}

private static boolean httpCondition(WorkflowModel obj) {
Map<String, Object> map = obj.asMap().orElseThrow();
return map.containsKey("photoUrls") || map.containsKey("petId");
@Test
void testHeadCall() {
mockServer.enqueue(
new MockResponse(
200,
Headers.of(
Map.of(
"Content-Length",
"123",
"Content-Type",
"application/json",
"X-Custom-Header",
"CustomValue")),
""));
assertDoesNotThrow(
() -> {
appl.workflowDefinition(
readWorkflowFromClasspath("workflows-samples/call-http-head.yaml"))
.instance(Map.of())
.start()
.join();
});
}

private static Stream<Arguments> provideParameters() {
Map<String, Object> petInput = Map.of("petId", 10);
Map<String, Object> starTrekInput = Map.of("uid", "MOMA0000092393");
Condition<WorkflowModel> petCondition =
new Condition<>(HTTPWorkflowDefinitionTest::httpCondition, "callHttpCondition");
Condition<WorkflowModel> starTrekCondition =
new Condition<>(
o ->
((Map<String, Object>) o.asMap().orElseThrow().get("movie"))
.get("title")
.equals("Star Trek"),
"StartTrek");
Condition<WorkflowModel> postCondition =
new Condition<WorkflowModel>(
o -> o.asText().orElseThrow().equals("Javierito"), "CallHttpPostCondition");
Map<String, String> postMap = Map.of("name", "Javierito", "surname", "Unknown");
return Stream.of(
Arguments.of("workflows-samples/callGetHttp.yaml", petInput, petCondition),
Arguments.of(
"workflows-samples/callGetHttp.yaml",
Map.of("petId", "-1"),
new Condition<WorkflowModel>(
o -> o.asMap().orElseThrow().containsKey("petId"), "notFoundCondition")),
Arguments.of(
"workflows-samples/call-http-endpoint-interpolation.yaml", petInput, petCondition),
Arguments.of(
"workflows-samples/call-http-query-parameters.yaml", starTrekInput, starTrekCondition),
Arguments.of(
"workflows-samples/callFindByStatusHttp.yaml",
Map.of(),
new Condition<WorkflowModel>(o -> !o.asCollection().isEmpty(), "HasElementCondition")),
Arguments.of(
"workflows-samples/call-http-query-parameters-external-schema.yaml",
starTrekInput,
starTrekCondition),
Arguments.of("workflows-samples/callPostHttp.yaml", postMap, postCondition),
Arguments.of("workflows-samples/callPostHttpAsExpr.yaml", postMap, postCondition));
@Test
void testOptionsCall() {
mockServer.enqueue(new MockResponse(200, Headers.of("Allow", "GET, POST, OPTIONS"), ""));

assertDoesNotThrow(
() -> {
appl.workflowDefinition(
readWorkflowFromClasspath("workflows-samples/call-http-options.yaml"))
.instance(Map.of())
.start()
.join();
});
}

@Test
void testRedirectAsFalse() {
mockServer.enqueue(
new MockResponse(301, Headers.of("Location", "http://localhost:9876/redirected"), ""));

CompletionException exception =
catchThrowableOfType(
CompletionException.class,
() ->
appl.workflowDefinition(
readWorkflowFromClasspath(
"workflows-samples/call-http-redirect-false.yaml"))
.instance(Map.of())
.start()
.join());

assertThat(exception.getCause().getMessage())
.contains(
"The property 'redirect' is set to false but received status 301 (Redirection); expected status in the 200-299 range");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
document:
dsl: 1.0.0-alpha1
namespace: test
name: call-http-delete
version: 1.0.0
do:
- deleteAuthor:
call: http
with:
method: delete
endpoint:
uri: https://fakerestapi.azurewebsites.net/api/v1/Authors/1
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
document:
dsl: 1.0.0-alpha1
namespace: test
name: http-call-find-by-status
name: call-http-find-by-status
version: 1.0.0
do:
- tryGetPet:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
document:
dsl: 1.0.0-alpha1
namespace: test
name: http-call-with-response
name: call-http-with-response
version: 1.0.0
do:
- tryGetPet:
Expand Down
Loading