[GitHub] Propagate fetch errors instead of swallowing them - #4119
Jan-Kazlouski-elastic merged 10 commits into
Conversation
The GitHub connector previously logged a warning and silently continued whenever fetching repos, pull requests, issues, or files failed, so a sync could "succeed" while syncing nothing. It also caught bare `except Exception` in several places. - Replace every bare `except Exception` with `gidgethub.GitHubException` (plus the connector's own Unauthorized/Forbidden exceptions where the block only logs and re-raises). - Add a dedicated `RateLimitingError`; `_put_to_sleep` now only sleeps and its callers re-raise `RateLimitingError`, letting the retry layer handle transient rate limiting. - Stop swallowing failures in `_fetch_repos`/`_fetch_pull_requests`/ `_fetch_issues`/`_fetch_files`/`_fetch_files_by_path`. Page/API-level failures now propagate and fail the sync, while a single bad document is WARN-logged and skipped (per-item `gidgethub.GitHubException` guard). Also fixes a latent test-isolation bug where shared mock fixtures were mutated across tests and the resulting error was hidden by the old catch-all.
…hub-connector-swallowing-errors
GitHub can return null for GraphQL connection fields (reviews, comments,
labels, assignees, reviewRequests, tree, edge nodes). The previous
`x.get("key", {}).get(...)` pattern crashes with AttributeError/TypeError
when the key is present but its value is null, because .get() only uses
the default when the key is absent. A single such document would abort
the whole sync.
Apply the same `... or {}` / `... or []` null-safe idiom already used for
`author` in _prepare_review_doc across the fetch/prepare helpers, and add
regression tests for issues and pull requests with null connection fields.
Co-authored-by: Cursor <cursoragent@cursor.com>
| field_type=field, | ||
| ) | ||
| issue.pop(field) | ||
| except gidgethub.GitHubException as exception: |
There was a problem hiding this comment.
@artem-shelkovnikov
Something worth flagging is that this will now catch only GitHubException and write a warning for them without failing the whole sync, but any non-GitHubException will now fail the whole sync.
I agree that we shouldn't be catching General Exception, but that is a risk.
There was a problem hiding this comment.
Did you do any testing in regards how errors are raised? I remember this connector was tricky because I believe under the hood the github library uses aiohttp and with certain usage of the library it raises aiohttp errors instead of its own ones.
I think we've fixed it at some point and only gidgethub errors are now exposed, but might be worth checking - like just configuring connector with wrong config, then with wrong repo name and see if the error is correctly propagated.
What do you think?
There was a problem hiding this comment.
Tested end-to-end against a local mock and live api.github.com.
gidgethub only maps HTTP status codes to GitHubExceptions; pre-response
failures (DNS, refused, TLS, timeout) come from aiohttp and aren't
GitHubExceptions.
- Bad token (local + real api) →
UnauthorizedException✅ - Wrong repo → 404 →
gidgethub.BadRequest, propagates ✅ - Page-level 500 →
gidgethub.GitHubBroken, propagates ✅ - Per-item 500 → WARN + skipped ✅
- Dead port →
aiohttp.ClientConnectorError(not a GitHubException)
So errors propagate correctly. Transport errors dodge the per-item
guard, but @retryable (3×) absorbs blips and a persistent outage should fail
the sync. Extending guard to aiohttp errors would
reintroduce #2395 (skip every doc, sync "succeeds" empty).
…) (#4135) Backports the following commits to 9.4: - [GitHub] Propagate fetch errors instead of swallowing them (#4119) Co-authored-by: Jan-Kazlouski-elastic <jan.kazlouski@elastic.co> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
) (#4136) ## Summary Manual backport of #4119 to `8.19`. The GitHub connector previously wrapped each fetch method in `try/except Exception: log warning`, so a sync could "succeed" while syncing nothing (e.g. if no issues could be fetched at all). This ports the two-tier error handling, removes the broad exception handling, and hardens null-handling of GraphQL responses. The automated backport could not cherry-pick because `main` has since split the GitHub connector into a package (`app/connectors_service/connectors/sources/github/{client,datasource,utils}.py`), whereas `8.19` still uses the single module `connectors/sources/github.py`. The changes were therefore ported by hand into that single module. ## Changes - **Two-tier error handling**: page/API-level failures propagate out of `_fetch_repos` / `_fetch_pull_requests` / `_fetch_issues` / `_fetch_files` / `_fetch_files_by_path` (so the sync fails when data genuinely can't be fetched), while per-document enrichment failures are WARN-logged and skipped via narrow `gidgethub.GitHubException` guards. - **Transient rate limiting**: added `RateLimitingError`. `_put_to_sleep` now only sleeps; its callers (`get_github_item`, `graphql`, `_update_installation_access_token`, `_github_app_get`) re-raise `RateLimitingError`, which is not in `skipped_exceptions`, so the existing `@retryable` layer retries it. - **No more bare `except Exception`**: replaced with `gidgethub.GitHubException` (plus the connector's own `UnauthorizedException` / `ForbiddenException` in log-and-reraise spots like `ping` and `_get_invalid_repos_for_personal_access_token`). - **Null-safe GraphQL handling**: switched connection-field access (`reviews`, `comments`, `labels`, `assignees`, `reviewRequests`, `tree`, edge nodes) to the `... or {}` / `... or []` idiom so a single document with `null` connection fields can no longer abort the sync. - **Tests**: ported coverage for propagation vs. per-item skip for each fetch method, `RateLimitingError` propagation, null-connection-field regressions, and the `deepcopy` test-isolation fixes. ## Test plan - `make clean install autoformat lint test PYTHON=python3.11` — lint clean; `tests/sources/test_github.py` passes (121 passed), overall coverage 92.35%. - The only failures in the full suite are pre-existing `pytest-randomly` ordering flakiness in `test_google_drive.py` / `test_google_cloud_storage.py` (they pass in isolation and with a deterministic order); they are unrelated to these GitHub-only changes. Part of #2393, closes #2395 for 8.19. Made with [Cursor](https://cursor.com) Co-authored-by: Cursor <cursoragent@cursor.com>
…) (#4134) Backports the following commits to 9.3: - [GitHub] Propagate fetch errors instead of swallowing them (#4119) Co-authored-by: Jan-Kazlouski-elastic <jan.kazlouski@elastic.co> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Summary
Fixes the GitHub connector silently swallowing errors during sync. Previously each fetch method wrapped its whole body in
try/except Exception: log warning, so a sync could "succeed" while syncing nothing (e.g. if no issues could be fetched at all). This implements the two-tier error handling requested in the issue, removes the broad exception handling, and hardens null-handling of GraphQL responses.Part of #2393, closes #2395.
Changes
Two-tier error handling (
datasource.py)_fetch_repos/_fetch_pull_requests/_fetch_issues/_fetch_files/_fetch_files_by_path, so the sync fails when data genuinely can't be fetched.gidgethub.GitHubException, so a single bad issue/PR/file no longer aborts the whole sync.Transient rate limiting
_put_to_sleepnow only sleeps; its callers re-raise a dedicatedRateLimitingError, which is not inskipped_exceptions, so the existing@retryablelayer retries it.No more bare
except Exceptiongithub/package withgidgethub.GitHubException(plus the connector's ownUnauthorizedException/ForbiddenExceptionin log-and-reraise spots likepingand_get_invalid_repos_for_personal_access_token).Null-safe GraphQL response handling
nullfor connection fields (reviews,comments,labels,assignees,reviewRequests,tree, edge nodes). The oldx.get("key", {}).get(...)pattern crashes withAttributeError/TypeErrorwhen the key is present but its value isnull(.get()only applies the default when the key is absent). Switched the fetch/prepare helpers to the... or {}/... or []idiom so a single such document can no longer abort the sync.Tests
RateLimitingErrorpropagation, and regression tests for issues/PRs with null connection fields.deepcopy).Acceptance criteria
@retryable+RateLimitingError)Test plan
pytest tests/sources/test_github.py— 121 passed-p randomlyseeds 1/2/42/100/777)ruff checkclean on changed filesNotes for reviewers
_fetch_remaining_fields, which itself makes paginated calls. The design assumes systemic backend failures surface on the initial page fetch (which propagates and fails the sync). If a systemic error only manifested during per-item enrichment, it could be WARN-skipped for every document without failing the sync. Happy to add per-repo "synced at least one" tracking if we want a hard guarantee.raise Exception(msg)remains for non-rate-limited GraphQLQueryErrors ingraphql(); left as-is as a possible small follow-up.