Skip to content

login() broken: TES SSO redirect loop causes "redirect count exceeded" (April 2026) #48

@s2owl

Description

@s2owl

Summary

As of approximately 27 April 2026, login() throws the following error for all parent accounts:

TypeError: fetch failed
  [cause]: Error: redirect count exceeded

Steps to Reproduce

const client = new ParentClient('email@example.com', 'password');
await client.login(); // throws redirect count exceeded

Root Cause

ClassCharts has introduced a TES SSO verification step. When login() internally calls getPupils(), the /apiv2parent/pupils endpoint now redirects to session.tes.com/v1/verify, which redirects back to /apiv2parent/pupils?prompt=login, creating an infinite redirect loop that exceeds Node's fetch redirect limit.

Redirect chain:

GET /apiv2parent/pupils
  → 302 https://session.tes.com/v1/verify?returnUrl=https://www.classcharts.com/apiv2parent/pupils
  → 302 https://www.classcharts.com/apiv2parent/pupils?prompt=login
  → 302 https://session.tes.com/v1/verify?returnUrl=...
  → (loop)

Workaround

The TES verification can be satisfied by following the redirect chain once manually after the login POST, before calling getPupils():

// After the login POST succeeds (still returns 302 + cookies as before):
const cookieHeader = `cc-session=${ccSession}; parent_session_credentials=${credMatch}`;

// Do the TES handshake manually
const tesRes = await fetch(
  'https://session.tes.com/v1/verify?returnUrl=https%3A%2F%2Fwww.classcharts.com%2Fapiv2parent%2Fpupils',
  { headers: { Cookie: cookieHeader }, redirect: 'manual' }
);
const tesLocation = tesRes.headers.get('location');
if (tesLocation) {
  await fetch(tesLocation, { headers: { Cookie: cookieHeader }, redirect: 'manual' });
}

// getPupils() now returns 200 as expected

Environment

  • classcharts-api version: 2.12.2 (latest)
  • Node.js: v24
  • Confirmed broken on both local machines and cloud (GCP Cloud Run)
  • Confirmed working with the above workaround
  • UK parent account, April 2026

Notes

The login POST itself is unchanged and still works correctly. Only the subsequent getPupils call (triggered inside login()) is affected. All other API calls continue to work once a valid session is established via the workaround.

Happy to submit a PR with a fix if helpful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions