HTTP Client Handler
The HTTP Client Handler enables actors to make HTTP requests to external services while maintaining Theater's state verification and security principles. This handler allows actors to interact with external APIs, fetch resources, and communicate with web services.
Overview
The HTTP Client Handler implements the ntwk:theater/http-client
interface, providing a way for actors to:
- Send HTTP requests to external services
- Process HTTP responses
- Record all HTTP interactions in the state chain
- Handle errors in a consistent way
Configuration
To use the HTTP Client Handler, add it to your actor's manifest:
[[handlers]]
type = "http-client"
config = {}
Currently, the HTTP Client Handler doesn't require any specific configuration parameters.
Interface
The HTTP Client Handler is defined using the following WIT interface:
interface http-client {
use types.{json};
use http-types.{http-request, http-response};
send-http: func(req: http-request) -> result<http-response, string>;
}
HTTP Request Structure
The HttpRequest
type has the following structure:
#![allow(unused)] fn main() { struct HttpRequest { method: String, uri: String, headers: Vec<(String, String)>, body: Option<Vec<u8>>, } }
method
: The HTTP method (GET, POST, PUT, DELETE, etc.)uri
: The target URLheaders
: A list of HTTP headers as key-value pairsbody
: Optional request body as bytes
HTTP Response Structure
The HttpResponse
type has the following structure:
#![allow(unused)] fn main() { struct HttpResponse { status: u16, headers: Vec<(String, String)>, body: Option<Vec<u8>>, } }
status
: The HTTP status codeheaders
: A list of response headers as key-value pairsbody
: Optional response body as bytes
Making HTTP Requests
To make an HTTP request, actors call the send-http
function with an HttpRequest
object:
#![allow(unused)] fn main() { let request = HttpRequest { method: "GET".to_string(), uri: "https://api.example.com/data".to_string(), headers: vec![ ("Content-Type".to_string(), "application/json".to_string()), ("Authorization".to_string(), "Bearer token123".to_string()), ], body: None, }; match http_client::send_http(request) { Ok(response) => { // Process response println!("Status: {}", response.status); if let Some(body) = response.body { // Handle response body } }, Err(error) => { // Handle error println!("Request failed: {}", error); } } }
State Chain Integration
Every HTTP request and response is recorded in the actor's state chain, creating a verifiable history of all external interactions. The chain events include:
-
HttpClientRequestCall: Records when a request is made, including:
- HTTP method
- Target URL
- Headers count
- Body size
-
HttpClientRequestResult: Records the result of a request, including:
- Status code
- Headers count
- Body size
- Success indicator
-
Error: Records any errors that occur during the request, including:
- Operation type
- URL path
- Error message
This state chain integration ensures that all external interactions are:
- Traceable
- Verifiable
- Reproducible
- Auditable
Error Handling
The HTTP Client Handler provides detailed error information for various failure scenarios:
- Invalid Method: When an invalid HTTP method is specified
- Network Errors: When network issues prevent the request from completing
- Timeout Errors: When the request times out
- Parser Errors: When response parsing fails
All errors are returned as strings and are also recorded in the state chain.
Security Considerations
When using the HTTP Client Handler, consider the following security aspects:
- URL Validation: Validate URLs before making requests to prevent SSRF attacks
- Sensitive Data: Be careful with sensitive data in requests, as they are recorded in the state chain
- Authentication: Use secure methods for authentication in external APIs
- TLS Verification: The handler performs TLS verification by default
- Timeouts: Set appropriate timeouts for requests to prevent resource exhaustion
Implementation Details
Under the hood, the HTTP Client Handler:
- Converts the
HttpRequest
into a reqwest client request - Sets up headers, body, and method
- Executes the request asynchronously
- Processes the response into an
HttpResponse
- Records all operations in the state chain
- Returns the response or error to the actor
The handler uses the reqwest crate for HTTP functionality, providing a robust and well-tested HTTP client implementation.
Limitations
The current HTTP Client Handler implementation has some limitations:
- No Direct Streaming: Large responses are loaded fully into memory
- No WebSocket Support: For WebSocket connections, use a dedicated WebSocket client
- No Client Certificate Authentication: TLS client certificates are not currently supported
- No Direct Proxy Configuration: Proxy settings cannot be configured per-request
Best Practices
- Error Handling: Always handle errors from HTTP requests properly
- Response Size: Be mindful of response sizes to avoid memory issues
- Request Rate: Implement rate limiting for external API calls
- Timeout Handling: Set appropriate timeouts for your use case
- Idempotency: Design requests to be idempotent when possible
- Retries: Implement retry logic for transient failures
Examples
Example 1: Simple GET Request
#![allow(unused)] fn main() { pub fn fetch_json_data() -> Result<serde_json::Value, String> { let request = HttpRequest { method: "GET".to_string(), uri: "https://api.example.com/data.json".to_string(), headers: vec![("Accept".to_string(), "application/json".to_string())], body: None, }; let response = http_client::send_http(request)?; if response.status != 200 { return Err(format!("API returned status code: {}", response.status)); } if let Some(body) = response.body { let json = serde_json::from_slice(&body) .map_err(|e| format!("Failed to parse JSON: {}", e))?; Ok(json) } else { Err("Response body was empty".to_string()) } } }
Example 2: POST Request with JSON Body
#![allow(unused)] fn main() { pub fn create_resource(data: &CreateResourceRequest) -> Result<ResourceResponse, String> { let json_body = serde_json::to_vec(data) .map_err(|e| format!("Failed to serialize request: {}", e))?; let request = HttpRequest { method: "POST".to_string(), uri: "https://api.example.com/resources".to_string(), headers: vec![ ("Content-Type".to_string(), "application/json".to_string()), ("Authorization".to_string(), format!("Bearer {}", get_token())), ], body: Some(json_body), }; let response = http_client::send_http(request)?; match response.status { 201 => { // Resource created successfully if let Some(body) = response.body { let resource: ResourceResponse = serde_json::from_slice(&body) .map_err(|e| format!("Failed to parse response: {}", e))?; Ok(resource) } else { Err("Response body was empty".to_string()) } }, 400..=499 => { // Client error Err(format!("Client error: {}", response.status)) }, 500..=599 => { // Server error Err(format!("Server error: {}", response.status)) }, _ => { // Unexpected status code Err(format!("Unexpected status code: {}", response.status)) } } } }
Example 3: File Download
#![allow(unused)] fn main() { pub fn download_file(url: &str) -> Result<Vec<u8>, String> { let request = HttpRequest { method: "GET".to_string(), uri: url.to_string(), headers: vec![], body: None, }; let response = http_client::send_http(request)?; if response.status != 200 { return Err(format!("Download failed with status: {}", response.status)); } if let Some(body) = response.body { Ok(body) } else { Err("Download resulted in empty file".to_string()) } } }
Related Topics
- HTTP Framework Handler - For creating HTTP servers
- Store Handler - For storing downloaded content
- Message Server Handler - For actor-to-actor communication