Skip to content

feat(http): standalone HTTP server worker (duplicate of built-in iii-http) - #389

Merged
guibeira merged 35 commits into
mainfrom
feat/http-worker
Jul 3, 2026
Merged

feat(http): standalone HTTP server worker (duplicate of built-in iii-http)#389
guibeira merged 35 commits into
mainfrom
feat/http-worker

Conversation

@guibeira

@guibeira guibeira commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

What

Standalone http worker in the registry that duplicates the engine's built-in iii-http (inbound HTTP server), running as a normal SDK worker binary instead of an in-process engine module. Registers the http trigger type, runs its own axum server, routes incoming requests to functions via the SDK. All code under http/; the engine is untouched.

Features (parity with engine/src/workers/rest_api)

  • Trigger type http (api_path, http_method, condition_function_id, middleware_function_ids); route table keyed by trigger id.
  • Routing: path params, query, headers, methods (GET/POST/PUT/PATCH/DELETE), literal-over-:param precedence, conflict rejection, 404, 405 + Allow header.
  • Response: status_code/headers/body mapping; propagates the invoked function's own error code/message on 500.
  • Middleware (global + per-route, continue/respond), conditions (422), CORS (config + permissive), request timeout (504 via TimeoutLayer), concurrency-limit layer.
  • Streaming (request + response) over SDK channels: control messages (set_status/set_headers) + chunked binary body; buffered fallback.
  • Configuration worker integration + full hot-reload: middleware/default_timeout via a per-request config cell; cors/timeout/concurrency_request_limit via a swappable HotRouter (same-address layer rebuild); host/port via live listener rebind (bind-new-before-stop-old, graceful drain + abort safety net). Overlapping config events serialized by an apply lock.
  • OpenTelemetry: per-request HTTP span + tags, W3C trace-context (traceparent) parent propagation, iii.http.requests counter metric, and the tracing→OTel bridge wired in main.rs so spans export in production.
  • Coexistence / migration: env-configurable trigger type (III_HTTP_TRIGGER_TYPE, default http-ng) so it runs alongside the built-in; boot refuses if set to http while the built-in iii-http is active.

Tests

Unit + e2e against a live engine (the e2e harness FAILS, not skips, when no engine is reachable): methods, routing, 405, middleware (with call-count assertions), condition, CORS, streaming (incl. trigger-wins-the-race + request-body), timeout/concurrency, config hot-reload (middleware, CORS same-address, host/port rebind), OTEL span/trace-context/metric capture, and cutover. Every change was reviewed (spec + quality) before landing.

Known non-blocking divergences (documented)

  • concurrency_request_limit is not a global cap — matches the engine's identical ConcurrencyLimitLayer construction (likely an upstream behavior).
  • condition.rs timeout maps to 500 vs middleware's 504 (unreachable behind the tower TimeoutLayer).
  • Request body is buffered to memory (16 MB cap) then sent as one frame, not streamed frame-by-frame.

Transition plan (support both now, remove iii-http later)

Worker default trigger type http-ng (coexist) → at cutover set III_HTTP_TRIGGER_TYPE=http and omit iii-http from the engine config.yaml (with a config file, an unlisted default worker does not run) → later remove the built-in. Same trigger config / request / response / RestApiConfig contracts, so function authors change nothing.

Summary by CodeRabbit

  • New Features

    • Added an HTTP worker that exposes registered functions as HTTP endpoints.
    • Added support for routing, path parameters, request-body streaming, middleware, conditions, CORS, and live configuration updates.
    • Added tracing and metrics visibility for HTTP requests.
  • Bug Fixes

    • Improved handling for method mismatches, missing routes, timeouts, and streamed responses.
    • Preserved backend-provided error codes/messages in HTTP error responses.

guibeira added 30 commits June 30, 2026 18:57
HttpTriggerHandler bridges the SDK's register/unregister_trigger
callbacks to RouteTable; unregister only relies on config.id since
the SDK passes a stub for the rest.
Add server.rs (bind, router, CORS/timeout/concurrency, graceful shutdown),
handler.rs (buffered dynamic_handler), boot.rs (register http trigger type +
serve). Wire main.rs. Add literal-over-param precedence to match_route. Derive
JsonSchema on HttpRequest/TriggerMetadata and Serialize on HttpTriggerConfig
for the trigger-type builder.
Add tests/common/{engine,worker,backend,mod} (connect-or-skip engine,
per-test worker boot, echo backend) and e2e_methods.rs covering
GET/POST/PUT/PATCH/DELETE echo + unmatched-route 404.
README covers install, quickstart, configuration, and the http trigger
type per worker-readme.md conventions. Closes the config.rs unit-test
gap for RestApiConfig::json_schema() embedding nested $ref definitions,
previously only covered indirectly via e2e.
get_or_init now panics (fails the test) if no engine is reachable, and
returns Arc<IIIClient> directly. e2e tests only make sense with an engine,
so a silent skip-as-pass gave false-green results.
Two requests to two routes; assert the respond-middleware counter == 2.
Replaces the static 404 test with one that registers a route, hits it (200),
unregisters the trigger, and asserts the route now 404s -- exercising the
TriggerHandler unregister -> RouteTable removal path end-to-end.
… when iii-http builtin active

