شبی که سایت به‌خاطر یک سرویس بیرونی خوابید

سال ۱۳۹۷، یک سایت فروشگاهی متوسط را پشتیبانی می‌کردم. یک شب جمعه، ساعت ۲۲، تماس گرفتند که «سایت کند شده و سبد خرید کار نمی‌کند.» رفتم سراغ لاگ سرور و بعد از نیم ساعت بررسی، ریشه را پیدا کردم: افزونه‌ای که قیمت ارز را از یک سرویس بیرونی می‌گرفت، درخواست HTTP را با file_get_contents خام PHP می‌فرستاد، بدون timeout. آن شب سرویس بیرونی کند شده بود، درخواست معطل مانده بود، PHP تا max_execution_time منتظر مانده بود و هر ریکوئست، یک پردازنده را برای ۳۰ ثانیه قفل می‌کرد. نتیجه: سایت با ۵۰ کاربر همزمان، خوابید. آن شب فهمیدم که HTTP در وردپرس، یک تابع ساده نیست؛ یک لایه زیرساختی است که اگر درست استفاده نشود، سایت شما را گروگان سرویس‌های دیگر می‌کند. این مقاله، تجربه‌ام از کار با توابع HTTP API وردپرس است — نه فهرست توابع، بلکه پروتکل عملیاتی.

اگر با مفاهیم پایه آشنا نیستید، وردپرس چیست و چگونه شروع کنیم و نحوه استفاده از توابع وردپرس در پروژه‌ها پیش‌نیاز این مقاله است. مکمل این مقاله اتصال وردپرس به سرویس‌های خارجی، ساخت API اختصاصی، و امنیت API است.

چرا HTTP API وردپرس، نه توابع خام PHP؟

سه دلیل بنیادین که در پروژه‌های واقعی، استفاده از توابع خام PHP مثل curl و file_get_contents را کنار گذاشته‌ام:

  1. سازگاری با محیط‌های مختلف: HTTP API وردپرس، خودکار تشخیص می‌دهد که curl نصب است یا باید از streams استفاده کند. روی بعضی هاست‌های اشتراکی، curl غیرفعال است ولی streams کار می‌کند. توابع خام، شما را به یک روش خاص گره می‌زنند.
  2. قابلیت فیلتر شدن: هر درخواست HTTP API از فیلترهای http_request_args و http_response عبور می‌کند. یعنی افزونه‌های دیگر (مثل کش‌ساز یا لاگ‌گیر) می‌توانند درخواست‌ها را مشاهده یا تغییر دهند. این ویژگی در پروژه‌های چندساله، ارزش زیادی دارد.
  3. مدیریت خطا و timeout یکپارچه: توابع وردپرس، خروجی خطا را به شکل WP_Error برمی‌گردانند که با بقیه اکوسیستم وردپرس سازگار است. توابع خام، شما را به مدیریت خطای دستی وادار می‌کنند.

مفاهیم پایه در وردپرس چیست و ساختار هسته وردپرس آمده؛ لایه HTTP API، بخشی از هسته است که از اولین نسخه‌های وردپرس در دسترس بوده و از نسخه ۳.۷ به‌طور جدی بازنویسی شده. فهرست کامل توابع این لایه در مستندات رسمی وردپرس آمده، ولی در این مقاله روی آنچه در پروژه‌های واقعی به‌کار می‌برم تمرکز می‌کنم.

هر درخواست HTTP، یک قول از یک غریبه است. اگر قول را با timeout و fallback محکم نکنید، روزی که غریبه بدقول شود، همه چیزِ شما لنگ می‌زند.

توابع پایه: wp_remote_get، wp_remote_post، wp_remote_request

سه تابع اصلی HTTP API وردپرس:

// درخواست GET
$response = wp_remote_get( $url, $args = array() );

// درخواست POST
$response = wp_remote_post( $url, $args = array() );

// درخواست با متد دلخواه (PUT، DELETE، PATCH، HEAD)
$response = wp_remote_request( $url, $args = array() );

پارامتر $args، مهم‌ترین بخش هر درخواست است. پارامترهای پرکاربرد:

