Summary
In strava-v3@3.2.0 (the axios rewrite), every successful 2xx response clears the in-memory rate-limit state instead of updating it. As a result, rateLimiting.fractionReached() and rateLimiting.readFractionReached() always return 0 regardless of how many requests have been made, and consumers that rely on these methods to back off proactively get no signal until Strava itself returns 429.
The error path is unaffected — 429 responses still carry headers through axios, so lib/httpClient.js:210 (rateLimiting.updateRateLimits(e.response.headers)) works. Only the success path is broken.
Root cause
The internal contract between lib/httpClient.js and axiosUtility.js is inconsistent.
lib/httpClient.js:170-216 (_requestHelper) does:
const response = await this.request(options)
// Update rate limits using headers from the successful response
rateLimiting.updateRateLimits(response.headers)
It expects this.request(...) to return something with a .headers property.
The default request is httpRequest from axiosUtility.js:37-68:
const response = await axiosInstance({ ... })
return response.data // ← only the body; headers are dropped
So response.headers is undefined at httpClient.js:183. That falls through rateLimiting.parseRateLimits(undefined) (returns null because the truthy-header guard fails), which causes updateRateLimits to call RateLimit.clear() — zeroing every counter on every successful response.
Pre-3.2.0 this worked because the prior request-promise implementation respected options.resolveWithFullResponse = true (still set at httpClient.js:172) and returned {statusCode, headers, body}. The new axiosUtility.httpRequest does not honor that flag.
Repro
const strava = require('strava-v3');
const client = new strava.client(process.env.STRAVA_ACCESS_TOKEN);
(async () => {
await client.athlete.get();
console.log({
shortTermLimit: strava.rateLimiting.shortTermLimit,
shortTermUsage: strava.rateLimiting.shortTermUsage,
fractionReached: strava.rateLimiting.fractionReached(),
readFractionReached: strava.rateLimiting.readFractionReached(),
});
})();
Expected (with a real successful call): non-zero shortTermLimit (typically 200) and a non-zero fractionReached.
Actual on 3.2.0: all zeros.
Impact
Any consumer using rateLimiting.fractionReached() / readFractionReached() to throttle requests proactively gets no backoff at all under 3.2.0 — they'll sail straight into 429s. Silent regression from 3.1.0.
Suggested fix
Either:
a) Have axiosUtility.httpRequest honor options.resolveWithFullResponse and return the full axios response object when set (matches the request-promise contract that _requestHelper was written against):
const response = await axiosInstance({ ... })
if (options.resolveWithFullResponse) {
return response
}
return response.data
_requestHelper already sets options.resolveWithFullResponse = true at line 172, so this lights it back up with no caller changes.
b) Or change _requestHelper to no longer rely on the body carrying .headers, and instead pull headers from the request layer directly (more invasive — and breaks any custom request functions that callers have written against the existing .headers contract).
(a) is strictly less risky and is what I'd recommend.
Happy to send a PR for (a) if useful.
Summary
In
strava-v3@3.2.0(the axios rewrite), every successful 2xx response clears the in-memory rate-limit state instead of updating it. As a result,rateLimiting.fractionReached()andrateLimiting.readFractionReached()always return0regardless of how many requests have been made, and consumers that rely on these methods to back off proactively get no signal until Strava itself returns 429.The error path is unaffected — 429 responses still carry headers through axios, so
lib/httpClient.js:210(rateLimiting.updateRateLimits(e.response.headers)) works. Only the success path is broken.Root cause
The internal contract between
lib/httpClient.jsandaxiosUtility.jsis inconsistent.lib/httpClient.js:170-216(_requestHelper) does:It expects
this.request(...)to return something with a.headersproperty.The default
requestishttpRequestfromaxiosUtility.js:37-68:So
response.headersisundefinedathttpClient.js:183. That falls throughrateLimiting.parseRateLimits(undefined)(returnsnullbecause the truthy-header guard fails), which causesupdateRateLimitsto callRateLimit.clear()— zeroing every counter on every successful response.Pre-3.2.0 this worked because the prior
request-promiseimplementation respectedoptions.resolveWithFullResponse = true(still set athttpClient.js:172) and returned{statusCode, headers, body}. The newaxiosUtility.httpRequestdoes not honor that flag.Repro
Expected (with a real successful call): non-zero
shortTermLimit(typically200) and a non-zerofractionReached.Actual on 3.2.0: all zeros.
Impact
Any consumer using
rateLimiting.fractionReached()/readFractionReached()to throttle requests proactively gets no backoff at all under 3.2.0 — they'll sail straight into 429s. Silent regression from 3.1.0.Suggested fix
Either:
a) Have
axiosUtility.httpRequesthonoroptions.resolveWithFullResponseand return the full axios response object when set (matches therequest-promisecontract that_requestHelperwas written against):_requestHelperalready setsoptions.resolveWithFullResponse = trueat line 172, so this lights it back up with no caller changes.b) Or change
_requestHelperto no longer rely on the body carrying.headers, and instead pull headers from the request layer directly (more invasive — and breaks any customrequestfunctions that callers have written against the existing.headerscontract).(a) is strictly less risky and is what I'd recommend.
Happy to send a PR for (a) if useful.