Skip to content

feat(rivetkit-rust): select serverless vs envoy runner mode via RIVETKIT_RUNTIME_MODE - #5684

Merged
abcxff merged 4 commits into
mainfrom
stack/feat-rivetkit-rust-select-serverless-vs-envoy-runner-mode-via-rivetkit_runtime_mode-rqvuzoyz
Sep 9, 2026
Merged

abcxff merged 4 commits into
mainfrom
stack/feat-rivetkit-rust-select-serverless-vs-envoy-runner-mode-via-rivetkit_runtime_mode-rqvuzoyz

Conversation

@abcxff

@abcxff abcxff commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

No description provided.

@abcxff

abcxff commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

@claude

claude Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Review: feat(rivetkit-rust): select serverless vs envoy runner mode via RIVETKIT_RUNTIME_MODE

1. serverless_listener.rs duplicates rivetkit_core::serverless_http::serve, which is already available for free

rivetkit-rust/packages/rivetkit/src/serverless_listener.rs (new, 155 lines) reimplements an axum HTTP listener that already exists as rivetkit_core::serverless_http::serve (rivetkit-rust/packages/rivetkit-core/src/serverless_http.rs:77). That function is the established pattern for exactly this use case and is already consumed by container-runner/src/main.rs:441 and rivetkit-typescript/packages/rivetkit-napi/src/registry.rs:503.

Notably, serverless_http is gated behind native-runtime (rivetkit-core/src/lib.rs:19-20), which is a default feature of rivetkit-core, and rivetkit's Cargo.toml depends on rivetkit-core with default features enabled. So serverless_http::serve + ListenerConfig were already reachable from the rivetkit crate at zero cost — the new axum, bytes, and tokio-stream direct dependencies added to rivetkit/Cargo.toml weren't necessary to add.

This also runs against this repo's explicit layering rule for this crate (rivetkit-rust/packages/rivetkit/CLAUDE.md and root CLAUDE.md "RivetKit Layer Architecture"): "rivetkit (Rust) is a thin typed wrapper. If it does more than deserialize, delegate to core, and serialize, the logic should move to rivetkit-core." Rebuilding routing/body-limit/CORS/streaming logic here is exactly the kind of thing that should delegate to serverless_http::serve instead.

Concretely, start_serverless (rivetkit-rust/packages/rivetkit/src/registry.rs:167) could just call:
```rust
rivetkit_core::serverless_http::serve(runtime, ListenerConfig { host: None, port, public_dir: None, application: None }, shutdown).await
```
This would also pick up fixes and behavior for free (see #2 and #3 below) instead of reproducing (and regressing) it.

2. Body-read errors are all misreported as "too long" (413)

serverless_listener.rs:92:
```rust
let body = match to_bytes(body, runtime.max_request_body_bytes()).await {
Ok(body) => body,
Err(_) => return into_response(runtime.incoming_too_long_response()),
};
```
Any failure to read the body (client disconnect mid-upload, malformed chunked encoding, etc.) is folded into "incoming too long" and returns 413. The existing core implementation (serverless_http.rs:214-229) distinguishes this correctly: it checks is_length_limit_error(&error) and only returns the 413 incoming_too_long_response() for that case, falling back to a 400 invalid_request_response("failed to read request body") for any other read failure. The new listener loses that distinction, another point in favor of just calling the core serve() function directly.

3. RuntimeMode::from_env() and listen_port() silently fall back instead of failing fast on invalid input

rivetkit-core/src/registry/mod.rs:256:
```rust
pub fn from_env() -> Self {
match env::var("RIVETKIT_RUNTIME_MODE") {
Ok(value) if value.eq_ignore_ascii_case("serverless") => Self::Serverless,
_ => Self::Envoy,
}
}
```
Any value other than exactly "serverless" (case-insensitive), including typos like "servreless" or an accidentally-set empty string, silently resolves to Envoy mode. The TypeScript counterpart (rivetkit-typescript/packages/rivetkit/src/utils/env-vars.ts:59-66) explicitly rejects anything other than "envoy"/"serverless" by throwing. This is exactly the case the root CLAUDE.md "Fail-By-Default Runtime" section calls out: "Avoid silent no-ops for required runtime behavior... throw an explicit error with actionable context instead of returning early." A platform (e.g. Rivet Compute) that sets this env var to select serverless mode would fail silently into the wrong runtime mode if the value is ever malformed, which is a hard bug to diagnose since nothing errors.

Same pattern in serverless_listener.rs:56-60 (listen_port()): an unparseable RIVET_PORT silently falls back to 3000 instead of erroring, while the TS parsePortEnv (rivetkit-typescript/packages/rivetkit/src/utils/env-vars.ts:73-84) explicitly throws for a non-integer or out-of-range port.

4. Minor: IPv4-only bind vs. established dual-stack pattern

serverless_listener.rs:22 binds Ipv4Addr::UNSPECIFIED (0.0.0.0) only. The existing analogous "serverless front door" in container-runner/src/main.rs:447 deliberately binds dual-stack (::) with an explicit comment: "the engine's metadata client may connect via localhost/::1." If the same constraint applies to this new Rust Registry::start_serverless() listener, the IPv4-only bind could fail to accept that connection in IPv6-preferring environments. Delegating to serverless_http::serve (which already supports host: Some("::")) sidesteps this too.

5. Test coverage

No tests were added for RuntimeMode::from_env() (valid/invalid/case-insensitive values) or for the new listener's request handling. The TypeScript side has explicit coverage for the equivalent invalid/empty RIVETKIT_RUNTIME_MODE and RIVET_PORT cases (rivetkit-typescript/packages/rivetkit/tests/listener.test.ts:38-48). Given finding #3, at minimum a test asserting current fallback behavior (or, better, the corrected fail-fast behavior) would be valuable.


Summary: the core idea (selecting envoy vs. serverless mode via RIVETKIT_RUNTIME_MODE, mirroring the TypeScript registry) is sound and matches the existing into_serverless_runtime split already documented in rivetkit-core/CLAUDE.md. The main issue is that the HTTP listener was hand-rolled in the rivetkit crate instead of reusing rivetkit_core::serverless_http::serve, which already exists, is already used by two other callers in this repo, and doesn't have the body-error and binding gaps introduced here. I'd recommend replacing serverless_listener.rs with a call into that function, and making the two env var parsers fail fast to match the TypeScript behavior and this repo's fail-by-default convention.

🤖 Generated with Claude Code

@abcxff
abcxff changed the base branch from stack/fix-rivetkit-core-scope-actor-stops-to-their-generation-ymnorrzk to main September 9, 2026 23:18
@abcxff
abcxff changed the base branch from main to stack/fix-rivetkit-core-scope-actor-stops-to-their-generation-ymnorrzk September 9, 2026 23:21
@abcxff
abcxff changed the base branch from stack/fix-rivetkit-core-scope-actor-stops-to-their-generation-ymnorrzk to main September 9, 2026 23:21
@abcxff
abcxff merged commit f5ff069 into main Sep 9, 2026
3 of 5 checks passed
@abcxff
abcxff deleted the stack/feat-rivetkit-rust-select-serverless-vs-envoy-runner-mode-via-rivetkit_runtime_mode-rqvuzoyz branch September 9, 2026 23:21
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