پارامترتوضیحمقدار پیشنهادی
timeoutحداکثر زمان انتظار برای پاسخ (ثانیه)۵ تا ۱۵
headersآرایه هدرها (Authorization، Accept، User-Agent)بسته به API
bodyداده‌های ارسالی در POST (آرایه یا رشته)آرایه برای فرم
methodمتد HTTP در wp_remote_requestGET، POST، PUT، DELETE
blockingآیا منتظر پاسخ بماند یا نهtrue (پیش‌فرض)
sslverifyبررسی گواهی SSLtrue (هرگز false نکنید)
redirectionحداکثر تعداد ریدایرکت‌های خودکار۵ (پیش‌فرض)
user-agentهدر User-Agent درخواستنام افزونه + URL سایت

الگوی پایه یک درخواست کامل:

$response = wp_remote_get( 'https://api.example.com/data', array(
    'timeout'    => 10,
    'sslverify'  => true,
    'user-agent' => 'MyPlugin/1.0; ' . home_url(),
    'headers'    => array(
        'Authorization' => 'Bearer ' . $api_key,
        'Accept'        => 'application/json',
    ),
) );

if ( is_wp_error( $response ) ) {
    error_log( 'HTTP Error: ' . $response->get_error_message() );
    return null;
}

$status = wp_remote_retrieve_response_code( $response );
if ( 200 !== $status ) {
    error_log( 'HTTP Status: ' . $status );
    return null;
}

$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
if ( json_last_error() !== JSON_ERROR_NONE ) {
    error_log( 'JSON Error: ' . json_last_error_msg() );
    return null;
}

return $data;

سه نکته حیاتی در این الگو: یک — is_wp_error اول: پیش از هر چیز، خطای شبکه را چک کنید. دو — کد وضعیت: سپس کد وضعیت HTTP را. سه — پارس JSON با بررسی خطا: پاسخ می‌تواند نامعتبر باشد. راهنمای کامل این توابع در اتصال وردپرس به سرویس‌های خارجی و ساخت API اختصاصی.

توابع کمکی: استخراج داده از پاسخ

وردپرس مجموعه‌ای از توابع کمکی برای استخراج داده از پاسخ $response فراهم می‌کند:

// کد وضعیت HTTP (200، 404، 500 و...)
$status = wp_remote_retrieve_response_code( $response );

// پیام وضعیت (OK، Not Found و...)
$message = wp_remote_retrieve_response_message( $response );

// بدنه پاسخ (بدون هدر)
$body = wp_remote_retrieve_body( $response );

// یک هدر خاص
$content_type = wp_remote_retrieve_header( $response, 'content-type' );

// آرایه کامل هدرها
$headers = wp_remote_retrieve_headers( $response );

// آرایه کامل پاسخ (شامل headers، body، response، cookies)
$raw = $response;

نکته مهم: هرگز به $response['body'] مستقیم دسترسی نگیرید، چون اگر $response یک WP_Error باشد، خطای Fatal می‌گیرید. همیشه از توابع wp_remote_retrieve_* استفاده کنید. راهنما در PHP امن در وردپرس و اعتبارسنجی داده‌ها. یک تجربه میدانی: در پروژه‌ای، کد به‌جای توابع کمکی، مستقیماً $response['response']['code'] را می‌خواند. وقتی سرویس بیرونی قطع شد، $response یک WP_Error بود و دسترسی مستقیم، خطای Fatal می‌داد. نتیجه: کل صفحه سفید می‌شد. تغییر به توابع کمکی، در سه خط کد، این باگ را برای همیشه بست.

مدیریت خطا و timeout

مدیریت خطا، مهم‌ترین بخش کار با HTTP API است. چهار نوع خطا وجود دارد:

  1. خطای شبکه (Network Error): سرور مقصد پاسخ نمی‌دهد. is_wp_error مقدار true برمی‌گرداند و کد خطا معمولاً http_request_failed است.
  2. خطای timeout: درخواست در مدت تعیین‌شده پاسخ نگرفت. کد خطا http_request_failed با پیام «cURL error 28: Operation timed out».
  3. خطای SSL: گواهی SSL سرور مقصد معتبر نیست. کد خطا http_request_failed با پیام مربوط به SSL. هرگز sslverify را false نکنید — راهنمای کامل در SSL و HTTPS و تأثیر HTTPS بر سئو.
  4. خطای سرور مقصد (HTTP Error): سرور پاسخ می‌دهد ولی با کد خطا (۴xx یا ۵xx). این خطا، WP_Error نیست؛ باید کد وضعیت را چک کنید.

