Skip to content

fix(github): handle null GraphQL fields in fetch_sub_issues - #2785

Merged
IsmaelMartinez merged 3 commits into
The-PR-Agent:mainfrom
erkdgn:fix/sub-issues-null-graphql
Aug 26, 2026
Merged

fix(github): handle null GraphQL fields in fetch_sub_issues#2785
IsmaelMartinez merged 3 commits into
The-PR-Agent:mainfrom
erkdgn:fix/sub-issues-null-graphql

Conversation

@erkdgn

@erkdgn erkdgn commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Fixes #2782.

What

GithubProvider.fetch_sub_issues() crashed with AttributeError: 'NoneType' object has no attribute 'get' whenever GitHub's GraphQL API returned null for a node it could not resolve.

.get(key, {}) falls back to the default only when the key is missing. GraphQL returns the key present with a null value, so the chain produced None and the next .get() raised:

issue_id = response_json.get("data", {}).get("repository", {}).get("issue", {}).get("id")

Switching to or {} makes both cases — missing key and explicit null — fall back to a dict. Same treatment for data.node in the sub-issues response, where the two duplicated traversals are now computed once.

Why it matters

The if not issue_id: warning("Issue ID not found") branch directly below was already written to handle exactly this case — it was just unreachable, because the chain raised one line earlier. This PR does not add new behaviour; it lets the intended behaviour run.

Two things make it fire more often than a "malformed link" would suggest, as @IsmaelMartinez noted on the issue:

  • repository.issue is null whenever the number in an /issues/N link is a pull request — GitHub's web UI redirects /issues/N/pull/N, so such links look correct to authors.
  • GITHUB_TICKET_PATTERN expands a bare #N (fewer than five digits) to the same URL, so a description containing fixes #12 is enough.

With require_ticket_analysis_review=true by default, affected repositories log a full traceback on every command run. Observed continuously on 0.43.0 in a GitHub App deployment.

Tests

Added TestFetchSubIssuesNullGraphQLFields to tests/unittest/test_ticket_extraction_async.py (as suggested — test_fetching_sub_issues.py is entirely commented out because it made live API calls). The new tests are fake-based, no network.

The tests assert on log output rather than the return value, and this is deliberate: the broad except at the end of the method swallows the AttributeError, so fetch_sub_issues returns an empty set with and without the fix. Log output is the only observable difference.

Verified that they actually catch the regression — with the source change reverted:

3 failed, 1 passed
FAILED ...::test_null_issue_is_handled_without_traceback
FAILED ...::test_null_repository_is_handled_without_traceback
FAILED ...::test_null_node_in_sub_issues_response_is_handled_without_traceback

The fourth test is the happy path, which passes either way and guards against the fix breaking normal operation.

With the change applied:

tests/unittest/test_ticket_extraction_async.py .................................  33 passed
tests/unittest                                                                    2037 passed, 1 skipped, 1 xfailed

Possible follow-up (not included)

The null response carries an errors array explaining the cause, e.g. Could not resolve to an Issue with the number of 89. Surfacing that in the warning would make the log self-explanatory:

Issue ID not found for {issue_url}: Could not resolve to an Issue with the number of 89.

Happy to add it here or in a separate PR, whichever you prefer — I left it out to keep this one focused on the crash.

@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Fix null GraphQL nodes in GitHubProvider.fetch_sub_issues

🐞 Bug fix 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Prevent AttributeError when GitHub GraphQL returns explicit null nodes/fields
• Make existing "Issue ID not found" and "Invalid response structure" branches reachable
• Add fake-based tests asserting logs to catch the swallowed-exception regression
Diagram

graph TD
  A["GithubProvider.fetch_sub_issues"] --> B["GraphQL: issue lookup"] --> C{"issue_id present?"}
  C -->|no| D["Warn: Issue ID not found"]
  C -->|yes| E["GraphQL: sub-issues"] --> F{"subIssues present?"} --> G["Return sub-issue URLs"]
  F -->|no| H["Error: invalid structure"]
  subgraph Legend
    direction LR
    _fn(["Function"]) ~~~ _api{{"GraphQL API"}} ~~~ _dec{"Decision"}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Introduce a shared null-safe JSON accessor helper
  • ➕ Avoids repeated (x.get(...) or {}) patterns across providers
  • ➕ Improves readability and consistency for nested traversal
  • ➖ Adds indirection for a one-off fix
  • ➖ Requires deciding/standardizing helper behavior (None vs missing vs type mismatches)
2. Parse GraphQL responses into typed models (e.g., pydantic/dataclasses)
  • ➕ Centralizes validation and makes nullability explicit
  • ➕ Better error messages and safer downstream code
  • ➖ Significantly larger refactor and new dependency/boilerplate
  • ➖ Overkill for a narrow bug fix in a single method

