Agent skill
ureq
Simple blocking HTTP client for Rust
Install this agent skill to your Project
npx add-skill https://github.com/johnlindquist/script-kit-next/tree/main/.opencode/skill/ureq
SKILL.md
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<Future>>
- 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.
// Get status
let status = response.status();
// Read as JSON (requires `json` feature)
let data: MyStruct = response.into_body().read_json()?;
// Read as string
let text: String = response.into_body().read_to_string()?;
// Get a reader for streaming
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) => { /* success */ }
Err(Error::StatusCode(code)) => { /* 4xx/5xx */ }
Err(e) => { /* network/protocol error */ }
}
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, // Reused across requests
}
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
// OpenAI
.header("Authorization", &format!("Bearer {}", api_key))
// Anthropic
.header("x-api-key", api_key)
.header("anthropic-version", "2023-06-01")
// SSE streaming
.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) => {
// 2xx success
let json: serde_json::Value = response.into_body().read_json()?;
}
Err(Error::StatusCode(code)) => {
// 4xx/5xx - API error
eprintln!("API error: {}", code);
}
Err(e) => {
// Network/timeout/etc
return Err(e.into());
}
}
JSON Parsing
Requires json feature in Cargo.toml:
// Deserialize to struct
let data: MyResponse = response.into_body().read_json()?;
// Deserialize to Value for dynamic parsing
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)?;
// Convert to buffered reader
let reader = BufReader::new(response.into_body().into_reader());
// Process SSE lines
for line in reader.lines() {
let line = line?;
// Handle CRLF
let line = line.trim_end_matches('\r');
// Empty line = end of event
if line.is_empty() {
continue;
}
// Parse data lines
if let Some(data) = line.strip_prefix("data: ") {
if data == "[DONE]" {
break;
}
// Parse JSON and extract content
if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(data) {
if let Some(content) = parsed["choices"][0]["delta"]["content"].as_str() {
on_chunk(content.to_string());
}
}
}
}
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: ") {
if !data_buf.is_empty() {
data_buf.push('\n');
}
data_buf.push_str(d);
}
}
Ok(())
}
Timeouts
Configure at Agent level:
Agent::config_builder()
.timeout_connect(Some(Duration::from_secs(10))) // TCP connect
.timeout_recv_body(Some(Duration::from_secs(60))) // Body receive
.timeout_global(Some(Duration::from_secs(120))) // Total request
.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) // Use OS TLS
.build()
)
.build();
Anti-patterns
Don't Create Agent Per Request
// BAD: Creates new connection pool each time
fn make_request() {
let agent = ureq::agent();
agent.get(url).call()
}
// GOOD: Reuse agent
struct Client {
agent: ureq::Agent,
}
impl Client {
fn make_request(&self) {
self.agent.get(url).call()
}
}
Don't Ignore Status Codes
// BAD: Assumes success
let json = agent.get(url).call()?.into_body().read_json()?;
// GOOD: Handle error responses
match agent.get(url).call() {
Ok(resp) => resp.into_body().read_json()?,
Err(Error::StatusCode(code)) => {
// Log or handle API error
return Err(anyhow!("API returned {}", code));
}
Err(e) => return Err(e.into()),
}
Don't Block Forever on Streaming
// BAD: No timeout, hangs forever if server stops sending
for line in reader.lines() { ... }
// GOOD: Set timeout_recv_body on agent, or implement read timeout
Agent::config_builder()
.timeout_recv_body(Some(Duration::from_secs(60)))
.build()
Don't Forget Content-Type
// BAD: Server may reject or misparse
agent.post(url).send(json_string)?;
// GOOD: Set Content-Type explicitly
agent.post(url)
.header("Content-Type", "application/json")
.send(json_string)?;
// BEST: Use send_json which sets Content-Type automatically
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(...) |
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
Generate Component Documentation
Based on existing docs styles and specific API implementations, and referencing same name stories, generate comprehensive documentation for the new component.
Generate Component Story
Generate a comprehensive story for a new component for as example.
new-component
How to write a new component of GPUI Component.
troubleshooting
Diagnose and fix common Script Kit issues. Use when the user reports bugs, crashes, missing features, or unexpected behavior in Script Kit GPUI.
script-authoring
Create and manage TypeScript scripts for Script Kit. Use when the user wants to write a new script, edit an existing script, or understand Script Kit's SDK and metadata system.
agents
Create mdflow-backed agent files for Script Kit. Use when the user wants to create AI agents, configure agent backends (Claude, Gemini, Codex), or manage agent metadata.
Didn't find tool you were looking for?