HTTP Client

Beamtalk's HTTPClient lets you make HTTP requests — both as simple class-level calls and through a configurable actor for repeated requests to the same server.

All request methods return a Result (see chapter 12). Use unwrap to extract the response or isOk / isError to check the outcome.

Quick requests with class methods

The simplest way to make a request — no setup needed:

// GET request
resp := (HTTPClient get: "https://api.example.com/users") unwrap
resp status     // HTTP status code (e.g. 200)
resp ok         // true if status is 200–299
resp body       // response body as String
resp headers    // response headers as a list

POST, PUT, and DELETE work the same way:

// POST with a body
resp := (HTTPClient post: "https://api.example.com/users" body: "data") unwrap

// PUT with a body
resp := (HTTPClient put: "https://api.example.com/users/1" body: "update") unwrap

// DELETE
resp := (HTTPClient delete: "https://api.example.com/users/1") unwrap

The HTTPResponse object

Every successful request returns an HTTPResponse:

resp := (HTTPClient get: url) unwrap
resp status      // → 200 (Integer)
resp ok          // → true (Boolean, true if 200–299)
resp body        // → "..." (String)
resp headers     // → list of header tuples
resp bodyAsJson  // → Dictionary (parsed JSON)

bodyAsJson parses the body as JSON, returning a Dictionary:

resp := (HTTPClient get: "https://api.example.com/users/1") unwrap
data := resp bodyAsJson
data at: #name    // → "Alice"
data at: #email   // → "alice@example.com"

Configurable client actor

For repeated requests to the same server, spawn an HTTPClient actor with a base URL. Paths are appended automatically:

client := HTTPClient spawnWith: #{
  #baseUrl => "https://api.example.com",
  #headers => #(),
  #timeout => 5000
}

// Requests use relative paths
resp := (client get: "/users") unwrap
resp := (client post: "/users" body: payload) unwrap

// Clean up when done
client stop

The actor maintains the base URL and default headers, reducing repetition when making many requests to the same API.

Custom request options

request:url:options: gives full control over headers, body, and timeout:

// Set custom headers
resp := (HTTPClient request: #get url: url options: #{
  #headers => #(#("Accept", "application/json"),
                #("Authorization", "Bearer token123"))
}) unwrap

// Set a custom timeout (in milliseconds)
resp := (HTTPClient request: #post url: url options: #{
  #body => payload,
  #timeout => 30000
}) unwrap

Error handling

All methods return Result. Use isOk / isError for safe checking:

result := HTTPClient get: "https://unreachable.example.com"
result isOk     // → false (connection failed)
result isError  // → true

Invalid arguments raise type errors:

// URL must be a String
HTTPClient get: 42           // raises #type_error

// Must use spawn, not new
HTTPClient new               // raises #instantiation_error

Working with JSON APIs

A common pattern: serialize a Dictionary to JSON, POST it, and parse the response:

payload := Json generate: #{#name => "Alice", #age => 30}

resp := (HTTPClient request: #post url: "https://api.example.com/users" options: #{
  #body => payload,
  #headers => #(#("Content-Type", "application/json"))
}) unwrap

user := resp bodyAsJson
user at: #id    // → server-assigned ID

Summary

Class methods (quick requests):

HTTPClient get: url                    → Result<HTTPResponse>
HTTPClient post: url body: string      → Result<HTTPResponse>
HTTPClient put: url body: string       → Result<HTTPResponse>
HTTPClient delete: url                 → Result<HTTPResponse>
HTTPClient request: method url: url options: dict  → Result<HTTPResponse>

Actor methods (configured client):

client := HTTPClient spawnWith: #{#baseUrl => url, #headers => ..., #timeout => ms}
client get: path              → Result<HTTPResponse>
client post: path body: str   → Result<HTTPResponse>
client put: path body: str    → Result<HTTPResponse>
client delete: path           → Result<HTTPResponse>
client stop                   → #ok

HTTPResponse accessors:

resp status      → Integer (HTTP status code)
resp ok          → Boolean (status 200–299)
resp body        → String
resp headers     → List of header tuples
resp bodyAsJson  → Dictionary (parsed JSON)

Options dictionary keys:

#body     → String (request body)
#headers  → List of #(name, value) tuples
#timeout  → Integer (milliseconds)

Exercises

1. Check a response. Describe how you would make a GET request to a URL and check whether it succeeded (status 200–299). What method on HTTPResponse tells you this?

Hint
result := HTTPClient get: "https://api.example.com/health"
result isOk ifTrue: [
  resp := result unwrap
  resp ok          // => true if status is 200-299
  resp status      // => the actual status code (e.g. 200)
]

resp ok returns true for 2xx status codes. result isOk tells you the request completed (no network error), while resp ok tells you the server returned a success status.

2. Reusable client. How would you set up an HTTPClient actor for repeated requests to https://api.example.com with a 10-second timeout?

Hint
client := HTTPClient spawnWith: #{
  #baseUrl => "https://api.example.com",
  #headers => #(),
  #timeout => 10000
}
// All requests use relative paths:
resp := (client get: "/users") unwrap
client stop    // clean up when done

3. POST JSON. How would you POST a JSON payload with the correct Content-Type header using request:url:options:?

Hint
payload := Json generate: #{"name" => "Alice", "age" => 30}
resp := (HTTPClient request: #post url: url options: #{
  #body => payload,
  #headers => #(#("Content-Type", "application/json"))
}) unwrap

Use Json generate: to serialize the dictionary, then set the Content-Type header so the server knows to parse it as JSON.

Next: this completes the learning guide! See docs/beamtalk-language-features.md for the full language reference.