Единая классификация ошибок установки соединения (L3/L4/DNS).
(error: Exception, bytes_read: int, stage: str = "unknown")
| 116 | |
| 117 | |
| 118 | def classify_connect_error(error: Exception, bytes_read: int, stage: str = "unknown") -> Tuple[str, str, int]: |
| 119 | """Единая классификация ошибок установки соединения (L3/L4/DNS).""" |
| 120 | full_text = collect_error_text(error) |
| 121 | err_errno = get_errno_from_chain(error) |
| 122 | |
| 123 | if isinstance(error, httpx.PoolTimeout) or "pool timeout" in full_text: |
| 124 | return ("[magenta]POOL TIMEOUT[/magenta]", "Нехватка сокетов, снизьте параллелизм", bytes_read) |
| 125 | |
| 126 | if isinstance(error, httpx.ConnectTimeout) or "connect timeout" in full_text or "timed out" in full_text: |
| 127 | if stage == "tls_handshake": |
| 128 | return ("[bold red]TLS DROP[/bold red]", "TLS Handshake timeout", bytes_read) |
| 129 | elif stage == "tcp_connect": |
| 130 | return ("[bold red]SYN DROP[/bold red]", "TCP SYN timeout", bytes_read) |
| 131 | elif stage == "sending_data": |
| 132 | return ("[red]SEND TIMEOUT[/red]", "Таймаут отправки данных", bytes_read) |
| 133 | elif stage == "reading_data": |
| 134 | return ("[red]READ TIMEOUT[/red]", "Таймаут чтения данных", bytes_read) |
| 135 | else: |
| 136 | return ("[red]TIMEOUT[/red]", f"Timeout ({stage})", bytes_read) |
| 137 | |
| 138 | # DNS |
| 139 | gai = find_cause(error, socket.gaierror) |
| 140 | if gai is not None: |
| 141 | gai_errno = getattr(gai, 'errno', None) |
| 142 | if gai_errno in (socket.EAI_NONAME, 11001): |
| 143 | return ("[yellow]DNS FAIL[/yellow]", "Домен не найден", bytes_read) |
| 144 | elif gai_errno in (getattr(socket, 'EAI_AGAIN', -3), 11002): |
| 145 | if "connection" in full_text and any(x in full_text for x in ("reset", "refused", "closed")): |
| 146 | return ("[yellow]DNS FAIL[/yellow]", "DNS ошибка/дроп", bytes_read) |
| 147 | return ("[yellow]DNS FAIL[/yellow]", "DNS таймаут/недоступен", bytes_read) |
| 148 | else: |
| 149 | return ("[yellow]DNS FAIL[/yellow]", "Ошибка DNS", bytes_read) |
| 150 | |
| 151 | if any(x in full_text for x in [ |
| 152 | "getaddrinfo failed", "name resolution", "11001", "11002", |
| 153 | "name or service not known", "nodename nor servname" |
| 154 | ]): |
| 155 | return ("[yellow]DNS FAIL[/yellow]", "Ошибка DNS", bytes_read) |
| 156 | |
| 157 | # TLS ALERT внутри ConnectError (DPI) |
| 158 | if "sslv3_alert" in full_text or "ssl alert" in full_text or ("alert" in full_text and "handshake" in full_text): |
| 159 | if "handshake_failure" in full_text or "handshake failure" in full_text: |
| 160 | return ("[bold red]TLS ALERT[/bold red]", "Handshake alert", bytes_read) |
| 161 | elif "unrecognized_name" in full_text: |
| 162 | return ("[bold red]TLS ALERT[/bold red]", "SNI alert", bytes_read) |
| 163 | elif "protocol_version" in full_text or "alert_protocol_version" in full_text: |
| 164 | return ("[bold red]TLS ALERT[/bold red]", "Version alert", bytes_read) |
| 165 | else: |
| 166 | return ("[bold red]TLS ALERT[/bold red]", "TLS alert", bytes_read) |
| 167 | |
| 168 | ssl_err = find_cause(error, ssl.SSLError) |
| 169 | if ssl_err is not None: |
| 170 | return classify_ssl_error(ssl_err, bytes_read) |
| 171 | |
| 172 | # TCP ОШИБКИ (L4) |
| 173 | if find_cause(error, ConnectionRefusedError) is not None or err_errno in (errno.ECONNREFUSED, config.WSAECONNREFUSED) or "refused" in full_text: |
| 174 | return ("[bold red]REFUSED[/bold red]", "TCP соединение отклонено", bytes_read) |
| 175 |
no test coverage detected