الگوی کامل مدیریت خطا:

function my_plugin_fetch_data( $url ) {
    $response = wp_remote_get( $url, array( 'timeout' => 10 ) );

    // لایه اول: خطای شبکه، timeout، SSL
    if ( is_wp_error( $response ) ) {
        $error_code = $response->get_error_code();
        $error_msg  = $response->get_error_message();
        error_log( sprintf( 'HTTP Network Error [%s]: %s', $error_code, $error_msg ) );
        return null;
    }

    // لایه دوم: خطای HTTP
    $status = wp_remote_retrieve_response_code( $response );
    if ( $status >= 400 ) {
        error_log( sprintf( 'HTTP Error %d for %s', $status, $url ) );
        return null;
    }

    // لایه سوم: پارس JSON
    $body = wp_remote_retrieve_body( $response );
    $data = json_decode( $body, true );
    if ( json_last_error() !== JSON_ERROR_NONE ) {
        error_log( 'JSON Parse Error: ' . json_last_error_msg() );
        return null;
    }

    return $data;
}

سه نکته درباره timeout: یک — timeout کوتاه: برای سایت‌های پرمخاطب، timeout بیش از ۱۵ ثانیه، یعنی قفل شدن پردازنده. دو — fallback: همیشه مقدار پیش‌فرض یا کش داشته باشید تا در صورت قطع سرویس بیرونی، سایت همچنان کار کند. سه — لاگ: خطاها را لاگ کنید تا الگوها را ببینید. راهنمای دیباگ در دیباگ کد سفارشی وردپرس و تست و دیباگ پروژه‌های وردپرس.

timeout روی درخواست HTTP، مثل بیمه عمر است: تا وقتی اتفاقی نیفتاده، هزینه به نظر می‌رسد؛ روزی که اتفاق بیفتد، همه چیز را نجات می‌دهد.

احراز هویت با سرویس‌های بیرونی

سه الگوی رایج احراز هویت در HTTP API وردپرس:

الگوی اول، API Key در هدر:

$response = wp_remote_get( $url, array(
    'headers' => array(
        'X-API-Key' => $api_key,
    ),
) );

الگوی دوم، Bearer Token:

$response = wp_remote_get( $url, array(
    'headers' => array(
        'Authorization' => 'Bearer ' . $token,
    ),
) );

الگوی سوم، Basic Auth:

$response = wp_remote_get( $url, array(
    'headers' => array(
        'Authorization' => 'Basic ' . base64_encode( $username . ':' . $password ),
    ),
) );

سه قاعده امنیتی حیاتی: یک — کلید API هرگز hardcode نشود. آن را در wp_options رمزنگاری‌شده نگه دارید. راهنما در کار با Options API و ساخت صفحه تنظیمات اختصاصی. دو — کلید را هر چند ماه تعویض کنید. سه — کلید را لاگ نکنید. در لاگ‌ها، کلید API را ماسک کنید. راهنمای کامل در امنیت API و احراز هویت API.

یک تجربه میدانی: در پروژه‌ای، کلید API در فایل JS فرانت‌اند قرار داشت. یک کاربر با DevTools آن را استخراج کرد و از طریق یک اسکریپت، هجده هزار درخواست به سرویس بیرونی فرستاد. هزینه ماهانه، سه برابر شد. راه‌حل: انتقال کلید به سرور و ارسال درخواست‌ها از PHP. این یک درس گران‌قیمت بود که از آن روز، در همه پروژه‌های خودم رعایت می‌کنم.

کش کردن پاسخ‌های بیرونی

