Skip to content

fix(ui): parse columns/queryByGroup from URL to stop escape accumulation#16902

Open
rodrigo-builtonveya wants to merge 1 commit into
payloadcms:mainfrom
rodrigo-builtonveya:fix/list-columns-url-encoding-16659
Open

fix(ui): parse columns/queryByGroup from URL to stop escape accumulation#16902
rodrigo-builtonveya wants to merge 1 commit into
payloadcms:mainfrom
rodrigo-builtonveya:fix/list-columns-url-encoding-16659

Conversation

@rodrigo-builtonveya
Copy link
Copy Markdown

Fixes #16659

Problem

On a collection list view, the columns query param accumulates an extra JSON.stringify escape layer on every page refresh after a specific navigation sequence (toggle a column → open a doc → back → sort → refresh…). The escaping roughly doubles each refresh until the URL exceeds the request size limit (~14KB on Vercel) and the admin panel becomes inaccessible with 414 URI Too Long.

Root cause

The columns / queryByGroup URL round-trip is asymmetric:

  • WriteListQueryProvider (packages/ui/src/providers/ListQuery/index.tsx) serializes both via JSON.stringify(newQuery.columns) on two paths (refineListData, syncPropsToURL).
  • ReadqueryFromURL = sanitizeQuery(parseSearchParams(rawSearchParams)) left them as JSON strings; sanitizeQuery never parsed them back into their canonical array/object form.

So whenever the value being written originated from the URL (a string), JSON.stringify(string) adds another layer. Each refresh re-reads the string and re-stringifies → escaping compounds.

(transformColumnsToPreferences on the server does JSON.parse strings, but the client state path did not — hence the asymmetry.)

Fix

sanitizeQuery now unwraps every accumulated JSON.stringify layer from columns/queryByGroup on read, so React state always holds the canonical shape and the existing JSON.stringify write becomes idempotent across URL round-trips.

Unwrapping all layers (not just one) also heals URLs already corrupted by the prior behavior, rather than leaving them in a broken fixed point. The fix lives at the single read boundary (sanitizeQuery's only caller is ListQueryProvider), so both write paths are covered. Consumers such as TableColumns already require columns to be a string[], so the read-side parse is the correct layer (a write-side guard that left a string in state would break them).

Tests

Added packages/ui/src/providers/ListQuery/sanitizeQuery.spec.ts:

  • parses a JSON-stringified columns array / queryByGroup object back from the URL
  • leaves already-parsed values untouched
  • does not accumulate escaping across repeated URL round-trips (idempotent)
  • recovers a multiply-encoded (already-corrupted) value back to its canonical form
  • preserves existing sanitization behavior (empty params, numeric coercion, empty where/columns drop)

Verified red→green (the regression tests fail against the previous sanitizeQuery), full ui unit suite passes, and tsc/eslint/prettier are clean.

`ListQueryProvider` writes `columns` and `queryByGroup` to the URL via
`JSON.stringify(...)`, but the read path (`parseSearchParams` -> `sanitizeQuery`)
left them as JSON strings instead of parsing them back into their canonical
array/object form. So whenever the value being written originated from the URL
(a string), `JSON.stringify` added another escape layer. On a list view this
compounds on every refresh until the `columns` query param overflows the request
size limit (414 URI Too Long) and the admin panel becomes inaccessible.

`sanitizeQuery` now unwraps every accumulated `JSON.stringify` layer from
`columns`/`queryByGroup` on read, so React state always holds the canonical shape
and the existing `JSON.stringify` write is idempotent across URL round-trips.
Unwrapping all layers (not just one) also heals URLs already corrupted by the
prior behavior instead of leaving them in a broken fixed point. Consumers
(e.g. `TableColumns`) already require `columns` to be a `string[]`.

Fixes payloadcms#16659
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 UI] List view columns query param re-encodes infinitely on refresh, eventually exceeding URL length limits

2 participants