skip to content

In Cypress, why does a cached cy.session() login start failing in later tests?

level: seniorimportance: must knowfreq 66%

answer

  1. A restored session is trusted
  2. validate defaults to undefined
  3. Credentials can die mid-run
  4. Setup re-runs only when uncached
  5. Prove it with an authenticated request

basics

~20 s

The session was cached with no validate function, so Cypress restores the saved cookies and storage without checking them. Once the credential dies mid-run, setup is never re-run and every test after the first lands on the sign-in page.

solid answer

~40 s

`cy.session(id, setup, options)` runs `setup` only when no session for that `id` is cached. As of Cypress 16 the `validate` option defaults to `undefined`, and with no `validate` a restore is trusted unconditionally: Cypress replays the saved cookies, `localStorage` and `sessionStorage` and moves on. That is fine for the first test, which just created them, and wrong for everything after it if the credential died in between — a short token lifetime, an API restart in CI, a flushed session store, or another test signing the same user out. Add a `validate` that does something only an authenticated caller can, such as `cy.request('/api/expense-reports').its('status').should('eq', 200)`. If it fails after a restore, Cypress re-runs `setup` and the test carries on; if it fails right after `setup`, the test fails there, which is the signal you want.

code

javascript · 21 lines
javascript
Cypress.Commands.add('signInAs', (role) => {
  cy.session(
    ['expense-app', role],
    () => {
      cy.request('POST', '/api/sessions', { role }).its('status').should('eq', 201)
    },
    {
      validate() {
        cy.request({ url: '/api/expense-reports', failOnStatusCode: false })
          .its('status')
          .should('eq', 200)
      },
      cacheAcrossSpecs: true,
    }
  )
})

beforeEach(() => {
  cy.signInAs('approver')
  cy.visit('/reports/awaiting-approval')
})

go deeper

for a junior

Know what cy.session() is for: run the sign-in once, reuse the cookies and storage for later tests. Recognise that a restored session is not re-checked unless you ask Cypress to check it.

for a middle

Explain the contract in both directions: setup runs when nothing is cached, validate runs after every create and every restore, and a restore that fails validation quietly re-runs setup.

for a senior

Diagnose it out loud. First test green and the rest red, 401s or a bounce to the sign-in route, a Command Log that reports a clean restore — then name the missing validate and a cheap authenticated check to add.

for a principal

Own the reuse policy. Decide how long a cached credential may live, whether sessions cross spec boundaries, and what the suite does when the environment can revoke a token underneath a run.

## What `cy.session()` promises, and what it does not `cy.session(id, setup, options)` caches the browser credential state produced by `setup` — all cookies, `localStorage` and `sessionStorage` — under the `id` you give it. On a later call with the same `id`, Cypress skips `setup` and restores the saved state instead. That is the whole point: one sign-in serves many tests. The part people miss is what "skips setup" means. As of Cypress 16 the `validate` option defaults to `undefined`, and with no `validate` a restore is **unconditionally trusted**. Cypress replays the saved cookies and storage and hands control back to your test. It does not call the API, load a page, or check anything. If the credential died between the first test and this one, the browser is now carrying a dead session and nothing has noticed. ## Why credentials die inside a single run The first test passes because it created the session moments earlier. Everything after it depends on that credential still being good, and plenty of ordinary things kill it: - A short access-token lifetime — ten or fifteen minutes is common, and a full spec can outlast it. - The API restarting mid-run in CI, dropping its server-side session store. - A shared staging environment where another job flushes sessions or logs the same user out. - Any test that signs the user out, revokes a token, or changes the account's role. - A `cacheAcrossSpecs: true` session surviving far longer than the token it holds. ## What the failure looks like The reported error is never "your session expired". You get one of: 1. **A redirect.** `cy.visit('/reports/awaiting-approval')` lands on the sign-in page and the very next assertion fails on an element that is not there. 2. **A wall of 401s.** `cy.request()` and `cy.intercept()` show the API refusing everything, and the Command Log fills with failures that all look like separate bugs. 3. **A "works locally" report.** In `cypress open` mode sessions are cached across spec reruns, so a developer re-running one spec may never keep a session alive long enough to see it. Because the session was restored rather than created, the Command Log shows the restore as a success and gives no reason to suspect it. The first test in the spec passing while the rest fail is the tell. ## What `validate` changes `validate` is a function Cypress runs immediately after `setup` creates a session and again every time a session is restored. The session is treated as invalid if the function throws, contains a failing Cypress command, returns a promise that rejects or resolves to `false`, or if its last Cypress command yielded `false`. | Situation | With no `validate` | With `validate` | |---|---|---| | Cached session still good | restored, test continues | validated, test continues | | Cached session dead | restored anyway, test fails later | `setup` re-runs, test continues | | `setup` produced a bad session | test fails somewhere downstream | test fails at `cy.session()`, with the cause | The second row is the one that saves the run: validation failing **after a restore** silently re-creates the session and carries on. The third row is a bonus — validation failing right after `setup` fails the test on the spot, which is exactly where you want the failure when sign-in itself broke. ## Writing a validate that is worth having The function has to prove the session, not just prove that a value exists: - **Good:** `cy.request('/api/expense-reports').its('status').should('eq', 200)` — the API answers only for an authenticated caller. - **Good:** visiting a page only a signed-in user can reach and asserting the URL did not bounce to the sign-in route. - **Weak:** asserting a cookie is present. A dead cookie is still present. - **Weak:** asserting a token exists in `localStorage`. An expired token exists too. - **Weak:** loading a page that renders for anonymous visitors as well. Keep it cheap. `validate` runs on every restore, so a single request beats a page load in a suite of two hundred tests. And keep the `id` honest: it must be unique per identity — an approver's session and a submitter's session need different ids, or one restores over the other. ## Diagnosing it in an existing suite 1. Confirm the pattern: first test in the spec green, everything after it red on missing elements or 401s. 2. Look at the `cy.session()` call. If there is no `validate`, you have your answer without going further. 3. Add a `validate` that calls an authenticated endpoint, re-run, and watch the Command Log — a recreated session shows up as a recreate rather than a restore, which confirms the credential was dying. 4. If it still fails, the credential is dying *within* one test, and the fix is on the server side or in the token lifetime, not in `cy.session()`.

  • What does Cypress do when a cy.session() validate fails right after setup ran?
    It fails the test at that point rather than re-running `setup` in a loop. Validation failing immediately after creation means the sign-in itself did not work, so failing there gives you the real cause instead of a downstream assertion error. Validation failing after a *restore* is the case that re-runs `setup` and continues.
  • How does cacheAcrossSpecs change the blast radius of a stale Cypress session?
    It widens it. `cacheAcrossSpecs` defaults to `false`, so a session lives only for the spec that made it and each new spec re-runs `setup`. Turning it on makes the session global to the run on that machine, so one dead credential can now poison every remaining spec — which makes `validate` more important, not less.

saying these in an interview costs you the question

  • Says cy.session() re-checks the session on every restore
  • Adds a validate that only asserts a cookie exists
  • Blames flaky CI rather than the missing validate
  • Thinks a failing validate always fails the test
  • Reuses one session id for every role in the suite