Skip to content

Defensive fix: surface invalid configuration as UserException in kds-team.app-custom-python - #31

Merged
matyas-jirat-keboola merged 2 commits into
mainfrom
notification-defensive-fix
Jul 29, 2026
Merged

Defensive fix: surface invalid configuration as UserException in kds-team.app-custom-python#31
matyas-jirat-keboola merged 2 commits into
mainfrom
notification-defensive-fix

Conversation

@matyas-jirat-keboola

@matyas-jirat-keboola matyas-jirat-keboola commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds defensive handling for an unhandled dacite.exceptions.UnionMatchError observed in
production. No change to the component's behaviour on inputs that already worked.

Root cause

dacite.exceptions.UnionMatchError at src/component.py:49 (the dacite.from_dict call in
Component.__init__):

can not match type "str" to any type of "user_properties" union: dict[str, object] | list

A configuration had user_properties set to a JSON string instead of an object. dacite
raised before Configuration.__post_init__ ran, so the existing user_properties handling never
got a chance to react. The exception reached the entrypoint's generic except Exception handler,
which calls exit(2) — so a plain configuration mistake was reported as an opaque platform
internal error and paged the team, while telling the user nothing about which field was wrong.

Classified as deterministic-but-defensible (user-actionable input surfacing as an internal error).
2 occurrences in the alert window.

Fix

Wrapped the existing dacite.from_dict call in a narrow try/except dacite.DaciteFieldError that
re-raises as UserException, so the job exits 1 with a plain-language message naming the offending
field (the raw library text is kept as trailing detail):

Invalid component configuration: please check the "user_properties" parameter in the configuration. Detail: can not match type "str" to any type of "user_properties" union: dict[str, object] | list

DaciteFieldError is the base class for exactly the errors that mean "a field in the input data is
wrong"
WrongTypeError, UnionMatchError, StrictUnionMatchError, MissingValueError. The two
sibling DaciteError subclasses that indicate a developer mistake rather than a user one
(ForwardReferenceError, UnexpectedDataError) are deliberately not caught, so a real internal
bug still fails loudly as an internal error.

Behaviour impact: none — defensive-only

  • Happy path unchanged; no outputs, transforms, schema, or config semantics touched. A configuration
    that parsed before parses identically — the except is only reachable once parsing has already
    failed.
  • A bad configuration still fails the job. Nothing is coerced, defaulted, or swallowed; the only
    change is exit 2 (internal error, opaque) → exit 1 (user error, actionable).
  • Configuration.__post_init__'s own UserException (the non-empty-list case) passes through
    unwrapped and keeps its original message — covered by a new test.
  • Existing tests pass unchanged (no assertions modified or deleted).

Evidence

Datadog monitor: https://app.datadoghq.eu/monitors/97152903

Tests

python -m unittest discover green — 10 passed (6 pre-existing, unchanged + 4 new).
The message wording is phrased type-agnostically on purpose: DaciteFieldError also covers
MissingValueError, for which "unexpected type" would read wrong.
flake8 . --config=flake8.cfg clean.

New TestConfigurationParsingErrors covers:

Test Asserts
test_string_user_properties_raises_user_exception the exact production input raises UserException naming user_properties (fails without this fix)
test_wrong_type_in_other_field_raises_user_exception any wrong-typed field is reported the same way (fails without this fix)
test_post_init_user_exception_is_not_rewrapped the pre-existing non-empty-list UserException keeps its original message
test_valid_configuration_is_parsed_unchanged a valid configuration still parses to the expected values

The last two pass both with and without the fix — they are the behaviour-preservation guards.

Noted, deliberately out of scope

  • An invalid enum value (e.g. venv: "3.11") raises a plain ValueError from the enum cast, not
    a DaciteFieldError, so it still exits 2. Catching ValueError here would be broad enough to
    mask genuine bugs, so it is left for a separate decision.
  • Whether user_properties: "" should be tolerated as {} (the way [] already is) is a
    product decision, not a defensive one — it would turn a failing job into a succeeding one, so it
    is intentionally not done here.