The built-in iii-http worker owns the `http` trigger type; two owners on one
engine collide (last-write-wins). Default this worker to `http-ng` so it runs
safely alongside iii-http, and add a boot guard that refuses to start when
III_HTTP_TRIGGER_TYPE=http is set while iii-http is still connected, turning
the silent collision into a fail-fast error.
…sting route

Match the engine's iii-http (axum per-method routing) parity: a path that
matches a registered route but with the wrong HTTP method now returns 405
Method Not Allowed with an Allow header, instead of falling through to the
generic 404.
Covers the previously-untested direction: a function reading its streamed
request body via ChannelReader on HttpRequest.request_body, for both
non-JSON and JSON content types. Mirrors the e2e_streaming.rs harness.
Verifies (test-driven) that a slow handler function returns 504 Gateway
Timeout, matching the engine's iii-http: the tower TimeoutLayer wraps the
whole router, so its deadline always starts (and thus fires) before the
handler's own per-invocation iii.trigger timeout of the same duration —
confirmed with repeated runs and a tight-margin stress loop, so no
production fix was needed. Also adds a throughput smoke test for the
ConcurrencyLimitLayer (queues excess requests; all must still succeed).

Adds start_http_worker_with_timeout / start_http_worker_with_concurrency_limit
test helpers and register_slow_backend / register_sleep_backend fixtures.
@vercel

vercel Bot commented Jul 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
workers Ready Ready Preview, Comment Jul 2, 2026 12:33pm
workers-tech-spec Ready Ready Preview, Comment Jul 2, 2026 12:33pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a new iii-http Rust worker crate that exposes registered functions as HTTP endpoints. Includes crate scaffolding, config model, boot/server lifecycle with hot-reload and rebind, trigger routing, middleware/condition execution, a streaming request handler, OpenTelemetry observability, CLI entrypoint, README, and a comprehensive e2e test suite. Also adds a release workflow tag pattern.

Changes

iii-http worker crate

