diff --git a/ballerina-tests/tests/http_client_data_binding.bal b/ballerina-tests/tests/http_client_data_binding.bal index ce612af583..a387c88c22 100644 --- a/ballerina-tests/tests/http_client_data_binding.bal +++ b/ballerina-tests/tests/http_client_data_binding.bal @@ -396,7 +396,8 @@ service /backend on clientDBBackendListener { } resource function get xmltype() returns http:NotFound { - return {body: xml `Bad Request`}; + xml payload = xml`Bad Request`; + return {body: payload}; } resource function get jsontype() returns http:InternalServerError { diff --git a/ballerina-tests/tests/http_client_url_encoded_content_test.bal b/ballerina-tests/tests/http_client_url_encoded_content_test.bal new file mode 100644 index 0000000000..940b3e6d3b --- /dev/null +++ b/ballerina-tests/tests/http_client_url_encoded_content_test.bal @@ -0,0 +1,107 @@ +// Copyright (c) 2021 WSO2 Inc. (http://www.wso2.org) All Rights Reserved. +// +// WSO2 Inc. 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. + +// NOTE: All the tokens/credentials used in this test are dummy tokens/credentials and used only for testing purposes. + +import ballerina/http; +import ballerina/url; +import ballerina/mime; +import ballerina/test; + +final http:Client clientUrlEncodedTestClient = check new(string`http://localhost:${clientFormUrlEncodedTestPort.toString()}/databinding`); +final string expectedResponse = "URL_ENCODED_key1=value1&key2=value2"; +final readonly & map payload = { + "key1": "value1", + "key2": "value2" +}; + +service /databinding on new http:Listener(clientFormUrlEncodedTestPort) { + resource function 'default .(http:Request req) returns string|error { + string contentType = req.getContentType(); + contentType = contentType == mime:APPLICATION_FORM_URLENCODED ? "URL_ENCODED": "INVALID"; + string payload = check req.getTextPayload(); + string decodedContent = check url:decode(payload, "UTF-8"); + return string`${contentType}_${decodedContent}`; + } +} + +@test:Config { + groups: ["urlEncodedContent"] +} +isolated function testUrlContentWithPost() returns error? { + string response = check clientUrlEncodedTestClient->post("", payload, mediaType = mime:APPLICATION_FORM_URLENCODED); + test:assertEquals(response, expectedResponse, msg = "Found unexpected output"); +} + +@test:Config { + groups: ["urlEncodedContent"] +} +isolated function testUrlContentWithPut() returns error? { + string response = check clientUrlEncodedTestClient->put("", payload, mediaType = mime:APPLICATION_FORM_URLENCODED); + test:assertEquals(response, expectedResponse, msg = "Found unexpected output"); +} + +@test:Config { + groups: ["urlEncodedContent"] +} +isolated function testUrlContentWithDelete() returns error? { + string response = check clientUrlEncodedTestClient->delete("", payload, mediaType = mime:APPLICATION_FORM_URLENCODED); + test:assertEquals(response, expectedResponse, msg = "Found unexpected output"); +} + +@test:Config { + groups: ["urlEncodedContent"] +} +isolated function testUrlContentWithPatch() returns error? { + string response = check clientUrlEncodedTestClient->patch("", payload, mediaType = mime:APPLICATION_FORM_URLENCODED); + test:assertEquals(response, expectedResponse, msg = "Found unexpected output"); +} + +@test:Config { + groups: ["urlEncodedContent"] +} +isolated function testUrlContentWithExecute() returns error? { + string response = check clientUrlEncodedTestClient->execute("POST", "", payload, mediaType = mime:APPLICATION_FORM_URLENCODED); + test:assertEquals(response, expectedResponse, msg = "Found unexpected output"); +} + +@test:Config { + groups: ["urlEncodedContent"] +} +isolated function testUrlContentWithIntPayload() returns error? { + string|error response = clientUrlEncodedTestClient->post("", 10, mediaType = mime:APPLICATION_FORM_URLENCODED); + test:assertTrue(response is error, "Found unexpected output"); + if response is error { + test:assertEquals(response.message(), "unsupported content for application/x-www-form-urlencoded media type", msg = "Found unexpected output"); + } +} + +@test:Config { + groups: ["urlEncodedContent"] +} +isolated function testUrlContentWithJsonPayload() returns error? { + json jsonPayload = { + "key1": "val1", + "key2": [ + "val2.1", "val2.2" + ] + }; + string|error response = clientUrlEncodedTestClient->post("", jsonPayload, mediaType = mime:APPLICATION_FORM_URLENCODED); + test:assertTrue(response is error, "Found unexpected output"); + if response is error { + test:assertEquals(response.message(), "unsupported content for application/x-www-form-urlencoded media type", msg = "Found unexpected output"); + } +} diff --git a/ballerina-tests/tests/test_service_ports.bal b/ballerina-tests/tests/test_service_ports.bal index ffdf4760fa..bbd0104017 100644 --- a/ballerina-tests/tests/test_service_ports.bal +++ b/ballerina-tests/tests/test_service_ports.bal @@ -156,6 +156,7 @@ const int requestInterceptorByteArrayPayloadBindingTestPort = 9606; const int requestInterceptorWithQueryParamTestPort = 9607; const int requestInterceptorServiceConfigTestPort1 = 9608; const int requestInterceptorServiceConfigTestPort2 = 9609; +const int clientFormUrlEncodedTestPort = 9610; //HTTP2 const int serverPushTestPort1 = 9701; diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index d31bbb7e47..21c556865e 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -12,7 +12,7 @@ distribution = "2201.0.0" path = "../native/build/libs/http-native-2.2.0-SNAPSHOT.jar" [[platform.java11.dependency]] -path = "./lib/mime-native-2.2.0-20211220-222200-7bd9f71.jar" +path = "./lib/mime-native-2.2.0-20211222-181300-dbf5417.jar" [[platform.java11.dependency]] path = "./lib/netty-common-4.1.71.Final.jar" diff --git a/ballerina/http_client_endpoint.bal b/ballerina/http_client_endpoint.bal index 6ea14f946e..0747016256 100644 --- a/ballerina/http_client_endpoint.bal +++ b/ballerina/http_client_endpoint.bal @@ -73,7 +73,7 @@ public client isolated class Client { private isolated function processPost(string path, RequestMessage message, TargetType targetType, string? mediaType, map? headers) returns Response|PayloadType|ClientError { - Request req = check buildRequest(message); + Request req = check buildRequest(message, mediaType); populateOptions(req, mediaType, headers); Response|ClientError response = self.httpClient->post(path, req); if (observabilityEnabled && response is Response) { @@ -100,7 +100,7 @@ public client isolated class Client { private isolated function processPut(string path, RequestMessage message, TargetType targetType, string? mediaType, map? headers) returns Response|PayloadType|ClientError { - Request req = check buildRequest(message); + Request req = check buildRequest(message, mediaType); populateOptions(req, mediaType, headers); Response|ClientError response = self.httpClient->put(path, req); if (observabilityEnabled && response is Response) { @@ -127,7 +127,7 @@ public client isolated class Client { private isolated function processPatch(string path, RequestMessage message, TargetType targetType, string? mediaType, map? headers) returns Response|PayloadType|ClientError { - Request req = check buildRequest(message); + Request req = check buildRequest(message, mediaType); populateOptions(req, mediaType, headers); Response|ClientError response = self.httpClient->patch(path, req); if (observabilityEnabled && response is Response) { @@ -154,7 +154,7 @@ public client isolated class Client { private isolated function processDelete(string path, RequestMessage message, TargetType targetType, string? mediaType, map? headers) returns Response|PayloadType|ClientError { - Request req = check buildRequest(message); + Request req = check buildRequest(message, mediaType); populateOptions(req, mediaType, headers); Response|ClientError response = self.httpClient->delete(path, req); if (observabilityEnabled && response is Response) { @@ -243,7 +243,7 @@ public client isolated class Client { private isolated function processExecute(string httpVerb, string path, RequestMessage message, TargetType targetType, string? mediaType, map? headers) returns Response|PayloadType|ClientError { - Request req = check buildRequest(message); + Request req = check buildRequest(message, mediaType); populateOptions(req, mediaType, headers); Response|ClientError response = self.httpClient->execute(httpVerb, path, req); if (observabilityEnabled && response is Response) { @@ -283,7 +283,7 @@ public client isolated class Client { # + message - An HTTP outbound request or any allowed payload # + return - An `http:HttpFuture` that represents an asynchronous service invocation or else an `http:ClientError` if the submission fails remote isolated function submit(string httpVerb, string path, RequestMessage message) returns HttpFuture|ClientError { - Request req = check buildRequest(message); + Request req = check buildRequest(message, ()); return self.httpClient->submit(httpVerb, path, req); } diff --git a/ballerina/http_commons.bal b/ballerina/http_commons.bal index 298fd6d02d..05025b97f1 100644 --- a/ballerina/http_commons.bal +++ b/ballerina/http_commons.bal @@ -21,6 +21,8 @@ import ballerina/io; import ballerina/observe; import ballerina/time; import ballerina/log; +import ballerina/lang.'string as strings; +import ballerina/url; final boolean observabilityEnabled = observe:isObservabilityEnabled(); @@ -37,7 +39,7 @@ public isolated function parseHeader(string headerValue) returns HeaderValue[]|C name: "parseHeader" } external; -isolated function buildRequest(RequestMessage message) returns Request|ClientError { +isolated function buildRequest(RequestMessage message, string? mediaType) returns Request|ClientError { Request request = new; if (message is ()) { request.noEntityBody = true; @@ -56,16 +58,46 @@ isolated function buildRequest(RequestMessage message) returns Request|ClientErr } else if (message is mime:Entity[]) { request.setBodyParts(message); } else { - var result = trap val:toJson(message); - if (result is error) { - return error InitializingOutboundRequestError("json conversion error: " + result.message(), result); - } else { - request.setJsonPayload(result); + match mediaType { + mime:APPLICATION_FORM_URLENCODED => { + string payload = check processUrlEncodedContent(message); + request.setTextPayload(payload, mime:APPLICATION_FORM_URLENCODED); + } + _ => { + json payload = check processJsonContent(message); + request.setJsonPayload(payload); + } } } return request; } +isolated function processUrlEncodedContent(anydata message) returns string|ClientError { + if message is map { + do { + string[] messageParams = []; + foreach var ['key, value] in message.entries() { + string encodedValue = check url:encode(value, "UTF-8"); + string entry = string`${'key}=${encodedValue}`; + messageParams.push(entry); + } + return strings:'join("&", ...messageParams); + } on fail var e { + return error InitializingOutboundRequestError("content encoding error: " + e.message(), e); + } + } else { + return error InitializingOutboundRequestError("unsupported content for application/x-www-form-urlencoded media type"); + } +} + +isolated function processJsonContent(anydata message) returns json|ClientError { + var result = trap val:toJson(message); + if (result is error) { + return error InitializingOutboundRequestError("json conversion error: " + result.message(), result); + } + return result; +} + isolated function buildResponse(ResponseMessage message) returns Response|ListenerError { Response response = new; if (message is ()) { diff --git a/ballerina/resiliency_failover_client.bal b/ballerina/resiliency_failover_client.bal index 172f434f71..a5a3c70ad4 100644 --- a/ballerina/resiliency_failover_client.bal +++ b/ballerina/resiliency_failover_client.bal @@ -82,7 +82,7 @@ public client isolated class FailoverClient { private isolated function processPost(string path, RequestMessage message, TargetType targetType, string? mediaType, map? headers) returns Response|PayloadType|ClientError { - Request req = check buildRequest(message); + Request req = check buildRequest(message, mediaType); populateOptions(req, mediaType, headers); var result = self.performFailoverAction(path, req, HTTP_POST); if (result is HttpFuture) { @@ -110,7 +110,7 @@ public client isolated class FailoverClient { private isolated function processPut(string path, RequestMessage message, TargetType targetType, string? mediaType, map? headers) returns Response|PayloadType|ClientError { - Request req = check buildRequest(message); + Request req = check buildRequest(message, mediaType); populateOptions(req, mediaType, headers); var result = self.performFailoverAction(path, req, HTTP_PUT); if (result is HttpFuture) { @@ -138,7 +138,7 @@ public client isolated class FailoverClient { private isolated function processPatch(string path, RequestMessage message, TargetType targetType, string? mediaType, map? headers) returns Response|PayloadType|ClientError { - Request req = check buildRequest(message); + Request req = check buildRequest(message, mediaType); populateOptions(req, mediaType, headers); var result = self.performFailoverAction(path, req, HTTP_PATCH); if (result is HttpFuture) { @@ -166,7 +166,7 @@ public client isolated class FailoverClient { private isolated function processDelete(string path, RequestMessage message, TargetType targetType, string? mediaType, map? headers) returns Response|PayloadType|ClientError { - Request req = check buildRequest(message); + Request req = check buildRequest(message, mediaType); populateOptions(req, mediaType, headers); var result = self.performFailoverAction(path, req, HTTP_DELETE); if (result is HttpFuture) { @@ -259,7 +259,7 @@ public client isolated class FailoverClient { private isolated function processExecute(string httpVerb, string path, RequestMessage message, TargetType targetType, string? mediaType, map? headers) returns Response|PayloadType|ClientError { - Request req = check buildRequest(message); + Request req = check buildRequest(message, mediaType); populateOptions(req, mediaType, headers); var result = self.performExecuteAction(path, req, httpVerb); if (result is HttpFuture) { @@ -302,7 +302,7 @@ public client isolated class FailoverClient { # + return - An `http:HttpFuture` that represents an asynchronous service invocation or else an `http:ClientError` if the submission # fails remote isolated function submit(string httpVerb, string path, RequestMessage message) returns HttpFuture|ClientError { - Request req = check buildRequest(message); + Request req = check buildRequest(message, ()); var result = self.performExecuteAction(path, req, "SUBMIT", verb = httpVerb); if (result is Response) { return getInvalidTypeError(); diff --git a/ballerina/resiliency_load_balance_client.bal b/ballerina/resiliency_load_balance_client.bal index 238e84e090..5b2cd50d33 100644 --- a/ballerina/resiliency_load_balance_client.bal +++ b/ballerina/resiliency_load_balance_client.bal @@ -74,7 +74,7 @@ public client isolated class LoadBalanceClient { private isolated function processPost(string path, RequestMessage message, TargetType targetType, string? mediaType, map? headers) returns Response|PayloadType|ClientError { - Request req = check buildRequest(message); + Request req = check buildRequest(message, mediaType); populateOptions(req, mediaType, headers); var result = self.performLoadBalanceAction(path, req, HTTP_POST); return processResponse(result, targetType); @@ -98,7 +98,7 @@ public client isolated class LoadBalanceClient { private isolated function processPut(string path, RequestMessage message, TargetType targetType, string? mediaType, map? headers) returns Response|PayloadType|ClientError { - Request req = check buildRequest(message); + Request req = check buildRequest(message, mediaType); populateOptions(req, mediaType, headers); var result = self.performLoadBalanceAction(path, req, HTTP_PUT); return processResponse(result, targetType); @@ -122,7 +122,7 @@ public client isolated class LoadBalanceClient { private isolated function processPatch(string path, RequestMessage message, TargetType targetType, string? mediaType, map? headers) returns Response|PayloadType|ClientError { - Request req = check buildRequest(message); + Request req = check buildRequest(message, mediaType); populateOptions(req, mediaType, headers); var result = self.performLoadBalanceAction(path, req, HTTP_PATCH); return processResponse(result, targetType); @@ -146,7 +146,7 @@ public client isolated class LoadBalanceClient { private isolated function processDelete(string path, RequestMessage message, TargetType targetType, string? mediaType, map? headers) returns Response|PayloadType|ClientError { - Request req = check buildRequest(message); + Request req = check buildRequest(message, mediaType); populateOptions(req, mediaType, headers); var result = self.performLoadBalanceAction(path, req, HTTP_DELETE); return processResponse(result, targetType); @@ -222,7 +222,7 @@ public client isolated class LoadBalanceClient { private isolated function processExecute(string httpVerb, string path, RequestMessage message, TargetType targetType, string? mediaType, map? headers) returns Response|PayloadType|ClientError { - Request req = check buildRequest(message); + Request req = check buildRequest(message, mediaType); populateOptions(req, mediaType, headers); var result = self.performLoadBalanceExecuteAction(path, req, httpVerb); return processResponse(result, targetType); diff --git a/changelog.md b/changelog.md index d7d587968d..a5c22d74f2 100644 --- a/changelog.md +++ b/changelog.md @@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Changed - [Rename RequestContext add function to set](https://github.com/ballerina-platform/ballerina-standard-library/issues/2414) - [Only allow default path in interceptors engaged at listener level](https://github.com/ballerina-platform/ballerina-standard-library/issues/2452) +- [Provide a better way to send with `application/x-www-form-urlencoded`](https://github.com/ballerina-platform/ballerina-standard-library/issues/1705) ## [2.0.1] - 2021-11-20