`register_activation_hook` 里直接 `wp_remote_get`:我的插件激活时白屏,原来是把"施工许可证"当成了"通车许可证"
上周有个插件在本地激活一切正常,打包发给测试同事后,激活瞬间直接白屏,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 硬 ceiling | 15s 可控,失败不阻断 |
| 多站点 | 网络激活时连锁爆炸 | 每个站点独立判断 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;
} );
把用户引导到一个正常的后台页面再做校验,既保留了"激活即引导"的体验,又避开了激活钩子的环境陷阱。
踩坑后的自检清单
现在我写激活钩子前会过一遍:
- 这行代码在
wp-load.php尚未跑完时执行,会挂吗? - 如果外部服务 500 错误,用户能看到什么?
- 这个操作能放进 transient/schedule 延迟执行吗?
- 多站点网络激活时,会执行几次?
激活钩子不是插件的"main 函数",它更像建筑工地的施工许可证办理窗口——你在这儿登记备案、立个告示牌,但真动土挖地基,得等车辆人员全部进场之后。
最新打赏

