Describe the Bug
The validateOptions hook in @payloadcms/plugin-ecommerce performs a duplicate-check against every existing variant of the same product. It does this by calling combo.length / combo.every(...) on each sibling variant's options array (source: packages/plugin-ecommerce/src/collections/variants/createVariantsCollection/hooks/validateOptions.ts):
const existingOptions: (number | string)[][] = []
variants.forEach((variant: any) => {
existingOptions.push(variant.options)
})
const exists = existingOptions.some(
(combo) => combo.length === values.length && combo.every((val) => values.includes(val)),
)
If any sibling variant has options that is not an array — e.g. undefined or null — then combo.length throws:
TypeError: Cannot read properties of undefined (reading 'length')
at validateOptions.js:53:64
at Array.some (<anonymous>)
This is easy to hit with:
- Legacy rows from before the
options field existed
- Partially migrated / imported variants
- Variants created via a direct DB write
- Any raw document where
options was dropped by a broken migration
Once that state exists in the DB, it becomes impossible to ever create or update a variant for that product through Payload — every create attempt crashes validation.
Suggested fix — skip sibling variants whose options is not an array instead of unconditionally dereferencing .length:
variants.forEach((variant: any) => {
- existingOptions.push(variant.options)
+ if (Array.isArray(variant.options)) {
+ existingOptions.push(variant.options)
+ }
})
Or, equivalently, guard inside the some predicate:
const exists = existingOptions.some(
- (combo) => combo.length === values.length && combo.every((val) => values.includes(val)),
+ (combo) =>
+ Array.isArray(combo) &&
+ combo.length === values.length &&
+ combo.every((val) => values.includes(val)),
)
Link to the code that reproduces this issue
https://github.com/jhb-dev/payload-ecommerce-variant-options-undefined
Reproduction Steps
- Clone the reproduction repository and run the development server.
- Hit any API route (e.g.
curl http://localhost:3000/api/users) — this triggers onInit which:
- Creates a
variantType (Color) and two variantOptions (Red, Blue).
- Creates a product with
variants enabled.
- Creates one valid variant via the Local API (
options: [red]).
- Inserts a legacy variant directly into MongoDB (bypassing Payload) with no
options field.
- Then calls
payload.create({ collection: 'variants', ... options: [blue] }) to trigger validateOptions.
- Expected: The create succeeds — the plugin should skip sibling variants whose
options is not an array and treat them as non-duplicates.
- Actual: The create throws before the variant is ever written:
[ERROR]: 🔴 variant creation threw while validating options:
[ERROR]: TypeError: Cannot read properties of undefined (reading 'length')
at eval (@payloadcms/plugin-ecommerce/dist/collections/variants/createVariantsCollection/hooks/validateOptions.js:53:64)
at Array.some (<anonymous>)
at eval (@payloadcms/plugin-ecommerce/dist/.../validateOptions.js:53:44)
at async promise (payload/dist/fields/hooks/beforeChange/promise.js:97:38)
at async Promise.all (index 2)
at async traverseFields (payload/dist/fields/hooks/beforeChange/traverseFields.js:40:5)
at async beforeChange (payload/dist/fields/hooks/beforeChange/index.js:15:5)
at async createOperation (payload/dist/collections/operations/create.js:137:35)
Which area(s) are affected?
plugin: ecommerce
Environment Info
```
Binaries:
Node: 24.3.0
npm: 11.4.2
pnpm: 10.33.0
Relevant Packages:
payload: 3.84.0
@payloadcms/plugin-ecommerce: 3.84.0
next: 15.4.8
@payloadcms/db-mongodb: 3.84.0
@payloadcms/graphql: 3.84.0
@payloadcms/next/utilities: 3.84.0
@payloadcms/richtext-lexical: 3.84.0
@payloadcms/translations: 3.84.0
@payloadcms/ui/shared: 3.84.0
react: 19.2.1
react-dom: 19.2.1
Operating System:
Platform: darwin
Arch: arm64
```
Describe the Bug
The
validateOptionshook in@payloadcms/plugin-ecommerceperforms a duplicate-check against every existing variant of the same product. It does this by callingcombo.length/combo.every(...)on each sibling variant'soptionsarray (source:packages/plugin-ecommerce/src/collections/variants/createVariantsCollection/hooks/validateOptions.ts):If any sibling variant has
optionsthat is not an array — e.g.undefinedornull— thencombo.lengththrows:This is easy to hit with:
optionsfield existedoptionswas dropped by a broken migrationOnce that state exists in the DB, it becomes impossible to ever create or update a variant for that product through Payload — every create attempt crashes validation.
Suggested fix — skip sibling variants whose
optionsis not an array instead of unconditionally dereferencing.length:variants.forEach((variant: any) => { - existingOptions.push(variant.options) + if (Array.isArray(variant.options)) { + existingOptions.push(variant.options) + } })Or, equivalently, guard inside the
somepredicate:Link to the code that reproduces this issue
https://github.com/jhb-dev/payload-ecommerce-variant-options-undefined
Reproduction Steps
curl http://localhost:3000/api/users) — this triggersonInitwhich:variantType(Color) and twovariantOptions(Red,Blue).variantsenabled.options: [red]).optionsfield.payload.create({ collection: 'variants', ... options: [blue] })to triggervalidateOptions.optionsis not an array and treat them as non-duplicates.Which area(s) are affected?
plugin: ecommerce
Environment Info
```
Binaries:
Node: 24.3.0
npm: 11.4.2
pnpm: 10.33.0
Relevant Packages:
payload: 3.84.0
@payloadcms/plugin-ecommerce: 3.84.0
next: 15.4.8
@payloadcms/db-mongodb: 3.84.0
@payloadcms/graphql: 3.84.0
@payloadcms/next/utilities: 3.84.0
@payloadcms/richtext-lexical: 3.84.0
@payloadcms/translations: 3.84.0
@payloadcms/ui/shared: 3.84.0
react: 19.2.1
react-dom: 19.2.1
Operating System:
Platform: darwin
Arch: arm64
```