Skip to content

Commit

Permalink
Add default headers to request in router
Browse files Browse the repository at this point in the history
Spin runtimes are expected to add certain headers to each request as can be seen [here](https://github.com/fermyon/spin/blob/efac5cf2249987b80bcb2c11a76564547df72327/crates/trigger-http/src/lib.rs#L498-L534). The most appropriate place for this to happen in `spin-test` is in the router. However, `wasi:http/types/incoming-request` does not allow for modifying the headers as the headers are set to immutable (and any mutating function called on them returns an `wasi:http/types/header-error.immutable`). This is a rather fundamental shortcoming of `wasi:[email protected]` that we won't be able to get around for the foreseeable future.

To work around the above, we add an optional `incoming-body` to `fermyon:spin-test/http-helper/new-request` which allows the non-body parts of the request to come from the `outgoing-request` while the body comes from the `incoming-request`.

Signed-off-by: Ryan Levick <[email protected]>
  • Loading branch information
rylev committed Apr 29, 2024
1 parent dc60b9d commit 21c4de5
Show file tree
Hide file tree
Showing 9 changed files with 209 additions and 45 deletions.
35 changes: 27 additions & 8 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/router/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = { workspace = true }
bitflags = "2.5.0"
wit-bindgen-rt = "0.24.0"
toml = { workspace = true }
spin-manifest = { workspace = true }
spin-http = { git = "https://github.com/fermyon/spin", default-features = false }

[lib]
crate-type = ["cdylib"]
Expand Down
78 changes: 55 additions & 23 deletions crates/router/src/bindings.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
// Generated by `wit-bindgen` 0.24.0. DO NOT EDIT!
// Options used:
pub type OutgoingRequest = wasi::http::types::OutgoingRequest;
pub type IncomingRequest = wasi::http::types::IncomingRequest;
pub type IncomingBody = wasi::http::types::IncomingBody;
#[allow(unused_unsafe, clippy::all)]
pub fn get_manifest() -> _rt::String {
unsafe {
Expand Down Expand Up @@ -47,6 +50,32 @@ pub fn set_component_id(component_id: &str) {
wit_import(ptr0.cast_mut(), len0);
}
}
#[allow(unused_unsafe, clippy::all)]
/// See `fermyon:spin-test/http-helper/new-request` for documentation on this function
pub fn new_request(
request: OutgoingRequest,
incoming_body: Option<IncomingBody>,
) -> IncomingRequest {
unsafe {
let (result0_0, result0_1) = match &incoming_body {
Some(e) => (1i32, (e).take_handle() as i32),
None => (0i32, 0i32),
};
#[cfg(target_arch = "wasm32")]
#[link(wasm_import_module = "$root")]
extern "C" {
#[link_name = "new-request"]
fn wit_import(_: i32, _: i32, _: i32) -> i32;
}

#[cfg(not(target_arch = "wasm32"))]
fn wit_import(_: i32, _: i32, _: i32) -> i32 {
unreachable!()
}
let ret = wit_import((&request).take_handle() as i32, result0_0, result0_1);
wasi::http::types::IncomingRequest::from_handle(ret as u32)
}
}
#[allow(dead_code)]
pub mod wasi {
#[allow(dead_code)]
Expand Down Expand Up @@ -7662,21 +7691,21 @@ pub(crate) use __export_router_impl as export;
#[cfg(target_arch = "wasm32")]
#[link_section = "component-type:wit-bindgen:0.24.0:router:encoded world"]
#[doc(hidden)]
pub static __WIT_BINDGEN_COMPONENT_TYPE: [u8; 6523] = *b"\
\0asm\x0d\0\x01\0\0\x19\x16wit-component-encoding\x04\0\x07\xfe1\x01A\x02\x01A\x19\
\x01B\x0a\x04\0\x08pollable\x03\x01\x01h\0\x01@\x01\x04self\x01\0\x7f\x04\0\x16[\
method]pollable.ready\x01\x02\x01@\x01\x04self\x01\x01\0\x04\0\x16[method]pollab\
le.block\x01\x03\x01p\x01\x01py\x01@\x01\x02in\x04\0\x05\x04\0\x04poll\x01\x06\x03\
\x01\x12wasi:io/[email protected]\x05\0\x02\x03\0\0\x08pollable\x01B\x0f\x02\x03\x02\x01\
\x01\x04\0\x08pollable\x03\0\0\x01w\x04\0\x07instant\x03\0\x02\x01w\x04\0\x08dur\
ation\x03\0\x04\x01@\0\0\x03\x04\0\x03now\x01\x06\x01@\0\0\x05\x04\0\x0aresoluti\
on\x01\x07\x01i\x01\x01@\x01\x04when\x03\0\x08\x04\0\x11subscribe-instant\x01\x09\
\x01@\x01\x04when\x05\0\x08\x04\0\x12subscribe-duration\x01\x0a\x03\x01!wasi:clo\
cks/monotonic[email protected]\x05\x02\x01B\x04\x04\0\x05error\x03\x01\x01h\0\x01@\x01\
\x04self\x01\0s\x04\0\x1d[method]error.to-debug-string\x01\x02\x03\x01\x13wasi:i\
o/error@0.2.0\x05\x03\x02\x03\0\x02\x05error\x01B(\x02\x03\x02\x01\x04\x04\0\x05\
error\x03\0\0\x02\x03\x02\x01\x01\x04\0\x08pollable\x03\0\x02\x01i\x01\x01q\x02\x15\
last-operation-failed\x01\x04\0\x06closed\0\0\x04\0\x0cstream-error\x03\0\x05\x04\
pub static __WIT_BINDGEN_COMPONENT_TYPE: [u8; 6682] = *b"\
\0asm\x0d\0\x01\0\0\x19\x16wit-component-encoding\x04\0\x07\x9d3\x01A\x02\x01A$\x01\
B\x0a\x04\0\x08pollable\x03\x01\x01h\0\x01@\x01\x04self\x01\0\x7f\x04\0\x16[meth\
od]pollable.ready\x01\x02\x01@\x01\x04self\x01\x01\0\x04\0\x16[method]pollable.b\
lock\x01\x03\x01p\x01\x01py\x01@\x01\x02in\x04\0\x05\x04\0\x04poll\x01\x06\x03\x01\
\x12wasi:io/[email protected]\x05\0\x02\x03\0\0\x08pollable\x01B\x0f\x02\x03\x02\x01\x01\
\x04\0\x08pollable\x03\0\0\x01w\x04\0\x07instant\x03\0\x02\x01w\x04\0\x08duratio\
n\x03\0\x04\x01@\0\0\x03\x04\0\x03now\x01\x06\x01@\0\0\x05\x04\0\x0aresolution\x01\
\x07\x01i\x01\x01@\x01\x04when\x03\0\x08\x04\0\x11subscribe-instant\x01\x09\x01@\
\x01\x04when\x05\0\x08\x04\0\x12subscribe-duration\x01\x0a\x03\x01!wasi:clocks/m\
onotonic[email protected]\x05\x02\x01B\x04\x04\0\x05error\x03\x01\x01h\0\x01@\x01\x04\
self\x01\0s\x04\0\x1d[method]error.to-debug-string\x01\x02\x03\x01\x13wasi:io/er\
ror@0.2.0\x05\x03\x02\x03\0\x02\x05error\x01B(\x02\x03\x02\x01\x04\x04\0\x05erro\
r\x03\0\0\x02\x03\x02\x01\x01\x04\0\x08pollable\x03\0\x02\x01i\x01\x01q\x02\x15l\
ast-operation-failed\x01\x04\0\x06closed\0\0\x04\0\x0cstream-error\x03\0\x05\x04\
\0\x0cinput-stream\x03\x01\x04\0\x0doutput-stream\x03\x01\x01h\x07\x01p}\x01j\x01\
\x0a\x01\x06\x01@\x02\x04self\x09\x03lenw\0\x0b\x04\0\x19[method]input-stream.re\
ad\x01\x0c\x04\0\"[method]input-stream.blocking-read\x01\x0c\x01j\x01w\x01\x06\x01\
Expand Down Expand Up @@ -7785,14 +7814,17 @@ e\x01\x8c\x01\x03\x01\x15wasi:http/[email protected]\x05\x09\x02\x03\0\x04\x10incoming
-request\x02\x03\0\x04\x11response-outparam\x01B\x08\x02\x03\x02\x01\x0a\x04\0\x10\
incoming-request\x03\0\0\x02\x03\x02\x01\x0b\x04\0\x11response-outparam\x03\0\x02\
\x01i\x01\x01i\x03\x01@\x02\x07request\x04\x0cresponse-out\x05\x01\0\x04\0\x06ha\
ndle\x01\x06\x03\x01\x20wasi:http/[email protected]\x05\x0c\x01@\0\0s\x03\0\
\x0cget-manifest\x01\x0d\x01@\x01\x0ccomponent-ids\x01\0\x03\0\x10set-component-\
id\x01\x0e\x01B\x08\x02\x03\x02\x01\x0a\x04\0\x10incoming-request\x03\0\0\x02\x03\
\x02\x01\x0b\x04\0\x11response-outparam\x03\0\x02\x01i\x01\x01i\x03\x01@\x02\x07\
request\x04\x0cresponse-out\x05\x01\0\x04\0\x06handle\x01\x06\x04\x01\x20wasi:ht\
tp/[email protected]\x05\x0f\x04\x01\x15fermyon:router/router\x04\0\x0b\x0c\
\x01\0\x06router\x03\0\0\0G\x09producers\x01\x0cprocessed-by\x02\x0dwit-componen\
t\x070.202.0\x10wit-bindgen-rust\x060.24.0";
ndle\x01\x06\x03\x01\x20wasi:http/[email protected]\x05\x0c\x02\x03\0\x04\x10\
outgoing-request\x03\0\x10outgoing-request\x03\0\x0d\x03\0\x10incoming-request\x03\
\0\x0a\x02\x03\0\x04\x0dincoming-body\x03\0\x0dincoming-body\x03\0\x10\x01@\0\0s\
\x03\0\x0cget-manifest\x01\x12\x01@\x01\x0ccomponent-ids\x01\0\x03\0\x10set-comp\
onent-id\x01\x13\x01i\x0e\x01i\x11\x01k\x15\x01i\x0f\x01@\x02\x07request\x14\x0d\
incoming-body\x16\0\x17\x03\0\x0bnew-request\x01\x18\x01B\x08\x02\x03\x02\x01\x0a\
\x04\0\x10incoming-request\x03\0\0\x02\x03\x02\x01\x0b\x04\0\x11response-outpara\
m\x03\0\x02\x01i\x01\x01i\x03\x01@\x02\x07request\x04\x0cresponse-out\x05\x01\0\x04\
\0\x06handle\x01\x06\x04\x01\x20wasi:http/[email protected]\x05\x19\x04\x01\
\x15fermyon:router/router\x04\0\x0b\x0c\x01\0\x06router\x03\0\0\0G\x09producers\x01\
\x0cprocessed-by\x02\x0dwit-component\x070.202.0\x10wit-bindgen-rust\x060.24.0";

#[inline(never)]
#[doc(hidden)]
Expand Down
105 changes: 96 additions & 9 deletions crates/router/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
#[allow(warnings)]
mod bindings;

use anyhow::Context as _;
use bindings::{
exports::wasi::http::incoming_handler::{IncomingRequest, ResponseOutparam},
wasi::http::types::ErrorCode,
};

use crate::bindings::{
exports::wasi::http::incoming_handler::Guest,
wasi::http::incoming_handler::handle as downstream,
wasi::http::types::{
ErrorCode, Headers, IncomingRequest, OutgoingRequest, ResponseOutparam, Scheme,
},
};

struct Component;
Expand Down Expand Up @@ -46,11 +45,20 @@ impl Guest for Component {
.split_once('?')
.map(|(path, _)| path)
.unwrap_or(&path_with_query);
let component_id = routes
let routing = routes
.iter()
.find(|(route, _)| path.starts_with(route) || *route == "/...")
.map(|(_, comp)| comp);
if let Some(component_id) = component_id {
.find(|(route, _)| path.starts_with(route) || *route == "/...");
if let Some((route, component_id)) = routing {
let request = match apply_request_transformations(request, (*route).to_owned()) {
Ok(request) => request,
Err(e) => {
ResponseOutparam::set(
response_out,
Err(ErrorCode::InternalError(Some(e.to_string()))),
);
return;
}
};
bindings::set_component_id(component_id);
downstream(request, response_out)
} else {
Expand All @@ -65,4 +73,83 @@ impl Guest for Component {
}
}

/// Apply any request transformations needed for the given route.
fn apply_request_transformations(
request: IncomingRequest,
raw_route: String,
) -> anyhow::Result<IncomingRequest> {
let headers_to_add = calculate_default_headers(&request, raw_route)
.context("could not calculate default headers to for request")?
.into_iter()
.flat_map(|(k, v)| {
k.iter()
.map(move |s| (s.to_string(), v.clone().into_bytes()))
})
.chain(request.headers().entries());
let headers = Headers::new();
for (key, value) in headers_to_add {
headers.append(&key, &value).unwrap();
}
let new = OutgoingRequest::new(headers);
let _ = new.set_scheme(request.scheme().as_ref());
let _ = new.set_authority(request.authority().as_deref());
let _ = new.set_method(&request.method());
let _ = new.set_path_with_query(request.path_with_query().as_deref());
Ok(bindings::new_request(new, Some(request.consume().unwrap())))
}

const FULL_URL: &[&str] = &["SPIN-FULL-URL", "X-FULL-URL"];
const PATH_INFO: &[&str] = &["SPIN-PATH-INFO", "PATH-INFO"];
const MATCHED_ROUTE: &[&str] = &["SPIN-MATCHED-ROUTE", "X-MATCHED-ROUTE"];
const COMPONENT_ROUTE: &[&str] = &["SPIN-COMPONENT-ROUTE", "X-COMPONENT-ROUTE"];
const RAW_COMPONENT_ROUTE: &[&str] = &["SPIN-RAW-COMPONENT-ROUTE", "X-RAW-COMPONENT-ROUTE"];
const BASE_PATH: &[&str] = &["SPIN-BASE-PATH", "X-BASE-PATH"];
const CLIENT_ADDR: &[&str] = &["SPIN-CLIENT-ADDR", "X-CLIENT-ADDR"];
/// Calculate the default headers for the given request.
fn calculate_default_headers<'a>(
req: &IncomingRequest,
raw_route: String,
) -> anyhow::Result<Vec<(&'a [&'a str], String)>> {
let mut res = vec![];
// TODO: calculate base path from manifest
let base = "/".to_owned();
let abs_path = req.path_with_query().context("cannot get path and query")?;
let scheme = req.scheme();
let scheme = match scheme.as_ref().unwrap_or(&Scheme::Https) {
Scheme::Http => "http",
Scheme::Https => "https",
Scheme::Other(s) => s,
};
let host = req
.headers()
.get(&"host".to_owned())
.into_iter()
.find(|v| !v.is_empty())
.map(String::from_utf8)
.transpose()
.context("expected 'host' header to be UTF-8 encoded but it was not")?
.unwrap_or_else(|| "localhost".to_owned());

let matched_route =
spin_http::routes::RoutePattern::sanitize_with_base(base.clone(), raw_route.clone());

let path_info = spin_http::routes::RoutePattern::from(base.clone(), raw_route.clone())
.relative(&abs_path)?;
let full_url = format!("{}://{}{}", scheme, host, abs_path);
let component_route = raw_route
.strip_suffix("/...")
.unwrap_or(&raw_route)
.to_owned();

res.push((PATH_INFO, path_info));
res.push((FULL_URL, full_url));
res.push((MATCHED_ROUTE, matched_route));
res.push((BASE_PATH, base));
res.push((RAW_COMPONENT_ROUTE, raw_route));
res.push((COMPONENT_ROUTE, component_route));
res.push((CLIENT_ADDR, "127.0.0.1:0".to_owned()));

Ok(res)
}

bindings::export!(Component with_types_in bindings);
4 changes: 4 additions & 0 deletions crates/router/wit/world.wit
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@ world router {
import wasi:http/incoming-handler@0.2.0;
import get-manifest: func() -> string;
import set-component-id: func(component-id: string);

use wasi:http/types@0.2.0.{outgoing-request, incoming-request, incoming-body};
/// See `fermyon:spin-test/http-helper/new-request` for documentation on this function
import new-request: func(request: outgoing-request, incoming-body: option<incoming-body>) -> incoming-request;
}
2 changes: 1 addition & 1 deletion crates/spin-test-sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use bindings::wasi::http;

/// Make a request to the Spin app and return the response.
pub fn perform_request(request: http::types::OutgoingRequest) -> http::types::IncomingResponse {
let request = spin_test::http_helper::new_request(request);
let request = spin_test::http_helper::new_request(request, None);
let (response_out, response_receiver) = spin_test::http_helper::new_response();
http::incoming_handler::handle(request, response_out);
response_receiver.get().unwrap()
Expand Down
8 changes: 6 additions & 2 deletions host-wit/world.wit
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,18 @@ world runner {
interface http-helper {
use wasi:http/types@0.2.0.{
incoming-request, incoming-response, outgoing-response,
outgoing-request, response-outparam, future-incoming-response
outgoing-request, response-outparam, future-incoming-response,
incoming-body
};
/// A receiver of an `incoming-response`
resource response-receiver {
get: func() -> option<incoming-response>;
}
/// Create an `incoming-request` from an `outgoing-request`
new-request: func(request: outgoing-request) -> incoming-request;
///
/// An optional `incoming-body` can also be supplied which will be
/// used instead of the body of the `outgoing-request`.
new-request: func(request: outgoing-request, incoming-body: option<incoming-body>) -> incoming-request;
/// Get a pair of a `response-outparam` and a `response-receiver`
new-response: func() -> tuple<response-outparam, response-receiver>;
/// Create a `future-incoming-response` from an `outgoing-response`
Expand Down
Loading

0 comments on commit 21c4de5

Please sign in to comment.