Skip to content

fix(serverless-hono): withWaitUntil must not destroy global state when context has no waitUntil#1214

Merged
omeraplak merged 2 commits intoVoltAgent:mainfrom
octo-patch:fix/issue-1203-with-wait-until-global-state
Apr 22, 2026
Merged

fix(serverless-hono): withWaitUntil must not destroy global state when context has no waitUntil#1214
omeraplak merged 2 commits intoVoltAgent:mainfrom
octo-patch:fix/issue-1203-with-wait-until-global-state

Conversation

@octo-patch
Copy link
Copy Markdown
Contributor

@octo-patch octo-patch commented Apr 21, 2026

Fixes #1203

Problem

withWaitUntil in @voltagent/serverless-hono had two bugs:

  1. Global state destruction: The else branch unconditionally set globals.___voltagent_wait_until = undefined whenever context.waitUntil was absent. This silently destroyed any value previously set by an outer scope, breaking background tasks (observability, logging) in middleware chains where an inner handler passed {} as context.

  2. Error propagation: Errors thrown by context.waitUntil (e.g. DataCloneError in Cloudflare Workers) propagated to callers. The test suite expected these to be swallowed, but the implementation let them through.

Additionally, the cleanup function only restored the previous waitUntil when currentWaitUntil was truthy, meaning cleanup was a no-op when called with a context that had no waitUntil — leaving the global state permanently altered.

Solution

  • Remove the else branch entirely: when context has no waitUntil, the global is left untouched.
  • Wrap the bound waitUntil call in try/catch: errors from the underlying implementation (like DataCloneError) are now swallowed so callers are not affected.
  • Simplify the cleanup function: always restores previousWaitUntil unconditionally, regardless of whether currentWaitUntil was set.

Testing

All 11 tests in wait-until-wrapper.spec.ts now pass, including the two that previously failed:

  • should handle errors from context.waitUntil gracefully
  • should not affect global state when context has no waitUntil
✓ should set global waitUntil when context has waitUntil
✓ should not set global waitUntil when context is undefined
✓ should not set global waitUntil when context is null
✓ should not set global waitUntil when context has no waitUntil
✓ should call context.waitUntil when global waitUntil is invoked
✓ should handle errors from context.waitUntil gracefully
✓ should restore previous waitUntil after cleanup
✓ should clear global waitUntil after cleanup when no previous value existed
✓ should handle nested calls with proper state restoration
✓ should not affect global state when context has no waitUntil
✓ should handle context with non-function waitUntil

Test Files  1 passed (1)
      Tests  11 passed (11)

Summary by cubic

Fixes global waitUntil handling in @voltagent/serverless-hono’s withWaitUntil. It no longer clears outer global state when a context lacks waitUntil, swallows underlying errors, and always restores the previous global value.

  • Bug Fixes
    • Removed the else branch so the global isn’t reset when context.waitUntil is missing.
    • Wrapped the bound waitUntil in try/catch to swallow errors (e.g., Cloudflare Workers’ DataCloneError).
    • Cleanup now always restores the previous global waitUntil.

Written for commit b91b5d5. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes
    • Prevented inadvertent modification of global state when execution context lacks async hooks.
    • Improved resilience by catching and suppressing non-critical errors from async callbacks to avoid cascading failures.
    • Simplified cleanup to more reliably restore prior runtime state after operations complete.

…n context has no waitUntil

Two bugs in withWaitUntil:

1. The else branch unconditionally set ___voltagent_wait_until = undefined
   even when the global already held a value from an outer scope. This
   silently killed background tasks (observability, logging) in middleware
   chains where an inner handler passed {} as context.

2. Errors thrown by context.waitUntil (e.g. DataCloneError in Cloudflare
   Workers) propagated to callers. The test suite expected them to be
   swallowed; wrapping the call in try/catch aligns implementation with
   that contract.

Fix:
- Remove the else branch entirely. When context has no waitUntil the
  global is left untouched.
- Wrap the bound waitUntil call in try/catch to swallow errors.
- Simplify the cleanup function to always restore previousWaitUntil
  unconditionally (no currentWaitUntil guard needed).

All 11 tests in wait-until-wrapper.spec.ts now pass.

Fixes VoltAgent#1203
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 21, 2026

🦋 Changeset detected

Latest commit: b91b5d5

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@voltagent/serverless-hono Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 21, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 30e2be91-8f4f-4512-bef3-a5bd351f3347

📥 Commits

Reviewing files that changed from the base of the PR and between 3491ff5 and b91b5d5.

📒 Files selected for processing (1)
  • .changeset/rude-pianos-marry.md

📝 Walkthrough

Walkthrough

Fixes withWaitUntil so it only sets the global ___voltagent_wait_until when context.waitUntil is a function, wraps invocations in try/catch to swallow errors, and always restores the previously captured global value on cleanup. Also adds a changeset entry.

Changes

