fix(ui): prevent 400 on second domain tag update (#32358) - #32444
Conversation
* fix(ui): prevent 400 on second domain tag update
Two bugs caused PATCH /api/v1/domains/{id} to fail with 400 when a user
updated tags twice in succession:
1. TagsContainerV2.handleSave converted style: null to style: {} via the
nullish-coalescing fallback, producing a spurious replace operation in
the JSON-Patch diff that could interact with tag validation to reject
the request.
2. DomainDetailPage.handleDomainUpdate replaced the entire React-Query
cache with the PATCH response, which only carries patchFields
(parent, children, experts, tags, owners, followers) and omits
extension, votes, and certification. Subsequent updates built their
diff from this incomplete baseline.
Fix: preserve the server's style value as-is and merge the PATCH
response into the existing cache instead of replacing it.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(ui): add tests for domain tag update fixes
- TagsContainerV2: verify style: null is preserved as null (not
converted to {}) when passing tag data through handleSave.
- DomainDetailPage: verify handleDomainUpdate merges the PATCH response
with the existing cached domain, so fields not returned by the PATCH
endpoint (e.g. extension) are preserved.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(ui): invalidate domain query after PATCH instead of blind merge
The previous cache-merge approach ({ ...prev, ...response }) preserved
stale fields like certification when the PATCH response omitted them.
After clearing a certification the old value survived in the UI because
the PATCH endpoint does not include certification in patchFields.
Replace the merge with setActiveDomain(response) for an immediate
optimistic update, followed by queryClient.invalidateQueries to re-fetch
the full entity with all fields. This fixes the Playwright
DomainTierCertificationVoting test which asserts the add-certification
button appears after removing a certification.
Update the DomainDetailPage test to verify invalidateQueries is called
after a successful PATCH.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
(cherry picked from commit c0e4229)
❌ PR checklist incompleteThis PR cannot be merged until the following are addressed on its linked issue:
The fields live on the linked issue in the Shipping project (open the issue → right sidebar → Projects). After you set them, re-run this check (or push a commit) — issue/project changes do not re-trigger it automatically. Maintainers can bypass this check by adding the |
Code Review ✅ ApprovedBackport fixes a 400 error on successive domain tag updates by preserving the server's OptionsDisplay: compact → Showing less information. Comment with these commands to change the behavior for this request:
Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source |
|



Summary
Backport of #32358 to
2.0.Fixes the Sentry issue where
PATCH /api/v1/domains/{id}returns 400 when a user updates tags on a domain twice in succession (first update succeeds, second fails).Sentry: https://collate-3b.sentry.io/issues/7703855605/
Root cause — two contributing bugs:
TagsContainerV2.handleSaveconvertedstyle: null→style: {}viaoption.style ?? {}. This created a spuriousreplace /tags/0/styleoperation in thefast-json-patchdiff that could interact with tag validation (mutually-exclusive checks afteraddDerivedTags) to reject the request.DomainDetailPage.handleDomainUpdatereplaced the entire React Query cache with the PATCH response viasetActiveDomain(response). The PATCH endpoint only returnspatchFields(parent, children, experts, tags, owners, followers) — not extension, votes, or certification. Subsequent updates built theircompare()diff from this incomplete baseline.Fix:
stylevalue as-is (style: option.styleinstead ofstyle: option.style ?? {})setActiveDomain(response)but follow it withqueryClient.invalidateQueries({ queryKey: domainCacheKey }), so the cache is re-hydrated with the full entity (extension, votes, certification) rather than a partial oneBackport notes
Cherry-picked cleanly from
c0e42293(squash merge of #32358) with-x; no conflicts and no adaptation needed. Diff is byte-identical to the original: 4 files, +138/-4.queryClientanddomainCacheKeyalready exist on2.0inDomainDetailPage.component.tsx, so the added line resolves without further changes.Test plan
invalidateQuerieschange and the PlaywrightDomainTierCertificationVotingtest)🤖 Generated with Claude Code
skip-pr-checksapplied per the 2.0 backport convention (#32416, #32395): a backport has no issue of its own to link — the original issue is tracked on #32358.