mirror of
https://github.com/hoppscotch/hoppscotch.git
synced 2026-04-26 01:06:00 +03:00
[PR #5874] [MERGED] fix: auto-recover from corrupted sandbox state #5381
Labels
No labels
CodeDay
a11y
browser limited
bug
bug fix
cli
core
critical
design
desktop
discussion
docker
documentation
duplicate
enterprise
feature
feature
fosshack
future
good first issue
hacktoberfest
help wanted
i18n
invalid
major
minor
need information
need testing
not applicable to hoppscotch
not reproducible
pull-request
question
refactor
resolved
sandbox
self-host
spam
stale
testmu
wip
wont fix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
starred/hoppscotch#5381
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/hoppscotch/hoppscotch/pull/5874
Author: @jamesgeorge007
Created: 2/12/2026
Status: ✅ Merged
Merged: 2/18/2026
Merged by: @jamesgeorge007
Base:
patch← Head:fix/sandbox-auto-recovery📝 Commits (1)
248f97cfix: auto-recover from corrupted sandbox state📊 Changes
13 files changed (+878 additions, -302 deletions)
View changed files
📝
packages/hoppscotch-common/src/helpers/RequestRunner.ts(+38 -2)➕
packages/hoppscotch-js-sandbox/src/__tests__/combined/script-error-recovery.spec.ts(+206 -0)📝
packages/hoppscotch-js-sandbox/src/__tests__/hopp-namespace/request.spec.ts(+5 -3)➕
packages/hoppscotch-js-sandbox/src/__tests__/utils/cage.spec.ts(+50 -0)📝
packages/hoppscotch-js-sandbox/src/cage-modules/scripting-modules.ts(+10 -8)📝
packages/hoppscotch-js-sandbox/src/node/pre-request/experimental.ts(+115 -56)📝
packages/hoppscotch-js-sandbox/src/node/test-runner/experimental.ts(+149 -103)📝
packages/hoppscotch-js-sandbox/src/types/index.ts(+3 -1)📝
packages/hoppscotch-js-sandbox/src/utils/cage.ts(+53 -10)📝
packages/hoppscotch-js-sandbox/src/web/pre-request/index.ts(+111 -50)📝
packages/hoppscotch-js-sandbox/src/web/pre-request/worker.ts(+5 -1)📝
packages/hoppscotch-js-sandbox/src/web/test-runner/index.ts(+125 -65)📝
packages/hoppscotch-js-sandbox/src/web/test-runner/worker.ts(+8 -3)📄 Description
Closes #5866, #5810, FE-1128.
Persistent errors in the experimental scripting sandbox cause two user-visible bugs:
FaradayCageretains a corrupted state across executions.script_failwhen the cached cage carries a stale bootstrap failure. This PR detects bootstrap/internal failures, resets the cage when needed, retries transparently on a fresh cage, and short-circuits empty scripts to avoid entering the sandbox entirely.What's changed
Bootstrap error detection via
captureHook: Extended thecaptureHooktype from{ capture?: () => void }to{ capture?: () => void; bootstrapError?: unknown }inscripting-modules.ts. Whenctx.vm.callFunction(bootstrapIIFE)returns an error, it is dumped and recorded oncaptureHook.bootstrapError. Bothpre-request/index.tsandtest-runner/index.tscheck this flag as the primary cage reset signal.Heuristic fallback via
isInfraError(): Defense-in-depth error classifier incage.tsthat catches errors escaping beforecallFunction()is reached — specifically fromevalCode(bootstrapCode).unwrap(),createScriptingInputsObj(), anddefineSandboxObject(). Usesinstanceof Erroras the sole discriminator: infrastructure errors (QuickJSUnwrapError from.unwrap(), marshal failures, WASM init) are realErrorinstances, while user script errors from QuickJS'sdump()are plain objects with{ name, message, stack }but are NOTinstanceof Error. This structural difference makes the check reliable with no false positives.Retry-on-bootstrap-error: When a bootstrap error is detected, the cage is reset and the script is retried once on a fresh cage within the same execution. This eliminates the regression where fixing a broken script still fails on the next immediate run before succeeding on the one after. Extracted
executePreRequestOnCage()andexecuteTestOnCage()helper functions that returnE.Either | "retry". The caller retries once; if the retry also fails with a bootstrap error, surfaces"sandbox initialisation error (persistent)".Cage reset on bootstrap errors: The reset decision is inlined at each call site as
bootstrapFailed || isInfraError(err). WhenbootstrapFailedistrue, it short-circuits without inspecting the error. Otherwise,isInfraError()checks the error structure. When a reset is needed, callers invokeresetCage()which nullscagePromise. User script errors (e.g.,ReferenceError: a is not defined) do not trigger resets, preserving the singleton optimization from #5800.cachedCage→cagePromisecaching: The singleton now caches the creation promise rather than the resolved instance, which is the correct pattern for async singleton initialization. This prevents duplicateFaradayCage.create()calls ifacquireCage()is called while a previous creation is still in-flight (e.g., after a cage reset, or on first use when two tabs fire requests before the cage is ready). Includes.catch()to null the promise on creation failure so subsequent retries work.Empty script short-circuit:
RequestRunner.tsreturns immediately (with identity envs/cookies and empty test results) whenstripModulePrefix(script).trim().length === 0, avoiding WASM initialisation and preventing stale cage errors from surfacing asscript_fail(#5810).Structured console logging:
RequestRunner.tserror paths now use prefixedconsole.error([Pre-Request Script Error],[Post-Request Script Error],[Request Error]) for dev-console visibility when debugging sandbox failures.SandboxTestResulttype: Simplified fromTestResult & { tests: TestDescriptor } & { consoleEntries?; updatedCookies }to a standalone type ({ tests: TestDescriptor; envs: TestResult["envs"]; consoleEntries?: ConsoleEntry[]; updatedCookies: Cookie[] | null }). The old intersection produced a contradictorytests: TestDescriptor[] & TestDescriptor— TypeScript allowed it structurally but every construction site always assigned a singleTestDescriptor. The new type correctly reflects actual usage. Construction sites usesatisfies SandboxTestResultfor compile-time validation.TaskEither contract compliance: Both node experimental runners (
pre-request/experimental.ts,test-runner/experimental.ts) now wrap the entire async body intry/catch, ensuring the returnedTaskEithernever rejects. The refactor replacespipe(TE.tryCatch(...))with explicitE.left/E.rightreturns and a manualtry/catch, preserving identical behavior while enabling the retry-on-bootstrap-error pattern. The inner helper functions format QuickJS error objects with the errornamewhen available (e.g.,TypeError: cannot read property...); the outer catch handles unexpected infrastructure throws. Web implementations (web/pre-request/index.ts,web/test-runner/index.ts) follow the same try-catch pattern for consistency.Worker error handling: Web worker catch blocks (
pre-request/worker.ts,test-runner/worker.ts) now handle non-Error throws safely viaerror instanceof Error ? error.message : String(error)instead of the previous unsafe(error as Error).messagecast. Web test-runner worker includesupdatedCookies: nullin theSandboxTestResultand usessatisfiesfor compile-time validation.Notes to reviewers
Try the reproduction steps mentioned in #5866 and similar patterns. Also, ensure to perform benchmarks to validate the previous performance optimisation strategies hold.
Relationship to #5800: This PR directly extends the singleton cage introduced in the memory leak fix (#5800). The key invariant preserved here: user script errors must never trigger a cage reset — only bootstrap/internal failures (corrupted QuickJS context, WASM init failure, module setup errors) cause a reset. The singleton is preserved for all normal operations.
Two-layer detection is intentional, not redundant.
captureHook.bootstrapErrorcovers errors fromctx.vm.callFunction(funcHandle)inscripting-modules.ts(L476).isInfraError()covers errors that escape beforecallFunction()— specificallyevalCode(bootstrapCode).unwrap()(L417),createScriptingInputsObj()(L423), anddefineSandboxObject()(L474). These throw duringFaradayCage.runCode()'s moduledef()phase and propagate as{ type: "error", err }without ever settingcaptureHook.bootstrapError. An audit of the full error flow confirmedisInfraError()is reachable, not dead code.instanceof Erroras the discriminator inisInfraError(). This is intentional, not overbroad. In the FaradayCage/QuickJS pipeline, user script errors are always plain objects (from QuickJS'sdump()), while infrastructure errors are alwaysErrorinstances (QuickJSUnwrapError from.unwrap(), marshal failures, etc.). The two shapes never overlap. The primary caller path only sendsresult.err(plain objects) throughisInfraError()whencaptureHook.bootstrapErroris not set — theinstanceof Errorbranch is a safety net for edge-case infrastructure throws.Test coverage:
cage.spec.ts— 5 unit tests forisInfraError()coveringinstanceof Errordiscrimination, QuickJSUnwrapError subclass detection, plain-object user error exclusions, and edge cases (null, string errors)script-error-recovery.spec.ts— 5 integration tests: 3 running real scripts through the fullFaradayCagepipeline verifying error → recovery (matching the exact reproduction steps from #5866), plus 2 singleton retry tests that inject a corrupted cage via_setCagePromiseForTesting()and verify (a) bootstrap errors trigger transparent retry on a fresh cage, and (b) user script errors do not trigger the retry pathrequest.spec.ts— 3 test expectations updated to include theTypeError:prefix in error messages, matching the new error name formatting in the script runners🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.