Skip to content

Commit

Permalink
Merge pull request #54 from fermyon/default-headers
Browse files Browse the repository at this point in the history
Add default headers to the `incoming-request` in router
  • Loading branch information
rylev authored Apr 29, 2024
2 parents a4d52e0 + 21c4de5 commit d2cd0d6
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 d2cd0d6

Please sign in to comment.