All posts
Forward Development

Medusa 2.21 restricts Store API fields: what breaks and how to adapt

Medusa v2.21.0 enforces a strict allowlist on Store API fields. How to find the storefront queries it silently breaks and fix them without over-exposing data.

  • Medusa.js
  • Migrations
  • Operations

You upgrade the backend and the build passes. The product listing loads and every request returns 200. Then someone notices that the brand name is gone from the product cards, or the variant price on the product page reads undefined, or a low-stock badge never appears. The backend logs show nothing, and no request fails in the browser. Medusa v2.21.0 can break a storefront in exactly this way, and you need to understand it before you bump versions on a live store.

Every Store API route now has a strict allowlist

Store API routes accept a fields query parameter so a client can choose which fields and relations come back. Before 2.21.0, only a handful of routes limited it. The allowed list those routes used was a prefix check: listing region also granted region.orders, region.orders.customer.email, and everything else nested under it. A client could walk from a public resource into private data.

In v2.21.0, every core Store route ships an allowed list, and matching is exact. Each list is built from the route's default fields plus a small set of vetted extras. Allowing region now allows region and nothing else; region.id needs its own entry. The disallowed denylist from the previous release is gone, because the allowlist is now the only boundary. The covered routes include carts, collections, currencies, customers, orders, payment collections, payment providers, product categories, options, tags, types, variants, products, product search, regions, return reasons, returns, and shipping options. PR #16702 has the full per-route list.

Medusa recommends that every store take this update. It also published patches for versions 2.15.3 through 2.20.1 for teams that cannot move yet, which it says it usually doesn't do. We think that was the right call. A public API that matches field selectors by prefix leaks data to anyone curious enough to try.

The failure is silent, except for sorting

A field outside the allowed list is removed from the query before the query runs. The response is still a 200, just without the field. The storefront then renders undefined or an empty array, or throws in some component far from the fetch that caused it.

Sorting is the one case that fails loudly. If the order parameter names a field outside the route's allowed list, the request throws Order field {field} is not valid. Audit sort parameters along with field selections.

These are not affected:

  • Admin routes.
  • Custom routes outside the /store prefix.
  • Custom Store routes, unless their own validateAndTransformQuery config sets allowed.
  • A storefront that never passes fields. Default fields are always in allowed, so only extras the client asks for can be dropped.

How the check reads your fields string

fields is a comma-separated list. Medusa strips a leading +, -, *, or space and a trailing .* from each entry, then looks for that exact dotted path in the list. An entry like -title removes a default and is never checked. id is always included and always allowed.

Watch out for nested paths. Allowing *variants does not allow variants.calculated_price, because selecting a whole relation grants nothing nested under it. If the storefront sends *variants,+variants.calculated_price, those are two separate checks, and the nested one is where things break. Links added by your own modules or by plugins are the other usual casualty, such as a brand relation on products. The core list cannot know about them.

Finding the queries that depend on dropped fields

Start with a text search. In the storefront repo, find every place that sends fields or order to a Store route, whether through the JS SDK, a fetch wrapper, or a hand-built URL:

rg -n -t ts -t js "fields|order" .

The output is noisy, but it's a complete starting list. For each call, write down the route, the paths it requests after normalization, and whether each path is in that route's allowed list. Take the lists from the installed package or the PR, not from memory, and not from articles like this one.

Then check at runtime, because storefronts often build field strings dynamically. On a staging backend that still runs your current version, add a temporary middleware that logs what clients actually request:

// src/api/middlewares.ts (staging only, remove after the audit)
import { defineMiddlewares } from "@medusajs/medusa"
import type {
  MedusaRequest,
  MedusaResponse,
  MedusaNextFunction,
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/store/*",
      middlewares: [
        (req: MedusaRequest, res: MedusaResponse, next: MedusaNextFunction) => {
          if (req.query.fields || req.query.order) {
            console.log(req.path, req.query.fields, req.query.order)
          }
          next()
        },
      ],
    },
  ],
})

Click through the whole storefront: listing, product page, cart, checkout, account, and order history. Save the responses. Then upgrade staging to 2.21.0, send the same requests again, and compare the full set of dotted key paths in each response. A top-level key comparison misses dropped nested fields. Any path that existed before the upgrade and is missing after it is a dropped field.

Add fields back deliberately with allowFields

Once you have the list of dropped paths, decide for each one whether the storefront really needs it. Some will be leftovers from components nobody renders anymore. Delete those requests instead of re-exposing the fields.

Add the rest back with the allowFields middleware. It adds paths to req.allowed, which the core query validation merges into the route's list:

import { defineMiddlewares } from "@medusajs/medusa"
import { allowFields } from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/store/products",
      middlewares: [allowFields("brand", "brand.name")],
    },
  ],
})

Follow two rules. First, never set method or methods on this entry. A method-scoped middleware runs after the core query validation, so it has no effect. Your addition would silently do nothing, which is the same failure you are trying to fix. Second, list exact paths, because brand does not grant brand.name. Don't add a relation and a pile of nested paths just in case. With the denylist gone, anyone can read every path you add, and there is no second check behind it. Treat each entry as data you are choosing to publish.

For your own custom routes, set allowed in the validateAndTransformQuery config as well. Medusa recommends it for custom API routes too, and it gives them an explicit field boundary.

What to do before you upgrade

  • Run the field and sort audit on staging while it still runs your current version, and record what the storefront actually requests.
  • Upgrade staging to 2.21.0 and compare responses route by route. Treat any Order field ... is not valid error as a real bug.
  • Remove requests for fields nothing uses. Add back the rest with allowFields, using exact paths, and review that middleware file as a security change.
  • Set allowed on your custom routes.
  • If you can't upgrade soon, apply the patch for your version range. Each patch covers @medusajs/framework and @medusajs/medusa, plus @medusajs/loyalty-plugin if it's installed.
  • Add storefront tests that check for the fields your pages depend on, so a future allowlist change fails in CI instead of in production.

If you are partway through a move to Medusa, whether from Magento 2 or Adobe Commerce or another platform, build against 2.21.0 now. Designing field selections against the allowlist from the start costs less than fixing a finished Next.js storefront afterward. If you want someone to run the audit with you or review what you plan to re-expose, get in touch.

Need this done on a real stack?

Magento 2, Adobe Commerce, migrations to Medusa.js or Vendure, enterprise Next.js, WordPress, and AI automation.

Contact us