Skip to content

fix(types): treat unresolvable nested RichText values as plain text - #2651

Open
beduldul wants to merge 1 commit into
eternnoir:masterfrom
beduldul:fix/richtext-non-text-values
Open

beduldul wants to merge 1 commit into
eternnoir:masterfrom
beduldul:fix/richtext-non-text-values

Conversation

@beduldul

Copy link
Copy Markdown
Contributor

RichText.de_json catches resolution failures and falls back to the string, but the guard misses ValueError:

>>> types.RichText.de_json('{"type": "bold", "text": 5}')
ValueError: json_type should be a json dict or string.

That error comes from check_json, reached through RichTextBold.de_json → RichText.de_json(5). The except (KeyError, TypeError, AttributeError) in de_json does not catch it, so a payload shaped like RichText but holding a scalar crashes the caller. Before that fix it raised too; the guard only ever covered missing keys.

What this changes

Resolution failures fall back to plain text at the two places that can raise:

  • the parsed-dict path in de_json, which now catches ValueError as well;
  • a new RichText._resolve, used for the nested text of every RichText* subclass, which rejects a nested value that is not a str, list or dict.

_resolve also rejects a nested value that does not resolve to a single RichText. Without that, {"type": "bold", "text": {"type": "not_a_richtext"}} built a RichTextBold with text=None, and to_dict() on it produced a payload Telegram rejects. Same for text: [], which kept a list where one RichText is expected.

The 22 subclasses that called RichText.de_json(obj['text']) call cls._resolve(obj['text']) now, so the rule lives in one place instead of being repeated per class.

UNRESOLVED is a class attribute, not a module global, so nothing new appears in telebot.types.

What stays as it is

Tests

Five cases in tests/test_types.py:

  • a nested text of 5, 1.5, true, false stays plain text, both through RichText.de_json and through RichBlockPreformatted;
  • an unknown nested type does not build a parent object;
  • a dict input that cannot resolve comes back untouched;
  • every RichText* type survives 5, true and [] without raising.

Full suite: 327 passed, 72 skipped.

I also ran 175 type/value combinations and 500 random payloads against the change: no exception, and round-trip de_json → to_dict is unchanged for valid input.

Python 3.12.3, macOS 27.2.

Checklist:

  • I added/edited example on new feature/change (if exists)
  • My changes won't break backward compatibility
  • I made changes both for sync and async

A nested "text" that is a number or a boolean reached check_json, which
raises ValueError, or was turned into a RichTextBold holding text=5. RichText
caught KeyError, TypeError and AttributeError but not ValueError, so the guard
added for eternnoir#2647 still let that input crash a bot.

Resolution failures now fall back to plain text at the only two places that
can raise: the parsed-dict path, and a new _resolve used for the nested text
of every RichText* subclass. A nested value that does not resolve to a single
RichText no longer builds a parent whose text cannot be serialized back.

ValueError on the top-level dict input is left to propagate: a dict we cannot
parse is a caller mistake, not text that arrived from Telegram.
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