{"record":{"id":"b0136e78fee0c14e","repo":"koala73/worldmonitor","slug":"imagery-search-rate-limited","errorCode":null,"errorMessage":"Imagery search rate limited","messagePattern":"Imagery search rate limited","errorType":"exception","errorClass":null,"httpStatus":429,"severity":"warning","filePath":"src/services/imagery.ts","lineNumber":16,"sourceCode":"import { toApiUrl } from '@/services/runtime';\nimport type { ImageryScene } from '@/generated/server/worldmonitor/imagery/v1/service_server';\n\nexport type { ImageryScene };\n\nexport interface ImagerySearchParams {\n  bbox: string;\n  datetime?: string;\n  source?: string;\n  limit?: number;\n}\n\nlet retryAfterUntil = 0;\n\nexport async function fetchImageryScenes(params: ImagerySearchParams): Promise<ImageryScene[]> {\n  if (Date.now() < retryAfterUntil) throw new Error('Imagery search rate limited');\n  const url = new URL(toApiUrl('/api/imagery/v1/search-imagery'), window.location.origin);\n  url.searchParams.set('bbox', params.bbox);\n  if (params.datetime) url.searchParams.set('datetime', params.datetime);\n  if (params.source) url.searchParams.set('source', params.source);\n  if (params.limit) url.searchParams.set('limit', String(params.limit));\n\n  const resp = await fetch(url.toString(), { signal: AbortSignal.timeout(15_000) });\n  if (!resp.ok) {\n    if (resp.status === 429) {\n      const retryAfter = resp.headers.get('Retry-After');\n      const seconds = retryAfter === null ? NaN : Number(retryAfter);\n      const deadline = Number.isFinite(seconds) ? Date.now() + Math.max(0, seconds) * 1000 : Date.parse(retryAfter ?? '');\n      retryAfterUntil = Number.isFinite(deadline) ? deadline : Date.now() + 60_000;\n    }\n    throw new Error(`Imagery search failed: ${resp.status}`);\n  }\n  const data = await resp.json();\n  return data.scenes ?? [];","sourceCodeStart":1,"sourceCodeEnd":34,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/src/services/imagery.ts#L1-L34","documentation":"fetchImageryScenes() keeps a module-level cooldown (retryAfterUntil) set when a previous request got HTTP 429. While that deadline is in the future, it throws immediately without a network call, honoring the API's Retry-After so clients stop hammering the imagery search endpoint.","triggerScenarios":"Calling fetchImageryScenes() (or the imagery scenes UI) after an earlier request returned 429 while the Retry-After cooldown window is still active.","commonSituations":"Auto-refreshing map panels polling imagery search faster than the rate limit; many components sharing the module-level limiter and one 429 blocks all of them; a server Retry-After of 60s (default) while a dashboard keeps retrying on a timer.","solutions":["Wait until the cooldown expires before calling again (catch the error and retry after a delay)","Reduce request frequency: cache scenes, back off polling intervals, or share one request across consumers","Inspect the Retry-After header of the prior 429 to know the exact window; honor it in your scheduler","If needed persistently, route requests through your own proxy with rate-limit budgeting"],"exampleFix":"// before\nconst scenes = await fetchImageryScenes(params);\n// after\nasync function fetchWithBackoff(params) {\n  try { return await fetchImageryScenes(params); }\n  catch (e) {\n    if (e.message === 'Imagery search rate limited') {\n      await new Promise(r => setTimeout(r, 30_000));\n      return fetchImageryScenes(params);\n    }\n    throw e;\n  }\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try { scenes = await fetchImageryScenes(p); } catch (e) { if (e.message === 'Imagery search rate limited') { await sleep(30_000); scenes = await fetchImageryScenes(p); } else throw e; }","preventionTips":["Throttle imagery polling below the API rate limit","Debounce/dedupe bbox-based searches across components","Honor Retry-After from 429 responses in your scheduler"],"tags":["rate-limit","imagery","http-429","backoff"],"backgroundTag":"rate-limit-exceeded","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-09-15T16:44:39.439Z","contentChangedAt":"2026-09-15T16:44:39.439Z","schemaVersion":2},"datasetVersion":"2026-09-15T18:17:12.389Z"}