0, 'data' => null, 'body' => '', 'error' => self::with_transport_hint( CSDT_Error_Text::in_seconds( $resp->get_error_message() ) ), 'too_large' => false, ); } $body = (string) wp_remote_retrieve_body( $resp ); $too_large = $max_bytes > 0 && strlen( $body ) > $max_bytes; $data = ( '' === $body || $too_large ) ? null : json_decode( $body, true ); return array( 'code' => (int) wp_remote_retrieve_response_code( $resp ), 'data' => is_array( $data ) ? $data : null, 'body' => $body, 'error' => '', 'too_large' => $too_large, ); } /** * A transport error with the one sentence that says what to do about it. * * "cURL error 7: Failed to connect to api.anthropic.com port 443" is accurate and, to a site * owner, opaque: it does not say the fault is between their server and the provider rather * than in their key or settings, which is the first thing they need to know (a customer saw a * bare cURL failure on Test Key, 2026-10-28). The original text is kept in front, unchanged, * so anything matching on it (the timeout and failover checks) still matches. * * @since 1.10.1075 * @param string $msg Transport error text. * @return string The same text, plus a hint when the cURL code is one we can explain. */ public static function with_transport_hint( string $msg ): string { if ( ! preg_match( '/cURL error (\d+)/i', $msg, $m ) ) { return $msg; } $hint = self::transport_hint( (int) $m[1] ); return '' === $hint ? $msg : rtrim( $msg, '. ' ) . '. ' . $hint; } /** * What a cURL error number means for the person reading it, '' for one we do not explain. * * @since 1.10.1075 * @param int $code cURL error number. * @return string */ public static function transport_hint( int $code ): string { switch ( $code ) { case 5: case 6: return 'Your server could not look up the service\'s address (DNS). Your key and settings are not the problem; ask your host to check the server\'s DNS and outbound internet access.'; case 7: return 'Your server could not open a connection to the service. Your key and settings are not the problem; a firewall or your host is likely blocking outbound HTTPS (port 443). Ask your host to allow it.'; case 28: return 'The service did not answer in time. Try again in a minute; if it keeps happening, your host may be slowing or filtering outbound requests.'; case 35: case 51: case 58: case 60: case 77: return 'The secure (TLS) connection could not be set up. Your server\'s CA certificates or OpenSSL are likely out of date; ask your host to update them.'; case 52: case 55: case 56: return 'The connection was dropped before the service answered, often by a proxy or firewall on your host. Try again; if it repeats, ask your host.'; default: return 'This is a network problem between your server and the service, not your key or settings. Your host can look up the cURL error number.'; } } /** * The explanation an API wrote in its body, or '' when it wrote none. * * Covers the two shapes the services here use: {"error":"text"} (the CloudScale proxy, * the broker) and {"error":{"message":"text"}} (Anthropic, OpenAI, Gemini), plus * Telegram's {"description":"text"}. * * @since 1.10.934 * @param array $r A json() result. * @return string */ public static function api_error( array $r ): string { $data = $r['data']; if ( ! is_array( $data ) ) { return ''; } $err = $data['error'] ?? null; if ( is_string( $err ) && '' !== trim( $err ) ) { return trim( $err ); } if ( is_array( $err ) ) { foreach ( array( 'message', 'status' ) as $field ) { if ( isset( $err[ $field ] ) && is_string( $err[ $field ] ) && '' !== trim( $err[ $field ] ) ) { return trim( $err[ $field ] ); } } } if ( isset( $data['description'] ) && is_string( $data['description'] ) && '' !== trim( $data['description'] ) ) { return trim( $data['description'] ); } return ''; } /** * The service's own explanation when it gave one, otherwise describe(). * * For messages that quote a provider directly ("Rate limit reached for model x"), where a * status prefix adds nothing the provider did not already say. * * @since 1.10.934 * @param array $r A json() result. * @return string */ public static function reason( array $r ): string { $said = 0 === $r['code'] ? '' : self::api_error( $r ); return '' !== $said ? $said : self::describe( $r ); } /** * One line saying what went wrong, for a message or a log. * * Names the three cases differently because they have different fixes: no response is the * network, a status without JSON is whatever sits in front of the service, and a status * with JSON is the service itself, whose own words are included. * * @since 1.10.934 * @param array $r A json() result. * @return string */ public static function describe( array $r ): string { if ( 0 === $r['code'] ) { return 'network error, ' . $r['error']; } if ( $r['too_large'] ) { return 'HTTP ' . $r['code'] . ', and the response was too large to read'; } if ( null === $r['data'] ) { return 'HTTP ' . $r['code'] . ', and the response was not JSON, so something in front of the service answered (a CDN, proxy or web server), not the service itself'; } $said = self::api_error( $r ); return 'HTTP ' . $r['code'] . ( '' !== $said ? ': ' . $said : '' ); } }