Repository: home-assistant/core
File: homeassistant/helpers/config_entry_oauth2_flow.py (Python)

Code before:
                breaks_in_ha_version="2026.8",
                core_behavior=ReportBehavior.LOG,
                integration_domain=self._domain,
            )

            if err.status == HTTPStatus.TOO_MANY_REQUESTS or 500 <= err.status <= 599:
                # Recoverable error
                raise OAuth2RefreshTokenTransientError(
                    request_info=err.request_info,
                    history=err.history,
                    status=err.status,
                    message=err.message,
                    headers=err.headers,
                    domain=self._domain,
                ) from err
            if 400 <= err.status <= 499:
                # Non-recoverable error
                raise OAuth2RefreshTokenReauthError(
                    request_info=err.request_info,
                    history=err.history,
                    status=err.status,
                    message=err.message,
                    headers=err.headers,
                    domain=self._domain,
                ) from err

            raise OAuth2RefreshTokenError(
                request_info=err.request_info,
                history=err.history,
                status=err.status,
                message=err.message,
                headers=err.headers,
                domain=self._domain,
            ) from err

        return {**token, **new_token}

    async def _token_request(self, data: dict) -> dict:
        """Make a token request."""
        session = async_get_clientsession(self.hass)

        data["client_id"] = self.client_id
        if self.client_secret:
            data["client_secret"] = self.client_secret

        _LOGGER.debug("Sending token request to %s", self.token_url)

        resp = await session.post(self.token_url, data=data)
        if resp.status >= 400:
            try:
                error_response = await resp.json()

Review comment (security):
I think we should consider moving the exception handling to `_token_request` instead. It's closer to the source of the aiohttp call in `_token_request`. If we move it there, we need to update `async_step_creation` to handle the new exceptions instead of aiohttp exceptions.

A benefit of moving it deeper is that some integrations override `_async_refresh_token` and would not benefit from the new exception if we raise it in `_async_refresh_token`.

In a follow up I have some suggestions for how to allow integrations to override less of `LocalOAuth2Implementation`, eg adding a `headers` property to `LocalOAuth2Implementation` that should return the headers to use in `_token_request`. A handful of integrations set custom headers today.

Code after:

        data["client_id"] = self.client_id
        if self.client_secret:
            data["client_secret"] = self.client_secret

        _LOGGER.debug("Sending token request to %s", self.token_url)

        try:
            resp = await session.post(self.token_url, data=data)
            if resp.status >= 400:
                try:
                    error_response = await resp.json()
                except ClientError, JSONDecodeError:
                    error_response = {}
                error_code = error_response.get("error", "unknown")
                error_description = error_response.get(
                    "error_description", "unknown error"
                )
                _LOGGER.error(
                    "Token request for %s failed (%s): %s",
                    self.domain,
                    error_code,
                    error_description,
                )
            resp.raise_for_status()
        except ClientResponseError as err:
            if err.status == HTTPStatus.TOO_MANY_REQUESTS or 500 <= err.status <= 599:
                # Recoverable error
                raise OAuth2TokenRequestTransientError(
                    request_info=err.request_info,
                    history=err.history,
                    status=err.status,
                    message=err.message,
                    headers=err.headers,
                    domain=self._domain,
                ) from err
            if 400 <= err.status <= 499:
                # Non-recoverable error
                raise OAuth2TokenRequestReauthError(
                    request_info=err.request_info,
                    history=err.history,
                    status=err.status,
                    message=err.message,
                    headers=err.headers,
                    domain=self._domain,
                ) from err

            raise OAuth2TokenRequestError(
                request_info=err.request_info,
                history=err.history,
                status=err.status,