Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,27 @@ import { clearAccessToken } from "./auth"
const REQUEST_RETRY_BUFFER = 1000
const MAX_RATE_LIMIT_RETRIES = 2 // 3 attempts in total
const MAX_ERROR_RETRIES = 2 // 3 attempts in total
const MAX_NETWORK_RETRIES = 5 // 6 attempts in total — throttle recovery on large playlists
const NETWORK_RETRY_BASE = 1000 // First backoff delay; doubles each subsequent retry
const limiter = new Bottleneck({
maxConcurrent: 1,
minTime: 0
})

limiter.on("failed", async (error, jobInfo) => {
// A rate-limited (429) response arrives without CORS headers, so the browser blocks it and
// surfaces only an opaque network error (ERR_NETWORK) with no `error.response`. That means
// we can't read the 429's `Retry-After` to honour Spotify's requested wait. As a fallback,
// apply our own exponential backoff — Spotify's own guidance when rate limited is to slow
// down and retry. If the errors persist through the whole backoff cycle we stop retrying and
// let the error propagate (which aborts the export) rather than hanging indefinitely.
if (error.response == null) {
if (jobInfo.retryCount < MAX_NETWORK_RETRIES) {
return NETWORK_RETRY_BASE * Math.pow(2, jobInfo.retryCount) // 1s, 2s, 4s, 8s, 16s
}
return undefined
}

if (error.response.status === 429 && jobInfo.retryCount < MAX_RATE_LIMIT_RETRIES) {
// Retry according to the indication from the server with a small buffer
return ((error.response.headers["retry-after"] || 1) * 1000) + REQUEST_RETRY_BUFFER
Expand Down
Loading