HTTP 200,页面仍是旧状态:定位一次前后端字段错配

AI SaaS工程诊断实战(一)

点了'确认订单',请求返回200,页面还停在草稿。继续给AI一句'再检查一下前端',它可能加刷新、加等待,或者把两种字段都兼容。画面终于变了,却没有说清楚问题在哪里。

遇到这类问题,我会先选定一次操作,把入口、请求、响应、取值和页面放在同一条链上看。下面给一个能本地重做的最小例子。

本文为构造演示,与我的AI Coding系列共用教学素材。字段错误是人为设置的,不是某个模型或IDE的实测故障;不使用客户资料。文稿由AI辅助整理,示例与结论按文中方式核对。

1. 先观察这个不一致

服务里有两个独立对象,初始状态均为draft。两个页面使用相同处理逻辑,分别确认各自对象;关键差别是前端读取字段。

确认接口返回:

json 复制代码
{"id":"broken","status":"confirmed"}

故障页面却这样取值:

javascript 复制代码
const next = data.state;
if (next !== undefined) {
  document.getElementById('display').textContent = next;
}

响应提供status,页面读取state,结果为undefined。条件分支没有更新文本,页面继续显示draft。服务端再读已经是confirmed,所以'请求成功''服务状态改变'和'界面显示正确'是这里需要分别检查的三件事。

2. 沿一次操作查六个位置

位置 检查动作 本例应观察到什么
入口 确认点了哪个对象 故障页操作对象为broken
请求 看方法、路径与对象ID POST /api/orders/broken/confirm
服务结果 再次读取同一对象 GET返回status=confirmed
响应 对照实际层级、字段与值 顶层status,值confirmed
消费 找前端实际读取位置 代码读取state,值undefined
展示 读取页面最后状态 仍是draft

本例的断点就在响应与前端消费之间。真实项目也可能是环境不一致、操作对象错误、缓存或异步任务尚未完成;这张表帮助定位,不把同一种表象都归为字段问题。

3. 依据约定决定改哪边

本例约定成功响应的顶层字段为status,所以将前端改为:

javascript 复制代码
const next = data.status;
if (next !== undefined) {
  document.getElementById('display').textContent = next;
}

这是一处最小修正。data.state ?? data.status在这里没有必要,因为示例不存在两个需要兼容的正式版本。如果真实服务有版本迁移,兼容可能合理,但要说明兼容谁、何时删除。

反过来,如果项目的权威约定是state,该调整的可能是服务端。先找实际约定,再决定改动位置。不要拿本文的字段名当成通用规范。

4. 修正之后要检查什么

在两个独立页面分别执行确认后,对照以下结果:

观察项 /broken /fixed
POST状态 200 200
返回status confirmed confirmed
前端读取 state→undefined status→confirmed
页面展示 draft confirmed
再执行GET confirmed confirmed

修正页只是证明本次操作中页面消费字段与响应一致。服务使用进程内字典,没有数据库持久化;重启会重置。登录、权限、并发、异步任务、缓存和错误提示均未作为本篇验证结论。

这里还留有一个刻意简化:缺字段时静默不更新。生产代码需要按实际契约校验结果并展示错误,不应把这段分支直接当成完整异常处理。

5. 在本地重做

把文末代码保存为demo.py,使用Python 3运行:

text 复制代码
python demo.py

分别打开:

text 复制代码
http://127.0.0.1:8767/broken
http://127.0.0.1:8767/fixed

点击'确认订单'。页面会显示请求、HTTP状态、实际响应、取值字段、页面状态及一次GET再读。用完在终端停止服务。本例仅监听本机,用于教学;Python自带的简单HTTP服务不作为生产部署方案。

6. 下次可以直接填这张检查卡

text 复制代码
本次操作/对象/环境:
期望结果/实际结果:
请求方法、路径、对象ID:
响应状态、层级、字段和类型:
独立读取结果及依据:
前端实际取值与写入位置:
页面最后状态:
断点、改动与验证结果:
尚未检查的环节:

先让AI按卡片列证据和未知,再提出修改。一次请求返回成功之后,继续看用户期待的结果有没有发生;把这条链说明白,修复范围通常也更容易确定。

------ Linksass|AI SaaS独立开发者

完整演示代码

代码仅使用Python标准库。两个页面和API都由同一服务提供。

