agent built token refresh around expires_in - the API only returns absolute expiry in a differently named field and...
A playbook for fixing expiry-field mismatches: read the provider's actual token response fields, map the real expiry field name, and compute refresh timing from the absolute timestamp. Use when generated refresh logic watches an expires_in field the provider never sends, so refresh never fires. Not for missing refresh wiring, revoked tokens, or clock-skew issues.
TL;DR
The refresh code is watching a field the provider never sends. Print one real token response, find the field that actually carries expiry (an absolute timestamp under a different name), and compute refresh time from that. A mismatch between assumed field names and real field names is a five-minute fix once you read the actual response.
The query
agent built token refresh around expires_in - the API only returns absolute expiry in a differently named field and refresh never firedSteps
1. Print one real token response
Request a token and log the full JSON response body. Do not read the docs first - read the wire. List every field name exactly as the provider spells it.
Expected: the real field names, e.g. an absolute expiry timestamp in a field like expires_at or token_expiry, and no expires_in anywhere.
2. Map the real expiry field
Identify which field carries the expiry: absolute timestamp versus seconds-until-expiry, and its exact name and format (unix seconds, unix milliseconds, ISO-8601 string). Write the mapping down - field name, format, and what "expired" means for it.
Expected: a one-line mapping like "expiry lives in FIELD_NAME as unix seconds, absolute."
3. Rewrite the refresh trigger around the real field
Replace the expires_in countdown with logic that reads the absolute expiry and refreshes a few minutes before it. Compute "time remaining" as expiry minus now, using the same clock the provider assumes, and trigger refresh when the remainder drops under your buffer.
Expected: refresh fires before tokens die, verified by watching one full token lifetime in the logs.
4. Handle the provider that omits expiry entirely
If the token response carries no expiry field at all, fall back to refresh-on-401 plus a conservative assumed lifetime taken from the provider's docs. Log a warning so the assumption is visible.
Expected: a documented fallback path instead of a silent never-refresh.
5. Add a test that would have caught this
Write a test that feeds the client a synthetic token response shaped like the real one (no expires_in, expiry in the real field) and asserts refresh fires. Run it in CI so the next regeneration cannot reintroduce the assumption.
Expected: a failing-before, passing-after test pinned to the provider's real response shape.
Use this when
- Refresh logic exists but never fires and tokens die on schedule
- The token response has no
expires_infield (check the wire, not the docs) - The provider returns an absolute expiry timestamp under a differently named field
- A regeneration reintroduced the same broken assumption
Not for this skill when
- No refresh logic exists at all (wire refresh first, then fix the trigger)
- Refresh fires but the refresh call itself fails (token endpoint problem)
- The field exists but clock skew makes the client think tokens are live (time-sync problem)
- The provider genuinely returns
expires_inand the code reads it wrong (parsing bug)
Variant phrasings
token refresh never fired even though refresh code exists
Classic symptom. Step 1 shows you what the code is actually watching versus what the provider actually sends.
provider returns expiry as an absolute timestamp
Same fix. Step 3 converts the absolute timestamp into a refresh trigger; the shape of the timestamp does not change the approach.
Why it happens
Most OAuth tutorials use expires_in, so the agent's training data says "expiry equals expires_in." The provider chose a different contract - an absolute timestamp in its own field name - and the agent generated against the tutorial, not the provider. The code runs fine, the field lookup returns nothing, and the refresh branch quietly never executes. Nobody sees an error because a missing field is not an exception, it is just a condition that is never true.
Edge cases
- Expiry in milliseconds versus seconds: a 13-digit timestamp treated as seconds puts expiry 30,000 years out. Normalize the magnitude in step 2.
- Provider changes the field name between API versions: the step-5 test pins the current shape; when the provider ships v3, the test fails loudly instead of refresh dying silently.
- Multiple token types with different expiry fields: map each one. Access and refresh tokens sometimes report expiry differently.
- Timezone or clock drift between client and provider: keep the refresh buffer generous (five minutes, not thirty seconds) so minor drift never causes a 401.
Provenance
Resolved from the public thread: https://vectle.com/posts/psttADJgHW3he6lSdQ2Twptg
Maintainer review
No maintainer verification is recorded for this version.
This records the version a maintainer checked. It does not assert that the version is the latest upstream release.