camel-component-http 0.13.0

HTTP client component for rust-camel
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
# camel-component-http

> HTTP client and server component for rust-camel

## Overview

The HTTP component provides HTTP client (producer) and HTTP server (consumer) capabilities for rust-camel. Built on `reqwest` for clients and `axum` for servers, it enables REST API integration, webhook handling, and HTTP-based messaging.

## Features

- **HTTP Server (Consumer)**: Listen for incoming HTTP requests
- **HTTP Client (Producer)**: Make outgoing HTTP requests
- **HTTPS Support**: Secure connections with `https` scheme
- **Configurable Timeouts**: Connect, response, and pool idle timeouts
- **Connection Pooling**: Shared `reqwest::Client` with configurable pool settings
- **SSRF Protection**: Optional private IP blocking
- **Streaming**: Direct stream-to-HTTP piping without materialization
- **Native Request Streaming**: Incoming bodies arrive as `Body::Stream`, no RAM materialization
- **Native Response Streaming**: `Body::Stream` responses use chunked transfer encoding automatically
- **Header Mapping**: Automatic header forwarding
- **Status Code Handling**: Configurable success ranges
- **Health Check**: Async TCP listener/connect probe

## Installation

Add to your `Cargo.toml`:

```toml
[dependencies]
camel-component-http = "*"
```

## URI Format

```
http://host:port/path[?options]
https://host:port/path[?options]
```

## Consumer Options (Server)

| Option | Default | Description |
|--------|---------|-------------|
| Host/path from URI | - | e.g., `http://0.0.0.0:8080/api` |
| `maxRequestBody` | `2097152` (2 MB) | If request `Content-Length` exceeds this value, responds 413 before opening the stream. Chunked uploads without `Content-Length` are not limited at the consumer level. |
| `maxInflightRequests` | `1024` | Maximum concurrently in-flight requests per `(host,port)` server. When saturated, the handler returns HTTP 503 immediately (before enqueueing into the consumer channel). |
| `maxResponseBody` | `16777216` (16MB) | Maximum response body size for materialized replies (Bytes, Text, Xml, Json). Returns HTTP 500 if exceeded. `Body::Stream` is excluded. |

## Producer Options (Client)

| Option | Default | Description |
|--------|---------|-------------|
| `httpMethod` | Auto | HTTP method (GET, POST, etc.) |
| `throwExceptionOnFailure` | `true` | Throw on non-2xx responses |
| `okStatusCodeRange` | `200-299` | Success status code range |
| `responseTimeout` | Global default | Response timeout (ms) |
| `allowPrivateIps` | Global default | Allow requests to private IPs |
| `blockedHosts` | Global default | Comma-separated blocked hosts |
| `maxBodySize` | Global default | Max response body size (bytes) |

> **Connection-level settings** (`connect_timeout_ms`, `pool_max_idle_per_host`, `pool_idle_timeout_ms`, `follow_redirects`) are configured globally in `Camel.toml` and cannot be overridden per URI. See [Global Configuration]#global-configuration.

## Usage

### HTTP Server (Consumer)

```rust
use camel_builder::RouteBuilder;
use camel_component_http::HttpComponent;
use camel_core::CamelContext;

let mut ctx = CamelContext::new();
ctx.register_component("http", Box::new(HttpComponent::new()));

// Simple API endpoint
let route = RouteBuilder::from("http://0.0.0.0:8080/hello")
    .process(|ex| async move {
        let mut ex = ex;
        ex.input.body = camel_component_api::Body::Text("Hello, World!".to_string());
        Ok(ex)
    })
    .build()?;
```

### HTTP Client (Producer)

```rust
// GET request
let route = RouteBuilder::from("timer:tick?period=60000")
    .to("http://api.example.com/data?allowPrivateIps=false")
    .log("Response received", camel_processor::LogLevel::Info)
    .build()?;

// POST request (body becomes request body)
let route = RouteBuilder::from("direct:submit")
    .to("http://api.example.com/submit?httpMethod=POST")
    .build()?;
```

### Dynamic HTTP Method

```rust
// Method from header
let route = RouteBuilder::from("direct:api")
    .set_header("CamelHttpMethod", Value::String("DELETE".into()))
    .to("http://api.example.com/resource")
    .build()?;
```

### Dynamic URL

```rust
// URL from header
let route = RouteBuilder::from("direct:proxy")
    .set_header("CamelHttpUri", Value::String("http://backend.service/api".into()))
    .to("http://localhost/dummy")  // Base URL, overridden by header
    .build()?;
```

### HTTPS

```rust
let route = RouteBuilder::from("timer:secure")
    .to("https://secure.api.com/endpoint")
    .build()?;
```

## Exchange Headers

### Request Headers (Consumer)

| Header | Description |
|--------|-------------|
| `CamelHttpMethod` | HTTP method (GET, POST, etc.) |
| `CamelHttpPath` | Request path |
| `CamelHttpQuery` | Query string |
| All HTTP headers | Forwarded from request |

