把博客从旧版 Typecho 升级到 1.3.0 之后,后台「新建文章」和「新建页面」直接打不开,页面只显示一行 Server Error。
第一反应是主题或插件冲突——于是换回默认主题、停掉所有插件,结果照样报错。
这个"怎么试都不行"的现象,恰恰是最有价值的线索。接下来是一段连踩四个坑的排查实录。
第一坑:Server Error,换主题停插件都没用
让错误现形
Typecho 对非预期的异常会统一渲染成 Server Error 字样,真实报错被藏起来了。想看真凶,需要临时打开调试模式——在 config.inc.php 里加一行:
define('__TYPECHO_DEBUG__', true);刷新页面后,真凶浮出水面:
TypeError: Widget\Options::tryDeserialize():
Argument #1 ($value) must be of type string, null given真凶:强类型 + 缺失的选项
Typecho 1.3.0 的核心代码 Widget\Options 用了 PHP 强类型签名:
private function tryDeserialize(string $value)而 ___panelTable() 会去读数据库里的 panelTable 选项。如果 options 表里根本没有这条记录,读出来就是 null,强类型模式下 PHP 直接抛 TypeError。
那为什么老站会缺这条记录?因为 panelTable、actionTable 这类选项是较新版本才引入的。而 Typecho 自带的升级程序只做序列化格式的转换,不会补写缺失的项——你升级前没有,升级后依然没有。
为什么换主题、停插件都无效?
因为报错发生在 Options 组件初始化阶段,比主题加载、插件加载都要早。
这解释了我一开始的困惑:不是主题或插件的问题,是核心自己在这一步就炸了,换什么、停什么都绕不过去。
💡 经验:当"换主题、停用所有插件"都无效时,基本可以确定问题出在更早的阶段——核心或环境,而不是主题插件。
解决
往 options 表补写缺失的记录:
INSERT IGNORE INTO typecho_options (name, value, user)
VALUES ('panelTable', '[]', 0);
INSERT IGNORE INTO typecho_options (name, value, user)
VALUES ('actionTable', '[]', 0);(表前缀按你自己数据库实际情况调整。)顺带检查 secret、generator 是否也缺失,一并补上。
第二坑:又拍云插件的 TypeError(array → Config)
修好新建文章后,后台「管理 → 文件」又炸了:
TypeError: UpyunFile_Plugin::attachmentHandle():
Argument #1 ($content) must be of type array, Typecho\Config given根因:1.3.0 改了类型契约
这是 1.2 → 1.3 的一次破坏性变更。核心把附件信息的传递类型从数组改成了对象:
| 版本 | attachmentHandle 签名 | 传入的是什么 |
|---|---|---|
| 1.2.x 及以前 | attachmentHandle(array $content) | 数组 |
| 1.3.0 | attachmentHandle(Config $attachment) | Config 对象 |
插件里还写着 array $content,收到 Config 对象,PHP 直接抛 TypeError。
这不是配置问题,是插件没跟上核心的变更。 我用的又拍云插件(UpyunFile)是老版本,作者没适配 1.3.0。
第三坑:Undefined array key "attachment"
按常规思路,把类型改成兼容写法(收到对象就 toArray() 转成数组)应该就完了。改完类型错误确实消失了,但页面又冒出满屏警告:
Warning: Undefined array key "attachment" in .../Plugin.php on line 296
Warning: Attempt to read property "path" on null in .../Plugin.php on line 296根因:不只是类型变了,层级也变了
toArray() 只解决了类型,没解决数据结构层级。两个版本传给插件的东西完全不是一回事:
// 1.2.x:传的是外层数组,附件信息藏在 ['attachment'] 里
$value['attachment']->url = Upload::attachmentHandle($value);
// 插件里写 $content['attachment']->path → ✔ 成立
// 1.3.0:传的就是附件本身,根本没有 attachment 这一层
$attachment->url = Upload::attachmentHandle($attachment);
// 插件里写 $content['attachment'] → ✘ 不存在 → 第一条警告
// ->path → ✘ 对 null 取属性 → 第二条警告解决:换用适配 1.3.0 的新版插件
与其手工打补丁,不如直接用作者已适配的版本。新版(v1.2,明确标注支持 Typecho 1.3.0+)把取路径改成了三分支兼容:
if (isset($content->attachment) && isset($content->attachment->path)) {
$path = $content->attachment->path; // 1.2.x 结构
} else if (isset($content->path)) {
$path = $content->path; // 1.3.0 结构 ← 走这条
} else if (isset($content->text)) {
// 从 text 字段兜底
}替换提醒:解压出来的目录常是 UpyunFile-master,但 Typecho 按目录名识别插件,必须改名为 UpyunFile 再覆盖,否则后台认不到。覆盖前记得先备份原插件目录。
第四坑:又拍云"操作员验证失败"
换上新插件后,填完服务名、操作员、密码点保存,表单报:
验证不通过,请核对 Upyun 操作员和密码是否输入正确
根因:相对路径少了一层目录
插件在验证时会真的去请求一次又拍云 API。而加载 SDK 的代码是这样的:
require_once 'Upyun/vendor/autoload.php'; // 相对路径但 SDK 的真实位置是:
usr/plugins/UpyunFile/Upyun/vendor/autoload.php
↑↑↑↑↑↑↑↑ 少了这一层Typecho 1.3.0 没有任何 include_path 机制,插件是用完整路径 require 的。所以这句相对路径的解析基准是网站根目录,在合理环境下都找不到文件——我实测过,stream_resolve_include_path() 在两种常见场景下都返回 false。
SDK 没加载 → 验证走不到真实请求 → 直接失败。
更坑的是:异常被吞了
validate() 里是这样的:
} catch (Exception $e) {
$hostUsage = -1; // ← 真实异常被丢掉,只返回一个 -1
}所以你永远只能看到那句万能的"验证不通过",看不到是"类找不到"、"密码错"还是"网络超时"。
解决
把两处 require_once(validate() 和 upyunInit() 里各一处)都改成绝对路径:
require_once __DIR__ . '/Upyun/vendor/autoload.php';__DIR__ 在插件文件里就是 usr/plugins/UpyunFile,拼出来正好是 SDK 的真实位置。
顺带建议把 catch (Exception $e) 改成 catch (\Throwable $e)——PHP 7+ 的 Error 不是 Exception 的子类,只捕获 Exception 会漏掉一部分错误。
⚠️ 潜在隐患:插件自带的 SDK 依赖 Guzzle 6.3(2017 年版本)。如果你的服务器是 PHP 8,这个组合可能有兼容性问题。我这边验证通过了,但若你遇到 Guzzle 相关报错,需要考虑升级 SDK。
番外:一次"白屏"事故
为了安全加固,我给插件加了一行防直接访问:
if (!defined('__TYPECHO_ROOT_DIR__')) exit;结果改完之后,主页变成纯白,而后台却能正常打开。
定位过程
| 页面 | 状态 |
|---|---|
| 主页 | HTTP 200,0 字节 |
| 后台 | 200,5315 字节(正常) |
| 文章页 | 正常 |
后台正常,说明核心、数据库、插件加载都没问题;白屏只影响前台渲染。而 HTTP 200 + 0 字节 这个组合,说明脚本"正常跑完了却没输出任何东西"。
根因:exit 加错了位置
那行被我误加进了 beforeRender() 函数体内。而这个插件有个前台专属钩子:
public static function Widget_Archive_beforeRender() {
ob_start('UpyunFile_Plugin::beforeRender'); // 开启输出缓冲
}页面渲染完成后,PHP 刷新缓冲区并调用回调 beforeRender($text)——回调一执行到 exit,整个缓冲区的输出全部被丢弃,于是浏览器收到 HTTP 200 和 0 字节的空白页。
因为后台不触发这个前台钩子,所以后台一切正常。这个"后台正常、前台白屏"的组合,正是定位到输出缓冲回调的关键线索。
教训
- 防直接访问的那行必须放在文件顶部、所有
function之外; HTTP 200 + 0 字节是输出被中途丢弃的典型特征;- 关掉
display_errors后,错误不会消失,只会变成白屏——排查期间建议临时打开。
经验总结
回头看,这四个坑有一个共同背景:核心往前跑了,插件还留在原地。
- 1.3.0 是一次破坏性升级:数组 →
Config对象、强类型签名全面引入。老插件几乎必然中招,报错多表现为must be of type array, Typecho\Config given。 - "换主题停插件都无效"本身就是强线索——它说明问题在更早的初始化阶段。
- 升级后先查
options表:确认有没有新增字段缺失,再看插件兼容性。 - 老插件逐个确认是否有适配版本,别急着手工打补丁——官方适配版通常更完整。
- 调试开关用完立刻删:
__TYPECHO_DEBUG__会把服务器路径、调用堆栈暴露在网页上。 - 改插件前先备份,且改完要验证前后台都正常。
附录:同类问题速查表
| 报错信息 | 含义 | 处理 |
|---|---|---|
Server Error(新建文章/页面) | 核心强类型读到 null | 补写 options 表缺失项 |
must be of type array, Typecho\Config given | 1.3.0 把数组改成了对象 | 去掉 array 类型声明 + instanceof \Typecho\Config 转换 |
Undefined array key "attachment" | 数据结构层级变了 | 用适配版插件,或按 $content->path 取值 |
Undefined array key ...(其它) | 多为同类变更 | 同上思路 |
operator/验证失败 | 多为 SDK 没加载或凭据错 | 检查 SDK 路径,把异常暴露出来看真因 |
白屏 + HTTP 200 + 0 字节 | 输出被中途丢弃 | 查 ob_start 回调里是否有 exit / 提前终止 |
如果你也在升级 Typecho 的路上遇到了类似的报错,希望这篇记录能帮你少走点弯路。
声明:本文由AI生成,整个处理过程由Workbuddy协助,本人对代码一窍不通。再次感谢AI。









晚乔