پاسخ سرویس‌های بیرونی، معمولاً در بازه‌های کوتاه تغییر نمی‌کند. کش کردن این پاسخ‌ها، هم سرعت سایت را بالا می‌برد و هم فشار روی سرویس بیرونی را کم می‌کند. الگو با Transients:

function my_plugin_get_exchange_rate( $currency ) {
    $cache_key = 'exchange_rate_' . sanitize_key( $currency );
    $cached = get_transient( $cache_key );

    if ( false !== $cached ) {
        return $cached;
    }

    $response = wp_remote_get( 'https://api.example.com/rate/' . $currency, array(
        'timeout' => 8,
    ) );

    if ( is_wp_error( $response ) ) {
        error_log( 'Exchange Rate Error: ' . $response->get_error_message() );
        // مقدار پیش‌فرض برای عدم قطع سایت
        return 0;
    }

    $body = wp_remote_retrieve_body( $response );
    $data = json_decode( $body, true );

    if ( ! isset( $data['rate'] ) ) {
        return 0;
    }

    set_transient( $cache_key, $data['rate'], HOUR_IN_SECONDS );
    return $data['rate'];
}

نکته حیاتی: در صورت خطا، مقدار پیش‌فرض یا مقدار کش‌شده قدیمی برگردانید، نه null. دلیلش ساده است: اگر سایت شما قیمت محصول را وابسته به این مقدار محاسبه می‌کند، null باعث نمایش «قیمت نامشخص» می‌شود که تجربه بدی است. راهنمای کامل در ترنزینت‌ها در وردپرس و افزونه‌های کش وردپرس. یک نکته پیشرفته: برای سرویس‌های بیرونی با محدودیت نرخ (rate limit)، علاوه بر کش، باید «شمارنده درخواست» هم داشته باشید تا از ارسال بیش از حد جلوگیری کنید. الگوی کامل در اتصال وردپرس به سرویس‌های خارجی.

درخواست Async: non-blocking

گاهی نیاز دارید درخواستی بفرستید ولی منتظر پاسخ نمانید — مثلاً ارسال رویداد تحلیلی یا نوتیفیکیشن به سرویس‌های جانبی. الگو با blocking => false:

wp_remote_post( 'https://analytics.example.com/track', array(
    'timeout'  => 0.01,
    'blocking' => false,
    'body'     => array(
        'event' => 'purchase',
        'order' => $order_id,
    ),
) );

نکته حیاتی: مقدار timeout باید بسیار کوچک (مثلاً 0.01) باشد تا PHP بلافاصله از فراخوانی عبور کند. بدون این، حتی با blocking => false، PHP تا زمان timeout منتظر می‌ماند. نکته دوم: در این حالت، پاسخ را نمی‌گیرید و خطاها را نمی‌بینید. برای این نوع درخواست‌ها، حتماً یک مکانیزم لاگ جداگانه در طرف سرویس بیرونی داشته باشید. راهنمای تکمیلی در افزایش سرعت وردپرس و بهینه‌سازی کد وردپرس. یک تجربه میدانی: در پروژه‌ای، سایت با هر بازدید، یک درخواست blocking به سرویس تحلیل می‌فرستاد. با تغییر به blocking => false، زمان پاسخ از ۸۰۰ میلی‌ثانیه به ۳۰۰ میلی‌ثانیه رسید.

امنیت در درخواست‌های HTTP

پنج قاعده امنیتی الزامی:

  1. HTTPS اجباری: همه درخواست‌ها به سرویس‌های بیرونی باید روی HTTPS باشند. HTTP ساده، خطر شنود دارد. راهنما در SSL و HTTPS.
  2. sslverify => true: هرگز این پارامتر را false نکنید، حتی در محیط توسعه. اگر سرور مقصد گواهی معتبر ندارد، با آن سرویس کار نکنید.
  3. allowlist دامنه: فقط به دامنه‌های شناخته‌شده درخواست بفرستید. اگر URL مقصد از ورودی کاربر می‌آید، با allowlist مقایسه کنید.
  4. پاک‌سازی پاسخ: پاسخ سرویس بیرونی هم باید پاک‌سازی شود، حتی اگر به سرویس اعتماد دارید. سرویس بیرونی ممکن است هک شده باشد.
  5. مدیریت کلید: کلید API را در دیتابیس نگه دارید، رمزنگاری کنید، و دوره‌ای تعویض کنید.

