async def prepare_for_next_attempt_async(
        self,
        *,
        state: RetryState,
        request: HttpRequest,
        response: Optional[HttpResponse] = None,
        error: Optional[Exception] = None,
    ) -> None:
        if response is None:
            raise error

        state.next_attempt_requested = True
        retry_after_header_name: Optional[str] = None
        for k in response.headers.keys():
            if k.lower() == "retry-after":
                retry_after_header_name = k
                break
        duration = 1
        if retry_after_header_name is None:
            # This situation usually does not arise. Just in case.
            duration += random.random()
        else:
            duration = (int(response.headers.get(retry_after_header_name)[0]) +
                        random.random())
        await asyncio.sleep(duration)
        state.increment_current_attempt()
Exemplo n.º 2
0
 async def prepare_for_next_attempt_async(
     self,
     *,
     state: RetryState,
     request: HttpRequest,
     response: Optional[HttpResponse] = None,
     error: Optional[Exception] = None,
 ) -> None:
     state.next_attempt_requested = True
     duration = self.interval_calculator.calculate_sleep_duration(
         state.current_attempt)
     await asyncio.sleep(duration)
     state.increment_current_attempt()
Exemplo n.º 3
0
    async def _perform_http_request(
        self, *, body: Dict[str, Any], headers: Dict[str, str]
    ) -> WebhookResponse:
        str_body: str = json.dumps(body)
        headers["Content-Type"] = "application/json;charset=utf-8"

        session: Optional[ClientSession] = None
        use_running_session = self.session and not self.session.closed
        if use_running_session:
            session = self.session
        else:
            session = aiohttp.ClientSession(
                timeout=aiohttp.ClientTimeout(total=self.timeout),
                auth=self.auth,
                trust_env=self.trust_env_in_session,
            )

        last_error: Optional[Exception] = None
        resp: Optional[WebhookResponse] = None
        try:
            request_kwargs = {
                "headers": headers,
                "data": str_body,
                "ssl": self.ssl,
                "proxy": self.proxy,
            }
            retry_request = RetryHttpRequest(
                method="POST",
                url=self.url,
                headers=headers,
                body_params=body,
            )

            retry_state = RetryState()
            counter_for_safety = 0
            while counter_for_safety < 100:
                counter_for_safety += 1
                # If this is a retry, the next try started here. We can reset the flag.
                retry_state.next_attempt_requested = False
                retry_response: Optional[RetryHttpResponse] = None
                response_body = ""

                if self.logger.level <= logging.DEBUG:
                    self.logger.debug(
                        f"Sending a request - url: {self.url}, body: {str_body}, headers: {headers}"
                    )

                try:
                    async with session.request(
                        "POST", self.url, **request_kwargs
                    ) as res:
                        try:
                            response_body = await res.text()
                            retry_response = RetryHttpResponse(
                                status_code=res.status,
                                headers=res.headers,
                                data=response_body.encode("utf-8")
                                if response_body is not None
                                else None,
                            )
                        except aiohttp.ContentTypeError:
                            self.logger.debug(
                                f"No response data returned from the following API call: {self.url}"
                            )

                        if res.status == 429:
                            for handler in self.retry_handlers:
                                if await handler.can_retry_async(
                                    state=retry_state,
                                    request=retry_request,
                                    response=retry_response,
                                ):
                                    if self.logger.level <= logging.DEBUG:
                                        self.logger.info(
                                            f"A retry handler found: {type(handler).__name__} "
                                            f"for POST {self.url} - rate_limited"
                                        )
                                    await handler.prepare_for_next_attempt_async(
                                        state=retry_state,
                                        request=retry_request,
                                        response=retry_response,
                                    )
                                    break

                        if retry_state.next_attempt_requested is False:
                            resp = WebhookResponse(
                                url=self.url,
                                status_code=res.status,
                                body=response_body,
                                headers=res.headers,
                            )
                            _debug_log_response(self.logger, resp)
                            return resp

                except Exception as e:
                    last_error = e
                    for handler in self.retry_handlers:
                        if await handler.can_retry_async(
                            state=retry_state,
                            request=retry_request,
                            response=retry_response,
                            error=e,
                        ):
                            if self.logger.level <= logging.DEBUG:
                                self.logger.info(
                                    f"A retry handler found: {type(handler).__name__} "
                                    f"for POST {self.url} - {e}"
                                )
                            await handler.prepare_for_next_attempt_async(
                                state=retry_state,
                                request=retry_request,
                                response=retry_response,
                                error=e,
                            )
                            break

                    if retry_state.next_attempt_requested is False:
                        raise last_error

            if resp is not None:
                return resp
            raise last_error

        finally:
            if not use_running_session:
                await session.close()

        return resp
