slackapi / python-slack-sdk

Slack Developer Kit for Python
https://tools.slack.dev/python-slack-sdk/
MIT License
3.85k stars 837 forks source link

Inconsistent SlackObjectFormationError raising when empty text is provided. #1267

Open kezabelle opened 2 years ago

kezabelle commented 2 years ago

Given the following:

>>> SectionBlock(text="").to_dict()
SlackObjectFormationError: text or fields attribute must be specified

This is all correct enough and good enough. What's happening behind the scenes appears to be that TextObject.parse is used and does a truthy test on the incoming text, and returns None as opposed to an empty string.

Where that begins to go wrong, is here:

>>> SectionBlock(text=MarkdownTextObject(text="")).to_dict()
{'text': {'type': 'mrkdwn'}, 'type': 'section'}

Slack will reject that as erroneous. If it's sent back as part of the acknowledgement response as a response_action we won't necessarily see (receive) an error related to failing to parse the JSON schema. Slack's block kit builder says of it: Missing required field: text

Category

Desired outcome

Ideally, I'd like to be able to trust (more) that the validation present in slack_sdk is going to raise on invalid configurations early, and consistently -- that is, where possible I'd like to assume that putting together something "empty" even if I've opted to use the classes instead of an equivalent base type (str, dict) should raise.

hello-ashleyintech commented 2 years ago

Hi, @kezabelle! Thanks so much for writing in!

I took a look at the issue you described and agree that the difference in behavior can feel confusing. I'll be reaching out to a member of my team to get additional context on whether this behavior is expected or not and to look into what can be done here. I'll add any updates or additional findings here within the next day or so!

eddyg commented 2 years ago

For reference, when I updated the text presence check here, it was implemented like this:

 if text and len(text.strip()) > 0:

It may be appropriate to do something like that here as well?

hello-ashleyintech commented 2 years ago

Hi, @kezabelle!

I chatted with a member of my team about this and we identified that adding in error handling for the described scenario could potentially cause breaking changes. Because of this, and also since there's still some SDK side validation for the scenario, we are hesitant to add in additional error handling here and alter the behavior of SectionBlock for these use cases. However, if the error message is confusing, we can definitely look into updating it!

kezabelle commented 2 years ago

Hey, @hello-ashleyintech, thanks for taking the time to look into it. I appreciate it. Apologies for the delay in responding, I've been away.

I'll preface everything hereafter with a degree of uncertainty, as I've primarily been branching out to using slack_sdk only for the type classes in models (ignoring all the other submodules) to replace fragile/ad-hoc dictionary construction for blocks. So please, take the following with a grain of salt and ignorance rather than being in any way combative.

[...] we identified that adding in error handling for the described scenario could potentially cause breaking changes.

Would you be able to describe in what scenarios it could produce valid output which Slack would actually consume? From my (comparatively limited) experience, failure to encode and raise an exception for the invalid schema only pushes the problem to the communication point with Slack's API, and whether you can even surface that error depends on whether that block is being pushed via an API call to views.update (et al) or as a response_action in the acknowledgement HTTP body. This hit me in production in both scenarios due to what looked like an innocuous change in text output, and it's only because submitting data to the API returned a JSON schema error caused an error, that I noticed it.

On that set of assumptions (which may be incorrect!), what breaking changes would this cause that aren't already broken? Any tightening of validation is always a backwards incompatible, breaking change in potentia, but this would seem to me (naively) to simply be bringing the API in-line with the schema.

[...] also since there's still some SDK side validation for the scenario [...]

Again, unfamiliar with the rest of the modules, but where might I expect to find this? I'm presuming SDK here means somewhere within slack_sdk rather than at the API boundary for communicating with Slack via HTTP? In broad strokes, in an ideal world and in isolation, if that validation exists elsewhere in the project, I would argue that it's in the wrong place given there is some expectation of validation on the classes themselves -- but, perfect is the enemy of good, and I fully understand that real world behaviours may have called for it to be elsewhere.

However, if the error message is confusing, we can definitely look into updating it!

There error message itself is fine, honestly. It's the complete absence of an error in another scenario which amounts to the same error, that I'm finding fault with. Were there no validation, I'd just chalk it up to something to deal with myself, but as there is some I now need to mentally keep track of which validations the slack_sdk.models package "bothers" with and which I need to manually check forever.


In case it's easier to read the (pseudo-)code I'd roughly be (naively) proposing, off the to of my head the validation would be something like the below, give or take personal proclivities for type-checking or shape-checking and stylistic choices for brevity etc:

if self.text is not None:
    # handle TextObjects (or anything with `.text` attributes)
    if hasattr(self.text, "text"):
        if not self.text.text.strip():
             raise ...
    # handle dictionaries (or anything with __getitem__(text))
    if isinstance(self.text, Mapping):
          if 'text' not in self.text:
             raise ...
          if not self.text['text'].strip():
             raise ...
    # handle strings (or really anything with strip as a method which returns truthy/falsy)
    if not self.text.strip():
             raise ...
hello-ashleyintech commented 2 years ago

@kezabelle Thanks for the reply! 🙌

Would you be able to describe in what scenarios it could produce valid output which Slack would actually consume?

If any valid string that is not empty is passed in to the MarkdownTextObject or PlainTextObject for the text property, this would be a valid output.

On that set of assumptions (which may be incorrect!), what breaking changes would this cause that aren't already broken?

This is a great question! On receiving this issue, I had actually tried to implement error handling for this locally in a similar place where the text or fields attribute must be specified was implemented. Sadly, the returning of an error for empty strings being passed into MarkdownTextObject or PlainTextObjects ended up breaking quite a few tests. This is what prompted me to consult the team internally, to see if all the errors being thrown from the tests were a valid tradeoff to add additional validation.

Basically, with my tested implementation (similar to what @/eddyg had suggested), it seemed like throwing an error in that particular location would end up altering the behavior of SectionBlock, which could cause a breaking change, which is why the unit tests were returning errors. Because of this cascading effect and potentially wider impact, we are hesitant to add additional error handling at this time.

Again, unfamiliar with the rest of the modules, but where might I expect to find this?

Within the Block Kit, there is some validation. Here is an example of a PlainTextObject being passed into a Block Kit - as you can see, it throws an error, must be more than 0 characters. The same would get thrown if a MarkdownTextObject was passed in (this can be emulated by changing the type to mrkdwn).

Let me know if you have any additional questions here! This is helpful feedback and definitely something we can consider a better pathway for in the future, especially if more folks run into similar issues with this. 🙌