diff --git a/ballerina-tests/http-advanced-tests/Dependencies.toml b/ballerina-tests/http-advanced-tests/Dependencies.toml index d6f22e7a4c..2454af7701 100644 --- a/ballerina-tests/http-advanced-tests/Dependencies.toml +++ b/ballerina-tests/http-advanced-tests/Dependencies.toml @@ -5,7 +5,7 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.9.0-20240405-165800-4b163f78" +distribution-version = "2201.9.0-20240419-152500-bd530dd2" [[package]] org = "ballerina" diff --git a/ballerina-tests/http-client-tests/Dependencies.toml b/ballerina-tests/http-client-tests/Dependencies.toml index 5d708dd96d..2aeae3bd36 100644 --- a/ballerina-tests/http-client-tests/Dependencies.toml +++ b/ballerina-tests/http-client-tests/Dependencies.toml @@ -5,7 +5,7 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.9.0-20240405-165800-4b163f78" +distribution-version = "2201.9.0-20240419-152500-bd530dd2" [[package]] org = "ballerina" diff --git a/ballerina-tests/http-client-tests/tests/client_res_binding_advanced.bal b/ballerina-tests/http-client-tests/tests/client_res_binding_advanced.bal new file mode 100644 index 0000000000..10f2002218 --- /dev/null +++ b/ballerina-tests/http-client-tests/tests/client_res_binding_advanced.bal @@ -0,0 +1,103 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.org). +// +// WSO2 LLC. licenses this file to you 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. +import ballerina/http; +import ballerina/mime; +import ballerina/test; + +service /api on new http:Listener(resBindingAdvancedPort) { + + resource function get 'string() returns string { + return "Hello, World!"; + } + + resource function get urlEncoded() returns http:Ok { + return { + mediaType: mime:APPLICATION_FORM_URLENCODED, + body: {"name": "John", "age": "23"} + }; + } + + resource function get 'json() returns json { + return {"name": "John", "age": "23"}; + } + + resource function get 'xml() returns xml { + return xml `Hello, World!`; + } + + resource function get byteArray() returns byte[] { + return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + } +} + +final http:Client clientEP = check new (string `localhost:${resBindingAdvancedPort}/api`); + +@test:Config {} +function testAnydataResBindingWithDifferentContentType() returns error? { + anydata response = check clientEP->/'string; + test:assertEquals(response, "Hello, World!"); + + response = check clientEP->/urlEncoded; + map expected = {"name": "John", "age": "23"}; + test:assertEquals(response, expected); + + response = check clientEP->/'json; + test:assertEquals(response, expected); + + response = check clientEP->/'xml; + test:assertEquals(response, xml `Hello, World!`); + + response = check clientEP->/'byteArray; + test:assertEquals(response, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); +} + +@test:Config {} +function testResponseWithAnydataResBinding() returns error? { + http:Response|anydata response = check clientEP->/'string; + if response is http:Response { + test:assertEquals(check response.getTextPayload(), "Hello, World!"); + } else { + test:assertFail("Invalid response type"); + } + + response = check clientEP->/urlEncoded; + if response is http:Response { + test:assertEquals(check response.getTextPayload(), "name=John&age=23"); + } else { + test:assertFail("Invalid response type"); + } + + response = check clientEP->/'json; + if response is http:Response { + test:assertEquals(check response.getJsonPayload(), {"name": "John", "age": "23"}); + } else { + test:assertFail("Invalid response type"); + } + + response = check clientEP->/'xml; + if response is http:Response { + test:assertEquals(check response.getXmlPayload(), xml `Hello, World!`); + } else { + test:assertFail("Invalid response type"); + } + + response = check clientEP->/'byteArray; + if response is http:Response { + test:assertEquals(check response.getBinaryPayload(), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + } else { + test:assertFail("Invalid response type"); + } +} diff --git a/ballerina-tests/http-client-tests/tests/test_service_ports.bal b/ballerina-tests/http-client-tests/tests/test_service_ports.bal index e2d261c743..6592ba4ab4 100644 --- a/ballerina-tests/http-client-tests/tests/test_service_ports.bal +++ b/ballerina-tests/http-client-tests/tests/test_service_ports.bal @@ -40,3 +40,5 @@ const int passthroughHostTestPort2 = 9608; const int statusCodeBindingPort1 = 9609; const int statusCodeBindingPort2 = 9610; + +const int resBindingAdvancedPort = 9611; diff --git a/ballerina-tests/http-dispatching-tests/Dependencies.toml b/ballerina-tests/http-dispatching-tests/Dependencies.toml index 70cac2cdc7..21c0fbbc7f 100644 --- a/ballerina-tests/http-dispatching-tests/Dependencies.toml +++ b/ballerina-tests/http-dispatching-tests/Dependencies.toml @@ -5,7 +5,7 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.9.0-20240405-165800-4b163f78" +distribution-version = "2201.9.0-20240419-152500-bd530dd2" [[package]] org = "ballerina" diff --git a/ballerina-tests/http-interceptor-tests/Dependencies.toml b/ballerina-tests/http-interceptor-tests/Dependencies.toml index 207bcb6d08..8528389356 100644 --- a/ballerina-tests/http-interceptor-tests/Dependencies.toml +++ b/ballerina-tests/http-interceptor-tests/Dependencies.toml @@ -5,7 +5,7 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.9.0-20240405-165800-4b163f78" +distribution-version = "2201.9.0-20240419-152500-bd530dd2" [[package]] org = "ballerina" diff --git a/ballerina-tests/http-misc-tests/Dependencies.toml b/ballerina-tests/http-misc-tests/Dependencies.toml index b353ee70fb..68e41d4196 100644 --- a/ballerina-tests/http-misc-tests/Dependencies.toml +++ b/ballerina-tests/http-misc-tests/Dependencies.toml @@ -5,7 +5,7 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.9.0-20240405-165800-4b163f78" +distribution-version = "2201.9.0-20240419-152500-bd530dd2" [[package]] org = "ballerina" diff --git a/ballerina-tests/http-resiliency-tests/Dependencies.toml b/ballerina-tests/http-resiliency-tests/Dependencies.toml index 5d3d24474b..11d55135b0 100644 --- a/ballerina-tests/http-resiliency-tests/Dependencies.toml +++ b/ballerina-tests/http-resiliency-tests/Dependencies.toml @@ -5,7 +5,7 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.9.0-20240405-165800-4b163f78" +distribution-version = "2201.9.0-20240419-152500-bd530dd2" [[package]] org = "ballerina" diff --git a/ballerina-tests/http-security-tests/Dependencies.toml b/ballerina-tests/http-security-tests/Dependencies.toml index 3527a1f05f..c5240c798b 100644 --- a/ballerina-tests/http-security-tests/Dependencies.toml +++ b/ballerina-tests/http-security-tests/Dependencies.toml @@ -5,7 +5,7 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.9.0-20240405-165800-4b163f78" +distribution-version = "2201.9.0-20240419-152500-bd530dd2" [[package]] org = "ballerina" diff --git a/ballerina-tests/http-service-tests/Dependencies.toml b/ballerina-tests/http-service-tests/Dependencies.toml index 2647c1b960..99aacaf832 100644 --- a/ballerina-tests/http-service-tests/Dependencies.toml +++ b/ballerina-tests/http-service-tests/Dependencies.toml @@ -5,7 +5,7 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.9.0-20240405-165800-4b163f78" +distribution-version = "2201.9.0-20240419-152500-bd530dd2" [[package]] org = "ballerina" diff --git a/ballerina-tests/http-test-common/Dependencies.toml b/ballerina-tests/http-test-common/Dependencies.toml index ae0cd54147..d44db61ceb 100644 --- a/ballerina-tests/http-test-common/Dependencies.toml +++ b/ballerina-tests/http-test-common/Dependencies.toml @@ -5,7 +5,7 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.9.0-20240405-165800-4b163f78" +distribution-version = "2201.9.0-20240419-152500-bd530dd2" [[package]] org = "ballerina" diff --git a/ballerina-tests/http2-tests/Dependencies.toml b/ballerina-tests/http2-tests/Dependencies.toml index 13db7719a2..496bba0348 100644 --- a/ballerina-tests/http2-tests/Dependencies.toml +++ b/ballerina-tests/http2-tests/Dependencies.toml @@ -5,7 +5,7 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.9.0-20240405-165800-4b163f78" +distribution-version = "2201.9.0-20240419-152500-bd530dd2" [[package]] org = "ballerina" diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index 5877dba418..ff97727775 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -5,7 +5,7 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.9.0-20240405-165800-4b163f78" +distribution-version = "2201.9.0-20240419-152500-bd530dd2" [[package]] org = "ballerina" diff --git a/ballerina/http_client_endpoint.bal b/ballerina/http_client_endpoint.bal index 55d0f0eb5c..00ca488b48 100644 --- a/ballerina/http_client_endpoint.bal +++ b/ballerina/http_client_endpoint.bal @@ -693,7 +693,7 @@ isolated function createStatusCodeResponseBindingError(boolean generalError, int isolated function processResponse(Response|ClientError response, TargetType targetType, boolean requireValidation) returns Response|anydata|ClientError { - if targetType is typedesc || response is ClientError { + if response is ClientError || hasHttpResponseType(targetType) { return response; } int statusCode = response.statusCode; @@ -741,3 +741,7 @@ isolated function externProcessResponseNew(Response response, typedesc { return response.getXmlPayload(); - } else if typeIncludedInUnion(targetType, xmlType) { + } else if matchingType(targetType, xmlType) { xml|ClientError payload = response.getXmlPayload(); - return payload is NoContentError ? (typeIncludedInUnion(targetType, nilType) ? () : payload) : payload; + return payload is NoContentError ? (matchingType(targetType, nilType) ? () : payload) : payload; } else { return getCommonError(response, targetType); } @@ -84,17 +84,17 @@ isolated function xmlPayloadBuilder(Response response, TargetType targetType) re isolated function textPayloadBuilder(Response response, TargetType targetType) returns string|byte[]|ClientError? { if targetType is typedesc { return response.getTextPayload(); - } else if typeIncludedInUnion(targetType, stringType) { + } else if matchingType(targetType, string) { string|ClientError payload = response.getTextPayload(); - return payload is NoContentError ? (typeIncludedInUnion(targetType, nilType) ? () : payload) : payload; + return payload is NoContentError ? (matchingType(targetType, nilType) ? () : payload) : payload; } else if targetType is typedesc { return response.getBinaryPayload(); - } else if typeIncludedInUnion(targetType, byteArrType) { + } else if matchingType(targetType, byteArrType) { string|ClientError payload = response.getTextPayload(); if payload is string { return payload.toBytes(); } else if payload is NoContentError { - return typeIncludedInUnion(targetType, nilType) ? () : payload; + return matchingType(targetType, nilType) ? () : payload; } return payload; } else { @@ -106,15 +106,15 @@ isolated function formPayloadBuilder(Response response, TargetType targetType) r if targetType is typedesc> { string payload = check response.getTextPayload(); return getFormDataMap(payload); - } else if typeIncludedInUnion(targetType, mapStringType) { + } else if matchingType(targetType, mapStringType) { string|ClientError payload = response.getTextPayload(); - return payload is NoContentError ? (typeIncludedInUnion(targetType, nilType) ? () : payload) : + return payload is NoContentError ? (matchingType(targetType, nilType) ? () : payload) : getFormDataMap(check payload); } else if targetType is typedesc { return response.getTextPayload(); - } else if typeIncludedInUnion(targetType, stringType) { + } else if matchingType(targetType, stringType) { string|ClientError payload = response.getTextPayload(); - return payload is NoContentError ? (typeIncludedInUnion(targetType, nilType) ? () : payload) : payload; + return payload is NoContentError ? (matchingType(targetType, nilType) ? () : payload) : payload; } else { return getCommonError(response, targetType); } @@ -123,10 +123,10 @@ isolated function formPayloadBuilder(Response response, TargetType targetType) r isolated function blobPayloadBuilder(Response response, TargetType targetType) returns byte[]|ClientError? { if targetType is typedesc { return response.getBinaryPayload(); - } else if typeIncludedInUnion(targetType, byteArrType) { + } else if matchingType(targetType, byteArrType) { byte[]|ClientError payload = response.getBinaryPayload(); if payload is byte[] && payload.length() == 0 { - return typeIncludedInUnion(targetType, nilType) ? () : payload; + return matchingType(targetType, nilType) ? () : payload; } return payload; } else { @@ -199,6 +199,6 @@ isolated function performDataValidation(anydata payload, typedesc targe return payload; } -isolated function typeIncludedInUnion(typedesc unionType, any targetType) returns boolean = @java:Method { +isolated function matchingType(typedesc unionType, any targetType) returns boolean = @java:Method { 'class: "io.ballerina.stdlib.http.api.service.signature.builder.AbstractPayloadBuilder" } external; diff --git a/changelog.md b/changelog.md index bcdc283c69..c285808a50 100644 --- a/changelog.md +++ b/changelog.md @@ -10,11 +10,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added - [Add status code response binding support for the HTTP client](https://github.com/ballerina-platform/ballerina-library/issues/6100) +- [Add response binding support for types union with `http:Response`](https://github.com/ballerina-platform/ballerina-library/issues/6416) ### Fixed - [Address CVE-2024-29025 netty's vulnerability](https://github.com/ballerina-platform/ballerina-library/issues/6242) - [Fix interceptor pipeline getting exited when there is a `nil` return](https://github.com/ballerina-platform/ballerina-library/issues/6278) +- [Fix response binding error for `anydata` type](https://github.com/ballerina-platform/ballerina-library/issues/6414) ## [2.10.12] - 2024-03-21 diff --git a/native/spotbugs-exclude.xml b/native/spotbugs-exclude.xml index ff478ee17f..0a80fdd752 100644 --- a/native/spotbugs-exclude.xml +++ b/native/spotbugs-exclude.xml @@ -167,4 +167,14 @@ + + + + + + + + + + diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/service/signature/builder/AbstractPayloadBuilder.java b/native/src/main/java/io/ballerina/stdlib/http/api/service/signature/builder/AbstractPayloadBuilder.java index 801c15ee36..9140d229a8 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/service/signature/builder/AbstractPayloadBuilder.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/service/signature/builder/AbstractPayloadBuilder.java @@ -19,6 +19,8 @@ package io.ballerina.stdlib.http.api.service.signature.builder; import io.ballerina.runtime.api.TypeTags; +import io.ballerina.runtime.api.types.ArrayType; +import io.ballerina.runtime.api.types.MapType; import io.ballerina.runtime.api.types.Type; import io.ballerina.runtime.api.types.TypedescType; import io.ballerina.runtime.api.types.UnionType; @@ -31,6 +33,10 @@ import java.util.Locale; import static io.ballerina.runtime.api.TypeTags.ARRAY_TAG; +import static io.ballerina.runtime.api.TypeTags.BYTE_ARRAY_TAG; +import static io.ballerina.runtime.api.TypeTags.BYTE_TAG; +import static io.ballerina.runtime.api.TypeTags.MAP_TAG; +import static io.ballerina.runtime.api.TypeTags.NULL_TAG; import static io.ballerina.runtime.api.TypeTags.STRING_TAG; import static io.ballerina.runtime.api.TypeTags.XML_TAG; @@ -102,14 +108,79 @@ public static boolean isSubtypeOfAllowedType(Type payloadType, int targetTypeTag return false; } - public static boolean typeIncludedInUnion(BTypedesc unionType, BTypedesc targetType) { - Type baseType = TypeUtils.getReferredType(targetType.getDescribingType()); - int targetTypeTag = ((TypedescType) baseType).getConstraint().getTag(); - Type unionTypeDescribingType = unionType.getDescribingType(); - if (unionTypeDescribingType.getTag() == TypeTags.UNION_TAG) { - List memberTypes = ((UnionType) unionTypeDescribingType).getMemberTypes(); - return memberTypes.stream().anyMatch(memberType -> memberType.getTag() == targetTypeTag); + // Target type can be `xml`, `string`, `map`, `()`, `byte[]` + public static boolean matchingType(BTypedesc sourceTypeDesc, BTypedesc targetTypeDesc) { + Type sourceType = TypeUtils.getImpliedType(sourceTypeDesc.getDescribingType()); + Type targetType = getConstraintedType(TypeUtils.getImpliedType(targetTypeDesc.getDescribingType())); + return matchingTypeInternal(sourceType, targetType); + } + + private static Type getConstraintedType(Type targetType) { + if (targetType.getTag() == TypeTags.TYPEDESC_TAG) { + return ((TypedescType) targetType).getConstraint(); + } + return targetType; + } + + private static boolean matchingTypeInternal(Type sourceType, Type targetType) { + int targetTypeTag = targetType.getTag(); + int sourceTypeTag = sourceType.getTag(); + + if (sourceTypeTag == TypeTags.ANYDATA_TAG || isMapOfStringType(sourceType, targetTypeTag) || + isXmlType(sourceTypeTag, targetTypeTag) || isStringType(sourceTypeTag, targetTypeTag) || + isNilType(sourceTypeTag, targetTypeTag) || isByteArrayType(sourceType, targetTypeTag)) { + return true; } + + if (sourceTypeTag == TypeTags.UNION_TAG) { + List memberTypes = ((UnionType) sourceType).getMemberTypes(); + return memberTypes.stream().anyMatch(memberType -> matchingTypeInternal(memberType, targetType)); + } + + if (sourceTypeTag == TypeTags.TYPE_REFERENCED_TYPE_TAG) { + return matchingTypeInternal(TypeUtils.getReferredType(sourceType), targetType); + } + return false; } + + private static boolean isNilType(int sourceTypeTag, int targetTypeTag) { + return sourceTypeTag == NULL_TAG && targetTypeTag == NULL_TAG; + } + + private static boolean isStringType(int sourceTypeTag, int targetTypeTag) { + return TypeTags.isStringTypeTag(sourceTypeTag) && targetTypeTag == STRING_TAG; + } + + private static boolean isXmlType(int sourceTypeTag, int targetTypeTag) { + return TypeTags.isXMLTypeTag(sourceTypeTag) && targetTypeTag == XML_TAG; + } + + private static boolean isMapOfStringType(Type sourceType, int targetTypeTag) { + return targetTypeTag == TypeTags.MAP_TAG && sourceType.getTag() == MAP_TAG && + TypeTags.isStringTypeTag(((MapType) sourceType).getConstrainedType().getTag()); + } + + private static boolean isByteArrayType(Type sourceType, int targetTypeTag) { + return (targetTypeTag == BYTE_ARRAY_TAG || targetTypeTag == ARRAY_TAG) && + (sourceType.getTag() == BYTE_ARRAY_TAG || (sourceType.getTag() == ARRAY_TAG && + ((ArrayType) sourceType).getElementType().getTag() == BYTE_TAG)); + } + + public static boolean hasHttpResponseType(BTypedesc targetTypeDesc) { + Type targetType = TypeUtils.getImpliedType(targetTypeDesc.getDescribingType()); + return hasHttpResponseTypeInternal(targetType); + } + + private static boolean hasHttpResponseTypeInternal(Type targetType) { + targetType = TypeUtils.getImpliedType(targetType); + return switch (targetType.getTag()) { + case TypeTags.OBJECT_TYPE_TAG -> true; + case TypeTags.UNION_TAG -> ((UnionType) targetType).getMemberTypes().stream().anyMatch( + AbstractPayloadBuilder::hasHttpResponseTypeInternal); + case TypeTags.TYPE_REFERENCED_TYPE_TAG -> + hasHttpResponseTypeInternal(TypeUtils.getReferredType(targetType)); + default -> false; + }; + } }