python 复制代码
import json
from http.server import BaseHTTPRequestHandler, HTTPServer

orders = {"broken": "draft", "fixed": "draft"}
PAGE = '''<!doctype html><html lang="zh-CN"><meta charset="utf-8">
<title>前后端字段契约演示</title><style>body{font:18px system-ui;max-width:850px;margin:50px auto;line-height:1.8}pre{background:#eef2f6;padding:20px;white-space:pre-wrap}button{font:inherit;padding:8px 20px}</style>
<h1>前后端字段契约:MODE</h1>
<p>构造演示:同一响应,分别读取 state / status。服务只在本机运行,存储为进程内字典。</p>
<p>当前展示:<strong id="display">draft</strong></p>
<button id="confirm">确认订单</button><pre id="evidence">尚未操作</pre>
<script>
document.getElementById('confirm').onclick = async () => {
  const response = await fetch('/api/orders/MODE/confirm', {method:'POST'});
  const data = await response.json();
  const next = data.FIELD;
  if (next !== undefined) document.getElementById('display').textContent = next;
  const rereadResponse = await fetch('/api/orders/MODE');
  const reread = await rereadResponse.json();
  document.getElementById('evidence').textContent = JSON.stringify({
    request: 'POST /api/orders/MODE/confirm', http: response.status,
    response: data, frontend_field: 'FIELD', frontend_value: next ?? '(undefined)',
    displayed: document.getElementById('display').textContent,
    reread_http: rereadResponse.status, reread: reread
  }, null, 2);
};
</script></html>'''

class Handler(BaseHTTPRequestHandler):
    def reply(self, payload, code=200, html=False):
        content = payload.encode('utf-8') if html else json.dumps(payload).encode('utf-8')
        self.send_response(code)
        self.send_header('Content-Type', 'text/html; charset=utf-8' if html else 'application/json')
        self.send_header('Content-Length', str(len(content)))
        self.end_headers()
        self.wfile.write(content)

    def do_GET(self):
        mode = self.path.strip('/')
        if mode in orders:
            self.reply(PAGE.replace('MODE', mode).replace('FIELD', 'state' if mode == 'broken' else 'status'), html=True)
        elif self.path.startswith('/api/orders/') and self.path.rsplit('/', 1)[-1] in orders:
            key = self.path.rsplit('/', 1)[-1]
            self.reply({'id': key, 'status': orders[key]})
        else:
            self.reply({'error': 'not found'}, 404)

    def do_POST(self):
        parts = self.path.strip('/').split('/')
        if len(parts) == 4 and parts[:2] == ['api', 'orders'] and parts[2] in orders and parts[3] == 'confirm':
            key = parts[2]
            orders[key] = 'confirmed'
            self.reply({'id': key, 'status': orders[key]})
        else:
            self.reply({'error': 'not found'}, 404)

if __name__ == '__main__':
    print('Demo: http://127.0.0.1:8767/broken and /fixed', flush=True)
    HTTPServer(('127.0.0.1', 8767), Handler).serve_forever()

参考:Python http.server、MDN Response.ok。

相关推荐
用户72526496629691 小时前
浏览器为什么加载不了超大图片
前端·javascript
OpsEye1 小时前
企业 AI 治理到底管什么:预算、权限、会话、效能四个维度
javascript·ai编程
keyipatience3 小时前
JS学习笔记(2)数组,函数,作用域,对象
前端·javascript·笔记·学习·html
donk13143 小时前
React Native for OpenHarmony 实战:三方库 react-native-segmented-control-tab 的鸿蒙化适配指南
javascript·react native·react.js·华为·harmonyos
Geoffwo3 小时前
robotjs0.6.0鼠标中轴滚动失效修复方案
前端·javascript
溪语流沙4 小时前
【Web全栈进阶】Vue Router + Pinia:SPA路由与全局状态
前端·javascript·vue.js
兰亭妙微UI设计公司4 小时前
兰亭妙微软件界面设计公司的解锁按钮设计10大密码
开发语言·前端·javascript
u0111026755 小时前
WebP 转成 PNG 后为什么更大?图片格式转换不等于压缩
前端·javascript·vue.js
小皮虾6 小时前
小程序全局状态还能这么简单?只需初始化一次,页面自动同步
前端·javascript·微信小程序