Skip to content

fix(oauth): parse ISO timestamps with negative UTC offsets - #1990

Open
sebxt wants to merge 1 commit into
slackapi:mainfrom
sebxt:fix/iso-timestamp-negative-offset
Open

sebxt wants to merge 1 commit into
slackapi:mainfrom
sebxt:fix/iso-timestamp-negative-offset

Conversation

@sebxt

@sebxt sebxt commented Oct 20, 2026 •

Copy link
Copy Markdown

Summary

_from_iso_format_to_datetime appended +00:00 to any string without a + character, so ISO 8601 timestamps with a negative offset (2021-07-14T03:00:17-05:00) were turned into invalid strings and raised ValueError. A Z suffix had the same problem on Python 3.11+ (on 3.7 to 3.10 it only parsed by accident). This affects Installation, Bot and anything else that goes through _timestamp_to_type with a string value.

The function now swaps a trailing Z or z for +00:00 (only the last character, per RFC 3339 §5.6), parses the string, and only sets UTC when the result has no tzinfo, the same approach #1970 took for naive datetime values. Strings without an offset are still treated as UTC, so existing behavior is unchanged for them.

Testing

./scripts/run_tests.sh tests/slack_sdk/oauth/installation_store/test_internals.py

New cases cover a positive offset, a negative offset, and the Z/z UTC designator, directly and through _timestamp_to_type. The negative offset cases fail on main and pass with this change; the UTC designator cases pass on Python 3.10 and 3.13.

Category

  • slack_sdk.web.WebClient (sync/async) (Web API client)
  • slack_sdk.webhook.WebhookClient (sync/async) (Incoming Webhook, response_url sender)
  • slack_sdk.socket_mode (Socket Mode client)
  • slack_sdk.signature (Request Signature Verifier)
  • slack_sdk.oauth (OAuth Flow Utilities)
  • slack_sdk.models (UI component builders)
  • slack_sdk.scim (SCIM API client)
  • slack_sdk.audit_logs (Audit Logs API client)
  • slack_sdk.rtm_v2 (RTM client)
  • /docs (Documents)
  • /tutorial (PythOnBoardingBot tutorial)
  • tests/integration_tests (Automated tests for this library)

Requirements

  • I've read and understood the Contributing Guidelines and have done my best effort to follow them.
  • I've read and agree to the Code of Conduct.
  • I've run python3 -m venv .venv && source .venv/bin/activate && ./scripts/run_validation.sh after making the changes.

Fixes #1989

@sebxt
sebxt requested a review from a team as a code owner October 20, 2026 02:02
@salesforce-cla

Copy link
Copy Markdown

Thanks for the contribution! Before we can merge this, we need @auraseb to sign the Salesforce Inc. Contributor License Agreement.

@WilliamBergamin

Copy link
Copy Markdown
Contributor

Hello 👋 Please sign the CLA

@WilliamBergamin WilliamBergamin 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.

Hello and thanks for your interest in our project 💯 The negative offset fix is real: I checked that the new tests fail on main and pass on this branch

There's one regression to fix before merging. On CPython 3.7-3.10, a timestamp ending in Z (e.g. "2021-07-14T08:00:17Z") parses on main and raises ValueError with this change. main only handles it by accident: it appends +00:00, and the C fromisoformat on those versions lets the stray Z through. With the string passed to fromisoformat as-is, nothing before 3.11 accepts Z (gh-80010). Since the SDK supports 3.7+ and both #1989 and the description say Z is fixed, I think we should handle it explicitly. Suggestions inline.

Also, the CLA check needs signing before we can run CI.

Comment on lines 5 to +9
def _from_iso_format_to_datetime(iso_datetime_str: str) -> datetime:
if "+" not in iso_datetime_str:
iso_datetime_str += "+00:00"
return datetime.fromisoformat(iso_datetime_str)
dt = datetime.fromisoformat(iso_datetime_str)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt

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.

Swap a trailing Z for +00:00 before parsing so Z works on every supported version of python. Only the last character, not str.replace: RFC 3339 §5.6 only allows Z as the final offset, which is also how CPython's own parser treats it. Lowercase z is allowed by the RFC and already parses on main under CPython, so keeping it avoids a second regression.

Suggested change
def _from_iso_format_to_datetime(iso_datetime_str: str) -> datetime:
if "+" not in iso_datetime_str:
iso_datetime_str += "+00:00"
return datetime.fromisoformat(iso_datetime_str)
dt = datetime.fromisoformat(iso_datetime_str)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt
def _replace_utc_designator(iso_datetime_str: str) -> str:
if iso_datetime_str.endswith(("Z", "z")):
return iso_datetime_str[:-1] + "+00:00"
return iso_datetime_str
def _from_iso_format_to_datetime(iso_datetime_str: str) -> datetime:
dt = datetime.fromisoformat(_replace_utc_designator(iso_datetime_str))
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt

dt = _from_iso_format_to_datetime("2021-07-14 03:00:17-05:00")
self.assertEqual(dt.timestamp(), 1626249617.0)


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.

Could we add coverage for the UTC designator? No test exercises Z right now, which is why the 3.7-3.10 regression doesn't show up in CI.

    def test_iso_format_with_utc_designator(self):
        for iso_datetime_str in ["2021-07-14T08:00:17Z", "2021-07-14T08:00:17z", "2021-07-14T08:00:17.000Z"]:
            dt = _from_iso_format_to_datetime(iso_datetime_str)
            self.assertEqual(dt.timestamp(), 1626249617.0)

and a ("2023-11-28T22:11:19Z", int, 1701209479) case in the test_timestamp_to_type parametrize list.

@codecov

codecov Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 84.36%. Comparing base (ab4e498) to head (6bd1361).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #1990   +/-   ##
=======================================
  Coverage   84.35%   84.36%           
=======================================
  Files         118      118           
  Lines       13793    13798    +5     
=======================================
+ Hits        11635    11640    +5     
  Misses       2158     2158           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

Signed-off-by: Sebastian Turner <sebastiant3484@gmail.com>
@sebxt
sebxt force-pushed the fix/iso-timestamp-negative-offset branch from 459c032 to 6bd1361 Compare October 20, 2026 15:46
@sebxt

sebxt commented Oct 20, 2026

Copy link
Copy Markdown
Author

Thanks for catching the 3.7 to 3.10 regression. Applied your suggestion as written, added test_iso_format_with_utc_designator and the trailing Z case in test_timestamp_to_type. test_internals.py passes 16 of 16 on 3.10 and 3.13.

@hello-ashleyintech

Copy link
Copy Markdown
Contributor

hi @auraseb! Thanks so much for your contribution, could you sign the Salesforce Inc. Contributor License Agreement?

@hello-ashleyintech hello-ashleyintech added the bug M-T: A confirmed bug report. Issues are confirmed when the reproduction steps are documented label Oct 20, 2026

This branch has not been deployed

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

Labels

bug M-T: A confirmed bug report. Issues are confirmed when the reproduction steps are documented cla:missing oauth semver:patch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Installation and Bot reject ISO 8601 timestamps with a negative UTC offset or a Z suffix

3 participants