Skip to content

Commit 4b0fb7d

Browse files
committed
feat(provider): Handle numeric HTTP Retry-After header for all responses
Might be necessary to avoid getting blocked by Spotify, but the logged header may be interesting for other providers as well. So far I have not encountered such a header for a successful request to the Spotify API, only when it is too late already (HTTP 429 with a huge Retry-After delay of multiple hours).
1 parent 966b38b commit 4b0fb7d

File tree

2 files changed

+22
-13
lines changed

2 files changed

+22
-13
lines changed

providers/Spotify/mod.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { ResponseError } from '@/utils/errors.ts';
88
import { selectLargestImage } from '@/utils/image.ts';
99
import { splitLabels } from '@/utils/label.ts';
1010
import { ResponseError as SnapResponseError } from 'snap-storage';
11-
import { delay } from 'std/async/delay.ts';
1211
import { encodeBase64 } from 'std/encoding/base64.ts';
1312
import { availableRegions } from './regions.ts';
1413

@@ -98,17 +97,10 @@ export default class SpotifyProvider extends MetadataApiProvider {
9897
let apiError: ApiError | undefined;
9998
if (error instanceof SnapResponseError) {
10099
const { response } = error;
100+
this.handleRateLimit(response);
101101
// Retry API query when we encounter a 429 rate limit error.
102102
if (response.status === 429) {
103-
const retryAfter = response.headers.get('Retry-After');
104-
this.log.info(`${this.name} rate limit error (HTTP 429): Retry-After ${retryAfter}`);
105-
if (retryAfter) {
106-
const retryAfterMs = parseInt(retryAfter) * 1000;
107-
if (retryAfterMs > 0 && retryAfterMs < this.requestMaxDelay) {
108-
this.requestDelay = delay(retryAfterMs);
109-
return this.query(apiUrl, maxTimestamp);
110-
}
111-
}
103+
return this.query(apiUrl, maxTimestamp);
112104
}
113105
try {
114106
// Clone the response so the body of the original response can be
@@ -126,9 +118,6 @@ export default class SpotifyProvider extends MetadataApiProvider {
126118
}
127119
}
128120

129-
/** Delay which should be awaited before the next request. */
130-
private requestDelay = Promise.resolve();
131-
132121
private async requestAccessToken(): Promise<ApiAccessToken> {
133122
// See https://developer.spotify.com/documentation/web-api/tutorials/client-credentials-flow
134123
const url = new URL('https://accounts.spotify.com/api/token');

providers/base.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { FeatureQuality, type FeatureQualityMap, type ProviderFeature } from './features.ts';
22
import { ProviderError, ResponseError } from '@/utils/errors.ts';
33
import { pluralWithCount } from '@/utils/plural.ts';
4+
import { delay } from 'std/async/delay.ts';
45
import { getLogger } from 'std/log/get_logger.ts';
56
import { rateLimit } from 'utils/async/rateLimit.js';
67
import { simplifyName } from 'utils/string/simplify.js';
@@ -200,6 +201,9 @@ export abstract class MetadataProvider {
200201

201202
protected fetch = fetch;
202203

204+
/** Delay which should be awaited before the next request. */
205+
protected requestDelay = Promise.resolve();
206+
203207
/** Maximum acceptable delay between the time a request is queued and its execution (in ms). */
204208
protected requestMaxDelay: number;
205209

@@ -217,11 +221,15 @@ export abstract class MetadataProvider {
217221
responseMutator: options?.responseMutator,
218222
});
219223
this.log.debug(`${input} => ${snapshot.path} (${snapshot.isFresh ? 'fresh' : 'old'})`);
224+
if (snapshot.isFresh) {
225+
this.handleRateLimit(snapshot.content);
226+
}
220227
} else {
221228
let response = await this.fetch(input, options?.requestInit);
222229
if (options?.responseMutator) {
223230
response = await options.responseMutator(response);
224231
}
232+
this.handleRateLimit(response);
225233
snapshot = {
226234
content: response,
227235
timestamp: Math.floor(Date.now() / 1000),
@@ -245,6 +253,18 @@ export abstract class MetadataProvider {
245253
isFresh: snapshot.isFresh ?? false,
246254
};
247255
}
256+
257+
/** Handles rate limit HTTP headers and sets the request delay. */
258+
protected handleRateLimit(response: Response) {
259+
const retryAfter = response.headers.get('Retry-After');
260+
if (retryAfter) {
261+
this.log.info(`${this.name} rate limit (HTTP ${response.status}): Retry-After ${retryAfter}`);
262+
const retryAfterMs = parseInt(retryAfter) * 1000;
263+
if (retryAfterMs > 0 && retryAfterMs < this.requestMaxDelay) {
264+
this.requestDelay = delay(retryAfterMs);
265+
}
266+
}
267+
}
248268
}
249269

250270
type ReleaseLookupConstructor = new (

0 commit comments

Comments
 (0)