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

headers

Map

Header names to values, sent on the request

basicAuth

Map

username / password for Basic auth

token

String

Sends Authorization: Bearer <token>

form

Map

Sends the body as application/x-www-form-urlencoded

timeout

Number

Total transfer timeout in milliseconds (default 20000)

connectTimeout

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.

Cookies#

Response Set-Cookie headers are collected into a process-wide cookie jar. On later calls, the stored cookies are sent back as a Cookie header — useful for maintaining a server-side session across calls:

// First call receives a Set-Cookie and stores it in the jar
Http.get("https://api.example.com/login", {"form": {"user": "ada"}})

// Subsequent calls automatically send Cookie: <stored cookies>
var profile = Http.get("https://api.example.com/me")

The jar is scoped to the whole process, not per host or per domain. If you need per-host isolation or want to opt out, set an explicit Cookie header in headers — it takes precedence over the jar.

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-Type is JSON.

  • The body string when the status is 2xx and Content-Type is not JSON.

  • null when the status is not 2xx (e.g. 404, 500).

  • false when 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

call(url, opts)

Performs the request. Returns true on transport success

method

Set before call: GET (default), POST, PUT, …

postData

Set before call for the request body

status

HTTP status code of the response

body

Raw response body (string)

headers(name)

A single response header value, lowercased key

headers

Map of all response headers, lowercased

error

Non-zero when the transport failed

errorMessage

Human-readable transport error message (curl string)

Pitfall: call returns true for any HTTP response, including 404 and 500. Check http.status yourself 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; use http.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 Cookie header to override the jar.

  • Request.post(name) on the other side returns null for 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.