Recommendation: Keep the current approach for this PR: replacing .get(..., {}) with or {} is the smallest change that correctly handles GraphQL’s explicit nulls and makes existing log-based handling reachable. If similar null-handling appears elsewhere, consider a small shared helper as a follow-up to reduce repetition.

Files changed (2) +142 / -4

Bug fix (1) +9 / -4
github_provider.pyMake GraphQL response traversal null-safe in fetch_sub_issues +9/-4

Make GraphQL response traversal null-safe in fetch_sub_issues

• Replaces chained '.get(key, {})' traversals with 'or {}' fallbacks so explicit GraphQL null values don’t produce 'None' and crash. Also computes 'sub_issues_data' once and reuses it for presence checks and node extraction.

pr_agent/git_providers/github_provider.py

Tests (1) +133 / -0
test_ticket_extraction_async.pyAdd unit tests for null GraphQL fields using faked requester + log capture +133/-0

Add unit tests for null GraphQL fields using faked requester + log capture

• Adds fake PyGithub requester/client to feed controlled GraphQL responses into 'fetch_sub_issues' without network calls. Captures loguru output to assert that null nodes take intended warning/error branches instead of hitting the broad exception handler, plus a happy-path test.

tests/unittest/test_ticket_extraction_async.py

@qodo-code-review

qodo-code-review Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (1) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Comments use narrative phrasing 📘 Rule violation ⚙ Maintainability ⭐ New
Description
The added explanatory comments state what the code does (Name matches... and here because...)
instead of using the imperative command style required for behavioral comments. Rewrite them as
direct instructions or remove the redundant explanation.
Code

tests/unittest/test_ticket_extraction_async.py[R699-700]

+        # Name matches the attribute the provider reads; it is not name-mangled
+        # here because it already starts with a single underscore.
Relevance

●●● Strong

Repo precedent accepts rewriting new test comments/docstrings to imperative style.

PR-#2703
PR-#2545

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance ID 2694688 requires inline comments describing behavior to use commands such as `Handle
timeout case`, rather than narrative descriptions. The changed comments describe the attribute name
and implementation rationale in declarative prose.

Rule 2694688: Docstrings and comments must use imperative phrasing
tests/unittest/test_ticket_extraction_async.py[699-700]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The newly added comments use narrative/descriptive phrasing rather than the required imperative style.

## Issue Context
Keep the explanation of the fake client's private requester attribute, but phrase behavioral guidance as a command or concise instruction.

## Fix Focus Areas
- tests/unittest/test_ticket_extraction_async.py[699-700]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. New docstrings use descriptive phrasing ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The added _FakeRequester class docstring begins with Mimics, a third-person descriptive verb
rather than the imperative phrasing required for new docstrings. This violates the repository's
documentation-style compliance rule.
Code

tests/unittest/test_ticket_extraction_async.py[684]

+    """Mimics PyGithub's private requester: ``requestJson`` -> (status, headers, body)."""
Relevance

●● Moderate

