Skip to content

Fix Meebook/Huskelisten crash on non-list API responses and handle JWT token expiry#341

Open
Jogge wants to merge 1 commit into
scaarup:mainfrom
Jogge:main
Open

Fix Meebook/Huskelisten crash on non-list API responses and handle JWT token expiry#341
Jogge wants to merge 1 commit into
scaarup:mainfrom
Jogge:main

Conversation

@Jogge
Copy link
Copy Markdown
Contributor

@Jogge Jogge commented Apr 6, 2026

Fixes #311 — Sensors become unavailable after a few hours/days and require a reload to recover.

Problem

When Meebook's JWT token expires, the API returns a JSON dict ({"message": "JWT-Token expired, please renew."}) instead of the expected JSON array. The code iterated over this response directly, causing TypeError: string indices must be integers, not 'str' which crashed the sensor update loop and made all sensors permanently unavailable until a manual reload.

The same class of bug existed in the Huskelisten flow, where a bare except: clause failed to set data = None, leaving data undefined if JSON parsing failed.

Solution

Meebook — JWT token expiry recovery:
When Meebook returns a JWT expiry message, the integration now invalidates the cached widget token, resets the HTTP session (to clear stale cookies), forces a full Aula token refresh via login(force_refresh=True), obtains a fresh widget token, and retries the Meebook request — all in a single pass without a retry loop.

Meebook — defensive response handling:

  • Wrapped json.loads() in try/except (json.JSONDecodeError, ValueError) (was previously unprotected)
  • Added isinstance(data, list) type guard before iterating
  • Preserved the existing exceptionMessage error handling within the type guard

Huskelisten — defensive response handling:

  • Narrowed bare except: to except (json.JSONDecodeError, ValueError)
  • Set data = None on parse failure (previously left data undefined)
  • Added isinstance(data, list) type guard before iterating

login() — force_refresh parameter:
Added force_refresh=False parameter to login() to allow bypassing the "token looks valid" cache check, needed for the Meebook recovery flow to force renew_access_token() even when the access token hasn't technically expired yet.

…T token expiry

Fixes scaarup#311 — Sensors become unavailable after a few hours/days
and require a reload to recover.

## Problem

When Meebook's JWT token expires, the API returns a JSON dict
(`{"message": "JWT-Token expired, please renew."}`) instead of the
expected JSON array. The code iterated over this response directly,
causing `TypeError: string indices must be integers, not 'str'` which
crashed the sensor update loop and made all sensors permanently
unavailable until a manual reload.

The same class of bug existed in the Huskelisten flow, where a bare
`except:` clause failed to set `data = None`, leaving `data` undefined
if JSON parsing failed.

## Solution

**Meebook — JWT token expiry recovery:**
When Meebook returns a JWT expiry message, the integration now
invalidates the cached widget token, resets the HTTP session (to clear
stale cookies), forces a full Aula token refresh via
`login(force_refresh=True)`, obtains a fresh widget token, and retries
the Meebook request — all in a single pass without a retry loop.

**Meebook — defensive response handling:**
- Wrapped `json.loads()` in `try/except (json.JSONDecodeError, ValueError)`
  (was previously unprotected)
- Added `isinstance(data, list)` type guard before iterating
- Preserved the existing `exceptionMessage` error handling within the
  type guard

**Huskelisten — defensive response handling:**
- Narrowed bare `except:` to `except (json.JSONDecodeError, ValueError)`
- Set `data = None` on parse failure (previously left `data` undefined)
- Added `isinstance(data, list)` type guard before iterating

**login() — force_refresh parameter:**
Added `force_refresh=False` parameter to `login()` to allow bypassing
the "token looks valid" cache check, needed for the Meebook recovery
flow to force `renew_access_token()` even when the access token hasn't
technically expired yet.
@scaarup
Copy link
Copy Markdown
Owner

scaarup commented Apr 11, 2026

The access token and the problem itself is only related to Meebook, right? I do not understand why the login function for Aula itself needs to be changed here?

@Jogge
Copy link
Copy Markdown
Contributor Author

Jogge commented Apr 13, 2026

@scaarup Jeg smider lige Copilots forklaring på, hvorfor det er implementeret sådan her:

Good question! The Meebook JWT token is not an independent token — it is derived from the Aula session. Specifically, when we call get_token("0004"), that calls the aulaToken.getAulaToken endpoint on the Aula API, which generates the Meebook widget token based on the current Aula session (using the session cookies and access token).

The problem is this chain of dependencies:

  1. Meebook says "JWT-Token expired, please renew."
  2. We clear the cached Meebook widget token and call get_token("0004") again
  3. get_token calls aulaToken.getAulaToken using the current Aula session
  4. But if the Aula session/access token is stale, the newly generated Meebook token will also be stale/expired — Meebook rejects it again

This is exactly what was observed across the iterative commits in the PR Jogge#1:

  • First attempt (commit 210cfc2): just cleared the cached Meebook token and re-fetched → didn't work because the Aula session was stale
  • Second attempt (1db8e8b): added a self.login() call before retrying → didn't work because login() saw the Aula access token was technically still "valid" and reused it (short-circuited)
  • Third attempt (b5a70d3): added force_refresh=True to login() to skip the "reuse valid token" shortcut → ensures the Aula access token is actually renewed
  • Fourth attempt (385aeef): also reset self._session = None to clear stale cookies from the HTTP session → the stale session cookies were causing the Aula server to still return tokens from the old session

So login(force_refresh=True) is needed because the Aula access token can appear locally valid (not expired by the clock) but still produce expired Meebook widget tokens — presumably because the server-side session state has drifted. The force_refresh parameter forces login() to actually go through renew_access_token() instead of short-circuiting on "token looks valid", and combined with the session reset, this ensures a completely fresh Aula session from which a valid Meebook token can be generated.

In short: the Meebook token is a downstream artifact of the Aula session, so fixing Meebook token expiry requires being able to force-refresh the upstream Aula session.

Jeg spurgte efterfølgende, om det kunne løses mere elegant, og fik denne opsumering:

The core issue is that a Meebook-specific problem (expired JWT) was solved by modifying the general Aula login() function and inlining retry logic at the Meebook call site. A cleaner approach is to push the retry-on-expiry logic down into get_token() and create a shared widget API call helper. This keeps login() unchanged, eliminates code duplication, and gives all widgets automatic token-expiry recovery.

Problemet er bare, at når jeg beder den implementere det, så bliver ændringerne ret invasive og begynder bl.a. at fjerne mock-data:

Jogge/aula@4ce91bf...bed0e89

Jeg kan godt følge, hvad Python-koden gør, men jeg ville ikke selv kunne implementere det fra bunden. Den nuværende løsning virker dog som forventet. Hvis du har et forslag til en mere elegant løsning, som undgår at ændre login-flowet, er du meget velkommen til at byde ind 🙌

HairingX added a commit to HairingX/aula that referenced this pull request Apr 29, 2026
Match upstream PR scaarup#341 widget JWT recovery; revert v0.2.6 regressions
@Jogge Jogge mentioned this pull request May 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sensor bliver inaktiv

3 participants