{"record":{"id":"62aa577f856bebd2","repo":"can1357/oh-my-pi","slug":"tinyfish-search-api-returned-an-unexpected-respons","errorCode":null,"errorMessage":"TinyFish Search API returned an unexpected response shape","messagePattern":"TinyFish Search API returned an unexpected response shape","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/web/search/providers/tinyfish.ts","lineNumber":192,"sourceCode":"\t\tconst excludeDomains = siteHosts(parsed.excludedSites);\n\t\tif (includeDomains.length > 0) tinyFishParams.include_domains = includeDomains;\n\t\tif (excludeDomains.length > 0) tinyFishParams.exclude_domains = excludeDomains;\n\t}\n\tconst { location, language } = tinyFishLocale(parsed.lang);\n\tif (location) tinyFishParams.location = location;\n\tif (language) tinyFishParams.language = language;\n\tconst keyOrResolver: ApiKey = params.authStorage.resolver(\"tinyfish\", {\n\t\tsessionId: params.sessionId,\n\t});\n\tconst sources = await withAuth(\n\t\tkeyOrResolver,\n\t\tasync key => {\n\t\t\tconst collected: SearchSource[] = [];\n\t\t\tconst seenUrls = new Set<string>();\n\t\t\tfor (let page = 0; page <= MAX_PAGE && collected.length < numResults; page += 1) {\n\t\t\t\tconst searchPage = await callTinyFishSearch(key, { ...tinyFishParams, page });\n\t\t\t\tif (!Array.isArray(searchPage.results)) {\n\t\t\t\t\tthrow new Error(\"TinyFish Search API returned an unexpected response shape\");\n\t\t\t\t}\n\t\t\t\tappendTinyFishSources(collected, searchPage.results, seenUrls);\n\t\t\t\tif (searchPage.results.length < pageSize) break;\n\t\t\t}\n\n\t\t\treturn collected.slice(0, numResults);\n\t\t},\n\t\t{\n\t\t\tsignal: params.signal,\n\t\t\tmissingKeyMessage:\n\t\t\t\t'TinyFish credentials not found. Set TINYFISH_API_KEY or configure an API key for provider \"tinyfish\".',\n\t\t},\n\t);\n\n\treturn {\n\t\tprovider: \"tinyfish\",\n\t\tsources,\n\t\tauthMode: \"api_key\",","sourceCodeStart":174,"sourceCodeEnd":210,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/web/search/providers/tinyfish.ts#L174-L210","documentation":"Thrown inside searchTinyFish when the TinyFish Search API's paginated response lacks a `results` array (searchPage.results is not an Array). This is a plain Error, indicating the API response shape does not match the expected contract, which usually means the API version changed, the request hit a proxy/gateway returning an error body with 200 status, or the key/endpoint is misrouted. The library validates defensively instead of crashing on `results.length` etc.","triggerScenarios":"callTinyFishSearch resolves but searchPage.results is undefined/null/not an array — e.g. API returns `{error: ...}` or `{data: [...]}` with HTTP 200, an auth gateway returns HTML/JSON without `results`, or the TinyFish API contract changed.","commonSituations":"Expired or free-tier key hitting a fallback endpoint that returns a JSON error body with 200; corporate proxy or mock server rewriting responses; TinyFish API schema update (results renamed/moved); pointing at a mirror URL that serves a different schema.","solutions":["Log the full raw response body from callTinyFishSearch to see what the API actually returned and why `results` is missing","Verify TINYFISH_API_KEY is valid and not rate-limited/expired, and re-test with curl against the official TinyFish endpoint","Remove or correct any custom base URL/proxy for the tinyfish provider so requests hit the official API","Pin or update the provider integration to the current TinyFish API schema (check for renamed `results` field or envelope like {data:[...]})","Wrap the loop in retry/fallback to another search provider when this shape error occurs"],"exampleFix":"// before (crash on shape mismatch)\nif (!Array.isArray(searchPage.results)) {\n  throw new Error(\"TinyFish Search API returned an unexpected response shape\");\n}\n// after (tolerate envelope variant + log payload)\nconst raw = searchPage.results ?? (searchPage as any).data;\nif (!Array.isArray(raw)) {\n  logger.error(\"TinyFish unexpected response\", { body: searchPage });\n  throw new Error(`TinyFish Search API returned an unexpected response shape: ${JSON.stringify(searchPage).slice(0, 500)}`);\n}","handlingStrategy":"validation","validationCode":"function hasTinyFishResults(res: unknown): res is { results: unknown[] } {\n  return typeof res === \"object\" && res !== null && Array.isArray((res as { results?: unknown }).results);\n}\n// call before consuming: if (!hasTinyFishResults(searchPage)) fallback();","typeGuard":"const isTinyFishPage = (p: unknown): p is { results: SearchSource[] } =>\n  typeof p === \"object\" && p !== null && Array.isArray((p as { results?: unknown }).results);","tryCatchPattern":"try {\n  results = await searchTinyFish(params);\n} catch (err) {\n  if (err instanceof Error && err.message.includes(\"unexpected response shape\")) {\n    results = await fallbackProvider.search(params); // or log + retry\n  } else throw err;\n}","preventionTips":["Pin/monitor the TinyFish API contract and add a CI smoke test asserting `results` is an array","Use the official endpoint — avoid proxies/gateways that can rewrite the 200 response body","Keep TINYFISH_API_KEY valid so you never fall through to fallback endpoints with different schemas","Log the raw response body once on shape failure for fast diagnosis"],"tags":["api-response-shape","search-provider","tinyfish","schema-validation-failed"],"backgroundTag":"schema-validation-failed","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}