30. REST Web Client
MALDA can call HTTP APIs from the host runtime with RestClient and the one-shot httpGet / httpPost / httpPut / httpDelete / httpPatch functions. This chapter is the client-side reference. Designing and hosting those APIs is 29. REST API Server. Browser fetch against a MALDA API is 31. Full-Stack Development with MALDA.
httpGet(url, ...) is this chapter's HTTP client. webFetch is the host one-shot fetch in 12.9. cap.httpGet(origin) mints an unforgeable GET token for cap.fetch / webFetch — see 12.6.
Examples/Web/http_client_json.malda. Start from 25. Web UI Overview. Course: Learn MALDA — After the core path.
30.1 Chapter Map
- 30.1.1 Concepts primer — HTTP client, status vs transport, Bearer vs Basic
- 30.2
RestClientconstructor, properties, and methods - 30.3 URL resolution, query parameters, and body
- 30.4 Success response vs transport-error object
- 30.5 Static
httpGet/httpPostand friends - 30.6 Authentication
- 30.7 Timeouts
- 30.8 Redirects and retries
- 30.9 CORS: host client vs browser
- 30.10 Interpret vs transpile
- 30.11 Calling a MALDA
RestServer - 30.12 Examples
30.1.1 Concepts primer
This chapter is the caller. Server terms (middleware, CSRF, CORS, JWT) are in 29.1.1 Concepts primer.
An HTTP client builds a request, sends it, and reads the response. RestClient and httpGet run on the MALDA host (not in the browser), so they ignore CORS. JS-transpiled http* uses the page's fetch, which does obey CORS.
Status vs transport: HTTP 404 still produced a response — check response.status / ok. If the TCP/TLS/DNS layer failed, you get a transport-error object (status usually 0, error set). See 30.4.
Bearer vs Basic: Bearer sends a token as-is in the Authorization header. Basic sends username:password Base64-encoded (encoding is not encryption). Neither hides credentials on the wire; use HTTPS. See 30.6.
Idempotent retries: GET and PUT can usually be repeated safely; POST often cannot (it may create a second resource). Retry transport errors and 5xx when the method is idempotent; do not blindly retry 4xx. See 30.8.
30.2 RestClient Class
Create a reusable client when you need a base URL, default headers, auth, or a timeout other than 30 seconds. The host implementation is RestClientInstance (interpret and C# transpile).
30.2.1 Constructor
var client = new RestClient(); // No base URL
var client = new RestClient("https://api.example.com"); // With base URL
var client = new RestClient("https://api.example.com", 60000); // With base URL and timeout (ms)
baseUrl(string, optional): prepended to relative request URLs. Empty when omitted.timeout(int, optional):HttpClient.Timeoutin milliseconds. Default:30000.
30.2.2 Properties
| Property | Type | Notes |
|---|---|---|
baseUrl | string | Current base URL |
timeout | int | Current timeout in milliseconds |
30.2.3 Request methods
| Method | Arguments |
|---|---|
get(url, headers?, queryParams?) | No body |
delete(url, headers?, queryParams?) | No body |
post(url, body?, headers?, queryParams?) | Optional body |
put(url, body?, headers?, queryParams?) | Optional body |
patch(url, body?, headers?, queryParams?) | Optional body |
url(string): relative ifbaseUrlis set, otherwise absolutebody(object / string / other, optional): see 30.3.3. Only sent for POST, PUT, and PATCHheaders(object, optional): extra request headers. String values only. Passnullwhen you need query parameters without headersqueryParams(object, optional): appended as a query string
30.2.4 Configuration methods
| Method | Notes |
|---|---|
setHeader(name, value) | Default header on every later request (both strings) |
setAuth(type, credentials) | "Bearer" or "Basic" only (case-insensitive). Other types are stored and ignored |
setTimeout(ms) | Integer milliseconds; updates the underlying HttpClient |
setBaseUrl(url) | Replace the base URL |
There is no setRetry, no redirect flag, and no cookie jar API. See 30.8.
30.3 URL, Query, and Body
30.3.1 URL resolution
If baseUrl is non-empty and the request URL does not start with http:// or https://, the client joins them: trim trailing / from the base, ensure the path starts with /, then concatenate. Absolute http(s) URLs are used as-is.
var client = new RestClient("https://api.example.com/v1");
// "/users" -> https://api.example.com/v1/users
// "users" -> https://api.example.com/v1/users
// "https://other.example/x" stays absolute
30.3.2 Query parameters
Object keys and values are URI-encoded and joined with &. A ? already on the URL becomes & for the extra pairs. Value types:
- string: as-is
- int / float: decimal form
- bool:
true/false(lowercase) - anything else:
AsString()
30.3.3 Body serialization
POST / PUT / PATCH always send StringContent with media type application/json unless you override Content-Type in the per-request headers:
- string body: sent as the raw content (still as JSON content unless you change
Content-Type) - object: JSON object from enumerable keys
- other values:
JsonSerializerof the runtime value (arrays become JSON arrays)
Content-Type is the one header that must go on the content, not on HttpRequestMessage.Headers. Other custom headers are added to the request; a duplicate of a header already set by setAuth or setHeader can fail that add and be skipped.
30.4 Response Format
A completed HTTP exchange (including 4xx / 5xx) returns:
| Field | Type | Notes |
|---|---|---|
status | int | HTTP status code |
statusText | string | Reason phrase ("OK", "Not Found", …) |
ok | bool | True when the status is 200–299 |
headers | object | Response and content headers; multi-values joined with ", " |
body | object / array / string | Parsed JSON when Content-Type contains json and the payload parses; otherwise the raw string |
HTTP error statuses still produce this shape. ok is false, but error is absent. Check status, not only error.
If the request never gets an HTTP response (DNS, connection, timeout, canceled send), the client catches the exception and returns a transport-error object instead:
error(string): exception messageok(bool):falsestatus(int): usually0. A status is only present when anHttpRequestExceptioncarries aStatusCodeinData
Transport-error objects have no body, headers, or statusText. Host header names keep the server's casing; the JS fetch path often lowercases them (30.10).
30.5 Static HTTP Functions
These create a fresh RestClient (empty base URL, 30 s timeout, no default headers, no setAuth) and call the matching method:
httpGet(url, headers?, queryParams?)httpPost(url, body?, headers?, queryParams?)httpPut(url, body?, headers?, queryParams?)httpDelete(url, headers?, queryParams?)httpPatch(url, body?, headers?, queryParams?)
Use them for one-off absolute URLs. Use a RestClient instance when several calls share a host, token, or timeout. On the JS backend they map to mlRuntime.http.get|post|put|delete|patch (fetch) with the same argument order and the same success / transport-error shapes.
Do not confuse these with cap.httpGet. The capability builtin mints a token; it does not perform the request.
30.6 Authentication
setAuth recognizes only two types (case-insensitive):
Bearer:Authorization: Bearer <credentials>as given (no extra encoding)Basic: UTF-8 bytes of the credentials string, then Base64. Pass"username:password"; do not Base64 it yourself
var client = new RestClient("https://api.example.com");
client.setAuth("Bearer", "your-token-here");
var response = client.get("/protected-endpoint");
var client = new RestClient("https://api.example.com");
client.setAuth("Basic", "username:password");
var response = client.get("/protected-endpoint");
Any other type is stored and never applied. For API keys or custom schemes, pass a per-request header and do not also call setAuth with a dummy type:
var headers = {"Authorization": "Custom token123"};
var response = httpGet("https://api.example.com/endpoint", headers);
Default headers from setHeader are applied before setAuth and before per-request headers.
30.7 Timeouts
On the host, the timeout is HttpClient.Timeout (constructor or setTimeout). When it elapses, you get a transport-error object: ok is false, status is 0, and error is the exception message (typically mentions timeout or cancellation). The one-shot http* functions always use the 30 s default.
// Set timeout to 60 seconds
var client = new RestClient("https://api.example.com", 60000);
client.setTimeout(10000); // Change to 10 seconds
var response = client.get("/slow-endpoint");
if (!response.ok && response.status == 0) {
print("Transport error: " + response.error);
}
The JS mlRuntime.http.* helpers do not set a fetch abort timeout. Bound a JS call with @within(ms) if you need a deadline there.
30.8 Redirects and Retries
Neither surface exposes a redirect or retry API.
- Redirects: host
HttpClientfollows redirects by default (handler auto-redirect, typical max 50). JSfetchuses the defaultredirect: "follow". You cannot turn this off from MALDA. Thestatusyou see is the final response after the hop chain, not the 3xx. - Retries: not built in. Loop in your program. Retry transport errors (
status == 0) and 5xx if the API is idempotent; do not blindly retry 4xx.
function getWithRetry(client, url, attempts) {
var i = 0;
var last = null;
while (i < attempts) {
last = client.get(url);
if (last.ok) {
return last;
}
if (last.status > 0 && last.status < 500) {
return last;
}
i = i + 1;
sleep(200);
}
return last;
}
30.9 CORS: Host vs Browser
RestClient and interpreted / C# http* run on the host. They are not a browser, so the CORS algorithm does not apply. Enable CORS on the server you call only if browsers will call it — see 29. REST API Server §29.12.
JS-transpiled httpGet / httpPost use fetch in the page. Cross-origin calls then need the remote API to allow your origin, method, and headers. Same-origin calls to a mounted MALDA API on the same host (chapter 31) do not need CORS.
Server-side enableCORS does not change how RestClient sends requests.
30.10 Interpret vs Transpile
- Interpret and C# transpile:
new RestClientandhttp*shareRestClientInstance. Calls are synchronous (HttpClient.Send). Default headers,setAuth,setTimeout, andsetBaseUrlwork. - JS transpile:
httpGet/httpPost/httpPut/httpDelete/httpPatchbecomemlRuntime.http.*(fetch, async). There is noRestClientclass in the JS runtime. Keep instance configuration on the host /@server()side, or pass headers on eachhttp*call from@client()code. - Header names: host responses keep the server's casing (
Content-Typeis common). JSfetchoften lowercases keys. Do not assume one spelling on both backends. - Timeout: host honors
RestClienttimeout; JShttp*does not (use@within).
30.11 Calling a MALDA RestServer
A 2xx from RestClient means the HTTP layer succeeded. The JSON body may still be a MALDA framework envelope from chapter 29:
{
"status": 401,
"error": "InvalidToken",
"message": "Invalid token signature.",
"correlationId": "..."
}
- Read
response.body.error/response.body.message/response.body.correlationIdon failures. The same id is onX-Correlation-ID. - Validation failures include
details:{location, field, message}. - If the API has
enableCsrf, mutating calls need the CSRF cookie plusX-CSRF-Token(or your configured header). See 29.9. - Do not treat a returned
{"status": 200, "data": ...}application envelope as the HTTP status. HTTP status isresponse.status.
Loopback smoke test in-repo: docs/llm/few-shot/13_http_client.malda (local RestServer + httpGet / httpPost).
30.12 Examples
30.12.1 RestClient instance
// Create client with base URL
var client = new RestClient("https://api.example.com");
// Set authentication
client.setAuth("Bearer", "your-token-here");
// GET request
var response = client.get("/users");
if (response.ok) {
print("Users: " + toJSON(response.body));
} else {
print("Error: " + response.error);
}
// GET with query parameters
var response = client.get("/users", null, {"limit": "10", "offset": "0"});
// POST request with JSON body
var body = parseJSON("{\"name\": \"Alice\", \"email\": \"alice@example.com\"}");
var response = client.post("/users", body);
if (response.status == 201) {
print("User created: " + toJSON(response.body));
}
// PUT request
var updateData = parseJSON("{\"name\": \"Alice Updated\"}");
var response = client.put("/users/123", updateData);
// DELETE request
var response = client.delete("/users/123");
30.12.2 Static functions
// Simple GET request
var response = httpGet("https://api.example.com/users");
if (response.ok) {
print("Users: " + toJSON(response.body));
}
// GET with headers
var headers = {"Authorization": "Bearer token123"};
var response = httpGet("https://api.example.com/users", headers);
// GET with query parameters
var response = httpGet("https://api.example.com/users", null, {"limit": "10"});
// POST with JSON body
var body = parseJSON("{\"name\": \"Alice\"}");
var response = httpPost("https://api.example.com/users", body);
// POST with custom headers
var headers = {"Content-Type": "application/json", "X-API-Key": "key123"};
var body = parseJSON("{\"name\": \"Alice\"}");
var response = httpPost("https://api.example.com/users", body, headers);
30.12.3 HTTP vs transport errors
var response = httpGet("https://api.example.com/users/999");
if (!response.ok) {
if (response.status == 404) {
print("User not found");
} else if (response.error != null) {
print("Error: " + response.error);
} else {
print("HTTP Error: " + response.status + " " + response.statusText);
}
}
30.12.4 Response body and headers
var response = httpGet("https://api.example.com/users");
if (response.ok) {
var users = response.body; // Already parsed as object/array if JSON
if (users.length > 0) {
print("First user: " + users[0].name);
}
// Access response headers
var contentType = response.headers["Content-Type"];
print("Content-Type: " + contentType);
}
30.12.5 Walkthrough against a public API
// Create REST client
var client = new RestClient("https://jsonplaceholder.typicode.com");
// GET all posts
var response = client.get("/posts");
if (response.ok) {
var posts = response.body;
print("Total posts: " + posts.length);
// GET specific post
var postResponse = client.get("/posts/1");
if (postResponse.ok) {
var post = postResponse.body;
print("Post title: " + post.title);
}
// POST new post
var newPost = parseJSON("{\"title\": \"My Post\", \"body\": \"Content\", \"userId\": 1}");
var createResponse = client.post("/posts", newPost);
if (createResponse.ok) {
print("Created post ID: " + createResponse.body.id);
}
// PUT update post
var update = parseJSON("{\"title\": \"Updated Title\"}");
var updateResponse = client.put("/posts/1", update);
// DELETE post
var deleteResponse = client.delete("/posts/1");
if (deleteResponse.ok) {
print("Post deleted");
}
}
See Also
- 29. REST API Server - Creating REST API servers
- 26. Web UI Server Components - Web UI + API integration patterns
- 31. Full-Stack Development with MALDA - End-to-end architecture and API calls from UI
- 12. Input/Output — console, files, paths, environment,
cap.httpGet