{"record":{"id":"023abeb282cdfa60","repo":"Crosstalk-Solutions/project-nomad","slug":"no-token-returned-from-registry","errorCode":null,"errorMessage":"No token returned from ${registry}","messagePattern":"No token returned from (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"admin/app/services/container_registry_service.ts","lineNumber":108,"sourceCode":"    if (registry === 'registry-1.docker.io') {\n      tokenUrl = `https://auth.docker.io/token?service=registry.docker.io&scope=repository:${fullName}:pull`\n    } else if (registry === 'ghcr.io') {\n      tokenUrl = `https://ghcr.io/token?service=ghcr.io&scope=repository:${fullName}:pull`\n    } else {\n      // For other registries, try the standard v2 token endpoint\n      tokenUrl = `https://${registry}/token?service=${registry}&scope=repository:${fullName}:pull`\n    }\n\n    const response = await this.fetchWithRetry(tokenUrl)\n    if (!response.ok) {\n      throw new Error(`Failed to get auth token from ${registry}: ${response.status}`)\n    }\n\n    const data = (await response.json()) as { token?: string; access_token?: string }\n    const token = data.token || data.access_token || ''\n\n    if (!token) {\n      throw new Error(`No token returned from ${registry}`)\n    }\n\n    // Cache for 5 minutes (tokens usually last longer, but be conservative)\n    this.tokenCache.set(cacheKey, {\n      token,\n      expiresAt: Date.now() + 5 * 60 * 1000,\n    })\n\n    return token\n  }\n\n  /**\n   * List all tags for a given image from the registry.\n   */\n  async listTags(parsed: ParsedImageReference): Promise<string[]> {\n    const token = await this.getToken(parsed.registry, parsed.fullName)\n    const allTags: string[] = []\n    let url = `https://${parsed.registry}/v2/${parsed.fullName}/tags/list?n=1000`","sourceCodeStart":90,"sourceCodeEnd":126,"githubUrl":"https://github.com/Crosstalk-Solutions/project-nomad/blob/0bd1c6f4f9888d577fe232de06ac144bb8337131/admin/app/services/container_registry_service.ts#L90-L126","documentation":"Thrown by ContainerRegistryService.getToken when the registry's token endpoint responds HTTP 200 but the JSON body contains neither 'token' nor 'access_token' (or they're empty strings). The endpoint answered successfully but didn't grant an anonymous pull token for the requested scope.","triggerScenarios":"Requesting a pull token for a private repository without credentials — some registries return 200 with an empty/error JSON instead of a 401; a proxy or login page intercepting the request and returning HTML/JSON without token fields; or a mistyped repository scope.","commonSituations":"Listing/pulling private images anonymously through a UI that assumes public images, captive-portal or corporate proxies rewriting responses, registries whose token endpoint returns errors with 200 status, or malformed image names producing an invalid scope.","solutions":["curl the token URL directly and inspect the JSON body to see what the registry actually returns","If the repository is private, add credentials (Authorization header) to the token request or skip anonymous auth for that registry","Verify the image name/scope string is exactly right (namespace/repo, correct tag/digest usage)","If a proxy is mangling responses, whitelist the registry domain or configure the proxy env for fetchWithRetry","Handle this error by falling back to unauthenticated manifest requests where the registry allows it"],"exampleFix":"// before\nconst token = data.token || data.access_token || ''\nif (!token) {\n  throw new Error(`No token returned from ${registry}`)\n}\n\n// after\nconst token = data.token || data.access_token || ''\nif (!token) {\n  logger.warn(`[ContainerRegistryService] Empty token body from ${registry}: ${JSON.stringify(data).slice(0, 200)}`)\n  return '' // some registries allow anonymous pulls; let the manifest request decide\n}","handlingStrategy":"fallback","validationCode":"const res = await fetch(tokenUrl)\nconst body = await res.json().catch(() => null)\nif (!body || !(body.token || body.access_token)) planAnonymousPull(registry) // no token path","typeGuard":"function hasRegistryToken(data: unknown): data is { token: string } | { access_token: string } {\n  if (typeof data !== 'object' || data === null) return false\n  const d = data as Record<string, unknown>\n  return (typeof d.token === 'string' && d.token.length > 0) || (typeof d.access_token === 'string' && d.access_token.length > 0)\n}","tryCatchPattern":"try { token = await svc.getToken(registry, repo) } catch (e) { if (e.message === `No token returned from ${registry}`) { token = ''; proceedAnonymousPull() } else throw e }","preventionTips":["Fall back to anonymous (no Authorization header) manifest requests when the token body is empty","Validate image names before building the scope string","Log the raw token response body when fields are missing","Add credentials to the token request for private repositories"],"tags":["docker","registry","oauth-token","authentication","json"],"backgroundTag":"registry-auth-token-missing","analyzedSha":"0bd1c6f4f9888d577fe232de06ac144bb8337131","analyzedAt":"2026-08-27T05:34:15.424Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}