MALDA™ Reference Manual

The AI-First Programming Language - Version 1.0.39

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.

Not the same name: 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.
First-read stop. Client calls need a network. That cannot run in the manual snippet harness. Smallest file: 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

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)

30.2.2 Properties

PropertyTypeNotes
baseUrlstringCurrent base URL
timeoutintCurrent timeout in milliseconds

30.2.3 Request methods

MethodArguments
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

30.2.4 Configuration methods

MethodNotes
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:

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:

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:

FieldTypeNotes
statusintHTTP status code
statusTextstringReason phrase ("OK", "Not Found", …)
okboolTrue when the status is 200–299
headersobjectResponse and content headers; multi-values joined with ", "
bodyobject / array / stringParsed 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:

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:

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):

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.

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

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": "..."
}

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