HTTP Deep-Dive With Ballerina: Client Communication
This article contains an in-depth journey of HTTP communication features using the Ballerina programming language.
Join the DZone community and get the full member experience.
Join For FreeIntroduction
In this article, we’re going to cover HTTP request and response processing using the Ballerina programming language. This will take an in-depth look at how HTTP clients are created, and how its functionality is used effectively.
Creating HTTP Clients
An HTTP client in Ballerina is created by instantiating an http:Client object while providing the host and client configuration. The client configuration has a default value of {}, and the default values of its fields can be found at the http:ClientConfiguration record definition.
http:Client clientEp = new("http://example.com");
The “clientEp” instance above now represents a client to the “example.com” host through the HTTP protocol. In the default configuration, a connection pooling configuration is attached by default, which means, a single client object is backed by a pool of network connections to the host.
xxxxxxxxxx
http:Client clientEp = new("http://example.com", {
poolConfig: {
maxActiveConnections: 100, // default -1
maxIdleConnections: 10, // default 100
waitTimeInMillis: 10000 // default 30000
}
});
The code above creates a client by providing an explicit client configuration, which provides the connection pooling configuration.
Basic HTTP Requests
After we have created an HTTP client object, we can now execute HTTP requests through the remote methods that are available. Let’s take a look at some of the most often used remote methods in the HTTP client object.
GET
An HTTP GET request is executed by using the get remote method in the HTTP client. This remote method takes in the request path as the first parameter, and the target type as the second parameter for data binding operations. The default value of the target type is http:Response. The remote method returns a union type consisting of http:Response, http:Payload, and error.
The following shows an example of its usage.
xxxxxxxxxx
import ballerina/io;
import ballerina/http;
public function main() returns @tainted error? {
http:Client clientEp = new("http://httpbin.org");
http:Response resp = <http:Response> check clientEp->get("/get");
io:println("Content Type: ", resp.getContentType());
io:println("Payload: ", resp.getJsonPayload());
io:println("Status Code: ", resp.statusCode);
io:println("Header [date]: ", resp.getHeader("date"));
}
xxxxxxxxxx
$ ballerina run client_demo.bal
Content Type: application/json
Payload: {"args":{},"headers":{"Host":"httpbin.org","User-Agent":"ballerina","X-Amzn-Trace-Id":"Root=1-5fd3b719-0d5a1625098ad73b53c0c094"},"origin":"45.30.94.9","url":"http://httpbin.org/get"}
Status Code: 200
Header [date]: Fri, 11 Dec 2020 18:14:49 GMT
The http:Response object can be used to access information such as the client response payload, content type, headers, and cookies.
POST
An HTTP POST is executed using the post remote method in the HTTP client. Here, we can provide the request path as the first parameter. The second parameter is a value of http:RequestMessage, which is a union type of http:Request and other data binding types such as XML, JSON, and other custom record types. The third parameter is the target type for providing the response data binding type, similar to the result of the HTTP GET functionality. The default value of the target type is http:Response.
xxxxxxxxxx
import ballerina/io;
import ballerina/http;
public function main() returns @tainted error? {
http:Client clientEp = new ("http://httpbin.org");
http:Request req = new;
req.setTextPayload("Hello!");
req.setHeader("x-user", "Jack");
http:Response resp = <http:Response> check clientEp->post("/post",
req);
io:println("Payload: ", resp.getJsonPayload());
}
xxxxxxxxxx
$ ballerina run client_demo.bal
Payload: {"args":{},"data":"Hello!","files":{},"form":{},"headers":{"Content-Length":"6","Content-Type":"text/plain","Host":"httpbin.org","User-Agent":"ballerina","X-Amzn-Trace-Id":"Root=1-5fd3b957-4110242263315d0a3fa66dcc","X-User":"Jack"},"json":null,"origin":"45.30.94.9","url":"http://httpbin.org/post"}
EXECUTE
Similar to get and post remote methods, there are other methods such as put, delete, patch, head, and options to represent the HTTP methods. There is also a generic execute remote method for users to specify the HTTP verb and execute the HTTP action.
Multipart Message Handling
HTTP multipart messages can be created by using the Multipurpose Internet Mail Extensions (MIME) standard. Using the http:Request object, we can provide MIME entity values to create single or multi-part HTTP messages.
A MIME entity in Ballerina is represented using the mime:Entity object. Let’s take a look at how we can use a MIME entity in setting a text payload in an HTTP request.
xxxxxxxxxx
import ballerina/http;
import ballerina/mime;
import ballerina/io;
public function main() returns @tainted error? {
http:Client clientEp = new("http://httpbin.org");
http:Request req = new;
mime:Entity entity = new;
entity.setText("Hello!", "text/plain");
req.setEntity(entity);
http:Response resp = <http:Response> check clientEp->post(
"/post", req);
io:println(resp.getTextPayload());
}
xxxxxxxxxx
$ ballerina run client_demo.bal
{
"args": {},
"data": "Hello!",
"files": {},
"form": {},
"headers": {
"Content-Length": "6",
"Content-Type": "text/plain",
"Host": "httpbin.org",
"User-Agent": "ballerina",
"X-Amzn-Trace-Id": "Root=1-5fd1da83-22ce662a2b5e80ac0f9cb5f0"
},
"json": null,
"origin": "45.30.94.9",
"url": "http://httpbin.org/post"
}
The code above explicitly creates the MIME entity and sets it in the HTTP request. The same operation happens if we use the setTextPayload method in the http:Request object. These functions are effectively helper functions to set the MIME entities in the HTTP request for often-used content types.
The mime:Entity object contains functions for setting the body with other data types, such as binary, XML, and JSON, as well.
A multipart message can be created by setting the body parts in the mime:Entity object using the setBodyParts method. This method takes in an array of mime:Entity objects, and also optionally the content type of the enclosing entity, where the default is set to multipart/form-data. If required, we can override this with other multipart values such as multipart/mixed, multipart/alternative, and multipart/related.
The following example shows how a multipart/mixed message is created using plain text content and an image file as an attachment.
xxxxxxxxxx
import ballerina/http;
import ballerina/mime;
import ballerina/io;
public function main() returns @tainted error? {
http:Client clientEp = new("http://httpbin.org");
http:Request req = new;
mime:Entity mpEntity = new;
mime:Entity textEntity = new;
textEntity.setText("Hello!");
mime:Entity imageEntity = new;
imageEntity.setByteArray(<@untainted> check io:fileReadBytes(
"/home/laf/input.jpeg"), "image/jpg");
mime:ContentDisposition contentDisp = new;
contentDisp.disposition = "attachment";
contentDisp.fileName = "input.jpeg";
imageEntity.setContentDisposition(contentDisp);
mpEntity.setBodyParts([textEntity, imageEntity], mime:MULTIPART_MIXED);
req.setEntity(mpEntity);
http:Response resp = <http:Response> check clientEp->post(
"/post", req);
io:println(resp.getTextPayload());
}
Here, the setContentDisposition method in the mime:Entity object is used to set the content disposition of the entity. This provides information on how the recipient should handle the data. For example, if it should be displayed inline, treated as form data, or downloaded as an attachment.
Similar to how we work with MIME entities in HTTP requests, the HTTP response entities can also be processed using the getEntity method in the http:Response object.
Example usage of this is shown below.
xxxxxxxxxx
import ballerina/http;
import ballerina/mime;
import ballerina/io;
public function main() returns @tainted error? {
http:Client clientEp = new("http://httpbin.org");
http:Response resp = <http:Response> check clientEp->get(
"/image/jpeg");
mime:Entity entity = check resp.getEntity();
io:println("Content Type: ", entity.getContentType());
io:println("Content Length: ", entity.getContentLength());
}
xxxxxxxxxx
$ ballerina run client_demo.bal
Content Type: image/jpeg
Content Length: 35588
Data Binding
In our earlier HTTP GET scenarios, we used the default value of the target type parameter in the get remote method of http:Client, which is http:Response. We can also pass in the types string, json, xml, map<json>, byte[], and custom record and record array types to perform automatic data binding with the returned payload.
In the data binding scenario, any HTTP response that returns status codes 4xx or 5xx are considered error situations, and thus, the get remote method will return the error value of type http:ClientError.
Here’s an example of using JSON and XML data binding.
xxxxxxxxxx
import ballerina/http;
import ballerina/io;
public function main() returns @tainted error? {
http:Client clientEp = new("https://freegeoip.app");
json jp = <json> check clientEp->get("/json/", targetType = json);
io:println("JSON Payload:\n", jp, "\n");
xml xp = <xml> check clientEp->get("/xml/", targetType = xml);
io:println("XML Payload:\n", xp);
}
xxxxxxxxxx
$ ballerina run client_demo.bal
JSON Payload:
{"ip":"45.30.94.9","country_code":"US","country_name":"United States","region_code":"CA","region_name":"California","city":"San Jose","zip_code":"95134","time_zone":"America/Los_Angeles","latitude":37.4073,"longitude":-121.939,"metro_code":807}
XML Payload:
<Response>
<IP>45.30.94.9</IP>
<CountryCode>US</CountryCode>
<CountryName>United States</CountryName>
<RegionCode>CA</RegionCode>
<RegionName>California</RegionName>
<City>San Jose</City>
<ZipCode>95134</ZipCode>
<TimeZone>America/Los_Angeles</TimeZone>
<Latitude>37.4073</Latitude>
<Longitude>-121.939</Longitude>
<MetroCode>807</MetroCode>
</Response>
Similarly, the following example demonstrates the usage of a custom record type in data binding.
xxxxxxxxxx
import ballerina/http;
import ballerina/io;
type Location record {
string ip;
string country_code;
string country_name;
string region_code;
string region_name;
string city;
string zip_code;
string time_zone;
float latitude;
float longitude;
int metro_code;
};
public function main() returns @tainted error? {
http:Client clientEp = new("https://freegeoip.app");
Location loc = <Location> check clientEp->get("/json/",
targetType = Location);
io:println("IP: ", loc.ip);
io:println("Latitude: ", loc.latitude);
io:println("Longitude: ", loc.longitude);
io:println("City/State/Country: ", string `${loc.city}, ${loc.region_code}, ${loc.country_name}`);
}
xxxxxxxxxx
$ ballerina run client_demo.bal
Compiling source
client_demo.bal
Running executable
IP: 45.30.94.9
Latitude: 37.4073
Longitude: -121.939
City/State/Country: San Jose, CA, United States
In the record data binding scenario, the field names must match the fields in the returning JSON payload.
Data Streaming
HTTP data streaming can be attained using chunked transfer encoding. In Ballerina, the clients automatically switch between chunked or non-chunked mode based on the size of the content provided as the payload. This is controlled from the http:ClientConfiguration object’s http1Settings.chunking property, which has a default value of AUTO. The fully supported modes are as follows:
- AUTO: If the payload is less than 8KB, the client will not use chunking. It will load the full content to the memory, set the “Content-Length” header with the content size, and send out the request. Otherwise, it will use chunking to stream the data to the remote endpoint.
- ALWAYS: The client will always use chunking to stream the payload to the remote endpoint.
- NEVER: The client will never use chunking, and it will fully read in the payload to memory and send out the request.
To effectively use the HTTP streaming feature, we need to create an HTTP request with a streaming input data channel. For example, if we want to stream the content of a large file to a remote endpoint, but we read its content using a function such as io:fileReadBytes to read in the full content as a byte array to memory, then we lose the benefit of streaming the data. So in this case, we should use a streaming data channel, by using an API such as io:openReadableFile, which returns a ReadableByteChannel. This streaming data channel can be used in places that accept streaming channels, such as the http:Request object’s setByteChannel.
The following example shows a scenario of opening a file with a streaming data channel and using it to create an HTTP request to stream its data to a remote endpoint.
xxxxxxxxxx
import ballerina/http;
import ballerina/io;
public function main() returns @tainted error? {
http:Client clientEp = new("http://httpbin.org");
http:Request req = new;
req.setByteChannel(check io:openReadableFile("/home/laf/input.jpeg"));
http:Response resp = <http:Response> check clientEp->post("/post",
req);
io:println(resp.getTextPayload());
}
In the code above, since we are using the default HTTP client configurations, if the input file is larger than 8KB, it will automatically stream the content to the remote endpoint using chunked transfer encoding.
Communication Resiliency
The HTTP client supports multiple communication resiliency options out of the box. These features allow us to handle and recover from unexpected communication scenarios gracefully.
Retry
The HTTP client can be configured with a retry configuration using the “retryConfig” property in the HTTP client configuration to retry sending the same request to the endpoint in the case of a failure. This follows an exponential backoff algorithm to execute the retry requests.
The example below shows an HTTP client configured with a retry configuration.
xxxxxxxxxx
import ballerina/io;
import ballerina/http;
public function main() returns @tainted error? {
http:Client clientEp = new ("http://httpbin.org", {
retryConfig: {
intervalInMillis: 3000,
count: 5,
backOffFactor: 2.0,
maxWaitIntervalInMillis: 20000
}});
http:Request req = new;
http:Response resp = <http:Response> check clientEp->get("/get");
io:println(resp.getTextPayload());
}
Here, the client is configured to have three retries in the case of request failure, with an initial 3,000 milliseconds retry interval. This interval is multiplied by two with each retry, so the second and third retries will have 6,000 and 12,000-millisecond intervals respectively. Also, it provides 20,000 milliseconds as the maximum value the retry interval will increase to. So in this scenario, the fourth and fifth retry intervals will be restricted to 20,000 milliseconds.
Circuit Breaker
The circuit breaker pattern is used to handle temporary faults when communicating across the network. If a remote endpoint is not available due to a high service load or a network failure, our application may be repeatedly trying to communicate with this service, waiting till it returns a successful result. If the requests keep failing continuously and adding more stress to the backend system, it is not a desirable state for our system. We should rather fail-fast and handle the error from the application. This will make sure the caller is not wasting too many resources by waiting for request timeouts and so on, thus holding back a chain of network calls, which in-turn may be holding up resources such as execution threads and network connections.
As a solution for the above scenario, we can have an intermediary between the service client and the backend service that acts as a circuit breaker. In this manner, when the backend service is healthy, the requests originated from the client go through the circuit breaker and the backend service will successfully return the response to the client through the circuit breaker. This is called the closed state in the circuit breaker.
Figure 1: Circuit breaker closed state
If the circuit breaker detects that the backend service is repeatedly failing, it can stop forwarding the client requests to the backend service, and fail the requests immediately by returning with a specific error message to the client. In this situation, the circuit breaker is in the open state.
Figure 2: Circuit breaker open state
While the circuit breaker is in the open state, and after a specific timeout since it was in this state, the circuit breaker will allow some requests from the client to be passed to the backend service. This is called the half-open state in the circuit breaker. If the requests sent to the backend service are successful in this state, it will go back to the fully closed state, and all requests will flow again to the backend service. If the requests sent to the backend in the half-open state fails, the circuit breaker will again go back to the open state.
Figure 3: Circuit breaker half-open state
The circuit breaker pattern can be used in Ballerina HTTP clients by using its client configuration. This contains the following configuration properties:
- rollingWindow: A rolling window is used to calculate the statistics for backend service errors.
timeWindowInMillis: The size of the rolling time window in milliseconds.
bucketSizeInMillis: The time increment of each rolling window slide. New stats are collected in a bucket of this time duration, this information is added to the current time window at the completion of the bucket time period, and the oldest bucket is removed from the window.
requestVolumeThreshold: The minimum number of requests that should be in the rolling window to trip the circuit.
- failureThreshold: The threshold for request failures, where if the threshold exceeds, the circuit is tripped. This is calculated as the ratio between the failures and the total requests in the current rolling window of requests.
- resetTimeInMillis: The time period in milliseconds to wait in the open state before trying again to contact the backend service.
- statusCodes: The HTTP status codes that are considered as request failures.
The example below shows an HTTP client configured with a circuit breaker.
xxxxxxxxxx
import ballerina/http;
import ballerina/io;
public function main() returns @tainted error? {
http:Client clientEp = new ("http://httpbin.org", {
circuitBreaker: {
rollingWindow: {
timeWindowInMillis: 10000,
bucketSizeInMillis: 2000,
requestVolumeThreshold: 5
},
failureThreshold: 0.2,
resetTimeInMillis: 10000,
statusCodes: [400, 404, 500]
}
}
);
http:Request req = new;
http:Response resp = <http:Response> check clientEp->get("/get");
io:println(resp.getTextPayload());
}
Here, the HTTP client is configured with a circuit breaker configuration that tracks a 10-second rolling window of request statistics and updates its information with 2-second intervals. By including a request volume threshold of 5, any fewer number of requests in the rolling window will not trigger the logic to trip the circuit. Otherwise, in the case of 20% requests failure in the rolling window, the circuit will trip and go into the open state. Now, the client will immediately return errors until 10 seconds, where it will go into a half-open state, and check again if the backend service is responding with successful responses. If the backend request succeeds, the circuit will go back to the closed state and all clients’ requests will be forwarded to the backend service. Or else, it will go back to an open state.
Load Balancing and Failover
In the event of load balancing requests to multiple remote endpoints, Ballerina has the http:LoadBalanceClient to provide a list of endpoints, and optionally an implementation of the algorithm to select the endpoint to distribute the traffic to. The default load balancer rule is to use a round-robin strategy to distribute the load.
The following example shows a scenario of HTTP client-side load balancing.
xxxxxxxxxx
import ballerina/http;
import ballerina/io;
public function main() returns @tainted error? {
http:LoadBalanceClient clientEp = new ({
targets: [{url: "http://httpbin.org"},
{url: "http://httpbin.com"},
{url: "http://httpbin.io"}]
});
http:Request req = new;
http:Response resp = <http:Response> check clientEp->get("/get");
io:println(resp.getTextPayload());
}
Here, the three hosts configured using the “targets” property provide the list of base URLs used for the load balancing requests. For more detailed configuration options, check http:LoadBalanceClientConfiguration.
Similarly, Ballerina supports fail-over scenarios using http:FailoverClient. Here, a list of target URLs can be provided to attempt requests in order, where in the case of failure, it will move on to the next available URL in the list for retrying the request. The following example shows this in action.
x
import ballerina/io;
import ballerina/http;
public function main() returns @tainted error? {
http:FailoverClient clientEp = new ({
targets: [{url: "http://localhost:8080"},
{url: "http://httpbin.org"},
{url: "http://httpbin.com"}]
});
http:Request req = new;
http:Response resp = <http:Response> check clientEp->get("/get");
io:println(resp.getTextPayload());
}
For more detailed configuration options for the failover client, check http:FailoverClientConfiguration.
Secure Communication
Ballerina’s HTTP client supports numerous secure communication features such as Transport Level Security (TLS) and mutual authentication.
TLS
The TLS features are used with the HTTP client by using the “https” protocol in the endpoint URL. Here, we can optionally provide the information on the trust store location to use for validating the server certificates received when creating an HTTP connection over TLS. This is provided using the “secureSocket” property in the HTTP client configuration.
The following example shows a scenario of communicating with an HTTPS endpoint.
xxxxxxxxxx
import ballerina/io;
import ballerina/http;
import ballerina/config;
public function main() returns @tainted error? {
http:Client clientEp = new ("https://httpbin.org");
http:Response resp = <http:Response> check clientEp->get("/get");
io:println("Payload: ", resp.getJsonPayload());
}
xxxxxxxxxx
$ ballerina run client_demo.bal --b7a.home=`ballerina home`
Payload: {"args":{},"headers":{"Host":"httpbin.org","User-Agent":"ballerina","X-Amzn-Trace-Id":"Root=1-5fd3bed7-6b8c04e179f3e1022231d67a"},"origin":"45.30.94.9","url":"https://httpbin.org/get"}
A custom trust store configuration can be created and provided to the client in the following manner.
xxxxxxxxxx
import ballerina/io;
import ballerina/http;
import ballerina/config;
public function main() returns @tainted error? {
http:ClientConfiguration clientEpConfig = {
secureSocket: {
trustStore: {
path: config:getAsString("b7a.home") +
"/bre/security/ballerinaTruststore.p12",
password: "ballerina"
}
}
};
http:Client clientEp = new ("https://httpbin.org", clientEpConfig);
http:Response resp = <http:Response> check clientEp->get("/get");
io:println("Payload: ", resp.getTextPayload());
}
xxxxxxxxxx
$ ballerina run client_demo.bal --b7a.home=`ballerina home`
Compiling source
client_demo.bal
Running executable
Payload: {
"args": {},
"headers": {
"Host": "httpbin.org",
"User-Agent": "ballerina",
"X-Amzn-Trace-Id": "Root=1-5fd3c1dc-0f5b5c3809c89dca2044ef70"
},
"origin": "45.30.94.9",
"url": "https://httpbin.org/get"
}
Mutual Authentication
In our general TLS case, the server is authenticated using the certificate provided to the client, and the secure communication is started based on this information. In the mutual SSL scenario, the client also gets the chance to authenticate itself with the remote server. This is done by additionally providing a key store (“secureSocket.keyStore” property in the HTTP client configuration), which will contain our private key and the certificates used to authenticate ourselves to the remote server.
The example below shows an HTTP client configured for mutual authentication.
xxxxxxxxxx
import ballerina/io;
import ballerina/http;
import ballerina/config;
public function main() returns @tainted error? {
http:ClientConfiguration clientEpConfig = {
secureSocket: {
trustStore: {
path: config:getAsString("b7a.home") +
"/bre/security/ballerinaTruststore.p12",
password: "ballerina"
},
keyStore: {
path: config:getAsString("b7a.home") +
"/bre/security/ballerinaKeystore.p12",
password: "ballerina"
}
}
};
http:Client clientEp = new ("https://httpbin.org", clientEpConfig);
http:Response resp = <http:Response> check clientEp->get("/get");
io:println("Payload: ", resp.getTextPayload());
}
More information on Ballerina’s authentication/authorization features can be found in the Ballerina auth module.
Summary
In this article, we took a look at the HTTP client communication features available in the Ballerina programming language. It provides a feature-complete client functionality that can be used to implement any required scenario. For more in-depth information on the HTTP APIs, check out the Ballerina API documentation for the HTTP module.
Also, quick examples of Ballerina features can be found in Ballerina by Example.
Opinions expressed by DZone contributors are their own.
Comments