Skip to content

Commit fc1a3b2

Browse files
authored
Merge pull request #64 from G-Core/fix/plugin-docs
update docs
2 parents 9f40778 + 237d074 commit fc1a3b2

7 files changed

Lines changed: 170 additions & 74 deletions

File tree

context/reference/PROPERTIES_REFERENCE.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
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.
44

5+
**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).
6+
57
---
68

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

4850
// Get client IP
49-
if let Some(ip) = self.get_property(vec!["request", "x_real_ip"]) {
51+
if let Some(ip) = self.get_property(vec!["request.x_real_ip"]) {
5052
// Use for rate limiting, logging, etc.
5153
}
5254

docs/CDN_APPS.md

Lines changed: 86 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ proxy_wasm::main! {{
8989

9090
### Root Context
9191

92-
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.
92+
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.
9393

9494
```rust,no_run
9595
# use proxy_wasm::traits::*;
@@ -109,11 +109,11 @@ impl RootContext for MyAppRoot {
109109
}
110110
```
111111

112-
`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`.
112+
`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`.
113113

114114
### HTTP Context
115115

116-
The HTTP context is where request and response processing happens. A new instance is created for each request by `create_http_context`.
116+
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.
117117

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

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

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

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

@@ -175,6 +175,40 @@ impl HttpContext for MyApp {
175175
}
176176
```
177177

178+
### Hook State Isolation
179+
180+
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.
181+
182+
This has critical consequences for application design:
183+
184+
- Struct fields on the HTTP context do **not** persist between callbacks.
185+
- A fresh context instance is created for each callback invocation.
186+
- Storing data as a struct field in one callback and reading it in another callback does **not** work.
187+
188+
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:
189+
190+
```rust,no_run
191+
# use proxy_wasm::traits::*;
192+
# use proxy_wasm::types::*;
193+
# struct MyApp;
194+
# impl Context for MyApp {}
195+
impl HttpContext for MyApp {
196+
fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action {
197+
// Store a value for use in a later callback
198+
self.set_property(vec!["my_custom_key"], Some(b"my_value"));
199+
Action::Continue
200+
}
201+
202+
fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action {
203+
// Retrieve the value set in a previous callback
204+
if let Some(value) = self.get_property(vec!["my_custom_key"]) {
205+
let _ = value; // use value
206+
}
207+
Action::Continue
208+
}
209+
}
210+
```
211+
178212
## Request and Response Manipulation
179213

180214
### Reading Headers and Properties
@@ -188,15 +222,13 @@ impl HttpContext for MyApp {
188222
fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action {
189223
// Read a request header
190224
if let Some(auth) = self.get_http_request_header("Authorization") {
191-
// use auth value
192-
let _ = auth;
225+
let _ = auth; // use auth value
193226
}
194227
195228
// Read a request property (UTF-8 string)
196229
if let Some(path_bytes) = self.get_property(vec!["request.path"]) {
197230
if let Ok(path) = std::str::from_utf8(&path_bytes) {
198-
// use path
199-
let _ = path;
231+
let _ = path; // use path
200232
}
201233
}
202234
@@ -226,13 +258,15 @@ impl HttpContext for MyApp {
226258
fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action {
227259
// Add a new response header
228260
self.add_http_response_header("x-powered-by", "FastEdge");
229-
// Remove a response header
261+
// Attempt to remove a response header
230262
self.set_http_response_header("server", None);
231263
Action::Continue
232264
}
233265
}
234266
```
235267

268+
**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.
269+
236270
### Generating Responses
237271

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

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

267-
| Property path | Type | Description |
268-
| -------------------------- | --------------------- | ------------------------------------------- |
269-
| `["request.path"]` | UTF-8 string | Request URL path |
270-
| `["request.query"]` | UTF-8 string | Query string |
271-
| `["request.country"]` | UTF-8 string | Client country code (geo-IP lookup) |
272-
| `["response", "status"]` | 2-byte big-endian u16 | Response status code (response phase only) |
273-
274-
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`:
301+
**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).
302+
303+
| Property | Encoding | Description |
304+
| ---------------------- | --------------------- | -------------------------------------------------------------------------------- |
305+
| `request.path` | UTF-8 string | URL path |
306+
| `request.query` | UTF-8 string | Query string |
307+
| `request.url` | UTF-8 string | Full request URL |
308+
| `request.host` | UTF-8 string | Domain (may have `shield_` prefix on edge shield nodes) |
309+
| `request.scheme` | UTF-8 string | HTTP scheme (from X-Forwarded-Proto) |
310+
| `request.extension` | UTF-8 string | File extension |
311+
| `request.x_real_ip` | UTF-8 string | Client IP address |
312+
| `request.country` | UTF-8 string | 2-letter ISO country code (geo-IP) |
313+
| `request.country.name` | UTF-8 string | Full country name |
314+
| `request.city` | UTF-8 string | City name |
315+
| `request.region` | UTF-8 string | Region/state |
316+
| `request.continent` | UTF-8 string | Continent |
317+
| `request.asn` | UTF-8 string | Autonomous System Number |
318+
| `request.geo.lat` | UTF-8 string | Latitude |
319+
| `request.geo.long` | UTF-8 string | Longitude |
320+
| `response.status` | 2-byte big-endian u16 | Response status code (**binary, NOT a string** — decode with `u16::from_be_bytes`) |
321+
322+
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.
323+
324+
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.
275325

276326
```rust,no_run
277327
# use proxy_wasm::traits::*;
@@ -281,7 +331,7 @@ Most properties are UTF-8 strings that can be decoded with `std::str::from_utf8(
281331
impl HttpContext for MyApp {
282332
fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action {
283333
// response.status is a 2-byte big-endian u16 — do NOT use String::from_utf8
284-
if let Some(bytes) = self.get_property(vec!["response", "status"]) {
334+
if let Some(bytes) = self.get_property(vec!["response.status"]) {
285335
if bytes.len() == 2 {
286336
let status = u16::from_be_bytes([bytes[0], bytes[1]]);
287337
println!("upstream status: {}", status);
@@ -327,15 +377,15 @@ Provides persistent key-value storage. The API shape mirrors `fastedge::key_valu
327377
pub struct Store { /* ... */ }
328378
```
329379

330-
| Method | Return Type | Description |
331-
| ------------------------------------------------------- | ------------------------------------ | ------------------------------------------------------- |
332-
| `Store::new()` | `Result<Self, Error>` | Open the default store |
333-
| `Store::open(name: &str)` | `Result<Self, Error>` | Open a named store |
334-
| `Store::get(key: &str)` | `Result<Option<Vec<u8>>, Error>` | Get the value for a key; `None` if key does not exist |
335-
| `Store::scan(pattern: &str)` | `Result<Vec<String>, Error>` | List keys matching a glob-style pattern |
336-
| `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 |
337-
| `Store::zscan(key: &str, pattern: &str)` | `Result<Vec<(Vec<u8>, f64)>, Error>` | Scan sorted-set members matching a pattern |
338-
| `Store::bf_exists(key: &str, item: &str)` | `Result<bool, Error>` | Test whether an item is in a Bloom filter |
380+
| Method | Return Type | Description |
381+
| ------------------------------------------------------- | ------------------------------------ | ------------------------------------------------------ |
382+
| `Store::new()` | `Result<Self, Error>` | Open the default store |
383+
| `Store::open(name: &str)` | `Result<Self, Error>` | Open a named store |
384+
| `Store::get(key: &str)` | `Result<Option<Vec<u8>>, Error>` | Get the value for a key; `None` if key does not exist |
385+
| `Store::scan(pattern: &str)` | `Result<Vec<String>, Error>` | List keys matching a glob-style pattern |
386+
| `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 |
387+
| `Store::zscan(key: &str, pattern: &str)` | `Result<Vec<(Vec<u8>, f64)>, Error>` | Scan sorted-set members matching a pattern |
388+
| `Store::bf_exists(key: &str, item: &str)` | `Result<bool, Error>` | Test whether an item is in a Bloom filter |
339389

340390
#### `Error`
341391

@@ -347,11 +397,11 @@ pub enum Error {
347397
}
348398
```
349399

350-
| Variant | Description |
351-
| --------------- | ------------------------------------------------------------ |
352-
| `NoSuchStore` | The store label is not recognized by the host |
353-
| `AccessDenied` | The application does not have access to the specified store |
354-
| `Other(String)` | An implementation-specific error (e.g., I/O failure) |
400+
| Variant | Description |
401+
| --------------- | ----------------------------------------------------------- |
402+
| `NoSuchStore` | The store label is not recognized by the host |
403+
| `AccessDenied` | The application does not have access to the specified store |
404+
| `Other(String)` | An implementation-specific error (e.g., I/O failure) |
355405

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

docs/HOST_SERVICES.md

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,11 @@ Scans the store for keys matching a glob-style pattern. Returns a list of matchi
7676

7777
Supported glob syntax:
7878

79-
| Pattern | Matches |
80-
| --------- | ------------------------------------------- |
81-
| `*` | Any sequence of characters within a segment |
82-
| `?` | Any single character |
83-
| `[abc]` | Any character in the set |
79+
| Pattern | Matches |
80+
| ------- | ------------------------------------------- |
81+
| `*` | Any sequence of characters within a segment |
82+
| `?` | Any single character |
83+
| `[abc]` | Any character in the set |
8484

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

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

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

357357
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.
358358

0 commit comments

Comments
 (0)