use std::cell::RefCell;
use std::rc::Rc;
use std::sync::mpsc;
use std::time::{Duration, Instant};
use boa_engine::object::ObjectInitializer;
use boa_engine::property::Attribute;
use boa_engine::{js_string, Context, JsValue, NativeFunction, Source};
pub(crate) const EXECUTION_TIMEOUT: Duration = Duration::from_millis(250);
const LOOP_ITERATION_LIMIT: u64 = 200_000;
const RECURSION_LIMIT: usize = 1_000;
const MAX_OUTPUT_BYTES: usize = 1024 * 1024;
#[derive(Debug, Clone, Default)]
pub(crate) struct JsExecution {
pub output: Option<String>,
pub error: Option<String>,
pub logs: Vec<String>,
pub compute_utilization: u32,
}
pub(crate) fn run_handler(code: &str, event_json: &[u8]) -> JsExecution {
let code = code.to_owned();
let event = event_json.to_vec();
let (tx, rx) = mpsc::sync_channel::<JsExecution>(1);
let started = Instant::now();
let _ = std::thread::Builder::new()
.name("cloudfront-js".to_string())
.stack_size(8 * 1024 * 1024)
.spawn(move || {
let result = run_handler_blocking(&code, &event, started);
let _ = tx.send(result);
});
match rx.recv_timeout(EXECUTION_TIMEOUT) {
Ok(mut exec) => {
if exec.error.is_none() && exec.compute_utilization == 0 {
exec.compute_utilization = 1;
}
exec
}
Err(mpsc::RecvTimeoutError::Timeout) => {
let msg = format!(
"function execution exceeded the {}ms time limit",
EXECUTION_TIMEOUT.as_millis()
);
JsExecution {
output: None,
error: Some(msg.clone()),
logs: vec![format!("ERROR: {msg}")],
compute_utilization: 101,
}
}
Err(mpsc::RecvTimeoutError::Disconnected) => {
let msg = "function execution worker thread panicked or failed to spawn".to_string();
JsExecution {
output: None,
error: Some(msg.clone()),
logs: vec![format!("ERROR: {msg}")],
compute_utilization: 101,
}
}
}
}
fn run_handler_blocking(code: &str, event_json: &[u8], started: Instant) -> JsExecution {
let logs: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
let mut ctx = Context::default();
ctx.runtime_limits_mut()
.set_loop_iteration_limit(LOOP_ITERATION_LIMIT);
ctx.runtime_limits_mut()
.set_recursion_limit(RECURSION_LIMIT);
if let Err(err) = install_console(&mut ctx, &logs) {
return error_execution(format!("failed to install console: {err}"), &logs, started);
}
if let Err(err) = ctx.eval(Source::from_bytes(code.as_bytes())) {
return error_execution(format!("{}", err), &logs, started);
}
let event_str = match std::str::from_utf8(event_json) {
Ok(s) => s,
Err(_) => {
return error_execution("EventObject is not valid UTF-8".to_string(), &logs, started);
}
};
let event_src = format!("({})", event_str);
let event = match ctx.eval(Source::from_bytes(event_src.as_bytes())) {
Ok(v) => v,
Err(err) => {
return error_execution(format!("invalid EventObject JSON: {err}"), &logs, started);
}
};
let handler = match ctx.global_object().get(js_string!("handler"), &mut ctx) {
Ok(h) => h,
Err(err) => {
return error_execution(
format!("function handler is not defined: {err}"),
&logs,
started,
);
}
};
let Some(handler_fn) = handler.as_callable() else {
return error_execution(
"function handler is not callable".to_string(),
&logs,
started,
);
};
let returned = match handler_fn.call(&JsValue::undefined(), &[event], &mut ctx) {
Ok(v) => v,
Err(err) => {
return error_execution(format!("{}", err), &logs, started);
}
};
let stringified = match stringify(&mut ctx, returned) {
Ok(s) => s,
Err(err) => {
return error_execution(
format!("failed to JSON.stringify result: {err}"),
&logs,
started,
);
}
};
if stringified.len() > MAX_OUTPUT_BYTES {
return error_execution(
format!("function output exceeded {MAX_OUTPUT_BYTES} bytes"),
&logs,
started,
);
}
let captured = logs.borrow().clone();
JsExecution {
output: Some(stringified),
error: None,
logs: captured,
compute_utilization: utilization_pct(started.elapsed()),
}
}
fn error_execution(msg: String, logs: &Rc<RefCell<Vec<String>>>, started: Instant) -> JsExecution {
let mut captured = logs.borrow().clone();
captured.push(format!("ERROR: {msg}"));
let elapsed_pct = utilization_pct(started.elapsed());
let pct = elapsed_pct.max(101);
JsExecution {
output: None,
error: Some(msg),
logs: captured,
compute_utilization: pct,
}
}
fn utilization_pct(elapsed: Duration) -> u32 {
let limit_us = EXECUTION_TIMEOUT.as_micros().max(1);
let used_us = elapsed.as_micros();
let pct = (used_us * 100) / limit_us;
if pct > 100 {
100
} else {
pct as u32
}
}
fn install_console(
ctx: &mut Context,
logs: &Rc<RefCell<Vec<String>>>,
) -> Result<(), boa_engine::JsError> {
let logs_log = Rc::clone(logs);
let log_fn = unsafe {
NativeFunction::from_closure(move |_this, args, ctx| {
let mut parts: Vec<String> = Vec::with_capacity(args.len());
for a in args {
let s = a
.to_string(ctx)
.map(|s| s.to_std_string_escaped())
.unwrap_or_default();
parts.push(s);
}
logs_log.borrow_mut().push(parts.join(" "));
Ok(JsValue::undefined())
})
};
let logs_err = Rc::clone(logs);
let err_fn = unsafe {
NativeFunction::from_closure(move |_this, args, ctx| {
let mut parts: Vec<String> = Vec::with_capacity(args.len());
for a in args {
let s = a
.to_string(ctx)
.map(|s| s.to_std_string_escaped())
.unwrap_or_default();
parts.push(s);
}
logs_err.borrow_mut().push(parts.join(" "));
Ok(JsValue::undefined())
})
};
let console = ObjectInitializer::new(ctx)
.function(log_fn, js_string!("log"), 0)
.function(err_fn, js_string!("error"), 0)
.build();
ctx.register_global_property(js_string!("console"), console, Attribute::all())?;
Ok(())
}
fn stringify(ctx: &mut Context, value: JsValue) -> Result<String, boa_engine::JsError> {
let stringify = ctx.eval(Source::from_bytes(b"JSON.stringify"))?;
let Some(stringify_fn) = stringify.as_callable() else {
return Err(boa_engine::JsNativeError::typ()
.with_message("JSON.stringify missing")
.into());
};
let result = stringify_fn.call(&JsValue::undefined(), &[value], ctx)?;
if result.is_undefined() {
return Ok(String::new());
}
Ok(result.to_string(ctx)?.to_std_string_escaped())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn returns_modified_event_as_json() {
let exec = run_handler(
r#"function handler(e) { e.x = "y"; return e; }"#,
br#"{"headers":{}}"#,
);
assert!(exec.error.is_none(), "unexpected error: {:?}", exec.error);
let out = exec.output.expect("output");
assert!(out.contains("\"x\":\"y\""), "got {out}");
assert!(
exec.compute_utilization <= 100,
"got {}",
exec.compute_utilization
);
}
#[test]
fn modifies_request_headers_aws_shape() {
let exec = run_handler(
r#"function handler(event) {
event.request.headers["x-foo"] = {value: "bar"};
return event.request;
}"#,
br#"{"version":"1.0","context":{},"viewer":{},"request":{"method":"GET","uri":"/","querystring":{},"headers":{},"cookies":{}}}"#,
);
assert!(exec.error.is_none(), "unexpected error: {:?}", exec.error);
let out = exec.output.expect("output");
assert!(out.contains("\"x-foo\""), "got {out}");
assert!(out.contains("\"bar\""), "got {out}");
}
#[test]
fn surfaces_thrown_error() {
let exec = run_handler(r#"function handler() { throw new Error("boom"); }"#, b"{}");
assert!(exec.output.is_none());
let err = exec.error.expect("error");
assert!(err.contains("boom"), "got {err}");
assert!(
exec.logs.iter().any(|l| l.contains("boom")),
"expected error in logs, got {:?}",
exec.logs
);
assert!(
exec.compute_utilization > 100,
"expected >100 on error, got {}",
exec.compute_utilization
);
}
#[test]
fn captures_console_log() {
let exec = run_handler(
r#"function handler(e) { console.log("a", "b"); return e; }"#,
b"{}",
);
assert!(exec.error.is_none());
assert!(exec.logs.iter().any(|l| l == "a b"));
}
#[test]
fn errors_when_handler_missing() {
let exec = run_handler("var x = 1;", b"{}");
assert!(exec.error.is_some());
assert!(
exec.compute_utilization > 100,
"expected >100 on error, got {}",
exec.compute_utilization
);
}
#[test]
fn errors_when_event_is_invalid_json() {
let exec = run_handler(r#"function handler(e) { return e; }"#, b"not-json");
assert!(exec.error.is_some());
}
#[test]
fn infinite_loop_is_killed_by_timeout() {
let exec = run_handler(r#"function handler() { while(1){} }"#, b"{}");
assert!(exec.output.is_none());
let err = exec.error.expect("error");
assert!(
err.contains("time limit") || err.contains("limit") || err.contains("iteration"),
"expected timeout/iteration error, got {err}"
);
assert!(
exec.compute_utilization > 100,
"expected >100 after timeout, got {}",
exec.compute_utilization
);
assert!(
!exec.logs.is_empty(),
"expected error log line, got empty logs"
);
}
}