From ae2134e62d622ebeb23f2af23215524d26093480 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Fri, 12 Jun 2026 20:43:10 +0200 Subject: [PATCH] feat(executor): expose ctx.request.method to scripts (F4) The request map exposed path/headers/body/params/query/rest but not the HTTP method, forcing one script per verb (the E2E app needed 7 scripts where ~3 would do). Add a `method` field to ExecRequest (populated from the HTTP method on the sync-execute bypass and the HTTP outbox payload; empty for non-HTTP triggers) and surface it as `ctx.request.method`, uppercased. A single script can now branch GET/POST on one route. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/executor-core/src/engine.rs | 1 + crates/executor-core/src/sdk/invoke.rs | 1 + crates/executor-core/src/types.rs | 9 +++++++++ crates/executor-core/tests/engine.rs | 15 +++++++++++++++ .../tests/module_redaction_logging.rs | 1 + crates/executor-core/tests/modules.rs | 1 + crates/executor-core/tests/sdk_contract.rs | 1 + crates/executor-core/tests/sdk_docs.rs | 1 + crates/executor-core/tests/sdk_email.rs | 1 + crates/executor-core/tests/sdk_files.rs | 1 + crates/executor-core/tests/sdk_http.rs | 1 + crates/executor-core/tests/sdk_invoke.rs | 1 + crates/executor-core/tests/sdk_kv.rs | 1 + crates/executor-core/tests/sdk_pubsub.rs | 1 + crates/executor-core/tests/sdk_queue.rs | 1 + crates/executor-core/tests/sdk_retry.rs | 1 + crates/executor-core/tests/sdk_secrets.rs | 1 + .../executor-core/tests/sdk_subscriber_token.rs | 1 + crates/executor-core/tests/stdlib.rs | 1 + crates/manager-core/src/dispatcher.rs | 6 ++++++ crates/orchestrator-core/src/api.rs | 2 ++ 21 files changed, 49 insertions(+) diff --git a/crates/executor-core/src/engine.rs b/crates/executor-core/src/engine.rs index 1790248..08d53f4 100644 --- a/crates/executor-core/src/engine.rs +++ b/crates/executor-core/src/engine.rs @@ -485,6 +485,7 @@ fn build_ctx_map(req: &ExecRequest) -> Map { let mut request = Map::new(); request.insert("path".into(), req.path.clone().into()); + request.insert("method".into(), req.method.clone().into()); let mut headers = Map::new(); for (k, v) in &req.headers { diff --git a/crates/executor-core/src/sdk/invoke.rs b/crates/executor-core/src/sdk/invoke.rs index 2031bd0..5bf9cf2 100644 --- a/crates/executor-core/src/sdk/invoke.rs +++ b/crates/executor-core/src/sdk/invoke.rs @@ -171,6 +171,7 @@ fn invoke_blocking( script_name: resolved.name.clone(), invocation_type: InvocationType::Function, path: "/invoke".into(), + method: String::new(), headers: BTreeMap::new(), body: args_json, params: BTreeMap::new(), diff --git a/crates/executor-core/src/types.rs b/crates/executor-core/src/types.rs index a6b84e7..b27d666 100644 --- a/crates/executor-core/src/types.rs +++ b/crates/executor-core/src/types.rs @@ -27,6 +27,15 @@ pub struct ExecRequest { pub script_name: String, pub invocation_type: InvocationType, pub path: String, + + /// HTTP method of the originating request, uppercased (`GET`, `POST`, + /// …). Exposed to scripts as `ctx.request.method` so a single script + /// can branch on the verb instead of needing one script per method. + /// Empty for non-HTTP trigger invocations (cron/kv/docs/…), matching + /// how `path`/`rest` are empty there. + #[serde(default)] + pub method: String, + pub headers: BTreeMap, pub body: serde_json::Value, diff --git a/crates/executor-core/tests/engine.rs b/crates/executor-core/tests/engine.rs index 6f27b62..eb57f4e 100644 --- a/crates/executor-core/tests/engine.rs +++ b/crates/executor-core/tests/engine.rs @@ -15,6 +15,7 @@ fn req(body: serde_json::Value) -> ExecRequest { script_name: "test".into(), invocation_type: InvocationType::Http, path: "/test".into(), + method: String::new(), headers: BTreeMap::new(), body, params: BTreeMap::new(), @@ -103,6 +104,7 @@ fn ctx_exposes_request_data() { "; let r = ExecRequest { path: "/payments".into(), + method: String::new(), body: json!({ "amount": 1234 }), script_name: "payments".into(), ..req(json!(null)) @@ -114,6 +116,19 @@ fn ctx_exposes_request_data() { ); } +#[test] +fn ctx_exposes_request_method() { + // A single script can branch on the verb instead of needing one + // script per method (E2E report F4). + let src = r"#{ statusCode: 200, body: #{ method: ctx.request.method } }"; + let r = ExecRequest { + method: "PATCH".into(), + ..req(json!(null)) + }; + let resp = engine().execute(src, r).unwrap(); + assert_eq!(resp.body, json!({ "method": "PATCH" })); +} + #[test] fn captures_log_calls() { let src = r#" diff --git a/crates/executor-core/tests/module_redaction_logging.rs b/crates/executor-core/tests/module_redaction_logging.rs index 382fdd3..09f5362 100644 --- a/crates/executor-core/tests/module_redaction_logging.rs +++ b/crates/executor-core/tests/module_redaction_logging.rs @@ -66,6 +66,7 @@ fn req(app_id: AppId) -> ExecRequest { script_name: "redaction-test".into(), invocation_type: InvocationType::Http, path: "/x".into(), + method: String::new(), headers: BTreeMap::new(), body: Value::Null, params: BTreeMap::new(), diff --git a/crates/executor-core/tests/modules.rs b/crates/executor-core/tests/modules.rs index 59e277c..7897918 100644 --- a/crates/executor-core/tests/modules.rs +++ b/crates/executor-core/tests/modules.rs @@ -120,6 +120,7 @@ fn req(app_id: AppId) -> ExecRequest { script_name: "test".into(), invocation_type: InvocationType::Http, path: "/test".into(), + method: String::new(), headers: BTreeMap::new(), body: serde_json::Value::Null, params: BTreeMap::new(), diff --git a/crates/executor-core/tests/sdk_contract.rs b/crates/executor-core/tests/sdk_contract.rs index 4c27889..e5183d3 100644 --- a/crates/executor-core/tests/sdk_contract.rs +++ b/crates/executor-core/tests/sdk_contract.rs @@ -43,6 +43,7 @@ fn baseline_request() -> ExecRequest { script_name: "contract".into(), invocation_type: InvocationType::Http, path: "/contract-test".into(), + method: String::new(), headers: BTreeMap::new(), body: Value::Null, params: BTreeMap::new(), diff --git a/crates/executor-core/tests/sdk_docs.rs b/crates/executor-core/tests/sdk_docs.rs index f7d5aa2..01db6e5 100644 --- a/crates/executor-core/tests/sdk_docs.rs +++ b/crates/executor-core/tests/sdk_docs.rs @@ -248,6 +248,7 @@ fn baseline_request(app_id: AppId) -> ExecRequest { script_name: "docs-test".into(), invocation_type: InvocationType::Http, path: "/docs-test".into(), + method: String::new(), headers: BTreeMap::new(), body: Value::Null, params: BTreeMap::new(), diff --git a/crates/executor-core/tests/sdk_email.rs b/crates/executor-core/tests/sdk_email.rs index a02e6ed..c562a7f 100644 --- a/crates/executor-core/tests/sdk_email.rs +++ b/crates/executor-core/tests/sdk_email.rs @@ -57,6 +57,7 @@ fn baseline_request(app_id: AppId) -> ExecRequest { script_name: "email-test".into(), invocation_type: InvocationType::Http, path: "/email-test".into(), + method: String::new(), headers: BTreeMap::new(), body: Value::Null, params: BTreeMap::new(), diff --git a/crates/executor-core/tests/sdk_files.rs b/crates/executor-core/tests/sdk_files.rs index 6790ec0..639bc37 100644 --- a/crates/executor-core/tests/sdk_files.rs +++ b/crates/executor-core/tests/sdk_files.rs @@ -185,6 +185,7 @@ fn baseline_request(app_id: AppId) -> ExecRequest { script_name: "files-test".into(), invocation_type: InvocationType::Http, path: "/files-test".into(), + method: String::new(), headers: BTreeMap::new(), body: Value::Null, params: BTreeMap::new(), diff --git a/crates/executor-core/tests/sdk_http.rs b/crates/executor-core/tests/sdk_http.rs index ea5cce9..672bcf4 100644 --- a/crates/executor-core/tests/sdk_http.rs +++ b/crates/executor-core/tests/sdk_http.rs @@ -108,6 +108,7 @@ fn baseline_request(app_id: AppId, script_id: ScriptId) -> ExecRequest { script_name: "http-test".into(), invocation_type: InvocationType::Http, path: "/http-test".into(), + method: String::new(), headers: BTreeMap::new(), body: Value::Null, params: BTreeMap::new(), diff --git a/crates/executor-core/tests/sdk_invoke.rs b/crates/executor-core/tests/sdk_invoke.rs index 0f5226c..b9cc325 100644 --- a/crates/executor-core/tests/sdk_invoke.rs +++ b/crates/executor-core/tests/sdk_invoke.rs @@ -105,6 +105,7 @@ fn baseline_request(app_id: AppId) -> ExecRequest { script_name: "caller".into(), invocation_type: InvocationType::Http, path: "/caller".into(), + method: String::new(), headers: BTreeMap::new(), body: Value::Null, params: BTreeMap::new(), diff --git a/crates/executor-core/tests/sdk_kv.rs b/crates/executor-core/tests/sdk_kv.rs index 968076f..c37a2a5 100644 --- a/crates/executor-core/tests/sdk_kv.rs +++ b/crates/executor-core/tests/sdk_kv.rs @@ -127,6 +127,7 @@ fn baseline_request(app_id: AppId) -> ExecRequest { script_name: "kv-test".into(), invocation_type: InvocationType::Http, path: "/kv-test".into(), + method: String::new(), headers: BTreeMap::new(), body: Value::Null, params: BTreeMap::new(), diff --git a/crates/executor-core/tests/sdk_pubsub.rs b/crates/executor-core/tests/sdk_pubsub.rs index 6057433..cdecc3d 100644 --- a/crates/executor-core/tests/sdk_pubsub.rs +++ b/crates/executor-core/tests/sdk_pubsub.rs @@ -65,6 +65,7 @@ fn baseline_request(app_id: AppId) -> ExecRequest { script_name: "pubsub-test".into(), invocation_type: InvocationType::Http, path: "/pubsub-test".into(), + method: String::new(), headers: BTreeMap::new(), body: Value::Null, params: BTreeMap::new(), diff --git a/crates/executor-core/tests/sdk_queue.rs b/crates/executor-core/tests/sdk_queue.rs index c135a63..c776b68 100644 --- a/crates/executor-core/tests/sdk_queue.rs +++ b/crates/executor-core/tests/sdk_queue.rs @@ -79,6 +79,7 @@ fn baseline_request(app_id: AppId) -> ExecRequest { script_name: "queue-test".into(), invocation_type: InvocationType::Http, path: "/queue-test".into(), + method: String::new(), headers: BTreeMap::new(), body: Value::Null, params: BTreeMap::new(), diff --git a/crates/executor-core/tests/sdk_retry.rs b/crates/executor-core/tests/sdk_retry.rs index 3c42ce3..11ac00f 100644 --- a/crates/executor-core/tests/sdk_retry.rs +++ b/crates/executor-core/tests/sdk_retry.rs @@ -43,6 +43,7 @@ fn baseline_request(app_id: AppId) -> ExecRequest { script_name: "retry-test".into(), invocation_type: InvocationType::Http, path: "/retry-test".into(), + method: String::new(), headers: BTreeMap::new(), body: Value::Null, params: BTreeMap::new(), diff --git a/crates/executor-core/tests/sdk_secrets.rs b/crates/executor-core/tests/sdk_secrets.rs index dfec151..2836134 100644 --- a/crates/executor-core/tests/sdk_secrets.rs +++ b/crates/executor-core/tests/sdk_secrets.rs @@ -118,6 +118,7 @@ fn baseline_request(app_id: AppId) -> ExecRequest { script_name: "secrets-test".into(), invocation_type: InvocationType::Http, path: "/secrets-test".into(), + method: String::new(), headers: BTreeMap::new(), body: Value::Null, params: BTreeMap::new(), diff --git a/crates/executor-core/tests/sdk_subscriber_token.rs b/crates/executor-core/tests/sdk_subscriber_token.rs index 5b2fc72..702313a 100644 --- a/crates/executor-core/tests/sdk_subscriber_token.rs +++ b/crates/executor-core/tests/sdk_subscriber_token.rs @@ -112,6 +112,7 @@ fn request(app_id: AppId, with_principal: bool) -> ExecRequest { script_name: "token-test".into(), invocation_type: InvocationType::Http, path: "/token-test".into(), + method: String::new(), headers: BTreeMap::new(), body: Value::Null, params: BTreeMap::new(), diff --git a/crates/executor-core/tests/stdlib.rs b/crates/executor-core/tests/stdlib.rs index 5c61733..b19a10b 100644 --- a/crates/executor-core/tests/stdlib.rs +++ b/crates/executor-core/tests/stdlib.rs @@ -29,6 +29,7 @@ fn baseline_request() -> ExecRequest { script_name: "stdlib".into(), invocation_type: InvocationType::Http, path: "/stdlib-test".into(), + method: String::new(), headers: BTreeMap::new(), body: Value::Null, params: BTreeMap::new(), diff --git a/crates/manager-core/src/dispatcher.rs b/crates/manager-core/src/dispatcher.rs index 5be31d7..8bdf734 100644 --- a/crates/manager-core/src/dispatcher.rs +++ b/crates/manager-core/src/dispatcher.rs @@ -357,6 +357,8 @@ impl Dispatcher { script_name: script.name.clone(), invocation_type: InvocationType::Function, path: "/trigger/queue".into(), + // Non-HTTP trigger invocation — no request method. + method: String::new(), headers: std::collections::BTreeMap::new(), body: serde_json::Value::Null, params: std::collections::BTreeMap::new(), @@ -658,6 +660,8 @@ impl Dispatcher { script_name: resolved.script_name.clone(), invocation_type: InvocationType::Function, path: format!("/trigger/{}", trigger_event.source()), + // Non-HTTP trigger invocation — no request method. + method: String::new(), headers: std::collections::BTreeMap::new(), body: serde_json::Value::Null, params: std::collections::BTreeMap::new(), @@ -719,6 +723,7 @@ impl Dispatcher { script_name: payload.script_name.clone(), invocation_type: InvocationType::Http, path: payload.path.clone(), + method: payload.method.clone(), headers: payload.headers, body: payload.body, params: payload.params, @@ -814,6 +819,7 @@ impl Dispatcher { script_name: script.name.clone(), invocation_type: InvocationType::Function, path: "/invoke_async".into(), + method: String::new(), headers: std::collections::BTreeMap::new(), body: args, params: std::collections::BTreeMap::new(), diff --git a/crates/orchestrator-core/src/api.rs b/crates/orchestrator-core/src/api.rs index 5ef7b6a..852affe 100644 --- a/crates/orchestrator-core/src/api.rs +++ b/crates/orchestrator-core/src/api.rs @@ -584,6 +584,8 @@ fn build_exec_request( script_name: name.to_string(), invocation_type: InvocationType::Http, path: format!("/api/execute/{id}"), + // The `/api/v1/execute/{id}` bypass is POST-only. + method: "POST".into(), headers: hmap, body: body_json, params: BTreeMap::new(),