{"record":{"id":"f261d4384c2c256a","repo":"koala73/worldmonitor","slug":"relay-returned-resp-status","errorCode":null,"errorMessage":"relay returned ${resp.status}","messagePattern":"relay returned (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"server/worldmonitor/aviation/v1/search-google-dates.ts","lineNumber":62,"sourceCode":"    sort_by_price: String(req.sortByPrice ?? false),\n    passengers: String(passengers),\n  });\n  for (const airline of airlines) {\n    params.append('airlines', airline);\n  }\n\n  const cacheKey = `aviation:gf-dates:${origin}:${destination}:${startDate}:${endDate}:${params.toString()}:v1`;\n\n  try {\n    const data = await cachedFetchJson<{ dates: unknown[]; partial?: boolean }>(\n      cacheKey,\n      CACHE_TTL,\n      async () => {\n        const resp = await fetch(`${relayBaseUrl}/google-flights/search-dates?${params}`, {\n          headers: getRelayHeaders(),\n          signal: AbortSignal.timeout(30_000),\n        });\n        if (!resp.ok) throw new Error(`relay returned ${resp.status}`);\n        const json = (await resp.json()) as { dates?: unknown[]; partial?: boolean; error?: string };\n        if (!Array.isArray(json.dates)) throw new Error(json.error ?? 'no results');\n        return { dates: json.dates, partial: json.partial };\n      },\n    );\n\n    if (!data) {\n      return { dates: [], degraded: true, error: 'no results' };\n    }\n\n    return {\n      dates: data.dates as SearchGoogleDatesResponse['dates'],\n      degraded: data.partial === true,\n      error: data.partial === true ? 'partial results: one or more date chunks failed' : '',\n    };\n  } catch (err) {\n    return { dates: [], degraded: true, error: err instanceof Error ? err.message : 'search failed' };\n  }","sourceCodeStart":44,"sourceCodeEnd":80,"githubUrl":"https://github.com/koala73/worldmonitor/blob/9361220cc013571781071f0206e4d80fd14b2f7f/server/worldmonitor/aviation/v1/search-google-dates.ts#L44-L80","documentation":"Inside the cachedFetchJson producer of searchGoogleDates, a non-OK response from the relay service (/google-flights/search-dates, 30s timeout) throws this Error. The handler's outer catch converts it into a degraded response {dates: [], degraded: true, error: 'relay returned <status>'}, so the RPC client sees HTTP 200 with degraded=true rather than a thrown error; failed fetches are not cached.","triggerScenarios":"Relay VM restarting or down (502/503); relay rejects the auth headers from getRelayHeaders() (401/403 — stale relay secret); relay rate-limits or blocks the request (429); Cloudflare/WAF in front of the relay rejecting the user-agent.","commonSituations":"Relay secret rotated but the server env (RELAY_* variables consumed by _shared/relay.ts) not updated; relay host under load during fare-search bursts; relay deployment mid-restart; network path between the Edge runtime and the relay failing.","solutions":["Check the response's degraded/error fields — do not treat an empty dates array as 'no flights'","Verify the relay env config (base URL and shared headers via getRelayBaseUrl()/getRelayHeaders) matches the deployed relay","Inspect relay service logs for the matching status code (401 → secret mismatch, 429 → throttling, 5xx → relay health)","Retry after a backoff window; nothing is cached on failure so a later attempt re-hits the relay"],"exampleFix":"// before — empty array read as 'no flights on those dates'\nconst { dates } = await client.searchGoogleDates(req);\nif (dates.length === 0) showNoFlights();\n\n// after — degraded flag distinguishes upstream failure from a true empty result\nconst res = await client.searchGoogleDates(req);\nif (res.degraded) showRetryableError(res.error); // 'relay returned 502'\nelse if (res.dates.length === 0) showNoFlights();","handlingStrategy":"fallback","validationCode":null,"typeGuard":"interface DatesResult { dates: unknown[]; degraded: boolean; error: string }\nfunction isDegradedDates(r: DatesResult): boolean {\n  return r.degraded === true;\n}","tryCatchPattern":"// No throw escapes this handler — it converts relay failures to degraded responses.\nconst res = await searchGoogleDates(req);\nif (res.degraded) {\n  if (res.error.startsWith('relay returned')) return retryLater(res.error); // upstream status embedded\n  return showEmptyState(res.error);\n}","preventionTips":["Always branch on the degraded flag before interpreting dates.length","Keep relay credentials (RELAY_* env) in the same rotation pipeline as other upstream secrets","Retry degraded relay responses with backoff — failures are not cached, so the next call re-hits the relay"],"tags":["aviation","relay","upstream","degraded","network"],"backgroundTag":"upstream-http-error","analyzedSha":"9361220cc013571781071f0206e4d80fd14b2f7f","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}