Skip to content

Fix errors when viewing version history of deleted domains - #2901

Merged
vohmar merged 4 commits into
masterfrom
2897-admin-internal-error-for-any-deletion-domain-in-domain-histroy
May 27, 2026
Merged

vohmar merged 4 commits into
masterfrom
2897-admin-internal-error-for-any-deletion-domain-in-domain-histroy

Conversation

@OlegPhenomenon

@OlegPhenomenon OlegPhenomenon commented Feb 25, 2026

Copy link
Copy Markdown
Contributor

close #2897

Resolves two issues that occurred when viewing the history of a domain that has already been deleted in /admin/domain_versions/:id

  1. ActiveRecord::RecordNotFound Domain.find was raising an error because the domain no longer exists in the database. Replaced find with find_by and added a fallback to reconstruct the domain object using PaperTrail version.reify. If it is a create event, where reify returns nil because there is no prior state, the object is reconstructed from the next available version or built directly from object_changes.

  2. NoMethodError undefined method updated_at for nil:NilClass When viewing the create event version of a deleted domain, the reconstructed Domain.new object did not have an updated_at or created_at timestamp, causing the view to crash. Added safe navigator methods and fallback to version.created_at in the view to prevent this.

Additionally, UI elements were adjusted for deleted domains:

  • Replaced dead links to the deleted domain with plain text
  • Hidden the Current state link in the version sidebar
  • Guarded the registrar link against being nil

@OlegPhenomenon
OlegPhenomenon requested review from vohmar and removed request for vohmar February 25, 2026 10:13
@vohmar
vohmar requested a review from maricavor March 12, 2026 15:17
@github-actions

github-actions Bot commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

🚀 Deploy Complete!

Property Value
App registry
Slot 1
URL https://registry1-dev.cloud.tld.ee
Namespace registry1-dev

(Environment ready for testing)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes admin errors when viewing /admin/domain_versions/:id for domains that have already been deleted, by reconstructing missing Domain data from PaperTrail and hardening the view against missing timestamps/links.

Changes:

  • Updated Admin::DomainVersionsController#show to tolerate missing Domain records and reconstruct a domain object from PaperTrail (reify / object_changes).
  • Hardened admin/domain_versions/show to avoid nil timestamp crashes and to remove/disable links that would 404 for deleted domains.
  • Added deleted-domain handling for registrar rendering and the version sidebar “Current state” link.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
app/controllers/admin/domain_versions_controller.rb Reconstructs @domain when the DB record is gone and adjusts version lookup to use item_id safely.
app/views/admin/domain_versions/show.haml Adds nil-safe timestamp handling and adjusts UI/link behavior for deleted domains.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +72 to +89
if @domain.nil?
next_version = Version::DomainVersion
.where(item_id: @version.item_id)
.where.not(object: nil)
.order(created_at: :asc, id: :asc)
.first
@domain = next_version&.reify

if @domain.nil?
@domain = Domain.new
changes = @version.object_changes || {}
changes.each do |attr, values|
value = values.is_a?(Array) ? values.last : values
@domain.send("#{attr}=", value) if @domain.respond_to?("#{attr}=")
rescue StandardError
next
end
end

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When reconstructing a deleted domain for a "create" version, the code prefers next_version&.reify and only falls back to @version.object_changes if reify returns nil. In fixtures (e.g., a later destroy version) object may not contain all fields like name, so reify can return a non-nil but incomplete Domain, and the view will still show missing data (e.g., name as N/A). Prefer reconstructing from the create version’s object_changes (or merging object_changes into the reified object) so required attributes like name, registrar_id, registrant_id are reliably present.

Copilot uses AI. Check for mistakes.
Comment on lines +84 to +87
value = values.is_a?(Array) ? values.last : values
@domain.send("#{attr}=", value) if @domain.respond_to?("#{attr}=")
rescue StandardError
next

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The per-attribute assignment loop rescues StandardError and silently skips failures. This can mask real issues (e.g., unexpected serialized types) and make reconstruction nondeterministic. Consider narrowing the rescued exceptions and/or recording enough context (e.g., attribute name) for troubleshooting instead of silently next-ing.

Suggested change
value = values.is_a?(Array) ? values.last : values
@domain.send("#{attr}=", value) if @domain.respond_to?("#{attr}=")
rescue StandardError
next
next unless @domain.respond_to?("#{attr}=")
value = values.is_a?(Array) ? values.last : values
@domain.public_send("#{attr}=", value)
rescue ArgumentError, TypeError => e
Rails.logger.warn(
"Failed to reconstruct Domain attribute #{attr.inspect} " \
"for version #{@version.id}: #{e.class}: #{e.message}"
)

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +91
@version = Version::DomainVersion.find(params[:id])
@domain = Domain.find(@version.item_id)
@domain = Domain.find_by(id: @version.item_id)

if @domain.nil?
@domain = @version.reify
@domain_deleted = true

# For 'create' events, reify returns nil because there's no prior state.
# Try to reconstruct from a later version or from object_changes.
if @domain.nil?
next_version = Version::DomainVersion
.where(item_id: @version.item_id)
.where.not(object: nil)
.order(created_at: :asc, id: :asc)
.first
@domain = next_version&.reify

