use std::sync::Arc;
use axum::body::Bytes;
use axum::extract::{DefaultBodyLimit, State};
use axum::http::{HeaderMap, StatusCode};
use axum::response::{IntoResponse, Response};
use axum::{Json, Router};
use base64::Engine;
use serde_json::{json, Value};
use crate::bus::{validate_message_name, Message, MessageKind};
use crate::microsvc::{Service, MAX_HTTP_BODY_BYTES};
const STRUCTURED_CONTENT_TYPE: &str = "application/cloudevents+json";
pub fn cloud_events_router<D: Send + Sync + 'static>(service: Arc<Service<D>>) -> Router {
Router::new()
.route("/", axum::routing::post(ingress_handler))
.route("/cloudevent/{type}", axum::routing::post(ingress_handler))
.layer(DefaultBodyLimit::max(MAX_HTTP_BODY_BYTES))
.with_state(service)
}
async fn ingress_handler<D: Send + Sync + 'static>(
State(service): State<Arc<Service<D>>>,
headers: HeaderMap,
body: Bytes,
) -> Response {
let message = match parse_cloud_event(&headers, &body) {
Ok(message) => message,
Err(reason) => return (StatusCode::BAD_REQUEST, reason).into_response(),
};
match service.dispatch_message(&message).await {
Ok(value) => (StatusCode::OK, Json(value)).into_response(),
Err(err) => {
let status = if err.transport_error_kind().is_retryable() {
StatusCode::SERVICE_UNAVAILABLE
} else {
StatusCode::UNPROCESSABLE_ENTITY
};
if err.status_code() >= 500 {
eprintln!("knative ingress `{}` failed: {err}", message.name());
}
(
status,
Json(json!({ "error": err.client_facing_message() })),
)
.into_response()
}
}
}
fn header<'a>(headers: &'a HeaderMap, name: &str) -> Option<&'a str> {
headers.get(name).and_then(|value| value.to_str().ok())
}
fn parse_cloud_event(headers: &HeaderMap, body: &Bytes) -> Result<Message, String> {
let content_type = header(headers, "content-type").unwrap_or("");
if content_type.starts_with(STRUCTURED_CONTENT_TYPE) {
parse_structured(body)
} else {
parse_binary(headers, body)
}
}
fn parse_binary(headers: &HeaderMap, body: &Bytes) -> Result<Message, String> {
let id = header(headers, "ce-id").ok_or("missing ce-id header")?;
let name = header(headers, "ce-type").ok_or("missing ce-type header")?;
validate_message_name(name).map_err(|e| format!("invalid ce-type: {e}"))?;
let content_type = header(headers, "content-type")
.unwrap_or("application/json")
.to_string();
let mut metadata = Vec::new();
for (key, value) in headers.iter() {
let key = key.as_str();
if let Some(attr) = key.strip_prefix("ce-") {
if attr == "id" || attr == "type" {
continue;
}
if let Ok(value) = value.to_str() {
metadata.push((attr.to_string(), value.to_string()));
}
}
}
Ok(Message {
id: Some(id.to_string()),
name: name.to_string(),
kind: MessageKind::Event,
payload: body.to_vec(),
content_type,
metadata,
})
}
fn parse_structured(body: &Bytes) -> Result<Message, String> {
let event: Value =
serde_json::from_slice(body).map_err(|e| format!("invalid cloudevents+json: {e}"))?;
let object = event
.as_object()
.ok_or("cloudevent must be a JSON object")?;
let id = object
.get("id")
.and_then(Value::as_str)
.ok_or("missing cloudevent id")?
.to_string();
let name = object
.get("type")
.and_then(Value::as_str)
.ok_or("missing cloudevent type")?
.to_string();
validate_message_name(&name).map_err(|e| format!("invalid cloudevent type: {e}"))?;
let content_type = object
.get("datacontenttype")
.and_then(Value::as_str)
.unwrap_or("application/json")
.to_string();
let payload = if let Some(data) = object.get("data") {
match data {
Value::String(s) => s.clone().into_bytes(),
other => serde_json::to_vec(other).map_err(|e| format!("invalid data: {e}"))?,
}
} else if let Some(Value::String(b64)) = object.get("data_base64") {
base64::engine::general_purpose::STANDARD
.decode(b64)
.map_err(|e| format!("invalid data_base64: {e}"))?
} else {
Vec::new()
};
let reserved = [
"specversion",
"id",
"type",
"datacontenttype",
"data",
"data_base64",
];
let mut metadata = Vec::new();
for (key, value) in object {
if reserved.contains(&key.as_str()) {
continue;
}
let value = match value {
Value::String(s) => s.clone(),
other => other.to_string(),
};
metadata.push((key.clone(), value));
}
Ok(Message {
id: Some(id),
name,
kind: MessageKind::Event,
payload,
content_type,
metadata,
})
}
#[cfg(test)]
mod tests {
use super::*;
use axum::http::HeaderValue;
fn headers(pairs: &[(&str, &str)]) -> HeaderMap {
let mut map = HeaderMap::new();
for (k, v) in pairs {
map.insert(
axum::http::HeaderName::from_bytes(k.as_bytes()).unwrap(),
HeaderValue::from_str(v).unwrap(),
);
}
map
}
#[test]
fn parses_binary_cloud_event() {
let h = headers(&[
("ce-id", "evt-1"),
("ce-type", "order.created"),
("ce-source", "/orders"),
("content-type", "application/json"),
]);
let body = Bytes::from_static(br#"{"order":"o1"}"#);
let message = parse_cloud_event(&h, &body).unwrap();
assert_eq!(message.id(), Some("evt-1"));
assert_eq!(message.name(), "order.created");
assert_eq!(message.payload(), br#"{"order":"o1"}"#);
assert_eq!(message.metadata("source"), Some("/orders"));
}
#[test]
fn parses_structured_cloud_event() {
let h = headers(&[("content-type", "application/cloudevents+json")]);
let body = Bytes::from(
json!({
"specversion": "1.0",
"id": "evt-2",
"type": "order.created",
"source": "/orders",
"datacontenttype": "application/json",
"data": {"order": "o2"},
})
.to_string(),
);
let message = parse_cloud_event(&h, &body).unwrap();
assert_eq!(message.id(), Some("evt-2"));
assert_eq!(message.name(), "order.created");
assert_eq!(message.payload(), br#"{"order":"o2"}"#);
assert_eq!(message.metadata("source"), Some("/orders"));
}
#[test]
fn missing_id_is_rejected() {
let h = headers(&[("ce-type", "order.created")]);
assert!(parse_cloud_event(&h, &Bytes::new()).is_err());
}
#[test]
fn binary_wildcard_ce_type_is_rejected() {
let h = headers(&[("ce-id", "evt-1"), ("ce-type", "order.*")]);
let body = Bytes::from_static(br#"{}"#);
let err = parse_cloud_event(&h, &body).unwrap_err();
assert!(err.contains("invalid ce-type"), "got {err}");
}
#[test]
fn structured_wildcard_type_is_rejected() {
let h = headers(&[("content-type", "application/cloudevents+json")]);
let body = Bytes::from(
json!({
"specversion": "1.0",
"id": "evt-2",
"type": "orders>",
"source": "/orders",
})
.to_string(),
);
let err = parse_cloud_event(&h, &body).unwrap_err();
assert!(err.contains("invalid cloudevent type"), "got {err}");
}
}