Recent precedent mixed: similar 'Tests...' descriptive docstring rejected (PR#2661), imperative
rewrite accepted (PR#2703).

PR-#2661
PR-#2703

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires newly added docstrings to use imperative mood. The added class docstring at
the cited line begins with Mimics, which is descriptive third-person phrasing.

Rule 2694688: Docstrings and comments must use imperative phrasing
tests/unittest/test_ticket_extraction_async.py[684-684]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new `_FakeRequester` docstring starts with descriptive third-person phrasing (`Mimics`) instead of an imperative verb.

## Issue Context
PR Compliance ID 2694688 requires newly added function and class docstrings to use imperative phrasing, such as `Mimic` or `Provide`.

## Fix Focus Areas
- tests/unittest/test_ticket_extraction_async.py[684-684]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


  • Author self-review: I have reviewed the code review findings, and addressed the relevant ones.

Grey Divider

Context sources
✅ Compliance rules (platform): 34 rules
Review mode: ⚖️ Balanced

Grey Divider

Tip of the day
💡 Did you know, you can start a comment with 'qodo' or '@qodo' to chat about any finding

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Previous reviews

Review updated until commit 6229134

Results up to commit a81f1e2 🚀 Fast


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 📜 Skill insights (0)


Remediation recommended
1. New docstrings use descriptive phrasing ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The added _FakeRequester class docstring begins with Mimics, a third-person descriptive verb
rather than the imperative phrasing required for new docstrings. This violates the repository's
documentation-style compliance rule.
Code

tests/unittest/test_ticket_extraction_async.py[684]

+    """Mimics PyGithub's private requester: ``requestJson`` -> (status, headers, body)."""
Relevance

●● Moderate

Recent precedent mixed: similar 'Tests...' descriptive docstring rejected (PR#2661), imperative
rewrite accepted (PR#2703).

PR-#2661
PR-#2703

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires newly added docstrings to use imperative mood. The added class docstring at
the cited line begins with Mimics, which is descriptive third-person phrasing.

Rule 2694688: Docstrings and comments must use imperative phrasing
tests/unittest/test_ticket_extraction_async.py[684-684]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new `_FakeRequester` docstring starts with descriptive third-person phrasing (`Mimics`) instead of an imperative verb.

## Issue Context
PR Compliance ID 2694688 requires newly added function and class docstrings to use imperative phrasing, such as `Mimic` or `Provide`.

## Fix Focus Areas
- tests/unittest/test_ticket_extraction_async.py[684-684]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

GitHub's GraphQL API returns `null` — not a missing key — for nodes it
cannot resolve. `.get(key, {})` only falls back when the key is absent, so
a present-but-null value yielded `None` and the next `.get()` in the chain
raised `AttributeError: 'NoneType' object has no attribute 'get'`.

This fires on ordinary input: `repository.issue` is null whenever the
number in an `/issues/N` link belongs to a pull request, and the ticket
pattern also expands a bare `#N` (under five digits) to that URL. With
`require_ticket_analysis_review` enabled by default, affected repos hit a
logged traceback on every run.

The exception was swallowed by the method's broad `except`, so the
existing `if not issue_id: warning(...)` branch was simply unreachable for
null values. Using `or {}` makes it reachable and the case is handled as
intended — one warning line instead of a traceback.

Same pattern fixed for `data.node` in the sub-issues response.

Tests assert on log output, since the swallowed exception makes the return
value identical with and without the fix.
@erkdgn
erkdgn force-pushed the fix/sub-issues-null-graphql branch from a81f1e2 to d39fc8b Compare August 23, 2026 21:55
@erkdgn

erkdgn commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

Applied — both new docstrings now use imperative mood (Mimic instead of Mimics), pushed as d39fc8b.

One note for context rather than pushback: I used that phrasing because the two sibling fakes already in this file are written the same way, and I was matching the surrounding style —

class _FakeRepoObj:
    """Mimics PyGithub Repository.get_issue lookup behaviour."""

class _FakeGithubClient:
    """Mimics PyGithub Github.get_repo lookup, counting calls."""

I've left those untouched so this PR stays focused on the crash. Happy to align them in a separate PR if you'd like the file consistent with the rule.

Test status after the change: tests/unittest → 2037 passed, 1 skipped, 1 xfailed, built and run the same way CI does (docker build --target test + pytest -v tests/unittest). The regression check still holds — reverting the github_provider.py change makes 3 of the 4 new tests fail.

@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit d39fc8b

@IsmaelMartinez IsmaelMartinez left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for taking this on, and for putting the tests where I suggested rather than reviving the commented-out file. Verified: reverting github_provider.py alone fails three of your four new tests and leaves the happy-path one green, exactly as you reported.

Comment on lines +1324 to +1326
issue_id = (((response_json.get("data") or {})
.get("repository") or {})
.get("issue") or {}).get("id")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed against the three sites I flagged on #2782; this one and 1356 both clear. The if not issue_id branch below is now reachable for an explicit null as well as a missing key, which was the whole point.

Comment on lines +1356 to +1363
sub_issues_data = (((sub_issues_response_json.get("data") or {})
.get("node") or {})
.get("subIssues") or {})
if not sub_issues_data:
get_logger().error("Invalid sub-issues response structure")
return sub_issues
nodes = sub_issues_response_json.get("data", {}).get("node", {}).get("subIssues", {}).get("nodes", [])

nodes = sub_issues_data.get("nodes") or []

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good that this collapsed the two duplicated traversals rather than only swapping the accessor. nodes now reads off the same dict the guard tested, so the two cannot disagree.

Comment on lines +699 to +700
# Name matches the attribute the provider reads; it is not name-mangled
# here because it already starts with a single underscore.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving Qodo's remaining phrasing finding as read, for the record: this comment explains why the attribute is not name-mangled, which is the kind of thing worth a comment. Rewriting it imperatively would not make it clearer. Not a blocker either way.

# Conflicts:
#	tests/unittest/test_ticket_extraction_async.py
IssueConnection.nodes is [Issue] in the GraphQL schema, so an element can be
null even when the connection resolves; that reached the broad except with a
TypeError and logged the same traceback this PR removes.
@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 6229134

@IsmaelMartinez IsmaelMartinez left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. I pushed the rebase myself rather than send you round again: #2802 had collided with your import line and nothing else, so main's MagicMock and your json both stay. Suite green with your diff unchanged.

I also folded in one more guard, on its own commit. Every object field GitHub can return null for was already handled, partial data alongside an errors array included, but a null element inside nodes is schema-legal and still reached the broad except with a TypeError, logging the same shape you set out to remove.

Thanks for the quick turnaround on the docstring nit. Merging once CI is green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fetch_sub_issues crashes when repository.issue is null (PR-number links in /issues/N form)

2 participants