pola-rs/polars · error · ValueError

expiration time in STS response did not contain timezone inf

Error message

expiration time in STS response did not contain timezone information

What it means

CredentialProviderAWS._finish_assume_role (py-polars/src/polars/io/cloud/credential_provider/_providers.py:226-247) calls boto3 STS assume_role and reads response['Credentials']['Expiration'], converting it to a UNIX timestamp via expiry.timestamp(). A naive datetime (tzinfo None) would make that conversion timezone-ambiguous, so polars raises ValueError rather than guessing. Real AWS STS always returns tz-aware datetimes, so this almost always indicates a non-standard STS endpoint.

Source

Thrown at py-polars/src/polars/io/cloud/credential_provider/_providers.py:239

            if isinstance(expiry := getattr(creds, "_expiry_time", None), datetime)
            else None
        )

        return creds_dict, expiry

    def _finish_assume_role(self, session: Any) -> CredentialProviderFunctionReturn:
        assert self.assume_role is not None

        client = session.client("sts")

        sts_response = client.assume_role(**self.assume_role)
        creds = sts_response["Credentials"]

        expiry = creds["Expiration"]

        if expiry.tzinfo is None:
            msg = "expiration time in STS response did not contain timezone information"
            raise ValueError(msg)

        return {
            "aws_access_key_id": creds["AccessKeyId"],
            "aws_secret_access_key": creds["SecretAccessKey"],
            "aws_session_token": creds["SessionToken"],
        }, int(expiry.timestamp())

    # Called from Rust, mainly for AWS endpoint_url
    def _storage_update_options(self) -> dict[str, str]:
        if self._storage_options_has_endpoint_url:
            return {}

        try:
            config = self._session()._session.get_scoped_config()
        except ImportError:
            return {}

        if endpoint_url := config.get("endpoint_url"):

View on GitHub (pinned to df599052da)

Solutions

  1. Fix the mock/endpoint to return tz-aware datetimes: datetime.now(timezone.utc) - matching real AWS behavior
  2. If a corporate proxy is stripping timezone info, report/fix it upstream
  3. In test fixtures, build the full Credentials dict including a tz-aware Expiration

Example fix

# before (test stub returning a naive datetime)
sts.assume_role.return_value = {"Credentials": {"Expiration": datetime.now(), ...}}
# after
from datetime import timezone
sts.assume_role.return_value = {"Credentials": {"Expiration": datetime.now(timezone.utc), ...}}
Defensive patterns

Strategy: try-catch

Try / catch

try:
    df = pl.scan_parquet(path, credential_provider=provider).collect()
except ValueError as e:
    if "timezone information" in str(e):
        raise RuntimeError(
            "STS endpoint returned a naive Expiration; fix the STS mock/proxy"
        ) from e
    raise

Prevention

When it happens

Trigger: scanning/writing s3:// with credential_provider=pl.CredentialProviderAWS(assume_role={...}) where the STS endpoint is a mock (moto, stubbed boto3) or a proxy returning an Expiration without a timezone offset.

Common situations: Unit/integration tests that stub boto3 Session.client('sts').assume_role with datetime.now() (naive); custom corporate STS proxies that strip offsets; patched boto3 responses in test fixtures.

Related errors


AI-assisted analysis of pola-rs/polars@df599052da (2026-08-16). Data as JSON: /api/errors/247486d780bb5214. Report an issue: GitHub.