{"record":{"id":"21965b7ce95f876d","repo":"dubinc/dub","slug":"failed-to-fetch-user-info-from-dub","errorCode":null,"errorMessage":"Failed to fetch user info from Dub.","messagePattern":"Failed to fetch user info from Dub\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/stripe-app/src/utils/oauth.ts","lineNumber":80,"sourceCode":"\n    return data as Token;\n  } catch (e) {\n    console.error(\"Unable to retrieve Dub access token:\", (e as Error).message);\n  }\n}\n\n// Returns the user info from Dub using the access token\nexport async function getUserInfo({ token }: { token: Token }) {\n  const response = await fetch(`${DUB_API_HOST}/oauth/userinfo`, {\n    headers: {\n      Authorization: `Bearer ${token.access_token}`,\n    },\n  });\n\n  const data = await response.json();\n\n  if (!response.ok) {\n    throw new Error(\"Failed to fetch user info from Dub.\", {\n      cause: data.error,\n    });\n  }\n\n  const { workspace } = data as { workspace: Workspace };\n\n  return workspace;\n}\n\n// If the token is expired, it will refresh it\nexport async function getValidToken({ stripe }: { stripe: Stripe }) {\n  const token = await getSecret<Token>({\n    stripe,\n    name: \"dub_token\",\n  });\n\n  if (!token) {\n    throw new Error(\"Access token not found for the account.\");","sourceCodeStart":62,"sourceCodeEnd":98,"githubUrl":"https://github.com/dubinc/dub/blob/f216b94a24ca5a0a48c6543ee10392c9006c8b75/packages/stripe-app/src/utils/oauth.ts#L62-L98","documentation":"getUserInfo calls Dub's /workspaces endpoint with the stored access token to fetch the user's workspace; if the response is not OK it throws this fixed message with the API error attached as `cause`. It is used to validate that a stored token still works before returning it from getValidToken.","triggerScenarios":"getValidToken → getUserInfo with an access token that the Dub API rejects (revoked, expired, or malformed) or when the workspace fetch request otherwise fails (network/5xx).","commonSituations":"User revoked Dub's access from their Dub workspace settings; token fetched from Stripe secrets was saved under an older OAuth scope; Dub API incident during a Stripe App session/token call.","solutions":["Inspect error.cause for the API status/message (typically 401 unauthorized).","Trigger token refresh: getValidToken already catches this and calls refreshToken — ensure that path runs.","If refresh also fails, have the user reconnect Dub from the Stripe App to issue a new token.","Check Dub API status if the cause indicates 5xx/network issues."],"exampleFix":"// before\n// token assumed valid every call\n// after\ntry { await getUserInfo({ token }); } catch { token = await refreshToken({ token }); }","handlingStrategy":"retry","validationCode":"const token = await getSecret({ stripe, name: \"dub_token\" });\nif (!token?.access_token) throw new Error(\"no Dub token stored; run the connect flow first\");","typeGuard":"function isWorkspacePayload(data: unknown): data is { workspace: { id: string } } {\n  return typeof data === \"object\" && data !== null && \"workspace\" in data && typeof (data as any).workspace?.id === \"string\";\n}","tryCatchPattern":"try {\n  const info = await getUserInfo({ token });\n} catch (e) {\n  if (e instanceof Error && e.cause && /unauthorized|401/i.test(JSON.stringify(e.cause))) {\n    const fresh = await refreshToken({ token });\n    return getUserInfo({ token: fresh });\n  }\n  if (e instanceof Error && e.cause && /5\\d\\d/.test(JSON.stringify(e.cause))) {\n    await sleep(1000); return getUserInfo({ token }); // retry transient\n  }\n  throw e;\n}","preventionTips":["Always route token use through getValidToken so invalid tokens trigger refresh.","Treat 401 as refresh-then-reconnect, not fatal.","Retry only 5xx/network causes; 401/403 need new credentials.","Surface reconnect instructions to the user when refresh also fails."],"tags":["api","oauth","stripe","token-validation"],"backgroundTag":"oauth-token-invalid","analyzedSha":"f216b94a24ca5a0a48c6543ee10392c9006c8b75","analyzedAt":"2026-08-31T18:35:50.395Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}