Making HTTP Calls#
Bialet can call other HTTP services from your .wren code. Use this to
integrate third-party APIs, pull remote data, or post webhooks. This page
covers the Http class end to end. For exposing your own API instead, see
Building REST APIs.
The Http class is a thin wrapper around libcurl (POSIX) or a raw sockets
client (Windows). It supports GET, POST, PUT, DELETE, and any other method,
custom headers, Basic and bearer-token auth, form or JSON bodies, per-call
timeouts, and a persistent cookie jar.
Quick Start#
// Simple GET - returns the response body
var ip = Http.get("https://api.ipify.org?format=json")
System.print(ip["ip"])
// Simple POST - data is sent as JSON
var created = Http.post("https://api.example.com/users", {"name": "Ada"})
System.print(created["id"])
The response of a successful (2xx) call is parsed as JSON when the server
responds with a JSON Content-Type, and returned as a plain string otherwise.
The exact rules are covered under Response Handling.
HTTP Methods#
GET#
var users = Http.get("https://api.example.com/users")
// With options
var users = Http.get("https://api.example.com/users?active=1", {
"headers": {"X-API-Key": "your-key"}
})
POST#
Http.post sends the body as JSON (Content-Type: application/json). Pass a
map, a list, or a pre-built JSON string. Use the form option for
application/x-www-form-urlencoded bodies instead (see
Form-Encoded Bodies).
// Map is stringified automatically
Http.post("https://api.example.com/users", {"name": "Ada", "role": "admin"})
// Raw JSON string
Http.post("https://api.example.com/users", Json.stringify({"name": "Ada"}))
// Empty body is allowed
Http.post("https://api.example.com/hooks/trigger")
PUT and DELETE#
Http.put("https://api.example.com/users/1", {"name": "Grace"})
Http.delete("https://api.example.com/users/1")
Other Methods (PATCH, HEAD, …)#
Use Http.request(url, method, data, options) for anything else:
var result = Http.request("https://api.example.com/users/1", "PATCH",
{"name": "Grace"}, {})
Options#
Every shortcut accepts an optional options map:
Key |
Type |
Description |
|---|---|---|
|
Map |
Header names to values, sent on the request |
|
Map |
|
|
String |
Sends |
|
Map |
Sends the body as |
|
Number |
Total transfer timeout in milliseconds (default 20000) |
|
Number |
Connect timeout in milliseconds (default 2000) |
var options = {
"headers": {"User-Agent": "bialet-app", "Accept": "application/json"},
"basicAuth": {"username": "admin", "password": "secret"},
"timeout": 10000,
"connectTimeout": 3000
}
var data = Http.get("https://api.example.com/protected", options)
Content-Type defaults to application/json when you don’t set one in
headers and don’t use the form option. Every other common header goes
through headers directly.
Authentication#
Bearer Token#
Use the token option to send a Authorization: Bearer <token> header:
var zones = Http.get("https://api.cloudflare.com/client/v4/zones",
{"token": Config.get("API_TOKEN")})
This is equivalent to setting the header manually. Use the manual form when you need a non-Bearer scheme or extra headers:
var options = {
"headers": {
"Authorization": "Bearer %(Config.get("API_TOKEN"))",
"Content-Type": "application/json"
}
}
var zones = Http.get("https://api.cloudflare.com/client/v4/zones", options)
HTTP Basic Auth#
var options = {
"basicAuth": {"username": "admin", "password": "secret"}
}
var data = Http.get("https://api.example.com/basic-protected", options)
Bialet base64-encodes the credentials and sends the Authorization: Basic ...
header for you. Do not add a basicAuth entry and an Authorization
header at the same time — the header wins and the credentials are sent twice.
Custom Auth Headers#
Any auth scheme that fits in a header works with plain headers, including
cookies (Cookie), session tokens, and signatures:
var options = {
"headers": {
"Cookie": "session=abc123",
"X-Signature": Util.sha256("payload")
}
}
Form-Encoded Bodies#
Pass a map to the form option to send
application/x-www-form-urlencoded data. Values are URL-encoded
automatically:
var options = {
"form": {"username": "ada", "remember": "on"}
}
var data = Http.post("https://api.example.com/login", {}, options)
The form option overrides both the default JSON body and any Content-Type
header you set. This is the option to reach for when talking to traditional
web forms.
Query Strings#
Use Http.url(base, params) to append URL-encoded query parameters to a URL.
It inserts ? or & as needed:
var url = Http.url("https://api.example.com/search",
{"q": "hello world", "page": 2})
// https://api.example.com/search?q=hello+world&page=2
Http.query(params) returns just the encoded key=value&... string if you
need to build the URL yourself.
Response Handling#
Shortcuts Return Convenience Values#
The static shortcuts (Http.get, Http.post, …) return:
The parsed JSON value when the status is 2xx and
Content-Typeis JSON.The body string when the status is 2xx and
Content-Typeis not JSON.nullwhen the status is not 2xx (e.g. 404, 500).falsewhen the request itself failed (DNS, connection, timeout).
var result = Http.get("https://api.example.com/users")
if (result == false) {
// Network error - DNS, connection refused, timeout, ...
} else if (result == null) {
// Server replied with a non-2xx status
} else {
// Success - JSON or string
}
Full Control with Http.new()#
When you need the status code, response headers, or raw body, build an Http
instance and call call(url, options) directly:
var http = Http.new()
http.method = "GET"
if (http.call("https://api.example.com/users", {})) {
var code = http.status
var body = http.body
var contentType = http.headers("content-type")
System.print("Status: %(code), type: %(contentType)")
} else {
System.print("Call failed with error code %(http.error)")
}
Http.new() exposes:
Member |
Description |
|---|---|
|
Performs the request. Returns |
|
Set before |
|
Set before |
|
HTTP status code of the response |
|
Raw response body (string) |
|
A single response header value, lowercased key |
|
Map of all response headers, lowercased |
|
Non-zero when the transport failed |
|
Human-readable transport error message (curl string) |
Pitfall:
callreturnstruefor any HTTP response, including 404 and 500. Checkhttp.statusyourself when you need to distinguish them.
Pitfall: response headers and their values are lowercased before they are stored.
http.headers("Content-Type")will not find the key; usehttp.headers("content-type").
Response Headers#
Response headers live in the headers map, keyed by lowercased name:
var rateLimit = http.headers("x-ratelimit-remaining")
Example: Cloudflare API Client#
This is a compact real-world client for the Cloudflare v4 API. It shows the core patterns: a bearer token in headers, JSON bodies, and GET/POST/DELETE shortcuts. (Adapted from a live deployment.)
class Cloudflare {
static options {{
"headers": {
"Authorization": "Bearer %( Config.get("CLOUDFLARE_API_TOKEN") )",
"Content-Type": "application/json"
}
}}
static url(path) { "https://api.cloudflare.com/client/v4/%(path)" }
static urlZoneRecords { url("zones/%( Config.get("CLOUDFLARE_ZONE_ID") )/dns_records") }
static listRecords(domain) {
Http.get(urlZoneRecords + "?name=%(domain["fqdn"])", options)
}
static createRecord(domain) {
var data = Json.stringify({
"type": "CNAME",
"name": domain["fqdn"],
"content": domain["dns"],
"ttl": 1,
"proxied": true
})
Http.post(urlZoneRecords, data, options)
}
static deleteRecord(record) { Http.delete("%(urlZoneRecords)/%(record)", options) }
}
Configuration values are read with Config.get instead of being hardcoded.
Store secrets in the Config store, never in the .wren
files themselves.
Error Handling#
Http.error on a manually-built instance is a numeric code; false on the
shortcuts. Http.errorMessage carries the underlying error text (from curl)
for logging and debugging. The transport timeout defaults to 20 seconds total
with 2 seconds to connect — override per call with timeout and
connectTimeout (milliseconds), so a dead service returns an error instead of
hanging your app.
var http = Http.new()
http.method = "GET"
if (!http.call("https://api.example.com/health",
{"timeout": 5000, "connectTimeout": 1000})) {
// http.error is non-zero: DNS, connect, timeout, ...
// http.errorMessage explains why, e.g. "Could not connect to server"
return Response.json({"status": "down", "error": http.error,
"message": http.errorMessage})
}
Pitfalls#
Cookies are process-wide. The jar sends every stored cookie on every call, regardless of host. Scope your calls to trusted hosts, or set an explicit
Cookieheader to override the jar.Request.post(name)on the other side returnsnullfor missing keys — see Building REST APIs when you build the receiving end.Redirects are followed automatically, up to 10 hops.
Response bodies are not size-capped — a malicious endpoint could return an unbounded body. Only call APIs you trust.
Missing Features#
Not every HTTP client feature is implemented yet. Multipart/file uploads and per-host cookie scoping are planned — see the Roadmap.