`register_activation_hook` 里直接 `wp_remote_get`:我的插件激活时白屏,原来是把"施工许可证"当成了"通车许可证"

小助手
小助手 版主圣羽星庭 勋望元宿志愿先锋
社区管理
插件开发 78 浏览 0 回复

上周有个插件在本地激活一切正常,打包发给测试同事后,激活瞬间直接白屏,Apache 错误日志里躺着一条:

Fatal error: Maximum execution time of 30 seconds exceeded in /wp-includes/class-wp-http-curl.php on line 257

我盯着这行看了十分钟——激活插件而已,怎么会跑去 HTTP 请求里耗死?

错误写法:把初始化逻辑直接塞进激活钩子

我当时脑子里的流程图是这样的:插件激活 → 调用远程 API 获取授权 → 写入配置 → 完事。代码大概长这样:

<?php
// 错误示范!不要复制
register_activation_hook( __FILE__, 'my_plugin_activate' );

function my_plugin_activate() {
    // 想在这里"一步到位":激活即联网校验
    $response = wp_remote_get( 'https://license.example.com/verify', [
        'timeout' => 45,  // 以为给够时间就行
        'sslverify' => true,
    ] );

    if ( is_wp_error( $response ) ) {
        wp_die( '授权服务器连接失败:' . $response->get_error_message() );
    }

    $body = json_decode( wp_remote_retrieve_body( $response ), true );
    update_option( 'my_plugin_license', $body['token'] );

    // 顺便建个表
    global $wpdb;
    $wpdb->query( "CREATE TABLE ..." );

    // 再顺便刷个重写规则
    flush_rewrite_rules();
}

这串代码犯了三个致命错误,但最隐蔽的是时机错位register_activation_hook 的运行环境极其特殊——它发生在插件文件被加载、但 WordPress 核心流程尚未完全就绪的夹缝里。

为什么这里不能放远程请求

激活钩子的执行上下文有几个"暗礁":

  • 无头请求环境:某些批量操作、CLI 激活、或某些主机环境会跳过 HTTP 层,wp_remote_get 底层可能直接卡死
  • 超时黑洞:30 秒是 PHP 默认上限,但授权服务器一旦抖动,用户看到的就是白屏,连错误信息都可能被主机拦截
  • 事务不可逆:激活钩子抛异常,插件状态会处于"激活失败但文件已加载"的暧昧状态,重装都可能报错
  • 多站点放大器:如果在网络激活里跑,每个子站点都可能触发一次,直接 timeout × N

更坑的是,有些主机(比如部分共享主机)会在激活瞬间额外做一次重定向或安全检查,让你的 HTTP 请求变成"嵌套请求",CURL 直接死锁。

正确写法:激活只做"立桩标记",延迟执行"联网验桩"

把"激活"和"初始化"拆成两个阶段,用 transient 或 option 做状态接力:

<?php
// === 阶段一:激活时只做轻量级标记 ===
register_activation_hook( __FILE__, 'my_plugin_activate' );

function my_plugin_activate() {
    // 只写标记,不联网
    set_transient( 'my_plugin_pending_activation', true, HOUR_IN_SECONDS );

    // 建表可以留在这里,但建议也拆出去
    my_plugin_create_tables();

    // 重写规则标记为待刷新,不立即执行
    update_option( 'my_plugin_rewrite_rules_flag', true );
}

// === 阶段二:在正常的 admin_init 里执行"重活" ===
add_action( 'admin_init', 'my_plugin_run_deferred_setup' );

function my_plugin_run_deferred_setup() {
    // 没有待处理标记?直接返回
    if ( ! get_transient( 'my_plugin_pending_activation' ) ) {
        return;
    }

    // 删除标记,防止重复执行
    delete_transient( 'my_plugin_pending_activation' );

    // 现在 WordPress 已完全就绪,HTTP 层稳定
    $response = wp_remote_get( 'https://license.example.com/verify', [
        'timeout'     => 15,        // 缩短超时,失败可重试
        'sslverify'   => true,
        'redirection' => 2,         // 限制跳转次数
    ] );

    if ( is_wp_error( $response ) ) {
        // 不 wp_die,只记日志 + 后台通知
        my_plugin_log_error( '授权校验失败: ' . $response->get_error_message() );
        add_action( 'admin_notices', function() {
            echo '<div class="notice notice-warning"><p>插件授权暂未完成,请检查网络或稍后重试。</p></div>';
        } );
        return;
    }

    $body = json_decode( wp_remote_retrieve_body( $response ), true );
    if ( ! empty( $body['token'] ) ) {
        update_option( 'my_plugin_license', sanitize_text_field( $body['token'] ) );
    }

    // 延迟的重写规则刷新
    if ( get_option( 'my_plugin_rewrite_rules_flag' ) ) {
        flush_rewrite_rules();
        delete_option( 'my_plugin_rewrite_rules_flag' );
    }
}

关键差异对照

维度错误写法正确写法
执行时机激活瞬间,WP 未就绪admin_init,完整环境
失败处理wp_die 白屏后台警告 + 日志,可重试
超时风险30s 硬 ceiling15s 可控,失败不阻断
多站点网络激活时连锁爆炸每个站点独立判断 transient
可测试性必须真激活才能测手动设 transient 即可模拟

延伸:如果必须在激活时"立即"知道结果

有些场景确实需要激活时给出明确反馈(比如授权失败就不让用)。这时候可以用重定向接力

function my_plugin_activate() {
    set_transient( 'my_plugin_activation_redirect', true, 30 );
}

add_action( 'admin_init', function() {
    if ( ! get_transient( 'my_plugin_activation_redirect' ) ) return;
    delete_transient( 'my_plugin_activation_redirect' );

    // 安全重定向到插件设置页,附带 nonce
    wp_safe_redirect( admin_url( 'admin.php?page=my-plugin-setup&step=license' ) );
    exit;
} );

把用户引导到一个正常的后台页面再做校验,既保留了"激活即引导"的体验,又避开了激活钩子的环境陷阱。

踩坑后的自检清单

现在我写激活钩子前会过一遍:

  1. 这行代码在 wp-load.php 尚未跑完时执行,会挂吗?
  2. 如果外部服务 500 错误,用户能看到什么?
  3. 这个操作能放进 transient/schedule 延迟执行吗?
  4. 多站点网络激活时,会执行几次?

激活钩子不是插件的"main 函数",它更像建筑工地的施工许可证办理窗口——你在这儿登记备案、立个告示牌,但真动土挖地基,得等车辆人员全部进场之后。

评论0
回复 · 0
还没有回复
微信客服 微信客服