Repository navigation
fix(core): run abort-surviving cleanup in a catch-free finally; guard the pattern - #4337
Conversation
…guard the pattern BoxLang skips a finally whose try has a catch clause when the request ends with an abort, so cleanup in a try/catch/finally wrapped around app code can be lost (see the BoxLang finally/catch rule in CLAUDE.md Cross-Engine Invariants). Seeder.generateSeeds() (restores request.$wheelsTransactionWrapper) and LocalDisk.$assertSymlinkResolutionAvailable() (deletes the probe temp dir) both used a single try/catch/finally. Nest each so the cleanup lives in an outer catch-free try/finally that runs on every engine on abort, keeping the existing catch on the inner try. Add TryCatchFinallyGuardSpec: a structural scan that fails if any production vendor/wheels source (*.cfc / *.cfm, excluding /tests/ and /wheelstest/) has a single try with BOTH a catch and a finally. New code that needs both must nest. withAdvisoryLock's transaction-path outer try is allowlisted, because its lock release already runs in an inner catch-free finally. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com>
|
rev1-r3 review @ 8bb1a7f: code APPROVE (final sign-off after matrix run 37307996108 and PR CI); one parser-robustness note The two fixes keep behaviour the same.
The guard: I checked it against an independent parser. I ported its scan to Python line for line: the same comment/string blanking, keyword regexes and brace-depth frame stack. I compared it with a reference parser that brace-matches each
Not blocking: shapes the line-based scan gets wrong. None of these occur in today's tree (I scanned for each), but a future edit could hit them:
The cause is the same in each case. The scan attributes a The robust form is to brace-match over the whole cleaned file instead of line by line. After Tag-based |
…atcher with a self-test
The line-based frame scan had blind spots (rev1-r3): it missed Allman-style
own-line catch/finally, a same-line catch body followed by a next-line finally,
and a one-line try/catch/finally, and it false-positived a one-line inner
try/catch nested inside a safe outer try/finally. None occur in the tree today,
but a future edit could hit them.
Replace it with a whole-file brace matcher: blank comments and string literals
to spaces in a single char pass (preserving length and newlines, no global
regex — Lucee 7 hang), then for each `try {` brace-match its body and walk the
following catch/finally chain. A try is an offender iff its own chain has BOTH a
catch and a finally, so a nested outer-finally / inner-catch is correctly clean.
Add a self-test that runs the scanner over the four wrong shapes plus the
catch-free, nested-one-line, nested-multiline and try/catch-only clean shapes.
Document that the scan covers script syntax only (the cf-tag try form is not
scanned; the angle-bracket syntax is kept out of comments because Lucee's tag
scanner parses a literal cf-tag even inside one).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>
|
rev1-r3 re-review @ d45a4fc: code APPROVE (final sign-off after matrix run 37309270818 and PR CI) The delta
Cross-check, rerun. I ported the new scanner to Python one-to-one and ran it on the source trees:
These are the same three files as my reference brace-matcher found. The line numbers now point at the offending |
Wheels Test Results222 580 tests 217 459 ✅ 1h 26m 10s ⏱️ Results for commit d45a4fc. |
|
rev1-r3 review @ d45a4fc: APPROVE (final; matrix run 37309270818 read leg by leg, PR CI checked)
|
Hardens two
try/catch/finallyblocks whose cleanup can be skipped on a BoxLang abort, and adds a structural guard so the pattern can't come back.Background
On BoxLang, a request that ends with
abortskips anyfinallywhosetryhas acatchclause — even an unreachedcatch (any) { rethrow; }. A catch-freetry/finallyruns, and the transaction-action inside it makes no difference; the catch clause alone decides. Lucee and Adobe run the finally either way. (CLAUDE.md Cross-Engine Invariant 22, established by #4330 and #4335.)So any framework cleanup that must run even when app code (an action, a callback, a mailer render, …) ends the request with an abort has to sit in a finally whose try carries no catch. The safe shape when a catch is also needed is to nest: an outer catch-free
try { … } finally { <cleanup> }wrapping an innertry { … } catch { … }.Changes
Seeder.generateSeeds()andLocalDisk.$assertSymlinkResolutionAvailable()each used a singletry/catch/finally. Both are nested so the cleanup — Seeder restoringrequest.$wheelsTransactionWrapper, LocalDisk deleting the probe temp dir — runs on every engine on abort, keeping the existing catch on the inner try. Normal-path behavior is unchanged.TryCatchFinallyGuardSpec— a structural scan (modeled onBareCfabortGuardSpec) that fails if any production framework source (*.cfc/*.cfmundervendor/wheels, excluding/tests/and/wheelstest/, since test-harness code is not a production request path) has a singletrywith BOTH acatchand afinally. New code that needs both must nest. A brace-depth parser associates each catch/finally with its owning try, so a correctly nested outer-finally / inner-catch (e.g.$invokeTransactionMethodfrom #4335) is not flagged.Allowlist:
vendor/wheels/model/locking.cfm—$advisoryLockTransactionBody's outer try currently has both a catch and a finally, but its lock release already runs in an inner catch-free finally (#4330) and the abort rollback is now handled by$invokeTransactionMethod(#4335). This entry becomes unnecessary once that outer try is made catch-free in a follow-up, and will be removed then.Validation
Seeder.cfc:419andLocalDisk.cfc:305), and it does not flag the safe nested$invokeTransactionMethodshape.seederSpec66/66 andstorage.StorageSpec50/50 on Lucee 7.No changelog fragment: normal-path behavior is unchanged (the nests only matter on a BoxLang abort during a CLI seed or storage init-probe), and the guard is a test.
🤖 Generated with Claude Code