الگوی allowlist دامنه:

$allowed_hosts = array( 'api.example.com', 'cdn.example.com' );
$url = esc_url_raw( $input_url );
$host = wp_parse_url( $url, PHP_URL_HOST );

if ( ! in_array( $host, $allowed_hosts, true ) ) {
    return new WP_Error( 'invalid_host', 'دامنه مجاز نیست' );
}

$response = wp_remote_get( $url, array( 'timeout' => 10 ) );

راهنمای کامل در امنیت API، امنیت API در وب، هدرهای امنیتی HTTP، و محافظت وردپرس در برابر هکرها. یک آسیب‌پذیری شایع که در پرونده‌های امنیتی دیده‌ام: endpointهایی که URL مقصد را از ورودی کاربر می‌گرفتند بدون allowlist. نتیجه: SSRF (Server-Side Request Forgery) که مهاجم می‌توانست از سرور شما به سرورهای داخلی درخواست بفرستد.

الگوهای پیشرفته: Circuit Breaker و Retry

برای پروژه‌های جدی، دو الگو ارزش پیاده‌سازی دارند:

الگوی اول، Retry با Backoff: اگر خطای گذرا بود، با تأخیر افزایشی دوباره تلاش کن:

function my_plugin_fetch_with_retry( $url, $max_attempts = 3 ) {
    for ( $i = 1; $i <= $max_attempts; $i++ ) {
        $response = wp_remote_get( $url, array( 'timeout' => 10 ) );

        if ( ! is_wp_error( $response ) ) {
            $status = wp_remote_retrieve_response_code( $response );
            if ( $status < 500 ) {
                return $response;
            }
        }

        if ( $i < $max_attempts ) {
            sleep( pow( 2, $i ) );
        }
    }

    return new WP_Error( 'max_retries', 'حداکثر تلاش انجام شد' );
}

الگوی دوم، Circuit Breaker: اگر سرویس چند بار پشت‌سرهم خطا داد، برای مدتی به آن درخواست نفرست:

function my_plugin_check_circuit( $service_key ) {
    $failures = (int) get_transient( 'cb_failures_' . $service_key );
    if ( $failures >= 5 ) {
        return false;
    }
    return true;
}

function my_plugin_record_failure( $service_key ) {
    $key = 'cb_failures_' . $service_key;
    $failures = (int) get_transient( $key );
    set_transient( $key, $failures + 1, 5 * MINUTE_IN_SECONDS );
}

راهنمای کامل در اتصال وردپرس به سرویس‌های خارجی و ترنزینت‌ها. یک تجربه میدانی: در پروژه‌ای با ده سرویس بیرونی، پیاده‌سازی Circuit Breaker باعث شد uptime سایت از ۹۹.۵٪ به ۹۹.۹۹٪ برسد.

کار با WP_Http_Response و کلاس‌ها

برای پروژه‌های پیچیده، می‌توانید از کلاس‌های اختصاصی استفاده کنید:

class My_Plugin_API_Client {
    const BASE_URL = 'https://api.example.com/v1/';
    const TIMEOUT = 10;

    protected $api_key;

    public function __construct( $api_key ) {
        $this->api_key = $api_key;
    }

    public function get( $endpoint, $params = array() ) {
        $url = self::BASE_URL . ltrim( $endpoint, '/' );
        if ( ! empty( $params ) ) {
            $url = add_query_arg( $params, $url );
        }

        $response = wp_remote_get( $url, $this->get_default_args() );
        return $this->handle_response( $response );
    }

    public function post( $endpoint, $body = array() ) {
        $url = self::BASE_URL . ltrim( $endpoint, '/' );
        $args = array_merge( $this->get_default_args(), array( 'body' => $body ) );

        $response = wp_remote_post( $url, $args );
        return $this->handle_response( $response );
    }

