Skip to content

v5.7.1 - Cache-Invalidation Hooks No Longer Block Application Hooks

Choose a tag to compare

@AdrianCurtin AdrianCurtin released this 02 Aug 23:20
· 8 commits to main since this release
349502c

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::Invalidation raised NoMethodError: undefined method 'guard' on every _User, _Role, and _Session trigger it registered, and the failure was not contained. A webhook handler block is bound to the payload before it runs, so the bare guard / bump_subject / subject_id calls in the handler bodies resolved against Parse::Webhooks::Payload, which defines none of them. guard's own rescue never 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 with Array#map, which abandons the collection on the first raise, and these triggers install during Parse.setup and therefore sit ahead of every application handler for the same trigger. One unresolvable method name silently prevented an application's own after_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 whatever self the dispatcher binds. Applications that set cache_invalidation_hooks: false to work around this can remove it.
  • FIXED: The invalidation tests dispatched handlers with a plain Proc#call, which leaves self bound 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_error decides whether an exception raised by one after_* handler prevents the remaining handlers for that trigger from running. It defaults to true, which is the existing behavior, so nothing changes on upgrade. Set it to false to isolate handlers from each other: each one runs regardless of what an earlier one raised, and the failure is reported with a warning and a parse.webhooks.handler_error notification instead of being swallowed silently. This was previously not a decision at all but a side effect of folding handlers with Array#map. Registration order is not fully under an application's control, since the SDK's own triggers install during Parse.setup and therefore sit ahead of handlers registered by application files loaded later.

Failed transactions restore the object they rolled back

  • FIXED: A failed Parse::Object.transaction left the in-memory object holding its modified values. The rollback snapshotted Parse::Object#attributes and 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 at batch.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 variable ActiveModel::Dirty keys its behavior on: once defined it builds an AttributeMutationTracker over that hash instead of the ForcedMutationTracker a Parse::Object needs, and clear_changes! then called forgetting_assignment on the [key, value] pairs Hash#map yields. 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 reads PARSE_TEST_MONGO_URI like every other mongo-direct integration file and still honors ANALYTICS_DATABASE_URI as an override.

Behavior Notes

  • Neither mode of abort_after_callbacks_on_error reverts anything. An after_* 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)
end

Commit: 349502c
Author: Adrian Curtin
Date: August 2, 2026