Skip to content

Commit f4da6cd

Browse files
authored
fix(api): guard malformed GraphQL error responses (JhaSourav07#812)
## Description Fixes JhaSourav07#807 ## Pillar - [x] 🛠️ Other (Bug fix, refactoring, docs) ## What changed The GitHub GraphQL client assumed every GraphQL error response had a non-empty `errors` array with a `message` string at index `0`. That meant responses like: ```json { "errors": [] } ``` or: ```json { "errors": [{}] } ``` could throw a runtime `TypeError` instead of a controlled API error. I added a small guard that normalizes malformed GraphQL error payloads to a stable fallback message while preserving the existing behavior for normal GitHub errors such as `Bad credentials`. I also made the response `data` field optional in the local response type and changed the user lookup to use optional chaining so malformed responses do not crash while checking `data.user`. ## Visual Preview N/A — this is API error handling and test coverage only. ## Testing ```bash npm run format npm run test -- lib/github.test.ts npm run lint npm run typecheck npm run test npm run build ``` ## Checklist before requesting a review: - [x] I have read the `CONTRIBUTING.md` file. - [x] I have tested these changes locally (`localhost:3000/api/streak?user=YOUR_USERNAME`). - [x] I have run `npm run format` and `npm run lint` locally and resolved all errors. - [x] My commits follow the Conventional Commits format. - [x] I have updated `README.md` if I added a new theme or URL parameter. - [x] I have starred the repo. - [x] I have made sure that I have only one commit to merge in this PR. - [x] The SVG output matches the CommitPulse quality standard.
2 parents 909833d + c615487 commit f4da6cd

2 files changed

Lines changed: 49 additions & 6 deletions

File tree

lib/github.test.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,34 @@ describe('fetchGitHubContributions', () => {
135135
})
136136
);
137137

138-
// Only the first error surfaces — the source always reads errors[0].
138+
// Preserve the existing behavior of surfacing the first GitHub error message.
139139
await expect(fetchGitHubContributions('octocat')).rejects.toThrow('Bad credentials');
140140
});
141141

142+
it('throws a stable fallback when GraphQL returns an empty errors array', async () => {
143+
vi.mocked(fetch).mockResolvedValue(
144+
mockResponse({
145+
errors: [],
146+
})
147+
);
148+
149+
await expect(fetchGitHubContributions('octocat')).rejects.toThrow(
150+
'GitHub GraphQL API returned an unknown error'
151+
);
152+
});
153+
154+
it('throws a stable fallback when the first GraphQL error has no message', async () => {
155+
vi.mocked(fetch).mockResolvedValue(
156+
mockResponse({
157+
errors: [{}],
158+
})
159+
);
160+
161+
await expect(fetchGitHubContributions('octocat')).rejects.toThrow(
162+
'GitHub GraphQL API returned an unknown error'
163+
);
164+
});
165+
142166
it('throws a descriptive "user not found" error when the username does not exist on GitHub', async () => {
143167
vi.mocked(fetch).mockResolvedValue(mockResponse({ data: { user: null } }));
144168

lib/github.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,16 +82,35 @@ const GITHUB_GRAPHQL_URL = 'https://api.github.com/graphql';
8282
const GITHUB_REST_URL = 'https://api.github.com';
8383

8484
type GitHubContributionResponse = {
85-
data: {
85+
data?: {
8686
user: {
8787
contributionsCollection: {
8888
contributionCalendar: ContributionCalendar;
8989
};
9090
} | null;
9191
};
92-
errors?: Array<{ message: string }>;
92+
errors?: unknown;
9393
};
9494

95+
const UNKNOWN_GRAPHQL_ERROR_MESSAGE = 'GitHub GraphQL API returned an unknown error';
96+
97+
function getGraphQLErrorMessage(errors: unknown): string {
98+
if (!Array.isArray(errors)) return UNKNOWN_GRAPHQL_ERROR_MESSAGE;
99+
100+
const firstError = errors[0];
101+
if (
102+
firstError &&
103+
typeof firstError === 'object' &&
104+
'message' in firstError &&
105+
typeof firstError.message === 'string' &&
106+
firstError.message.trim() !== ''
107+
) {
108+
return firstError.message;
109+
}
110+
111+
return UNKNOWN_GRAPHQL_ERROR_MESSAGE;
112+
}
113+
95114
/**
96115
* Configuration options for GitHub API fetch requests.
97116
*/
@@ -264,11 +283,11 @@ export async function fetchGitHubContributions(
264283

265284
const data: GitHubContributionResponse = await res.json();
266285

267-
if (data.errors) {
268-
throw new Error(data.errors[0].message);
286+
if (data.errors !== undefined) {
287+
throw new Error(getGraphQLErrorMessage(data.errors));
269288
}
270289

271-
if (!data.data.user) {
290+
if (!data.data?.user) {
272291
throw new Error(`GitHub user "${username}" not found`);
273292
}
274293

0 commit comments

Comments
 (0)