Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions context/reference/PROPERTIES_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

Properties are read-only metadata about the current request and client, available via `proxy_get_property()` in ProxyWasm apps. They provide context that isn't in the HTTP headers themselves.

**Path format:** Always pass the property identifier as a single dotted string in a one-element vec — e.g., `vec!["request.path"]`, `vec!["request.geo.long"]`. Do **not** split on dots (e.g., `vec!["request", "country"]` is incorrect).

---

## Available Properties
Expand Down Expand Up @@ -40,13 +42,13 @@ Properties are read-only metadata about the current request and client, availabl
// In an HttpContext implementation
fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action {
// Get client's country
if let Some(country) = self.get_property(vec!["request", "country"]) {
if let Some(country) = self.get_property(vec!["request.country"]) {
let country_str = String::from_utf8(country).unwrap_or_default();
// Use for geo-routing, access control, etc.
}

// Get client IP
if let Some(ip) = self.get_property(vec!["request", "x_real_ip"]) {
if let Some(ip) = self.get_property(vec!["request.x_real_ip"]) {
// Use for rate limiting, logging, etc.
}

Expand Down
122 changes: 86 additions & 36 deletions docs/CDN_APPS.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ proxy_wasm::main! {{

### Root Context

The root context is a singleton created once when the filter loads. Its primary role is to create a new HTTP context for each incoming request.
The root context is a singleton created once when the filter loads. Its primary role is to create a new HTTP context for each lifecycle callback invocation.

```rust,no_run
# use proxy_wasm::traits::*;
Expand All @@ -109,11 +109,11 @@ impl RootContext for MyAppRoot {
}
```

`get_type()` must return `Some(ContextType::HttpContext)` for HTTP traffic interception. `create_http_context` is called once per request and receives a unique `context_id`.
`get_type()` must return `Some(ContextType::HttpContext)` for HTTP traffic interception. `create_http_context` is called once per lifecycle callback invocation and receives a unique `context_id`.

### HTTP Context

The HTTP context is where request and response processing happens. A new instance is created for each request by `create_http_context`.
The HTTP context is where request and response processing happens. A new instance is created for each lifecycle callback invocation — not once per request. See [Hook State Isolation](#hook-state-isolation) for the consequences this has on state management.

```rust,no_run
# use proxy_wasm::traits::*;
Expand Down Expand Up @@ -151,11 +151,11 @@ All callbacks have default no-op implementations. Override only the phases your

Every lifecycle callback returns an `Action` that controls what happens next.

| Action | Meaning |
| -------------------------------- | --------------------------------------------------------------------------- |
| `Action::Continue` | Pass the request or response through to the next stage |
| `Action::Pause` | Stop processing; used after `send_http_response` to short-circuit origin |
| `Action::StopIterationAndBuffer` | Buffer the current body chunk; continue accumulating until `end_of_stream` |
| Action | Meaning |
| -------------------------------- | -------------------------------------------------------------------------- |
| `Action::Continue` | Pass the request or response through to the next stage |
| `Action::Pause` | Stop processing; used after `send_http_response` to short-circuit origin |
| `Action::StopIterationAndBuffer` | Buffer the current body chunk; continue accumulating until `end_of_stream` |

For body callbacks, return `Action::StopIterationAndBuffer` until `end_of_stream` is `true`, then process the full body and return `Action::Continue`.

Expand All @@ -175,6 +175,40 @@ impl HttpContext for MyApp {
}
```

### Hook State Isolation

On the FastEdge CDN platform, an HTTP context instance exists only for the duration of a single lifecycle callback invocation. It does **not** persist across the request. Different hooks may run on entirely different servers: `on_http_request_headers` runs in nginx, while `on_http_request_body`, `on_http_response_headers`, and `on_http_response_body` run in core-proxy.

This has critical consequences for application design:

- Struct fields on the HTTP context do **not** persist between callbacks.
- A fresh context instance is created for each callback invocation.
- Storing data as a struct field in one callback and reading it in another callback does **not** work.

To pass data between callbacks, use `self.set_property` and `self.get_property` with a custom property path. The host preserves these values across callback invocations for the same logical request:

```rust,no_run
# use proxy_wasm::traits::*;
# use proxy_wasm::types::*;
# struct MyApp;
# impl Context for MyApp {}
impl HttpContext for MyApp {
fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action {
// Store a value for use in a later callback
self.set_property(vec!["my_custom_key"], Some(b"my_value"));
Action::Continue
}

fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action {
// Retrieve the value set in a previous callback
if let Some(value) = self.get_property(vec!["my_custom_key"]) {
let _ = value; // use value
}
Action::Continue
}
}
```

## Request and Response Manipulation

### Reading Headers and Properties
Expand All @@ -188,15 +222,13 @@ impl HttpContext for MyApp {
fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action {
// Read a request header
if let Some(auth) = self.get_http_request_header("Authorization") {
// use auth value
let _ = auth;
let _ = auth; // use auth value
}

// Read a request property (UTF-8 string)
if let Some(path_bytes) = self.get_property(vec!["request.path"]) {
if let Ok(path) = std::str::from_utf8(&path_bytes) {
// use path
let _ = path;
let _ = path; // use path
}
}

Expand Down Expand Up @@ -226,13 +258,15 @@ impl HttpContext for MyApp {
fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action {
// Add a new response header
self.add_http_response_header("x-powered-by", "FastEdge");
// Remove a response header
// Attempt to remove a response header
self.set_http_response_header("server", None);
Action::Continue
}
}
```

**Known limitation**: On the FastEdge CDN platform, passing `None` to `set_http_request_header` or `set_http_response_header` sets the header value to an empty string rather than removing the header entirely. When checking for header absence, test for an empty string as well as a missing value.

### Generating Responses

To short-circuit the request and respond directly to the client without forwarding to origin, call `send_http_response` and return `Action::Pause`.
Expand Down Expand Up @@ -264,14 +298,30 @@ impl HttpContext for MyApp {

CDN apps access request metadata through `self.get_property(vec![...])`. The return type is `Option<Vec<u8>>`.

| Property path | Type | Description |
| -------------------------- | --------------------- | ------------------------------------------- |
| `["request.path"]` | UTF-8 string | Request URL path |
| `["request.query"]` | UTF-8 string | Query string |
| `["request.country"]` | UTF-8 string | Client country code (geo-IP lookup) |
| `["response", "status"]` | 2-byte big-endian u16 | Response status code (response phase only) |

Most properties are UTF-8 strings that can be decoded with `std::str::from_utf8()`. The `response.status` property is a binary-encoded integer, not a string — it must be decoded as a big-endian `u16`:
**Path format:** Always pass the property identifier as a single dotted string in a one-element vec — e.g., `vec!["request.path"]`, `vec!["response.status"]`, `vec!["request.geo.long"]`. Do **not** split on dots (e.g., `vec!["response", "status"]` is incorrect).

| Property | Encoding | Description |
| ---------------------- | --------------------- | -------------------------------------------------------------------------------- |
| `request.path` | UTF-8 string | URL path |
| `request.query` | UTF-8 string | Query string |
| `request.url` | UTF-8 string | Full request URL |
| `request.host` | UTF-8 string | Domain (may have `shield_` prefix on edge shield nodes) |
| `request.scheme` | UTF-8 string | HTTP scheme (from X-Forwarded-Proto) |
| `request.extension` | UTF-8 string | File extension |
| `request.x_real_ip` | UTF-8 string | Client IP address |
| `request.country` | UTF-8 string | 2-letter ISO country code (geo-IP) |
| `request.country.name` | UTF-8 string | Full country name |
| `request.city` | UTF-8 string | City name |
| `request.region` | UTF-8 string | Region/state |
| `request.continent` | UTF-8 string | Continent |
| `request.asn` | UTF-8 string | Autonomous System Number |
| `request.geo.lat` | UTF-8 string | Latitude |
| `request.geo.long` | UTF-8 string | Longitude |
| `response.status` | 2-byte big-endian u16 | Response status code (**binary, NOT a string** — decode with `u16::from_be_bytes`) |

Most properties are UTF-8 strings decoded with `std::str::from_utf8()`. The `response.status` property is binary-encoded and must be decoded as a big-endian `u16`. Do not use `String::from_utf8` for this property.

Geo-IP properties (`request.country`, `request.country.name`, `request.city`, `request.region`, `request.continent`, `request.geo.lat`, `request.geo.long`) are derived from the client IP address.

Comment thread
godronus marked this conversation as resolved.
```rust,no_run
# use proxy_wasm::traits::*;
Expand All @@ -281,7 +331,7 @@ Most properties are UTF-8 strings that can be decoded with `std::str::from_utf8(
impl HttpContext for MyApp {
fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action {
// response.status is a 2-byte big-endian u16 — do NOT use String::from_utf8
if let Some(bytes) = self.get_property(vec!["response", "status"]) {
if let Some(bytes) = self.get_property(vec!["response.status"]) {
if bytes.len() == 2 {
let status = u16::from_be_bytes([bytes[0], bytes[1]]);
println!("upstream status: {}", status);
Expand Down Expand Up @@ -327,15 +377,15 @@ Provides persistent key-value storage. The API shape mirrors `fastedge::key_valu
pub struct Store { /* ... */ }
```

| Method | Return Type | Description |
| ------------------------------------------------------- | ------------------------------------ | ------------------------------------------------------- |
| `Store::new()` | `Result<Self, Error>` | Open the default store |
| `Store::open(name: &str)` | `Result<Self, Error>` | Open a named store |
| `Store::get(key: &str)` | `Result<Option<Vec<u8>>, Error>` | Get the value for a key; `None` if key does not exist |
| `Store::scan(pattern: &str)` | `Result<Vec<String>, Error>` | List keys matching a glob-style pattern |
| `Store::zrange_by_score(key: &str, min: f64, max: f64)` | `Result<Vec<(Vec<u8>, f64)>, Error>` | Get sorted-set members with scores between min and max |
| `Store::zscan(key: &str, pattern: &str)` | `Result<Vec<(Vec<u8>, f64)>, Error>` | Scan sorted-set members matching a pattern |
| `Store::bf_exists(key: &str, item: &str)` | `Result<bool, Error>` | Test whether an item is in a Bloom filter |
| Method | Return Type | Description |
| ------------------------------------------------------- | ------------------------------------ | ------------------------------------------------------ |
| `Store::new()` | `Result<Self, Error>` | Open the default store |
| `Store::open(name: &str)` | `Result<Self, Error>` | Open a named store |
| `Store::get(key: &str)` | `Result<Option<Vec<u8>>, Error>` | Get the value for a key; `None` if key does not exist |
| `Store::scan(pattern: &str)` | `Result<Vec<String>, Error>` | List keys matching a glob-style pattern |
| `Store::zrange_by_score(key: &str, min: f64, max: f64)` | `Result<Vec<(Vec<u8>, f64)>, Error>` | Get sorted-set members with scores between min and max |
| `Store::zscan(key: &str, pattern: &str)` | `Result<Vec<(Vec<u8>, f64)>, Error>` | Scan sorted-set members matching a pattern |
| `Store::bf_exists(key: &str, item: &str)` | `Result<bool, Error>` | Test whether an item is in a Bloom filter |

#### `Error`

Expand All @@ -347,11 +397,11 @@ pub enum Error {
}
```

| Variant | Description |
| --------------- | ------------------------------------------------------------ |
| `NoSuchStore` | The store label is not recognized by the host |
| `AccessDenied` | The application does not have access to the specified store |
| `Other(String)` | An implementation-specific error (e.g., I/O failure) |
| Variant | Description |
| --------------- | ----------------------------------------------------------- |
| `NoSuchStore` | The store label is not recognized by the host |
| `AccessDenied` | The application does not have access to the specified store |
| `Other(String)` | An implementation-specific error (e.g., I/O failure) |

#### Example — Bloom filter check in request headers phase

Expand Down
26 changes: 13 additions & 13 deletions docs/HOST_SERVICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,11 @@ Scans the store for keys matching a glob-style pattern. Returns a list of matchi

Supported glob syntax:

| Pattern | Matches |
| --------- | ------------------------------------------- |
| `*` | Any sequence of characters within a segment |
| `?` | Any single character |
| `[abc]` | Any character in the set |
| Pattern | Matches |
| ------- | ------------------------------------------- |
| `*` | Any sequence of characters within a segment |
| `?` | Any single character |
| `[abc]` | Any character in the set |

```rust,no_run
use fastedge::key_value::Store;
Expand Down Expand Up @@ -345,14 +345,14 @@ async fn main(_request: Request<Body>) -> anyhow::Result<Response<Body>> {

### When to Use Dictionary vs Key-Value vs Secrets

| Criterion | `dictionary` | `key_value` | `secret` |
| ---------------------------- | -------------------------------------- | ----------------------------------------- | ------------------------------------------- |
| **Mutability** | Read-only; set at deployment time | Read-only from application code | Read-only; managed by platform |
| **Value type** | UTF-8 strings only | Arbitrary bytes | Arbitrary bytes |
| **Advanced data structures** | No | Sorted sets, bloom filters, glob scan | No |
| **Confidentiality** | Not encrypted; visible in config | Not encrypted at the application layer | Encrypted at rest; access-controlled |
| **Typical use cases** | Feature flags, routing config, tuning | Caching, counters, state, rate-limit data | API keys, tokens, certificates, credentials |
| **Versioning / rotation** | No | No | Yes, via `get_effective_at` |
| Criterion | `dictionary` | `key_value` | `secret` |
| ---------------------------- | ------------------------------------- | ----------------------------------------- | ------------------------------------------- |
| **Mutability** | Read-only; set at deployment time | Read-only from application code | Read-only; managed by platform |
| **Value type** | UTF-8 strings only | Arbitrary bytes | Arbitrary bytes |
| **Advanced data structures** | No | Sorted sets, bloom filters, glob scan | No |
| **Confidentiality** | Not encrypted; visible in config | Not encrypted at the application layer | Encrypted at rest; access-controlled |
| **Typical use cases** | Feature flags, routing config, tuning | Caching, counters, state, rate-limit data | API keys, tokens, certificates, credentials |
| **Versioning / rotation** | No | No | Yes, via `get_effective_at` |

Use `dictionary` for simple, non-sensitive string configuration that is known at deployment time. Use `key_value` for larger datasets, binary values, or data that requires advanced query patterns. Use `secret` for any value that must be kept confidential.

Expand Down
Loading
Loading