|
30 | 30 | BUILTIN_EXCEPTIONS, |
31 | 31 | DEFAULT_CA_CERTS, |
32 | 32 | RERAISE_EXCEPTIONS, |
| 33 | + BaseNode, |
33 | 34 | NodeApiResponse, |
34 | 35 | ssl_context_from_node_config, |
35 | 36 | ) |
|
45 | 46 | _HTTPX_META_VERSION = "" |
46 | 47 |
|
47 | 48 |
|
| 49 | +class HttpxHttpNode(BaseNode): |
| 50 | + _CLIENT_META_HTTP_CLIENT = ("hx", _HTTPX_META_VERSION) |
| 51 | + |
| 52 | + def __init__(self, config: NodeConfig): |
| 53 | + if not _HTTPX_AVAILABLE: # pragma: nocover |
| 54 | + raise ValueError("You must have 'httpx' installed to use HttpxNode") |
| 55 | + super().__init__(config) |
| 56 | + |
| 57 | + if config.ssl_assert_fingerprint: |
| 58 | + raise ValueError( |
| 59 | + "httpx does not support certificate pinning. https://github.com/encode/httpx/issues/761" |
| 60 | + ) |
| 61 | + |
| 62 | + ssl_context: Union[ssl.SSLContext, Literal[False]] = False |
| 63 | + if config.scheme == "https": |
| 64 | + if config.ssl_context is not None: |
| 65 | + ssl_context = ssl_context_from_node_config(config) |
| 66 | + else: |
| 67 | + ssl_context = ssl_context_from_node_config(config) |
| 68 | + |
| 69 | + ca_certs = ( |
| 70 | + DEFAULT_CA_CERTS if config.ca_certs is None else config.ca_certs |
| 71 | + ) |
| 72 | + if config.verify_certs: |
| 73 | + if not ca_certs: |
| 74 | + raise ValueError( |
| 75 | + "Root certificates are missing for certificate " |
| 76 | + "validation. Either pass them in using the ca_certs parameter or " |
| 77 | + "install certifi to use it automatically." |
| 78 | + ) |
| 79 | + else: |
| 80 | + if config.ssl_show_warn: |
| 81 | + warnings.warn( |
| 82 | + f"Connecting to {self.base_url!r} using TLS with verify_certs=False is insecure", |
| 83 | + stacklevel=warn_stacklevel(), |
| 84 | + category=SecurityWarning, |
| 85 | + ) |
| 86 | + |
| 87 | + if ca_certs is not None: |
| 88 | + if os.path.isfile(ca_certs): |
| 89 | + ssl_context.load_verify_locations(cafile=ca_certs) |
| 90 | + elif os.path.isdir(ca_certs): |
| 91 | + ssl_context.load_verify_locations(capath=ca_certs) |
| 92 | + else: |
| 93 | + raise ValueError("ca_certs parameter is not a path") |
| 94 | + |
| 95 | + # Use client_cert and client_key variables for SSL certificate configuration. |
| 96 | + if config.client_cert and not os.path.isfile(config.client_cert): |
| 97 | + raise ValueError("client_cert is not a path to a file") |
| 98 | + if config.client_key and not os.path.isfile(config.client_key): |
| 99 | + raise ValueError("client_key is not a path to a file") |
| 100 | + if config.client_cert and config.client_key: |
| 101 | + ssl_context.load_cert_chain(config.client_cert, config.client_key) |
| 102 | + elif config.client_cert: |
| 103 | + ssl_context.load_cert_chain(config.client_cert) |
| 104 | + |
| 105 | + self.client = httpx.Client( |
| 106 | + base_url=f"{config.scheme}://{config.host}:{config.port}", |
| 107 | + limits=httpx.Limits(max_connections=config.connections_per_node), |
| 108 | + verify=ssl_context or False, |
| 109 | + timeout=config.request_timeout, |
| 110 | + ) |
| 111 | + |
| 112 | + def perform_request( |
| 113 | + self, |
| 114 | + method: str, |
| 115 | + target: str, |
| 116 | + body: Optional[bytes] = None, |
| 117 | + headers: Optional[HttpHeaders] = None, |
| 118 | + request_timeout: Union[DefaultType, Optional[float]] = DEFAULT, |
| 119 | + ) -> NodeApiResponse: |
| 120 | + resolved_headers = self._headers.copy() |
| 121 | + if headers: |
| 122 | + resolved_headers.update(headers) |
| 123 | + |
| 124 | + if body: |
| 125 | + if self._http_compress: |
| 126 | + resolved_body = gzip.compress(body) |
| 127 | + resolved_headers["content-encoding"] = "gzip" |
| 128 | + else: |
| 129 | + resolved_body = body |
| 130 | + else: |
| 131 | + resolved_body = None |
| 132 | + |
| 133 | + try: |
| 134 | + start = time.perf_counter() |
| 135 | + if request_timeout is DEFAULT: |
| 136 | + resp = self.client.request( |
| 137 | + method, |
| 138 | + target, |
| 139 | + content=resolved_body, |
| 140 | + headers=dict(resolved_headers), |
| 141 | + ) |
| 142 | + else: |
| 143 | + resp = self.client.request( |
| 144 | + method, |
| 145 | + target, |
| 146 | + content=resolved_body, |
| 147 | + headers=dict(resolved_headers), |
| 148 | + timeout=request_timeout, |
| 149 | + ) |
| 150 | + response_body = resp.read() |
| 151 | + duration = time.perf_counter() - start |
| 152 | + except RERAISE_EXCEPTIONS + BUILTIN_EXCEPTIONS: |
| 153 | + raise |
| 154 | + except Exception as e: |
| 155 | + err: Exception |
| 156 | + if isinstance(e, (TimeoutError, httpx.TimeoutException)): |
| 157 | + err = ConnectionTimeout( |
| 158 | + "Connection timed out during request", errors=(e,) |
| 159 | + ) |
| 160 | + elif isinstance(e, ssl.SSLError): |
| 161 | + err = TlsError(str(e), errors=(e,)) |
| 162 | + # Detect SSL errors for httpx v0.28.0+ |
| 163 | + # Needed until https://github.com/encode/httpx/issues/3350 is fixed |
| 164 | + elif isinstance(e, httpx.ConnectError) and e.__cause__: |
| 165 | + context = e.__cause__.__context__ |
| 166 | + if isinstance(context, ssl.SSLError): |
| 167 | + err = TlsError(str(context), errors=(e,)) |
| 168 | + else: |
| 169 | + err = ConnectionError(str(e), errors=(e,)) |
| 170 | + else: |
| 171 | + err = ConnectionError(str(e), errors=(e,)) |
| 172 | + self._log_request( |
| 173 | + method=method, |
| 174 | + target=target, |
| 175 | + headers=resolved_headers, |
| 176 | + body=body, |
| 177 | + exception=err, |
| 178 | + ) |
| 179 | + raise err from None |
| 180 | + |
| 181 | + meta = ApiResponseMeta( |
| 182 | + resp.status_code, |
| 183 | + resp.http_version, |
| 184 | + HttpHeaders(resp.headers), |
| 185 | + duration, |
| 186 | + self.config, |
| 187 | + ) |
| 188 | + |
| 189 | + self._log_request( |
| 190 | + method=method, |
| 191 | + target=target, |
| 192 | + headers=resolved_headers, |
| 193 | + body=body, |
| 194 | + meta=meta, |
| 195 | + response=response_body, |
| 196 | + ) |
| 197 | + |
| 198 | + return NodeApiResponse(meta, response_body) |
| 199 | + |
| 200 | + def close(self) -> None: |
| 201 | + self.client.close() |
| 202 | + |
| 203 | + |
48 | 204 | class HttpxAsyncHttpNode(BaseAsyncNode): |
49 | 205 | _CLIENT_META_HTTP_CLIENT = ("hx", _HTTPX_META_VERSION) |
50 | 206 |
|
|
0 commit comments