if @domain.nil?
@domain = Domain.new
changes = @version.object_changes || {}
changes.each do |attr, values|
value = values.is_a?(Array) ? values.last : values
@domain.send("#{attr}=", value) if @domain.respond_to?("#{attr}=")
rescue StandardError
next
end
end
end
end

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are integration tests for the domain versions controller, but none cover the deleted-domain path introduced here (Domain.find_by returning nil and reconstructing from PaperTrail data). Please add a test using the existing log_domains fixtures (e.g., a create+destroy pair for a non-existent item_id) to assert the show page renders and includes key attributes like the domain name and registrar info.

Copilot uses AI. Check for mistakes.
Comment on lines +125 to +138
- if !@domain_deleted && @domain.registrar
%dt= t(:registrar_name)
%dd{class: changing_css_class(@version,"registrar_id")}
= link_to admin_registrar_path(@domain.registrar), target: "registrar_#{@domain.registrar.id}" do
= @domain.registrar.name
- elsif @domain_deleted && @version.try(:object).try(:[], 'registrar_id')
- registrar = Registrar.find_by(id: @version.object['registrar_id'])
%dt= t(:registrar_name)
%dd{class: changing_css_class(@version,"registrar_id")}
- if registrar
= link_to admin_registrar_path(registrar), target: "registrar_#{registrar.id}" do
= registrar.name
- else
= "Registrar ID: #{@version.object['registrar_id']}"

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This registrar fallback for deleted domains only checks @version.object['registrar_id'], but for create events object is often null (see test/fixtures/log_domains.yml), so the registrar will not be shown even if the ID exists in object_changes / the reconstructed @domain.registrar_id. Consider sourcing the registrar id from @domain.registrar_id and/or @version.object_changes as a fallback, and avoid doing the lookup in the view.

Copilot uses AI. Check for mistakes.
Comment on lines +131 to +138
- registrar = Registrar.find_by(id: @version.object['registrar_id'])
%dt= t(:registrar_name)
%dd{class: changing_css_class(@version,"registrar_id")}
- if registrar
= link_to admin_registrar_path(registrar), target: "registrar_#{registrar.id}" do
= registrar.name
- else
= "Registrar ID: #{@version.object['registrar_id']}"

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registrar.find_by(...) introduces a database query in the view, which makes rendering harder to reason about and can hurt performance. Prefer resolving the registrar (or registrar name/ID display string) in the controller and passing it to the view, so the template stays presentation-only.

Suggested change
- registrar = Registrar.find_by(id: @version.object['registrar_id'])
%dt= t(:registrar_name)
%dd{class: changing_css_class(@version,"registrar_id")}
- if registrar
= link_to admin_registrar_path(registrar), target: "registrar_#{registrar.id}" do
= registrar.name
- else
= "Registrar ID: #{@version.object['registrar_id']}"
%dt= t(:registrar_name)
%dd{class: changing_css_class(@version,"registrar_id")}
= "Registrar ID: #{@version.object['registrar_id']}"

Copilot uses AI. Check for mistakes.
%dt= t(:name)
- if !@domain.name
- domain_name = Domain.find(@version.item_id).try(:name)
- domain_name = @version.try(:object).try(:[], 'name') || 'N/A'

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For deleted domains, domain_name falls back to @version.object['name'], but for create events object can be null (fixtures have only object_changes), resulting in N/A even though the name is available. Consider falling back to @version.object_changes.dig('name', 1) (or equivalent) when object is missing.

Suggested change
- domain_name = @version.try(:object).try(:[], 'name') || 'N/A'
- domain_name = @version.try(:object).try(:[], 'name') || @version.try(:object_changes).try(:dig, 'name', 1) || 'N/A'

Copilot uses AI. Check for mistakes.
@OlegPhenomenon
OlegPhenomenon force-pushed the 2897-admin-internal-error-for-any-deletion-domain-in-domain-histroy branch from fd2eff8 to 25f4633 Compare April 10, 2026 08:19
Resolves two issues that occurred when viewing the history of a domain that has already been deleted in /admin/domain_versions/:id

1. ActiveRecord::RecordNotFound
Domain.find was raising an error because the domain no longer exists in the database. Replaced find with find_by and added a fallback to reconstruct the domain object using PaperTrail version.reify. If it is a create event, where reify returns nil because there is no prior state, the object is reconstructed from the next available version or built directly from object_changes.

2. NoMethodError undefined method updated_at for nil:NilClass
When viewing the create event version of a deleted domain, the reconstructed Domain.new object did not have an updated_at or created_at timestamp, causing the view to crash. Added safe navigator methods and fallback to version.created_at in the view to prevent this.

Additionally, UI elements were adjusted for deleted domains:
- Replaced dead links to the deleted domain with plain text
- Hidden the Current state link in the version sidebar
- Guarded the registrar link against being nil
@OlegPhenomenon
OlegPhenomenon force-pushed the 2897-admin-internal-error-for-any-deletion-domain-in-domain-histroy branch from 72a10a1 to 0892b97 Compare May 26, 2026 12:42
@vohmar
vohmar merged commit 90de774 into master May 27, 2026
4 of 5 checks passed
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.

Admin: internal error for any deleted domain in domain history

4 participants