搜索“不存在的标签”得到空列表,是合法结果;请求不存在的文章得到首页 HTML,则是错误路由。两者都可能让用户看不到目标内容,但 HTTP 语义与恢复方式不同。把所有情况返回 200,会让监控、爬虫和用户一起失去判断依据。
本文讨论静态博客与边缘接口。生产平台可能有 SPA 回退行为,必须确认不存在的路径到底返回什么,不能用开发服务器结果替代部署验证。
先把错误分类变成一个状态模型
假设文章接口需要读取内容索引并渲染页面。没有文章记录属于资源不存在;索引读取失败属于依赖故障;文章存在但模板抛错属于执行故障;搜索执行正常却没有匹配项属于合法空结果。四种状态应该从数据层保留到响应层,不用同一个 null 表达全部情况。若读取失败被捕获成空数组,后续代码就再也无法知道应该返回空列表还是错误。
内部结果可以采用明确的成功、缺失和失败分支,失败包含非敏感错误类别与关联标识。页面组件消费成功数据,缺失进入四百零四,失败进入相应故障响应。不要让渲染组件通过“列表长度为零”猜测依赖是否正常。接口契约清楚后,监控和用户提示都能由同一原因产生,不必各自推断。
资源存在性还可能受权限限制。系统有时选择用四百零四避免向未授权用户确认私人资源存在,这属于明确的隐私策略,不是任意隐藏所有授权错误。内部日志仍应区分拒绝与真实缺失,公开响应不泄漏敏感细节。身份验证失败、权限不足和业务资源不存在,需要根据产品威胁模型决定对外表达。
404 表示目标资源不存在,500 表示服务器执行失败,502 常用于上游失败。搜索无结果通常仍是 200,因为搜索任务成功完成。权限相关的 401 与 403 也有区别,但公开博客不应把内部身份信息暴露在错误详情中。
if (!article) {
return new Response('Article not found', { status: 404 });
}
try {
return await renderArticle(article);
} catch (error) {
logRenderFailure(error);
return new Response('Unable to render article', { status: 500 });
}
不要在 catch 中返回空对象并继续生成“正常页面”。这会让依赖失败伪装成没有内容,错误持续发生却不触发告警。日志函数也应避免泄漏正文或凭据,只保留必要上下文与关联标识。
公开 API 与 HTML 页面使用不同表示
HTML 错误页需要正常导航和可恢复操作,API 错误需要可机器处理的稳定字段。两者可以来自相同错误类别,却不应直接返回同一段 HTML。客户端请求 JSON 而收到首页模板,会在解析阶段出现第二个错误,掩盖原始故障。内容类型、状态码和正文结构必须匹配,错误响应也要遵守接口约定。
可以采用标准问题详情格式或项目既有错误结构,提供类型、状态、简短标题和关联 ID。面向用户的说明不包含数据库语句、内部路径与堆栈;内部日志保存定位信息并进行脱敏。关联 ID 让客服和维护者找到对应事件,不让用户复制整段敏感错误。字段错误则用稳定字段路径,帮助表单把提示放在正确控件附近。
重试信息也要准确。依赖暂时不可用时可以提供受控重试建议,非法输入不应该自动重试。支付或其他写入接口超时后,客户端不能假设操作未发生,应查询状态或使用幂等标识。博客阅读接口相对简单,但通用错误处理中间件可能被其他业务复用,必须避免把所有失败都标记为安全重试。
用户需要知道下一步,而不是内部堆栈
404 页面可以提供返回文章列表、主页和搜索的入口,并保留正常导航。500 页面应说明暂时无法完成,并提供重试或稳定入口。不要把用户引导到一个依赖相同失败服务的按钮,否则恢复路径只是视觉装饰。
错误页面不应过度使用动画、插画或大型装饰,把真正的链接挤到屏幕之外。标题需要明确,键盘焦点可达。若表单失败,保留非敏感已输入内容和具体字段错误,避免用户重新完成整个任务。
若搜索服务失败,错误页里的“重新搜索”仍调用同一失败接口,就只能重复故障。可以提供静态文章索引、主题导航或首页作为独立路径。若整个站点渲染失败,错误页面也应尽量轻量,不加载同一个复杂内容模块。恢复路径的依赖图应比正常流程更小,否则故障会把提示页面一起带走。
表单失败则保留非敏感输入,但不要把密码、支付信息或一次性令牌重新写入页面。字段错误与全局错误有不同位置:非法邮箱在输入附近提示,服务故障在任务区域解释。错误出现时合理管理焦点,屏幕阅读器能够发现新提示;不要只改变边框颜色。按钮在提交中禁用可以减少重复点击,但网络异常后应恢复可用并表达当前状态。
对于静态博客,四百零四可以建议相近内容,却不要伪造“你想找的是”并自动跳转。用户访问的路径可能来自过期链接,也可能只是输入错误。显示原路径时进行安全转义,日志记录则限制长度和参数,防止攻击者借错误页输出危险 HTML 或写入过量日志。
缓存错误需要独立策略
某些平台可能缓存 404 等响应。刚发布的新文章如果之前被访问过,旧的不存在状态可能继续出现。公开静态内容应根据平台规则设置合适错误缓存时间,并了解清理方式。
500 不应被长期共享缓存,否则一个瞬间故障会变成持续故障。另一方面,适度负缓存可以避免大量重复不存在路径压垮上游。策略必须区分真实路由缺失与临时依赖失败,不能给所有非 200 状态同一 TTL。
假设某篇排期文章在公开前被访问,边缘缓存保存了四百零四;九点发布后源站已经有文章,用户仍可能看到旧错误。发布流程应清理对应缺失对象,或让缺失响应具有可接受的短有效期。缓存键与清理工具需要一致,不能仅清首页。新资源可见性由生成、部署与缓存三项共同决定。
攻击者随机请求大量不存在路径时,适度负缓存可能减少重复回源,但每个随机路径只出现一次就没有复用价值。需要输入限制、请求预算和观察,而不是对所有错误永久缓存。临时服务错误与真实缺失分开处理,防止依赖短暂失败导致文章地址长期变成不存在。
监控也需要分类。四百零四比例上升可能来自爬虫、旧链接或路由回归,五百错误上升可能来自执行故障;合法空搜索增加可能表示搜索质量问题,不是服务器可用性问题。记录页面类型、原因类别与版本,再与访问量比较。仅统计非二百数量,会把正常重定向、条件请求和真实故障混在一起。
构建失败不该生成半个成功站点
若一个内容文件无法解析,发布流程应失败并保留上一个完整部署,而不是跳过坏文章后宣称成功。跳过策略仅适用于明确允许部分结果的业务,并应记录失败数量与原因。
对 RSS、sitemap 和搜索索引同样如此。返回 200 的无效 XML 是内容错误;返回空 feed 则可能造成订阅器误判文章消失。构建校验应验证格式与条目,而不只是 HTTP 状态。
验收包括不存在路径、上游超时、非法内容、空搜索与离线图片。检查真实状态码、恢复入口、日志关联与缓存行为。把错误当成产品流程的一部分,才能同时得到可信监控和可恢复体验。
用一个搜索失败案例校准状态与恢复
用户输入 Rust 事务,索引请求超时。输入仍然合法,任务却未完成,所以界面不能显示“没有相关文章”。可以保留查询与旧结果,标明旧结果所属查询,并提供受控重试和静态主题入口。若用户随后更换查询,旧失败请求返回也不能覆盖新状态。错误属于具体任务版本,不是一个永久挂在全局页面上的字符串。
接口返回受控错误类别后,客户端决定恢复动作。临时不可用允许读操作有限重试,非法查询要求修改输入,权限不足引导重新验证或提供当前可用内容。服务端不需要把自然语言说明作为机器决策字段;稳定类别与安全用户文案分别维护。重试按钮应提交当前任务参数,不能意外重复旧查询或覆盖已更新输入。
监控将这次事件归入依赖超时,而不是空搜索,也不单纯统计前端渲染成功。用户看到了错误说明,只证明错误体验工作,不证明任务成功。可以分别统计搜索完成、空结果、失败与恢复完成,明确分母。没有真实数据时只能说明建议指标,不能用合成测试推导生产失败比例。
写操作失败需要增加一个未知结果分支
同一套错误中间件被用于保存文章时,网络超时并不证明保存失败。数据库可能提交成功,响应在返回途中丢失。客户端应保留未确认状态,查询当前版本或使用操作标识确认,而不是显示“未保存”再无条件提交。阅读接口失败与写入结果未知在语义上不同,通用错误模型应容纳这个区别。
服务端内部也可能在写入成功后发送通知失败。若把整个流程捕获成五百,客户端重试可能重复写入或通知。应清楚定义事务内的成功边界与外部交付责任,必要时用后续可靠任务处理通知。错误分类区分未执行、已执行但后续失败和无法确认,才能给出真实恢复策略。代码抛出异常不等于业务从未发生。
对测试上游使用合成计数器,分别注入提交前失败、提交后响应丢失和通知失败。检查客户端不会把这三种情形都当作安全重试。幂等键有自己的范围、有效期与请求内容绑定,不能只添加一个 UUID 字段就宣称重复问题已解决。错误页面应该帮助用户确认结果,而不是迫使他用连续点击猜测系统状态。
负缓存策略应跟随资源生命周期
准备发布新文章时,先前缺失对象与新正文竞争同一公开地址。可以在发布计划中登记新增地址,执行精确清理,并验证实际正文版本。删除文章时则需要让旧二百对象退出,不仅生成四百零四页面。新增与删除的缓存转换方向相反,都需要独立测试。一个统一错误 TTL 无法覆盖这些生命周期。
某些缺失请求来自随机扫描,路径基数很高,存储大量一次性四百零四不会保护重复热点。按路由范围统计真实缺失、恶意扫描与依赖错误,限制日志和缓存资源。对用户可控路径输出进行转义,对日志限制长度和去除敏感参数。错误入口接收不可信输入,不能因为请求没有找到资源就省略安全处理。
错误表示也需要兼容性测试
API 客户端可能依赖错误字段的类型和结构。升级时保持稳定代码、状态与关联 ID,文案可以改善但不用于分支判断。标准问题详情格式提供共同结构,具体错误类型仍然需要项目契约。不要把上游错误完整嵌入 detail,也不要把内部异常类名当成公开稳定类型。
HTML 错误页面则检查正常导航、键盘顺序和长说明在小屏是否可读。页面自身依赖尽量少,图片与字体缺失时仍可理解。不存在路径的标题应该明确当前资源缺失,不能继续显示主页标题并宣称成功。分享、搜索与辅助技术也会读取这些信息,正确状态与正确正文需要一起成立。
恢复验收不能只看警报消失
修复内容加载器后,检查公开条目数量与代表性文章正文,再检查 RSS 和搜索索引。警报可能因为流量下降而停止,不能证明系统已经恢复。缓存里的故障响应也可能继续存在,验收从真实域名读取而非直接绕过所有中间层。恢复记录包含版本、时间、验证范围与尚未确认事项,避免一个二百样本覆盖所有入口。
对用户失败任务,允许继续从保留输入恢复,但不保存敏感凭据或一次性值到可公开页面。错误说明中的关联 ID 应能够在内部定位,过期或未知 ID 返回普通说明,不成为匿名日志查询接口。诊断便利与访问权限保持分离,恢复流程才不会为解决一个故障制造新的泄漏。
HEAD 请求没有正文,不等于错误表示损坏
部署探针用 HEAD 检查文章存在,收到四百零四却没有 HTML。若它把“正文为空”一律判为模板错误,就会误报:HEAD 的响应不发送正文,状态与相关元数据仍表达结果。验收应按请求方法选择断言;验证错误文案时另发 GET,而不是要求平台给 HEAD 附上一页说明。
反过来,HEAD 返回二百也不足以证明 GET 正文可正常渲染。有的平台分别处理两种方法,轻量元数据路径可能成功,正文路径仍失败。可以用 HEAD 做低成本状态观察,用定期 GET 核对内容,明确两者覆盖范围。禁止只因为无正文就把 HEAD 改成二百,否则原本正确的缺失状态也被隐藏。
接口不支持某个方法时,错误类别属于方法合同,不应假装目标资源不存在。测试已有资源的受支持与不受支持方法,再测试缺失资源,核对路由与中间件的实际选择。错误处理只记录方法类别与安全路径摘要,不把任意请求载荷加入诊断;方法测试也不向生产写接口发送真实副作用请求。
故障注入给错误契约建立可重复证据
合成检查应选择可持续运行的受控样本,不在生产中制造真实数据库故障。预览环境验证故障处理,生产环境观察入口状态和正文特征,两类证据分别保存。测试端点必须限制范围并避免泄漏内部配置,不能为了监控新增一个匿名触发错误或读取日志的接口。
事件恢复后再验证缓存中的错误副本是否消失、表单是否重新可提交、警报是否正确恢复。仅修复依赖而忘记已缓存五百响应,用户仍然看到故障;仅关闭警报而未修复正文,则会让问题继续隐蔽。错误契约的终点不是日志出现了异常,而是系统能准确识别失败并证明恢复完成。
测试时让内容索引读取抛出受控异常,而不是仅请求一个不存在的 slug。这两种输入分别证明依赖失败和资源缺失的处理,不应得到同一成功空页面。再让渲染器失败,检查日志含同一关联标识、公开响应不含堆栈。空搜索则必须保留正常二百状态,提示调整查询,并且不会触发服务器故障警报。
部署平台可能把未知路径回退到首页,这是单页应用常见能力,却未必适合静态博客。直接请求一个随机深路径,检查真实状态和正文,而不是从站内链接导航后观察界面。若平台配置统一回退,修复路由或适配器行为,不仅修改四百零四页面文件。页面存在与服务器选择该页面是两件事。
错误页的静态资源也需要自包含恢复能力。若 CSS 或字体失败,文字与链接仍可使用;若主 JavaScript 失败,导航仍是普通链接。检查移动端长错误说明和键盘顺序,避免恢复按钮被固定页脚遮挡。稳定布局与清楚动作比装饰更重要,用户正在遭遇问题时不需要额外理解复杂视觉隐喻。
另一个反例是代理把 API 错误页替换为品牌 HTML。后端原本返回 JSON 错误类别,前端却在解析 JSON 时得到语法异常,原始失败原因因此消失。让测试后端返回一份固定故障结构,经过真实代理入口核对状态、内容类型和安全字段。修复应在替换发生的层,不让每个客户端靠猜测 HTML 标题恢复错误类别。
若同一路径确实支持 HTML 与 JSON,先定义如何选择表示,再为错误复用相同选择规则;无法满足的媒体类型按明确合同处理,不默默退回首页。缓存也必须认识这些不同表示,不能把先到的 HTML 错误给后来的 JSON 客户端。测试两种客户端反向请求,并让代理故障与应用故障分别发生,才能知道结构是谁生成或替换的。