|
| 1 | +import { OctokitPlugin } from "@octokit/core/types" |
| 2 | + |
| 3 | +import { logTrace } from "~/log" |
| 4 | + |
| 5 | +// HTTP status code for permanent redirect |
| 6 | +const HTTP_PERMANENT_REDIRECT = 301 |
| 7 | + |
| 8 | +// Map to store permanent redirects. Key is original URL, value is new URL |
| 9 | +const redirectCache = new Map<string, string>() |
| 10 | + |
| 11 | +/** |
| 12 | + * Octokit plugin that caches 301 Moved Permanently redirects and automatically |
| 13 | + * applies them to subsequent requests to the same URL. 301 redirects count against the REST API limit so we want to minimize them by caching the new location and transparently applying it to future requests. |
| 14 | + * |
| 15 | + * This plugin uses octokit.hook.wrap to intercept all requests and: |
| 16 | + * 1. Checks if the current URL has a cached redirect before requesting |
| 17 | + * 2. Applies cached redirects transparently |
| 18 | + * 3. On 301 responses, caches the new location and retries the request |
| 19 | + * |
| 20 | + * @param octokit - The Octokit instance to wrap with redirect handling |
| 21 | + * |
| 22 | + * @link https://octokit.github.io/rest.js/v22/#plugins |
| 23 | + */ |
| 24 | +// eslint-disable-next-line @typescript-eslint/no-explicit-any |
| 25 | +export const cacheRedirect: OctokitPlugin = (octokit) => { |
| 26 | + // hook into the request lifecycle |
| 27 | + octokit.hook.wrap("request", async (request, options) => { |
| 28 | + const repoCacheKey = [options.owner || undefined, options.repo || undefined, options.url].join("-") |
| 29 | + |
| 30 | + const redirectCacheKey = [repoCacheKey, options.url].join("-") |
| 31 | + const redirectCacheHit = redirectCache.get(redirectCacheKey) |
| 32 | + if (redirectCacheHit) { |
| 33 | + octokit.log.debug(`Redirect cache hit for ${options.url} → ${redirectCacheHit} [${redirectCacheKey}]`) |
| 34 | + options.baseUrl = "" |
| 35 | + options.url = redirectCacheHit |
| 36 | + } |
| 37 | + |
| 38 | + let response: any = await request(options) |
| 39 | + if (response.status === HTTP_PERMANENT_REDIRECT) { |
| 40 | + const locationHeader = response.headers["location"] |
| 41 | + |
| 42 | + if (locationHeader) { |
| 43 | + redirectCache.set(redirectCacheKey, locationHeader) |
| 44 | + octokit.log.info( |
| 45 | + `↩️ Permanent Redirect Detected and Cached: ${options.url} → ${locationHeader} [${redirectCacheKey}]`, |
| 46 | + ) |
| 47 | + options.url = locationHeader |
| 48 | + response = await request(options) |
| 49 | + } |
| 50 | + } |
| 51 | + return response |
| 52 | + }) |
| 53 | +} |
| 54 | + |
| 55 | +/** |
| 56 | + * Clears all cached redirects. Useful for testing or resetting state. |
| 57 | + */ |
| 58 | +export function clearRedirectCache(): void { |
| 59 | + redirectCache.clear() |
| 60 | + logTrace("🗑️ Redirect cache cleared") |
| 61 | +} |
0 commit comments