async def _request_with_session(
    *,
    current_session: Optional[ClientSession],
    timeout: int,
    logger: Logger,
    http_verb: str,
    api_url: str,
    req_args: dict,
    # set the default to an empty array for legacy clients
    retry_handlers: Optional[List[AsyncRetryHandler]] = None,
) -> Dict[str, Any]:
    """Submit the HTTP request with the running session or a new session.
    Returns:
        A dictionary of the response data.
    """
    retry_handlers = retry_handlers if retry_handlers is not None else []
    session = None
    use_running_session = current_session and not current_session.closed
    if use_running_session:
        session = current_session
    else:
        session = aiohttp.ClientSession(
            timeout=aiohttp.ClientTimeout(total=timeout),
            auth=req_args.pop("auth", None),
        )

    last_error: Optional[Exception] = None
    resp: Optional[Dict[str, Any]] = None
    try:
        retry_request = RetryHttpRequest(
            method=http_verb,
            url=api_url,
            headers=req_args.get("headers", {}),
            body_params=req_args.get("params"),
            data=req_args.get("data"),
        )

        retry_state = RetryState()
        counter_for_safety = 0
        while counter_for_safety < 100:
            counter_for_safety += 1
            # If this is a retry, the next try started here. We can reset the flag.
            retry_state.next_attempt_requested = False
            retry_response: Optional[RetryHttpResponse] = None

            if logger.level <= logging.DEBUG:

                def convert_params(values: dict) -> dict:
                    if not values or not isinstance(values, dict):
                        return {}
                    return {
                        k: ("(bytes)" if isinstance(v, bytes) else v)
                        for k, v in values.items()
                    }

                headers = {
                    k: "(redacted)" if k.lower() == "authorization" else v
                    for k, v in req_args.get("headers", {}).items()
                }
                logger.debug(
                    f"Sending a request - url: {http_verb} {api_url}, "
                    f"params: {convert_params(req_args.get('params'))}, "
                    f"files: {convert_params(req_args.get('files'))}, "
                    f"data: {convert_params(req_args.get('data'))}, "
                    f"json: {convert_params(req_args.get('json'))}, "
                    f"proxy: {convert_params(req_args.get('proxy'))}, "
                    f"headers: {headers}")

            try:
                async with session.request(http_verb, api_url,
                                           **req_args) as res:
                    data: Union[dict, bytes] = {}
                    if res.content_type == "application/gzip":
                        # admin.analytics.getFile
                        data = await res.read()
                        retry_response = RetryHttpResponse(
                            status_code=res.status,
                            headers=res.headers,
                            data=data,
                        )
                    else:
                        try:
                            data = await res.json()
                            retry_response = RetryHttpResponse(
                                status_code=res.status,
                                headers=res.headers,
                                body=data,
                            )
                        except aiohttp.ContentTypeError:
                            logger.debug(
                                f"No response data returned from the following API call: {api_url}."
                            )
                        except json.decoder.JSONDecodeError:
                            try:
                                body: str = await res.text()
                                message = _build_unexpected_body_error_message(
                                    body)
                                raise SlackApiError(message, res)
                            except Exception as e:
                                raise SlackApiError(
                                    f"Unexpectedly failed to read the response body: {str(e)}",
                                    res,
                                )

                    if logger.level <= logging.DEBUG:
                        body = data if isinstance(data, dict) else "(binary)"
                        logger.debug("Received the following response - "
                                     f"status: {res.status}, "
                                     f"headers: {dict(res.headers)}, "
                                     f"body: {body}")

                    if res.status == 429:
                        for handler in retry_handlers:
                            if await handler.can_retry_async(
                                    state=retry_state,
                                    request=retry_request,
                                    response=retry_response,
                            ):
                                if logger.level <= logging.DEBUG:
                                    logger.info(
                                        f"A retry handler found: {type(handler).__name__} "
                                        f"for {http_verb} {api_url} - rate_limited"
                                    )
                                await handler.prepare_for_next_attempt_async(
                                    state=retry_state,
                                    request=retry_request,
                                    response=retry_response,
                                )
                                break

                    if retry_state.next_attempt_requested is False:
                        response = {
                            "data": data,
                            "headers": res.headers,
                            "status_code": res.status,
                        }
                        return response

            except Exception as e:
                last_error = e
                for handler in retry_handlers:
                    if await handler.can_retry_async(
                            state=retry_state,
                            request=retry_request,
                            response=retry_response,
                            error=e,
                    ):
                        if logger.level <= logging.DEBUG:
                            logger.info(
                                f"A retry handler found: {type(handler).__name__} "
                                f"for {http_verb} {api_url} - {e}")
                        await handler.prepare_for_next_attempt_async(
                            state=retry_state,
                            request=retry_request,
                            response=retry_response,
                            error=e,
                        )
                        break

                if retry_state.next_attempt_requested is False:
                    raise last_error

        if resp is not None:
            return resp
        raise last_error

    finally:
        if not use_running_session:
            await session.close()

    return response
Exemplo n.º 5
0
    def _perform_http_request(
        self,
        *,
        http_verb: str = "GET",
        url: str,
        body: Optional[Dict[str, Any]] = None,
        headers: Dict[str, str],
    ) -> SCIMResponse:
        if body is not None:
            if body.get("schemas") is None:
                body["schemas"] = ["urn:scim:schemas:core:1.0"]
            body = json.dumps(body)
        headers["Content-Type"] = "application/json;charset=utf-8"

        if self.logger.level <= logging.DEBUG:
            headers_for_logging = {
                k: "(redacted)" if k.lower() == "authorization" else v
                for k, v in headers.items()
            }
            self.logger.debug(
                f"Sending a request - {http_verb} url: {url}, body: {body}, headers: {headers_for_logging}"
            )

        # NOTE: Intentionally ignore the `http_verb` here
        # Slack APIs accepts any API method requests with POST methods
        req = Request(
            method=http_verb,
            url=url,
            data=body.encode("utf-8") if body is not None else None,
            headers=headers,
        )
        resp = None
        last_error = None

        retry_state = RetryState()
        counter_for_safety = 0
        while counter_for_safety < 100:
            counter_for_safety += 1
            # If this is a retry, the next try started here. We can reset the flag.
            retry_state.next_attempt_requested = False

            try:
                resp = self._perform_http_request_internal(url, req)
                # The resp is a 200 OK response
                return resp

            except HTTPError as e:
                # read the response body here
                charset = e.headers.get_content_charset() or "utf-8"
                response_body: str = e.read().decode(charset)
                # As adding new values to HTTPError#headers can be ignored, building a new dict object here
                response_headers = dict(e.headers.items())
                resp = SCIMResponse(
                    url=url,
                    status_code=e.code,
                    raw_body=response_body,
                    headers=response_headers,
                )
                if e.code == 429:
                    # for backward-compatibility with WebClient (v.2.5.0 or older)
                    if ("retry-after" not in resp.headers
                            and "Retry-After" in resp.headers):
                        resp.headers["retry-after"] = resp.headers[
                            "Retry-After"]
                    if ("Retry-After" not in resp.headers
                            and "retry-after" in resp.headers):
                        resp.headers["Retry-After"] = resp.headers[
                            "retry-after"]
                _debug_log_response(self.logger, resp)

                # Try to find a retry handler for this error
                retry_request = RetryHttpRequest.from_urllib_http_request(req)
                retry_response = RetryHttpResponse(
                    status_code=e.code,
                    headers={k: [v]
                             for k, v in e.headers.items()},
                    data=response_body.encode("utf-8")
                    if response_body is not None else None,
                )
                for handler in self.retry_handlers:
                    if handler.can_retry(
                            state=retry_state,
                            request=retry_request,
                            response=retry_response,
                            error=e,
                    ):
                        if self.logger.level <= logging.DEBUG:
                            self.logger.info(
                                f"A retry handler found: {type(handler).__name__} for {req.method} {req.full_url} - {e}"
                            )
                        handler.prepare_for_next_attempt(
                            state=retry_state,
                            request=retry_request,
                            response=retry_response,
                            error=e,
                        )
                        break

                if retry_state.next_attempt_requested is False:
                    return resp

            except Exception as err:
                last_error = err
                self.logger.error(
                    f"Failed to send a request to Slack API server: {err}")

                # Try to find a retry handler for this error
                retry_request = RetryHttpRequest.from_urllib_http_request(req)
                for handler in self.retry_handlers:
                    if handler.can_retry(
                            state=retry_state,
                            request=retry_request,
                            response=None,
                            error=err,
                    ):
                        if self.logger.level <= logging.DEBUG:
                            self.logger.info(
                                f"A retry handler found: {type(handler).__name__} for {req.method} {req.full_url} - {err}"
                            )
                        handler.prepare_for_next_attempt(
                            state=retry_state,
                            request=retry_request,
                            response=None,
                            error=err,
                        )
                        self.logger.info(
                            f"Going to retry the same request: {req.method} {req.full_url}"
                        )
                        break

                if retry_state.next_attempt_requested is False:
                    raise err

        if resp is not None:
            return resp
        raise last_error
Exemplo n.º 6
0
    async def _perform_http_request(
        self,
        *,
        http_verb: str,
        url: str,
        body_params: Optional[Dict[str, Any]],
        headers: Dict[str, str],
    ) -> SCIMResponse:
        if body_params is not None:
            if body_params.get("schemas") is None:
                body_params["schemas"] = ["urn:scim:schemas:core:1.0"]
            body_params = json.dumps(body_params)
        headers["Content-Type"] = "application/json;charset=utf-8"

        session: Optional[ClientSession] = None
        use_running_session = self.session and not self.session.closed
        if use_running_session:
            session = self.session
        else:
            session = aiohttp.ClientSession(
                timeout=aiohttp.ClientTimeout(total=self.timeout),
                auth=self.auth,
                trust_env=self.trust_env_in_session,
            )

        last_error: Optional[Exception] = None
        resp: Optional[SCIMResponse] = None
        try:
            request_kwargs = {
                "headers": headers,
                "data": body_params,
                "ssl": self.ssl,
                "proxy": self.proxy,
            }
            retry_request = RetryHttpRequest(
                method=http_verb,
                url=url,
                headers=headers,
                body_params=body_params,
            )

            retry_state = RetryState()
            counter_for_safety = 0
            while counter_for_safety < 100:
                counter_for_safety += 1
                # If this is a retry, the next try started here. We can reset the flag.
                retry_state.next_attempt_requested = False
                retry_response: Optional[RetryHttpResponse] = None
                response_body = ""

                if self.logger.level <= logging.DEBUG:
                    headers_for_logging = {
                        k: "(redacted)" if k.lower() == "authorization" else v
                        for k, v in headers.items()
                    }
                    self.logger.debug(
                        f"Sending a request - url: {url}, params: {body_params}, headers: {headers_for_logging}"
                    )

                try:
                    async with session.request(http_verb, url,
                                               **request_kwargs) as res:
                        try:
                            response_body = await res.text()
                            retry_response = RetryHttpResponse(
                                status_code=res.status,
                                headers=res.headers,
                                data=response_body.encode("utf-8")
                                if response_body is not None else None,
                            )
                        except aiohttp.ContentTypeError:
                            self.logger.debug(
                                f"No response data returned from the following API call: {url}."
                            )

                        if res.status == 429:
                            for handler in self.retry_handlers:
                                if await handler.can_retry_async(
                                        state=retry_state,
                                        request=retry_request,
                                        response=retry_response,
                                ):
                                    if self.logger.level <= logging.DEBUG:
                                        self.logger.info(
                                            f"A retry handler found: {type(handler).__name__} "
                                            f"for {http_verb} {url} - rate_limited"
                                        )
                                    await handler.prepare_for_next_attempt_async(
                                        state=retry_state,
                                        request=retry_request,
                                        response=retry_response,
                                    )
                                    break

                        if retry_state.next_attempt_requested is False:
                            resp = SCIMResponse(
                                url=url,
                                status_code=res.status,
                                raw_body=response_body,
                                headers=res.headers,
                            )
                            _debug_log_response(self.logger, resp)
                            return resp

                except Exception as e:
                    last_error = e
                    for handler in self.retry_handlers:
                        if await handler.can_retry_async(
                                state=retry_state,
                                request=retry_request,
                                response=retry_response,
                                error=e,
                        ):
                            if self.logger.level <= logging.DEBUG:
                                self.logger.info(
                                    f"A retry handler found: {type(handler).__name__} "
                                    f"for {http_verb} {url} - {e}")
                            await handler.prepare_for_next_attempt_async(
                                state=retry_state,
                                request=retry_request,
                                response=retry_response,
                                error=e,
                            )
                            break

                    if retry_state.next_attempt_requested is False:
                        raise last_error

            if resp is not None:
                return resp
            raise last_error

        finally:
            if not use_running_session:
                await session.close()

        return resp
Exemplo n.º 7
0
    def _perform_urllib_http_request(
            self, *, url: str, args: Dict[str, Dict[str,
                                                    Any]]) -> Dict[str, Any]:
        """Performs an HTTP request and parses the response.

        Args:
            url: Complete URL (e.g., https://www.slack.com/api/chat.postMessage)
            args: args has "headers", "data", "params", and "json"
                "headers": Dict[str, str]
                "data": Dict[str, Any]
                "params": Dict[str, str],
                "json": Dict[str, Any],

        Returns:
            dict {status: int, headers: Headers, body: str}
        """
        headers = args["headers"]
        if args["json"]:
            body = json.dumps(args["json"])
            headers["Content-Type"] = "application/json;charset=utf-8"
        elif args["data"]:
            boundary = f"--------------{uuid.uuid4()}"
            sep_boundary = b"\r\n--" + boundary.encode("ascii")
            end_boundary = sep_boundary + b"--\r\n"
            body = io.BytesIO()
            data = args["data"]
            for key, value in data.items():
                readable = getattr(value, "readable", None)
                if readable and value.readable():
                    filename = "Uploaded file"
                    name_attr = getattr(value, "name", None)
                    if name_attr:
                        filename = (name_attr.decode("utf-8") if isinstance(
                            name_attr, bytes) else name_attr)
                    if "filename" in data:
                        filename = data["filename"]
                    mimetype = (mimetypes.guess_type(filename)[0]
                                or "application/octet-stream")
                    title = (
                        f'\r\nContent-Disposition: form-data; name="{key}"; filename="{filename}"\r\n'
                        + f"Content-Type: {mimetype}\r\n")
                    value = value.read()
                else:
                    title = f'\r\nContent-Disposition: form-data; name="{key}"\r\n'
                    value = str(value).encode("utf-8")
                body.write(sep_boundary)
                body.write(title.encode("utf-8"))
                body.write(b"\r\n")
                body.write(value)

            body.write(end_boundary)
            body = body.getvalue()
            headers[
                "Content-Type"] = f"multipart/form-data; boundary={boundary}"
            headers["Content-Length"] = len(body)
        elif args["params"]:
            body = urlencode(args["params"])
            headers["Content-Type"] = "application/x-www-form-urlencoded"
        else:
            body = None

        if isinstance(body, str):
            body = body.encode("utf-8")

        # NOTE: Intentionally ignore the `http_verb` here
        # Slack APIs accepts any API method requests with POST methods
        req = Request(method="POST", url=url, data=body, headers=headers)
        resp = None
        last_error = None

        retry_state = RetryState()
        counter_for_safety = 0
        while counter_for_safety < 100:
            counter_for_safety += 1
            # If this is a retry, the next try started here. We can reset the flag.
            retry_state.next_attempt_requested = False

            try:
                resp = self._perform_urllib_http_request_internal(url, req)
                # The resp is a 200 OK response
                return resp

            except HTTPError as e:
                # As adding new values to HTTPError#headers can be ignored, building a new dict object here
                response_headers = dict(e.headers.items())
                resp = {"status": e.code, "headers": response_headers}
                if e.code == 429:
                    # for compatibility with aiohttp
                    if ("retry-after" not in response_headers
                            and "Retry-After" in response_headers):
                        response_headers["retry-after"] = response_headers[
                            "Retry-After"]
                    if ("Retry-After" not in response_headers
                            and "retry-after" in response_headers):
                        response_headers["Retry-After"] = response_headers[
                            "retry-after"]

                # read the response body here
                charset = e.headers.get_content_charset() or "utf-8"
                response_body: str = e.read().decode(charset)
                resp["body"] = response_body

                # Try to find a retry handler for this error
                retry_request = RetryHttpRequest.from_urllib_http_request(req)
                retry_response = RetryHttpResponse(
                    status_code=e.code,
                    headers={k: [v]
                             for k, v in response_headers.items()},
                    data=response_body.encode("utf-8")
                    if response_body is not None else None,
                )
                for handler in self.retry_handlers:
                    if handler.can_retry(
                            state=retry_state,
                            request=retry_request,
                            response=retry_response,
                            error=e,
                    ):
                        if self._logger.level <= logging.DEBUG:
                            self._logger.info(
                                f"A retry handler found: {type(handler).__name__} for {req.method} {req.full_url} - {e}"
                            )
                        handler.prepare_for_next_attempt(
                            state=retry_state,
                            request=retry_request,
                            response=retry_response,
                            error=e,
                        )
                        break

                if retry_state.next_attempt_requested is False:
                    return resp

            except Exception as err:
                last_error = err
                self._logger.error(
                    f"Failed to send a request to Slack API server: {err}")

                # Try to find a retry handler for this error
                retry_request = RetryHttpRequest.from_urllib_http_request(req)
                for handler in self.retry_handlers:
                    if handler.can_retry(
                            state=retry_state,
                            request=retry_request,
                            response=None,
                            error=err,
                    ):
                        if self._logger.level <= logging.DEBUG:
                            self._logger.info(
                                f"A retry handler found: {type(handler).__name__} for {req.method} {req.full_url} - {err}"
                            )
                        handler.prepare_for_next_attempt(
                            state=retry_state,
                            request=retry_request,
                            response=None,
                            error=err,
                        )
                        self._logger.info(
                            f"Going to retry the same request: {req.method} {req.full_url}"
                        )
                        break

                if retry_state.next_attempt_requested is False:
                    raise err

        if resp is not None:
            return resp
        raise last_error