表单发出去了,转圈也出现了,控制台却说 JSON 里第 0 个位置有意外字符 <。页面本身加载一切正常,所以第一反应往往是怪解析器或者怪隧道。通常两者都没参与:提交是和页面加载完全不同的一次请求,而它带回来的东西并不是 JSON。请按浏览器真正暴露它的顺序来做,因为四种可能的原因对应四种不同的修法。

第 1 步:抓住真正发生的那次提交

在提交之前先打开网络面板,把日志设成跨导航保留记录,然后用一个你有权限使用的账号做一次无害的测试提交。要选表单自己发起的那条请求,而不是页面的文档请求。

小心过滤。XHR 或 Fetch 过滤器很方便,但若框架仍然通过隐藏的 iframe 或动态创建的表单提交,过滤器就会把那唯一重要的请求藏起来。先不过滤地提交一次,看看到底出现了什么。

对选中的请求,在解释之前先记下六件事:方法、包含查询串的完整 URL、状态、重定向链、响应的 Content-Type,以及正文的头几行。Chrome DevTools 的网络面板参考覆盖了这些信息所在的 Headers、Response 与 Initiator 视图,其他浏览器的同类面板也显示同样的字段。

第 2 步:确认它确实是你以为的那条请求

数量惊人的 JSON 错误其实是路由错误。如果提交处理器在调用 preventDefault 之前就抛了异常,浏览器会按 action 属性写的地址做一次原生表单 POST,服务器于是返回整页 HTML。处理器根本没跑,JSON 从未被请求过。

把面板里的 URL 和 JavaScript 实际调用的 URL 对比。找一找:跳去别处的重定向、缺了或多了一个尾部斜杠、方法不一致(处理器发 GET 而端点只收 POST),以及从已经搬迁的页面提交到相对路径的表单。顺着重定向链走到最后,记的是最终 URL,不是第一个。

然后看发起者。如果请求来自文档导航而不是你的脚本,问题已经找到了,再改响应解析器也没有用。

第 3 步:把结果归入四类

每个结果恰好属于其中一类,而类别决定了下一步看哪里。

What you seeCategoryWhere to look
Request never finishes, TLS or connection errorNo usable responseEndpoint reachability, certificate, front-end timeout
Request completes, script gets an opaque failureAccess blockedCross-origin policy, credentials mode, missing response header
200 with an HTML body or a login formWrong representationRedirect, expired session, route fallback, error handler
Body begins with a notice then valid JSONWrong representationServer-side warning output before serialisation
Valid JSON, framework rejects itContract mismatchEnvelope shape, field names, value types

中间几行最容易被误判,因为状态是 200。一次重定向之后落到登录页,是完完全全合格的 HTTP 响应,却是糟糕透顶的 JSON 文档。application/json 这个内容类型救不了它:那只是一个标签,标签贴错的身体照样解析不了。访问被拦截是最像「请求已完成」的一行:面板显示了状态,但跨源策略扣住了正文,对调用脚本来说它和超时一样不可用。

第四行在老 PHP 与老框架的技术栈上同样常见。编码后的载荷之前打出一条弃用提示,就会产出任何 JSON 解析器都不接受的身体,而那条提示往往就是全部原因。

第 4 步:照字面读解析器的报错

报错文本会点名第一个让解析失败的字符,通常不用猜就能定位类别。

Error text (Chrome/V8)What it means
Unexpected token < in JSON at position 0The body starts with markup: an HTML page or an error document
Unexpected end of JSON inputEmpty body, or the response was truncated
Unexpected token o in JSON at position 1You parsed something already parsed, typically an object rather than text
Unexpected token uThe body starts with the bare word undefined, usually an empty response rendered as a string

状态 200 却是空正文,是另一种独立情况:端点跑了,什么也没产出,而下游把这种结果当成了成功。这是应用层契约失败,不是传输失败,该在下一步处理,而不是在代理上。Firefox 与 Safari 对同样四种失败的措辞不同,所以要比对字符或空正文,而不是比对完全一致的字符串。

第 5 步:修证据指向的那一层

路由问题就修请求:改正 action,别让处理器掉下去,让端点与方法对上,然后重测。表现问题就修产出正文的那一环,查清楚为什么本该返回数据的地方返回了错误文档。契约问题就去读框架在你所用版本上真正期望的结构,让响应去贴合它,而不是放松客户端。

访问被拦截的响应要单独走一遍,因为请求本身可能是对的,正文却根本到不了你的代码。把响应的 Access-Control-Allow-Origin 和浏览器发出的确切 Origin 对比,并记住通配符满足不了带凭据的请求。客户端用 credentials: 'include',服务端就要配 Access-Control-Allow-Credentials: true,还要确认调用没有用 no-cors 模式——在那里不透明正文是文档写明的结果,不是缺陷。

把解析器换成更宽容的东西,是唯一应当避免的修法。把响应当代码执行,或者不断剥字符直到能解析,都会掩盖契约错配,并且用注入面换取安静。同样地,状态必须和正文分开检查:携带应用层错误对象的 200 在很多 API 里是常态,只能靠读载荷来处理。

需要别人帮忙时,分享一份最小记录——方法、URL、状态、重定向后的最终 URL、响应内容类型、正文形状和期望的契约——而不是原始导出。网络存档里带着 Cookie、令牌、查询串,以及表单里那些个人数据,自动脱敏并不等于人工审阅。

  • 动手改代码之前,先不过滤地把请求抓下来。
  • 确认发起者是自己的脚本,最终 URL 是自己的端点。
  • 碰解析器之前,先把响应归类。
  • 修路由、访问、表现或契约,绝不靠放松解析器了事。

相关文章