symfony/thanks · error · TransportException
errors
Error message
errors
What it means
The GitHub GraphQL API responded with one or more top-level errors and no partial 'data' payload, so call() throws a TransportException whose message is the first GraphQL error message returned by GitHub. This means the whole query failed server-side rather than individual fields being null; per-field errors with a 'path' would instead be recorded in $failures and stripped from the data. The library treats this as a transport-level abort because nothing usable was returned.
Solutions
- Check your GitHub token is set and valid (GITHUB_TOKEN or COMPOSER_GITHUB_TOKEN env vars) and re-authenticate if expired
- Retry later if rate-limited; GraphQL rate limits reset hourly — reduce query volume or raise the limit by authenticating
- Verify network/proxy access to api.github.com and retry on transient 5xx failures
- Update the composer plugin/library to the latest version in case the GraphQL query is outdated
Example fix
// before: run with no token $ composer outdated --direct # -> TransportException: Bad credentials // after $ export GITHUB_TOKEN=ghp_xxx # or configure github.oauth in COMPOSER_HOME/auth.json $ composer outdated --direct
Defensive patterns
Strategy: try-catch
Validate before calling
if (!getenv('GITHUB_TOKEN') && !getenv('COMPOSER_GITHUB_TOKEN')) {
fwrite(STDERR, "No GitHub token configured; GraphQL calls may fail with 'Bad credentials'.\n");
} Type guard
function hasUsableGraphqlPayload(?array $result): bool
{
return $result !== null
&& isset($result['data'])
&& !isset($result['errors'][0]['message']) === false;
} Try / catch
try {
$repos = $client->getRepositories($urls);
} catch (Composer\Downloader\TransportException $e) {
$msg = $e->getMessage();
if (str_contains($msg, 'API rate limit exceeded')) {
// wait for rate-limit reset, then retry
} elseif (str_contains($msg, 'Bad credentials')) {
// prompt user to refresh their GitHub token
} else {
throw $e;
}
} Prevention
- Always configure a GitHub token (GITHUB_TOKEN env var or auth.json) before running queries
- Keep queries chunked (as processChunks does) to stay under GraphQL complexity/rate limits
- Check X-RateLimit-Remaining headers / GH rate-limit API before large batches
- Handle partial results via the $failures array instead of treating any error as fatal
When it happens
Trigger: Any call() to https://api.github.com/graphql (via getRepositories -> processChunks) where the JSON response contains result['errors'][0]['message'] but no result['data']: malformed GraphQL query, authentication failure (bad/expired GitHub token), rate limiting, or GitHub returning a top-level error object.
Common situations: Running composer-funding lookups with an unset or revoked GITHUB_TOKEN, hitting the GraphQL rate limit after many chunked queries, GitHub API 5xx responses serialized as an errors payload, or a library/version mismatch producing an invalid query shape.
AI-assisted analysis of symfony/thanks@f455cc9ba4 (2026-09-13).
Data as JSON: /api/errors/b00109c0e33c5e4f.
Report an issue: GitHub.
Appendix: source
Thrown at src/GitHubClient.php:180
$options = [
'http' => [
'method' => 'POST',
'content' => json_encode(['query' => $graphql]),
'header' => ['Content-Type: application/json'],
],
];
if ($this->rfs instanceof HttpDownloader) {
$result = $this->rfs->get('https://api.github.com/graphql', $options)->getBody();
} else {
$result = $this->rfs->getContents('github.com', 'https://api.github.com/graphql', false, $options);
}
$result = json_decode($result, true);
if (isset($result['errors'][0]['message'])) {
if (!isset($result['data'])) {
throw new TransportException($result['errors'][0]['message']);
}
foreach ($result['errors'] as $error) {
if (!isset($error['path'])) {
$failures[isset($error['type']) ? $error['type'] : $error['message']] = $error['message'];
continue;
}
foreach ($error['path'] as $path) {
$failures += [$path => $error['message']];
unset($result['data'][$path]);
}
}
}
return isset($result['data']) ? $result['data'] : [];
}
View on GitHub (pinned to f455cc9ba4)