Managing HTTP Request-Response Cycles in Ballerina
Learn how to manage HTTP request-response cycles in Ballerina, balancing automatic serialization with the manual control of the http:Caller object for custom status codes.
20 Jun 2026, 23:02 UTC

The Problem: Balancing Automation and Control in API Development
When building network services, developers often struggle to balance the speed of automatic serialization with the need for granular control over HTTP status codes and headers. In many languages, this requires switching between high-level frameworks and low-level request/response objects. Ballerina solves this by providing two distinct paths within the same resource function: automatic return-value serialization and explicit response handling via the http:Caller object.
The key takeaway is that returning a value from a resource function is best for standard success paths (200 OK), while using caller->respond() is necessary for custom status codes, headers, or complex error responses.
The Mechanism: Service Resources and the Caller Object
In Ballerina, an http:Service defines the entry point, and resource functions define the specific endpoints. These functions are bound to HTTP methods (GET, POST, etc.) and URI paths.
To handle a request, Ballerina provides an http:Caller object. This object represents the active connection to the client. If you return a record or a JSON value from the function, Ballerina automatically calls respond() for you with a 200 OK status. However, if you call respond() manually, you take full control of the transmission.
Worked Example: User Management Endpoint
The following example demonstrates a service that uses a record for the payload and handles both a successful retrieval and a custom "Not Found" error.
import ballerina/http;
// Define the data structure for the response
type User record {
int id;
string username;
string email;
};
service /users on new http:Listener(8080) {
// Path: /users/{id}
resource function get User([string id]) returns User|http:ByteArray|error {
// Simulated database lookup
if (id == "123") {
// Automatic serialization: returns 200 OK with JSON body
return {id: 123, username: "ballerina_dev", email: "[contact removed]"};
}
// Manual control: return a 404 Not Found
// We use the 'caller' object to specify the status code
http:Caller caller = http:getCaller();
caller->respond({
status: 404,
reason: "User not found"
});
// Return an empty byte array or a specific error to satisfy the return type
return http:ByteArray {};
}
}
Execution and Verification
- Initialize: Run
bal new http-demoin your terminal to create a project structure. - Implement: Replace the contents of
main.balwith the code above. - Run: Execute
bal run. This compiles the code to bytecode and starts the listener on port 8080. - Test Success: Run
curl http://localhost:8080/users/123. You should receive a JSON object and a 200 OK status. - Test Failure: Run
curl -i http://localhost:8080/users/999. You should see a 404 Not Found status.
Handling Failures with the Check Operator
Network calls are inherently unreliable. Ballerina treats errors as first-class values rather than exceptions. When using an http:Client to call another service, the check operator is used to propagate errors upward to the caller.
If a client call fails (e.g., DNS failure or timeout), check ensures the error is returned from the current function immediately, preventing the program from proceeding with null or invalid data. This requires the function signature to include | error in its return type.
Limitations and Common Pitfalls
The Single-Response Constraint
The http:Caller is single-use. Once caller->respond() is invoked, the connection is closed. Attempting to return a value from the function after calling respond() will result in a runtime error because the response has already been sent.
Resource Exhaustion
A common mistake is omitting the response entirely. If a resource function reaches the end of its execution without returning a value or calling respond(), the client will remain hanging until the socket times out. This can quickly exhaust the server's connection pool under high load.
Type Mismatches in Serialization
While Ballerina's json type is flexible, binding payloads to record types is the recommended practice for production. Using generic json types bypasses compile-time type checking, which often leads to runtime errors when the API provider changes the payload structure.
Comparison: Automatic vs. Manual Response
| Feature | Automatic Return | Manual caller->respond() |
|---|---|---|
| Status Code | Fixed (usually 200 OK) | Fully Customizable |
| Headers | Default Content-Type | Custom Headers allowed |
| Complexity | Low (Clean code) | Medium (More boilerplate) |
| Use Case | Standard Data Retrieval | Error states, Redirects, File downloads |
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.