-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow pre-parsing of ESI document, add example of fetching per-user fragment variants #30
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
[target.wasm32-wasi] | ||
rustflags = ["-C", "debuginfo=2"] | ||
runner = "viceroy run -C fastly.toml -- " | ||
|
||
[build] | ||
target = "wasm32-wasi" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
[package] | ||
name = "esi_example_variants" | ||
version.workspace = true | ||
authors.workspace = true | ||
license.workspace = true | ||
edition.workspace = true | ||
publish = false | ||
|
||
[dependencies] | ||
fastly = "^0.11" | ||
esi = { path = "../../esi" } | ||
log = "^0.4" | ||
env_logger = "^0.11" |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,28 @@ | ||||||
# This file describes a Fastly Compute package. To learn more visit: | ||||||
# https://developer.fastly.com/reference/fastly-toml/ | ||||||
|
||||||
authors = ["kailan@enviark.com"] | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I think we usually leaving those empty or with fastly emails, no? |
||||||
description = "" | ||||||
language = "rust" | ||||||
manifest_version = 2 | ||||||
name = "esi_example_variants" | ||||||
service_id = "" | ||||||
|
||||||
[local_server] | ||||||
|
||||||
[local_server.backends] | ||||||
|
||||||
[local_server.backends.mock-s3] | ||||||
url = "https://mock-s3.edgecompute.app" | ||||||
override_host = "mock-s3.edgecompute.app" | ||||||
|
||||||
[scripts] | ||||||
build = "cargo build --bin esi_example_variants --release --target wasm32-wasi --color always" | ||||||
|
||||||
[setup] | ||||||
|
||||||
[setup.backends] | ||||||
|
||||||
[setup.backends.mock-s3] | ||||||
address = "mock-s3.edgecompute.app" | ||||||
port = 443 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<title>My Broken Website</title> | ||
</head> | ||
<body> | ||
<header style="background: #f1f1f1; padding: 16px"> | ||
<h1>My Broken Website</h1> | ||
</header> | ||
<p>There is <em>some</em> valid content on this page, like the text you're reading right now.</p> | ||
<p>But watch this... I'm about to include an ESI fragment that does not exist:</p> | ||
<esi:include src="/_fragments/doesnotexist.html"/> | ||
<p>If you're seeing this text in the browser, then something is broken.</p> | ||
</body> | ||
</html> |
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,123 @@ | ||||||||||||||
use std::{ | ||||||||||||||
collections::{HashMap, VecDeque}, | ||||||||||||||
io::Write, | ||||||||||||||
}; | ||||||||||||||
Comment on lines
+1
to
+4
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need to use
Suggested change
|
||||||||||||||
|
||||||||||||||
use esi::{parse_tags, Reader, Writer}; | ||||||||||||||
use fastly::{http::StatusCode, mime, Request, Response}; | ||||||||||||||
use log::{error, info}; | ||||||||||||||
|
||||||||||||||
// Take a list of fragment URLs and return a map that maps each URL to a variant for the specific request. | ||||||||||||||
fn get_variant_urls(urls: Vec<String>) -> HashMap<String, String> { | ||||||||||||||
let mut variant_urls = HashMap::new(); | ||||||||||||||
for url in urls { | ||||||||||||||
// For demonstration, add a query parameter to each request | ||||||||||||||
let variant_url = if url.contains('?') { | ||||||||||||||
format!("{}&variant=1", url) | ||||||||||||||
} else { | ||||||||||||||
format!("{}?variant=1", url) | ||||||||||||||
}; | ||||||||||||||
variant_urls.insert(url, variant_url); | ||||||||||||||
} | ||||||||||||||
variant_urls | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
fn main() { | ||||||||||||||
env_logger::builder() | ||||||||||||||
.filter(None, log::LevelFilter::Trace) | ||||||||||||||
.init(); | ||||||||||||||
|
||||||||||||||
let req = Request::from_client(); | ||||||||||||||
|
||||||||||||||
if req.get_path() != "/" { | ||||||||||||||
Response::from_status(StatusCode::NOT_FOUND).send_to_client(); | ||||||||||||||
return; | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
// Generate synthetic test response from "index.html" file. | ||||||||||||||
// You probably want replace this with a backend call, e.g. `req.clone_without_body().send("origin_0")` | ||||||||||||||
let mut beresp = | ||||||||||||||
Response::from_body(include_str!("index.html")).with_content_type(mime::TEXT_HTML); | ||||||||||||||
|
||||||||||||||
// If the response is HTML, we can parse it for ESI tags. | ||||||||||||||
if beresp | ||||||||||||||
.get_content_type() | ||||||||||||||
.is_some_and(|c| c.subtype() == mime::HTML) | ||||||||||||||
{ | ||||||||||||||
let processor = esi::Processor::new(Some(req), esi::Configuration::default()); | ||||||||||||||
|
||||||||||||||
// Create a response to send the headers to the client | ||||||||||||||
let resp = Response::from_status(StatusCode::OK).with_content_type(mime::TEXT_HTML); | ||||||||||||||
|
||||||||||||||
// Send the response headers to the client and open an output stream | ||||||||||||||
let output_writer = resp.stream_to_client(); | ||||||||||||||
|
||||||||||||||
// Set up an XML writer to write directly to the client output stream. | ||||||||||||||
let mut xml_writer = Writer::new(output_writer); | ||||||||||||||
|
||||||||||||||
// Parse the ESI document and store it in memory | ||||||||||||||
let mut events = VecDeque::new(); | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need to use
Suggested change
|
||||||||||||||
let mut beresp_reader = Reader::from_reader(beresp.take_body()); | ||||||||||||||
parse_tags("esi", &mut beresp_reader, &mut |event| { | ||||||||||||||
events.push_back(event); | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need to use
Suggested change
|
||||||||||||||
Ok(()) | ||||||||||||||
}) | ||||||||||||||
.expect("failed to parse ESI template"); | ||||||||||||||
|
||||||||||||||
// Extract the `src` URLs from ESI includes | ||||||||||||||
let urls = events | ||||||||||||||
.iter() | ||||||||||||||
.filter_map(|event| match event { | ||||||||||||||
esi::Event::ESI(esi::Tag::Include { src, .. }) => Some(src.clone()), | ||||||||||||||
_ => None, | ||||||||||||||
}) | ||||||||||||||
.collect::<Vec<_>>(); | ||||||||||||||
|
||||||||||||||
// Check the variant database to determine the URLs to fetch | ||||||||||||||
let variant_urls = get_variant_urls(urls); | ||||||||||||||
|
||||||||||||||
// Process the already-parsed ESI document, replacing the request URLs with the variant URLs | ||||||||||||||
let result = processor.process_parsed_document( | ||||||||||||||
events, | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need to use
Suggested change
|
||||||||||||||
&mut xml_writer, | ||||||||||||||
Some(&move |req| { | ||||||||||||||
let original_url = req.get_url().to_string(); | ||||||||||||||
let variant_url = variant_urls.get(&original_url).unwrap_or(&original_url); | ||||||||||||||
info!( | ||||||||||||||
"Sending request - original URL: ({}) variant URL: ({})", | ||||||||||||||
req.get_path(), | ||||||||||||||
variant_url | ||||||||||||||
); | ||||||||||||||
Ok(esi::PendingFragmentContent::PendingRequest( | ||||||||||||||
req.with_url(variant_url) | ||||||||||||||
.with_ttl(120) | ||||||||||||||
.send_async("mock-s3")?, | ||||||||||||||
)) | ||||||||||||||
}), | ||||||||||||||
Some(&|req, resp| { | ||||||||||||||
info!( | ||||||||||||||
"Received response for {} {}", | ||||||||||||||
req.get_method(), | ||||||||||||||
req.get_path() | ||||||||||||||
); | ||||||||||||||
Ok(resp) | ||||||||||||||
}), | ||||||||||||||
); | ||||||||||||||
|
||||||||||||||
match result { | ||||||||||||||
Ok(()) => { | ||||||||||||||
xml_writer.into_inner().finish().unwrap(); | ||||||||||||||
} | ||||||||||||||
Err(err) => { | ||||||||||||||
error!("error processing ESI document: {}", err); | ||||||||||||||
let _ = xml_writer.get_mut().write(b"Internal server error"); | ||||||||||||||
xml_writer.into_inner().finish().unwrap_or_else(|_| { | ||||||||||||||
error!("error flushing error response to client"); | ||||||||||||||
}); | ||||||||||||||
} | ||||||||||||||
} | ||||||||||||||
} else { | ||||||||||||||
// Otherwise, we can just return the response. | ||||||||||||||
beresp.send_to_client(); | ||||||||||||||
} | ||||||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is pretty obvious and no more explicit notation required.