HTTP requests
TypeScript
HTTP requests can be made with the standard fetch function.
For example:
const response = await fetch(`http://localhost:${port}/todos`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "foo",
body: "bar",
userId: 1,
}),
});
const data = await response.json();
console.log("Body:", data);Effect TypeScript
Effect TypeScript agents can use the standard fetch function and wrap its Promise in an Effect:
import { Effect } from "effect"
const request = Effect.tryPromise({
try: () => fetch(`http://localhost:${port}/todos`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title: "foo",
body: "bar",
userId: 1,
}),
}),
catch: (cause) => new Error("HTTP request failed", { cause }),
}).pipe(
Effect.flatMap((response) =>
Effect.tryPromise({
try: () => response.json(),
catch: (cause) => new Error("Invalid JSON response", { cause }),
}),
),
)This constructs a lazy Effect. Return it from an agent method, or evaluate it with yield* request inside Effect.gen.
Rust
HTTP requests can be made using the wstd crate. Make sure to have the crate added to your Cargo.toml:
[dependencies]
wstd = { version = "=0.6.5", features = ["default", "json"] }use serde::Serialize;
use wstd::http::{Body, Client, Request};
#[derive(Serialize)]
struct ExampleRequest {
title: String,
body: String,
user_id: u16,
}
let request_body = ExampleRequest {
title: "foo".to_string(),
body: "bar".to_string(),
user_id: 1,
};
let request = Request::post(&format!("http://localhost:{port}/todos"))
.header("Content-Type", "application/json")
.body(Body::from_json(&request_body).expect("Failed to serialize request"))
.expect("Failed to build request");
let mut response = Client::new()
.send(request)
.await
.expect("Request failed");
let data: serde_json::Value = response
.body_mut()
.json()
.await
.expect("Failed to parse response");
println!("Body: {:?}", data);It’s recommended to use wstd, but you can also use golem-wasi-http for a more reqwest-like client.
Make sure to have the crate added to your Cargo.toml:
[dependencies]
golem-wasi-http = { version = "0.2.0", features = ["async", "json"] }use golem_wasi_http::{Client, Response};
use serde::Serialize;
#[derive(Serialize)]
struct ExampleRequest {
title: String,
body: String,
user_id: u16,
}
let request_body = ExampleRequest {
title: "foo".to_string(),
body: "bar".to_string(),
user_id: 1,
};
let client = Client::builder().build().expect("Failed to build client");
let response: Response = client
.post(&format!("http://localhost:{port}/todos"))
.header("X-Test", "Golem")
.json(&request_body)
.basic_auth("some", Some("body"))
.send()
.await
.expect("Request failed");
let data: serde_json::Value = response.json().await.expect("Failed to parse response");
println!("Body: {:?}", data);Generating clients from OpenAPI specs
For typed HTTP clients generated from an OpenAPI spec, use progenitor-wasi-http. It is a fork of Oxide Computer’s Progenitor that replaces reqwest with wstd::http, so the generated client works on top of wasi:http (and therefore inside Golem workers).
There are three ways to use it: via a procedural macro, a build.rs script, or by generating a standalone crate with the cargo-progenitor CLI.
Macro-based generation
Add the following dependencies to your Cargo.toml:
[dependencies]
http = "1.4"
progenitor = { git = "https://github.com/golemcloud/progenitor-wasi-http" }
serde = { version = "1.0", features = ["derive"] }
wstd = "=0.5.4"Then expand the client directly into your source file:
use progenitor::generate_api;
generate_api!("path/to/openapi.json");
async fn example() {
let client = Client::new("https://api.example.com");
// generated methods are now available on `client`
}The macro supports additional options such as interface = Builder for a builder-style API, extra derives, and pre/post request hooks (useful for injecting authentication headers):
use progenitor::generate_api;
use wstd::http;
generate_api!(
spec = "openapi/my-api.json",
interface = Builder,
pre_hook_async = crate::add_auth_headers,
);
async fn add_auth_headers<B: http::Body>(
req: &mut http::Request<B>,
) -> Result<(), http::error::InvalidHeaderValue> {
req.headers_mut().insert(
::http::header::AUTHORIZATION,
::http::header::HeaderValue::from_str("Bearer my-token")?,
);
Ok(())
}build.rs-based generation
If you prefer to inspect the generated code on disk, run the generator from a build script. Add the runtime client to [dependencies] and the generator to [build-dependencies]:
[dependencies]
http = "1.4"
progenitor-client = { git = "https://github.com/golemcloud/progenitor-wasi-http" }
serde = { version = "1.0", features = ["derive"] }
wstd = "=0.5.4"
[build-dependencies]
prettyplease = "0.2"
progenitor = { git = "https://github.com/golemcloud/progenitor-wasi-http", default-features = false }
serde_json = "1.0"
syn = "2.0"A minimal build.rs:
use std::{env, fs, fs::File, path::Path};
fn main() {
let src = "openapi/my-api.json";
println!("cargo:rerun-if-changed={}", src);
let file = File::open(src).unwrap();
let spec = serde_json::from_reader(file).unwrap();
let mut generator = progenitor::Generator::default();
let tokens = generator.generate_tokens(&spec).unwrap();
let ast = syn::parse2(tokens).unwrap();
let content = prettyplease::unparse(&ast);
let mut out_file = Path::new(&env::var("OUT_DIR").unwrap()).to_path_buf();
out_file.push("codegen.rs");
fs::write(out_file, content).unwrap();
}Include the generated module from your code:
include!(concat!(env!("OUT_DIR"), "/codegen.rs"));
async fn example() {
let client = Client::new("https://api.example.com");
// generated methods are now available on `client`
}Standalone crate generation
To generate a self-contained client crate that can be committed and reused independently, use the cargo-progenitor CLI:
cargo install --git https://github.com/golemcloud/progenitor-wasi-http cargo-progenitor
cargo progenitor -i openapi/my-api.json -o ./my-api-client -n my-api-client -v 0.1.0In the generated crate’s Cargo.toml, use the WASI fork of the runtime client and add the http dependency used by generated methods:
progenitor-client = { git = "https://github.com/golemcloud/progenitor-wasi-http" }
http = "1.4"See the progenitor-wasi-http README and the example-build and example-macro directories for complete worked examples, including pagination and customizing the generated builder API.
Scala
HTTP requests can be made with the standard fetch function through Scala.js interop.
For example:
import scala.scalajs.concurrent.JSExecutionContext.Implicits.queue
import scala.scalajs.js
import scala.scalajs.js.Thenable.Implicits._
for {
response <- js.Dynamic.global
.fetch(
s"http://localhost:$port/todos",
js.Dynamic.literal(
method = "POST",
headers = js.Dynamic.literal(
"Content-Type" -> "application/json"
),
body = js.JSON.stringify(
js.Dynamic.literal(
title = "foo",
body = "bar",
userId = 1
)
)
)
)
.asInstanceOf[js.Promise[js.Dynamic]]
.toFuture
data <- response.json().asInstanceOf[js.Promise[js.Dynamic]].toFuture
} yield println(s"Body: ${js.JSON.stringify(data)}")MoonBit
The current MoonBit SDK provides an async wrapper around Preview 3 wasi:http. Import golemcloud/golem_sdk/http as @http, construct an @http.Request with its generated accessors, and pass it to @http.send:
async fn send(request : @http.Request) -> Result[@http.Response, @http.ErrorCode] {
@http.send(request)
}This is the exact SDK wrapper signature. Request bodies and response bodies are async byte streams; consume the full response stream before decoding JSON, and use moonbitlang/core/encoding/utf8 rather than character-to-byte casts.
For WebSocket connections, see the WebSocket client page. The HTTP APIs described above do not support WebSocket upgrades.