An unexpected type in a configuration field escaped dacite.from_dict as a raw
DaciteFieldError, which the entrypoint caught as a generic exception and turned
into an opaque internal error (exit 2). Observed in production as
UnionMatchError on the user_properties field.

Re-raise dacite field errors as UserException so the job exits 1 with a message
naming the offending field. Valid configurations parse exactly as before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@matyas-jirat-keboola

Copy link
Copy Markdown
Contributor Author

@keboola-pr-reviewer review profile=component-factory

@keboola-pr-reviewer-bot keboola-pr-reviewer-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: auto_approve (risk 1/5) · profile component-factory

Auto-approve: textbook defensive-only fix that surfaces a config error as UserException without changing happy-path behaviour.

@matyas-jirat-keboola matyas-jirat-keboola left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Automated component-review-pr pass. 1 finding — the defensive-only intent holds: the new except is reachable only after dacite.from_dict has already failed, so a configuration that parsed before parses identically, and a bad one still fails the job (exit 2 -> exit 1).

Usage: not established — the telemetry project is not authorized for this session, so severity is judged on the code alone. This change touches no schema, config field, output table or state file, so there is no backward-compatibility surface to weigh against config counts.

# Area Sev Location Finding
1 Error handling Important src/component.py:62 The UserException message embeds dacite's raw text verbatim (`can not match type "str" to any type of "user_properties" union: dict[str, object]

Checked and clean: from err preserves the original traceback; the except is narrow (DaciteFieldError, deliberately not the DaciteError siblings that signal a developer bug); Configuration.__post_init__'s own UserException passes through unwrapped; no secrets added; the new tests share a helper rather than copy-pasting setup, and two of the four are behaviour-preservation guards that pass with and without the fix.

Happy to discuss any of these.

Comment thread src/component.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR makes configuration parsing failures user-actionable by converting dacite field-level parsing errors (e.g., UnionMatchError for wrong input types) into UserException, so invalid user configuration exits with code 1 and an explicit message instead of being treated as an internal error (exit 2).

Changes:

  • Wraps dacite.from_dict(...) in Component.__init__ with a narrow except dacite.DaciteFieldError and re-raises as UserException with a clear “Invalid component configuration…” message.
  • Adds unit tests that exercise the reported production failure (user_properties provided as a JSON string) and other wrong-type fields, plus guards for behavior preservation (including ensuring __post_init__’s UserException is not rewrapped).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
src/component.py Converts dacite field parsing errors into UserException during component initialization to surface invalid configuration as a user error.
tests/test_component.py Adds test coverage to ensure invalid configuration types are reported as UserException and valid configurations parse unchanged.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Addresses PR review finding #1: the message surfaced dacite's raw text on its
own. It now names the offending field in plain language and keeps the library
text as trailing detail. Phrased type-agnostically so it also reads correctly
for a missing required field.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@matyas-jirat-keboola

Copy link
Copy Markdown
Contributor Author

Round 2 (babysit-pr) — converged.

# Sev Outcome
#1 Important Fixed in 35c91ab — plain-language lead naming the offending field, library text kept as trailing detail. Thread resolved.

Re-review of the current head finds no new findings: the except is still narrow (DaciteFieldError only), from err preserved, no schema/config/output/state surface touched, no secrets, existing assertions untouched. 10 tests green, flake8 clean.

Copilot reviewed both revisions and generated no comments.

@keboola-pr-reviewer review profile=component-factory

@keboola-pr-reviewer-bot keboola-pr-reviewer-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: auto_approve (risk 1/5) · profile component-factory

Auto-approve: textbook behaviour-preserving defensive hardening with a full regression test.

@matyas-jirat-keboola
matyas-jirat-keboola merged commit aea8963 into main Jul 29, 2026
9 checks passed
@matyas-jirat-keboola
matyas-jirat-keboola deleted the notification-defensive-fix branch July 29, 2026 08:24
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.

3 participants