v5.7.1 - Cache-Invalidation Hooks No Longer Block Application Hooks
Cache-invalidation webhooks no longer break application hooks
A focused fix release for webhook dispatch and transaction rollback. The SDK's own cache-invalidation triggers raised on every _User, _Role, and _Session webhook and took every application handler for the same trigger down with them. Whether one failing after_* handler stops the rest is now an explicit setting rather than an accident of how handlers are folded, and a failed Parse::Object.transaction now actually restores the object it rolled back.
Changes
Cache-invalidation webhooks no longer break every application hook for the same trigger
- FIXED:
Parse::Cache::InvalidationraisedNoMethodError: undefined method 'guard'on every_User,_Role, and_Sessiontrigger it registered, and the failure was not contained. A webhook handler block is bound to the payload before it runs, so the bareguard/bump_subject/subject_idcalls in the handler bodies resolved againstParse::Webhooks::Payload, which defines none of them.guard's ownrescuenever ran, because it lives inside the body that was never entered, so the error escaped into the route dispatcher. The dispatcher folds a trigger's handlers withArray#map, which abandons the collection on the first raise, and these triggers install duringParse.setupand therefore sit ahead of every application handler for the same trigger. One unresolvable method name silently prevented an application's ownafter_save "_User"hook from running at all, so any work downstream of it stopped with nothing logged to say it had been skipped. The handlers now call the module on an explicit receiver captured in a local, which is independent of whateverselfthe dispatcher binds. Applications that setcache_invalidation_hooks: falseto work around this can remove it. - FIXED: The invalidation tests dispatched handlers with a plain
Proc#call, which leavesselfbound to the module the block closed over, so every one of them passed against handlers that could not run in production. They now dispatch through the real handler invocation and fail against the broken code.
Whether one failing after_* handler stops the rest is now a setting
- NEW:
Parse::Webhooks.abort_after_callbacks_on_errordecides whether an exception raised by oneafter_*handler prevents the remaining handlers for that trigger from running. It defaults totrue, which is the existing behavior, so nothing changes on upgrade. Set it tofalseto isolate handlers from each other: each one runs regardless of what an earlier one raised, and the failure is reported with a warning and aparse.webhooks.handler_errornotification instead of being swallowed silently. This was previously not a decision at all but a side effect of folding handlers withArray#map. Registration order is not fully under an application's control, since the SDK's own triggers install duringParse.setupand therefore sit ahead of handlers registered by application files loaded later.
Failed transactions restore the object they rolled back
- FIXED: A failed
Parse::Object.transactionleft the in-memory object holding its modified values. The rollback snapshottedParse::Object#attributesand restored it, but that method returns a schema map of field name to type symbol rather than values, so nothing was ever restored. Property values live in@<field>instance variables; those are now what the rollback captures and restores, including values nested inside arrays and hashes, relation operation queues, and properties whose instance variable did not exist before the transaction. State is captured when the object first enters the transaction rather than atbatch.add, so the documented pattern of mutating an object and adding it afterwards rolls back correctly. - FIXED: A failed transaction also left the object with broken change tracking. Restoring the schema map defined
@attributes, which is the instance variableActiveModel::Dirtykeys its behavior on: once defined it builds anAttributeMutationTrackerover that hash instead of theForcedMutationTrackeraParse::Objectneeds, andclear_changes!then calledforgetting_assignmenton the[key, value]pairsHash#mapyields. Both call sites rescue and warn, so every rollback quietly downgraded the object rather than failing. The rollback no longer defines@attributes.
A silently skipped integration test runs again
- FIXED: The mongo-direct role-graph integration test gated on
ANALYTICS_DATABASE_URI, a production variable name the test stack never sets, so both of its traversal assertions skipped on every run while the per-file reporter still reported the file as passing. It now readsPARSE_TEST_MONGO_URIlike every other mongo-direct integration file and still honorsANALYTICS_DATABASE_URIas an override.
Behavior Notes
- Neither mode of
abort_after_callbacks_on_errorreverts anything. Anafter_*trigger fires once the write has already committed, so there is no version of this setting that can undo a save. The only question it answers is whether the remaining handlers still run. before_*dispatch is untouched. A raise there is how a handler denies an operation and must continue to abort, and a rejectable trigger must deny if any handler denies. Only the accumulating, non-rejectable triggers (after_save,after_delete,after_logout) can hold more than one handler in the first place.- A cache backend that is unreachable or misconfigured does not reach application handlers. The invalidation triggers install only when a keyspace is configured and the resulting view exposes role and identity planes, and every invalidation runs inside a rescue that degrades a backend failure to a warning. A plain Moneta store registers no triggers at all.
Code Example
# Keep one failing handler from starving the others registered after it.
Parse::Webhooks.abort_after_callbacks_on_error = false
ActiveSupport::Notifications.subscribe("parse.webhooks.handler_error") do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
Rails.logger.warn "webhook #{event.payload[:trigger]} handler raised #{event.payload[:error]}"
end
# Runs even if a handler registered before it raised.
Parse::Webhooks.route(:after_save, "_User") do |payload|
SyncUserProfileJob.perform_later(payload.parse_object.id)
endCommit: 349502c
Author: Adrian Curtin
Date: August 2, 2026