{"record":{"id":"b8708b3943389843","repo":"decolua/9router","slug":"google-programmable-search-requires-both-apikey-an","errorCode":null,"errorMessage":"Google Programmable Search requires both apiKey and cx","messagePattern":"Google Programmable Search requires both apiKey and cx","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"open-sse/handlers/search/callers.js","lineNumber":199,"sourceCode":"  };\n  if (includes.length) body.include_domains = includes;\n  if (excludes.length) body.exclude_domains = excludes;\n  if (params.country) body.country = params.country;\n  return {\n    url: resolveBaseUrl(config, params),\n    init: {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${params.token}` },\n      body: JSON.stringify(body),\n    },\n  };\n}\n\nfunction buildGooglePseRequest(config, params) {\n  const apiKey = params.token;\n  const cx = getProviderSetting(params, \"cx\");\n  if (!apiKey || !cx) {\n    throw new Error(\"Google Programmable Search requires both apiKey and cx\");\n  }\n  const qp = new URLSearchParams({\n    key: apiKey,\n    cx,\n    q: params.query,\n    num: String(Math.min(params.maxResults, 10)),\n  });\n  if (params.country) qp.set(\"gl\", params.country.toLowerCase());\n  if (params.language) qp.set(\"hl\", params.language);\n  if (params.timeRange && params.timeRange !== \"any\") {\n    const dateRestrictMap = { day: \"d1\", week: \"w1\", month: \"m1\", year: \"y1\" };\n    const dateRestrict = dateRestrictMap[params.timeRange];\n    if (dateRestrict) qp.set(\"dateRestrict\", dateRestrict);\n  }\n  if (typeof params.offset === \"number\" && params.offset > 0) {\n    qp.set(\"start\", String(Math.min(params.offset + 1, 91)));\n  }\n  return {","sourceCodeStart":181,"sourceCodeEnd":217,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/open-sse/handlers/search/callers.js#L181-L217","documentation":"buildGooglePseRequest requires two credentials before it can construct a Google Programmable Search (Custom Search JSON API) request: an API key (`params.token`) and the search-engine ID `cx` (read via getProviderSetting from providerOptions/providerSpecificData). If either is missing or empty, the builder throws instead of sending a doomed request — Google PSE cannot work without both.","triggerScenarios":"A search request routed to the google-pse provider where params.token is absent (no apiKey passed) or getProviderSetting(params, \"cx\") returns undefined — i.e. neither providerOptions.cx nor providerSpecificData.cx contains a non-empty trimmed string.","commonSituations":"Developer created a Programmable Search Engine but forgot to enable the Custom Search JSON API key; cx configured under the wrong key name (e.g. \"engineId\" or \"searchEngineId\" instead of \"cx\"); token not forwarded by the calling handler for this provider; swapped credentials — putting the API key in cx or vice versa; whitespace-only cx value (trimmed to empty).","solutions":["Pass the Google API key as the request token (params.token / apiKey field for the provider) and confirm it's not empty","Set cx in providerOptions: { cx: \"<search-engine-id-from-programmable-search>\" } (or providerSpecificData.cx) — the 17+ char engine ID like \"017576662512468239146:omuauf_lfve\"","Check the key name is exactly \"cx\" — other names like engineId are ignored by getProviderSetting","Verify the API key has the Custom Search JSON API enabled in Google Cloud console and belongs to the same project as the search engine"],"exampleFix":"// before — missing cx\nconst params = { query: \"rust async\", token: process.env.GOOGLE_API_KEY };\n// after\nconst params = {\n  query: \"rust async\",\n  token: process.env.GOOGLE_API_KEY,\n  providerOptions: { cx: process.env.GOOGLE_PSE_CX },\n};","handlingStrategy":"validation","validationCode":"function validateGooglePseParams(params) {\n  const apiKey = params.token;\n  const cx = params.providerOptions?.cx ?? params.providerSpecificData?.cx;\n  if (!apiKey || typeof apiKey !== \"string\") throw new Error(\"Google PSE: apiKey (token) is required\");\n  if (!cx || typeof cx !== \"string\" || !cx.trim()) throw new Error(\"Google PSE: cx (search engine ID) is required in providerOptions\");\n}\n// call before issuing the search request","typeGuard":"function hasGooglePseCreds(params) {\n  const cx = params?.providerOptions?.cx ?? params?.providerSpecificData?.cx;\n  return typeof params?.token === \"string\" && params.token.length > 0 &&\n         typeof cx === \"string\" && cx.trim().length > 0;\n}","tryCatchPattern":"try {\n  const result = await search({ provider: \"google-pse\", query, token: GOOGLE_API_KEY, providerOptions: { cx: GOOGLE_CX } });\n} catch (err) {\n  if (err.message.includes(\"requires both apiKey and cx\")) {\n    // configuration error, not transient — fail fast, surface which credential is missing\n    // do not retry; check token and providerOptions.cx\n  } else {\n    throw err;\n  }\n}","preventionTips":["Store both GOOGLE_API_KEY and GOOGLE_PSE_CX together; validate the pair at startup","Use the exact key name \"cx\" in providerOptions — other names are silently ignored","Verify the API key has Custom Search JSON API enabled in Google Cloud before deploying"],"tags":["google-pse","search","missing-credentials","configuration"],"backgroundTag":"missing-api-key","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}