diff --git a/ballerina-tests/http-advanced-tests/Ballerina.toml b/ballerina-tests/http-advanced-tests/Ballerina.toml index 6189f1ab9e..dcf2db347b 100644 --- a/ballerina-tests/http-advanced-tests/Ballerina.toml +++ b/ballerina-tests/http-advanced-tests/Ballerina.toml @@ -1,17 +1,17 @@ [package] org = "ballerina" name = "http_advanced_tests" -version = "2.10.13" +version = "2.11.0" [[dependency]] org = "ballerina" name = "http_test_common" repository = "local" -version = "2.10.13" +version = "2.11.0" [platform.java17] graalvmCompatible = true [[platform.java17.dependency]] scope = "testOnly" -path = "../../test-utils/build/libs/http-test-utils-2.10.13-SNAPSHOT.jar" +path = "../../test-utils/build/libs/http-test-utils-2.11.0-SNAPSHOT.jar" diff --git a/ballerina-tests/http-advanced-tests/Dependencies.toml b/ballerina-tests/http-advanced-tests/Dependencies.toml index 4a5d29e655..1b60df4e4a 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.8.0" +distribution-version = "2201.9.0-20240404-200900-0a7e4c9e" [[package]] org = "ballerina" @@ -35,7 +35,7 @@ dependencies = [ [[package]] org = "ballerina" name = "constraint" -version = "1.4.0" +version = "1.5.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"} @@ -44,7 +44,7 @@ dependencies = [ [[package]] org = "ballerina" name = "crypto" -version = "2.5.0" +version = "2.6.2" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"}, @@ -72,7 +72,7 @@ modules = [ [[package]] org = "ballerina" name = "http" -version = "2.10.13" +version = "2.11.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "auth"}, @@ -105,7 +105,7 @@ modules = [ [[package]] org = "ballerina" name = "http_advanced_tests" -version = "2.10.13" +version = "2.11.0" dependencies = [ {org = "ballerina", name = "crypto"}, {org = "ballerina", name = "file"}, @@ -125,7 +125,7 @@ modules = [ [[package]] org = "ballerina" name = "http_test_common" -version = "2.10.13" +version = "2.11.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "lang.string"}, @@ -346,6 +346,7 @@ version = "0.0.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.array"}, {org = "ballerina", name = "lang.error"} ] modules = [ diff --git a/ballerina-tests/http-client-tests/Ballerina.toml b/ballerina-tests/http-client-tests/Ballerina.toml index afee86a497..77d1aa1317 100644 --- a/ballerina-tests/http-client-tests/Ballerina.toml +++ b/ballerina-tests/http-client-tests/Ballerina.toml @@ -1,17 +1,17 @@ [package] org = "ballerina" name = "http_client_tests" -version = "2.10.13" +version = "2.11.0" [[dependency]] org = "ballerina" name = "http_test_common" repository = "local" -version = "2.10.13" +version = "2.11.0" [platform.java17] graalvmCompatible = true [[platform.java17.dependency]] scope = "testOnly" -path = "../../test-utils/build/libs/http-test-utils-2.10.13-SNAPSHOT.jar" +path = "../../test-utils/build/libs/http-test-utils-2.11.0-SNAPSHOT.jar" diff --git a/ballerina-tests/http-client-tests/Dependencies.toml b/ballerina-tests/http-client-tests/Dependencies.toml index 4f21f0e96a..f0f81991a4 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.8.0" +distribution-version = "2201.9.0-20240404-200900-0a7e4c9e" [[package]] org = "ballerina" @@ -35,7 +35,7 @@ dependencies = [ [[package]] org = "ballerina" name = "constraint" -version = "1.4.0" +version = "1.5.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"} @@ -47,7 +47,7 @@ modules = [ [[package]] org = "ballerina" name = "crypto" -version = "2.5.0" +version = "2.6.2" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"}, @@ -69,7 +69,7 @@ dependencies = [ [[package]] org = "ballerina" name = "http" -version = "2.10.13" +version = "2.11.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "auth"}, @@ -102,7 +102,7 @@ modules = [ [[package]] org = "ballerina" name = "http_client_tests" -version = "2.10.13" +version = "2.11.0" dependencies = [ {org = "ballerina", name = "constraint"}, {org = "ballerina", name = "http"}, @@ -121,7 +121,7 @@ modules = [ [[package]] org = "ballerina" name = "http_test_common" -version = "2.10.13" +version = "2.11.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "lang.string"}, @@ -342,6 +342,7 @@ version = "0.0.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.array"}, {org = "ballerina", name = "lang.error"} ] modules = [ diff --git a/ballerina-tests/http-client-tests/tests/sc_res_binding_tests.bal b/ballerina-tests/http-client-tests/tests/sc_res_binding_tests.bal new file mode 100644 index 0000000000..f33da5019a --- /dev/null +++ b/ballerina-tests/http-client-tests/tests/sc_res_binding_tests.bal @@ -0,0 +1,673 @@ +// 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/constraint; +import ballerina/http; +import ballerina/test; + +type Album record {| + readonly string id; + string name; + string artist; + string genre; +|}; + +type MockAlbum record {| + *Album; + string 'type = "mock"; +|}; + +type AlbumUnion1 Album|MockAlbum; + +type AlbumUnion2 MockAlbum|Album; + +table key(id) albums = table [ + {id: "1", name: "The Dark Side of the Moon", artist: "Pink Floyd", genre: "Progressive Rock"}, + {id: "2", name: "Back in Black", artist: "AC/DC", genre: "Hard Rock"}, + {id: "3", name: "The Wall", artist: "Pink Floyd", genre: "Progressive Rock"} +]; + +type ErrorMessage record {| + string albumId; + string message; +|}; + +type Headers record {| + string user\-id; + int req\-id; +|}; + +type ArrayHeaders record {| + string[] user\-id; + int[] req\-id; +|}; + +type ArrayHeaderWithUnion record {| + string[]|int[] user\-id; + int[]|boolean[] req\-id; +|}; + +type ReqIdUnionType int|boolean[]; + +enum UserIds { + USER1 = "user-1", + USER2 = "user-2", + USER3 = "user-3" +} + +type ArrayHeaderWithTypes record {| + UserIds user\-id; + ReqIdUnionType req\-id; +|}; + +type IntHeaders record {| + int user\-id; + int req\-id; +|}; + +type AdditionalHeaders record {| + string user\-id; + int req\-id; + string content\-type; +|}; + +type AdditionalMissingHeaders record {| + string user\-id; + int req\-id; + string x\-content\-type; +|}; + +type AdditionalOptionalHeaders record {| + string user\-id; + int req\-id; + string x\-content\-type?; +|}; + +type AlbumNotFound record {| + *http:NotFound; + ErrorMessage body; + Headers headers; +|}; + +type AlbumFound record {| + *http:Ok; + Album body; + Headers headers; +|}; + +type AlbumFoundMock1 record {| + *http:Ok; + Album|MockAlbum body; + Headers headers; +|}; + +type AlbumFoundMock2 record {| + *http:Ok; + AlbumUnion1 body; + Headers headers; +|}; + +type AlbumFoundMock3 record {| + *http:Ok; + AlbumUnion2 body; + Headers headers; +|}; + +type AlbumInvalid record {| + *Album; + string invalidField; +|}; + +type AlbumFoundInvalid record {| + *http:Ok; + AlbumInvalid body; + Headers headers; +|}; + +enum AllowedMediaTypes { + APPLICATION_JSON = "application/json", + APPLICATION_XML = "application/xml" +} + +@constraint:String {pattern: re `application/json|application/xml`} +type MediaTypeWithConstraint string; + +type AlbumFoundWithConstraints record {| + *http:Ok; + record {| + @constraint:String {minLength: 1, maxLength: 10} + string id; + string name; + string artist; + @constraint:String {pattern: re `Hard Rock|Progressive Rock`} + string genre; + |} body; + record {| + @constraint:String {minLength: 1, maxLength: 10} + string user\-id; + @constraint:Int {minValue: 1, maxValue: 10} + int req\-id; + |} headers; + MediaTypeWithConstraint mediaType; +|}; + +type AlbumFoundWithInvalidConstraints1 record {| + *http:Ok; + record {| + @constraint:String {minLength: 1, maxLength: 10} + string id; + string name; + string artist; + @constraint:String {pattern: re `Hard-Rock|Progressive-Rock`} + string genre; + |} body; + record {| + @constraint:String {minLength: 1, maxLength: 10} + string user\-id; + @constraint:Int {minValue: 1, maxValue: 10} + int req\-id; + |} headers; + MediaTypeWithConstraint mediaType; +|}; + +type AlbumFoundWithInvalidConstraints2 record {| + *http:Ok; + record {| + @constraint:String {minLength: 1, maxLength: 10} + string id; + string name; + string artist; + @constraint:String {pattern: re `Hard Rock|Progressive Rock`} + string genre; + |} body; + record {| + @constraint:String {minLength: 1, maxLength: 10} + string user\-id; + @constraint:Int {minValue: 10} + int req\-id; + |} headers; + MediaTypeWithConstraint mediaType; +|}; + +@constraint:String {pattern: re `application+org/json|application/xml`} +type MediaTypeWithInvalidPattern string; + +type AlbumFoundWithInvalidConstraints3 record {| + *http:Ok; + record {| + @constraint:String {minLength: 1, maxLength: 10} + string id; + string name; + string artist; + @constraint:String {pattern: re `Hard Rock|Progressive Rock`} + string genre; + |} body; + record {| + @constraint:String {minLength: 1, maxLength: 10} + string user\-id; + @constraint:Int {minValue: 1, maxValue: 10} + int req\-id; + |} headers; + MediaTypeWithInvalidPattern mediaType; +|}; + +service /api on new http:Listener(statusCodeBindingPort2) { + + resource function get albums/[string id]() returns AlbumFound|AlbumNotFound { + if albums.hasKey(id) { + return { + body: albums.get(id), + headers: {user\-id: "user-1", req\-id: 1} + }; + } + return { + body: {albumId: id, message: "Album not found"}, + headers: {user\-id: "user-1", req\-id: 1} + }; + } +} + +final http:Client albumClient = check new (string `localhost:${statusCodeBindingPort2}/api`); + +@test:Config {} +function testGetSuccessStatusCodeResponse() returns error? { + Album album = check albumClient->/albums/'1; + Album expectedAlbum = albums.get("1"); + test:assertEquals(album, expectedAlbum, "Invalid album returned"); + + AlbumFound albumFound = check albumClient->get("/albums/1"); + test:assertEquals(albumFound.body, expectedAlbum, "Invalid album returned"); + test:assertEquals(albumFound.headers.user\-id, "user-1", "Invalid user-id header"); + test:assertEquals(albumFound.headers.req\-id, 1, "Invalid req-id header"); + test:assertEquals(albumFound.mediaType, "application/json", "Invalid media type"); + + http:Response res = check albumClient->/albums/'1; + test:assertEquals(res.statusCode, 200, "Invalid status code"); + json payload = check res.getJsonPayload(); + album = check payload.fromJsonWithType(); + test:assertEquals(album, expectedAlbum, "Invalid album returned"); + + Album|AlbumFound res1 = check albumClient->get("/albums/1"); + if res1 is AlbumFound { + test:assertEquals(res1.body, expectedAlbum, "Invalid album returned"); + test:assertEquals(res1.headers.user\-id, "user-1", "Invalid user-id header"); + test:assertEquals(res1.headers.req\-id, 1, "Invalid req-id header"); + test:assertEquals(res1.mediaType, "application/json", "Invalid media type"); + } else { + test:assertFail("Invalid response type"); + } + + AlbumFound|Album res2 = check albumClient->/albums/'1; + if res2 is AlbumFound { + test:assertEquals(res2.body, expectedAlbum, "Invalid album returned"); + test:assertEquals(res2.headers.user\-id, "user-1", "Invalid user-id header"); + test:assertEquals(res2.headers.req\-id, 1, "Invalid req-id header"); + test:assertEquals(res2.mediaType, "application/json", "Invalid media type"); + } else { + test:assertFail("Invalid response type"); + } + + Album|AlbumNotFound res3 = check albumClient->get("/albums/1"); + if res3 is Album { + test:assertEquals(res3, expectedAlbum, "Invalid album returned"); + } else { + test:assertFail("Invalid response type"); + } + + AlbumFound|AlbumNotFound res4 = check albumClient->/albums/'1; + if res4 is AlbumFound { + test:assertEquals(res4.body, expectedAlbum, "Invalid album returned"); + test:assertEquals(res4.headers.user\-id, "user-1", "Invalid user-id header"); + test:assertEquals(res4.headers.req\-id, 1, "Invalid req-id header"); + test:assertEquals(res4.mediaType, "application/json", "Invalid media type"); + } else { + test:assertFail("Invalid response type"); + } + + Album|AlbumFound|AlbumNotFound res5 = check albumClient->get("/albums/1"); + if res5 is AlbumFound { + test:assertEquals(res5.body, expectedAlbum, "Invalid album returned"); + test:assertEquals(res5.headers.user\-id, "user-1", "Invalid user-id header"); + test:assertEquals(res5.headers.req\-id, 1, "Invalid req-id header"); + test:assertEquals(res5.mediaType, "application/json", "Invalid media type"); + } else { + test:assertFail("Invalid response type"); + } + + Album|AlbumNotFound|http:Response res6 = check albumClient->/albums/'1; + if res6 is Album { + test:assertEquals(res6, expectedAlbum, "Invalid album returned"); + } else { + test:assertFail("Invalid response type"); + } + + AlbumNotFound|http:Response res7 = check albumClient->get("/albums/1"); + if res7 is http:Response { + test:assertEquals(res.statusCode, 200, "Invalid status code"); + payload = check res.getJsonPayload(); + album = check payload.fromJsonWithType(); + test:assertEquals(album, expectedAlbum, "Invalid album returned"); + } else { + test:assertFail("Invalid response type"); + } + + AlbumNotFound|error res8 = albumClient->/albums/'1; + if res8 is error { + test:assertTrue(res8 is http:PayloadBindingError); + test:assertEquals(res8.message(), "incompatible http_client_tests:AlbumNotFound found for response with 200", + "Invalid error message"); + error? cause = res8.cause(); + if cause is error { + test:assertEquals(cause.message(), "no 'anydata' type found in the target type", "Invalid cause error message"); + } + } else { + test:assertFail("Invalid response type"); + } +} + +@test:Config {} +function testGetFailureStatusCodeResponse() returns error? { + AlbumNotFound albumNotFound = check albumClient->/albums/'4; + ErrorMessage expectedErrorMessage = {albumId: "4", message: "Album not found"}; + test:assertEquals(albumNotFound.body, expectedErrorMessage, "Invalid error message"); + test:assertEquals(albumNotFound.headers.user\-id, "user-1", "Invalid user-id header"); + test:assertEquals(albumNotFound.headers.req\-id, 1, "Invalid req-id header"); + test:assertEquals(albumNotFound.mediaType, "application/json", "Invalid media type"); + + http:Response res = check albumClient->get("/albums/4"); + test:assertEquals(res.statusCode, 404, "Invalid status code"); + json payload = check res.getJsonPayload(); + ErrorMessage errorMessage = check payload.fromJsonWithType(); + test:assertEquals(errorMessage, expectedErrorMessage, "Invalid error message"); + + Album|AlbumNotFound res1 = check albumClient->/albums/'4; + if res1 is AlbumNotFound { + test:assertEquals(res1.body, expectedErrorMessage, "Invalid error message"); + test:assertEquals(res1.headers.user\-id, "user-1", "Invalid user-id header"); + test:assertEquals(res1.headers.req\-id, 1, "Invalid req-id header"); + test:assertEquals(res1.mediaType, "application/json", "Invalid media type"); + } else { + test:assertFail("Invalid response type"); + } + + AlbumNotFound|http:Response res2 = check albumClient->get("/albums/4"); + if res2 is AlbumNotFound { + test:assertEquals(res2.body, expectedErrorMessage, "Invalid error message"); + test:assertEquals(res2.headers.user\-id, "user-1", "Invalid user-id header"); + test:assertEquals(res2.headers.req\-id, 1, "Invalid req-id header"); + test:assertEquals(res2.mediaType, "application/json", "Invalid media type"); + } else { + test:assertFail("Invalid response type"); + } + + Album|http:Response res3 = check albumClient->/albums/'4; + if res3 is http:Response { + test:assertEquals(res3.statusCode, 404, "Invalid status code"); + payload = check res3.getJsonPayload(); + errorMessage = check payload.fromJsonWithType(); + test:assertEquals(errorMessage, expectedErrorMessage, "Invalid error message"); + } else { + test:assertFail("Invalid response type"); + } + + http:Response|AlbumFound res4 = check albumClient->get("/albums/4"); + if res4 is http:Response { + test:assertEquals(res4.statusCode, 404, "Invalid status code"); + payload = check res4.getJsonPayload(); + errorMessage = check payload.fromJsonWithType(); + test:assertEquals(errorMessage, expectedErrorMessage, "Invalid error message"); + } else { + test:assertFail("Invalid response type"); + } + + Album|error res5 = albumClient->/albums/'4; + if res5 is error { + test:assertTrue(res5 is http:ClientRequestError); + test:assertEquals(res5.message(), "Not Found", "Invalid error message"); + test:assertEquals(res5.detail()["statusCode"], 404, "Invalid status code"); + test:assertEquals(res5.detail()["body"], expectedErrorMessage, "Invalid error message"); + if res5.detail()["headers"] is map { + map headers = check res5.detail()["headers"].ensureType(); + test:assertEquals(headers.get("user-id")[0], "user-1", "Invalid user-id header"); + test:assertEquals(headers.get("req-id")[0], "1", "Invalid req-id header"); + } + + } else { + test:assertFail("Invalid response type"); + } + + AlbumFound|error res6 = albumClient->get("/albums/4"); + if res6 is error { + test:assertTrue(res6 is http:ClientRequestError); + test:assertEquals(res6.message(), "Not Found", "Invalid error message"); + test:assertEquals(res6.detail()["statusCode"], 404, "Invalid status code"); + test:assertEquals(res6.detail()["body"], expectedErrorMessage, "Invalid error message"); + if res6.detail()["headers"] is map { + map headers = check res6.detail()["headers"].ensureType(); + test:assertEquals(headers.get("user-id")[0], "user-1", "Invalid user-id header"); + test:assertEquals(headers.get("req-id")[0], "1", "Invalid req-id header"); + } + + } else { + test:assertFail("Invalid response type"); + } +} + +@test:Config {} +function testUnionPayloadBindingWithStatusCodeResponse() returns error? { + Album|AlbumNotFound|map|json res1 = check albumClient->/albums/'1; + if res1 is Album { + test:assertEquals(res1, albums.get("1"), "Invalid album returned"); + } else { + test:assertFail("Invalid response type"); + } + + map|AlbumNotFound|Album|json res2 = check albumClient->get("/albums/1"); + if res2 is map { + test:assertEquals(res2, albums.get("1"), "Invalid album returned"); + } else { + test:assertFail("Invalid response type"); + } + + Album|MockAlbum|AlbumNotFound res3 = check albumClient->/albums/'1; + if res3 is Album { + test:assertEquals(res3, albums.get("1"), "Invalid album returned"); + } else { + test:assertFail("Invalid response type"); + } + + MockAlbum|Album|AlbumNotFound res4 = check albumClient->get("/albums/1"); + if res4 is MockAlbum { + test:assertEquals(res4, {...albums.get("1"), "type": "mock"}, "Invalid album returned"); + } else { + test:assertFail("Invalid response type"); + } + + AlbumUnion1|AlbumNotFound res5 = check albumClient->/albums/'1; + if res5 is Album { + test:assertEquals(res5, albums.get("1"), "Invalid album returned"); + } else { + test:assertFail("Invalid response type"); + } + + AlbumUnion2|AlbumNotFound res6 = check albumClient->get("/albums/1"); + if res6 is MockAlbum { + test:assertEquals(res6, {...albums.get("1"), "type": "mock"}, "Invalid album returned"); + } else { + test:assertFail("Invalid response type"); + } + + AlbumFound|AlbumNotFound|AlbumFoundMock1 res7 = check albumClient->/albums/'1; + if res7 is AlbumFound { + test:assertEquals(res7.body, albums.get("1"), "Invalid album returned"); + test:assertEquals(res7.headers.user\-id, "user-1", "Invalid user-id header"); + test:assertEquals(res7.headers.req\-id, 1, "Invalid req-id header"); + test:assertEquals(res7.mediaType, "application/json", "Invalid media type"); + } else { + test:assertFail("Invalid response type"); + } + + AlbumFoundMock1|AlbumFound|AlbumNotFound res8 = check albumClient->get("/albums/1"); + if res8 is AlbumFoundMock1 { + test:assertEquals(res8.body, albums.get("1"), "Invalid album returned"); + test:assertEquals(res8.headers.user\-id, "user-1", "Invalid user-id header"); + test:assertEquals(res8.headers.req\-id, 1, "Invalid req-id header"); + test:assertEquals(res8.mediaType, "application/json", "Invalid media type"); + } else { + test:assertFail("Invalid response type"); + } + + AlbumFoundMock2|AlbumFound|AlbumFoundMock1|AlbumNotFound res9 = check albumClient->/albums/'1; + if res9 is AlbumFoundMock2 { + test:assertEquals(res9.body, albums.get("1"), "Invalid album returned"); + test:assertEquals(res9.headers.user\-id, "user-1", "Invalid user-id header"); + test:assertEquals(res9.headers.req\-id, 1, "Invalid req-id header"); + test:assertEquals(res9.mediaType, "application/json", "Invalid media type"); + } else { + test:assertFail("Invalid response type"); + } + + AlbumFoundMock3|AlbumFound|AlbumFoundMock1|AlbumFoundMock2|AlbumNotFound res10 = check albumClient->get("/albums/1"); + if res10 is AlbumFoundMock3 { + test:assertEquals(res10.body, {...albums.get("1"), "type": "mock"}, "Invalid album returned"); + test:assertEquals(res10.headers.user\-id, "user-1", "Invalid user-id header"); + test:assertEquals(res10.headers.req\-id, 1, "Invalid req-id header"); + test:assertEquals(res10.mediaType, "application/json", "Invalid media type"); + } else { + test:assertFail("Invalid response type"); + } + + AlbumFoundInvalid|AlbumFound|AlbumNotFound|error res11 = albumClient->/albums/'1; + if res11 is error { + test:assertTrue(res11 is http:PayloadBindingError); + test:assertTrue(res11.message().includes("Payload binding failed: 'map' value cannot be" + + " converted to 'http_client_tests:AlbumInvalid"), "Invalid error message"); + } else { + test:assertFail("Invalid response type"); + } +} + +@test:Config {} +function testStatusCodeBindingWithDifferentHeaders() returns error? { + record {|*http:Ok; ArrayHeaders headers;|} res1 = check albumClient->/albums/'1; + test:assertEquals(res1?.body, albums.get("1"), "Invalid album returned"); + test:assertEquals(res1.headers.user\-id, ["user-1"], "Invalid user-id header"); + test:assertEquals(res1.headers.req\-id, [1], "Invalid req-id header"); + test:assertEquals(res1.mediaType, "application/json", "Invalid media type"); + + record {|*http:Ok; ArrayHeaderWithUnion headers;|} res2 = check albumClient->/albums/'1; + test:assertEquals(res2?.body, albums.get("1"), "Invalid album returned"); + test:assertEquals(res2.headers.user\-id, ["user-1"], "Invalid user-id header"); + test:assertEquals(res2.headers.req\-id, [1], "Invalid req-id header"); + test:assertEquals(res2.mediaType, "application/json", "Invalid media type"); + + record {|*http:Ok; ArrayHeaderWithTypes headers;|} res3 = check albumClient->/albums/'1; + test:assertEquals(res3?.body, albums.get("1"), "Invalid album returned"); + test:assertEquals(res3.headers.user\-id, USER1, "Invalid user-id header"); + test:assertEquals(res3.headers.req\-id, 1, "Invalid req-id header"); + test:assertEquals(res3.mediaType, "application/json", "Invalid media type"); + + record {|*http:Ok; IntHeaders headers;|}|error res4 = albumClient->/albums/'1; + if res4 is error { + test:assertTrue(res4 is http:HeaderBindingError); + test:assertEquals(res4.message(), "header binding failed for parameter: 'user-id'", "Invalid error message"); + } else { + test:assertFail("Invalid response type"); + } + + record {|*http:Ok; AdditionalHeaders headers;|} res5 = check albumClient->/albums/'1; + test:assertEquals(res5?.body, albums.get("1"), "Invalid album returned"); + test:assertEquals(res5.headers.user\-id, "user-1", "Invalid user-id header"); + test:assertEquals(res5.headers.req\-id, 1, "Invalid req-id header"); + test:assertEquals(res5.headers.content\-type, "application/json", "Invalid content-type header"); + test:assertEquals(res5.mediaType, "application/json", "Invalid media type"); + + record {|*http:Ok; AdditionalMissingHeaders headers;|}|error res6 = albumClient->/albums/'1; + if res6 is error { + test:assertTrue(res6 is http:HeaderNotFoundError); + test:assertEquals(res6.message(), "no header value found for 'x-content-type'", "Invalid error message"); + } else { + test:assertFail("Invalid response type"); + } + + record {|*http:Ok; AdditionalOptionalHeaders headers;|} res7 = check albumClient->/albums/'1; + test:assertEquals(res7?.body, albums.get("1"), "Invalid album returned"); + test:assertEquals(res7.headers.user\-id, "user-1", "Invalid user-id header"); + test:assertEquals(res7.headers.req\-id, 1, "Invalid req-id header"); + test:assertEquals(res7.headers.x\-content\-type, (), "Invalid x-content-type header"); + test:assertEquals(res7.mediaType, "application/json", "Invalid media type"); +} + +@test:Config {} +function testStatusCodeBindingWithMediaTypes() returns error? { + record {|*http:Ok; "application/json" mediaType;|} res1 = check albumClient->/albums/'1; + test:assertEquals(res1?.body, albums.get("1"), "Invalid album returned"); + test:assertEquals(res1.mediaType, "application/json", "Invalid media type"); + map headers = res1.headers ?: {}; + test:assertEquals(headers.get("user-id"), "user-1", "Invalid user-id header"); + test:assertEquals(headers.get("req-id"), "1", "Invalid req-id header"); + + record {|*http:Ok; "application/xml"|"application/json" mediaType;|} res2 = check albumClient->/albums/'1; + test:assertEquals(res2?.body, albums.get("1"), "Invalid album returned"); + test:assertEquals(res2.mediaType, "application/json", "Invalid media type"); + headers = res2.headers ?: {}; + test:assertEquals(headers.get("user-id"), "user-1", "Invalid user-id header"); + test:assertEquals(headers.get("req-id"), "1", "Invalid req-id header"); + + record {|*http:Ok; AllowedMediaTypes mediaType;|} res3 = check albumClient->/albums/'1; + test:assertEquals(res3?.body, albums.get("1"), "Invalid album returned"); + test:assertEquals(res3.mediaType, APPLICATION_JSON, "Invalid media type"); + headers = res3.headers ?: {}; + test:assertEquals(headers.get("user-id"), "user-1", "Invalid user-id header"); + test:assertEquals(headers.get("req-id"), "1", "Invalid req-id header"); + + record {|*http:Ok; "application/xml" mediaType;|}|error res4 = albumClient->/albums/'1; + if res4 is error { + test:assertTrue(res4 is http:MediaTypeBindingError); + test:assertEquals(res4.message(), "media-type binding failed", "Invalid error message"); + } else { + test:assertFail("Invalid response type"); + } +} + +@test:Config {} +function testStatusCodeBindingWithConstraintsSuccess() returns error? { + AlbumFoundWithConstraints res1 = check albumClient->/albums/'2; + test:assertEquals(res1.body, albums.get("2"), "Invalid album returned"); + test:assertEquals(res1.headers.user\-id, "user-1", "Invalid user-id header"); + test:assertEquals(res1.headers.req\-id, 1, "Invalid req-id header"); + test:assertEquals(res1.mediaType, "application/json", "Invalid media type"); + + AlbumFoundWithConstraints|AlbumFound|Album|http:Response res2 = check albumClient->get("/albums/2"); + if res2 is AlbumFoundWithConstraints { + test:assertEquals(res2.body, albums.get("2"), "Invalid album returned"); + test:assertEquals(res2.headers.user\-id, "user-1", "Invalid user-id header"); + test:assertEquals(res2.headers.req\-id, 1, "Invalid req-id header"); + test:assertEquals(res2.mediaType, "application/json", "Invalid media type"); + } else { + test:assertFail("Invalid response type"); + } + + AlbumFound|AlbumFoundWithConstraints res3 = check albumClient->/albums/'2; + if res3 is AlbumFound { + test:assertEquals(res3.body, albums.get("2"), "Invalid album returned"); + test:assertEquals(res3.headers.user\-id, "user-1", "Invalid user-id header"); + test:assertEquals(res3.headers.req\-id, 1, "Invalid req-id header"); + test:assertEquals(res3.mediaType, "application/json", "Invalid media type"); + } else { + test:assertFail("Invalid response type"); + } +} + +@test:Config {} +function testStatusCodeBindingWithConstraintsFailure() returns error? { + AlbumFoundWithInvalidConstraints1|error res1 = albumClient->/albums/'2; + if res1 is error { + test:assertTrue(res1 is http:PayloadValidationError); + test:assertEquals(res1.message(), "payload validation failed: Validation failed for " + + "'$.genre:pattern' constraint(s).", "Invalid error message"); + } else { + test:assertFail("Invalid response type"); + } + + AlbumFoundWithInvalidConstraints2|AlbumFound|error res2 = albumClient->get("/albums/2"); + if res2 is error { + test:assertTrue(res2 is http:HeaderValidationError); + test:assertEquals(res2.message(), "header binding failed: Validation failed for " + + "'$.req-id:minValue' constraint(s).", "Invalid error message"); + } else { + test:assertFail("Invalid response type"); + } + + AlbumFoundWithInvalidConstraints3|Album|error res3 = albumClient->/albums/'2; + if res3 is error { + test:assertTrue(res3 is http:MediaTypeValidationError); + test:assertEquals(res3.message(), "media-type binding failed: Validation failed for " + + "'$:pattern' constraint(s).", "Invalid error message"); + } else { + test:assertFail("Invalid response type"); + } + + AlbumFound|AlbumFoundWithInvalidConstraints1|error res4 = albumClient->get("/albums/2"); + if res4 is AlbumFound { + test:assertEquals(res4.body, albums.get("2"), "Invalid album returned"); + test:assertEquals(res4.headers.user\-id, "user-1", "Invalid user-id header"); + test:assertEquals(res4.headers.req\-id, 1, "Invalid req-id header"); + test:assertEquals(res4.mediaType, "application/json", "Invalid media type"); + } else { + test:assertFail("Invalid response type"); + } +} diff --git a/ballerina-tests/http-client-tests/tests/sc_res_binding_with_all_status_codes_tests.bal b/ballerina-tests/http-client-tests/tests/sc_res_binding_with_all_status_codes_tests.bal new file mode 100644 index 0000000000..f5e25b4a6b --- /dev/null +++ b/ballerina-tests/http-client-tests/tests/sc_res_binding_with_all_status_codes_tests.bal @@ -0,0 +1,510 @@ +// 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/test; + +final http:Client statusCodeBindingClient1 = check new (string `localhost:${statusCodeBindingPort1}`); + +service /api on new http:Listener(statusCodeBindingPort1) { + + resource function get status\-code\-response(int code) returns http:StatusCodeResponse { + return getStatusCodeResponse(code); + } +} + +function getStatusCodeResponse(int code) returns http:StatusCodeResponse { + match code { + // Cannot test these since these status codes expects different behaviors at transport level + // 100 => { + // return {body: {msg: "Continue response"}, mediaType: "application/org+json", headers: {"x-error": "Continue"}}; + // } + // 101 => { + // return {body: {msg: "Switching protocols response"}, mediaType: "application/org+json", headers: {"x-error": "Switching protocols"}}; + // } + 102 => { + return {body: {msg: "Processing response"}, mediaType: "application/org+json", headers: {"x-error": "Processing"}}; + } + 103 => { + return {body: {msg: "Early hints response"}, mediaType: "application/org+json", headers: {"x-error": "Early hints"}}; + } + 200 => { + return {body: {msg: "OK response"}, mediaType: "application/org+json", headers: {"x-error": "OK"}}; + } + 201 => { + return {body: {msg: "Created response"}, mediaType: "application/org+json", headers: {"x-error": "Created"}}; + } + 202 => { + return {body: {msg: "Accepted response"}, mediaType: "application/org+json", headers: {"x-error": "Accepted"}}; + } + 203 => { + return {body: {msg: "Non-authoritative information response"}, mediaType: "application/org+json", headers: {"x-error": "Non-authoritative information"}}; + } + 204 => { + return {headers: {"x-error": "No content"}}; + } + 205 => { + return {body: {msg: "Reset content response"}, mediaType: "application/org+json", headers: {"x-error": "Reset content"}}; + } + 206 => { + return {body: {msg: "Partial content response"}, mediaType: "application/org+json", headers: {"x-error": "Partial content"}}; + } + 207 => { + return {body: {msg: "Multi-status response"}, mediaType: "application/org+json", headers: {"x-error": "Multi-status"}}; + } + 208 => { + return {body: {msg: "Already reported response"}, mediaType: "application/org+json", headers: {"x-error": "Already reported"}}; + } + 226 => { + return {body: {msg: "IM used response"}, mediaType: "application/org+json", headers: {"x-error": "IM used"}}; + } + 300 => { + return {body: {msg: "Multiple choices response"}, mediaType: "application/org+json", headers: {"x-error": "Multiple choices"}}; + } + 301 => { + return {body: {msg: "Moved permanently response"}, mediaType: "application/org+json", headers: {"x-error": "Moved permanently"}}; + } + 302 => { + return {body: {msg: "Found response"}, mediaType: "application/org+json", headers: {"x-error": "Found"}}; + } + 303 => { + return {body: {msg: "See other response"}, mediaType: "application/org+json", headers: {"x-error": "See other"}}; + } + 304 => { + return {body: {msg: "Not modified response"}, mediaType: "application/org+json", headers: {"x-error": "Not modified"}}; + } + 305 => { + return {body: {msg: "Use proxy response"}, mediaType: "application/org+json", headers: {"x-error": "Use proxy"}}; + } + 307 => { + return {body: {msg: "Temporary redirect response"}, mediaType: "application/org+json", headers: {"x-error": "Temporary redirect"}}; + } + 308 => { + return {body: {msg: "Permanent redirect response"}, mediaType: "application/org+json", headers: {"x-error": "Permanent redirect"}}; + } + 400 => { + return {body: {msg: "Bad request error"}, mediaType: "application/org+json", headers: {"x-error": "Bad request"}}; + } + 401 => { + return {body: {msg: "Unauthorized error"}, mediaType: "application/org+json", headers: {"x-error": "Unauthorized"}}; + } + 402 => { + return {body: {msg: "Payment required error"}, mediaType: "application/org+json", headers: {"x-error": "Payment required"}}; + } + 403 => { + return {body: {msg: "Forbidden error"}, mediaType: "application/org+json", headers: {"x-error": "Forbidden"}}; + } + 404 => { + return {body: {msg: "Not found error"}, mediaType: "application/org+json", headers: {"x-error": "Not found"}}; + } + 405 => { + return {body: {msg: "Method not allowed error"}, mediaType: "application/org+json", headers: {"x-error": "Method not allowed"}}; + } + 406 => { + return {body: {msg: "Not acceptable error"}, mediaType: "application/org+json", headers: {"x-error": "Not acceptable"}}; + } + 407 => { + return {body: {msg: "Proxy authentication required error"}, mediaType: "application/org+json", headers: {"x-error": "Proxy authentication required"}}; + } + 408 => { + return {body: {msg: "Request timeout error"}, mediaType: "application/org+json", headers: {"x-error": "Request timeout"}}; + } + 409 => { + return {body: {msg: "Conflict error"}, mediaType: "application/org+json", headers: {"x-error": "Conflict"}}; + } + 410 => { + return {body: {msg: "Gone error"}, mediaType: "application/org+json", headers: {"x-error": "Gone"}}; + } + 411 => { + return {body: {msg: "Length required error"}, mediaType: "application/org+json", headers: {"x-error": "Length required"}}; + } + 412 => { + return {body: {msg: "Precondition failed error"}, mediaType: "application/org+json", headers: {"x-error": "Precondition failed"}}; + } + 413 => { + return {body: {msg: "Payload too large error"}, mediaType: "application/org+json", headers: {"x-error": "Payload too large"}}; + } + 414 => { + return {body: {msg: "URI too long error"}, mediaType: "application/org+json", headers: {"x-error": "URI too long"}}; + } + 415 => { + return {body: {msg: "Unsupported media type error"}, mediaType: "application/org+json", headers: {"x-error": "Unsupported media type"}}; + } + 416 => { + return {body: {msg: "Range not satisfiable error"}, mediaType: "application/org+json", headers: {"x-error": "Range not satisfiable"}}; + } + 417 => { + return {body: {msg: "Expectation failed error"}, mediaType: "application/org+json", headers: {"x-error": "Expectation failed"}}; + } + 421 => { + return {body: {msg: "Misdirected request error"}, mediaType: "application/org+json", headers: {"x-error": "Misdirected request"}}; + } + 422 => { + return {body: {msg: "Unprocessable entity error"}, mediaType: "application/org+json", headers: {"x-error": "Unprocessable entity"}}; + } + 423 => { + return {body: {msg: "Locked error"}, mediaType: "application/org+json", headers: {"x-error": "Locked"}}; + } + 424 => { + return {body: {msg: "Failed dependency error"}, mediaType: "application/org+json", headers: {"x-error": "Failed dependency"}}; + } + 425 => { + return {body: {msg: "Too early error"}, mediaType: "application/org+json", headers: {"x-error": "Too early"}}; + } + 426 => { + return {body: {msg: "Upgrade required error"}, mediaType: "application/org+json", headers: {"x-error": "Upgrade required"}}; + } + 428 => { + return {body: {msg: "Precondition required error"}, mediaType: "application/org+json", headers: {"x-error": "Precondition required"}}; + } + 429 => { + return {body: {msg: "Too many requests error"}, mediaType: "application/org+json", headers: {"x-error": "Too many requests"}}; + } + 431 => { + return {body: {msg: "Request header fields too large error"}, mediaType: "application/org+json", headers: {"x-error": "Request header fields too large"}}; + } + 451 => { + return {body: {msg: "Unavailable for legal reasons error"}, mediaType: "application/org+json", headers: {"x-error": "Unavailable for legal reasons"}}; + } + 500 => { + return {body: {msg: "Internal server error"}, mediaType: "application/org+json", headers: {"x-error": "Internal server error"}}; + } + 501 => { + return {body: {msg: "Not implemented error"}, mediaType: "application/org+json", headers: {"x-error": "Not implemented"}}; + } + 502 => { + return {body: {msg: "Bad gateway error"}, mediaType: "application/org+json", headers: {"x-error": "Bad gateway"}}; + } + 503 => { + return {body: {msg: "Service unavailable error"}, mediaType: "application/org+json", headers: {"x-error": "Service unavailable"}}; + } + 504 => { + return {body: {msg: "Gateway timeout error"}, mediaType: "application/org+json", headers: {"x-error": "Gateway timeout"}}; + } + 505 => { + return {body: {msg: "HTTP version not supported error"}, mediaType: "application/org+json", headers: {"x-error": "HTTP version not supported"}}; + } + 506 => { + return {body: {msg: "Variant also negotiates error"}, mediaType: "application/org+json", headers: {"x-error": "Variant also negotiates"}}; + } + 507 => { + return {body: {msg: "Insufficient storage error"}, mediaType: "application/org+json", headers: {"x-error": "Insufficient storage"}}; + } + 508 => { + return {body: {msg: "Loop detected error"}, mediaType: "application/org+json", headers: {"x-error": "Loop detected"}}; + } + 510 => { + return {body: {msg: "Not extended error"}, mediaType: "application/org+json", headers: {"x-error": "Not extended"}}; + } + 511 => { + return {body: {msg: "Network authentication required error"}, mediaType: "application/org+json", headers: {"x-error": "Network authentication required"}}; + } + _ => { + return {body: {msg: "Bad request with unknown status code"}, mediaType: "application/org+json", headers: {"x-error": "Unknown status code"}}; + } + } +} + +@test:Config {} +function testSCResBindingWith1XXStatusCodes() returns error? { + http:Processing processingResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 102); + test:assertTrue(processingResponse is http:Processing, "Response type mismatched"); + testStatusCodeResponse(processingResponse, 102, "Processing", "Processing response"); + + http:EarlyHints earlyHintsResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 103); + test:assertTrue(earlyHintsResponse is http:EarlyHints, "Response type mismatched"); + testStatusCodeResponse(earlyHintsResponse, 103, "Early hints", "Early hints response"); +} + +@test:Config {} +function testSCResBindingWith2XXStatusCodes() returns error? { + http:Ok okResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 200); + test:assertTrue(okResponse is http:Ok, "Response type mismatched"); + testStatusCodeResponse(okResponse, 200, "OK", "OK response"); + + http:Created createdResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 201); + test:assertTrue(createdResponse is http:Created, "Response type mismatched"); + testStatusCodeResponse(createdResponse, 201, "Created", "Created response"); + + http:Accepted acceptedResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 202); + test:assertTrue(acceptedResponse is http:Accepted, "Response type mismatched"); + testStatusCodeResponse(acceptedResponse, 202, "Accepted", "Accepted response"); + + http:NonAuthoritativeInformation nonAuthoritativeInformationResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 203); + test:assertTrue(nonAuthoritativeInformationResponse is http:NonAuthoritativeInformation, "Response type mismatched"); + testStatusCodeResponse(nonAuthoritativeInformationResponse, 203, "Non-authoritative information", "Non-authoritative information response"); + + http:NoContent noContentResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 204); + test:assertTrue(noContentResponse is http:NoContent, "Response type mismatched"); + testStatusCodeResponse(noContentResponse, 204, "No content"); + + http:ResetContent resetContentResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 205); + test:assertTrue(resetContentResponse is http:ResetContent, "Response type mismatched"); + testStatusCodeResponse(resetContentResponse, 205, "Reset content", "Reset content response"); + + http:PartialContent partialContentResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 206); + test:assertTrue(partialContentResponse is http:PartialContent, "Response type mismatched"); + testStatusCodeResponse(partialContentResponse, 206, "Partial content", "Partial content response"); + + http:MultiStatus multiStatusResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 207); + test:assertTrue(multiStatusResponse is http:MultiStatus, "Response type mismatched"); + testStatusCodeResponse(multiStatusResponse, 207, "Multi-status", "Multi-status response"); + + http:AlreadyReported alreadyReportedResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 208); + test:assertTrue(alreadyReportedResponse is http:AlreadyReported, "Response type mismatched"); + testStatusCodeResponse(alreadyReportedResponse, 208, "Already reported", "Already reported response"); + + http:IMUsed imUsedResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 226); + test:assertTrue(imUsedResponse is http:IMUsed, "Response type mismatched"); + testStatusCodeResponse(imUsedResponse, 226, "IM used", "IM used response"); +} + +@test:Config {} +function testSCResBindingWith3XXStatusCodes() returns error? { + http:MultipleChoices multipleChoicesResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 300); + test:assertTrue(multipleChoicesResponse is http:MultipleChoices, "Response type mismatched"); + testStatusCodeResponse(multipleChoicesResponse, 300, "Multiple choices", "Multiple choices response"); + + http:MovedPermanently movedPermanentlyResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 301); + test:assertTrue(movedPermanentlyResponse is http:MovedPermanently, "Response type mismatched"); + testStatusCodeResponse(movedPermanentlyResponse, 301, "Moved permanently", "Moved permanently response"); + + http:Found foundResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 302); + test:assertTrue(foundResponse is http:Found, "Response type mismatched"); + testStatusCodeResponse(foundResponse, 302, "Found", "Found response"); + + http:SeeOther seeOtherResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 303); + test:assertTrue(seeOtherResponse is http:SeeOther, "Response type mismatched"); + testStatusCodeResponse(seeOtherResponse, 303, "See other", "See other response"); + + http:NotModified notModifiedResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 304); + test:assertTrue(notModifiedResponse is http:NotModified, "Response type mismatched"); + testStatusCodeResponse(notModifiedResponse, 304, "Not modified", "Not modified response"); + + http:UseProxy useProxyResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 305); + test:assertTrue(useProxyResponse is http:UseProxy, "Response type mismatched"); + testStatusCodeResponse(useProxyResponse, 305, "Use proxy", "Use proxy response"); + + http:TemporaryRedirect temporaryRedirectResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 307); + test:assertTrue(temporaryRedirectResponse is http:TemporaryRedirect, "Response type mismatched"); + testStatusCodeResponse(temporaryRedirectResponse, 307, "Temporary redirect", "Temporary redirect response"); + + http:PermanentRedirect permanentRedirectResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 308); + test:assertTrue(permanentRedirectResponse is http:PermanentRedirect, "Response type mismatched"); + testStatusCodeResponse(permanentRedirectResponse, 308, "Permanent redirect", "Permanent redirect response"); +} + +@test:Config {} +function testSCResBindingWith4XXStatusCodes() returns error? { + http:BadRequest badRequestResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 400); + test:assertTrue(badRequestResponse is http:BadRequest, "Response type mismatched"); + testStatusCodeResponse(badRequestResponse, 400, "Bad request", "Bad request error"); + + http:Unauthorized unauthorizedResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 401); + test:assertTrue(unauthorizedResponse is http:Unauthorized, "Response type mismatched"); + testStatusCodeResponse(unauthorizedResponse, 401, "Unauthorized", "Unauthorized error"); + + http:PaymentRequired paymentRequiredResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 402); + test:assertTrue(paymentRequiredResponse is http:PaymentRequired, "Response type mismatched"); + testStatusCodeResponse(paymentRequiredResponse, 402, "Payment required", "Payment required error"); + + http:Forbidden forbiddenResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 403); + test:assertTrue(forbiddenResponse is http:Forbidden, "Response type mismatched"); + testStatusCodeResponse(forbiddenResponse, 403, "Forbidden", "Forbidden error"); + + http:NotFound notFoundResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 404); + test:assertTrue(notFoundResponse is http:NotFound, "Response type mismatched"); + testStatusCodeResponse(notFoundResponse, 404, "Not found", "Not found error"); + + http:MethodNotAllowed methodNotAllowedResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 405); + test:assertTrue(methodNotAllowedResponse is http:MethodNotAllowed, "Response type mismatched"); + testStatusCodeResponse(methodNotAllowedResponse, 405, "Method not allowed", "Method not allowed error"); + + http:NotAcceptable notAcceptableResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 406); + test:assertTrue(notAcceptableResponse is http:NotAcceptable, "Response type mismatched"); + testStatusCodeResponse(notAcceptableResponse, 406, "Not acceptable", "Not acceptable error"); + + http:ProxyAuthenticationRequired proxyAuthenticationRequiredResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 407); + test:assertTrue(proxyAuthenticationRequiredResponse is http:ProxyAuthenticationRequired, "Response type mismatched"); + testStatusCodeResponse(proxyAuthenticationRequiredResponse, 407, "Proxy authentication required", "Proxy authentication required error"); + + http:RequestTimeout requestTimeoutResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 408); + test:assertTrue(requestTimeoutResponse is http:RequestTimeout, "Response type mismatched"); + testStatusCodeResponse(requestTimeoutResponse, 408, "Request timeout", "Request timeout error"); + + http:Conflict conflictResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 409); + test:assertTrue(conflictResponse is http:Conflict, "Response type mismatched"); + testStatusCodeResponse(conflictResponse, 409, "Conflict", "Conflict error"); + + http:Gone goneResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 410); + test:assertTrue(goneResponse is http:Gone, "Response type mismatched"); + testStatusCodeResponse(goneResponse, 410, "Gone", "Gone error"); + + http:LengthRequired lengthRequiredResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 411); + test:assertTrue(lengthRequiredResponse is http:LengthRequired, "Response type mismatched"); + testStatusCodeResponse(lengthRequiredResponse, 411, "Length required", "Length required error"); + + http:PreconditionFailed preconditionFailedResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 412); + test:assertTrue(preconditionFailedResponse is http:PreconditionFailed, "Response type mismatched"); + testStatusCodeResponse(preconditionFailedResponse, 412, "Precondition failed", "Precondition failed error"); + + http:PayloadTooLarge payloadTooLargeResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 413); + test:assertTrue(payloadTooLargeResponse is http:PayloadTooLarge, "Response type mismatched"); + testStatusCodeResponse(payloadTooLargeResponse, 413, "Payload too large", "Payload too large error"); + + http:UriTooLong uriTooLongResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 414); + test:assertTrue(uriTooLongResponse is http:UriTooLong, "Response type mismatched"); + testStatusCodeResponse(uriTooLongResponse, 414, "URI too long", "URI too long error"); + + http:UnsupportedMediaType unsupportedMediaTypeResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 415); + test:assertTrue(unsupportedMediaTypeResponse is http:UnsupportedMediaType, "Response type mismatched"); + testStatusCodeResponse(unsupportedMediaTypeResponse, 415, "Unsupported media type", "Unsupported media type error"); + + http:RangeNotSatisfiable rangeNotSatisfiableResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 416); + test:assertTrue(rangeNotSatisfiableResponse is http:RangeNotSatisfiable, "Response type mismatched"); + testStatusCodeResponse(rangeNotSatisfiableResponse, 416, "Range not satisfiable", "Range not satisfiable error"); + + http:ExpectationFailed expectationFailedResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 417); + test:assertTrue(expectationFailedResponse is http:ExpectationFailed, "Response type mismatched"); + testStatusCodeResponse(expectationFailedResponse, 417, "Expectation failed", "Expectation failed error"); + + http:MisdirectedRequest misdirectedRequestResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 421); + test:assertTrue(misdirectedRequestResponse is http:MisdirectedRequest, "Response type mismatched"); + testStatusCodeResponse(misdirectedRequestResponse, 421, "Misdirected request", "Misdirected request error"); + + http:UnprocessableEntity unprocessableEntityResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 422); + test:assertTrue(unprocessableEntityResponse is http:UnprocessableEntity, "Response type mismatched"); + testStatusCodeResponse(unprocessableEntityResponse, 422, "Unprocessable entity", "Unprocessable entity error"); + + http:Locked lockedResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 423); + test:assertTrue(lockedResponse is http:Locked, "Response type mismatched"); + testStatusCodeResponse(lockedResponse, 423, "Locked", "Locked error"); + + http:FailedDependency failedDependencyResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 424); + test:assertTrue(failedDependencyResponse is http:FailedDependency, "Response type mismatched"); + testStatusCodeResponse(failedDependencyResponse, 424, "Failed dependency", "Failed dependency error"); + + http:TooEarly tooEarlyResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 425); + test:assertTrue(tooEarlyResponse is http:TooEarly, "Response type mismatched"); + testStatusCodeResponse(tooEarlyResponse, 425, "Too early", "Too early error"); + + http:UpgradeRequired upgradeRequiredResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 426); + test:assertTrue(upgradeRequiredResponse is http:UpgradeRequired, "Response type mismatched"); + testStatusCodeResponse(upgradeRequiredResponse, 426, "Upgrade required", "Upgrade required error"); + + http:PreconditionRequired preconditionRequiredResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 428); + test:assertTrue(preconditionRequiredResponse is http:PreconditionRequired, "Response type mismatched"); + testStatusCodeResponse(preconditionRequiredResponse, 428, "Precondition required", "Precondition required error"); + + http:TooManyRequests tooManyRequestsResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 429); + test:assertTrue(tooManyRequestsResponse is http:TooManyRequests, "Response type mismatched"); + testStatusCodeResponse(tooManyRequestsResponse, 429, "Too many requests", "Too many requests error"); + + http:RequestHeaderFieldsTooLarge requestHeaderFieldsTooLargeResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 431); + test:assertTrue(requestHeaderFieldsTooLargeResponse is http:RequestHeaderFieldsTooLarge, "Response type mismatched"); + testStatusCodeResponse(requestHeaderFieldsTooLargeResponse, 431, "Request header fields too large", "Request header fields too large error"); + + http:UnavailableDueToLegalReasons unavailableDueToLegalReasonsResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 451); + test:assertTrue(unavailableDueToLegalReasonsResponse is http:UnavailableDueToLegalReasons, "Response type mismatched"); + testStatusCodeResponse(unavailableDueToLegalReasonsResponse, 451, "Unavailable for legal reasons", "Unavailable for legal reasons error"); +} + +@test:Config {} +function testSCResBindingWith5XXStatusCodes() returns error? { + http:InternalServerError internalServerErrorResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 500); + test:assertTrue(internalServerErrorResponse is http:InternalServerError, "Response type mismatched"); + testStatusCodeResponse(internalServerErrorResponse, 500, "Internal server error", "Internal server error"); + + http:NotImplemented notImplementedResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 501); + test:assertTrue(notImplementedResponse is http:NotImplemented, "Response type mismatched"); + testStatusCodeResponse(notImplementedResponse, 501, "Not implemented", "Not implemented error"); + + http:BadGateway badGatewayResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 502); + test:assertTrue(badGatewayResponse is http:BadGateway, "Response type mismatched"); + testStatusCodeResponse(badGatewayResponse, 502, "Bad gateway", "Bad gateway error"); + + http:ServiceUnavailable serviceUnavailableResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 503); + test:assertTrue(serviceUnavailableResponse is http:ServiceUnavailable, "Response type mismatched"); + testStatusCodeResponse(serviceUnavailableResponse, 503, "Service unavailable", "Service unavailable error"); + + http:GatewayTimeout gatewayTimeoutResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 504); + test:assertTrue(gatewayTimeoutResponse is http:GatewayTimeout, "Response type mismatched"); + testStatusCodeResponse(gatewayTimeoutResponse, 504, "Gateway timeout", "Gateway timeout error"); + + http:HttpVersionNotSupported httpVersionNotSupportedResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 505); + test:assertTrue(httpVersionNotSupportedResponse is http:HttpVersionNotSupported, "Response type mismatched"); + testStatusCodeResponse(httpVersionNotSupportedResponse, 505, "HTTP version not supported", "HTTP version not supported error"); + + http:VariantAlsoNegotiates variantAlsoNegotiatesResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 506); + test:assertTrue(variantAlsoNegotiatesResponse is http:VariantAlsoNegotiates, "Response type mismatched"); + testStatusCodeResponse(variantAlsoNegotiatesResponse, 506, "Variant also negotiates", "Variant also negotiates error"); + + http:InsufficientStorage insufficientStorageResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 507); + test:assertTrue(insufficientStorageResponse is http:InsufficientStorage, "Response type mismatched"); + testStatusCodeResponse(insufficientStorageResponse, 507, "Insufficient storage", "Insufficient storage error"); + + http:LoopDetected loopDetectedResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 508); + test:assertTrue(loopDetectedResponse is http:LoopDetected, "Response type mismatched"); + testStatusCodeResponse(loopDetectedResponse, 508, "Loop detected", "Loop detected error"); + + http:NotExtended notExtendedResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 510); + test:assertTrue(notExtendedResponse is http:NotExtended, "Response type mismatched"); + testStatusCodeResponse(notExtendedResponse, 510, "Not extended", "Not extended error"); + + http:NetworkAuthenticationRequired networkAuthenticationRequiredResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 511); + test:assertTrue(networkAuthenticationRequiredResponse is http:NetworkAuthenticationRequired, "Response type mismatched"); + testStatusCodeResponse(networkAuthenticationRequiredResponse, 511, "Network authentication required", "Network authentication required error"); + + http:BadRequest badRequestWithUnknownStatusCodeResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 999); + test:assertTrue(badRequestWithUnknownStatusCodeResponse is http:BadRequest, "Response type mismatched"); + testStatusCodeResponse(badRequestWithUnknownStatusCodeResponse, 400, "Unknown status code", "Bad request with unknown status code"); +} + +@test:Config {} +function testSCResBindingWithCommonType() returns error? { + http:StatusCodeResponse statusCodeResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 103); + test:assertTrue(statusCodeResponse is http:EarlyHints, "Response type mismatched"); + testStatusCodeResponse(statusCodeResponse, 103, "Early hints", "Early hints response"); + + statusCodeResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 201); + test:assertTrue(statusCodeResponse is http:Created, "Response type mismatched"); + testStatusCodeResponse(statusCodeResponse, 201, "Created", "Created response"); + + statusCodeResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 204); + test:assertTrue(statusCodeResponse is http:NoContent, "Response type mismatched"); + testStatusCodeResponse(statusCodeResponse, 204, "No content"); + + statusCodeResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 304); + test:assertTrue(statusCodeResponse is http:NotModified, "Response type mismatched"); + testStatusCodeResponse(statusCodeResponse, 304, "Not modified", "Not modified response"); + + statusCodeResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 405); + test:assertTrue(statusCodeResponse is http:MethodNotAllowed, "Response type mismatched"); + testStatusCodeResponse(statusCodeResponse, 405, "Method not allowed", "Method not allowed error"); + + statusCodeResponse = check statusCodeBindingClient1->/api/status\-code\-response(code = 504); + test:assertTrue(statusCodeResponse is http:GatewayTimeout, "Response type mismatched"); + testStatusCodeResponse(statusCodeResponse, 504, "Gateway timeout", "Gateway timeout error"); +} + +function testStatusCodeResponse(http:StatusCodeResponse statusCodeResponse, int statusCode, string header, string? body = ()) { + test:assertEquals(statusCodeResponse.status.code, statusCode, "Status code mismatched"); + test:assertEquals(statusCodeResponse.headers["x-error"], header, "Header mismatched"); + if body is string { + test:assertEquals(statusCodeResponse?.body, {msg: body}, "Response body mismatched"); + test:assertEquals(statusCodeResponse?.mediaType, "application/org+json", "Media type mismatched"); + } +} 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 706826a252..e2d261c743 100644 --- a/ballerina-tests/http-client-tests/tests/test_service_ports.bal +++ b/ballerina-tests/http-client-tests/tests/test_service_ports.bal @@ -37,3 +37,6 @@ const int http2ClientHostHeaderTestPort = 9605; const int httpClientHostHeaderTestPort = 9606; const int passthroughHostTestPort1 = 9607; const int passthroughHostTestPort2 = 9608; + +const int statusCodeBindingPort1 = 9609; +const int statusCodeBindingPort2 = 9610; diff --git a/ballerina-tests/http-dispatching-tests/Ballerina.toml b/ballerina-tests/http-dispatching-tests/Ballerina.toml index 14d64f0d70..fd0e652876 100644 --- a/ballerina-tests/http-dispatching-tests/Ballerina.toml +++ b/ballerina-tests/http-dispatching-tests/Ballerina.toml @@ -1,17 +1,17 @@ [package] org = "ballerina" name = "http_dispatching_tests" -version = "2.10.13" +version = "2.11.0" [[dependency]] org = "ballerina" name = "http_test_common" repository = "local" -version = "2.10.13" +version = "2.11.0" [platform.java17] graalvmCompatible = true [[platform.java17.dependency]] scope = "testOnly" -path = "../../test-utils/build/libs/http-test-utils-2.10.13-SNAPSHOT.jar" +path = "../../test-utils/build/libs/http-test-utils-2.11.0-SNAPSHOT.jar" diff --git a/ballerina-tests/http-dispatching-tests/Dependencies.toml b/ballerina-tests/http-dispatching-tests/Dependencies.toml index fd72573961..8723bda48a 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.8.0" +distribution-version = "2201.9.0-20240404-200900-0a7e4c9e" [[package]] org = "ballerina" @@ -35,7 +35,7 @@ dependencies = [ [[package]] org = "ballerina" name = "constraint" -version = "1.4.0" +version = "1.5.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"} @@ -47,7 +47,7 @@ modules = [ [[package]] org = "ballerina" name = "crypto" -version = "2.5.0" +version = "2.6.2" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"}, @@ -69,7 +69,7 @@ dependencies = [ [[package]] org = "ballerina" name = "http" -version = "2.10.13" +version = "2.11.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "auth"}, @@ -102,7 +102,7 @@ modules = [ [[package]] org = "ballerina" name = "http_dispatching_tests" -version = "2.10.13" +version = "2.11.0" dependencies = [ {org = "ballerina", name = "constraint"}, {org = "ballerina", name = "http"}, @@ -124,7 +124,7 @@ modules = [ [[package]] org = "ballerina" name = "http_test_common" -version = "2.10.13" +version = "2.11.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "lang.string"}, @@ -381,6 +381,7 @@ version = "0.0.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.array"}, {org = "ballerina", name = "lang.error"} ] modules = [ diff --git a/ballerina-tests/http-interceptor-tests/Ballerina.toml b/ballerina-tests/http-interceptor-tests/Ballerina.toml index e06cc6b545..046773fd38 100644 --- a/ballerina-tests/http-interceptor-tests/Ballerina.toml +++ b/ballerina-tests/http-interceptor-tests/Ballerina.toml @@ -1,17 +1,17 @@ [package] org = "ballerina" name = "http_interceptor_tests" -version = "2.10.13" +version = "2.11.0" [[dependency]] org = "ballerina" name = "http_test_common" repository = "local" -version = "2.10.13" +version = "2.11.0" [platform.java17] graalvmCompatible = true [[platform.java17.dependency]] scope = "testOnly" -path = "../../test-utils/build/libs/http-test-utils-2.10.13-SNAPSHOT.jar" +path = "../../test-utils/build/libs/http-test-utils-2.11.0-SNAPSHOT.jar" diff --git a/ballerina-tests/http-interceptor-tests/Dependencies.toml b/ballerina-tests/http-interceptor-tests/Dependencies.toml index f72b10f0ff..f325e48490 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.8.0" +distribution-version = "2201.9.0-20240404-200900-0a7e4c9e" [[package]] org = "ballerina" @@ -35,7 +35,7 @@ dependencies = [ [[package]] org = "ballerina" name = "constraint" -version = "1.4.0" +version = "1.5.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"} @@ -44,7 +44,7 @@ dependencies = [ [[package]] org = "ballerina" name = "crypto" -version = "2.5.0" +version = "2.6.2" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"}, @@ -66,7 +66,7 @@ dependencies = [ [[package]] org = "ballerina" name = "http" -version = "2.10.13" +version = "2.11.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "auth"}, @@ -99,7 +99,7 @@ modules = [ [[package]] org = "ballerina" name = "http_interceptor_tests" -version = "2.10.13" +version = "2.11.0" dependencies = [ {org = "ballerina", name = "http"}, {org = "ballerina", name = "http_test_common"}, @@ -115,7 +115,7 @@ modules = [ [[package]] org = "ballerina" name = "http_test_common" -version = "2.10.13" +version = "2.11.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "lang.string"}, @@ -333,6 +333,7 @@ version = "0.0.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.array"}, {org = "ballerina", name = "lang.error"} ] modules = [ diff --git a/ballerina-tests/http-misc-tests/Ballerina.toml b/ballerina-tests/http-misc-tests/Ballerina.toml index be9c89cba1..415cb314c4 100644 --- a/ballerina-tests/http-misc-tests/Ballerina.toml +++ b/ballerina-tests/http-misc-tests/Ballerina.toml @@ -1,17 +1,17 @@ [package] org = "ballerina" name = "http_misc_tests" -version = "2.10.13" +version = "2.11.0" [[dependency]] org = "ballerina" name = "http_test_common" repository = "local" -version = "2.10.13" +version = "2.11.0" [platform.java17] graalvmCompatible = true [[platform.java17.dependency]] scope = "testOnly" -path = "../../test-utils/build/libs/http-test-utils-2.10.13-SNAPSHOT.jar" +path = "../../test-utils/build/libs/http-test-utils-2.11.0-SNAPSHOT.jar" diff --git a/ballerina-tests/http-misc-tests/Dependencies.toml b/ballerina-tests/http-misc-tests/Dependencies.toml index 6df15e7b92..9ab6c4119b 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.8.0" +distribution-version = "2201.9.0-20240404-200900-0a7e4c9e" [[package]] org = "ballerina" @@ -44,7 +44,7 @@ dependencies = [ [[package]] org = "ballerina" name = "crypto" -version = "2.5.0" +version = "2.6.2" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"}, @@ -66,7 +66,7 @@ dependencies = [ [[package]] org = "ballerina" name = "http" -version = "2.10.13" +version = "2.11.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "auth"}, @@ -99,7 +99,7 @@ modules = [ [[package]] org = "ballerina" name = "http_misc_tests" -version = "2.10.13" +version = "2.11.0" dependencies = [ {org = "ballerina", name = "http"}, {org = "ballerina", name = "http_test_common"}, @@ -118,7 +118,7 @@ modules = [ [[package]] org = "ballerina" name = "http_test_common" -version = "2.10.13" +version = "2.11.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "lang.string"}, @@ -342,6 +342,7 @@ version = "0.0.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.array"}, {org = "ballerina", name = "lang.error"} ] modules = [ diff --git a/ballerina-tests/http-resiliency-tests/Ballerina.toml b/ballerina-tests/http-resiliency-tests/Ballerina.toml index 8efb9afbe5..3f12777866 100644 --- a/ballerina-tests/http-resiliency-tests/Ballerina.toml +++ b/ballerina-tests/http-resiliency-tests/Ballerina.toml @@ -1,17 +1,17 @@ [package] org = "ballerina" name = "http_resiliency_tests" -version = "2.10.13" +version = "2.11.0" [[dependency]] org = "ballerina" name = "http_test_common" repository = "local" -version = "2.10.13" +version = "2.11.0" [platform.java17] graalvmCompatible = true [[platform.java17.dependency]] scope = "testOnly" -path = "../../test-utils/build/libs/http-test-utils-2.10.13-SNAPSHOT.jar" +path = "../../test-utils/build/libs/http-test-utils-2.11.0-SNAPSHOT.jar" diff --git a/ballerina-tests/http-resiliency-tests/Dependencies.toml b/ballerina-tests/http-resiliency-tests/Dependencies.toml index fb3738df82..7338899a16 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.8.0" +distribution-version = "2201.9.0-20240404-200900-0a7e4c9e" [[package]] org = "ballerina" @@ -35,7 +35,7 @@ dependencies = [ [[package]] org = "ballerina" name = "constraint" -version = "1.4.0" +version = "1.5.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"} @@ -44,7 +44,7 @@ dependencies = [ [[package]] org = "ballerina" name = "crypto" -version = "2.5.0" +version = "2.6.2" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"}, @@ -66,7 +66,7 @@ dependencies = [ [[package]] org = "ballerina" name = "http" -version = "2.10.13" +version = "2.11.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "auth"}, @@ -99,7 +99,7 @@ modules = [ [[package]] org = "ballerina" name = "http_resiliency_tests" -version = "2.10.13" +version = "2.11.0" dependencies = [ {org = "ballerina", name = "http"}, {org = "ballerina", name = "http_test_common"}, @@ -116,7 +116,7 @@ modules = [ [[package]] org = "ballerina" name = "http_test_common" -version = "2.10.13" +version = "2.11.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "lang.string"}, @@ -337,6 +337,7 @@ version = "0.0.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.array"}, {org = "ballerina", name = "lang.error"} ] modules = [ diff --git a/ballerina-tests/http-security-tests/Ballerina.toml b/ballerina-tests/http-security-tests/Ballerina.toml index b5bdd5f6af..3f2b2243ca 100644 --- a/ballerina-tests/http-security-tests/Ballerina.toml +++ b/ballerina-tests/http-security-tests/Ballerina.toml @@ -1,17 +1,17 @@ [package] org = "ballerina" name = "http_security_tests" -version = "2.10.13" +version = "2.11.0" [[dependency]] org = "ballerina" name = "http_test_common" repository = "local" -version = "2.10.13" +version = "2.11.0" [platform.java17] graalvmCompatible = true [[platform.java17.dependency]] scope = "testOnly" -path = "../../test-utils/build/libs/http-test-utils-2.10.13-SNAPSHOT.jar" +path = "../../test-utils/build/libs/http-test-utils-2.11.0-SNAPSHOT.jar" diff --git a/ballerina-tests/http-security-tests/Dependencies.toml b/ballerina-tests/http-security-tests/Dependencies.toml index 21927f472e..42a0b64ae6 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.8.0" +distribution-version = "2201.9.0-20240404-200900-0a7e4c9e" [[package]] org = "ballerina" @@ -38,7 +38,7 @@ dependencies = [ [[package]] org = "ballerina" name = "constraint" -version = "1.4.0" +version = "1.5.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"} @@ -47,7 +47,7 @@ dependencies = [ [[package]] org = "ballerina" name = "crypto" -version = "2.5.0" +version = "2.6.2" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"}, @@ -69,7 +69,7 @@ dependencies = [ [[package]] org = "ballerina" name = "http" -version = "2.10.13" +version = "2.11.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "auth"}, @@ -102,7 +102,7 @@ modules = [ [[package]] org = "ballerina" name = "http_security_tests" -version = "2.10.13" +version = "2.11.0" dependencies = [ {org = "ballerina", name = "auth"}, {org = "ballerina", name = "http"}, @@ -120,7 +120,7 @@ modules = [ [[package]] org = "ballerina" name = "http_test_common" -version = "2.10.13" +version = "2.11.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "lang.string"}, @@ -338,6 +338,7 @@ version = "0.0.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.array"}, {org = "ballerina", name = "lang.error"} ] modules = [ diff --git a/ballerina-tests/http-service-tests/Ballerina.toml b/ballerina-tests/http-service-tests/Ballerina.toml index 1b0c1bd7bd..9ec9cd7ddd 100644 --- a/ballerina-tests/http-service-tests/Ballerina.toml +++ b/ballerina-tests/http-service-tests/Ballerina.toml @@ -1,17 +1,17 @@ [package] org = "ballerina" name = "http_service_tests" -version = "2.10.13" +version = "2.11.0" [[dependency]] org = "ballerina" name = "http_test_common" repository = "local" -version = "2.10.13" +version = "2.11.0" [platform.java17] graalvmCompatible = true [[platform.java17.dependency]] scope = "testOnly" -path = "../../test-utils/build/libs/http-test-utils-2.10.13-SNAPSHOT.jar" +path = "../../test-utils/build/libs/http-test-utils-2.11.0-SNAPSHOT.jar" diff --git a/ballerina-tests/http-service-tests/Dependencies.toml b/ballerina-tests/http-service-tests/Dependencies.toml index 5047ee4403..d13b98f56a 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.8.0" +distribution-version = "2201.9.0-20240404-200900-0a7e4c9e" [[package]] org = "ballerina" @@ -35,7 +35,7 @@ dependencies = [ [[package]] org = "ballerina" name = "constraint" -version = "1.4.0" +version = "1.5.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"} @@ -44,7 +44,7 @@ dependencies = [ [[package]] org = "ballerina" name = "crypto" -version = "2.5.0" +version = "2.6.2" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"}, @@ -69,7 +69,7 @@ modules = [ [[package]] org = "ballerina" name = "http" -version = "2.10.13" +version = "2.11.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "auth"}, @@ -102,7 +102,7 @@ modules = [ [[package]] org = "ballerina" name = "http_service_tests" -version = "2.10.13" +version = "2.11.0" dependencies = [ {org = "ballerina", name = "file"}, {org = "ballerina", name = "http"}, @@ -121,7 +121,7 @@ modules = [ [[package]] org = "ballerina" name = "http_test_common" -version = "2.10.13" +version = "2.11.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "lang.string"}, @@ -342,6 +342,7 @@ version = "0.0.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.array"}, {org = "ballerina", name = "lang.error"} ] modules = [ diff --git a/ballerina-tests/http-test-common/Ballerina.toml b/ballerina-tests/http-test-common/Ballerina.toml index 259ae645ae..e7b61366b7 100644 --- a/ballerina-tests/http-test-common/Ballerina.toml +++ b/ballerina-tests/http-test-common/Ballerina.toml @@ -1,4 +1,4 @@ [package] org = "ballerina" name = "http_test_common" -version = "2.10.13" +version = "2.11.0" diff --git a/ballerina-tests/http-test-common/Dependencies.toml b/ballerina-tests/http-test-common/Dependencies.toml index 3dbce05f84..158038c182 100644 --- a/ballerina-tests/http-test-common/Dependencies.toml +++ b/ballerina-tests/http-test-common/Dependencies.toml @@ -5,12 +5,12 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.8.0" +distribution-version = "2201.9.0-20240404-200900-0a7e4c9e" [[package]] org = "ballerina" name = "http_test_common" -version = "2.10.13" +version = "2.11.0" dependencies = [ {org = "ballerina", name = "lang.string"}, {org = "ballerina", name = "mime"}, @@ -45,6 +45,15 @@ dependencies = [ {org = "ballerina", name = "lang.object"} ] +[[package]] +org = "ballerina" +name = "lang.array" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.__internal"} +] + [[package]] org = "ballerina" name = "lang.error" @@ -115,6 +124,7 @@ name = "test" version = "0.0.0" dependencies = [ {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.array"}, {org = "ballerina", name = "lang.error"} ] modules = [ diff --git a/ballerina-tests/http2-tests/Ballerina.toml b/ballerina-tests/http2-tests/Ballerina.toml index 23fcde7ac1..f4c4980194 100644 --- a/ballerina-tests/http2-tests/Ballerina.toml +++ b/ballerina-tests/http2-tests/Ballerina.toml @@ -1,17 +1,17 @@ [package] org = "ballerina" name = "http2_tests" -version = "2.10.13" +version = "2.11.0" [[dependency]] org = "ballerina" name = "http_test_common" repository = "local" -version = "2.10.13" +version = "2.11.0" [platform.java17] graalvmCompatible = true [[platform.java17.dependency]] scope = "testOnly" -path = "../../test-utils/build/libs/http-test-utils-2.10.13-SNAPSHOT.jar" +path = "../../test-utils/build/libs/http-test-utils-2.11.0-SNAPSHOT.jar" diff --git a/ballerina-tests/http2-tests/Dependencies.toml b/ballerina-tests/http2-tests/Dependencies.toml index 752f811de1..d08b774e77 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.8.0" +distribution-version = "2201.9.0-20240404-200900-0a7e4c9e" [[package]] org = "ballerina" @@ -35,7 +35,7 @@ dependencies = [ [[package]] org = "ballerina" name = "constraint" -version = "1.4.0" +version = "1.5.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"} @@ -44,7 +44,7 @@ dependencies = [ [[package]] org = "ballerina" name = "crypto" -version = "2.5.0" +version = "2.6.2" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"}, @@ -69,7 +69,7 @@ modules = [ [[package]] org = "ballerina" name = "http" -version = "2.10.13" +version = "2.11.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "auth"}, @@ -102,7 +102,7 @@ modules = [ [[package]] org = "ballerina" name = "http2_tests" -version = "2.10.13" +version = "2.11.0" dependencies = [ {org = "ballerina", name = "file"}, {org = "ballerina", name = "http"}, @@ -121,7 +121,7 @@ modules = [ [[package]] org = "ballerina" name = "http_test_common" -version = "2.10.13" +version = "2.11.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "lang.string"}, @@ -342,6 +342,7 @@ version = "0.0.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.array"}, {org = "ballerina", name = "lang.error"} ] modules = [ diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index 761542514d..f05f3cb816 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -1,13 +1,13 @@ [package] org = "ballerina" name = "http" -version = "2.10.13" +version = "2.11.0" authors = ["Ballerina"] keywords = ["http", "network", "service", "listener", "client"] repository = "https://github.com/ballerina-platform/module-ballerina-http" icon = "icon.png" license = ["Apache-2.0"] -distribution = "2201.8.0" +distribution = "2201.9.0" export = ["http", "http.httpscerr"] [platform.java17] @@ -16,8 +16,8 @@ graalvmCompatible = true [[platform.java17.dependency]] groupId = "io.ballerina.stdlib" artifactId = "http-native" -version = "2.10.13" -path = "../native/build/libs/http-native-2.10.13-SNAPSHOT.jar" +version = "2.11.0" +path = "../native/build/libs/http-native-2.11.0-SNAPSHOT.jar" [[platform.java17.dependency]] groupId = "io.ballerina.stdlib" diff --git a/ballerina/CompilerPlugin.toml b/ballerina/CompilerPlugin.toml index ec52292d2b..763ca3e699 100644 --- a/ballerina/CompilerPlugin.toml +++ b/ballerina/CompilerPlugin.toml @@ -3,4 +3,4 @@ id = "http-compiler-plugin" class = "io.ballerina.stdlib.http.compiler.HttpCompilerPlugin" [[dependency]] -path = "../compiler-plugin/build/libs/http-compiler-plugin-2.10.13-SNAPSHOT.jar" +path = "../compiler-plugin/build/libs/http-compiler-plugin-2.11.0-SNAPSHOT.jar" diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index 4ef26fb408..be615f0816 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -5,7 +5,7 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.8.0" +distribution-version = "2201.9.0-20240404-200900-0a7e4c9e" [[package]] org = "ballerina" @@ -39,7 +39,7 @@ modules = [ [[package]] org = "ballerina" name = "constraint" -version = "1.4.0" +version = "1.5.0" dependencies = [ {org = "ballerina", name = "jballerina.java"} ] @@ -50,7 +50,7 @@ modules = [ [[package]] org = "ballerina" name = "crypto" -version = "2.5.0" +version = "2.6.2" dependencies = [ {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "time"} @@ -76,7 +76,7 @@ modules = [ [[package]] org = "ballerina" name = "http" -version = "2.10.13" +version = "2.11.0" dependencies = [ {org = "ballerina", name = "auth"}, {org = "ballerina", name = "cache"}, diff --git a/ballerina/http_client_endpoint.bal b/ballerina/http_client_endpoint.bal index 9e0ca8e123..8edb594675 100644 --- a/ballerina/http_client_endpoint.bal +++ b/ballerina/http_client_endpoint.bal @@ -88,7 +88,7 @@ public client isolated class Client { } external; private isolated function processPost(string path, RequestMessage message, TargetType targetType, - string? mediaType, map? headers) returns Response|anydata|ClientError { + string? mediaType, map? headers) returns Response|StatusCodeResponse|anydata|ClientError { Request req = check buildRequest(message, mediaType); populateOptions(req, mediaType, headers); Response|ClientError response = self.httpClient->post(path, req); @@ -130,7 +130,7 @@ public client isolated class Client { } external; private isolated function processPut(string path, RequestMessage message, TargetType targetType, - string? mediaType, map? headers) returns Response|anydata|ClientError { + string? mediaType, map? headers) returns Response|StatusCodeResponse|anydata|ClientError { Request req = check buildRequest(message, mediaType); populateOptions(req, mediaType, headers); Response|ClientError response = self.httpClient->put(path, req); @@ -172,7 +172,7 @@ public client isolated class Client { } external; private isolated function processPatch(string path, RequestMessage message, TargetType targetType, - string? mediaType, map? headers) returns Response|anydata|ClientError { + string? mediaType, map? headers) returns Response|StatusCodeResponse|anydata|ClientError { Request req = check buildRequest(message, mediaType); populateOptions(req, mediaType, headers); Response|ClientError response = self.httpClient->patch(path, req); @@ -214,7 +214,7 @@ public client isolated class Client { } external; private isolated function processDelete(string path, RequestMessage message, TargetType targetType, - string? mediaType, map? headers) returns Response|anydata|ClientError { + string? mediaType, map? headers) returns Response|StatusCodeResponse|anydata|ClientError { Request req = check buildRequest(message, mediaType); populateOptions(req, mediaType, headers); Response|ClientError response = self.httpClient->delete(path, req); @@ -277,7 +277,7 @@ public client isolated class Client { } external; private isolated function processGet(string path, map? headers, TargetType targetType) - returns Response|anydata|ClientError { + returns Response|StatusCodeResponse|anydata|ClientError { Request req = buildRequestWithHeaders(headers); Response|ClientError response = self.httpClient->get(path, message = req); if observabilityEnabled && response is Response { @@ -313,7 +313,7 @@ public client isolated class Client { } external; private isolated function processOptions(string path, map? headers, TargetType targetType) - returns Response|anydata|ClientError { + returns Response|StatusCodeResponse|anydata|ClientError { Request req = buildRequestWithHeaders(headers); Response|ClientError response = self.httpClient->options(path, message = req); if observabilityEnabled && response is Response { @@ -340,7 +340,7 @@ public client isolated class Client { private isolated function processExecute(string httpVerb, string path, RequestMessage message, TargetType targetType, string? mediaType, map? headers) - returns Response|anydata|ClientError { + returns Response|StatusCodeResponse|anydata|ClientError { Request req = check buildRequest(message, mediaType); populateOptions(req, mediaType, headers); Response|ClientError response = self.httpClient->execute(httpVerb, path, req); @@ -363,7 +363,7 @@ public client isolated class Client { } external; private isolated function processForward(string path, Request request, TargetType targetType) - returns Response|anydata|ClientError { + returns Response|StatusCodeResponse|anydata|ClientError { Response|ClientError response = self.httpClient->forward(path, request); if observabilityEnabled && response is Response { addObservabilityInformation(path, request.method, response.statusCode, self.url); @@ -620,37 +620,6 @@ isolated function createDefaultClient(string url, ClientConfiguration configurat return createHttpSecureClient(url, configuration); } -isolated function processResponse(Response|ClientError response, TargetType targetType, boolean requireValidation) - returns Response|anydata|ClientError { - if targetType is typedesc || response is ClientError { - return response; - } - int statusCode = response.statusCode; - if 400 <= statusCode && statusCode <= 599 { - string reasonPhrase = response.reasonPhrase; - map headers = getHeaders(response); - anydata|error payload = getPayload(response); - if payload is error { - if payload is NoContentError { - return createResponseError(statusCode, reasonPhrase, headers); - } - return error PayloadBindingClientError("http:ApplicationResponseError creation failed: " + statusCode.toString() + - " response payload extraction failed", payload); - } else { - return createResponseError(statusCode, reasonPhrase, headers, payload); - } - } - if targetType is typedesc { - anydata payload = check performDataBinding(response, targetType); - if requireValidation { - return performDataValidation(payload, targetType); - } - return payload; - } else { - panic error GenericClientError("invalid payload target type"); - } -} - isolated function getPayload(Response response) returns anydata|error { string|error contentTypeValue = response.getHeader(CONTENT_TYPE); string value = ""; @@ -707,3 +676,17 @@ isolated function createResponseError(int statusCode, string reasonPhrase, map targetType, boolean requireValidation) returns anydata|ClientError { + anydata payload = check performDataBinding(self, targetType); + if requireValidation { + return performDataValidation(payload, targetType); + } + return payload; + } + + isolated function getApplicationResponseError() returns ClientError { + string reasonPhrase = self.reasonPhrase; + map headers = getHeaders(self); + anydata|error payload = getPayload(self); + int statusCode = self.statusCode; + if payload is error { + if payload is NoContentError { + return createResponseError(statusCode, reasonPhrase, headers); + } + return error PayloadBindingClientError("http:ApplicationResponseError creation failed: " + statusCode.toString() + + " response payload extraction failed", payload); + } else { + return createResponseError(statusCode, reasonPhrase, headers, payload); + } + } } isolated function externCreateNewResEntity(Response response) returns mime:Entity = diff --git a/ballerina/http_types.bal b/ballerina/http_types.bal index f4d95bab04..27f53c5a41 100644 --- a/ballerina/http_types.bal +++ b/ballerina/http_types.bal @@ -29,7 +29,7 @@ public type Service distinct service object { }; # The types of data values that are expected by the HTTP `client` to return after the data binding operation. -public type TargetType typedesc; +public type TargetType typedesc; # Defines the HTTP operations related to circuit breaker, failover and load balancer. # diff --git a/ballerina/resiliency_failover_client.bal b/ballerina/resiliency_failover_client.bal index 76cb7283e9..c6692cf468 100644 --- a/ballerina/resiliency_failover_client.bal +++ b/ballerina/resiliency_failover_client.bal @@ -99,13 +99,13 @@ public client isolated class FailoverClient { } external; private isolated function processPost(string path, RequestMessage message, TargetType targetType, - string? mediaType, map? headers) returns Response|anydata|ClientError { + string? mediaType, map? headers) returns Response|StatusCodeResponse|anydata|ClientError { Request req = check buildRequest(message, mediaType); populateOptions(req, mediaType, headers); var result = self.performFailoverAction(path, req, HTTP_POST); if result is HttpFuture { return getInvalidTypeError(); - } else if result is Response || result is ClientError { + } else if result is ClientError|Response { return processResponse(result, targetType, self.requireValidation); } else { panic error ClientError("invalid response type received"); @@ -144,13 +144,13 @@ public client isolated class FailoverClient { } external; private isolated function processPut(string path, RequestMessage message, TargetType targetType, - string? mediaType, map? headers) returns Response|anydata|ClientError { + string? mediaType, map? headers) returns Response|StatusCodeResponse|anydata|ClientError { Request req = check buildRequest(message, mediaType); populateOptions(req, mediaType, headers); var result = self.performFailoverAction(path, req, HTTP_PUT); if result is HttpFuture { return getInvalidTypeError(); - } else if result is Response || result is ClientError { + } else if result is ClientError|Response { return processResponse(result, targetType, self.requireValidation); } else { panic error ClientError("invalid response type received"); @@ -189,13 +189,13 @@ public client isolated class FailoverClient { } external; private isolated function processPatch(string path, RequestMessage message, TargetType targetType, - string? mediaType, map? headers) returns Response|anydata|ClientError { + string? mediaType, map? headers) returns Response|StatusCodeResponse|anydata|ClientError { Request req = check buildRequest(message, mediaType); populateOptions(req, mediaType, headers); var result = self.performFailoverAction(path, req, HTTP_PATCH); if result is HttpFuture { return getInvalidTypeError(); - } else if result is Response || result is ClientError { + } else if result is ClientError|Response { return processResponse(result, targetType, self.requireValidation); } else { panic error ClientError("invalid response type received"); @@ -234,13 +234,13 @@ public client isolated class FailoverClient { } external; private isolated function processDelete(string path, RequestMessage message, TargetType targetType, - string? mediaType, map? headers) returns Response|anydata|ClientError { + string? mediaType, map? headers) returns Response|StatusCodeResponse|anydata|ClientError { Request req = check buildRequest(message, mediaType); populateOptions(req, mediaType, headers); var result = self.performFailoverAction(path, req, HTTP_DELETE); if result is HttpFuture { return getInvalidTypeError(); - } else if result is Response || result is ClientError { + } else if result is ClientError|Response { return processResponse(result, targetType, self.requireValidation); } else { panic error ClientError("invalid response type received"); @@ -303,12 +303,12 @@ public client isolated class FailoverClient { } external; private isolated function processGet(string path, map? headers, TargetType targetType) - returns Response|anydata|ClientError { + returns Response|StatusCodeResponse|anydata|ClientError { Request req = buildRequestWithHeaders(headers); var result = self.performFailoverAction(path, req, HTTP_GET); if result is HttpFuture { return getInvalidTypeError(); - } else if result is Response || result is ClientError { + } else if result is ClientError|Response { return processResponse(result, targetType, self.requireValidation); } else { panic error ClientError("invalid response type received"); @@ -342,12 +342,12 @@ public client isolated class FailoverClient { } external; private isolated function processOptions(string path, map? headers, TargetType targetType) - returns Response|anydata|ClientError { + returns Response|StatusCodeResponse|anydata|ClientError { Request req = buildRequestWithHeaders(headers); var result = self.performFailoverAction(path, req, HTTP_OPTIONS); if result is HttpFuture { return getInvalidTypeError(); - } else if result is Response || result is ClientError { + } else if result is ClientError|Response { return processResponse(result, targetType, self.requireValidation); } else { panic error ClientError("invalid response type received"); @@ -372,13 +372,13 @@ public client isolated class FailoverClient { private isolated function processExecute(string httpVerb, string path, RequestMessage message, TargetType targetType, string? mediaType, map? headers) - returns Response|anydata|ClientError { + returns Response|StatusCodeResponse|anydata|ClientError { Request req = check buildRequest(message, mediaType); populateOptions(req, mediaType, headers); var result = self.performExecuteAction(path, req, httpVerb); if result is HttpFuture { return getInvalidTypeError(); - } else if result is Response || result is ClientError { + } else if result is ClientError|Response { return processResponse(result, targetType, self.requireValidation); } else { panic error ClientError("invalid response type received"); @@ -398,11 +398,11 @@ public client isolated class FailoverClient { } external; private isolated function processForward(string path, Request request, TargetType targetType) - returns Response|anydata|ClientError { + returns Response|StatusCodeResponse|anydata|ClientError { var result = self.performFailoverAction(path, request, HTTP_FORWARD); if result is HttpFuture { return getInvalidTypeError(); - } else if result is Response || result is ClientError { + } else if result is ClientError|Response { return processResponse(result, targetType, self.requireValidation); } else { panic error ClientError("invalid response type received"); diff --git a/ballerina/resiliency_load_balance_client.bal b/ballerina/resiliency_load_balance_client.bal index 790c18ad4c..071eabde05 100644 --- a/ballerina/resiliency_load_balance_client.bal +++ b/ballerina/resiliency_load_balance_client.bal @@ -92,7 +92,7 @@ public client isolated class LoadBalanceClient { } external; private isolated function processPost(string path, RequestMessage message, TargetType targetType, - string? mediaType, map? headers) returns Response|anydata|ClientError { + string? mediaType, map? headers) returns Response|StatusCodeResponse|anydata|ClientError { Request req = check buildRequest(message, mediaType); populateOptions(req, mediaType, headers); var result = self.performLoadBalanceAction(path, req, HTTP_POST); @@ -129,9 +129,9 @@ public client isolated class LoadBalanceClient { returns targetType|ClientError = @java:Method { 'class: "io.ballerina.stdlib.http.api.client.actions.HttpClientAction" } external; - - private isolated function processPut(string path, RequestMessage message, TargetType targetType, - string? mediaType, map? headers) returns Response|anydata|ClientError { + + private isolated function processPut(string path, RequestMessage message, TargetType targetType, + string? mediaType, map? headers) returns Response|StatusCodeResponse|anydata|ClientError { Request req = check buildRequest(message, mediaType); populateOptions(req, mediaType, headers); var result = self.performLoadBalanceAction(path, req, HTTP_PUT); @@ -168,9 +168,9 @@ public client isolated class LoadBalanceClient { returns targetType|ClientError = @java:Method { 'class: "io.ballerina.stdlib.http.api.client.actions.HttpClientAction" } external; - - private isolated function processPatch(string path, RequestMessage message, TargetType targetType, - string? mediaType, map? headers) returns Response|anydata|ClientError { + + private isolated function processPatch(string path, RequestMessage message, TargetType targetType, + string? mediaType, map? headers) returns Response|StatusCodeResponse|anydata|ClientError { Request req = check buildRequest(message, mediaType); populateOptions(req, mediaType, headers); var result = self.performLoadBalanceAction(path, req, HTTP_PATCH); @@ -207,9 +207,9 @@ public client isolated class LoadBalanceClient { returns targetType|ClientError = @java:Method { 'class: "io.ballerina.stdlib.http.api.client.actions.HttpClientAction" } external; - - private isolated function processDelete(string path, RequestMessage message, TargetType targetType, - string? mediaType, map? headers) returns Response|anydata|ClientError { + + private isolated function processDelete(string path, RequestMessage message, TargetType targetType, + string? mediaType, map? headers) returns Response|StatusCodeResponse|anydata|ClientError { Request req = check buildRequest(message, mediaType); populateOptions(req, mediaType, headers); var result = self.performLoadBalanceAction(path, req, HTTP_DELETE); @@ -253,7 +253,7 @@ public client isolated class LoadBalanceClient { } external; # The GET remote function implementation of the LoadBalancer Connector. - # + # # + path - Request path # + headers - The entity headers # + targetType - HTTP response or `anydata`, which is expected to be returned after data binding @@ -263,9 +263,9 @@ public client isolated class LoadBalanceClient { returns targetType|ClientError = @java:Method { 'class: "io.ballerina.stdlib.http.api.client.actions.HttpClientAction" } external; - + private isolated function processGet(string path, map? headers, TargetType targetType) - returns Response|anydata|ClientError { + returns Response|StatusCodeResponse|anydata|ClientError { Request req = buildRequestWithHeaders(headers); var result = self.performLoadBalanceAction(path, req, HTTP_GET); return processResponse(result, targetType, self.requireValidation); @@ -296,9 +296,9 @@ public client isolated class LoadBalanceClient { returns targetType|ClientError = @java:Method { 'class: "io.ballerina.stdlib.http.api.client.actions.HttpClientAction" } external; - + private isolated function processOptions(string path, map? headers, TargetType targetType) - returns Response|anydata|ClientError { + returns Response|StatusCodeResponse|anydata|ClientError { Request req = buildRequestWithHeaders(headers); var result = self.performLoadBalanceAction(path, req, HTTP_OPTIONS); return processResponse(result, targetType, self.requireValidation); @@ -319,10 +319,10 @@ public client isolated class LoadBalanceClient { returns targetType|ClientError = @java:Method { 'class: "io.ballerina.stdlib.http.api.client.actions.HttpClientAction" } external; - + private isolated function processExecute(string httpVerb, string path, RequestMessage message, - TargetType targetType, string? mediaType, map? headers) - returns Response|anydata|ClientError { + TargetType targetType, string? mediaType, map? headers) + returns Response|StatusCodeResponse|anydata|ClientError { Request req = check buildRequest(message, mediaType); populateOptions(req, mediaType, headers); var result = self.performLoadBalanceExecuteAction(path, req, httpVerb); @@ -342,7 +342,7 @@ public client isolated class LoadBalanceClient { } external; private isolated function processForward(string path, Request request, TargetType targetType) - returns Response|anydata|ClientError { + returns Response|StatusCodeResponse|anydata|ClientError { var result = self.performLoadBalanceAction(path, request, HTTP_FORWARD); return processResponse(result, targetType, self.requireValidation); } diff --git a/build-config/resources/Ballerina.toml b/build-config/resources/Ballerina.toml index 845e650908..6732b94cc4 100644 --- a/build-config/resources/Ballerina.toml +++ b/build-config/resources/Ballerina.toml @@ -7,7 +7,7 @@ keywords = ["http", "network", "service", "listener", "client"] repository = "https://github.com/ballerina-platform/module-ballerina-http" icon = "icon.png" license = ["Apache-2.0"] -distribution = "2201.8.0" +distribution = "2201.9.0" export = ["http", "http.httpscerr"] [platform.java17] diff --git a/changelog.md b/changelog.md index e0ee8206fc..e3986ef987 100644 --- a/changelog.md +++ b/changelog.md @@ -7,6 +7,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Added + +- [Add status code response binding support for the HTTP client](https://github.com/ballerina-platform/ballerina-library/issues/6100) + ### Fixed - [Address CVE-2024-29025 netty's vulnerability](https://github.com/ballerina-platform/ballerina-library/issues/6242) diff --git a/gradle.properties b/gradle.properties index 7acbad74fe..3248021763 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ org.gradle.caching=true group=io.ballerina.stdlib -version=2.10.13-SNAPSHOT -ballerinaLangVersion=2201.8.0 +version=2.11.0-SNAPSHOT +ballerinaLangVersion=2201.9.0-20240404-200900-0a7e4c9e ballerinaTomlParserVersion=1.2.2 commonsLang3Version=3.12.0 nettyVersion=4.1.108.Final diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/HttpConstants.java b/native/src/main/java/io/ballerina/stdlib/http/api/HttpConstants.java index b31d83f132..84af585f01 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/HttpConstants.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/HttpConstants.java @@ -258,8 +258,6 @@ public final class HttpConstants { public static final String HTTP_ERROR_RECORD = "HTTPError"; public static final BString HTTP_ERROR_MESSAGE = StringUtils.fromString("message"); public static final BString HTTP_ERROR_STATUS_CODE = StringUtils.fromString("statusCode"); - public static final String HTTP_CLIENT_REQUEST_ERROR = "ClientRequestError"; - public static final String HTTP_REMOTE_SERVER_ERROR = "RemoteServerError"; // ServeConnector struct indices public static final BString HTTP_CONNECTOR_CONFIG_FIELD = StringUtils.fromString("config"); @@ -296,6 +294,10 @@ public final class HttpConstants { //StatusCodeResponse struct field names public static final String STATUS_CODE_RESPONSE_BODY_FIELD = "body"; public static final String STATUS_CODE_RESPONSE_STATUS_FIELD = "status"; + public static final String STATUS_CODE_RESPONSE_STATUS_CODE_FIELD = "code"; + public static final String STATUS_CODE = "statusCode"; + public static final String STATUS_CODE_RESPONSE_MEDIA_TYPE_FIELD = "mediaType"; + public static final String STATUS_CODE_RESPONSE_HEADERS_FIELD = "headers"; //PushPromise struct field names public static final BString PUSH_PROMISE_PATH_FIELD = StringUtils.fromString("path"); diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/HttpErrorType.java b/native/src/main/java/io/ballerina/stdlib/http/api/HttpErrorType.java index 3353093ab0..e04405e5dd 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/HttpErrorType.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/HttpErrorType.java @@ -63,9 +63,10 @@ public enum HttpErrorType { INVALID_CONTENT_LENGTH("InvalidContentLengthError"), HEADER_NOT_FOUND_ERROR("HeaderNotFoundError"), CLIENT_ERROR("ClientError"), + PAYLOAD_BINDING_CLIENT_ERROR("PayloadBindingClientError"), INTERNAL_PAYLOAD_BINDING_LISTENER_ERROR("InternalPayloadBindingListenerError"), INTERNAL_PAYLOAD_VALIDATION_LISTENER_ERROR("InternalPayloadValidationListenerError"), - INTERNAL_HEADER_BINDING_ERROR("InternalHeaderBindingError"), + INTERNAL_HEADER_BINDING_LISTENER_ERROR("InternalHeaderBindingListenerError"), INTERNAL_QUERY_PARAM_BINDING_ERROR("InternalQueryParameterBindingError"), INTERNAL_PATH_PARAM_BINDING_ERROR("InternalPathParameterBindingError"), INTERNAL_INTERCEPTOR_RETURN_ERROR("InternalInterceptorReturnError"), @@ -81,8 +82,14 @@ public enum HttpErrorType { INTERNAL_LISTENER_AUTHN_ERROR("InternalListenerAuthnError"), CLIENT_CONNECTOR_ERROR("ClientConnectorError"), INTERNAL_RESOURCE_PATH_VALIDATION_ERROR("InternalResourcePathValidationError"), - INTERNAL_HEADER_VALIDATION_ERROR("InternalHeaderValidationError"), - INTERNAL_QUERY_PARAM_VALIDATION_ERROR("InternalQueryParameterValidationError"); + INTERNAL_HEADER_VALIDATION_LISTENER_ERROR("InternalHeaderValidationListenerError"), + INTERNAL_QUERY_PARAM_VALIDATION_ERROR("InternalQueryParameterValidationError"), + STATUS_CODE_RECORD_BINDING_ERROR("StatusCodeRecordBindingError"), + HEADER_NOT_FOUND_CLIENT_ERROR("HeaderNotFoundClientError"), + HEADER_BINDING_CLIENT_ERROR("HeaderBindingClientError"), + HEADER_VALIDATION_CLIENT_ERROR("HeaderValidationClientError"), + MEDIA_TYPE_BINDING_CLIENT_ERROR("MediaTypeBindingClientError"), + MEDIA_TYPE_VALIDATION_CLIENT_ERROR("MediaTypeValidationClientError"); private final String errorName; diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/ValueCreatorUtils.java b/native/src/main/java/io/ballerina/stdlib/http/api/ValueCreatorUtils.java index f32ca11892..c706322f66 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/ValueCreatorUtils.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/ValueCreatorUtils.java @@ -101,6 +101,10 @@ public static BMap createHTTPRecordValue(String recordTypeName) return ValueCreator.createRecordValue(ModuleUtils.getHttpPackage(), recordTypeName); } + public static Object createStatusCodeObject(String statusCodeObjName) { + return createObjectValue(ModuleUtils.getHttpPackage(), statusCodeObjName); + } + /** * Method that creates a runtime object value using the given package id and object type name. * diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/nativeimpl/ExternResponseProcessor.java b/native/src/main/java/io/ballerina/stdlib/http/api/nativeimpl/ExternResponseProcessor.java new file mode 100644 index 0000000000..84b58154e2 --- /dev/null +++ b/native/src/main/java/io/ballerina/stdlib/http/api/nativeimpl/ExternResponseProcessor.java @@ -0,0 +1,580 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com). + * + * 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. + */ +package io.ballerina.stdlib.http.api.nativeimpl; + +import io.ballerina.runtime.api.Environment; +import io.ballerina.runtime.api.PredefinedTypes; +import io.ballerina.runtime.api.Runtime; +import io.ballerina.runtime.api.TypeTags; +import io.ballerina.runtime.api.async.Callback; +import io.ballerina.runtime.api.creators.ErrorCreator; +import io.ballerina.runtime.api.creators.TypeCreator; +import io.ballerina.runtime.api.creators.ValueCreator; +import io.ballerina.runtime.api.flags.SymbolFlags; +import io.ballerina.runtime.api.types.ArrayType; +import io.ballerina.runtime.api.types.Field; +import io.ballerina.runtime.api.types.MapType; +import io.ballerina.runtime.api.types.ObjectType; +import io.ballerina.runtime.api.types.RecordType; +import io.ballerina.runtime.api.types.ReferenceType; +import io.ballerina.runtime.api.types.Type; +import io.ballerina.runtime.api.types.UnionType; +import io.ballerina.runtime.api.utils.JsonUtils; +import io.ballerina.runtime.api.utils.StringUtils; +import io.ballerina.runtime.api.utils.TypeUtils; +import io.ballerina.runtime.api.utils.ValueUtils; +import io.ballerina.runtime.api.values.BArray; +import io.ballerina.runtime.api.values.BError; +import io.ballerina.runtime.api.values.BMap; +import io.ballerina.runtime.api.values.BObject; +import io.ballerina.runtime.api.values.BString; +import io.ballerina.runtime.api.values.BTypedesc; +import io.ballerina.stdlib.constraint.Constraints; +import io.ballerina.stdlib.http.api.HttpErrorType; +import io.ballerina.stdlib.http.api.HttpUtil; +import io.ballerina.stdlib.http.api.ValueCreatorUtils; +import io.netty.handler.codec.http.HttpHeaders; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CountDownLatch; + +import static io.ballerina.stdlib.http.api.HttpConstants.HTTP_HEADERS; +import static io.ballerina.stdlib.http.api.HttpConstants.STATUS_CODE; +import static io.ballerina.stdlib.http.api.HttpConstants.STATUS_CODE_RESPONSE_BODY_FIELD; +import static io.ballerina.stdlib.http.api.HttpConstants.STATUS_CODE_RESPONSE_HEADERS_FIELD; +import static io.ballerina.stdlib.http.api.HttpConstants.STATUS_CODE_RESPONSE_MEDIA_TYPE_FIELD; +import static io.ballerina.stdlib.http.api.HttpConstants.STATUS_CODE_RESPONSE_STATUS_CODE_FIELD; +import static io.ballerina.stdlib.http.api.HttpConstants.STATUS_CODE_RESPONSE_STATUS_FIELD; +import static io.ballerina.stdlib.http.api.HttpErrorType.CLIENT_ERROR; +import static io.ballerina.stdlib.http.api.HttpErrorType.HEADER_BINDING_CLIENT_ERROR; +import static io.ballerina.stdlib.http.api.HttpErrorType.HEADER_NOT_FOUND_CLIENT_ERROR; +import static io.ballerina.stdlib.http.api.HttpErrorType.HEADER_VALIDATION_CLIENT_ERROR; +import static io.ballerina.stdlib.http.api.HttpErrorType.MEDIA_TYPE_BINDING_CLIENT_ERROR; +import static io.ballerina.stdlib.http.api.HttpErrorType.MEDIA_TYPE_VALIDATION_CLIENT_ERROR; +import static io.ballerina.stdlib.http.api.HttpErrorType.PAYLOAD_BINDING_CLIENT_ERROR; +import static io.ballerina.stdlib.http.api.HttpErrorType.STATUS_CODE_RECORD_BINDING_ERROR; +import static io.ballerina.stdlib.http.api.HttpUtil.createHttpError; + +/** + * Extern response processor to process the response and generate the response with the given target type. + * + * @since 2.11.0 + */ +public final class ExternResponseProcessor { + + private static final String NO_HEADER_VALUE_ERROR_MSG = "no header value found for '%s'"; + private static final String HEADER_BINDING_FAILED_ERROR_MSG = "header binding failed for parameter: '%s'"; + private static final String HEADER_BINDING_FAILED = "header binding failed"; + private static final String UNSUPPORTED_HEADERS_TYPE = "unsupported headers type: %s"; + private static final String UNSUPPORTED_STATUS_CODE = "unsupported status code: %d"; + private static final String INCOMPATIBLE_TYPE_FOUND_FOR_RESPONSE = "incompatible %s found for response with %d"; + private static final String NO_ANYDATA_TYPE_FOUND_IN_THE_TARGET_TYPE = "no 'anydata' type found in the target type"; + private static final String PAYLOAD_BINDING_FAILED = "payload binding failed"; + private static final String MEDIA_TYPE_BINDING_FAILED = "media-type binding failed"; + private static final String APPLICATION_RES_ERROR_CREATION_FAILED = "http:ApplicationResponseError creation failed"; + + private static final String PERFORM_DATA_BINDING = "performDataBinding"; + private static final String GET_APPLICATION_RESPONSE_ERROR = "getApplicationResponseError"; + + + private static final Map STATUS_CODE_OBJS = new HashMap<>(); + + static { + STATUS_CODE_OBJS.put("100", "StatusContinue"); + STATUS_CODE_OBJS.put("101", "StatusSwitchingProtocols"); + STATUS_CODE_OBJS.put("102", "StatusProcessing"); + STATUS_CODE_OBJS.put("103", "StatusEarlyHints"); + STATUS_CODE_OBJS.put("200", "StatusOK"); + STATUS_CODE_OBJS.put("201", "StatusCreated"); + STATUS_CODE_OBJS.put("202", "StatusAccepted"); + STATUS_CODE_OBJS.put("203", "StatusNonAuthoritativeInformation"); + STATUS_CODE_OBJS.put("204", "StatusNoContent"); + STATUS_CODE_OBJS.put("205", "StatusResetContent"); + STATUS_CODE_OBJS.put("206", "StatusPartialContent"); + STATUS_CODE_OBJS.put("207", "StatusMultiStatus"); + STATUS_CODE_OBJS.put("208", "StatusAlreadyReported"); + STATUS_CODE_OBJS.put("226", "StatusIMUsed"); + STATUS_CODE_OBJS.put("300", "StatusMultipleChoices"); + STATUS_CODE_OBJS.put("301", "StatusMovedPermanently"); + STATUS_CODE_OBJS.put("302", "StatusFound"); + STATUS_CODE_OBJS.put("303", "StatusSeeOther"); + STATUS_CODE_OBJS.put("304", "StatusNotModified"); + STATUS_CODE_OBJS.put("305", "StatusUseProxy"); + STATUS_CODE_OBJS.put("307", "StatusTemporaryRedirect"); + STATUS_CODE_OBJS.put("308", "StatusPermanentRedirect"); + STATUS_CODE_OBJS.put("400", "StatusBadRequest"); + STATUS_CODE_OBJS.put("401", "StatusUnauthorized"); + STATUS_CODE_OBJS.put("402", "StatusPaymentRequired"); + STATUS_CODE_OBJS.put("403", "StatusForbidden"); + STATUS_CODE_OBJS.put("404", "StatusNotFound"); + STATUS_CODE_OBJS.put("405", "StatusMethodNotAllowed"); + STATUS_CODE_OBJS.put("406", "StatusNotAcceptable"); + STATUS_CODE_OBJS.put("407", "StatusProxyAuthenticationRequired"); + STATUS_CODE_OBJS.put("408", "StatusRequestTimeout"); + STATUS_CODE_OBJS.put("409", "StatusConflict"); + STATUS_CODE_OBJS.put("410", "StatusGone"); + STATUS_CODE_OBJS.put("411", "StatusLengthRequired"); + STATUS_CODE_OBJS.put("412", "StatusPreconditionFailed"); + STATUS_CODE_OBJS.put("413", "StatusPayloadTooLarge"); + STATUS_CODE_OBJS.put("414", "StatusUriTooLong"); + STATUS_CODE_OBJS.put("415", "StatusUnsupportedMediaType"); + STATUS_CODE_OBJS.put("416", "StatusRangeNotSatisfiable"); + STATUS_CODE_OBJS.put("417", "StatusExpectationFailed"); + STATUS_CODE_OBJS.put("421", "StatusMisdirectedRequest"); + STATUS_CODE_OBJS.put("422", "StatusUnprocessableEntity"); + STATUS_CODE_OBJS.put("423", "StatusLocked"); + STATUS_CODE_OBJS.put("424", "StatusFailedDependency"); + STATUS_CODE_OBJS.put("425", "StatusTooEarly"); + STATUS_CODE_OBJS.put("426", "StatusUpgradeRequired"); + STATUS_CODE_OBJS.put("428", "StatusPreconditionRequired"); + STATUS_CODE_OBJS.put("429", "StatusTooManyRequests"); + STATUS_CODE_OBJS.put("431", "StatusRequestHeaderFieldsTooLarge"); + STATUS_CODE_OBJS.put("451", "StatusUnavailableDueToLegalReasons"); + STATUS_CODE_OBJS.put("500", "StatusInternalServerError"); + STATUS_CODE_OBJS.put("501", "StatusNotImplemented"); + STATUS_CODE_OBJS.put("502", "StatusBadGateway"); + STATUS_CODE_OBJS.put("503", "StatusServiceUnavailable"); + STATUS_CODE_OBJS.put("504", "StatusGatewayTimeout"); + STATUS_CODE_OBJS.put("505", "StatusHttpVersionNotSupported"); + STATUS_CODE_OBJS.put("506", "StatusVariantAlsoNegotiates"); + STATUS_CODE_OBJS.put("507", "StatusInsufficientStorage"); + STATUS_CODE_OBJS.put("508", "StatusLoopDetected"); + STATUS_CODE_OBJS.put("510", "StatusNotExtended"); + STATUS_CODE_OBJS.put("511", "StatusNetworkAuthenticationRequired"); + } + + private ExternResponseProcessor() { + } + + public static Object processResponse(Environment env, BObject response, BTypedesc targetType, + boolean requireValidation) { + return getResponseWithType(response, targetType.getDescribingType(), requireValidation, env.getRuntime()); + } + + private static Object getResponseWithType(BObject response, Type targetType, boolean requireValidation, + Runtime runtime) { + long responseStatusCode = getStatusCode(response); + Optional statusCodeResponseType = getStatusCodeResponseType(targetType, + Long.toString(responseStatusCode)); + if (statusCodeResponseType.isPresent() && + TypeUtils.getImpliedType(statusCodeResponseType.get()) instanceof RecordType statusCodeRecordType) { + return generateStatusCodeResponseType(response, requireValidation, runtime, statusCodeRecordType, + responseStatusCode); + } else if ((399 < responseStatusCode) && (responseStatusCode < 600)) { + return hasHttpResponseType(targetType) ? response : getApplicationResponseError(runtime, response); + } else { + return generatePayload(response, targetType, requireValidation, runtime); + } + } + + private static Object generatePayload(BObject response, Type targetType, boolean requireValidation, + Runtime runtime) { + try { + return getPayload(runtime, response, getAnydataType(targetType), requireValidation); + } catch (BError e) { + if (hasHttpResponseType(targetType)) { + return response; + } + return createHttpError(String.format(INCOMPATIBLE_TYPE_FOUND_FOR_RESPONSE, targetType, + getStatusCode(response)), PAYLOAD_BINDING_CLIENT_ERROR, e); + } + } + + private static long getStatusCode(BObject response) { + return response.getIntValue(StringUtils.fromString(STATUS_CODE)); + } + + private static Object generateStatusCodeResponseType(BObject response, boolean requireValidation, Runtime runtime, + RecordType statusCodeRecordType, long responseStatusCode) { + BMap statusCodeRecord = ValueCreator.createRecordValue(statusCodeRecordType); + + String statusCodeObjName = STATUS_CODE_OBJS.get(Long.toString(responseStatusCode)); + if (Objects.isNull(statusCodeObjName)) { + return createHttpError(String.format(UNSUPPORTED_STATUS_CODE, responseStatusCode), + STATUS_CODE_RECORD_BINDING_ERROR); + } + + populateStatusCodeObject(statusCodeObjName, statusCodeRecord); + + Object headerMap = getHeaders(response, requireValidation, statusCodeRecordType); + if (headerMap instanceof BError) { + return headerMap; + } + statusCodeRecord.put(StringUtils.fromString(STATUS_CODE_RESPONSE_HEADERS_FIELD), headerMap); + + if (statusCodeRecordType.getFields().containsKey(STATUS_CODE_RESPONSE_MEDIA_TYPE_FIELD)) { + Object mediaType = getMediaType(response, requireValidation, statusCodeRecordType); + if (mediaType instanceof BError) { + return mediaType; + } + statusCodeRecord.put(StringUtils.fromString(STATUS_CODE_RESPONSE_MEDIA_TYPE_FIELD), mediaType); + } + + if (statusCodeRecordType.getFields().containsKey(STATUS_CODE_RESPONSE_BODY_FIELD)) { + Object payload = getBody(response, requireValidation, runtime, statusCodeRecordType); + if (payload instanceof BError) { + return payload; + } + statusCodeRecord.put(StringUtils.fromString(STATUS_CODE_RESPONSE_BODY_FIELD), payload); + } + return statusCodeRecord; + } + + private static Object getBody(BObject response, boolean requireValidation, Runtime runtime, + RecordType statusCodeRecordType) { + Type bodyType = statusCodeRecordType.getFields().get(STATUS_CODE_RESPONSE_BODY_FIELD).getFieldType(); + return getPayload(runtime, response, bodyType, requireValidation); + } + + private static Object getHeaders(BObject response, boolean requireValidation, RecordType statusCodeRecordType) { + Type headersType = statusCodeRecordType.getFields().get(STATUS_CODE_RESPONSE_HEADERS_FIELD).getFieldType(); + return getHeadersMap(response, headersType, requireValidation); + } + + private static Object getMediaType(BObject response, boolean requireValidation, RecordType statusCodeRecordType) { + Type mediaTypeType = statusCodeRecordType.getFields().get(STATUS_CODE_RESPONSE_MEDIA_TYPE_FIELD).getFieldType(); + return getMediaType(response, mediaTypeType, requireValidation); + } + + private static void populateStatusCodeObject(String statusCodeObjName, BMap statusCodeRecord) { + Object status = ValueCreatorUtils.createStatusCodeObject(statusCodeObjName); + statusCodeRecord.put(StringUtils.fromString(STATUS_CODE_RESPONSE_STATUS_FIELD), status); + } + + private static Type getAnydataType(Type targetType) { + List anydataTypes = extractAnydataTypes(targetType, new ArrayList<>()); + if (anydataTypes.isEmpty()) { + throw ErrorCreator.createError(StringUtils.fromString(NO_ANYDATA_TYPE_FOUND_IN_THE_TARGET_TYPE)); + } else if (anydataTypes.size() == 1) { + return anydataTypes.get(0); + } else { + return TypeCreator.createUnionType(anydataTypes); + } + } + + private static List extractAnydataTypes(Type targetType, List anydataTypes) { + if (targetType.isAnydata()) { + anydataTypes.add(targetType); + return anydataTypes; + } + + switch (targetType.getTag()) { + case TypeTags.UNION_TAG: + List memberTypes = ((UnionType) targetType).getMemberTypes(); + for (Type memberType : memberTypes) { + extractAnydataTypes(memberType, anydataTypes); + } + return anydataTypes; + case TypeTags.TYPE_REFERENCED_TYPE_TAG: + return extractAnydataTypes(TypeUtils.getImpliedType(targetType), anydataTypes); + default: + return anydataTypes; + } + } + + private static Object getMediaType(BObject response, Type mediaTypeType, boolean requireValidation) { + String contentType = getContentType(response); + try { + Object convertedValue = ValueUtils.convert(Objects.nonNull(contentType) ? + StringUtils.fromString(contentType) : null, mediaTypeType); + return validateConstraints(requireValidation, convertedValue, mediaTypeType, + MEDIA_TYPE_VALIDATION_CLIENT_ERROR, MEDIA_TYPE_BINDING_FAILED); + } catch (BError conversionError) { + return createHttpError(MEDIA_TYPE_BINDING_FAILED, MEDIA_TYPE_BINDING_CLIENT_ERROR, conversionError); + } + } + + private static String getContentType(BObject response) { + HttpHeaders httpHeaders = (HttpHeaders) response.getNativeData(HTTP_HEADERS); + return httpHeaders.get("Content-Type"); + } + + private static Object getHeadersMap(BObject response, Type headersType, boolean requireValidation) { + HttpHeaders httpHeaders = (HttpHeaders) response.getNativeData(HTTP_HEADERS); + Type headersImpliedType = TypeUtils.getImpliedType(headersType); + if (headersImpliedType.getTag() == TypeTags.TYPE_REFERENCED_TYPE_TAG) { + headersImpliedType = TypeUtils.getReferredType(headersImpliedType); + } + + Object headerMap; + if (headersImpliedType.getTag() == TypeTags.MAP_TAG) { + headerMap = createHeaderMap(httpHeaders, + TypeUtils.getImpliedType(((MapType) headersImpliedType).getConstrainedType())); + } else if (headersImpliedType.getTag() == TypeTags.RECORD_TYPE_TAG) { + headerMap = createHeaderRecord(httpHeaders, (RecordType) headersImpliedType); + } else { + return createHttpError(String.format(UNSUPPORTED_HEADERS_TYPE, headersType), + STATUS_CODE_RECORD_BINDING_ERROR); + } + + if (headerMap instanceof BError) { + return headerMap; + } + + try { + Object convertedHeaderMap = ValueUtils.convert(headerMap, headersType); + return validateConstraints(requireValidation, convertedHeaderMap, headersType, + HEADER_VALIDATION_CLIENT_ERROR, HEADER_BINDING_FAILED); + } catch (BError conversionError) { + return createHttpError(HEADER_BINDING_FAILED, HEADER_BINDING_CLIENT_ERROR, conversionError); + } + } + + private static boolean hasHttpResponseType(Type targetType) { + return switch (targetType.getTag()) { + case TypeTags.OBJECT_TYPE_TAG -> true; + case TypeTags.UNION_TAG -> ((UnionType) targetType).getMemberTypes().stream().anyMatch( + ExternResponseProcessor::hasHttpResponseType); + case TypeTags.TYPE_REFERENCED_TYPE_TAG -> hasHttpResponseType(TypeUtils.getImpliedType(targetType)); + default -> false; + }; + } + + private static Optional getStatusCodeResponseType(Type targetType, String statusCode) { + if (isStatusCodeResponseType(targetType)) { + if (getStatusCode(targetType).equals(statusCode)) { + return Optional.of(targetType); + } + } else if (targetType instanceof UnionType unionType) { + return unionType.getMemberTypes().stream() + .map(member -> getStatusCodeResponseType(member, statusCode)) + .filter(Optional::isPresent) + .flatMap(Optional::stream) + .findFirst(); + } else if (targetType instanceof ReferenceType + && (!targetType.equals(TypeUtils.getImpliedType(targetType)))) { + return getStatusCodeResponseType(TypeUtils.getImpliedType(targetType), statusCode); + } + return Optional.empty(); + } + + private static boolean isStatusCodeResponseType(Type targetType) { + return targetType instanceof ReferenceType referenceType && + TypeUtils.getImpliedType(referenceType) instanceof RecordType recordType && + recordType.getFields().containsKey(STATUS_CODE_RESPONSE_STATUS_FIELD) && + recordType.getFields().get(STATUS_CODE_RESPONSE_STATUS_FIELD).getFieldType() instanceof ObjectType; + } + + private static String getStatusCode(Type targetType) { + return ((ObjectType) ((RecordType) TypeUtils.getImpliedType(targetType)).getFields(). + get(STATUS_CODE_RESPONSE_STATUS_FIELD).getFieldType()).getFields(). + get(STATUS_CODE_RESPONSE_STATUS_CODE_FIELD).getFieldType().getEmptyValue().toString(); + } + + private static Object createHeaderMap(HttpHeaders httpHeaders, Type elementType) { + BMap headerMap = ValueCreator.createMapValue(); + Set headerNames = httpHeaders.names(); + for (String headerName : headerNames) { + List headerValues = getHeader(httpHeaders, headerName); + try { + Object convertedValue = convertHeaderValues(headerValues, elementType); + headerMap.put(StringUtils.fromString(headerName), convertedValue); + } catch (BError ex) { + return createHttpError(String.format(HEADER_BINDING_FAILED_ERROR_MSG, headerName), + HEADER_BINDING_CLIENT_ERROR, ex); + } + } + return headerMap; + } + + private static Object createHeaderRecord(HttpHeaders httpHeaders, RecordType headersType) { + Map headers = headersType.getFields(); + BMap headerMap = ValueCreator.createMapValue(); + for (Map.Entry header : headers.entrySet()) { + Field headerField = header.getValue(); + Type headerFieldType = TypeUtils.getImpliedType(headerField.getFieldType()); + + String headerName = header.getKey(); + List headerValues = getHeader(httpHeaders, headerName); + + if (headerValues.isEmpty()) { + // Only optional is allowed at the moment + if (isOptionalHeaderField(headerField)) { + continue; + } + // Return Header Not Found Error + return createHttpError(String.format(NO_HEADER_VALUE_ERROR_MSG, headerName), + HEADER_NOT_FOUND_CLIENT_ERROR); + } + + try { + Object convertedValue = convertHeaderValues(headerValues, headerFieldType); + headerMap.put(StringUtils.fromString(headerName), convertedValue); + } catch (BError ex) { + return createHttpError(String.format(HEADER_BINDING_FAILED_ERROR_MSG, headerName), + HEADER_BINDING_CLIENT_ERROR, ex); + } + } + return headerMap; + } + + private static BArray parseHeaderValue(List header) { + Object[] parsedValues; + try { + parsedValues = header.stream().map(JsonUtils::parse).toList().toArray(); + } catch (Exception e) { + parsedValues = header.stream().map(StringUtils::fromString).toList().toArray(); + } + return ValueCreator.createArrayValue(parsedValues, TypeCreator.createArrayType(PredefinedTypes.TYPE_JSON)); + } + + static class HeaderTypeInfo { + private boolean hasString = false; + private boolean hasStringArray = false; + private boolean hasArray = false; + } + + private static HeaderTypeInfo getHeaderTypeInfo(Type headerType, HeaderTypeInfo headerTypeInfo, + boolean fromArray) { + switch (headerType.getTag()) { + case TypeTags.STRING_TAG: + if (fromArray) { + headerTypeInfo.hasStringArray = true; + } else { + headerTypeInfo.hasString = true; + } + return headerTypeInfo; + case TypeTags.ARRAY_TAG: + headerTypeInfo.hasArray = true; + Type elementType = ((ArrayType) headerType).getElementType(); + return getHeaderTypeInfo(elementType, headerTypeInfo, true); + case TypeTags.UNION_TAG: + List memberTypes = ((UnionType) headerType).getMemberTypes(); + for (Type memberType : memberTypes) { + headerTypeInfo = getHeaderTypeInfo(memberType, headerTypeInfo, false); + } + return headerTypeInfo; + case TypeTags.TYPE_REFERENCED_TYPE_TAG: + return getHeaderTypeInfo(TypeUtils.getImpliedType(headerType), headerTypeInfo, false); + default: + return headerTypeInfo; + } + } + + private static Object convertHeaderValues(List headerValues, Type headerType) { + HeaderTypeInfo headerTypeInfo = getHeaderTypeInfo(headerType, new HeaderTypeInfo(), false); + if (headerTypeInfo.hasString) { + return StringUtils.fromString(headerValues.get(0)); + } else if (headerTypeInfo.hasStringArray) { + return StringUtils.fromStringArray(headerValues.toArray(new String[0])); + } else { + BArray parsedValues = parseHeaderValue(headerValues); + if (headerTypeInfo.hasArray) { + try { + return ValueUtils.convert(parsedValues, headerType); + } catch (BError e) { + return ValueUtils.convert(parsedValues.get(0), headerType); + } + } else { + return ValueUtils.convert(parsedValues.get(0), headerType); + } + } + } + + private static Object getPayload(Runtime runtime, BObject response, Type payloadType, boolean requireValidation) { + return performBalDataBinding(runtime, response, payloadType, requireValidation); + } + + private static boolean isOptionalHeaderField(Field headerField) { + return SymbolFlags.isFlagOn(headerField.getFlags(), SymbolFlags.OPTIONAL); + } + + private static List getHeader(HttpHeaders httpHeaders, String headerName) { + return httpHeaders.getAllAsString(headerName); + } + + private static Object validateConstraints(boolean requireValidation, Object convertedValue, Type type, + HttpErrorType errorType, String errorMsg) { + if (requireValidation) { + Object result = Constraints.validate(convertedValue, ValueCreator.createTypedescValue(type)); + if (result instanceof BError bError) { + String message = errorMsg + ": " + HttpUtil.getPrintableErrorMsg(bError); + return createHttpError(message, errorType); + } + } + return convertedValue; + } + + public static Object performBalDataBinding(Runtime runtime, BObject response, Type payloadType, + boolean requireValidation) { + final Object[] payload = new Object[1]; + CountDownLatch countDownLatch = new CountDownLatch(1); + Callback returnCallback = new Callback() { + @Override + public void notifySuccess(Object result) { + payload[0] = result; + countDownLatch.countDown(); + } + + @Override + public void notifyFailure(BError result) { + payload[0] = result; + countDownLatch.countDown(); + } + }; + Object[] paramFeed = new Object[4]; + paramFeed[0] = ValueCreator.createTypedescValue(payloadType); + paramFeed[1] = true; + paramFeed[2] = requireValidation; + paramFeed[3] = true; + runtime.invokeMethodAsyncSequentially(response, PERFORM_DATA_BINDING, null, + ModuleUtils.getNotifySuccessMetaData(), returnCallback, null, PredefinedTypes.TYPE_ANY, + paramFeed); + try { + countDownLatch.await(); + return payload[0]; + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + return createHttpError(PAYLOAD_BINDING_FAILED, CLIENT_ERROR, HttpUtil.createError(exception)); + } + } + + public static Object getApplicationResponseError(Runtime runtime, BObject response) { + final Object[] clientError = new Object[1]; + CountDownLatch countDownLatch = new CountDownLatch(1); + Callback returnCallback = new Callback() { + @Override + public void notifySuccess(Object result) { + clientError[0] = result; + countDownLatch.countDown(); + } + + @Override + public void notifyFailure(BError result) { + clientError[0] = result; + countDownLatch.countDown(); + } + }; + runtime.invokeMethodAsyncSequentially(response, GET_APPLICATION_RESPONSE_ERROR, null, + ModuleUtils.getNotifySuccessMetaData(), returnCallback, null, PredefinedTypes.TYPE_ERROR); + try { + countDownLatch.await(); + return clientError[0]; + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + return createHttpError(APPLICATION_RES_ERROR_CREATION_FAILED, + PAYLOAD_BINDING_CLIENT_ERROR, HttpUtil.createError(exception)); + } + } +} diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/service/signature/AllHeaderParams.java b/native/src/main/java/io/ballerina/stdlib/http/api/service/signature/AllHeaderParams.java index 68ebac151a..e937454eea 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/service/signature/AllHeaderParams.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/service/signature/AllHeaderParams.java @@ -33,7 +33,7 @@ import java.util.ArrayList; import java.util.List; -import static io.ballerina.stdlib.http.api.HttpErrorType.INTERNAL_HEADER_BINDING_ERROR; +import static io.ballerina.stdlib.http.api.HttpErrorType.INTERNAL_HEADER_BINDING_LISTENER_ERROR; import static io.ballerina.stdlib.http.api.service.signature.ParamUtils.castParam; import static io.ballerina.stdlib.http.api.service.signature.ParamUtils.castParamArray; @@ -80,7 +80,7 @@ public void populateFeed(HttpCarbonMessage httpCarbonMessage, Object[] paramFeed try { castedHeader = ValueUtils.convert(parsedHeader, headerParam.getOriginalType()); } catch (Exception ex) { - throw HttpUtil.createHttpStatusCodeError(INTERNAL_HEADER_BINDING_ERROR, + throw HttpUtil.createHttpStatusCodeError(INTERNAL_HEADER_BINDING_LISTENER_ERROR, String.format(HEADER_BINDING_FAILED_ERROR_MSG, headerParam.getHeaderName()), null, HttpUtil.createError(ex)); } @@ -96,7 +96,7 @@ public void populateFeed(HttpCarbonMessage httpCarbonMessage, Object[] paramFeed paramFeed[index] = true; continue; } else { - throw HttpUtil.createHttpStatusCodeError(INTERNAL_HEADER_BINDING_ERROR, + throw HttpUtil.createHttpStatusCodeError(INTERNAL_HEADER_BINDING_LISTENER_ERROR, String.format(NO_HEADER_VALUE_ERROR_MSG, token)); } } @@ -106,7 +106,7 @@ public void populateFeed(HttpCarbonMessage httpCarbonMessage, Object[] paramFeed paramFeed[index] = true; continue; } else { - throw HttpUtil.createHttpStatusCodeError(INTERNAL_HEADER_BINDING_ERROR, + throw HttpUtil.createHttpStatusCodeError(INTERNAL_HEADER_BINDING_LISTENER_ERROR, String.format(NO_HEADER_VALUE_ERROR_MSG, token)); } } @@ -121,7 +121,7 @@ public void populateFeed(HttpCarbonMessage httpCarbonMessage, Object[] paramFeed } castedHeaderValue = ValueUtils.convert(parsedHeaderValue, headerParam.getOriginalType()); } catch (Exception ex) { - throw HttpUtil.createHttpStatusCodeError(INTERNAL_HEADER_BINDING_ERROR, + throw HttpUtil.createHttpStatusCodeError(INTERNAL_HEADER_BINDING_LISTENER_ERROR, String.format(HEADER_BINDING_FAILED_ERROR_MSG, token), null, HttpUtil.createError(ex)); } @@ -147,7 +147,7 @@ private BMap processHeaderRecord(HeaderParam headerParam, HttpH } else if (headerParam.isNilable()) { return null; } else { - throw HttpUtil.createHttpStatusCodeError(INTERNAL_HEADER_BINDING_ERROR, + throw HttpUtil.createHttpStatusCodeError(INTERNAL_HEADER_BINDING_LISTENER_ERROR, String.format(NO_HEADER_VALUE_ERROR_MSG, key)); } } @@ -158,7 +158,7 @@ private BMap processHeaderRecord(HeaderParam headerParam, HttpH } else if (headerParam.isNilable()) { return null; } else { - throw HttpUtil.createHttpStatusCodeError(INTERNAL_HEADER_BINDING_ERROR, + throw HttpUtil.createHttpStatusCodeError(INTERNAL_HEADER_BINDING_LISTENER_ERROR, String.format(NO_HEADER_VALUE_ERROR_MSG, key)); } } @@ -171,7 +171,7 @@ private BMap processHeaderRecord(HeaderParam headerParam, HttpH recordValue.put(StringUtils.fromString(key), castParam(fieldTypeTag, headerValues.get(0))); } } catch (Exception ex) { - throw HttpUtil.createHttpStatusCodeError(INTERNAL_HEADER_BINDING_ERROR, + throw HttpUtil.createHttpStatusCodeError(INTERNAL_HEADER_BINDING_LISTENER_ERROR, String.format(HEADER_BINDING_FAILED_ERROR_MSG, key), null, HttpUtil.createError(ex)); } } diff --git a/native/src/main/java/io/ballerina/stdlib/http/api/service/signature/HeaderParam.java b/native/src/main/java/io/ballerina/stdlib/http/api/service/signature/HeaderParam.java index bd29b95806..9d4de89504 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/api/service/signature/HeaderParam.java +++ b/native/src/main/java/io/ballerina/stdlib/http/api/service/signature/HeaderParam.java @@ -31,7 +31,7 @@ import static io.ballerina.runtime.api.TypeTags.RECORD_TYPE_TAG; import static io.ballerina.stdlib.http.api.HttpConstants.HEADER_PARAM; -import static io.ballerina.stdlib.http.api.HttpErrorType.INTERNAL_HEADER_VALIDATION_ERROR; +import static io.ballerina.stdlib.http.api.HttpErrorType.INTERNAL_HEADER_VALIDATION_LISTENER_ERROR; /** * {@code {@link HeaderParam }} represents a inbound request header parameter details. @@ -98,7 +98,7 @@ public Object validateConstraints(Object headerValue) { Object result = Constraints.validateAfterTypeConversion(headerValue, getOriginalType()); if (result instanceof BError) { String message = "header validation failed: " + HttpUtil.getPrintableErrorMsg((BError) result); - throw HttpUtil.createHttpStatusCodeError(INTERNAL_HEADER_VALIDATION_ERROR, message); + throw HttpUtil.createHttpStatusCodeError(INTERNAL_HEADER_VALIDATION_LISTENER_ERROR, message); } } return headerValue;