    protected function get_default_args() {
        return array(
            'timeout'    => self::TIMEOUT,
            'sslverify'  => true,
            'user-agent' => 'MyPlugin/1.0; ' . home_url(),
            'headers'    => array(
                'Authorization' => 'Bearer ' . $this->api_key,
                'Accept'        => 'application/json',
            ),
        );
    }

    protected function handle_response( $response ) {
        if ( is_wp_error( $response ) ) {
            return $response;
        }

        $status = wp_remote_retrieve_response_code( $response );
        if ( $status >= 400 ) {
            return new WP_Error( 'http_error', 'کد خطا: ' . $status );
        }

        $body = wp_remote_retrieve_body( $response );
        $data = json_decode( $body, true );

        if ( json_last_error() !== JSON_ERROR_NONE ) {
            return new WP_Error( 'json_error', json_last_error_msg() );
        }

        return $data;
    }
}

مزیت این ساختار: تمام منطق ارتباط با سرویس بیرونی در یک نقطه، قابل تست، قابل نگهداری. الگوهای مشابه در کدنویسی اختصاصی افزونه، ساختار فایل‌های افزونه استاندارد، و اصول کدنویسی تمیز. در پروژه‌ای با سه سرویس بیرونی، این ساختار، زمان دیباگ را نصف کرد.

اشتباهات رایج در HTTP API وردپرس

  • استفاده از curl یا file_get_contents: عدم سازگاری با محیط‌های مختلف و عدم پشتیبانی از فیلترها. راهنما در اتصال وردپرس به سرویس‌های خارجی.
  • نبود timeout: خطر خوابیدن سایت در قطع سرویس بیرونی. راهنما در رفع کندی شدید سایت.
  • نبود is_wp_error: خطای Fatal هنگام قطع سرویس. راهنما در PHP امن.
  • دسترسی مستقیم به $response['body']: خطر Fatal در خطا. راهنمای توابع کمکی در همین مقاله.
  • hardcode کردن کلید API: خطر افشا. راهنما در Options API.
  • sslverify => false: خطر شنود. راهنما در SSL و HTTPS.
  • نبود کش: فشار مضاعف روی سایت و سرویس بیرونی. راهنما در ترنزینت‌ها.
  • نبود allowlist دامنه: خطر SSRF. راهنما در امنیت API.
  • درخواست blocking در مسیر کاربر: کندی محسوس. راهنما در افزایش سرعت وردپرس.
  • نبود لاگ: دیباگ در بحران دشوار. راهنما در دیباگ کد سفارشی.
  • نادیده‌گرفتن نرخ پاسخ سرویس: خطر rate limit. راهنما در اتصال به سرویس خارجی.
  • نبود fallback: قطع سرویس بیرونی، قطع سایت شما. راهنمای دقیق در همین مقاله.

جمع‌بندی

توابع HTTP API وردپرس، در پنج گروه خلاصه می‌شوند: توابع پایه (wp_remote_get، wp_remote_post، wp_remote_request)، توابع کمکی (wp_remote_retrieve_*)، مدیریت خطا (is_wp_error، timeout، SSL)، کش (set_transient، get_transient)، و امنیت (HTTPS، allowlist، مدیریت کلید). سه اصل را در پایان تاکید می‌کنم: اول، همیشه از HTTP API وردپرس استفاده کنید، نه توابع خام PHP. دوم، timeout کوتاه و fallback داشته باشید تا سایت شما گروگان سرویس‌های بیرونی نشود. سوم، پاسخ‌های بیرونی را کش کنید و امنیت را در هر لایه جدی بگیرید.

اگر امروز یک کار در این مسیر انجام می‌دهید: به آخرین درخواست HTTP پروژه خود نگاه کنید و ببینید آیا timeout دارد، آیا is_wp_error چک می‌شود، و آیا پاسخ کش می‌شود. همان یک بازبینی کوچک، در روز بحران نجات‌دهنده است. اگر تجربه‌ای از یک حادثه HTTP یا یک الگوی موفق در پروژه‌ای دارید، در دیدگاه‌ها بنویسید — همان گزارش‌های واقعی، این راهنما را برای توسعه‌دهنده بعدی دقیق‌تر می‌کند. 🔌