Skip to content

actor_class followed the constant it cached, not the one the app has - #8

Merged
rameerez merged 1 commit into
mainfrom
fix/actor-class-survives-reloading
Aug 31, 2026
Merged

actor_class followed the constant it cached, not the one the app has#8
rameerez merged 1 commit into
mainfrom
fix/actor-class-survives-reloading

Conversation

@rameerez

Copy link
Copy Markdown
Owner

The report

From a Rails app in development: every signup after the first code change of the session failed with

Clickwrap::ConfigurationError
Clickwrap was asked to record User as the actor, but `config.actor_class_name`
says the records that can act are User.

Both halves of that sentence name the same class. It reads as a contradiction, and it sends whoever hits it to an initializer that is perfectly correct.

The cause

They were two different Ruby objects that print the same name.

Configuration#actor_class cached the constantized Class and re-resolved only when the class name changed:

@actor_class = name.constantize if @actor_class.nil? ||
                                   @actor_class_name_at_resolution != name

The name never changes — it is set once in an initializer. But reloading replaces the class behind the name on every edit: Zeitwerk unloads the old User and defines a new one. From the second request of a dev session onwards, the cache held a class no living record was an instance of, so actor.is_a?(config.actor_class) was false for an ordinary User and capture refused to record it.

The tell, from the host app's side: a restart fixed it, and the next edit broke it again.

The fix

Drop the cache.

constantize after the first load is a const_get, and actor_class is called once per capture rather than in a loop, so the memo was never buying anything. parent_controller_class, two lines below, has always resolved this way.

The test

Reloads a constant the way Rails does — remove_const then const_set — and asserts the resolved class follows it. Verified to fail against the old implementation and pass against the new one.

Verification

  • Gem suite: 837 runs, 5,415 assertions, 0 failures
  • Reproduced in the host app, fixed there with no workaround: two signups either side of a touch app/models/user.rb, both 303. Before the fix the second was a 500.

Reported from a Rails app in development: every signup after the first code
change of the session failed with

  Clickwrap was asked to record User as the actor, but
  `config.actor_class_name` says the records that can act are User.

Both halves name the same class, which reads as a contradiction and sends
whoever hits it to an initializer that is correct.

They were two different Ruby objects printing the same name. `actor_class`
cached the constantized Class and re-resolved only when the class NAME
changed — which never happens, because the name is set once in an
initializer. Reloading replaces the class behind the name on every edit:
Zeitwerk unloads the old User and defines a new one. From the second request
of a dev session onwards the cache held a class no living record was an
instance of, so `actor.is_a?(config.actor_class)` was false for an ordinary
User and capture refused to record it.

The cache is gone. `constantize` after the first load is a const_get, and
this is called once per capture rather than in a loop, so it was never
buying anything; `parent_controller_class` two lines below has always
resolved this way.

The test reloads a constant the way Rails does — remove_const, const_set —
and fails against the old implementation.

Found the hard way in a host app, where the tell was that a restart fixed it
and the next edit broke it again.
@rameerez
rameerez merged commit 611cc46 into main Aug 31, 2026
22 of 23 checks passed
@rameerez
rameerez deleted the fix/actor-class-survives-reloading branch August 31, 2026 15:16
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.

1 participant