Defensive fix: surface invalid configuration as UserException in kds-team.app-custom-python - #31
Conversation
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>
|
@keboola-pr-reviewer review profile=component-factory |
keboola-pr-reviewer-bot
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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(...)inComponent.__init__with a narrowexcept dacite.DaciteFieldErrorand re-raises asUserExceptionwith a clear “Invalid component configuration…” message. - Adds unit tests that exercise the reported production failure (
user_propertiesprovided as a JSON string) and other wrong-type fields, plus guards for behavior preservation (including ensuring__post_init__’sUserExceptionis 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>
|
Round 2 (
Re-review of the current head finds no new findings: the Copilot reviewed both revisions and generated no comments. @keboola-pr-reviewer review profile=component-factory |
keboola-pr-reviewer-bot
left a comment
There was a problem hiding this comment.
Verdict: auto_approve (risk 1/5) · profile component-factory
Auto-approve: textbook behaviour-preserving defensive hardening with a full regression test.
Summary
Adds defensive handling for an unhandled
dacite.exceptions.UnionMatchErrorobserved inproduction. No change to the component's behaviour on inputs that already worked.
Root cause
dacite.exceptions.UnionMatchErroratsrc/component.py:49(thedacite.from_dictcall inComponent.__init__):A configuration had
user_propertiesset to a JSON string instead of an object.daciteraised before
Configuration.__post_init__ran, so the existinguser_propertieshandling nevergot a chance to react. The exception reached the entrypoint's generic
except Exceptionhandler,which calls
exit(2)— so a plain configuration mistake was reported as an opaque platforminternal 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_dictcall in a narrowtry/except dacite.DaciteFieldErrorthatre-raises as
UserException, so the job exits 1 with a plain-language message naming the offendingfield (the raw library text is kept as trailing detail):
DaciteFieldErroris the base class for exactly the errors that mean "a field in the input data iswrong" —
WrongTypeError,UnionMatchError,StrictUnionMatchError,MissingValueError. The twosibling
DaciteErrorsubclasses that indicate a developer mistake rather than a user one(
ForwardReferenceError,UnexpectedDataError) are deliberately not caught, so a real internalbug still fails loudly as an internal error.
Behaviour impact: none — defensive-only
that parsed before parses identically — the
exceptis only reachable once parsing has alreadyfailed.
change is exit 2 (internal error, opaque) → exit 1 (user error, actionable).
Configuration.__post_init__'s ownUserException(the non-empty-list case) passes throughunwrapped and keeps its original message — covered by a new test.
Evidence
Datadog monitor: https://app.datadoghq.eu/monitors/97152903
Tests
python -m unittest discovergreen — 10 passed (6 pre-existing, unchanged + 4 new).The message wording is phrased type-agnostically on purpose:
DaciteFieldErroralso coversMissingValueError, for which "unexpected type" would read wrong.flake8 . --config=flake8.cfgclean.New
TestConfigurationParsingErrorscovers:test_string_user_properties_raises_user_exceptionUserExceptionnaminguser_properties(fails without this fix)test_wrong_type_in_other_field_raises_user_exceptiontest_post_init_user_exception_is_not_rewrappedUserExceptionkeeps its original messagetest_valid_configuration_is_parsed_unchangedThe last two pass both with and without the fix — they are the behaviour-preservation guards.
Noted, deliberately out of scope
venv: "3.11") raises a plainValueErrorfrom the enum cast, nota
DaciteFieldError, so it still exits 2. CatchingValueErrorhere would be broad enough tomask genuine bugs, so it is left for a separate decision.
user_properties: ""should be tolerated as{}(the way[]already is) is aproduct decision, not a defensive one — it would turn a failing job into a succeeding one, so it
is intentionally not done here.