Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

HTTP Configuration

HTTP handlers require careful configuration for reliability, security, and performance. This chapter covers advanced HTTP configuration patterns.

Complete Configuration Example

tools:
  - type: http
    name: api_call
    description: "Configured API call with all options"
    endpoint: "https://api.example.com/{{resource}}"
    method: POST
    headers:
      User-Agent: "pforge/1.0"
      Authorization: "Bearer {{token}}"
      Content-Type: "application/json"
      X-Request-ID: "{{request_id}}"
    query:
      version: "v2"
      format: "json"
    body:
      data: "{{payload}}"
    timeout_ms: 30000
    retry:
      max_attempts: 3
      backoff_ms: 1000
    params:
      resource: { type: string, required: true }
      token: { type: string, required: true }
      request_id: { type: string, required: false }
      payload: { type: object, required: true }

Header Management

Static Headers

headers:
  User-Agent: "pforge-client/1.0"
  Accept: "application/json"
  Accept-Language: "en-US"

Dynamic Headers (Templated)

headers:
  Authorization: "Bearer {{access_token}}"
  X-Tenant-ID: "{{tenant_id}}"
  X-Correlation-ID: "{{correlation_id}}"

Conditional Headers

For conditional headers, use a Native handler:

async fn handle(&self, input: Input) -> Result<Output> {
    let mut headers = HashMap::new();
    headers.insert("User-Agent", "pforge");

    if let Some(token) = input.auth_token {
        headers.insert("Authorization", format!("Bearer {}", token));
    }

    if input.use_compression {
        headers.insert("Accept-Encoding", "gzip, deflate");
    }

    let client = reqwest::Client::new();
    let response = client
        .get(&input.url)
        .headers(headers)
        .send()
        .await?;

    // ...
}

Query Parameter Patterns

Simple Query Params

query:
  page: "{{page}}"
  limit: "{{limit}}"
  sort: "name"  # Static value

Array Query Params

# Input: { "tags": ["rust", "mcp", "api"] }
# URL: ?tags=rust&tags=mcp&tags=api

query:
  tags: "{{tags}}"  # Automatically handles arrays

Complex Filtering

query:
  filter: "created_at>{{start_date}},status={{status}}"
  fields: "id,name,created_at"

Request Body Configuration

JSON Body

tools:
  - type: http
    name: create_resource
    method: POST
    body:
      name: "{{name}}"
      description: "{{description}}"
      metadata:
        source: "pforge"
        timestamp: "{{timestamp}}"

Nested Objects

body:
  user:
    name: "{{user_name}}"
    email: "{{user_email}}"
    preferences:
      theme: "{{theme}}"
      notifications: "{{notifications}}"

Array Payloads

body:
  items: "{{items}}"  # Array of objects

# Input:
# {
#   "items": [
#     { "id": 1, "name": "foo" },
#     { "id": 2, "name": "bar" }
#   ]
# }

Timeout Configuration

Global Timeout

timeout_ms: 30000  # 30 seconds for entire request

Per-Endpoint Timeouts

tools:
  - type: http
    name: quick_lookup
    endpoint: "https://api.example.com/lookup"
    timeout_ms: 1000  # 1 second

  - type: http
    name: heavy_computation
    endpoint: "https://api.example.com/compute"
    timeout_ms: 120000  # 2 minutes

Native Handler Timeout Control

use tokio::time::{timeout, Duration};

let response = timeout(
    Duration::from_millis(input.timeout_ms),
    client.get(&url).send()
).await
.map_err(|_| Error::Timeout)?;

Retry Configuration

Basic Retry

retry:
  max_attempts: 3
  backoff_ms: 1000  # Wait 1s between retries

Exponential Backoff (Native Handler)

use backoff::{ExponentialBackoff, Error as BackoffError};

let backoff = ExponentialBackoff {
    initial_interval: Duration::from_millis(100),
    max_interval: Duration::from_secs(10),
    max_elapsed_time: Some(Duration::from_secs(60)),
    ..Default::default()
};

