Crosstalk-Solutions/project-nomad · error · Error

No token returned from ${registry}

Error message

No token returned from ${registry}

What it means

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.

Source

Thrown at admin/app/services/container_registry_service.ts:108

    if (registry === 'registry-1.docker.io') {
      tokenUrl = `https://auth.docker.io/token?service=registry.docker.io&scope=repository:${fullName}:pull`
    } else if (registry === 'ghcr.io') {
      tokenUrl = `https://ghcr.io/token?service=ghcr.io&scope=repository:${fullName}:pull`
    } else {
      // For other registries, try the standard v2 token endpoint
      tokenUrl = `https://${registry}/token?service=${registry}&scope=repository:${fullName}:pull`
    }

    const response = await this.fetchWithRetry(tokenUrl)
    if (!response.ok) {
      throw new Error(`Failed to get auth token from ${registry}: ${response.status}`)
    }

    const data = (await response.json()) as { token?: string; access_token?: string }
    const token = data.token || data.access_token || ''

    if (!token) {
      throw new Error(`No token returned from ${registry}`)
    }

    // Cache for 5 minutes (tokens usually last longer, but be conservative)
    this.tokenCache.set(cacheKey, {
      token,
      expiresAt: Date.now() + 5 * 60 * 1000,
    })

    return token
  }

  /**
   * List all tags for a given image from the registry.
   */
  async listTags(parsed: ParsedImageReference): Promise<string[]> {
    const token = await this.getToken(parsed.registry, parsed.fullName)
    const allTags: string[] = []
    let url = `https://${parsed.registry}/v2/${parsed.fullName}/tags/list?n=1000`

View on GitHub (pinned to 0bd1c6f4f9)

Solutions

  1. curl the token URL directly and inspect the JSON body to see what the registry actually returns
  2. If the repository is private, add credentials (Authorization header) to the token request or skip anonymous auth for that registry
  3. Verify the image name/scope string is exactly right (namespace/repo, correct tag/digest usage)
  4. If a proxy is mangling responses, whitelist the registry domain or configure the proxy env for fetchWithRetry
  5. Handle this error by falling back to unauthenticated manifest requests where the registry allows it

Example fix

// before
const token = data.token || data.access_token || ''
if (!token) {
  throw new Error(`No token returned from ${registry}`)
}

// after
const token = data.token || data.access_token || ''
if (!token) {
  logger.warn(`[ContainerRegistryService] Empty token body from ${registry}: ${JSON.stringify(data).slice(0, 200)}`)
  return '' // some registries allow anonymous pulls; let the manifest request decide
}
Defensive patterns

Strategy: fallback

Validate before calling

const res = await fetch(tokenUrl)
const body = await res.json().catch(() => null)
if (!body || !(body.token || body.access_token)) planAnonymousPull(registry) // no token path

Type guard

function hasRegistryToken(data: unknown): data is { token: string } | { access_token: string } {
  if (typeof data !== 'object' || data === null) return false
  const d = data as Record<string, unknown>
  return (typeof d.token === 'string' && d.token.length > 0) || (typeof d.access_token === 'string' && d.access_token.length > 0)
}

Try / catch

try { token = await svc.getToken(registry, repo) } catch (e) { if (e.message === `No token returned from ${registry}`) { token = ''; proceedAnonymousPull() } else throw e }

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of Crosstalk-Solutions/project-nomad@0bd1c6f4f9 (2026-08-27). Data as JSON: /api/errors/023abeb282cdfa60. Report an issue: GitHub.