payloadcms / payloadcms/payload

Sparse blocks array persisted with gapped _order (db-postgres) — undefined block row bricks the entire edit view

Open
#17,508 3 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

area: ui Bug db: postgres status: needs-triage v3
Dominant language
TypeScript
Stars
44.8k
Forks
4.2k
Avg merge
2d 21h
Merged PRs (30d)
53

Description

Describe the Bug

A block add through the admin UI persisted a sparse blocks array to Postgres: the surviving rows kept their original array indexes (_order 1 and 3) with no row at _order 2. On every subsequent read, Payload rehydrates the gap as an undefined array entry, and the form-state builder throws:

Block with type "undefined" was found in block data, but no block with that type is defined in the config for field with schema path promotionListings.layout.

The result is that the entire edit view for the document fails to load in the affected locale — editors are locked out of the document until the underlying rows are repaired (a draft save that happens to rewrite the array also clears it).

This is the same end-state reported in #10485 (closed as unreproducible, now locked — the bot's closing message says to open a new issue if it persists). We hit it on payload 3.84.1, i.e. long after the form-state overhaul (#11946) that was believed to make it unreproducible, and we have DB forensics narrowing down when and how the bad data was written.

Forensic evidence

Collection: drafts enabled (localizeStatus: true, schedulePublish: true, maxPerDoc: 50), a localized: true blocks field named layout with 4 block types, @payloadcms/db-postgres.

Version-table trace for the affected document (version_updated_by_id joined, block rows unioned per version, one locale shown):

Version Saved (UTC) Action layout rows (type:_order)
v4/v5 2026-07-22 08:50 publish (locale A) header:1
v6/v7 2026-07-22 08:52 publish (locale A) header:1, cardGrid:3gap born here
v8/v9 2026-07-23 12:28 publish (locale B) header:1, cardGrid:3 (inherited)

Between two saves 2 minutes apart, a block add went from 1 block to rows at positions 1 and 3 — the middle row was silently dropped at write time while its neighbour kept index 3. This was not a reorder/delete; the same editing session also produced a This field requires at least 1 Rows. validation error on one of the block types (an array field with minRows: 1), which may be the row that vanished. Only one locale of the document was affected; the corruption then propagated into every subsequent version snapshot, and into the published tables on publish.

Two independent problems
  1. Write path: the postgres adapter accepts a sparse/invalid rows array and persists non-contiguous _order values instead of rejecting the write or compacting the array. Whatever produces the undefined middle row client-side, the server persisting it turns a UI glitch into durable data corruption.
  2. Read path: addFieldStatePromise throws on an undefined block row, which fails form-state for the whole document. A degraded render (skip the row / show an error placeholder, as requested in #10485) would keep the document editable so editors could fix it themselves.

We'd be happy to contribute a PR for (2) if maintainers agree with the skip-with-warning approach.

Link to the code that reproduces this issue

https://github.com/infonomic/payload-block-error

Reproduction Steps

Note on the reproduction link: it is the repro repo from #10485 (by @58bits), which reproduces the same failure class. We cannot yet reproduce the client-side trigger on demand — matching #10485 — but the corrupt state and the resulting lock-out reproduce deterministically:

  1. Create a blank app (pnpx create-payload-app@latest -t blank) with @payloadcms/db-postgres, drafts enabled, and a blocks field (e.g. layout) with 2+ block types. Localization is how we hit it in production but is not required for the read-path failure.
  2. Create a document with 3 blocks in layout and publish/save it.
  3. Simulate the sparse write our production trace shows (a middle row dropped at write time while the neighbour keeps its index) by removing the middle block row directly in the DB, leaving _order 1 and 3 — e.g. DELETE FROM <collection>_blocks_<middle_block> WHERE _parent_id = <doc_id> AND _order = 2; (do the same in the _<collection>_v_blocks_* version tables for the latest draft version).
  4. Open the document in the admin panel.
  5. Observed: the edit view fails to load; the server logs Block with type "undefined" was found in block data, but no block with that type is defined in the config for field with schema path <collection>.layout on every form-state request. The document is uneditable.
  6. Expected: the write path never persists a gapped _order sequence (reject or compact), and/or the form-state builder degrades gracefully (skip the undefined row with a warning) so the document stays editable.
Which area(s) are affected?

db: postgres, area: ui

Environment Info
Production (where the corruption was written): Payload 3.84.1, Next.js 16.2.9, Node.js 22 (Docker on Kubernetes), Postgres 18 (AWS RDS), multiple locales, localized blocks field, drafts with localizeStatus.

`pnpm payload info` from the affected project:

Relevant Packages:
  payload: 3.84.1
  next: 16.2.9
  @payloadcms/db-postgres: 3.84.1
  @payloadcms/live-preview-react: 3.84.1
  @payloadcms/next/utilities: 3.84.1
  @payloadcms/plugin-form-builder: 3.84.1
  @payloadcms/plugin-import-export: 3.84.1
  @payloadcms/plugin-mcp: 3.84.1
  @payloadcms/plugin-nested-docs: 3.84.1
  @payloadcms/plugin-redirects: 3.84.1
  @payloadcms/plugin-search: 3.84.1
  @payloadcms/plugin-seo: 3.84.1
  @payloadcms/richtext-lexical: 3.84.1
  @payloadcms/storage-s3: 3.84.1
  @payloadcms/ui/shared: 3.84.1
  react: 19.2.4
  react-dom: 19.2.4

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the addFieldStatePromise form-state path and the linked reproduction repository, then trace how the Postgres adapter writes block _order values. Reproduce the gapped rows from the SQL steps and inspect the form-state request failure. Done means the write path rejects or compacts gaps, or the form state handles an undefined row without locking the document.

Written by the indexing model from the issue text.

Assessment

Tech stack
nextjs, postgresql, typescript
Domain
databases, frontend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.