### Response Headers (Producer)

| Header | Description |
|--------|-------------|
| `CamelHttpResponseCode` | HTTP status code |
| `CamelHttpResponseText` | Status text |
| All response headers | Forwarded from response |

### Request Control Headers (Producer)

| Header | Description |
|--------|-------------|
| `CamelHttpUri` | Override target URL |
| `CamelHttpPath` | Append to base URL path |
| `CamelHttpQuery` | Append query string |
| `CamelHttpMethod` | Override HTTP method |

## Example: REST API Server

```rust
use camel_builder::RouteBuilder;
use camel_component_http::{HttpComponent, HttpsComponent};
use camel_core::CamelContext;
use camel_component_api::Body;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut ctx = CamelContext::new();
    ctx.register_component("http", Box::new(HttpComponent::new()));

    // GET /api/users
    ctx.add_route(
        RouteBuilder::from("http://0.0.0.0:8080/api/users")
            .process(|ex| async move {
                let mut ex = ex;
                ex.input.body = Body::Text(r#"[{"id":1,"name":"Alice"}]"#.to_string());
                ex.input.set_header("Content-Type", serde_json::Value::String("application/json".into()));
                Ok(ex)
            })
            .build()?
    ).await?;

    // POST /api/users
    ctx.add_route(
        RouteBuilder::from("http://0.0.0.0:8080/api/users")
            .filter(|ex| ex.input.header("CamelHttpMethod").and_then(|v| v.as_str()) == Some("POST"))
                .process(|ex| async move {
                    // Create user from request body
                    Ok(ex)
                })
                .set_body(Body::Text(r#"{"status":"created"}"#))
            .end_filter()
            .build()?
    ).await?;

    ctx.start().await?;
    tokio::signal::ctrl_c().await?;
    ctx.stop().await?;

    Ok(())
}
```

## Example: HTTP Client with Error Handling

```rust
let route = RouteBuilder::from("direct:api-call")
    .to("http://api.service.com/endpoint?throwExceptionOnFailure=true&responseTimeout=5000")
    .build()?;

// With custom error handling
let route = RouteBuilder::from("direct:resilient")
    .error_handler(ErrorHandlerConfig::log_only())
    .to("http://api.service.com/endpoint?throwExceptionOnFailure=false")
    .process(|ex| async move {
        let status = ex.input.header("CamelHttpResponseCode")
            .and_then(|v| v.as_u64())
            .unwrap_or(0);
        if status >= 400 {
            // Handle error response
        }
        Ok(ex)
    })
    .build()?;
```

## Global Configuration

Configure default HTTP settings in `Camel.toml` that apply to all HTTP endpoints:

```toml
[default.components.http]
connect_timeout_ms = 5000           # Connection timeout (default: 5000)
pool_max_idle_per_host = 100        # Max idle connections per host (default: 100)
pool_idle_timeout_ms = 90000        # Idle connection timeout (default: 90000)
follow_redirects = false            # Follow HTTP redirects (default: false)
response_timeout_ms = 30000         # Response timeout (default: 30000)
max_body_size = 10485760            # Max response body size, 10MB (default: 10MB)
max_request_body = 2097152          # Max request body for server, 2MB (default: 2MB)
allow_private_ips = false           # Allow requests to private IPs (default: false)
blocked_hosts = []                  # Blocked host list (default: empty)
```

URI parameters override **request-level** defaults (`responseTimeout`, `allowPrivateIps`, `blockedHosts`, `maxBodySize`). Connection-level settings (`connect_timeout_ms`, `pool_*`, `follow_redirects`) are baked into the shared connection pool and cannot be overridden per URI.

### Profile-Specific Configuration

```toml
[default.components.http]
connect_timeout_ms = 30000
pool_max_idle_per_host = 100

[production.components.http]
connect_timeout_ms = 5000   # Faster fail in production
pool_idle_timeout_ms = 60000
allow_private_ips = false

[development.components.http]
connect_timeout_ms = 60000  # More lenient in dev
allow_private_ips = true    # Allow internal services in dev
```

## SSRF Protection

By default, the HTTP client blocks requests to private IP addresses for security. To allow:

```rust
.to("http://internal.service/api?allowPrivateIps=true")
```

To block specific hosts:

```rust
.to("http://api.example.com?blockedHosts=localhost,127.0.0.1,internal.local")
```

## Streaming & Memory Management

The HTTP component supports native streaming for both producer and consumer.

### Producer (Client) Streaming

The HTTP producer supports streaming request bodies directly without materializing them in memory. Stream bodies are piped to reqwest using `wrap_stream()`.

Memory limits apply when materialization is required (default: 10MB).

### Consumer (Server) Streaming

