Skip to content

[CDF-28123] fix(graphql): surface real API error when upsertGraphQlDmlVersion returns null#3088

Open
ronpal wants to merge 8 commits into
mainfrom
fix/graphql-dml-upsert-error-surfacing
Open

[CDF-28123] fix(graphql): surface real API error when upsertGraphQlDmlVersion returns null#3088
ronpal wants to merge 8 commits into
mainfrom
fix/graphql-dml-upsert-error-surfacing

Conversation

@ronpal

@ronpal ronpal commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Description

When the CDF DML API rejects a GraphQL mutation (e.g. unknown fields, schema
validation failures, auth issues) it returns:

{"data": {"upsertGraphQlDmlVersion": null}, "errors": [{"message": "...real reason..."}]}

The previous client called GraphQLResponse.model_validate_json() first — Pydantic
crashed on the null before the code ever reached the errors check. The customer saw:

Input should be an object (type=model_type, loc=["data","upsertGraphQlDmlVersion"])

instead of the actual API rejection reason, making the failure impossible to self-diagnose.

Changes:

  • Check top-level GraphQL errors before Pydantic validation so the real message is always surfaced
  • UpsertResponseData.errors corrected from dictlist[DMLError] (matches the API contract)
  • UpsertResponseData.result made nullable (API returns null when DML validation fails)
  • GraphQLUpsertResponse.upsert_graph_ql_dml_version made nullable (correct per API contract)
  • Nested DML validation errors (kind/message/hint) are now surfaced as well
  • Two regression tests added

Bump

  • Patch
  • Skip

Changelog

Fixed

  • cdf deploy for GraphQL data models: the real API rejection reason is now surfaced
    instead of an opaque Pydantic "Input should be an object" error when
    upsertGraphQlDmlVersion returns null.

ronpal added 7 commits June 24, 2026 15:22
## Bump

- [x] Patch
- [ ] Skip

## Changelog

### Fixed

- `cdf build` rejecting unquoted timestamp-like values in config.yaml variables
…urns null

When the CDF DML API rejects a mutation it returns
{"data": {"upsertGraphQlDmlVersion": null}, "errors": [...]} — the
previous client parsed the response with Pydantic first, crashing on
the null before ever reaching the error list, leaving the user with an
opaque "Input should be an object" message.

Now check top-level GraphQL errors before Pydantic validation so the
real rejection reason is always surfaced. Also fix UpsertResponseData:
errors was typed as dict but the API returns a list of DMLError objects,
and result is now correctly nullable for when DML validation fails.
@ronpal

ronpal commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist

Copy link
Copy Markdown
Contributor
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request improves error handling in the GraphQL Data Models API by parsing raw JSON responses to surface top-level GraphQL and DML validation errors before Pydantic validation, and adds corresponding regression tests. The review feedback suggests enhancing robustness by ensuring the parsed response is a dictionary and handling cases where error messages are missing to avoid empty error outputs.

Comment on lines +94 to +97
raw = json.loads(response.body)
if top_errors := raw.get("errors"):
messages = [e.get("message", str(e)) for e in top_errors if isinstance(e, dict)]
raise ToolkitAPIError(f"GraphQL mutation failed: {humanize_collection(messages)}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

If the response body is not a JSON object (e.g., if it is a JSON array, a primitive, or if an intermediate proxy returns an unexpected format), raw will not be a dictionary and calling raw.get will raise an AttributeError. Additionally, if top_errors is not a list or contains non-dictionary elements, the current list comprehension will either ignore them or fail. Checking that raw is a dictionary and handling non-list/non-dict errors gracefully ensures robust error surfacing.

Suggested change
raw = json.loads(response.body)
if top_errors := raw.get("errors"):
messages = [e.get("message", str(e)) for e in top_errors if isinstance(e, dict)]
raise ToolkitAPIError(f"GraphQL mutation failed: {humanize_collection(messages)}")
raw = json.loads(response.body)
if isinstance(raw, dict) and (top_errors := raw.get("errors")):
if isinstance(top_errors, list):
messages = [e.get("message", str(e)) if isinstance(e, dict) else str(e) for e in top_errors]
else:
messages = [str(top_errors)]
raise ToolkitAPIError(f"GraphQL mutation failed: {humanize_collection(messages)}")
References
  1. Error Handling: Graceful handling: Provide meaningful error messages. (link)

Comment on lines +104 to +106
if upsert.errors:
messages = [e.message for e in upsert.errors if e.message]
raise ToolkitAPIError(f"DML validation failed: {humanize_collection(messages)}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

If upsert.errors is populated but none of the errors contain a message (e.g., they only have kind and hint), the list comprehension will produce an empty list, resulting in an unhelpful empty error message (DML validation failed: ). We should fall back to formatting the kind and hint fields if message is absent.

Suggested change
if upsert.errors:
messages = [e.message for e in upsert.errors if e.message]
raise ToolkitAPIError(f"DML validation failed: {humanize_collection(messages)}")
if upsert.errors:
messages = [
e.message or f"{e.kind or 'Error'}(hint={e.hint})"
for e in upsert.errors
]
raise ToolkitAPIError(f"DML validation failed: {humanize_collection(messages)}")
References
  1. Error Handling: Graceful handling: Provide meaningful error messages. (link)

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

☂️ Code Coverage

current status: ✅

Overall Coverage

Statements Covered Coverage Threshold Status
43847 37729 86% 80% 🟢

New Files

No new covered files...

Modified Files

File Coverage Status
cognite_toolkit/_cdf_tk/client/api/graphql_data_models.py 96% 🟢
TOTAL 96% 🟢

updated for commit: 9a67e65 by action🐍

@ronpal ronpal marked this pull request as ready for review July 1, 2026 08:40
@ronpal ronpal requested review from a team as code owners July 1, 2026 08:40
@codecov

codecov Bot commented Jul 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.88889% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.04%. Comparing base (080ba9e) to head (9a67e65).

Files with missing lines Patch % Lines
..._toolkit/_cdf_tk/client/api/graphql_data_models.py 88.88% 3 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #3088   +/-   ##
=======================================
  Coverage   86.04%   86.04%           
=======================================
  Files         475      475           
  Lines       43828    43847   +19     
=======================================
+ Hits        37712    37729   +17     
- Misses       6116     6118    +2     
Files with missing lines Coverage Δ
..._toolkit/_cdf_tk/client/api/graphql_data_models.py 95.89% <88.88%> (-2.26%) ⬇️

... and 3 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant