| name | ureq |
| description | Simple blocking HTTP client for Rust |
ureq
ureq is a simple, safe HTTP client written in pure Rust. It uses blocking I/O instead of async, which keeps the API simple and dependencies minimal. This is ideal for script-kit-gpui's AI provider calls where simplicity trumps concurrent request handling.
Why Blocking Can Be Good
- Simpler mental model: No async/await, no runtime, no Pin<Box>
- Easier error handling: Standard Result types, no async error propagation complexity
- Lower dependency count: No tokio/async-std runtime required
- Thread-based concurrency: Spawn threads for parallel requests if needed
- Perfect for CLI tools: Where async overhead isn't justified
Key Types
Agent
Connection pool + configuration holder. Reuse across requests for connection reuse.
use ureq::Agent;
use std::time::Duration;
let agent: Agent = Agent::config_builder()
.timeout_connect(Some(Duration::from_secs(10)))
.timeout_recv_body(Some(Duration::from_secs(60)))
.build()
.new_agent();
RequestBuilder
Fluent builder for constructing requests.
let response = agent.post("https://api.example.com")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer token")
.send_json(&body)?;
Response / Body
Response wraps the HTTP response. Body provides methods to read response data.
let status = response.status();
let data: MyStruct = response.into_body().read_json()?;
let text: String = response.into_body().read_to_string()?;
let reader = response.into_body().into_reader();
Error
Unified error type covering network, protocol, and HTTP status errors.
use ureq::Error;
match agent.get(url).call() {
Ok(response) => { }
Err(Error::StatusCode(code)) => { }
Err(e) => { }
}
Usage in script-kit-gpui
script-kit-gpui uses ureq for AI provider API calls (OpenAI, Anthropic, Vercel Gateway).
Agent Creation Pattern
const CONNECT_TIMEOUT_SECS: u64 = 10;
const READ_TIMEOUT_SECS: u64 = 60;
fn create_agent() -> ureq::Agent {
ureq::Agent::config_builder()
.timeout_connect(Some(Duration::from_secs(CONNECT_TIMEOUT_SECS)))
.timeout_recv_body(Some(Duration::from_secs(READ_TIMEOUT_SECS)))
.build()
.new_agent()
}
Provider Struct Pattern
pub struct OpenAiProvider {
config: ProviderConfig,
agent: ureq::Agent,
}
impl OpenAiProvider {
pub fn new(api_key: impl Into<String>) -> Self {
Self {
config: ProviderConfig::new("openai", "OpenAI", api_key),
agent: create_agent(),
}
}
}
Request Building
GET Request
let response = ureq::get("https://api.example.com/data")
.header("Authorization", "Bearer token")
.call()?;
POST with JSON
let body = serde_json::json!({
"model": "gpt-4o",
"messages": [{"role": "user", "content": "Hello"}]
});
let response = agent
.post("https://api.openai.com/v1/chat/completions")
.header("Content-Type", "application/json")
.header("Authorization", &format!("Bearer {}", api_key))
.send_json(&body)?;
Headers for AI APIs
.header("Authorization", &format!("Bearer {}", api_key))
.header("x-api-key", api_key)
.header("anthropic-version", "2023-06-01")
.header("Accept", "text/event-stream")
Response Handling
Status Codes
By default, ureq returns Error::StatusCode for 4xx/5xx responses. Handle explicitly:
match agent.post(url).send_json(&body) {
Ok(response) => {
let json: serde_json::Value = response.into_body().read_json()?;
}
Err(Error::StatusCode(code)) => {
eprintln!("API error: {}", code);
}
Err(e) => {
return Err(e.into());
}
}
JSON Parsing
Requires json feature in Cargo.toml:
let data: MyResponse = response.into_body().read_json()?;
let json: serde_json::Value = response.into_body().read_json()?;
let content = json["choices"][0]["message"]["content"].as_str();
Using anyhow Context
use anyhow::{Context, Result};
let response = agent
.post(url)
.send_json(&body)
.context("Failed to send request to OpenAI API")?;
let json: serde_json::Value = response
.into_body()
.read_json()
.context("Failed to parse OpenAI response")?;
Streaming Responses
For AI streaming (SSE), convert body to a reader and process line-by-line:
use std::io::{BufRead, BufReader};
let response = agent
.post(url)
.header("Accept", "text/event-stream")
.send_json(&body)?;
let reader = BufReader::new(response.into_body().into_reader());
for line in reader.lines() {
let line = line?;
let line = line.trim_end_matches('\r');
if line.is_empty() {
continue;
}
if let Some(data) = line.strip_prefix("data: ") {
if data == "[DONE]" {
break;
}
if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(data) {
if let Some(content) = parsed[][][][].() {
(content.());
}
}
}
}
SSE Helper Pattern (from script-kit-gpui)
fn stream_sse_lines<R: BufRead>(
reader: R,
mut on_data: impl FnMut(&str) -> Result<bool>,
) -> Result<()> {
let mut data_buf = String::new();
for line in reader.lines() {
let mut line = line.context("Failed to read SSE line")?;
if line.ends_with('\r') {
line.pop();
}
if line.is_empty() {
if data_buf.is_empty() {
continue;
}
if data_buf == "[DONE]" {
break;
}
if !on_data(&data_buf)? {
break;
}
data_buf.clear();
continue;
}
if let Some(d) = line.strip_prefix("data: ") {
!data_buf.() {
data_buf.();
}
data_buf.(d);
}
}
(())
}
Timeouts
Configure at Agent level:
Agent::config_builder()
.timeout_connect(Some(Duration::from_secs(10)))
.timeout_recv_body(Some(Duration::from_secs(60)))
.timeout_global(Some(Duration::from_secs(120)))
.build()
TLS
ureq uses rustls by default (pure Rust TLS). For native TLS:
use ureq::tls::{TlsConfig, TlsProvider};
let config = Config::builder()
.tls_config(
TlsConfig::builder()
.provider(TlsProvider::NativeTls)
.build()
)
.build();
Anti-patterns
Don't Create Agent Per Request
fn make_request() {
let agent = ureq::agent();
agent.get(url).call()
}
struct Client {
agent: ureq::Agent,
}
impl Client {
fn make_request(&self) {
self.agent.get(url).call()
}
}
Don't Ignore Status Codes
let json = agent.get(url).call()?.into_body().read_json()?;
match agent.get(url).call() {
Ok(resp) => resp.into_body().read_json()?,
Err(Error::StatusCode(code)) => {
return Err(anyhow!("API returned {}", code));
}
Err(e) => return Err(e.into()),
}
Don't Block Forever on Streaming
for line in reader.lines() { ... }
Agent::config_builder()
.timeout_recv_body(Some(Duration::from_secs(60)))
.build()
Don't Forget Content-Type
agent.post(url).send(json_string)?;
agent.post(url)
.header("Content-Type", "application/json")
.send(json_string)?;
agent.post(url).send_json(&body)?;
Feature Flags
Enable in Cargo.toml:
[dependencies]
ureq = { version = "3", features = ["json", "gzip"] }
- rustls (default): TLS via rustls
- native-tls: OS-native TLS
- json: serde_json integration (send_json, read_json)
- gzip: Automatic gzip decompression
- cookies: Cookie jar support
- charset: Non-UTF-8 charset handling
Quick Reference
| Operation | Code |
|---|
| GET | ureq::get(url).call()? |
| POST JSON | agent.post(url).send_json(&body)? |
| Add header | .header("Key", "value") |
| Read JSON | resp.into_body().read_json::<T>() |
| Read string | resp.into_body().read_to_string() |
| Get reader | resp.into_body().into_reader() |
| Status code | resp.status() |
| Set timeout | Agent::config_builder().timeout_global(...) |