Skip to content

Rate-limit tracking broken in 3.2.0: successful responses clear all counters to 0 #197

@bendalton

Description

@bendalton

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.

Metadata

Metadata

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