Skip to content

HTTP Client

mik-sdk provides an HTTP client for making outbound requests to external services. Enable it with the http-client feature.

#[allow(warnings)]
mod bindings;
use bindings::exports::mik::core::handler::{self, Guest, Response};
use mik_sdk::prelude::*;

The fetch! macro is included in mik_sdk::prelude::* by default.

fn fetch_data(_req: &Request) -> Response {
let response = fetch!(GET "https://api.example.com/data")
.send()?;
if response.is_success() {
ok!({ "data": response.json() })
} else {
error! {
status: 502,
title: "Upstream Error",
detail: format!("API returned {}", response.status())
}
}
}
fn create_item(_req: &Request) -> Response {
let response = fetch!(POST "https://api.example.com/items", json: {
"name": "Widget",
"price": 19.99
}).send()?;
if response.is_success() {
ok!({ "created": true })
} else {
error! { status: 500, title: "Failed", detail: "Could not create item" }
}
}
// PUT
fetch!(PUT "https://api.example.com/items/123", json: {
"name": "Updated Widget"
}).send()?;
// PATCH
fetch!(PATCH "https://api.example.com/items/123", json: {
"price": 24.99
}).send()?;
// DELETE
fetch!(DELETE "https://api.example.com/items/123").send()?;
let response = fetch!(GET "https://api.example.com/protected",
headers: {
"Authorization": format!("Bearer {}", token),
"X-Custom-Header": "value"
}
).send()?;

Set a timeout in milliseconds:

let response = fetch!(GET "https://slow-api.example.com/data",
timeout: 5000 // 5 seconds
).send()?;

For non-JSON payloads:

let response = fetch!(POST "https://api.example.com/upload",
headers: { "Content-Type": "text/plain" },
body: b"Raw text content"
).send()?;
let response = fetch!(POST "https://api.example.com/data",
headers: {
"Authorization": format!("Bearer {}", token),
"X-Request-ID": random::uuid()
},
json: {
"name": "Example",
"value": 42
},
timeout: 10000
).send()?;
let response = fetch!(GET "https://api.example.com/data").send()?;
// Status code
let status = response.status(); // u16
// Check success (2xx)
if response.is_success() {
// ...
}
// Body as bytes
let body = response.bytes(); // &[u8]
// Parse JSON
if let Some(json) = response.json() {
let value = json.path_str(&["field"]);
}
// Headers
let content_type = response.header("Content-Type");
fn call_api(_req: &Request) -> Response {
match fetch!(GET "https://api.example.com/data").send() {
Ok(response) => {
if response.is_success() {
ok!({ "data": response.json() })
} else {
error! {
status: response.status(),
title: "API Error",
detail: format!("Upstream returned {}", response.status())
}
}
}
Err(_) => {
error! {
status: 503,
title: "Service Unavailable",
detail: "Could not reach upstream service"
}
}
}
}

Block requests to private IP addresses when using user-provided URLs:

fn fetch_url(body: UrlInput, _req: &Request) -> Response {
// User-provided URL - enable SSRF protection
let response = fetch!(GET &body.url)
.deny_private_ips() // Blocks localhost, 10.x, 192.168.x, etc.
.send()?;
ok!({ "fetched": true })
}

Blocked addresses include:

  • 127.0.0.0/8 (loopback)
  • 10.0.0.0/8 (private)
  • 172.16.0.0/12 (private)
  • 192.168.0.0/16 (private)
  • 169.254.0.0/16 (link-local)
  • ::1 (IPv6 loopback)
  • Private IPv6 ranges

Forward trace IDs for distributed tracing:

fn call_downstream(req: &Request) -> Response {
let trace_id = req.trace_id_or("");
let response = fetch!(GET "https://api.example.com/data")
.with_trace_id(if trace_id.is_empty() { None } else { Some(trace_id) })
.send()?;
ok!({ "traced": true })
}

This adds the traceparent header (W3C Trace Context) to outbound requests if present in the incoming request.

Use string interpolation for dynamic URLs:

fn get_user(path: Id, _req: &Request) -> Response {
let url = format!("https://api.example.com/users/{}", path.as_str());
let response = fetch!(GET &url).send()?;
ok!({ "data": response.json() })
}

Or inline:

let response = fetch!(GET format!("https://api.example.com/users/{}", user_id))
.send()?;
fn create_order(body: OrderInput, req: &Request) -> Response {
// Validate order
guard!(body.items.len() > 0, 400, "Order must have items");
let trace_id = req.trace_id_or("");
let trace_opt = if trace_id.is_empty() { None } else { Some(trace_id) };
// Call inventory service
let inventory = fetch!(POST "http://inventory:8080/reserve",
headers: { "X-Request-ID": random::uuid() },
json: { "items": body.items },
timeout: 5000
)
.with_trace_id(trace_opt)
.send();
let inv_response = ensure!(inventory.ok(), 503, "Inventory service unavailable");
guard!(inv_response.is_success(), 409, "Items not available");
// Call payment service
let payment = fetch!(POST "http://payments:8080/charge",
json: {
"amount": body.total,
"customer_id": body.customer_id
},
timeout: 10000
)
.with_trace_id(trace_opt)
.send();
let pay_response = ensure!(payment.ok(), 503, "Payment service unavailable");
guard!(pay_response.is_success(), 402, "Payment failed");
// Create order
let order_id = random::uuid();
log!(info, "order created", order_id: &order_id, customer: &body.customer_id);
created!(format!("/orders/{}", order_id), {
"id": order_id,
"status": "confirmed"
})
}
OptionDescription
headers: { ... }HTTP headers
json: { ... }JSON body (sets Content-Type)
body: bytesRaw body bytes
timeout: msTimeout in milliseconds
MethodDescription
.send()Execute the request
.deny_private_ips()Enable SSRF protection
.with_trace_id(opt)Add traceparent header
MethodReturnsDescription
status()u16HTTP status code
is_success()boolTrue if 2xx
body()Vec<u8>Response body
header(name)Option<&str>Response header