docs(adapters): pin routing, SSE-payload, and percent contracts in the module doc (FWD-18/19)
Module doc gains three sections: the FWD-18 routing rule (placeholder keys never double-emit as query; structural path values are INVALID_INPUT), the FWD-19 percent trade-off (% in values always encoded; % in assembly-supplied template/base text survives verbatim — documented instead of rejected, per the ADR-066 assembly trust boundary), and the FWD-17 streaming payload contract. FWD-19 behavior pinned by a wire-level template test.
This commit is contained in:
@@ -38,6 +38,29 @@
|
|||||||
//! the buffer grows, so the reassembly buffer can never exceed the
|
//! the buffer grows, so the reassembly buffer can never exceed the
|
||||||
//! line cap.
|
//! line cap.
|
||||||
//!
|
//!
|
||||||
|
//! # Streaming payload contract (FWD-17)
|
||||||
|
//!
|
||||||
|
//! Each parsed SSE frame becomes one success envelope:
|
||||||
|
//!
|
||||||
|
//! - A `data:` payload that is valid JSON surfaces as the decoded value
|
||||||
|
//! itself — `123` as a number, `"123"` as a string (the raw-text
|
||||||
|
//! fallback this replaces erased that distinction). This holds even
|
||||||
|
//! when the frame carries an `event:` name.
|
||||||
|
//! - Any other payload surfaces as the
|
||||||
|
//! `{"data": <raw payload>, "event": <name|null>}` wrapper: the raw
|
||||||
|
//! text stays field-addressable and an upstream's named-event
|
||||||
|
//! conventions (`event: error`, `event: ping`, …) are visible on
|
||||||
|
//! non-JSON frames. The name is the frame's `event:` field (WHATWG
|
||||||
|
//! last-wins); `null` when the upstream sent none — the spec's
|
||||||
|
//! implicit `message` default is *not* substituted, so "upstream
|
||||||
|
//! named it" and "default name" remain distinguishable.
|
||||||
|
//! - An empty payload is `Null`.
|
||||||
|
//!
|
||||||
|
//! Known limitation: a JSON body under a named event surfaces as the
|
||||||
|
//! decoded value only — the event name is not carried on JSON frames;
|
||||||
|
//! a non-JSON body under a named event is the only combination where
|
||||||
|
//! both are visible.
|
||||||
|
//!
|
||||||
//! # Credentials and input routing
|
//! # Credentials and input routing
|
||||||
//!
|
//!
|
||||||
//! The forwarding handler is the no-env-vars credential injection point
|
//! The forwarding handler is the no-env-vars credential injection point
|
||||||
@@ -56,6 +79,36 @@
|
|||||||
//! request header, the declared `GATEWAY_BODY_KEY` property becomes the
|
//! request header, the declared `GATEWAY_BODY_KEY` property becomes the
|
||||||
//! request body, and every other declared key becomes an upstream query
|
//! request body, and every other declared key becomes an upstream query
|
||||||
//! parameter.
|
//! parameter.
|
||||||
|
//!
|
||||||
|
//! # Input routing and path placeholders (FWD-18)
|
||||||
|
//!
|
||||||
|
//! A key that matches a `{placeholder}` in the path template is consumed
|
||||||
|
//! by the path and never also emits as a query parameter, regardless of
|
||||||
|
//! its value's shape — the placeholder check precedes query routing in
|
||||||
|
//! the request builder. A placeholder renders exactly one literal path
|
||||||
|
//! segment, so its value must be a scalar: object/array values fail with
|
||||||
|
//! `INVALID_INPUT` (structural values have no faithful single-segment
|
||||||
|
//! rendering; splicing the minified JSON into the path was the
|
||||||
|
//! pre-decision behavior and is rejected now).
|
||||||
|
//!
|
||||||
|
//! # Percent handling in the rendered path (FWD-19)
|
||||||
|
//!
|
||||||
|
//! Two different contracts apply, by design:
|
||||||
|
//!
|
||||||
|
//! - A `%` arriving inside a *value* is always encoded (`%` → `%25`),
|
||||||
|
//! so values carrying `%2F` cannot be mistaken for this crate's own
|
||||||
|
//! escapes and a value can never inject URL structure.
|
||||||
|
//! - A `%` in *template/base text* survives verbatim. Templates and
|
||||||
|
//! base URLs are assembly-supplied (ADR-066 trust boundary), so an
|
||||||
|
//! assembly that writes `/s3%2Fkeys` is presumed to mean a
|
||||||
|
//! pre-encoded segment for upstreams that route `%2F` differently
|
||||||
|
//! from `/` — that upstream-semantics choice belongs to the assembly,
|
||||||
|
//! not this crate. No injection results: the surviving `%2F` still
|
||||||
|
//! forms a single segment (the origin check plus the two-pass
|
||||||
|
//! percent-encoding over template text see to that), and the
|
||||||
|
//! value-side rule above means every bare `%` in a rendered path
|
||||||
|
//! traces to template text the assembly author wrote.
|
||||||
|
//!
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -1608,6 +1661,32 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// FWD-19 pin (documented trade-off, not a rejection): literal `%`
|
||||||
|
/// in template text is preserved as-is, so an assembly-supplied
|
||||||
|
/// pre-encoded template like `/s3%2Fkeys` reaches the upstream
|
||||||
|
/// verbatim (most stacks route `%2F` differently from `/` — the
|
||||||
|
/// assembly layer owns that choice, ADR-066). A `%` arriving in a
|
||||||
|
/// *value* is always encoded to `%25`, so the only bare `%` in a
|
||||||
|
/// rendered path is one the template author placed.
|
||||||
|
#[test]
|
||||||
|
fn percent_in_template_text_survives_and_percent_in_values_are_always_encoded() {
|
||||||
|
let url = request_url(
|
||||||
|
"https://api.example.com",
|
||||||
|
"/s3%2Fkeys/{name}",
|
||||||
|
json!({"name": "x"}),
|
||||||
|
)
|
||||||
|
.expect("template text may carry literal percent escapes");
|
||||||
|
assert_eq!(url.path(), "/s3%2Fkeys/x");
|
||||||
|
|
||||||
|
let url = request_url(
|
||||||
|
"https://api.example.com",
|
||||||
|
"/files/{name}",
|
||||||
|
json!({"name": "a%2Fb"}),
|
||||||
|
)
|
||||||
|
.expect("value percent is encoded, not preserved");
|
||||||
|
assert_eq!(url.path(), "/files/a%252Fb");
|
||||||
|
}
|
||||||
|
|
||||||
fn ctx_with_capability(namespace: &str, value: String) -> OperationContext {
|
fn ctx_with_capability(namespace: &str, value: String) -> OperationContext {
|
||||||
let mut ctx = noop_context();
|
let mut ctx = noop_context();
|
||||||
ctx.capabilities = Capabilities::new().with_http_token(namespace, value);
|
ctx.capabilities = Capabilities::new().with_http_token(namespace, value);
|
||||||
|
|||||||
Reference in New Issue
Block a user