Cohort / File(s) Summary
Wait-Until Wrapper
packages/serverless-hono/src/utils/wait-until-wrapper.ts
Only assign globalThis.___voltagent_wait_until when context.waitUntil is a function; bind it to the context; wrap calls in try/catch to swallow invocation errors; simplify cleanup to always restore the previous global value.
Changeset
.changeset/rude-pianos-marry.md
Add patch-level changeset documenting the fix to withWaitUntil behavior for @voltagent/serverless-hono.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 A tiny wrapper hopped along the way,
It guarded globals, let no value stray.
Errors tucked softly, not sent to roam,
Then put things back where they belong — home. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main fix: preventing withWaitUntil from destroying global state when context lacks waitUntil, which is the primary bug addressed in this PR.
Description check ✅ Passed The description comprehensively covers the problem statement, solution approach, and testing results. It directly links to issue #1203, explains both bugs with examples, and provides concrete evidence of test passage.
Linked Issues check ✅ Passed All three coding objectives from issue #1203 are fully met: the else branch is removed to preserve global state [#1203], errors are wrapped in try/catch to prevent propagation [#1203], and cleanup is simplified to always restore previous value [#1203].
Out of Scope Changes check ✅ Passed All changes in wait-until-wrapper.ts directly address the two bugs and objectives specified in issue #1203. No unrelated modifications were introduced.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/serverless-hono/src/utils/wait-until-wrapper.ts (1)

35-46: LGTM — correctly guards global state and swallows invocation errors.

Binding before wrapping avoids Illegal invocation, and the try/catch inside the wrapper (rather than around .bind) correctly catches per-call failures like DataCloneError. Removing the else branch preserves any outer-scope global, matching the fix for #1203.

One optional consideration: fully silent swallowing can hide real bugs during development. The sibling flushOnFinish in volt-agent-observability.ts logs a console.warn when waitUntil throws; you may want a similar debug-level log here for symmetry, though it's not required for correctness.

Optional: log swallowed errors for observability parity
     globals.___voltagent_wait_until = (promise: Promise<unknown>) => {
       try {
         boundWaitUntil(promise);
-      } catch {
-        // Swallow errors to avoid breaking the caller
+      } catch (error) {
+        // Swallow errors (e.g. DataCloneError) to avoid breaking the caller
+        // eslint-disable-next-line no-console
+        console.warn("[voltagent] waitUntil invocation failed", error);
       }
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/serverless-hono/src/utils/wait-until-wrapper.ts` around lines 35 -
46, The wrapper for globals.___voltagent_wait_until currently swallows errors
silently; update the catch block in wait-until-wrapper.ts (inside the if
checking currentWaitUntil and after binding via currentWaitUntil.bind(context))
to emit a debug/console.warn log including the caught error and a short context
message (e.g., that waitUntil invocation failed) before swallowing, referencing
globals.___voltagent_wait_until, boundWaitUntil and context so logs mirror the
observability behavior used by flushOnFinish in volt-agent-observability.ts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/serverless-hono/src/utils/wait-until-wrapper.ts`:
- Around line 35-46: The wrapper for globals.___voltagent_wait_until currently
swallows errors silently; update the catch block in wait-until-wrapper.ts
(inside the if checking currentWaitUntil and after binding via
currentWaitUntil.bind(context)) to emit a debug/console.warn log including the
caught error and a short context message (e.g., that waitUntil invocation
failed) before swallowing, referencing globals.___voltagent_wait_until,
boundWaitUntil and context so logs mirror the observability behavior used by
flushOnFinish in volt-agent-observability.ts.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 150ec58a-50a5-4ffe-a24a-3a0f9901c2e0

📥 Commits

Reviewing files that changed from the base of the PR and between 71c9f84 and 3491ff5.

📒 Files selected for processing (1)
  • packages/serverless-hono/src/utils/wait-until-wrapper.ts

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 1 file

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/serverless-hono/src/utils/wait-until-wrapper.ts">

<violation number="1" location="packages/serverless-hono/src/utils/wait-until-wrapper.ts:52">
P2: Cleanup unconditionally restores a stale global snapshot even when this call never set the global, allowing interleaved executions to be clobbered.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

globals.___voltagent_wait_until = undefined;
}
}
globals.___voltagent_wait_until = previousWaitUntil;
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 21, 2026

Choose a reason for hiding this comment

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

P2: Cleanup unconditionally restores a stale global snapshot even when this call never set the global, allowing interleaved executions to be clobbered.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/serverless-hono/src/utils/wait-until-wrapper.ts, line 52:

<comment>Cleanup unconditionally restores a stale global snapshot even when this call never set the global, allowing interleaved executions to be clobbered.</comment>

<file context>
@@ -34,20 +34,21 @@ export function withWaitUntil(context?: WaitUntilContext | null): () => void {
-        globals.___voltagent_wait_until = undefined;
-      }
-    }
+    globals.___voltagent_wait_until = previousWaitUntil;
   };
 }
</file context>
Fix with Cubic

Copy link
Copy Markdown
Member

@omeraplak omeraplak left a comment

Choose a reason for hiding this comment

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

thank you so much 🔥

@omeraplak omeraplak merged commit fbbdc9e into VoltAgent:main Apr 22, 2026
21 of 22 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.

[BUG] withWaitUntil destroys existing global state when called with context lacking waitUntil

3 participants