#![doc = include_str!("../README.md")]
#![forbid(unsafe_code)]
mod body_limit;
mod config;
mod ports;
mod runtime;
mod tracing_setup;
pub use config::EnvConfig;
pub use ports::{
ActorDriver, ContextDefer, D1Database, D1RateLimiter, FetchClient, KvStorePort, Limit,
RATE_LIMIT_COUNTERS_SQL, RateLimitPolicy, RateLimitPort, RoomDriver, ScheduleDefer,
WorkersClock, client_ip,
};
pub use runtime::Cloudflare;
pub use tracing_setup::install_tracing;
use crate::body_limit::{BodyPlan, Capped, body_plan, read_capped};
use crate::runtime::{WARNED_UNRESOLVED_LIMITER, warn_once};
use cratefield_core::{
Harness, Problem, RequestStream, RequestSummary, ResponseStream, SLUGS, ScheduledLimits,
ScheduledSplit,
};
use futures_core::Stream;
use futures_util::StreamExt;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context as TaskContext, Poll};
use time::OffsetDateTime;
use tower::ServiceExt;
use worker::send::SendWrapper;
use worker::{Context, Env, Request as WorkerRequest, Response as WorkerResponse};
fn check_module_config_once(harness: &Harness, config: &dyn cratefield_core::Config) {
use std::sync::atomic::{AtomicBool, Ordering};
static CHECKED: AtomicBool = AtomicBool::new(false);
if CHECKED.swap(true, Ordering::Relaxed) {
return;
}
for module in harness.modules() {
if let Err(err) = module.validate_config(config) {
worker::console_error!(
"[config] module `{}` has invalid configuration: {err}",
module.name()
);
}
}
}
#[cfg(feature = "push")]
fn check_push_wiring_once(
harness: &Harness,
runtime: &Cloudflare,
env: &Env,
config: &dyn cratefield_core::Config,
) {
use cratefield_push_wiring::WiringSeverity;
use std::sync::atomic::{AtomicBool, Ordering};
static CHECKED: AtomicBool = AtomicBool::new(false);
if CHECKED.swap(true, Ordering::Relaxed) {
return;
}
let Some(wiring) = runtime.push_wiring(env) else {
return;
};
worker::console_log!("[push] {}", wiring.summary());
let deployed = cratefield_core::deployed_env(harness.venture().env, config);
match wiring.severity(deployed) {
WiringSeverity::Ok => {}
WiringSeverity::Warning => {
for problem in wiring.problems() {
worker::console_log!("[push] warning: {problem}");
}
}
WiringSeverity::Error => {
for problem in wiring.problems() {
worker::console_error!("[push] {problem}");
}
}
}
}
pub async fn serve(
harness: &Harness,
runtime: &Cloudflare,
mut req: WorkerRequest,
env: Env,
ctx: Context,
) -> worker::Result<WorkerResponse> {
install_tracing();
let ports = runtime.ports(&env, Arc::new(ContextDefer(ctx)));
check_module_config_once(harness, ports.config.as_ref());
#[cfg(feature = "push")]
check_push_wiring_once(harness, runtime, &env, ports.config.as_ref());
let config = Arc::clone(&ports.config);
let url = req.url()?;
let problem_type_base = harness.venture().problem_type_base();
if let Some(detail) = runtime.unresolved_limiter_refusal(ports.rate_limiter.is_some())
&& url.path().starts_with("/v1/")
{
warn_once(&WARNED_UNRESOLVED_LIMITER, &detail);
return response_to_worker(unresolved_limiter_response(detail, &problem_type_base)).await;
}
let router = harness.router(ports);
let method = http::Method::from_bytes(req.method().to_string().as_bytes())
.map_err(|err| worker::Error::RustError(err.to_string()))?;
let streaming = harness.streaming_route(url.path(), &method);
let limit = streaming.unwrap_or_else(|| harness.max_body_bytes(url.path(), config.as_ref()));
let is_head = method == http::Method::HEAD;
let declared = req.headers().get("content-length").ok().flatten();
let mut request_stream: Option<RequestStream> = None;
let bytes = match body_plan(declared.as_deref(), limit) {
BodyPlan::Refuse => {
return response_to_worker(
Problem::request_too_large().into_response_with_base(&problem_type_base),
)
.await;
}
BodyPlan::Stream | BodyPlan::Buffer if streaming.is_some() => {
request_stream = Some(match req.inner().body() {
None => RequestStream::empty(),
Some(_) => match req.stream() {
Ok(stream) => RequestStream::new(send_stream(stream), limit),
Err(_) => RequestStream::empty(),
},
});
Vec::new()
}
BodyPlan::Buffer => req.bytes().await?,
BodyPlan::Stream => {
match req.inner().body() {
None => Vec::new(),
Some(_) => match req.stream() {
Ok(stream) => match read_capped(stream, limit).await {
Ok(Capped::Within(bytes)) => bytes,
Ok(Capped::TooLarge) => {
return response_to_worker(
Problem::request_too_large()
.into_response_with_base(&problem_type_base),
)
.await;
}
Err(err) => return Err(err),
},
Err(_) => Vec::new(),
},
}
}
};
let mut builder = http::Request::builder().method(method).uri(url.to_string());
{
let headers = req.headers();
for (name, value) in headers {
builder = builder.header(name.as_str(), value.as_str());
}
}
let mut buffered = builder
.body(axum::body::Body::from(bytes))
.map_err(|err| worker::Error::RustError(err.to_string()))?;
if let Some(stream) = request_stream {
buffered.extensions_mut().insert(stream);
}
let mut response = router
.oneshot(buffered)
.await
.map_err(|err| worker::Error::RustError(err.to_string()))?;
if is_head {
response.extensions_mut().remove::<ResponseStream>();
}
if let Some(line) = response.extensions().get().and_then(summary_line) {
#[cfg(target_arch = "wasm32")]
worker::console_log!("{line}");
#[cfg(not(target_arch = "wasm32"))]
tracing::info!("{line}");
}
response_to_worker(response).await
}
fn summary_line(summary: &RequestSummary) -> Option<String> {
serde_json::to_string(summary).ok()
}
async fn response_to_worker(
response: http::Response<axum::body::Body>,
) -> worker::Result<WorkerResponse> {
let (parts, body) = response.into_parts();
if let Some(stream) = parts.extensions.get::<ResponseStream>()
&& let Some(inner) = stream.take()
{
let mapped =
inner.map(|chunk| chunk.map_err(|err| worker::Error::RustError(err.to_string())));
let mut out = WorkerResponse::from_stream(mapped)?.with_status(parts.status.as_u16());
copy_headers(out.headers_mut(), &parts.headers);
return Ok(out);
}
let bytes = axum::body::to_bytes(body, MAX_RESPONSE_BUFFER)
.await
.map_err(|err| worker::Error::RustError(err.to_string()))?;
let mut out =
WorkerResponse::from_bytes(bytes.as_ref().to_vec())?.with_status(parts.status.as_u16());
copy_headers(out.headers_mut(), &parts.headers);
Ok(out)
}
fn copy_headers(worker_headers: &mut worker::Headers, headers: &http::HeaderMap) {
for name in headers.keys() {
let _ = worker_headers.delete(name.as_str());
for value in headers.get_all(name) {
let _ = worker_headers.append(name.as_str(), value.to_str().unwrap_or_default());
}
}
}
type BoxedWorkerBody = Pin<Box<dyn Stream<Item = Result<Vec<u8>, worker::Error>>>>;
struct SendStream(SendWrapper<BoxedWorkerBody>);
impl Stream for SendStream {
type Item = Result<Vec<u8>, worker::Error>;
fn poll_next(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
self.get_mut().0.0.as_mut().poll_next(cx)
}
}
fn send_stream<S>(stream: S) -> SendStream
where
S: Stream<Item = Result<Vec<u8>, worker::Error>> + 'static,
{
let boxed: Box<dyn Stream<Item = Result<Vec<u8>, worker::Error>>> = Box::new(stream);
SendStream(SendWrapper::new(Pin::from(boxed)))
}
const MAX_RESPONSE_BUFFER: usize = 1024 * 1024;
pub const CLOUDFLARE_SCHEDULED_LIMITS: ScheduledLimits = ScheduledLimits {
wall: Some(time::Duration::seconds(25)),
subrequests: Some(40),
};
pub async fn serve_scheduled(
harness: &Harness,
runtime: &Cloudflare,
event: worker::ScheduledEvent,
env: Env,
ctx: worker::ScheduleContext,
) {
serve_scheduled_with_limits(
harness,
runtime,
event,
env,
ctx,
CLOUDFLARE_SCHEDULED_LIMITS,
)
.await;
}
pub async fn serve_scheduled_with_limits(
harness: &Harness,
runtime: &Cloudflare,
event: worker::ScheduledEvent,
env: Env,
ctx: worker::ScheduleContext,
limits: ScheduledLimits,
) {
install_tracing();
let cron = event.cron();
let ports = runtime.ports(&env, Arc::new(ScheduleDefer(ctx)));
check_module_config_once(harness, ports.config.as_ref());
#[cfg(feature = "push")]
check_push_wiring_once(harness, runtime, &env, ports.config.as_ref());
let clock = ports.clock.clone();
let mut split = match clock.as_ref() {
Some(clock) => ScheduledSplit::new(limits, clock.now(), harness.modules().len()),
None => ScheduledSplit::new(
ScheduledLimits::UNBOUNDED,
OffsetDateTime::UNIX_EPOCH,
harness.modules().len(),
),
};
for module in harness.modules() {
let now = clock
.as_ref()
.map_or(OffsetDateTime::UNIX_EPOCH, |clock| clock.now());
let budget = Arc::new(split.next(now));
let mut module_ctx = harness.module_context(module.as_ref(), &ports);
module_ctx.scheduled = Arc::clone(&budget);
if let Err(err) = module.scheduled(&module_ctx, &cron).await {
tracing::error!(
module = module.name(),
cron = %cron,
error = %err,
"scheduled module work failed",
);
}
let after = clock
.as_ref()
.map_or(OffsetDateTime::UNIX_EPOCH, |clock| clock.now());
if budget.exhausted(after) {
tracing::warn!(
module = module.name(),
cron = %cron,
spent = budget.spent(),
"scheduled module work exhausted its budget",
);
}
split.settle(&budget);
}
}
fn unresolved_limiter_response(
detail: String,
problem_type_base: &str,
) -> axum::response::Response {
Problem::new(&SLUGS.not_production_ready)
.with_detail(detail)
.into_response_with_base(problem_type_base)
}
#[cfg(test)]
mod tests {
use super::*;
async fn body_json(response: axum::response::Response) -> serde_json::Value {
let bytes = axum::body::to_bytes(response.into_body(), 64 * 1024)
.await
.expect("the body reads");
serde_json::from_slice(&bytes).expect("the body is JSON")
}
#[test]
fn unresolved_limiter_refusal_names_the_serving_venture() {
let venture = cratefield_core::Venture::new("acme", "acme.example")
.public_url("https://acme.example");
let response = unresolved_limiter_response(
"limiter binding RATE_LIMITER did not resolve".to_owned(),
&venture.problem_type_base(),
);
assert_eq!(response.status(), 503);
assert_eq!(
response.headers()[axum::http::header::CONTENT_TYPE],
"application/problem+json"
);
let body = pollster::block_on(body_json(response));
assert_eq!(
body["type"],
"https://acme.example/problems/not-production-ready"
);
assert!(
!body.to_string().contains("factory0.ventures"),
"no other venture's domain: {body}"
);
}
#[test]
fn unresolved_limiter_refusal_without_public_url_is_about_blank() {
let venture = cratefield_core::Venture::new("acme", "acme.example").public_url("");
let response = unresolved_limiter_response(
"limiter binding RATE_LIMITER did not resolve".to_owned(),
&venture.problem_type_base(),
);
assert_eq!(
pollster::block_on(body_json(response))["type"],
cratefield_core::ABOUT_BLANK
);
}
#[test]
fn summary_line_is_one_json_object_carrying_the_request_id() {
let summary = RequestSummary {
request_id: "01J9ZQ4V6X8Y2K3M5N7P9R1T3W".to_owned(),
method: "GET".to_owned(),
route: "/v1/sample/hello".to_owned(),
module: "sample".to_owned(),
status: 200,
ip_hash: "0123456789ab".to_owned(),
ua_family: "mozilla".to_owned(),
};
let line = summary_line(&summary).expect("serializes");
assert!(!line.contains('\n'), "one line: {line}");
let parsed: serde_json::Value = serde_json::from_str(&line).expect("valid JSON");
assert_eq!(parsed["request_id"], "01J9ZQ4V6X8Y2K3M5N7P9R1T3W");
assert_eq!(parsed["route"], "/v1/sample/hello");
assert_eq!(parsed["status"], 200);
}
}