**Request Bodies:** Incoming HTTP request bodies arrive as `Body::Stream` in the Exchange, with no RAM materialization by default. The `Content-Length` header (if present) populates `StreamMetadata.size_hint`, and `Content-Type` populates `StreamMetadata.content_type`.

**413 Protection:** If the `Content-Length` header exceeds `maxRequestBody`, the server responds with HTTP 413 before opening the stream. Chunked uploads without a `Content-Length` header are not limited at the consumer level.

**In-flight backpressure (`503`):** The consumer channel has a fixed buffer (`mpsc(64)`) that can absorb short bursts. Saturation protection is enforced at the HTTP handler via a semaphore (`maxInflightRequests`), so HTTP 503 is returned early before queue growth. This avoids consumer-side-only throttling behavior where requests can sit in the channel and fail late.

**Response Bodies:** 
- `Body::Stream` responses use `Transfer-Encoding: chunked` automatically (no buffering)
- `Body::Bytes` / `Body::Text` responses use standard `Content-Length`

### Streaming Response Example

```rust
// Streaming response example (server-sent data)
from("http://0.0.0.0:8080/stream")
    .process(|exchange| Box::pin(async move {
        let chunks = vec![
            Ok(Bytes::from("chunk1\n")),
            Ok(Bytes::from("chunk2\n")),
        ];
        let stream = Box::pin(futures::stream::iter(chunks));
        exchange.input.body = Body::Stream(StreamBody {
            stream: Arc::new(tokio::sync::Mutex::new(Some(stream))),
            metadata: StreamMetadata::default(),
        });
        Ok(())
    }))
    .build()
```

### Request Body Access

```rust
// Access request body as stream (default) or materialize it
from("http://0.0.0.0:8080/upload")
    .process(|exchange| Box::pin(async move {
        // Option A: keep as stream (zero-copy)
        // exchange.input.body is already Body::Stream
        
        // Option B: materialize when you need the bytes
        let bytes = exchange.input.body.into_bytes(10 * 1024 * 1024).await?;
        exchange.input.body = Body::Bytes(bytes);
        Ok(())
    }))
    .build()
```

## Health Check

The `http` component registers an async health check via `AsyncHealthCheck`.

- **Probe**: TCP connect to the configured `host:port` (wildcard addresses like `0.0.0.0` are remapped to `127.0.0.1`)
- **Healthy**: TCP socket is reachable
- **Unhealthy**: Connection refused or probe times out (3s default)

Health checks are exposed via the health server:

```toml
[observability.health]
enabled = true
port = 8080
```

## Documentation

- [API Documentation]https://docs.rs/camel-component-http
- [Repository]https://github.com/kennycallado/rust-camel

## http-static: Static File Serving & SPA Fallback

### Overview

The `http-static` scheme serves static files with optional SPA fallback, custom error pages, and port sharing with API routes. It co-hosts on the same port as `http:` API routes, using longest-prefix routing to dispatch requests.

### URI Format

```
http-static:<mount-path>?dir=/path/to/files&options
```

**Examples:**
- `http-static:/?dir=/var/www` — serve on root
- `http-static:/assets?dir=/var/www/assets` — serve on `/assets` prefix
- `http-static:/app?dir=/var/www/spa&spaFallback=true` — SPA mode

### Consumer Options

| Parameter | Default | Description |
|-----------|---------|-------------|
| `dir` | (required) | Root directory to serve static files from |
| `port` | `8080` | TCP port |
| `host` | `0.0.0.0` | Bind address |
| `spaFallback` | `false` | Enable SPA mode (serve index.html for unmatched paths) |
| `cacheControl` | `public, max-age=0` | Cache-Control header value |
| `errorPages` | (none) | JSON map of status code → file path, e.g. `{"404":"404.html"}` |

### Features

- **Co-hosting**: Shares port with `http:` API routes
- **Mount path prefix**: URI path derived prefix, longest prefix wins
- **SPA fallback**: GET/HEAD + `Accept: text/html` + no file extension → index.html
- **Custom error pages**: Per-mount error pages for 404, 500, etc.
- **Precompressed serving**: gzip, brotli (`ServeDir`)
- **Cache-Control headers**: Configurable per mount
- **Path traversal prevention**: `..` segments are rejected
- **Directory index**: `index.html` appended for directory requests

### TOML Configuration (Camel.toml)

```toml
[default.components.http-static]
dir = "/var/www"
port = 8080
spaFallback = true
cacheControl = "public, max-age=3600"
```

Error pages are configured as a table:

```toml
[default.components.http-static]
dir = "/var/www"

[default.components.http-static.errorPages]
404 = "errors/404.html"
500 = "errors/500.html"
```

### Routing Precedence

API exact match → static file (longest prefix) → SPA fallback (if qualified) → custom error page → 404

## License

Apache-2.0

## Contributing

Contributions are welcome! Please see the [main repository](https://github.com/kennycallado/rust-camel) for details.