BoxProbe
Public lab Confirmed upstream bug Medusa 2.14.0

An intermittent 404 the admin SPA sees, that the DB doesn't

Running scout against a clean Medusa 2.14.0 cold-boot, 8 of 15 admin scenarios started failing seconds after login. The trigger turned out to be a 3/5 (60%) intermittent 404 on GET /admin/users/me — with the same cookie, the same actor_id, and the same SQL hitting postgres and returning the row. Filed upstream; Medusa maintainer confirmed in 25 minutes and identified a buffer-aliasing race in the mikro-orm entity serializer.

3/5 (60%)
Reproduces on 2.14.0
0/5
Reproduces on 2.13.6
25 min
To maintainer confirm
4
Evidence layers in report
Bug confirmed — routed to team for fix Filed 2026-06-04

Confirmed regression in mikro-orm-serializer. Routed to the team for a fix with regression coverage.

Headline regression · auth race

Admin /admin/users/me intermittently 404s within seconds of login (~60% reproducible)

On Medusa 2.14.0, after a successful login, the admin SPA's first call to GET /admin/users/me during navigation to any sub-route returns 404 in about 60% of attempts. The SPA reacts by redirecting the user back to /login. The same flow is rock-solid on 2.13.6 (0/5 across many trials).

confirmed upstream bug — routed for fix

Same request, same cookie session, same actor_id — but the response flips between 200 (with user) and 404 ("User with id: X was not found") within a 2-second window. Postgres receives the exact ORM-generated query and returns the row; the row is dropped somewhere between the DB driver and the route handler.

How we found it: Running scout's UI-driven regression scenarios against a clean 2.14.0 cold-boot caused 8 of 15 admin scenarios to fail at the first post-login navigation. Drilling in past concurrency, DB state, and pg-driver hypotheses isolated a 4-line Playwright repro hitting the same intermittent 404.
Confirmed Medusa maintainer reply on issue thread, 25 minutes after filing

This is a regression in the entity serialization layer, not in the auth/session or DB path. The row is dropped in mikroOrmSerializer, which every module read passes through. getKeys() returns the shared keyCollectionBuffer by reference (not a copy) when the buffer is exactly full; a nested serialize() call during relation traversal then mutates that buffer while the parent loop is still iterating it, intermittently overwriting the parent's remaining keys (potentially including id). The previous implementation (retained as mikro-orm-serializer-old.ts) allocated a fresh Array.from(keys) per serialize() call, so nested calls could never corrupt a parent frame — which is why 2.13.6 does not reproduce.

Root-cause file: packages/core/utils/src/dal/mikro-orm/mikro-orm-serializer.ts

Scope: This is a core, hot serialization path that can intermittently drop rows from any query result. Admin login is just one visible symptom.

The whole point of the report we filed was that we couldn't pinpoint the in-Medusa cause from outside — we just stacked enough evidence that the maintainer could. Four observable layers, in order:

  1. 1

    Symptom (user-visible)

    Login → navigate to /collections → 2s later the page bounces back to /login. Sometimes. Roughly 3 in 5 attempts on 2.14.0.

  2. 2

    Same request, different response

    The 200 (right after login) and the 404 (2s later) carry the exact same connect.sid cookie. The 404 body interpolates the real, current admin user_id — so req.auth_context.actor_id resolved correctly. Handler reached the if (!user) NOT_FOUND branch.

  3. 3

    DB returns the row, handler sees nothing

    Postgres log_statement = 'all' shows the exact ORM-emitted SELECT against the user table reaches the DB during the 404. Running that same SQL by hand 30 times in a row returns 1 row, 30/30. So the row leaves postgres but is gone by the time const [user] = await remoteQuery(query) runs.

  4. 4

    Source code on this path is bit-identical

    md5-comparing every .js file in the request chain — @medusajs/user, modules-sdk, orchestration, framework/http, authenticate-middleware, /admin/users/me/route, /auth/session/route, medusa-config — between 2.13.6 and 2.14.0 produces zero diffs.

Each hypothesis we tested ourselves before filing — so the maintainer didn't have to ask “did you try X?”

Twenty lines. Runs against an unmodified dtc-starter at the v2.14.0 tag.

# pip install playwright && playwright install chromium
import asyncio
from playwright.async_api import async_playwright

async def attempt(p, base_url):
    browser = await p.chromium.launch(headless=True)
    ctx = await browser.new_context()
    page = await ctx.new_page()
    await page.goto(f"{base_url}/login", wait_until="networkidle")
    await page.locator("input").nth(0).fill("admin@medusa-test.com")
    await page.locator("input").nth(1).fill("supersecret")
    await page.locator("button").filter(has_text="Continue with Email").click()
    await asyncio.sleep(2)
    await page.goto(f"{base_url}/collections", wait_until="networkidle")
    await asyncio.sleep(2)
    kicked = "login" in page.url
    await browser.close()
    return kicked

async def main():
    async with async_playwright() as p:
        kicks = sum(await attempt(p, "http://localhost:9000/app") for _ in range(5))
        print(f"kicked: {kicks}/5")

asyncio.run(main())
dtc-starter @ v2.14.0 Related case: 2.13.6 → 2.14.0 contract drift

Want this kind of audit for your application?

Send a URL and a user scenario. We'll run scout against your baseline and target and share the analysis.

Get a Free Sample Report