From 4231107cd22129bfb9343327defdf8aee580cb34 Mon Sep 17 00:00:00 2001 From: ztefanie Date: Mon, 20 Jan 2025 13:30:08 +0100 Subject: [PATCH] feat(rest-connector): Add flag to get multiple set cookie headers as list (#3859) * feat(rest-connector): Add flag to get multiple set cookie headers as list * add flag to get multiple set cookie headers as list --- .../client/apache/CustomApacheHttpClient.java | 4 +- .../HttpCommonResultResponseHandler.java | 47 ++++++++++++++----- .../base/document/FileResponseHandler.java | 5 +- .../exception/ConnectorExceptionMapper.java | 2 +- .../http/base/model/HttpCommonRequest.java | 18 +++++++ .../http/base/model/HttpCommonResult.java | 10 ++-- .../apache/CustomApacheHttpClientTest.java | 33 ++++++++++++- .../HttpCommonResultResponseHandlerTest.java | 15 ++++-- .../http-json-connector.json | 11 +++++ .../hybrid/http-json-connector-hybrid.json | 11 +++++ .../connector/http/rest/HttpJsonFunction.java | 3 +- 11 files changed, 131 insertions(+), 28 deletions(-) diff --git a/connectors/http/http-base/src/main/java/io/camunda/connector/http/base/client/apache/CustomApacheHttpClient.java b/connectors/http/http-base/src/main/java/io/camunda/connector/http/base/client/apache/CustomApacheHttpClient.java index addbe27f91..81bd8e74ae 100644 --- a/connectors/http/http-base/src/main/java/io/camunda/connector/http/base/client/apache/CustomApacheHttpClient.java +++ b/connectors/http/http-base/src/main/java/io/camunda/connector/http/base/client/apache/CustomApacheHttpClient.java @@ -109,7 +109,9 @@ public HttpCommonResult execute( .execute( apacheRequest, new HttpCommonResultResponseHandler( - executionEnvironment, request.isStoreResponse())); + executionEnvironment, + request.isStoreResponse(), + request.getGroupSetCookieHeaders())); if (HttpStatusHelper.isError(result.status())) { throw ConnectorExceptionMapper.from(result); } diff --git a/connectors/http/http-base/src/main/java/io/camunda/connector/http/base/client/apache/HttpCommonResultResponseHandler.java b/connectors/http/http-base/src/main/java/io/camunda/connector/http/base/client/apache/HttpCommonResultResponseHandler.java index b989167ce4..196c724301 100644 --- a/connectors/http/http-base/src/main/java/io/camunda/connector/http/base/client/apache/HttpCommonResultResponseHandler.java +++ b/connectors/http/http-base/src/main/java/io/camunda/connector/http/base/client/apache/HttpCommonResultResponseHandler.java @@ -28,9 +28,7 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.Base64; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; import javax.annotation.Nullable; import org.apache.commons.lang3.StringUtils; @@ -51,23 +49,27 @@ public class HttpCommonResultResponseHandler private final boolean isStoreResponseSelected; + private final boolean groupSetCookieHeaders; + public HttpCommonResultResponseHandler( - @Nullable ExecutionEnvironment executionEnvironment, boolean isStoreResponseSelected) { + @Nullable ExecutionEnvironment executionEnvironment, + boolean isStoreResponseSelected, + boolean groupSetCookieHeaders) { this.executionEnvironment = executionEnvironment; this.isStoreResponseSelected = isStoreResponseSelected; this.fileResponseHandler = new FileResponseHandler(executionEnvironment, isStoreResponseSelected); + this.groupSetCookieHeaders = groupSetCookieHeaders; } @Override public HttpCommonResult handleResponse(ClassicHttpResponse response) { int code = response.getCode(); String reason = response.getReasonPhrase(); - Map headers = - Arrays.stream(response.getHeaders()) - .collect( - // Collect the headers into a map ignoring duplicates (Set Cookies for instance) - Collectors.toMap(Header::getName, Header::getValue, (first, second) -> first)); + Map headers = + HttpCommonResultResponseHandler.formatHeaders( + response.getHeaders(), this.groupSetCookieHeaders); + if (response.getEntity() != null) { try (InputStream content = response.getEntity().getContent()) { if (executionEnvironment instanceof ExecutionEnvironment.SaaSCluster) { @@ -88,7 +90,30 @@ public HttpCommonResult handleResponse(ClassicHttpResponse response) { return new HttpCommonResult(code, headers, null, reason); } - private Document handleFileResponse(Map headers, byte[] content) { + private static Map formatHeaders( + Header[] headersArray, Boolean groupSetCookieHeaders) { + return Arrays.stream(headersArray) + .collect( + Collectors.toMap( + Header::getName, + header -> { + if (groupSetCookieHeaders && header.getName().equalsIgnoreCase("Set-Cookie")) { + return new ArrayList(List.of(header.getValue())); + } else { + return header.getValue(); + } + }, + (existingValue, newValue) -> { + if (groupSetCookieHeaders + && existingValue instanceof List + && newValue instanceof List) { + ((List) existingValue).add(((List) newValue).getFirst()); + } + return existingValue; + })); + } + + private Document handleFileResponse(Map headers, byte[] content) { var document = fileResponseHandler.handle(headers, content); LOGGER.debug("Stored response as document. Document reference: {}", document); return document; @@ -99,7 +124,7 @@ private Document handleFileResponse(Map headers, byte[] content) * unwrapped as an ErrorResponse. Otherwise, it will be unwrapped as a HttpCommonResult. */ private HttpCommonResult getResultForCloudFunction( - int code, InputStream content, Map headers, String reason) + int code, InputStream content, Map headers, String reason) throws IOException { if (HttpStatusHelper.isError(code)) { // unwrap as ErrorResponse diff --git a/connectors/http/http-base/src/main/java/io/camunda/connector/http/base/document/FileResponseHandler.java b/connectors/http/http-base/src/main/java/io/camunda/connector/http/base/document/FileResponseHandler.java index 4d5281f684..8574b0d449 100644 --- a/connectors/http/http-base/src/main/java/io/camunda/connector/http/base/document/FileResponseHandler.java +++ b/connectors/http/http-base/src/main/java/io/camunda/connector/http/base/document/FileResponseHandler.java @@ -55,7 +55,7 @@ public Document handleCloudFunctionResult(HttpCommonResult result) { return null; } - public Document handle(Map headers, byte[] content) { + public Document handle(Map headers, byte[] content) { if (storeResponseSelected() && executionEnvironment instanceof ExecutionEnvironment.StoresDocument env) { try (var byteArrayInputStream = new ByteArrayInputStream(content)) { @@ -72,10 +72,11 @@ public Document handle(Map headers, byte[] content) { return null; } - private String getContentType(Map headers) { + private String getContentType(Map headers) { return headers.entrySet().stream() .filter(e -> e.getKey().equalsIgnoreCase(CONTENT_TYPE)) .map(Map.Entry::getValue) + .map(Object::toString) .findFirst() .orElse(null); } diff --git a/connectors/http/http-base/src/main/java/io/camunda/connector/http/base/exception/ConnectorExceptionMapper.java b/connectors/http/http-base/src/main/java/io/camunda/connector/http/base/exception/ConnectorExceptionMapper.java index 68f5286d72..7d4e3632c9 100644 --- a/connectors/http/http-base/src/main/java/io/camunda/connector/http/base/exception/ConnectorExceptionMapper.java +++ b/connectors/http/http-base/src/main/java/io/camunda/connector/http/base/exception/ConnectorExceptionMapper.java @@ -28,7 +28,7 @@ public class ConnectorExceptionMapper { public static ConnectorException from(HttpCommonResult result) { String status = String.valueOf(result.status()); String reason = Optional.ofNullable(result.reason()).orElse("[no reason]"); - Map headers = result.headers(); + Map headers = result.headers(); Object body = result.body(); Map response = new HashMap<>(); response.put("headers", headers); diff --git a/connectors/http/http-base/src/main/java/io/camunda/connector/http/base/model/HttpCommonRequest.java b/connectors/http/http-base/src/main/java/io/camunda/connector/http/base/model/HttpCommonRequest.java index 42d945ab88..5f47c771f1 100644 --- a/connectors/http/http-base/src/main/java/io/camunda/connector/http/base/model/HttpCommonRequest.java +++ b/connectors/http/http-base/src/main/java/io/camunda/connector/http/base/model/HttpCommonRequest.java @@ -120,6 +120,16 @@ public class HttpCommonRequest { optional = true) private String skipEncoding; + @TemplateProperty( + label = "Group set-cookie headers to a list", + description = + "Group incoming headers with same name into a List to support multiple Set-Cookie headers.", + type = TemplateProperty.PropertyType.Hidden, + feel = Property.FeelMode.disabled, + group = "endpoint", + optional = true) + private String groupSetCookieHeaders; + public Object getBody() { return body; } @@ -156,6 +166,14 @@ public void setSkipEncoding(final String skipEncoding) { this.skipEncoding = skipEncoding; } + public boolean getGroupSetCookieHeaders() { + return Objects.equals(groupSetCookieHeaders, "true"); + } + + public void setGroupSetCookieHeaders(final String groupSetCookieHeaders) { + this.groupSetCookieHeaders = groupSetCookieHeaders; + } + public boolean hasAuthentication() { return authentication != null; } diff --git a/connectors/http/http-base/src/main/java/io/camunda/connector/http/base/model/HttpCommonResult.java b/connectors/http/http-base/src/main/java/io/camunda/connector/http/base/model/HttpCommonResult.java index a8d5e6a44e..9be7b40d7d 100644 --- a/connectors/http/http-base/src/main/java/io/camunda/connector/http/base/model/HttpCommonResult.java +++ b/connectors/http/http-base/src/main/java/io/camunda/connector/http/base/model/HttpCommonResult.java @@ -27,24 +27,24 @@ import java.util.Map; public record HttpCommonResult( - int status, Map headers, Object body, String reason, Document document) { + int status, Map headers, Object body, String reason, Document document) { - public HttpCommonResult(int status, Map headers, Object body, String reason) { + public HttpCommonResult(int status, Map headers, Object body, String reason) { this(status, headers, body, reason, null); } public HttpCommonResult( - int status, Map headers, Object body, Document documentReference) { + int status, Map headers, Object body, Document documentReference) { this(status, headers, body, null, documentReference); } - public HttpCommonResult(int status, Map headers, Object body) { + public HttpCommonResult(int status, Map headers, Object body) { this(status, headers, body, null, null); } @DataExample(id = "basic", feel = "= body.order.id") public static HttpCommonResult exampleResult() { - Map headers = Map.of("Content-Type", "application/json"); + Map headers = Map.of("Content-Type", "application/json"); DocumentReference.CamundaDocumentReference documentReference = new CamundaDocumentReferenceImpl( "theStoreId", diff --git a/connectors/http/http-base/src/test/java/io/camunda/connector/http/base/client/apache/CustomApacheHttpClientTest.java b/connectors/http/http-base/src/test/java/io/camunda/connector/http/base/client/apache/CustomApacheHttpClientTest.java index 525b7d00f7..77ecf46b51 100644 --- a/connectors/http/http-base/src/test/java/io/camunda/connector/http/base/client/apache/CustomApacheHttpClientTest.java +++ b/connectors/http/http-base/src/test/java/io/camunda/connector/http/base/client/apache/CustomApacheHttpClientTest.java @@ -45,7 +45,9 @@ import io.camunda.document.store.InMemoryDocumentStore; import java.io.IOException; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.stream.Stream; import org.apache.commons.text.StringEscapeUtils; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.core5.http.ContentType; @@ -53,8 +55,7 @@ import org.apache.hc.core5.http.HttpStatus; import org.junit.jupiter.api.*; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; -import org.junit.jupiter.params.provider.ValueSource; +import org.junit.jupiter.params.provider.*; import org.skyscreamer.jsonassert.JSONAssert; import org.skyscreamer.jsonassert.JSONCompareMode; import org.testcontainers.Testcontainers; @@ -382,6 +383,34 @@ public void shouldReturn200WithoutBody_whenEmptyGet(WireMockRuntimeInfo wmRuntim assertThat(result.status()).isEqualTo(200); } + private static Stream provideTestDataForHeaderTest() { + return Stream.of( + Arguments.of("Set-Cookie", "false", false, "Test-Value-1"), + Arguments.of("Set-Cookie", "true", true, List.of("Test-Value-1", "Test-Value-2")), + Arguments.of("other-than-set-cookie", "false", false, "Test-Value-1"), + Arguments.of("other-than-set-cookie", "true", false, "Test-Value-1")); + } + + @ParameterizedTest + @MethodSource("provideTestDataForHeaderTest") + public void shouldReturn200_whenDuplicatedHeadersAsListDisabled( + String headerKey, + String groupSetCookieHeaders, + Boolean expectedDoesReturnList, + Object expectedValue, + WireMockRuntimeInfo wmRuntimeInfo) { + stubFor(get("/path").willReturn(ok().withHeader(headerKey, "Test-Value-1", "Test-Value-2"))); + HttpCommonRequest request = new HttpCommonRequest(); + request.setMethod(HttpMethod.GET); + request.setUrl(wmRuntimeInfo.getHttpBaseUrl() + "/path"); + request.setGroupSetCookieHeaders(groupSetCookieHeaders); + HttpCommonResult result = customApacheHttpClient.execute(request); + assertThat(result).isNotNull(); + assertThat(result.status()).isEqualTo(200); + assertThat(result.headers().get(headerKey) instanceof List).isEqualTo(expectedDoesReturnList); + assertThat(result.headers().get(headerKey)).isEqualTo(expectedValue); + } + @Test public void shouldReturn200WithBody_whenGetWithBody(WireMockRuntimeInfo wmRuntimeInfo) throws Exception { diff --git a/connectors/http/http-base/src/test/java/io/camunda/connector/http/base/client/apache/HttpCommonResultResponseHandlerTest.java b/connectors/http/http-base/src/test/java/io/camunda/connector/http/base/client/apache/HttpCommonResultResponseHandlerTest.java index e612211e0f..1cc12faf09 100644 --- a/connectors/http/http-base/src/test/java/io/camunda/connector/http/base/client/apache/HttpCommonResultResponseHandlerTest.java +++ b/connectors/http/http-base/src/test/java/io/camunda/connector/http/base/client/apache/HttpCommonResultResponseHandlerTest.java @@ -35,7 +35,8 @@ public class HttpCommonResultResponseHandlerTest { @Test public void shouldHandleJsonResponse_whenCloudFunctionDisabled() throws Exception { // given - HttpCommonResultResponseHandler handler = new HttpCommonResultResponseHandler(null, false); + HttpCommonResultResponseHandler handler = + new HttpCommonResultResponseHandler(null, false, false); ClassicHttpResponse response = new BasicClassicHttpResponse(200); Header[] headers = new Header[] {new BasicHeader("Content-Type", "application/json")}; response.setHeaders(headers); @@ -55,7 +56,8 @@ public void shouldHandleJsonResponse_whenCloudFunctionDisabled() throws Exceptio @Test public void shouldHandleTextResponse_whenCloudFunctionDisabled() throws Exception { // given - HttpCommonResultResponseHandler handler = new HttpCommonResultResponseHandler(null, false); + HttpCommonResultResponseHandler handler = + new HttpCommonResultResponseHandler(null, false, false); ClassicHttpResponse response = new BasicClassicHttpResponse(200); Header[] headers = new Header[] {new BasicHeader("Content-Type", "text/plain")}; response.setHeaders(headers); @@ -76,7 +78,8 @@ public void shouldHandleTextResponse_whenCloudFunctionDisabled() throws Exceptio public void shouldHandleJsonResponse_whenCloudFunctionEnabled() throws Exception { // given HttpCommonResultResponseHandler handler = - new HttpCommonResultResponseHandler(new ExecutionEnvironment.SaaSCluster(null), false); + new HttpCommonResultResponseHandler( + new ExecutionEnvironment.SaaSCluster(null), false, false); ClassicHttpResponse response = new BasicClassicHttpResponse(201); Header[] headers = new Header[] {new BasicHeader("Content-Type", "application/json")}; response.setHeaders(headers); @@ -101,7 +104,8 @@ public void shouldHandleJsonResponse_whenCloudFunctionEnabled() throws Exception public void shouldHandleError_whenCloudFunctionEnabled() throws Exception { // given HttpCommonResultResponseHandler handler = - new HttpCommonResultResponseHandler(new ExecutionEnvironment.SaaSCluster(null), false); + new HttpCommonResultResponseHandler( + new ExecutionEnvironment.SaaSCluster(null), false, false); ClassicHttpResponse response = new BasicClassicHttpResponse(500); Header[] headers = new Header[] { @@ -130,7 +134,8 @@ public void shouldHandleError_whenCloudFunctionEnabled() throws Exception { public void shouldHandleJsonAsTextResponse_whenCloudFunctionEnabled() throws Exception { // given HttpCommonResultResponseHandler handler = - new HttpCommonResultResponseHandler(new ExecutionEnvironment.SaaSCluster(null), false); + new HttpCommonResultResponseHandler( + new ExecutionEnvironment.SaaSCluster(null), false, false); ClassicHttpResponse response = new BasicClassicHttpResponse(201); Header[] headers = new Header[] {new BasicHeader("Content-Type", "application/json")}; response.setHeaders(headers); diff --git a/connectors/http/rest/element-templates/http-json-connector.json b/connectors/http/rest/element-templates/http-json-connector.json index 05bb3e9a0b..16085cb8a6 100644 --- a/connectors/http/rest/element-templates/http-json-connector.json +++ b/connectors/http/rest/element-templates/http-json-connector.json @@ -413,6 +413,17 @@ "type" : "zeebe:input" }, "type" : "Hidden" + }, { + "id" : "groupSetCookieHeaders", + "label" : "Group set-cookie headers to a list", + "description" : "Group incoming headers with same name into a List to support multiple Set-Cookie headers.", + "optional" : true, + "group" : "endpoint", + "binding" : { + "name" : "groupSetCookieHeaders", + "type" : "zeebe:input" + }, + "type" : "Hidden" }, { "id" : "connectionTimeoutInSeconds", "label" : "Connection timeout in seconds", diff --git a/connectors/http/rest/element-templates/hybrid/http-json-connector-hybrid.json b/connectors/http/rest/element-templates/hybrid/http-json-connector-hybrid.json index adae7f5209..b1af450ff8 100644 --- a/connectors/http/rest/element-templates/hybrid/http-json-connector-hybrid.json +++ b/connectors/http/rest/element-templates/hybrid/http-json-connector-hybrid.json @@ -418,6 +418,17 @@ "type" : "zeebe:input" }, "type" : "Hidden" + }, { + "id" : "groupSetCookieHeaders", + "label" : "Group set-cookie headers to a list", + "description" : "Group incoming headers with same name into a List to support multiple Set-Cookie headers.", + "optional" : true, + "group" : "endpoint", + "binding" : { + "name" : "groupSetCookieHeaders", + "type" : "zeebe:input" + }, + "type" : "Hidden" }, { "id" : "connectionTimeoutInSeconds", "label" : "Connection timeout in seconds", diff --git a/connectors/http/rest/src/main/java/io/camunda/connector/http/rest/HttpJsonFunction.java b/connectors/http/rest/src/main/java/io/camunda/connector/http/rest/HttpJsonFunction.java index 5bd6bc8409..7e740a3f55 100644 --- a/connectors/http/rest/src/main/java/io/camunda/connector/http/rest/HttpJsonFunction.java +++ b/connectors/http/rest/src/main/java/io/camunda/connector/http/rest/HttpJsonFunction.java @@ -38,7 +38,8 @@ "readTimeoutInSeconds", "writeTimeoutInSeconds", "body", - "storeResponse" + "storeResponse", + "groupSetCookieHeaders" }, type = HttpJsonFunction.TYPE) @ElementTemplate(