Making outgoing HTTP requests from a Rust Golem agent. Use when the user asks to call an external API, make HTTP requests, use an HTTP client, or send HTTP requests from agent code.
Making outgoing HTTP requests from a Rust Golem agent. Use when the user asks to call an external API, make HTTP requests, use an HTTP client, or send HTTP requests from agent code.
Making Outgoing HTTP Requests (Rust)
Overview
Golem Rust agents use the wstd crate for outgoing HTTP requests. wstd provides an async HTTP client built on WASI HTTP — it is included by default in every Golem Rust component's Cargo.toml.
⚠️ WARNING: Third-party HTTP client crates like reqwest, ureq, hyper (client), or surf will NOT work in Golem. These crates depend on native networking (tokio, OpenSSL, etc.) which is not available in WebAssembly. Use wstd::http or another crate that targets the WASI HTTP interface.
Imports
use wstd::http::{Client, Request, Body, HeaderValue};
For JSON support (enabled by default via the json feature):
use serde::{Serialize, Deserialize};
// Body::from_json(&T) for serializing request bodies// response.body_mut().json::<T>() for deserializing response bodies
wstd::http resolves successfully when the server returns an HTTP response, even if the status
is 500. From Golem's durability point of view, that means the recorded oplog entry already
contains the 500 response, and any subsequent application-level Err/? you produce based on
the status is a separate failure after the side effect.
There are two ways to make Golem retry the HTTP call itself when a non-2xx response arrives.
Preferred: a status-code-keyed retry policy
Define a named retry policy whose predicate explicitly references status-code. When the
response arrives the host transparently re-sends the request — no with_atomic_operation and no
application-level error needed. The resolved Response is the last attempt. See the
golem-retry-policies-rust skill for the policy YAML / SDK shape.
POST/PUT/PATCH work out of the box. Idempotence mode defaults to true, so a plain POST /charge with a body is already eligible for status-code retry — there is no need to wrap
the call in with_idempotence_mode(true, ...). See golem-atomic-block-rust for details on
when (rarely) to opt out with with_idempotence_mode(false, ...).
Minimal POST example — the policy is defined once (in golem.yaml or via the SDK) and the call
site is just a plain HTTP call:
// Plain POST — no with_atomic_operation, no with_idempotence_mode wrapper.letrequest = Request::post("https://payments.example.com/charge")
.header("Content-Type", "application/json")
.body(Body::from_json(&ChargeRequest { order_id, amount })?)?;
letmut response = Client::new().send(request).await?;
if !response.status().is_success() {
// The host already retried up to maxRetries times against the 5xx policy;// this is the *final* response after retries were exhausted.returnErr(format!("charge failed: {}", response.status()));
}
letcharge: ChargeResponse = response.body_mut().json().await?;
If status retry "doesn't seem to work", load the golem-local-dev-server skill and look for the
HTTP status retry skipped, reason: … debug line — it pinpoints why a particular request was
not retried (e.g. BodyNotFinished, NotIdempotent, NoRetry).
Fallback: explicit error inspection
When the retry trigger is application-level (e.g. retrying based on a parsed body field, or
combining multiple side effects into one logical step), inspect the response and convert non-2xx
into an Err. Wrap in with_atomic_operation (see golem-atomic-block-rust) if you need recovery
to re-execute the whole request rather than replay the recorded failed response.
The golem-wasi-http crate provides a reqwest-inspired API on top of the same WASI HTTP interface, with additional convenience features. Use it with the async and json features:
[dependencies]golem-wasi-http = { version = "0.2.0", features = ["async", "json"] }
use golem_wasi_http::{Client, Response};
letclient = Client::builder()
.default_headers(my_headers)
.connect_timeout(Duration::from_secs(5))
.build()
.unwrap();
// GET with authletresponse = client
.get("https://api.example.com/data")
.bearer_auth("my-token")
.send()
.await
.unwrap();
letdata: MyData = response.json().await.unwrap();
// POST with JSON + query paramsletresponse = client
.post("https://api.example.com/users")
.json(&payload)
.query(&[("format", "full")])
.send()
.await
.unwrap();
// Multipart form upload (feature = "multipart")letform = golem_wasi_http::multipart::Form::new()
.text("name", "file.txt")
.file("upload", path)?;
letresponse = client.post(url).multipart(form).send().await?;
What golem-wasi-http adds over wstd::http:
Reqwest-style builder API — .get(), .post(), .bearer_auth(), .basic_auth(), .query(), .form()
Response charset decoding (.text() with automatic charset sniffing)
.error_for_status() to convert 4xx/5xx into errors
CustomRequestExecution for manual control over the WASI HTTP request lifecycle — separate steps for sending the body, firing the request, and receiving the response, useful for streaming large request bodies
Raw stream escape hatch via response.get_raw_input_stream() for direct access to the WASI InputStream
When to use wstd::http (default, recommended):
You are writing new code and want a lightweight, standard async client
Your requests have simple bodies (JSON, strings, bytes)
When to use golem-wasi-http:
You need convenience methods like .bearer_auth(), .query(), .form(), .multipart()
You need streaming request body uploads with manual lifecycle control
You need response charset decoding or .error_for_status()
You are porting code from a reqwest-based codebase
Both crates use the same underlying WASI HTTP interface and work correctly with Golem's durable execution.
Calling Golem Agent HTTP Endpoints
When making HTTP requests to other Golem agent endpoints (or your own), the request body must match the Golem HTTP body mapping convention: non-binary body parameters are always deserialized from a JSON object where each top-level field corresponds to a method parameter name. This is true even when the endpoint has a single body parameter.
The correct HTTP request must send a JSON object with a body field — not a raw text string:
// ✅ CORRECT — use Body::from_json with a struct whose fields match parameter namesuse serde::Serialize;
#[derive(Serialize)]structRecordRequest {
body: String,
}
letrequest = Request::post("http://my-app.localhost:9006/recorder/main/record")
.header("Content-Type", "application/json")
.body(Body::from_json(&RecordRequest { body: "a".to_string() }).unwrap())?;
Client::new().send(request).await?;
// ✅ ALSO CORRECT — inline JSON via raw body stringletrequest = Request::post("http://my-app.localhost:9006/recorder/main/record")
.header("Content-Type", "application/json")
.body(Body::from(r#"{"body": "a"}"#))?;
Client::new().send(request).await?;
// ❌ WRONG — raw text body does NOT match Golem's JSON body mappingletrequest = Request::post("http://my-app.localhost:9006/recorder/main/record")
.header("Content-Type", "text/plain")
.body(Body::from("a"))?;
Rule of thumb: If the target endpoint is a Golem agent, always send application/json with parameter names as JSON keys. Load the golem-http-params-rust skill for the full body mapping rules.
Key Constraints
Use wstd::http (async) or golem-wasi-http (reqwest-like; sync by default, async with the async feature) — both target the WASI HTTP interface
reqwest, ureq, hyper (client), surf, and similar crates will NOT work — they depend on native networking stacks (tokio, OpenSSL) unavailable in WebAssembly
Third-party crates that internally use one of these clients (e.g., many SDK crates) will also fail to compile or run
Any crate that targets the WASI HTTP interface (wasi:http/outgoing-handler) will work
When using wstd::http: GET requests require an explicit body(Body::empty()) call
When using wstd::http: use Body::from_json(&data) to serialize a struct as a JSON request body, and set the Content-Type: application/json header manually
When using wstd::http: use response.body_mut().json::<T>() to deserialize a JSON response body
wstd is included by default in Golem Rust project templates; golem-wasi-http must be added manually