Skip to content

Commit

Permalink
Merge pull request #797 from ayeshLK/form_url_encoded
Browse files Browse the repository at this point in the history
Improve support for x-form-urlencoded content usage with `http:Client`
  • Loading branch information
shafreenAnfar authored Dec 23, 2021
2 parents 6a81b87 + 92bbefa commit 8736602
Show file tree
Hide file tree
Showing 9 changed files with 167 additions and 25 deletions.
3 changes: 2 additions & 1 deletion ballerina-tests/tests/http_client_data_binding.bal
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,8 @@ service /backend on clientDBBackendListener {
}

resource function get xmltype() returns http:NotFound {
return {body: xml `<test>Bad Request</test>`};
xml payload = xml`<test>Bad Request</test>`;
return {body: payload};
}

resource function get jsontype() returns http:InternalServerError {
Expand Down
107 changes: 107 additions & 0 deletions ballerina-tests/tests/http_client_url_encoded_content_test.bal
Original file line number Diff line number Diff line change
@@ -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<string> 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");
}
}
1 change: 1 addition & 0 deletions ballerina-tests/tests/test_service_ports.bal
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion ballerina/Ballerina.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
12 changes: 6 additions & 6 deletions ballerina/http_client_endpoint.bal
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public client isolated class Client {

private isolated function processPost(string path, RequestMessage message, TargetType targetType,
string? mediaType, map<string|string[]>? 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) {
Expand All @@ -100,7 +100,7 @@ public client isolated class Client {

private isolated function processPut(string path, RequestMessage message, TargetType targetType,
string? mediaType, map<string|string[]>? 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) {
Expand All @@ -127,7 +127,7 @@ public client isolated class Client {

private isolated function processPatch(string path, RequestMessage message, TargetType targetType,
string? mediaType, map<string|string[]>? 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) {
Expand All @@ -154,7 +154,7 @@ public client isolated class Client {

private isolated function processDelete(string path, RequestMessage message, TargetType targetType,
string? mediaType, map<string|string[]>? 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) {
Expand Down Expand Up @@ -243,7 +243,7 @@ public client isolated class Client {
private isolated function processExecute(string httpVerb, string path, RequestMessage message,
TargetType targetType, string? mediaType, map<string|string[]>? 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) {
Expand Down Expand Up @@ -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);

}
Expand Down
44 changes: 38 additions & 6 deletions ballerina/http_commons.bal
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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;
Expand All @@ -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<string> {
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 ()) {
Expand Down
12 changes: 6 additions & 6 deletions ballerina/resiliency_failover_client.bal
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public client isolated class FailoverClient {

private isolated function processPost(string path, RequestMessage message, TargetType targetType,
string? mediaType, map<string|string[]>? 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) {
Expand Down Expand Up @@ -110,7 +110,7 @@ public client isolated class FailoverClient {

private isolated function processPut(string path, RequestMessage message, TargetType targetType,
string? mediaType, map<string|string[]>? 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) {
Expand Down Expand Up @@ -138,7 +138,7 @@ public client isolated class FailoverClient {

private isolated function processPatch(string path, RequestMessage message, TargetType targetType,
string? mediaType, map<string|string[]>? 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) {
Expand Down Expand Up @@ -166,7 +166,7 @@ public client isolated class FailoverClient {

private isolated function processDelete(string path, RequestMessage message, TargetType targetType,
string? mediaType, map<string|string[]>? 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) {
Expand Down Expand Up @@ -259,7 +259,7 @@ public client isolated class FailoverClient {
private isolated function processExecute(string httpVerb, string path, RequestMessage message,
TargetType targetType, string? mediaType, map<string|string[]>? 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) {
Expand Down Expand Up @@ -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();
Expand Down
10 changes: 5 additions & 5 deletions ballerina/resiliency_load_balance_client.bal
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public client isolated class LoadBalanceClient {

private isolated function processPost(string path, RequestMessage message, TargetType targetType,
string? mediaType, map<string|string[]>? 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);
Expand All @@ -98,7 +98,7 @@ public client isolated class LoadBalanceClient {

private isolated function processPut(string path, RequestMessage message, TargetType targetType,
string? mediaType, map<string|string[]>? 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);
Expand All @@ -122,7 +122,7 @@ public client isolated class LoadBalanceClient {

private isolated function processPatch(string path, RequestMessage message, TargetType targetType,
string? mediaType, map<string|string[]>? 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);
Expand All @@ -146,7 +146,7 @@ public client isolated class LoadBalanceClient {

private isolated function processDelete(string path, RequestMessage message, TargetType targetType,
string? mediaType, map<string|string[]>? 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);
Expand Down Expand Up @@ -222,7 +222,7 @@ public client isolated class LoadBalanceClient {
private isolated function processExecute(string httpVerb, string path, RequestMessage message,
TargetType targetType, string? mediaType, map<string|string[]>? 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);
Expand Down
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit 8736602

Please sign in to comment.