Skip to content

fix(rivetkit): add serverless request validation - #5608

Open
NathanFlurry wants to merge 1 commit into
mainfrom
start-bug
Open

fix(rivetkit): add serverless request validation#5608
NathanFlurry wants to merge 1 commit into
mainfrom
start-bug

Conversation

@NathanFlurry

Copy link
Copy Markdown
Member

No description provided.

@railway-app

railway-app Bot commented Aug 27, 2026

Copy link
Copy Markdown

🚅 Deployed to the actors-pr-5608 environment in rivet-frontend

Service Status Web Updated (UTC)
kitchen-sink 😴 Sleeping (View Logs) Web Aug 27, 2026 at 7:19 am
frontend-inspector 😴 Sleeping (View Logs) Web Aug 27, 2026 at 7:16 am
frontend-cloud 😴 Sleeping (View Logs) Web Aug 27, 2026 at 7:15 am
website ❌ Build Failed (View Logs) Web Aug 27, 2026 at 7:07 am
mcp-hub ✅ Success (View Logs) Web Aug 27, 2026 at 2:14 am
ladle ✅ Success (View Logs) Web Aug 27, 2026 at 2:14 am

@claude

claude Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Code Review

This PR hardcodes serverless_cache_envoy: false (previously true) at three call sites — envoy_callbacks.rs::ServeConfig::from_env and both serve_config_from_js(...) calls in rivetkit-napi/src/registry.rs — with a TODO citing a need to verify the serverless token per-connection before caching can be safely re-enabled.

I traced the change through serverless.rs and the NAPI bridge. There are two real regressions worth addressing before this lands, plus one no-op hunk.

1. health() becomes permanently unhealthy for serverless registries

ensure_envoy() in rivetkit-rust/packages/rivetkit-core/src/serverless.rs:499-505 only writes into self.envoy when cache_envoy is true:

```rust
async fn ensure_envoy(&self, headers: &StartHeaders) -> Result {
if self.shutting_down.load(Ordering::Acquire) {
return Err(RuntimeShutDown.build());
}
if !self.settings.cache_envoy {
return self.start_envoy(headers).await; // never written into self.envoy
}
...
```

With cache_envoy now false everywhere, self.envoy (TokioMutex<Option<EnvoyHandle>>) is never populated, so active_envoy_status() (serverless.rs:241-247) always returns None. The NAPI-exposed #[napi] pub async fn health(&self) (rivetkit-typescript/packages/rivetkit-napi/src/registry.rs:390-401) handles that as:

```rust
let response = match serverless_runtime.active_envoy_status().await {
Some(envoy) => health_response(if envoy.ping_healthy { 200 } else { 503 }, ...),
None => health_response(503, "engine_ping_stale", &version),
};
```

None unconditionally maps to a 503. This is the method the TypeScript side actually calls (rivetkit-typescript/packages/rivetkit/src/registry/napi-runtime.ts:276: asNativeRegistry(registry).health()), so any serverless-mode host's /health route now returns 503 permanently, even while actors are starting and serving traffic fine. A platform doing liveness/readiness probing on that endpoint (Cloud Run, Fly.io, k8s) would restart-loop a healthy container.

(There's a second, separate /health handler inside serverless.rs::handle_request_inner for raw-HTTP hosts that treats a None envoy as healthy via unwrap_or(true) — that one degrades more gracefully by just losing the ping-staleness signal rather than going permanently 503. Worth noting the two /health surfaces already diverge in "no envoy yet" semantics, independent of this PR.)

2. shutdown() becomes a no-op — loses graceful drain

```rust
pub async fn shutdown(&self) {
self.shutting_down.store(true, Ordering::Release);
let handle = { self.envoy.lock().await.take() };
let Some(handle) = handle else { return };
...
}
```

(serverless.rs:219-233) Since self.envoy is never populated when cache_envoy is false, shutdown() always takes None and returns immediately. Per-request envoy handles are instead built fresh in start_response (serverless.rs:371-419) and live only inside a fire-and-forget RuntimeSpawner::spawn(...) whose JoinHandle is discarded — nothing tracks them in a way shutdown() could wait on. On a deploy/rolling-restart that calls CoreRegistry::shutdown(), in-flight /start SSE streams (and the actor-start requests behind them) no longer get a bounded graceful drain via SHUTDOWN_DRAIN_TIMEOUT; they're just dropped when the process exits. That contradicts the doc comment on shutdown() and the "Run modes" guarantee in rivetkit-core/CLAUDE.md.

3. No-op edit in serve() (rivetkit-napi/src/registry.rs:200-205)

The same TODO/false edit was applied inside CoreRegistry::serve() (persistent, non-serverless mode):

```rust
let serve_config = serve_config_from_js(config, false, false);
```

serve() calls registry.serve_with_config_and_handle_observer(...), which never reads ServeConfig.serverless_cache_envoy — only CoreServerlessRuntime::new (reached via into_serverless_runtime) consumes that field. This hunk has no runtime effect, but the added TODO comment implies envoy caching behavior changed for persistent mode too, which could mislead a future reader. Consider dropping this hunk.

Suggestion

Rather than disabling caching wholesale, the actual gap described in the TODO could be closed directly: ensure_envoy's cache-hit branch (serverless.rs:507-517) checks endpoint/namespace/pool_name on reuse but explicitly skips validating headers.token against the cached handle (per the existing comment there: "the start request token ... may differ from the token used for the engine connection"). Adding a token check on cache-hit would close the real gap while keeping the health-check and bounded-shutdown-drain guarantees intact, instead of trading them away for a stopgap.

Test coverage

Existing serverless.rs tests still construct fixtures with cache_envoy: true and don't appear to cover the cache_envoy: false interaction with shutdown() or health(). If this flip lands even as a temporary stopgap, it'd be worth adding a regression test asserting shutdown() still bounds on SHUTDOWN_DRAIN_TIMEOUT while a /start request is in flight with cache_envoy: false, and one confirming the intended/accepted health() behavior in that mode (since as written it's a silent, permanent regression rather than a documented tradeoff).


Generated with the code-review skill.

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