Skip to content

[Bug]: Product CSV export produces misaligned columns across pagination batches #16209

Description

@cyrenity

Package.json file

{
  "name": "@aquahealth/backend",
  "version": "0.0.1",
  "description": "A starter for Medusa projects.",
  "author": "Medusa (https://medusajs.com)",
  "license": "MIT",
  "keywords": [
    "sqlite",
    "postgres",
    "typescript",
    "ecommerce",
    "headless",
    "medusa"
  ],
  "scripts": {
    "build": "medusa build",
    "start": "medusa start",
    "dev": "medusa develop",
    "lint": "medusa lint",
    "test:integration:http": "TEST_TYPE=integration:http NODE_OPTIONS=--experimental-vm-modules jest --silent=false --runInBand --forceExit",
    "test:integration:modules": "TEST_TYPE=integration:modules NODE_OPTIONS=--experimental-vm-modules jest --silent=false --runInBand --forceExit",
    "test:unit": "TEST_TYPE=unit NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit"
  },
  "dependencies": {
    "@medusajs/admin-sdk": "2.18.0",
    "@medusajs/admin-shared": "2.18.0",
    "@medusajs/caching": "2.18.0",
    "@medusajs/caching-redis": "2.18.0",
    "@medusajs/cli": "2.18.0",
    "@medusajs/core-flows": "2.18.0",
    "@medusajs/dashboard": "2.18.0",
    "@medusajs/draft-order": "2.18.0",
    "@medusajs/event-bus-redis": "2.18.0",
    "@medusajs/framework": "2.18.0",
    "@medusajs/icons": "2.18.0",
    "@medusajs/js-sdk": "2.18.0",
    "@medusajs/locking-redis": "2.18.0",
    "@medusajs/medusa": "2.18.0",
    "@medusajs/ui": "4.2.0",
    "@medusajs/workflow-engine-redis": "2.18.0",
    "@react-email/components": "1.0.12",
    "@tanstack/react-query": "5.64.2",
    "react-i18next": "13.5.0",
    "react-router-dom": "6.30.4",
    "resend": "^6.18.0",
    "zod": "4.2.0"
  },
  "devDependencies": {
    "@medusajs/test-utils": "2.18.0",
    "@swc/core": "^1.7.28",
    "@swc/jest": "^0.2.36",
    "@types/jest": "^29.5.13",
    "@types/node": "^20.12.11",
    "@types/react": "^18.3.2",
    "@types/react-dom": "^18.2.25",
    "jest": "^29.7.0",
    "prop-types": "^15.8.1",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "ts-node": "^10.9.2",
    "typescript": "^5.6.2",
    "vite": "^5.4.14",
    "yalc": "^1.0.0-pre.53"
  },
  "engines": {
    "node": ">=20"
  }
}

Node.js version

v20.20.2

Database and its version

PostgreSQL 15.18

Operating system name and version

Docker Desktop on macOS 26.5.1

Browser name

Google Chrome

What happended?

Exporting more than 50 products from Medusa Admin generates a syntactically
valid CSV whose data columns become misaligned after the first pagination
batch.

The exported catalog contains 111 products. With the default export batch size
of 50 products, the file has these structures:

Batch Products Expanded variant rows Fields per row
1 50 62 59
2 50 50 61
3 11 27 64

The single CSV header contains 59 fields. All 62 rows produced from the first
batch have 59 fields, but all subsequent rows contain either 61 or 64 fields.
As a result, 77 of the 139 data rows do not match the header.

For example, the first batch's header reserves three initial product-image
columns, placing Product Sales Channel 1 at field 27 and Variant Id at
field 28. A product in the second batch has four product images. Its sales
channel is written at field 28 and its variant ID at field 29, shifting all
remaining values away from their declared columns.

The file parses successfully with an RFC-compatible CSV parser. It has 140
physical lines and 140 logical records, so this is not caused by quoting,
delimiters, or embedded newlines.

The likely cause is exportProductsStep in:

packages/core/core-flows/src/product/steps/export-products.ts

The workflow:

  1. Queries products in batches of 50.
  2. Calls normalizeForExport for each batch.
  3. Calls json2csv separately for each normalized batch.
  4. Uses prependHeader: !hasHeader, so only the first batch emits a header.
  5. Appends later batches without ensuring that they use the first batch's
    field list and order.

normalizeForExport creates dynamic keys for product images, tags, sales
channels, categories, variant prices, variant options, and variant images.
Therefore, each json2csv call can infer a different schema and field order
from the products present in that batch.

This also explains why exporting and immediately importing the same file fails
or maps values into the wrong columns.

Steps to reproduce

  1. Create more than 50 products.
  2. Ensure the first 50 and later products do not have identical dynamic
    structures. For example:
    • products in the first batch have at most three images;
    • a product after position 50 has four or more images; or
    • later products have an additional option, currency price, or variant
      image.
  3. In Medusa Admin, open Products → Export Products.
  4. Export all products and download the generated CSV.
  5. Parse the CSV and compare the number of fields in every record with the
    number of header fields.
  6. Optionally attempt to import the unchanged export.

Expected behavior

The exporter should determine one stable union of columns for the complete
export and serialize every product batch using that same ordered field list.
Every data row must have the same number of fields as the header.

An exported file should be importable into a compatible Medusa installation
without manual column repair.

Actual behavior

Only the first batch determines the CSV header. Later batches independently
infer their fields from their own products, but their headers are suppressed.
Rows from those batches consequently have different lengths and column
positions.

Link to reproduction repo

https://github.com/medusajs/medusa

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions