Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,22 @@ fastedge-run http \
--max-duration 5000 # 5-second execution timeout
```

### Internal status codes

On every 5xx error response the runtime adds an `X-CDN-Internal-Status` response header with a
numeric code in the range **3000–3999** that identifies the exact failure reason:

| Code | HTTP status | Meaning |
|------|-------------|---------|
| `3000` | 530 | Context setup error — failed to instantiate the Wasm executor |
| `3001` | 530 | Generic execute error (unclassified internal failure) |
| `3002` | 533 | App exited with a non-zero exit code |
| `3003` | 533 | Wasm trap — unknown or unclassified trap |
| `3010` | 532 | Execution timeout — Wasm interrupt trap (`Trap::Interrupt`) |
| `3011` | 532 | Execution timeout — async deadline exceeded (`tokio::Elapsed`) |
| `3012` | 532 | Execution timeout — deadline elapsed (string-matched error) |
| `3020` | 531 | Out of memory — `Trap::UnreachableCodeReached` |

### Dotenv support

Pass `--dotenv` (optionally with a directory path; defaults to the current directory) to load
Expand Down
23 changes: 20 additions & 3 deletions crates/http-service/src/executor/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,8 @@ mod tests {
use crate::executor::http::HttpExecutorImpl;
use crate::{
ContextHeaders, ExecutorFactory, HttpService, FASTEDGE_EXECUTION_TIMEOUT,
FASTEDGE_OUT_OF_MEMORY,
FASTEDGE_OUT_OF_MEMORY, INTERNAL_STATUS_OUT_OF_MEMORY, INTERNAL_STATUS_TIMEOUT_ELAPSED,
INTERNAL_STATUS_TIMEOUT_INTERRUPT, X_CDN_INTERNAL_STATUS,
};
use bytes::Bytes;
use claims::*;
Expand Down Expand Up @@ -474,13 +475,23 @@ mod tests {
let res = assert_ok!(http_service.handle_request("2".to_smolstr(), req).await);
assert_eq!(FASTEDGE_EXECUTION_TIMEOUT, res.status());
let headers = res.headers();
assert_eq!(3, headers.len());
assert_eq!(4, headers.len());
assert_eq!(
"*",
assert_some!(headers.get("access-control-allow-origin"))
);
assert_eq!("no-store", assert_some!(headers.get("cache-control")));
assert_eq!("03", assert_some!(headers.get("RES_HEADER_03")));
let internal_status = assert_some!(headers.get(X_CDN_INTERNAL_STATUS))
.to_str()
.unwrap()
.parse::<u16>()
.unwrap();
assert!(
internal_status == INTERNAL_STATUS_TIMEOUT_INTERRUPT
|| internal_status == INTERNAL_STATUS_TIMEOUT_ELAPSED,
"expected timeout internal status code, got {internal_status}"
);
}

#[tokio::test]
Expand Down Expand Up @@ -525,13 +536,19 @@ mod tests {
let res = assert_ok!(http_service.handle_request("3".to_smolstr(), req).await);
assert_eq!(FASTEDGE_OUT_OF_MEMORY, res.status());
let headers = res.headers();
assert_eq!(3, headers.len());
assert_eq!(4, headers.len());
assert_eq!(
"*",
assert_some!(headers.get("access-control-allow-origin"))
);
assert_eq!("no-store", assert_some!(headers.get("cache-control")));
assert_eq!("03", assert_some!(headers.get("RES_HEADER_03")));
assert_eq!(
INTERNAL_STATUS_OUT_OF_MEMORY.to_string(),
assert_some!(headers.get(X_CDN_INTERNAL_STATUS))
.to_str()
.unwrap()
);
}

#[tokio::test]
Expand Down
41 changes: 33 additions & 8 deletions crates/http-service/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ pub mod executor;
pub mod state;

pub(crate) static TRACEPARENT: &str = "traceparent";
pub(crate) static X_CDN_INTERNAL_STATUS: &str = "x-cdn-internal-status";

#[cfg(target_family = "unix")]
type OwnedFd = std::os::fd::OwnedFd;
Expand All @@ -45,6 +46,16 @@ const FASTEDGE_OUT_OF_MEMORY: u16 = 531;
const FASTEDGE_EXECUTION_TIMEOUT: u16 = 532;
const FASTEDGE_EXECUTION_PANIC: u16 = 533;

/// Internal status codes returned in `X-CDN-Internal-Status` response header (range 3000–3999).
pub(crate) const INTERNAL_STATUS_CONTEXT_ERROR: u16 = 3000;
pub(crate) const INTERNAL_STATUS_EXECUTE_ERROR: u16 = 3001;
pub(crate) const INTERNAL_STATUS_APP_EXIT_ERROR: u16 = 3002;
pub(crate) const INTERNAL_STATUS_WASM_TRAP_OTHER: u16 = 3003;
pub(crate) const INTERNAL_STATUS_TIMEOUT_INTERRUPT: u16 = 3010;
pub(crate) const INTERNAL_STATUS_TIMEOUT_ELAPSED: u16 = 3011;
pub(crate) const INTERNAL_STATUS_TIMEOUT_DEADLINE: u16 = 3012;
pub(crate) const INTERNAL_STATUS_OUT_OF_MEMORY: u16 = 3020;

#[derive(Default)]
pub struct HttpConfig {
pub all_interfaces: bool,
Expand Down Expand Up @@ -310,7 +321,7 @@ where
tracing::warn!(cause=?error, app=%app_name,
"failure on getting context"
);
return internal_fastedge_error("context error");
return internal_fastedge_error("context error", INTERNAL_STATUS_CONTEXT_ERROR);
}
};

Expand All @@ -331,7 +342,7 @@ where
}
Err(error) => {
tracing::warn!(cause=?error, "execute");
let (status_code, fail_reason, msg) = map_err(error);
let (status_code, fail_reason, msg, internal_code) = map_err(error);
stats.status_code(status_code);
stats.fail_reason(fail_reason as u32);
tracing::debug!(?fail_reason, ?request_id, "stats");
Expand All @@ -344,7 +355,9 @@ where
None,
);

let builder = hyper::Response::builder().status(status_code);
let builder = hyper::Response::builder()
.status(status_code)
.header(X_CDN_INTERNAL_STATUS, internal_code);
let res_headers = app_res_headers(cfg);
let builder = res_headers
.iter()
Expand All @@ -357,15 +370,16 @@ where
}
}

fn map_err(error: Error) -> (u16, AppResult, HyperOutgoingBody) {
fn map_err(error: Error) -> (u16, AppResult, HyperOutgoingBody, u16) {
let root_cause = error.root_cause();
let (status_code, fail_reason, msg) =
let (status_code, fail_reason, msg, internal_code) =
if let Some(exit) = root_cause.downcast_ref::<wasi_common::I32Exit>() {
if exit.0 == 0 {
(
StatusCode::OK.as_u16(),
AppResult::SUCCESS,
Empty::new().map_err(|never| match never {}).boxed(),
0,
)
} else {
(
Expand All @@ -374,6 +388,7 @@ fn map_err(error: Error) -> (u16, AppResult, HyperOutgoingBody) {
Full::new(Bytes::from("fastedge: App failed"))
.map_err(|never| match never {})
.boxed(),
INTERNAL_STATUS_APP_EXIT_ERROR,
)
}
} else if let Some(trap) = root_cause.downcast_ref::<wasmtime::Trap>() {
Expand All @@ -384,20 +399,23 @@ fn map_err(error: Error) -> (u16, AppResult, HyperOutgoingBody) {
Full::new(Bytes::from("fastedge: Execution timeout"))
.map_err(|never| match never {})
.boxed(),
INTERNAL_STATUS_TIMEOUT_INTERRUPT,
),
wasmtime::Trap::UnreachableCodeReached => (
FASTEDGE_OUT_OF_MEMORY,
AppResult::OOM,
Full::new(Bytes::from("fastedge: Out of memory"))
.map_err(|never| match never {})
.boxed(),
INTERNAL_STATUS_OUT_OF_MEMORY,
),
_ => (
FASTEDGE_EXECUTION_PANIC,
AppResult::OTHER,
Full::new(Bytes::from("fastedge: App failed"))
.map_err(|never| match never {})
.boxed(),
INTERNAL_STATUS_WASM_TRAP_OTHER,
),
}
} else if let Some(_elapsed) = root_cause.downcast_ref::<Elapsed>() {
Expand All @@ -407,6 +425,7 @@ fn map_err(error: Error) -> (u16, AppResult, HyperOutgoingBody) {
Full::new(Bytes::from("fastedge: Execution timeout"))
.map_err(|never| match never {})
.boxed(),
INTERNAL_STATUS_TIMEOUT_ELAPSED,
)
} else if root_cause.to_string().ends_with("deadline has elapsed") {
(
Expand All @@ -415,6 +434,7 @@ fn map_err(error: Error) -> (u16, AppResult, HyperOutgoingBody) {
Full::new(Bytes::from("fastedge: Execution timeout"))
.map_err(|never| match never {})
.boxed(),
INTERNAL_STATUS_TIMEOUT_DEADLINE,
)
} else {
(
Expand All @@ -423,9 +443,10 @@ fn map_err(error: Error) -> (u16, AppResult, HyperOutgoingBody) {
Full::new(Bytes::from("fastedge: Execute error"))
.map_err(|never| match never {})
.boxed(),
INTERNAL_STATUS_EXECUTE_ERROR,
)
};
(status_code, fail_reason, msg)
(status_code, fail_reason, msg, internal_code)
}

fn remote_traceparent(req: &hyper::Request<hyper::body::Incoming>) -> SmolStr {
Expand All @@ -436,10 +457,14 @@ fn remote_traceparent(req: &hyper::Request<hyper::body::Incoming>) -> SmolStr {
.unwrap_or(nanoid::nanoid!().to_smolstr())
}

/// Creates an HTTP 500 response.
fn internal_fastedge_error(msg: &'static str) -> Result<hyper::Response<HyperOutgoingBody>> {
/// Creates an HTTP 530 response with an `X-CDN-Internal-Status` header.
fn internal_fastedge_error(
msg: &'static str,
internal_code: u16,
) -> Result<hyper::Response<HyperOutgoingBody>> {
Ok(hyper::Response::builder()
.status(FASTEDGE_INTERNAL_ERROR)
.header(X_CDN_INTERNAL_STATUS, internal_code)
.body(
Full::new(Bytes::from(format!("fastedge: {}", msg)))
.map_err(|never| match never {})
Expand Down
Loading