let result = backoff::retry(backoff, || async {
    match client.get(&url).send().await {
        Ok(response) if response.status().is_success() => Ok(response),
        Ok(response) => Err(BackoffError::transient(Error::Http(...))),
        Err(e) => Err(BackoffError::permanent(Error::from(e))),
    }
}).await?;

Response Handling

Status Code Mapping

HTTP handlers return all responses (2xx, 4xx, 5xx):

# Handler returns:
{
  "status": 404,
  "body": { "error": "Not found" },
  "headers": {...}
}

Client decides:

const result = await client.callTool("get_user", { id: "123" });

if (result.status === 404) {
  console.log("User not found");
} else if (result.status >= 400) {
  throw new Error(`API error: ${result.status}`);
}

Header Extraction

const result = await client.callTool("api_call", params);

// Rate limiting
const rateLimit = parseInt(result.headers["x-ratelimit-remaining"]);
if (rateLimit < 10) {
  console.warn("Approaching rate limit");
}

// Pagination
const nextPage = result.headers["link"]?.match(/page=(\d+)/)?.[1];

SSL/TLS Configuration

Accept Self-Signed Certificates (Development)

Use Native handler with custom client:

let client = reqwest::Client::builder()
    .danger_accept_invalid_certs(true)  // DEVELOPMENT ONLY
    .build()?;

Custom CA Certificates

use reqwest::Certificate;

let cert = std::fs::read("ca-cert.pem")?;
let cert = Certificate::from_pem(&cert)?;

let client = reqwest::Client::builder()
    .add_root_certificate(cert)
    .build()?;

Connection Pooling

HTTP handlers automatically use connection pooling via reqwest.

Pool Configuration (Native Handler)

let client = reqwest::Client::builder()
    .pool_max_idle_per_host(10)
    .pool_idle_timeout(Duration::from_secs(30))
    .build()?;

Common Configuration Patterns

Pattern 1: Paginated API

tools:
  - type: http
    name: list_items
    endpoint: "https://api.example.com/items"
    method: GET
    query:
      page: "{{page}}"
      per_page: "{{per_page}}"
    params:
      page: { type: integer, required: false, default: 1 }
      per_page: { type: integer, required: false, default: 100 }

Pattern 2: Webhook Receiver

tools:
  - type: http
    name: trigger_webhook
    endpoint: "https://webhook.example.com/events"
    method: POST
    headers:
      X-Webhook-Secret: "{{secret}}"
    body:
      event: "{{event_type}}"
      payload: "{{data}}"

Pattern 3: File Upload (Use Native Handler)

use reqwest::multipart;

async fn handle(&self, input: UploadInput) -> Result<UploadOutput> {
    let file_content = std::fs::read(&input.file_path)?;

    let form = multipart::Form::new()
        .text("description", input.description)
        .part("file", multipart::Part::bytes(file_content)
            .file_name(input.file_name));

    let response = self.client
        .post(&input.upload_url)
        .multipart(form)
        .send()
        .await?;

    // ...
}

Testing HTTP Configuration

Mock Server

use wiremock::{Mock, MockServer, ResponseTemplate};

#[tokio::test]
async fn test_http_handler() {
    let mock_server = MockServer::start().await;

    Mock::given(method("GET"))
        .and(path("/users/123"))
        .respond_with(ResponseTemplate::new(200)
            .set_body_json(json!({
                "id": "123",
                "name": "Alice"
            })))
        .mount(&mock_server)
        .await;

    let handler = HttpHandler::new(
        format!("{}/users/{{id}}", mock_server.uri()),
        HttpMethod::Get,
        HashMap::new(),
        None,
    );

    let result = handler.execute(HttpInput {
        body: None,
        query: [("id", "123")].into(),
    }).await.unwrap();

    assert_eq!(result.status, 200);
    assert_eq!(result.body["name"], "Alice");
}

Next Steps

Chapter 5.2 covers authentication patterns including Bearer tokens, API keys, Basic Auth, and OAuth integration.


“Configuration is declarative. Complexity is in the runtime.” - pforge HTTP design