Layer / File(s) Summary
Crate scaffolding and docs
http/Cargo.toml, http/build.rs, http/iii.worker.yaml, http/README.md, .github/workflows/release.yml
New crate metadata/deps, build script, worker manifest, README docs, and an added http/v* release tag pattern.
Configuration model
http/src/config.rs
RestApiConfig, MiddlewareConfig, CorsConfig with defaults, normalization, JSON/YAML serde, schema generation, and unit tests.
HTTP types and routing
http/src/types.rs, http/src/trigger.rs
HttpRequest/HttpResponse/ControlMessage types and RouteTable/HttpTriggerHandler route matching/registration.
Server and hot router
http/src/server.rs
Axum router construction, HotRouter for live swaps, and bind-new/stop-old rebinding logic.
Condition and middleware execution
http/src/condition.rs, http/src/middleware.rs
check_condition truthiness mapping and execute_middleware continue/respond/timeout/error handling.
Observability bridge
http/src/observability.rs
LazyGlobalTracer and otel_layer bridging tracing spans to OpenTelemetry.
Request handler
http/src/handler.rs
dynamic_handler implementing routing, tracing, body streaming, condition/middleware, and streaming/buffered responses.
Boot and hot-reload
http/src/boot.rs, http/src/configuration.rs
BootHandle/start(), built-in worker guard, and live config registration/apply/rebind.
Entrypoint and manifest
http/src/lib.rs, http/src/manifest.rs, http/src/main.rs
Public module exports, ModuleManifest, and CLI binary wiring boot, config, and shutdown.
E2E test harness and suite
http/tests/common/*, http/tests/e2e_*.rs
Shared engine/worker/backend helpers and tests for methods, routing, CORS, errors, conditions, middleware, streaming, timeouts, OTEL tracing/metrics, and hot-reload.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant DynamicHandler
  participant Middleware
  participant ConditionFn
  participant BackendFunction
  participant ResponseChannel

  Client->>DynamicHandler: HTTP request
  DynamicHandler->>DynamicHandler: match route, snapshot config, start span
  DynamicHandler->>Middleware: execute_middleware(preHandler)
  Middleware-->>DynamicHandler: Continue or short-circuit Response
  DynamicHandler->>ConditionFn: check_condition(condition_function_id)
  ConditionFn-->>DynamicHandler: true/false
  DynamicHandler->>BackendFunction: trigger(HttpRequest)
  BackendFunction->>ResponseChannel: write control frames + body chunks
  ResponseChannel-->>DynamicHandler: streamed bytes / buffered return
  DynamicHandler-->>Client: HTTP response (buffered or chunked)
Loading
sequenceDiagram
  participant ConfigBus
  participant ConfigTrigger
  participant ConfigCell
  participant HotRouter
  participant NewServer
  participant OldServer

  ConfigBus->>ConfigTrigger: configuration:updated (http)
  ConfigTrigger->>ConfigBus: fetch_config
  ConfigBus-->>ConfigTrigger: RestApiConfig
  alt same host/port
    ConfigTrigger->>ConfigCell: apply_config (swap snapshot)
    ConfigTrigger->>HotRouter: rebuild_layers
  else host/port changed
    ConfigTrigger->>NewServer: bind new listener
    ConfigTrigger->>ConfigCell: apply_config (swap snapshot)
    ConfigTrigger->>HotRouter: rebuild_layers
    ConfigTrigger->>NewServer: spawn_server
    ConfigTrigger->>OldServer: stop_old_server (graceful then hard abort)
  end
Loading

Possibly related PRs

  • iii-hq/workers#138: Harness-side baggage/traceparent and message-id injection aligns with this PR's OTEL span propagation and attribute recording in the HTTP handler.

Suggested reviewers: sergiofilhowz

Poem

A rabbit built a door of code,
Where HTTP requests find their road,
Streams and spans and configs bright,
Hot-reload swaps without a fight,
Hop, hop, hooray — the worker's live tonight! 🐇🌐

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding a standalone HTTP worker that duplicates the built-in iii-http server.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/http-worker

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

skill-check — worker

0 verified, 31 skipped (no docs/).

Layer Result
structure
vale
ai
render

Four for four. Nicely done.

…th engine)

The SDK dispatches each function invocation via tokio::spawn, so
http::on-config-change can run concurrently for overlapping
configuration:updated events, letting two edits race on the ConfigCell/
ServerControlCell swap. Add an apply_lock: Arc<tokio::sync::Mutex<()>>,
created in boot::start alongside the other shared cells and threaded
through register_config_trigger into on_config_change, which now holds
it for the whole re-fetch/validate/swap/rebuild-or-rebind sequence.
Mirrors the engine's iii-http apply_lock in api_core.rs.
…ngine, dead_code, fmt)

Trigger type is now a constant `iii_http::TRIGGER_TYPE = "http"` (no
III_HTTP_TRIGGER_TYPE env, no http-ng default) and the boot guard
unconditionally refuses to start if the built-in iii-http worker is active,
since there's no longer a coexistence mode. Drops the now-obsolete cutover
test and updates the README accordingly.

Also makes the resulting CI (fmt/clippy/test, run with no engine) green:
- tests/common/engine.rs: get_or_init/connect_fresh go back to
  connect-or-skip (Option<Arc<IIIClient>>, matching sibling workers) instead
  of panicking when no engine is reachable, so cargo test doesn't fail in
  CI. Set III_E2E_REQUIRE=1 to force the old panic-on-missing-engine
  behavior for intentional e2e runs. All call sites updated to
  `let Some(iii) = engine::get_or_init().await else { return };`.
- tests/common/{backend,engine,mod,worker}.rs: `#![allow(dead_code)]` per
  file, since each e2e binary only exercises a subset of the shared
  helpers and -D warnings was failing on dead_code.
- cargo fmt --all from inside http/ (its own workspace) to clear
  pre-existing formatting drift across src/ and tests/.
…idation)

iii worker add extracts the release archive by worker name, so the [[bin]]
name and iii.worker.yaml bin must equal 'http', not 'iii-http'.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (7)
http/src/config.rs (2)

424-437: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test doesn't exercise automatic sorting.

rest_api_config_middleware_pre_sorted manually calls config.middleware.sort_by_key(...) after deserializing rather than calling .normalized(), so it doesn't actually verify the sorting behavior implemented in RestApiConfig::normalized() (Lines 70-79). It only re-tests Vec::sort_by_key itself. Consider asserting via config.normalized() instead to cover the real code path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@http/src/config.rs` around lines 424 - 437, The test
rest_api_config_middleware_pre_sorted is bypassing the real sorting logic by
calling sort_by_key directly, so it does not cover RestApiConfig::normalized.
Update the test to deserialize into RestApiConfig and then assert on the result
of config.normalized() instead, using the middleware field and
function_id/priority values to verify the automatic ordering path.

128-142: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

phase is an unvalidated free-form string.

Only "preHandler" is currently supported per the doc comment, but phase accepts any string with no enum or validation — a typo (e.g. "preHalndler") would silently pass deny_unknown_fields and JSON schema validation, and only fail (or silently no-op) downstream wherever phase is consumed. Consider using a #[serde(rename_all = "camelCase")] enum with a single PreHandler variant (extensible later) instead of String, so invalid values are rejected at deserialize/schema time rather than at runtime.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@http/src/config.rs` around lines 128 - 142, The MiddlewareConfig.phase field
is currently an unvalidated String, so invalid values can slip through
deserialization and schema checks. Replace it with a strongly typed enum (for
example a single PreHandler variant) and update the defaulting logic currently
in default_phase to return that enum value. Make sure the serde representation
uses the existing camelCase naming so MiddlewareConfig, phase, and any
downstream consumers reject typos at config parse time instead of failing later.
http/tests/common/backend.rs (1)

22-48: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting the repeated register_function + register_trigger boilerplate.

Each of the seven register_* helpers repeats the same register_function(...) + register_trigger(RegisterTriggerInput { trigger_type: iii_http::TRIGGER_TYPE, ... }).expect(...) shape, differing only in function id and config JSON. A small shared helper (e.g. fn bind_http_trigger(iii, function_id, config: Value)) would reduce duplication across this file.

Separately, register_slow_backend (Lines 232-258) and register_sleep_backend (Lines 264-290) have identical bodies aside from the function-id prefix; one could delegate to the other.

Also applies to: 54-78, 85-120, 232-258, 264-290, 296-317

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@http/tests/common/backend.rs` around lines 22 - 48, The HTTP backend helpers
repeat the same register_function plus RegisterTriggerInput::register_trigger
pattern, so extract that boilerplate into a shared helper such as
bind_http_trigger and have register_echo_backend and the other register_*
helpers call it with their function_id and config JSON. Also remove the
duplication between register_slow_backend and register_sleep_backend by making
one delegate to the other, keeping only the differing function-id prefix.
http/src/main.rs (1)

128-133: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add SIGTERM handling for graceful shutdown.

Only ctrl_c() is awaited before shutdown. In containerized/orchestrated environments (Docker, Kubernetes), stop/redeploy typically sends SIGTERM, not SIGINT — without a SIGTERM branch this worker would be force-killed without running boot.shutdown()/iii.shutdown_async(), skipping graceful connection/listener cleanup.

♻️ Proposed fix to also await SIGTERM on unix
+    #[cfg(unix)]
+    let terminate = async {
+        tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
+            .expect("failed to install SIGTERM handler")
+            .recv()
+            .await;
+    };
+    #[cfg(not(unix))]
+    let terminate = std::future::pending::<()>();
+
-    tokio::signal::ctrl_c().await?;
+    tokio::select! {
+        _ = tokio::signal::ctrl_c() => {},
+        _ = terminate => {},
+    }
     tracing::info!("iii-http shutting down");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@http/src/main.rs` around lines 128 - 133, Add SIGTERM-aware shutdown handling
in main so graceful cleanup runs in orchestrated environments. Update the
shutdown wait logic around the existing tokio::signal::ctrl_c() call in main to
also listen for SIGTERM on Unix (for example via tokio::signal::unix), then
proceed through the same shutdown path that calls boot.shutdown() and
iii.shutdown_async(). Keep the shutdown flow centralized so both ctrl_c and
SIGTERM trigger the same cleanup sequence.
http/tests/e2e_methods.rs (1)

16-128: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract shared boilerplate across the 5 method tests.

GET/POST/PUT/PATCH/DELETE tests are structurally identical (connect → boot → register → wait_for_route → request → assert → shutdown), differing only in method name, request body, and expected echoed field. Consolidating into a table-driven helper would cut ~90 lines of duplication and make adding new methods trivial.

♻️ Suggested direction (table-driven helper)
async fn run_method_test(method: reqwest::Method, path: &str, body: Option<Value>) {
    let Some(iii) = engine::get_or_init().await else {
        return;
    };
    let boot = worker::start_http_worker(iii.clone()).await;
    backend::register_echo_backend(&iii, path, method.as_str()).await;
    common::wait_for_route(&boot.routes, method.as_str(), path).await;

    let url = format!("http://{}{}", boot.local_addr, path);
    let mut req = reqwest::Client::new().request(method.clone(), &url);
    if let Some(b) = &body {
        req = req.json(b);
    }
    let resp = req.send().await.unwrap();
    assert_eq!(resp.status(), 200);
    let v: Value = resp.json().await.unwrap();
    assert_eq!(v["method"], method.as_str());
    if let Some(b) = &body {
        assert_eq!(&v["body"], b);
    }

    boot.shutdown().await;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@http/tests/e2e_methods.rs` around lines 16 - 128, The five tests in
get_echoes_method_and_query, post_echoes_body, put_echoes_body,
patch_echoes_body, and delete_echoes_method duplicate the same
setup/request/assert/shutdown flow. Refactor this into a shared helper (for
example, a table-driven runner near these test functions) that takes the HTTP
method, route path, and optional JSON body, then performs engine::get_or_init,
worker::start_http_worker, backend::register_echo_backend,
common::wait_for_route, and the request/assertions. Keep each test as a thin
invocation of the helper with its method-specific expectations.
http/tests/e2e_reload.rs (1)

20-42: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider consolidating the repeated poll-until helpers.

wait_for_middleware, wait_for_cors, wait_for_port, wait_until_serves, and wait_until_refused all repeat the same "loop N times, sleep, check predicate, else panic/return false" shape. A single generic poll_until(predicate, attempts, interval) helper (in common/mod.rs, alongside the existing wait_for_route) would reduce duplication as more reload tests are added.

♻️ Example generic helper
pub async fn poll_until<F, Fut>(mut check: F, attempts: usize, interval: Duration) -> bool
where
    F: FnMut() -> Fut,
    Fut: std::future::Future<Output = bool>,
{
    for _ in 0..attempts {
        if check().await {
            return true;
        }
        tokio::time::sleep(interval).await;
    }
    false
}

Also applies to: 238-276

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@http/tests/e2e_reload.rs` around lines 20 - 42, The reload test helpers
repeat the same poll/sleep/check pattern across wait_for_middleware and
wait_for_cors, and the same shape also appears in wait_for_port,
wait_until_serves, and wait_until_refused. Extract this into a reusable generic
polling helper, like poll_until, in common/mod.rs near wait_for_route, then
update the existing helpers to delegate to it with their specific predicates and
success/failure behavior. Keep the helper flexible enough to support both
“return early” and “panic/false on timeout” call sites.
http/src/configuration.rs (1)

120-133: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Retrying on NOT_FOUND adds needless boot latency.

configuration::get returning NOT_FOUND is the expected answer on first boot (no stored value yet), but trigger_with_retry treats every Err as transient and retries CONFIG_RETRIES times with backoff (~250ms + 500ms ≈ 750ms) before the NOT_FOUND branch here even runs. Since try_get_config_value is called by both should_seed_initial_value and fetch_config, a fresh install pays this delay more than once. Consider short-circuiting NOT_FOUND so it isn't retried (e.g., a non-retrying get, or a "retryable" predicate passed to trigger_with_retry).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@http/src/configuration.rs` around lines 120 - 133, `try_get_config_value` is
retrying the expected `NOT_FOUND` case via `trigger_with_retry`, which adds
unnecessary boot delay. Update the configuration lookup path in
`try_get_config_value` (and any shared helper used by
`should_seed_initial_value`/`fetch_config`) so `NOT_FOUND` is treated as a
non-retryable result and returns `Ok(None)` immediately, either by using a
non-retrying get call or by adding a retryability predicate to
`trigger_with_retry` that excludes `NOT_FOUND`.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@http/iii.worker.yaml`:
- Around line 9-15: The seeded HTTP worker manifest uses a host default that
conflicts with the documented/configured default. Update the host value in the
http worker seed to match config.rs::default_host() and the README Configuration
table, or change those docs if the new default is intentional. Keep the seed
aligned with iii worker add http so new installs bind consistently.

In `@http/README.md`:
- Around line 70-77: The HTTP docs and the shipped worker template disagree on
the default host value, so update the README table to match the actual
new-install seed used by the HTTP worker config or otherwise align both sources.
Use the existing `RestApiConfig::default()`/`config.rs` default and the
`http/iii.worker.yaml` template as the reference points, and make sure the
`host` row in `http/README.md` reflects the same bind address users get after
`iii worker add http`.

In `@http/src/trigger.rs`:
- Around line 34-47: `route_signature` is not normalizing path segments the same
way as `extract_path_params` and `match_route`, so routes that differ only by
leading/trailing slashes can evade conflict detection. Update `route_signature`
to drop empty segments when building the signature, keeping its normalization
consistent with the other path-matching helpers and the `api_path` convention.
Use `route_signature` as the main place to align signature generation with
`extract_path_params`/`match_route`.

In `@http/tests/e2e_methods.rs`:
- Around line 16-145: The e2e tests currently rely on explicit
boot.shutdown().await calls in functions like get_echoes_method_and_query,
post_echoes_body, put_echoes_body, patch_echoes_body, delete_echoes_method, and
unmatched_route_returns_404_envelope, so an early panic can leave the worker
running. Add a panic-safe teardown path by introducing RAII ownership around
BootHandle or a guard that triggers shutdown automatically in Drop, and apply
the same pattern to the matching e2e_routing.rs, e2e_cors.rs, and e2e_errors.rs
tests so cleanup always runs even when assertions fail.

---

Nitpick comments:
In `@http/src/config.rs`:
- Around line 424-437: The test rest_api_config_middleware_pre_sorted is
bypassing the real sorting logic by calling sort_by_key directly, so it does not
cover RestApiConfig::normalized. Update the test to deserialize into
RestApiConfig and then assert on the result of config.normalized() instead,
using the middleware field and function_id/priority values to verify the
automatic ordering path.
- Around line 128-142: The MiddlewareConfig.phase field is currently an
unvalidated String, so invalid values can slip through deserialization and
schema checks. Replace it with a strongly typed enum (for example a single
PreHandler variant) and update the defaulting logic currently in default_phase
to return that enum value. Make sure the serde representation uses the existing
camelCase naming so MiddlewareConfig, phase, and any downstream consumers reject
typos at config parse time instead of failing later.

In `@http/src/configuration.rs`:
- Around line 120-133: `try_get_config_value` is retrying the expected
`NOT_FOUND` case via `trigger_with_retry`, which adds unnecessary boot delay.
Update the configuration lookup path in `try_get_config_value` (and any shared
helper used by `should_seed_initial_value`/`fetch_config`) so `NOT_FOUND` is
treated as a non-retryable result and returns `Ok(None)` immediately, either by
using a non-retrying get call or by adding a retryability predicate to
`trigger_with_retry` that excludes `NOT_FOUND`.

In `@http/src/main.rs`:
- Around line 128-133: Add SIGTERM-aware shutdown handling in main so graceful
cleanup runs in orchestrated environments. Update the shutdown wait logic around
the existing tokio::signal::ctrl_c() call in main to also listen for SIGTERM on
Unix (for example via tokio::signal::unix), then proceed through the same
shutdown path that calls boot.shutdown() and iii.shutdown_async(). Keep the
shutdown flow centralized so both ctrl_c and SIGTERM trigger the same cleanup
sequence.

In `@http/tests/common/backend.rs`:
- Around line 22-48: The HTTP backend helpers repeat the same register_function
plus RegisterTriggerInput::register_trigger pattern, so extract that boilerplate
into a shared helper such as bind_http_trigger and have register_echo_backend
and the other register_* helpers call it with their function_id and config JSON.
Also remove the duplication between register_slow_backend and
register_sleep_backend by making one delegate to the other, keeping only the
differing function-id prefix.

In `@http/tests/e2e_methods.rs`:
- Around line 16-128: The five tests in get_echoes_method_and_query,
post_echoes_body, put_echoes_body, patch_echoes_body, and delete_echoes_method
duplicate the same setup/request/assert/shutdown flow. Refactor this into a
shared helper (for example, a table-driven runner near these test functions)
that takes the HTTP method, route path, and optional JSON body, then performs
engine::get_or_init, worker::start_http_worker, backend::register_echo_backend,
common::wait_for_route, and the request/assertions. Keep each test as a thin
invocation of the helper with its method-specific expectations.

In `@http/tests/e2e_reload.rs`:
- Around line 20-42: The reload test helpers repeat the same poll/sleep/check
pattern across wait_for_middleware and wait_for_cors, and the same shape also
appears in wait_for_port, wait_until_serves, and wait_until_refused. Extract
this into a reusable generic polling helper, like poll_until, in common/mod.rs
near wait_for_route, then update the existing helpers to delegate to it with
their specific predicates and success/failure behavior. Keep the helper flexible
enough to support both “return early” and “panic/false on timeout” call sites.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e62ce3ac-5546-4439-8448-a66f9ffc3143

📥 Commits

Reviewing files that changed from the base of the PR and between 336d1b5 and 1e84a51.

⛔ Files ignored due to path filters (1)
  • http/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (34)
  • .github/workflows/release.yml
  • http/Cargo.toml
  • http/README.md
  • http/build.rs
  • http/iii.worker.yaml
  • http/src/boot.rs
  • http/src/condition.rs
  • http/src/config.rs
  • http/src/configuration.rs
  • http/src/handler.rs
  • http/src/lib.rs
  • http/src/main.rs
  • http/src/manifest.rs
  • http/src/middleware.rs
  • http/src/observability.rs
  • http/src/server.rs
  • http/src/trigger.rs
  • http/src/types.rs
  • http/tests/common/backend.rs
  • http/tests/common/engine.rs
  • http/tests/common/mod.rs
  • http/tests/common/worker.rs
  • http/tests/e2e_condition.rs
  • http/tests/e2e_cors.rs
  • http/tests/e2e_errors.rs
  • http/tests/e2e_methods.rs
  • http/tests/e2e_middleware.rs
  • http/tests/e2e_otel.rs
  • http/tests/e2e_otel_metrics.rs
  • http/tests/e2e_reload.rs
  • http/tests/e2e_request_body.rs
  • http/tests/e2e_routing.rs
  • http/tests/e2e_streaming.rs
  • http/tests/e2e_timeout.rs

Comment thread http/iii.worker.yaml
Comment on lines +9 to +15
port: 3111
host: 127.0.0.1
default_timeout: 30000
concurrency_request_limit: 1024
cors:
allowed_origins: ['*']
allowed_methods: [GET, POST, PUT, DELETE, OPTIONS]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

host seed value diverges from the code default and README doc.

Code default (config.rs::default_host()) and the README's Configuration table both document host as 0.0.0.0, but this manifest seeds new installs with 127.0.0.1. A user reading the README quickstart would expect the worker to listen on all interfaces by default, but iii worker add http will actually bind only to loopback.

💡 Align the seed with the documented default, or update the README
 config:
   port: 3111
-  host: 127.0.0.1
+  host: 0.0.0.0
   default_timeout: 30000
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
port: 3111
host: 127.0.0.1
default_timeout: 30000
concurrency_request_limit: 1024
cors:
allowed_origins: ['*']
allowed_methods: [GET, POST, PUT, DELETE, OPTIONS]
port: 3111
host: 0.0.0.0
default_timeout: 30000
concurrency_request_limit: 1024
cors:
allowed_origins: ['*']
allowed_methods: [GET, POST, PUT, DELETE, OPTIONS]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@http/iii.worker.yaml` around lines 9 - 15, The seeded HTTP worker manifest
uses a host default that conflicts with the documented/configured default.
Update the host value in the http worker seed to match config.rs::default_host()
and the README Configuration table, or change those docs if the new default is
intentional. Keep the seed aligned with iii worker add http so new installs bind
consistently.

Comment thread http/README.md
Comment on lines +70 to +77
| Field | Default | Description |
|---|---|---|
| `port` | `3111` | TCP port the HTTP server binds to. `0` binds an OS-assigned ephemeral port. |
| `host` | `0.0.0.0` | Host/interface to bind. |
| `default_timeout` | `30000` (ms) | Per-request timeout; on expiry the server returns `504`. |
| `cors.allowed_origins` | `[]` (permissive) | Allowed CORS origins. An empty list allows any origin. |
| `cors.allowed_methods` | `[]` (permissive) | Allowed CORS methods. An empty list allows any method. |
| `concurrency_request_limit` | `1024` | Maximum in-flight requests; requests over the limit wait for a slot. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Documented host default doesn't match the shipped worker config.

This table states host defaults to 0.0.0.0, matching RestApiConfig::default() in config.rs. However, http/iii.worker.yaml seeds new installs with host: 127.0.0.1, so users following this doc will see a different actual bind address after iii worker add http. Reconcile the two (see companion comment on http/iii.worker.yaml).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@http/README.md` around lines 70 - 77, The HTTP docs and the shipped worker
template disagree on the default host value, so update the README table to match
the actual new-install seed used by the HTTP worker config or otherwise align
both sources. Use the existing `RestApiConfig::default()`/`config.rs` default
and the `http/iii.worker.yaml` template as the reference points, and make sure
the `host` row in `http/README.md` reflects the same bind address users get
after `iii worker add http`.

Comment thread http/src/trigger.rs
Comment on lines +34 to +47
pub fn route_signature(http_method: &str, http_path: &str) -> String {
let shape = http_path
.split('/')
.map(|segment| {
if segment.starts_with(':') {
"{}"
} else {
segment
}
})
.collect::<Vec<&str>>()
.join("/");
format!("{}:{}", http_method.to_uppercase(), shape)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

route_signature doesn't normalize empty segments, so conflict detection can be bypassed.

route_signature splits on / without dropping empty segments, whereas extract_path_params/match_route filter them (!s.is_empty()). As a result, two routes that differ only by leading/trailing slashes (e.g. "/a/:x" vs "a/:x", or "/a/:x/" vs "/a/:x") produce different signatures and escape the conflict check, yet both match the same request paths with identical param counts — leaving match_route's outcome dependent on nondeterministic HashMap iteration order. Normalizing here (filtering empty segments) keeps signature/matching consistent. This also aligns with the guidance that api_path should be free of leading slashes.

🛠️ Proposed normalization
 pub fn route_signature(http_method: &str, http_path: &str) -> String {
     let shape = http_path
         .split('/')
+        .filter(|segment| !segment.is_empty())
         .map(|segment| {
             if segment.starts_with(':') {
                 "{}"
             } else {
                 segment
             }
         })
         .collect::<Vec<&str>>()
         .join("/");
     format!("{}:{}", http_method.to_uppercase(), shape)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub fn route_signature(http_method: &str, http_path: &str) -> String {
let shape = http_path
.split('/')
.map(|segment| {
if segment.starts_with(':') {
"{}"
} else {
segment
}
})
.collect::<Vec<&str>>()
.join("/");
format!("{}:{}", http_method.to_uppercase(), shape)
}
pub fn route_signature(http_method: &str, http_path: &str) -> String {
let shape = http_path
.split('/')
.filter(|segment| !segment.is_empty())
.map(|segment| {
if segment.starts_with(':') {
"{}"
} else {
segment
}
})
.collect::<Vec<&str>>()
.join("/");
format!("{}:{}", http_method.to_uppercase(), shape)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@http/src/trigger.rs` around lines 34 - 47, `route_signature` is not
normalizing path segments the same way as `extract_path_params` and
`match_route`, so routes that differ only by leading/trailing slashes can evade
conflict detection. Update `route_signature` to drop empty segments when
building the signature, keeping its normalization consistent with the other
path-matching helpers and the `api_path` convention. Use `route_signature` as
the main place to align signature generation with
`extract_path_params`/`match_route`.

Source: Learnings

Comment thread http/tests/e2e_methods.rs
Comment on lines +16 to +145
#[tokio::test]
#[serial]
async fn get_echoes_method_and_query() {
let Some(iii) = engine::get_or_init().await else {
return;
};
let boot = worker::start_http_worker(iii.clone()).await;
backend::register_echo_backend(&iii, "/echo-get", "GET").await;
common::wait_for_route(&boot.routes, "GET", "/echo-get").await;

let url = format!("http://{}/echo-get?q=1", boot.local_addr);
let resp = reqwest::Client::new().get(&url).send().await.unwrap();
assert_eq!(resp.status(), 200);
let v: serde_json::Value = resp.json().await.unwrap();
assert_eq!(v["method"], "GET");
assert_eq!(v["query_params"]["q"], "1");

boot.shutdown().await;
}

#[tokio::test]
#[serial]
async fn post_echoes_body() {
let Some(iii) = engine::get_or_init().await else {
return;
};
let boot = worker::start_http_worker(iii.clone()).await;
backend::register_echo_backend(&iii, "/echo-post", "POST").await;
common::wait_for_route(&boot.routes, "POST", "/echo-post").await;

let url = format!("http://{}/echo-post", boot.local_addr);
let resp = reqwest::Client::new()
.post(&url)
.json(&json!({ "hi": 1 }))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let v: serde_json::Value = resp.json().await.unwrap();
assert_eq!(v["method"], "POST");
assert_eq!(v["body"]["hi"], 1);

boot.shutdown().await;
}

#[tokio::test]
#[serial]
async fn put_echoes_body() {
let Some(iii) = engine::get_or_init().await else {
return;
};
let boot = worker::start_http_worker(iii.clone()).await;
backend::register_echo_backend(&iii, "/echo-put", "PUT").await;
common::wait_for_route(&boot.routes, "PUT", "/echo-put").await;

let url = format!("http://{}/echo-put", boot.local_addr);
let resp = reqwest::Client::new()
.put(&url)
.json(&json!({ "n": 7 }))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let v: serde_json::Value = resp.json().await.unwrap();
assert_eq!(v["method"], "PUT");
assert_eq!(v["body"]["n"], 7);

boot.shutdown().await;
}

#[tokio::test]
#[serial]
async fn patch_echoes_body() {
let Some(iii) = engine::get_or_init().await else {
return;
};
let boot = worker::start_http_worker(iii.clone()).await;
backend::register_echo_backend(&iii, "/echo-patch", "PATCH").await;
common::wait_for_route(&boot.routes, "PATCH", "/echo-patch").await;

let url = format!("http://{}/echo-patch", boot.local_addr);
let resp = reqwest::Client::new()
.patch(&url)
.json(&json!({ "p": true }))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let v: serde_json::Value = resp.json().await.unwrap();
assert_eq!(v["method"], "PATCH");
assert_eq!(v["body"]["p"], true);

boot.shutdown().await;
}

#[tokio::test]
#[serial]
async fn delete_echoes_method() {
let Some(iii) = engine::get_or_init().await else {
return;
};
let boot = worker::start_http_worker(iii.clone()).await;
backend::register_echo_backend(&iii, "/echo-delete", "DELETE").await;
common::wait_for_route(&boot.routes, "DELETE", "/echo-delete").await;

let url = format!("http://{}/echo-delete", boot.local_addr);
let resp = reqwest::Client::new().delete(&url).send().await.unwrap();
assert_eq!(resp.status(), 200);
let v: serde_json::Value = resp.json().await.unwrap();
assert_eq!(v["method"], "DELETE");

boot.shutdown().await;
}

#[tokio::test]
#[serial]
async fn unmatched_route_returns_404_envelope() {
let Some(iii) = engine::get_or_init().await else {
return;
};
let boot = worker::start_http_worker(iii.clone()).await;

let url = format!("http://{}/no-such-route", boot.local_addr);
let resp = reqwest::Client::new().get(&url).send().await.unwrap();
assert_eq!(resp.status(), 404);
let v: serde_json::Value = resp.json().await.unwrap();
assert_eq!(v["error"]["code"], "NOT_FOUND");

boot.shutdown().await;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether BootHandle has Drop-based cleanup.
fd -e rs . http/src --exec rg -n -A5 'struct BootHandle|impl Drop for BootHandle' {}

Repository: iii-hq/workers

Length of output: 354


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant HTTP worker files first, then inspect BootHandle and shutdown logic.
fd -a -e rs . http | sort

printf '\n--- BootHandle outline ---\n'
ast-grep outline http/src -q 'BootHandle' || true

printf '\n--- Search for BootHandle / shutdown / Drop definitions ---\n'
rg -n -A8 -B4 'struct BootHandle|impl Drop for BootHandle|async fn shutdown|fn shutdown' http/src

Repository: iii-hq/workers

Length of output: 3106


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- http/src/boot.rs ---'
sed -n '1,180p' http/src/boot.rs

printf '\n%s\n' '--- http/tests/common/worker.rs ---'
sed -n '1,220p' http/tests/common/worker.rs

printf '\n%s\n' '--- http/src/server.rs ---'
sed -n '1,260p' http/src/server.rs

Repository: iii-hq/workers

Length of output: 22232


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- Drop implementations in http/src and http/tests/common ---'
rg -n 'impl Drop for|shutdown\(self\)|graceful\.send|join\.await|ServerControlCell|type ServerControlCell' http/src http/tests/common

Repository: iii-hq/workers

Length of output: 1683


Make worker cleanup panic-safe
BootHandle has no Drop, so an early panic skips boot.shutdown().await and can leave the test worker running for later #[serial] cases. Move teardown into RAII/Drop (or a guard that always shuts down) in these e2e tests and the matching e2e_routing.rs, e2e_cors.rs, and e2e_errors.rs cases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@http/tests/e2e_methods.rs` around lines 16 - 145, The e2e tests currently
rely on explicit boot.shutdown().await calls in functions like
get_echoes_method_and_query, post_echoes_body, put_echoes_body,
patch_echoes_body, delete_echoes_method, and
unmatched_route_returns_404_envelope, so an early panic can leave the worker
running. Add a panic-safe teardown path by introducing RAII ownership around
BootHandle or a guard that triggers shutdown automatically in Drop, and apply
the same pattern to the matching e2e_routing.rs, e2e_cors.rs, and e2e_errors.rs
tests so cleanup always runs even when assertions fail.

@guibeira
guibeira merged commit cfb1ab3 into main Jul 3, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant