Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement requests-like exception hierarchy, close #201 #250

Merged
merged 7 commits into from
Aug 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions curl_cffi/curl.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
import warnings
from http.cookies import SimpleCookie
from pathlib import Path
from typing import Any, List, Optional, Tuple, Union
from typing import Any, List, Literal, Optional, Tuple, Union, cast

import certifi

from ._wrapper import ffi, lib
from .const import CurlHttpVersion, CurlInfo, CurlOpt, CurlWsFlag
from .const import CurlECode, CurlHttpVersion, CurlInfo, CurlOpt, CurlWsFlag

DEFAULT_CACERT = certifi.where()
REASON_PHRASE_RE = re.compile(rb"HTTP/\d\.\d [0-9]{3} (.*)")
Expand All @@ -17,9 +17,9 @@
class CurlError(Exception):
"""Base exception for curl_cffi package"""

def __init__(self, msg, code: int = 0, *args, **kwargs):
def __init__(self, msg, code: Union[CurlECode, Literal[0]] = 0, *args, **kwargs):
super().__init__(msg, *args, **kwargs)
self.code = code
self.code: Union[CurlECode, Literal[0]] = code


CURLINFO_TEXT = 0
Expand Down Expand Up @@ -143,7 +143,7 @@ def _get_error(self, errcode: int, *args: Any):
return CurlError(
f"Failed to {action}, curl: ({errcode}) {errmsg}. "
"See https://curl.se/libcurl/c/libcurl-errors.html first for more details.",
code=errcode,
code=cast(CurlECode, errcode),
)

def setopt(self, option: CurlOpt, value: Any) -> int:
Expand Down
20 changes: 5 additions & 15 deletions curl_cffi/requests/errors.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,7 @@
from .. import CurlError


class RequestsError(CurlError):
"""Base exception for curl_cffi.requests package"""

def __init__(self, msg, code=0, response=None, *args, **kwargs):
super().__init__(msg, code, *args, **kwargs)
self.response = response
# for compatibility with 0.5.x

__all__ = ["CurlError", "RequestsError", "CookieConflict", "SessionClosed"]

class CookieConflict(RequestsError):
pass


class SessionClosed(RequestsError):
pass
from .. import CurlError
from .exceptions import CookieConflict, SessionClosed
from .exceptions import RequestException as RequestsError
218 changes: 216 additions & 2 deletions curl_cffi/requests/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,217 @@
from .errors import RequestsError
# Apache 2.0 License
# Vendored from https://github.com/psf/requests/blob/main/src/requests/exceptions.py
# With our own addtions

RequestsException = RequestsError
import json
from typing import Literal, Union

from .. import CurlError
from ..const import CurlECode


# Note IOError is an alias of OSError in Python 3.x
class RequestException(CurlError, OSError):
"""Base exception for curl_cffi.requests package"""

def __init__(self, msg, code: Union[CurlECode, Literal[0]] = 0, response=None, *args, **kwargs):
super().__init__(msg, code, *args, **kwargs)
self.response = response


class CookieConflict(RequestException):
"""Same cookie exists for different domains."""


class SessionClosed(RequestException):
"""The session has already been closed."""


class ImpersonateError(RequestException):
"""The impersonate config was wrong or impersonate failed."""


# not used
class InvalidJSONError(RequestException):
"""A JSON error occurred."""


# not used
class JSONDecodeError(InvalidJSONError, json.JSONDecodeError):
"""Couldn't decode the text into json"""


class HTTPError(RequestException):
"""An HTTP error occurred."""


class IncompleteRead(HTTPError):
"""Incomplete read of content"""


class ConnectionError(RequestException):
"""A Connection error occurred."""


class DNSError(ConnectionError):
"""Could not resolve"""


class ProxyError(RequestException):
"""A proxy error occurred."""


class SSLError(ConnectionError):
"""An SSL error occurred."""


class CertificateVerifyError(SSLError):
"""Raised when certificate validated has failed"""


class Timeout(RequestException):
"""The request timed out."""


# not used
class ConnectTimeout(ConnectionError, Timeout):
"""The request timed out while trying to connect to the remote server.

Requests that produced this error are safe to retry.
"""


# not used
class ReadTimeout(Timeout):
"""The server did not send any data in the allotted amount of time."""


# not used
class URLRequired(RequestException):
"""A valid URL is required to make a request."""


class TooManyRedirects(RequestException):
"""Too many redirects."""


# not used
class MissingSchema(RequestException, ValueError):
"""The URL scheme (e.g. http or https) is missing."""


class InvalidSchema(RequestException, ValueError):
"""The URL scheme provided is either invalid or unsupported."""


class InvalidURL(RequestException, ValueError):
"""The URL provided was somehow invalid."""


# not used
class InvalidHeader(RequestException, ValueError):
"""The header value provided was somehow invalid."""


# not used
class InvalidProxyURL(InvalidURL):
"""The proxy URL provided is invalid."""


# not used
class ChunkedEncodingError(RequestException):
"""The server declared chunked encoding but sent an invalid chunk."""


# not used
class ContentDecodingError(RequestException):
"""Failed to decode response content."""


# not used
class StreamConsumedError(RequestException, TypeError):
"""The content for this response was already consumed."""


# does not support
class RetryError(RequestException):
"""Custom retries logic failed"""


# not used
class UnrewindableBodyError(RequestException):
"""Requests encountered an error when trying to rewind a body."""


class InterfaceError(RequestException):
"""A specified outgoing interface could not be used."""


# Warnings


# not used
class RequestsWarning(Warning):
"""Base warning for Requests."""


# not used
class FileModeWarning(RequestsWarning, DeprecationWarning):
"""A file was opened in text mode, but Requests determined its binary length."""


# not used
class RequestsDependencyWarning(RequestsWarning):
"""An imported dependency doesn't match the expected version range."""


CODE2ERROR = {
0: RequestException,
CurlECode.UNSUPPORTED_PROTOCOL: InvalidSchema,
CurlECode.URL_MALFORMAT: InvalidURL,
CurlECode.COULDNT_RESOLVE_PROXY: ProxyError,
CurlECode.COULDNT_RESOLVE_HOST: DNSError,
CurlECode.COULDNT_CONNECT: ConnectionError,
CurlECode.WEIRD_SERVER_REPLY: ConnectionError,
CurlECode.REMOTE_ACCESS_DENIED: ConnectionError,
CurlECode.HTTP2: HTTPError,
CurlECode.HTTP_RETURNED_ERROR: HTTPError,
CurlECode.WRITE_ERROR: RequestException,
CurlECode.READ_ERROR: RequestException,
CurlECode.OUT_OF_MEMORY: RequestException,
CurlECode.OPERATION_TIMEDOUT: Timeout,
CurlECode.SSL_CONNECT_ERROR: SSLError,
CurlECode.INTERFACE_FAILED: InterfaceError,
CurlECode.TOO_MANY_REDIRECTS: TooManyRedirects,
CurlECode.UNKNOWN_OPTION: RequestException,
CurlECode.SETOPT_OPTION_SYNTAX: RequestException,
CurlECode.GOT_NOTHING: ConnectionError,
CurlECode.SSL_ENGINE_NOTFOUND: SSLError,
CurlECode.SSL_ENGINE_SETFAILED: SSLError,
CurlECode.SEND_ERROR: ConnectionError,
CurlECode.RECV_ERROR: ConnectionError,
CurlECode.SSL_CERTPROBLEM: SSLError,
CurlECode.SSL_CIPHER: SSLError,
CurlECode.PEER_FAILED_VERIFICATION: CertificateVerifyError,
CurlECode.BAD_CONTENT_ENCODING: HTTPError,
CurlECode.SSL_ENGINE_INITFAILED: SSLError,
CurlECode.SSL_CACERT_BADFILE: SSLError,
CurlECode.SSL_CRL_BADFILE: SSLError,
CurlECode.SSL_ISSUER_ERROR: SSLError,
CurlECode.SSL_PINNEDPUBKEYNOTMATCH: SSLError,
CurlECode.SSL_INVALIDCERTSTATUS: SSLError,
CurlECode.HTTP2_STREAM: HTTPError,
CurlECode.HTTP3: HTTPError,
CurlECode.QUIC_CONNECT_ERROR: ConnectionError,
CurlECode.PROXY: ProxyError,
CurlECode.SSL_CLIENTCERT: SSLError,
CurlECode.ECH_REQUIRED: SSLError,
CurlECode.PARTIAL_FILE: IncompleteRead,
}


# credits: https://github.com/yt-dlp/yt-dlp/blob/master/yt_dlp/networking/_curlcffi.py#L241
# Unlicense
def code2error(code: Union[CurlECode, Literal[0]], msg: str):
if code == CurlECode.RECV_ERROR and "CONNECT" in msg:
return ProxyError
return CODE2ERROR.get(code, RequestException)
8 changes: 4 additions & 4 deletions curl_cffi/requests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from .. import Curl
from .cookies import Cookies
from .errors import RequestsError
from .exceptions import HTTPError, RequestException
from .headers import Headers

CHARSET_RE = re.compile(r"charset=([\w-]+)")
Expand Down Expand Up @@ -138,7 +138,7 @@ def _decode(self, content: bytes) -> str:
def raise_for_status(self):
"""Raise an error if status code is not in [200, 400)"""
if not self.ok:
raise RequestsError(f"HTTP Error {self.status_code}: {self.reason}")
raise HTTPError(f"HTTP Error {self.status_code}: {self.reason}")

def iter_lines(self, chunk_size=None, decode_unicode=False, delimiter=None):
"""
Expand Down Expand Up @@ -179,7 +179,7 @@ def iter_content(self, chunk_size=None, decode_unicode=False):
chunk = self.queue.get()

# re-raise the exception if something wrong happened.
if isinstance(chunk, RequestsError):
if isinstance(chunk, RequestException):
self.curl.reset()
raise chunk

Expand Down Expand Up @@ -242,7 +242,7 @@ async def aiter_content(self, chunk_size=None, decode_unicode=False):
chunk = await self.queue.get()

# re-raise the exception if something wrong happened.
if isinstance(chunk, RequestsError):
if isinstance(chunk, RequestException):
await self.aclose()
raise chunk

Expand Down
Loading
Loading