'application/json' ); if ( $with_token ) { if ( ! self::is_connected() ) { return new WP_Error( 'convrail_no_token', 'Convrail bridge token missing' ); } $headers['x-ocl-bridge-token'] = self::bridge_token(); } $args = array( 'method' => $method, 'headers' => $headers, 'timeout' => $timeout, ); if ( 'GET' !== $method ) { $args['headers']['Content-Type'] = 'application/json'; $args['body'] = wp_json_encode( null === $payload ? array() : $payload ); } $response = wp_remote_request( self::api_base() . $path, $args ); if ( 'GET' === $method && self::is_transient_transport_error( $response ) ) { /* * Une seule relance, courte, et pour les lectures seulement. Le 2026-09-23, la moitie des * appels du conteneur de test vers l'API tombaient en « cURL error 35 » (poignee de main * TLS coupee): quinze ecrans sur vingt affichaient « Convrail n'a pas repondu » alors * que la seconde tentative passait. Une lecture rejouee ne change rien cote serveur. * Une ecriture n'est jamais rejouee ici: sans cle d'idempotence de bout en bout, un * envoi arrive mais dont la reponse s'est perdue serait applique deux fois. */ self::log( 'Retrying ' . $method . ' ' . $path . ' after: ' . $response->get_error_message() ); if ( self::$retry_delay_ms > 0 ) { usleep( self::$retry_delay_ms * 1000 ); } $response = wp_remote_request( self::api_base() . $path, $args ); } if ( is_wp_error( $response ) ) { self::log( 'HTTP error on ' . $method . ' ' . $path . ': ' . $response->get_error_message() ); return $response; } $code = (int) wp_remote_retrieve_response_code( $response ); $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( $code >= 400 ) { self::log( 'API ' . $method . ' ' . $path . ' returned ' . $code ); return new WP_Error( 'convrail_api_error', 'Convrail API error ' . $code, array( 'status' => $code, 'body' => $body, ) ); } return is_array( $body ) ? $body : array(); } public static function post( $path, $payload, $with_token = true, $timeout = 15 ) { return self::request( 'POST', $path, $payload, $with_token, $timeout ); } /** * Envoi sans attendre la reponse, pour ce qui part depuis une page VISITEE. * * Un appel bloquant dans une page vitrine ferait attendre le visiteur sur notre API, et une * seule lenteur de notre cote ralentirait toute la boutique. Ici WordPress ouvre la connexion, * envoie, et rend la main: aucune reponse n'est lue, aucun echec n'est signale. C'est * acceptable pour une session d'atterrissage, dont la perte ne coute qu'un echelon * d'attribution, jamais une vente. * * @param string $path Chemin relatif a l'API. * @param array $payload Corps JSON. * @return void */ public static function post_async( $path, $payload ) { if ( ! self::is_connected() ) { return; } wp_remote_post( self::api_base() . $path, array( 'blocking' => false, 'timeout' => 1, 'headers' => array( 'Accept' => 'application/json', 'Content-Type' => 'application/json', 'x-ocl-bridge-token' => self::bridge_token(), ), 'body' => wp_json_encode( $payload ), ) ); } public static function put( $path, $payload ) { return self::request( 'PUT', $path, $payload, true ); } public static function get( $path ) { return self::request( 'GET', $path ); } /** Pause avant la relance d'une lecture, en millisecondes. Modifiable par les tests. */ public static $retry_delay_ms = 300; /** * Codes cURL d'une panne de TRANSPORT passagere: nom introuvable (6), connexion refusee (7), * delai depasse (28), poignee de main TLS coupee (35), reponse vide (52), reception * interrompue (56). Une reponse HTTP de l'API, meme une erreur, n'en fait jamais partie. */ const TRANSIENT_CURL_ERRORS = array( 6, 7, 28, 35, 52, 56 ); /** * Vrai si `$response` est un echec de transport qu'une seconde tentative peut resoudre. * * @param mixed $response Retour de wp_remote_request(). */ public static function is_transient_transport_error( $response ) { if ( ! is_wp_error( $response ) || 'http_request_failed' !== $response->get_error_code() ) { return false; } if ( ! preg_match( '/cURL error (\d+)/', (string) $response->get_error_message(), $match ) ) { return false; } return in_array( (int) $match[1], self::TRANSIENT_CURL_ERRORS, true ); } /** * Vrai si l'erreur vient du transport (l'API n'a pas repondu), faux si l'API a repondu par une * erreur HTTP ou si la boutique n'est pas connectee. * * @param mixed $error Retour de request(). */ public static function is_transport_error( $error ) { return is_wp_error( $error ) && 'http_request_failed' === $error->get_error_code(); } /** * Code HTTP porte par un WP_Error renvoye par request(), 0 si erreur reseau. */ public static function error_status( $error ) { if ( ! is_wp_error( $error ) ) { return 0; } $data = $error->get_error_data(); return is_array( $data ) && isset( $data['status'] ) ? (int) $data['status'] : 0; } public static function log( $message ) { if ( function_exists( 'wc_get_logger' ) ) { wc_get_logger()->info( $message, array( 'source' => 'convrail' ) ); } } }