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), orsurfwill NOT work in Golem. These crates depend on native networking (tokio, OpenSSL, etc.) which is not available in WebAssembly. Usewstd::httpor 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 bodiesGET Request
let request = Request::get("https://api.example.com/data")
.header("Accept", HeaderValue::from_static("application/json"))
.body(Body::empty())?;
let mut response = Client::new().send(request).await?;
let body_bytes = response.body_mut().contents().await?;
let body_str = String::from_utf8_lossy(body_bytes);Important: GET requests require an explicit empty body — use
Body::empty()orBody::from(()).
GET with JSON Response
#[derive(Deserialize)]
struct ApiResponse {
id: u64,
name: String,
}
let request = Request::get("https://api.example.com/users/1")
.body(Body::empty())?;
let mut response = Client::new().send(request).await?;
let user: ApiResponse = response.body_mut().json::<ApiResponse>().await?;POST with JSON Body
#[derive(Serialize)]
struct CreateUser {
name: String,
email: String,
}
let payload = CreateUser {
name: "Alice".to_string(),
email: "alice@example.com".to_string(),
};
// Body::from_json serializes the payload to JSON bytes.
// You must also set the Content-Type header manually.
let request = Request::post("https://api.example.com/users")
.header("Content-Type", "application/json")
.body(Body::from_json(&payload).expect("Failed to serialize"))?;
let mut response = Client::new().send(request).await?;POST with Raw Body
let request = Request::post("https://api.example.com/submit")
.header("Content-Type", "application/json")
.body(Body::from(r#"{"key": "value"}"#))?;
let mut response = Client::new().send(request).await?;Body::from converts from &str, String, Vec<u8>, &[u8], and () (empty body).
Setting Headers
let request = Request::get("https://api.example.com/secure")
.header("Authorization", HeaderValue::from_static("Bearer my-token"))
.header("Accept", "application/json") // &str works directly
.header("X-Custom", HeaderValue::from_str("dynamic-value")?) // fallible for runtime values
.body(Body::empty())?;Reading the Response
let mut response = Client::new().send(request).await?;
// Status
let status = response.status(); // e.g. StatusCode::OK (200)
// Headers
if let Some(ct) = response.headers().get("Content-Type") {
println!("Content-Type: {}", ct.to_str()?);
}
// Body — choose one:
let bytes = response.body_mut().contents().await?; // &[u8]
// or
let text = response.body_mut().str_contents().await?; // &str
// or (with json feature)
let parsed: MyStruct = response.body_mut().json::<MyStruct>().await?;Error Handling
let request = Request::get(url).body(Body::empty())?;
match Client::new().send(request).await {
Ok(mut response) => {
if response.status().is_success() {
let data: MyData = response.body_mut().json().await?;
Ok(data)
} else {
let error_body = String::from_utf8_lossy(
response.body_mut().contents().await?
).to_string();
Err(format!("API error {}: {}", response.status(), error_body))
}
}
Err(e) => Err(format!("Request failed: {}", e)),
}Timeouts
use wstd::http::Client;
use std::time::Duration;
let mut client = Client::new();
client.set_connect_timeout(Duration::from_secs(5));
client.set_first_byte_timeout(Duration::from_secs(10));
client.set_between_bytes_timeout(Duration::from_secs(30));
let response = client.send(request).await?;Complete Example in an Agent
use golem_rust::{agent_definition, agent_implementation, endpoint, Schema};
use serde::Deserialize;
use wstd::http::{Client, Body, HeaderValue, Request};
#[derive(Clone, Schema, Deserialize)]
pub struct WeatherReport {
pub temperature: f64,
pub description: String,
}
#[agent_definition(mount = "/weather/{city}")]
pub trait WeatherAgent {
fn new(city: String) -> Self;
#[endpoint(get = "/current")]
async fn get_current(&self) -> WeatherReport;
}
struct WeatherAgentImpl {
city: String,
}
#[agent_implementation]
impl WeatherAgent for WeatherAgentImpl {
fn new(city: String) -> Self {
Self { city }
}
async fn get_current(&self) -> WeatherReport {
let url = format!(
"https://api.weather.example.com/current?city={}",
&self.city
);
let request = Request::get(&url)
.header("Accept", HeaderValue::from_static("application/json"))
.body(Body::empty())
.expect("Failed to build request");
let mut response = Client::new()
.send(request)
.await
.expect("Request failed");
response
.body_mut()
.json::<WeatherReport>()
.await
.expect("Failed to parse response")
}
}Alternative: golem-wasi-http
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};
let client = Client::builder()
.default_headers(my_headers)
.connect_timeout(Duration::from_secs(5))
.build()
.unwrap();
// GET with auth
let response = client
.get("https://api.example.com/data")
.bearer_auth("my-token")
.send()
.await
.unwrap();
let data: MyData = response.json().await.unwrap();
// POST with JSON + query params
let response = client
.post("https://api.example.com/users")
.json(&payload)
.query(&[("format", "full")])
.send()
.await
.unwrap();
// Multipart form upload (feature = "multipart")
let form = golem_wasi_http::multipart::Form::new()
.text("name", "file.txt")
.file("upload", path)?;
let response = 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() - Multipart form-data uploads (with
multipartfeature) - Response charset decoding (
.text()with automatic charset sniffing) .error_for_status()to convert 4xx/5xx into errorsCustomRequestExecutionfor 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 WASIInputStream
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.
For example, given this endpoint definition:
#[endpoint(post = "/record")]
fn record(&mut self, body: String);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 names
use serde::Serialize;
#[derive(Serialize)]
struct RecordRequest {
body: String,
}
let request = 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 string
let request = 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 mapping
let request = 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/jsonwith parameter names as JSON keys. See thegolem-http-params-rustskill for the full body mapping rules.
Key Constraints
- Use
wstd::http(async) orgolem-wasi-http(reqwest-like; sync by default, async with theasyncfeature) — 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 explicitbody(Body::empty())call - When using
wstd::http: useBody::from_json(&data)to serialize a struct as a JSON request body, and set theContent-Type: application/jsonheader manually - When using
wstd::http: useresponse.body_mut().json::<T>()to deserialize a JSON response body wstdis included by default in Golem Rust project templates;golem-wasi-httpmust be added manually