iOS 中 Web service AJAX POST 请求返回 400 错误
结论
POST 数据的编码方式没有问题。最可能的原因在请求地址上。AJAX 使用的是:
http://localhost/testPost
而成功的 curl 请求使用的是:
http://localhost/testPost/
如果 /testPost 是一个目录,Apache 通常会把不带尾部 / 的地址重定向到 /testPost/。浏览器跟随 301 或 302 重定向时,原来的 POST 请求很可能变成 GET,POST body 也会丢失。这样一来,PHP 就收不到 name 和 age,随后执行:
sendResponse(400, 'Invalid request');
代码里还有三个互不相关的问题:
- JavaScript 不应该设置
Content-Length和Connection。 onreadystatechange没有检查readyState === 4,因此提示会弹出多次。- 从本地
file://页面访问http://localhost属于跨源请求,可能被 CORS 拦截,并表现为status === 0。
先修正请求地址
把 AJAX 的 URL 改成与成功的 curl 命令完全一致:
xmlhttp.open("POST", "http://localhost/testPost/", true);
这里不能漏掉尾部的 /。也可以直接请求实际的 PHP 文件:
xmlhttp.open("POST", "http://localhost/testPost/index.php", true);
具体使用哪个路径,要看服务器的目录和路由配置。
可以运行下面的命令,确认请求是否发生了重定向:
curl -v -d "name=john&age=40" http://localhost/testPost
如果响应中出现类似内容,基本就能确认主要原因:
HTTP/1.1 301 Moved Permanently
Location: http://localhost/testPost/
然后再测试带 / 的地址:
curl -v -d "name=john&age=40" http://localhost/testPost/
这里使用 -d,是为了发送与 AJAX 相同的 application/x-www-form-urlencoded 数据。原来的 curl -F 发送的是 multipart/form-data。PHP 虽然也能解析,但它与浏览器发送的请求并不完全相同。
修正后的 AJAX 代码
function submitData() {
var xmlhttp = new XMLHttpRequest();
var params =
"name=" + encodeURIComponent("john") +
"&age=" + encodeURIComponent("40");
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState !== 4) {
return;
}
if (xmlhttp.status >= 200 && xmlhttp.status < 300) {
console.log(xmlhttp.responseText);
} else {
console.error(
"Request failed:",
xmlhttp.status,
xmlhttp.responseText
);
}
};
xmlhttp.open("POST", "http://localhost/testPost/", true);
xmlhttp.setRequestHeader(
"Content-Type",
"application/x-www-form-urlencoded; charset=UTF-8"
);
xmlhttp.send(params);
}
删除下面两个请求头:
xmlhttp.setRequestHeader("Content-length", params.length);
xmlhttp.setRequestHeader("Connection", "close");
Content-Length 和 Connection 由浏览器的网络层管理。脚本尝试设置它们时,浏览器可能会忽略,也可能直接拒绝操作。字符数也不一定等于 UTF-8 编码后的字节数,所以用 params.length 手工计算并不可靠。
为什么提示会出现三次
每次 readyState 发生变化,onreadystatechange 都会执行。常见状态包括:
2:已收到响应头3:正在接收响应正文4:请求完成
原代码会在每次状态变化时执行:
alert(xmlhttp.status);
所以提示会连续弹出多次。应该等请求完成后再读取最终状态:
if (xmlhttp.readyState === 4) {
alert(xmlhttp.status);
}
也可以改用更简洁的 onload:
var xhr = new XMLHttpRequest();
xhr.open("POST", "http://localhost/testPost/", true);
xhr.setRequestHeader(
"Content-Type",
"application/x-www-form-urlencoded; charset=UTF-8"
);
xhr.onload = function () {
console.log(xhr.status, xhr.responseText);
};
xhr.onerror = function () {
console.error("Network or CORS error");
};
xhr.send("name=john&age=40");
本地 HTML 为什么会返回状态码 0
如果 HTML 是直接从磁盘打开的:
file:///path/to/test.html
而请求发往:
http://localhost/testPost/
页面和接口就不是同源。浏览器可能把本地文件视为 null origin,并根据 CORS 规则限制脚本读取响应。JavaScript 中常见的结果是:
status = 0
状态码 0 不是服务器返回的 HTTP 状态码。它通常表示浏览器没有向脚本提供有效响应,可能的原因包括:
- 请求被 CORS 策略拦截;
- 网络连接失败;
- 请求被安全策略阻止;
- 页面导航或请求被取消。
开发时,最简单的处理方式是让 HTML 和 PHP 由同一个 HTTP 服务提供,例如:
http://localhost/app/index.html
http://localhost/testPost/
如果确实需要跨源访问,后端必须返回合适的 CORS 响应头。下面的示例仅适用于明确允许该来源的情况:
header('Access-Control-Allow-Origin: http://localhost');
如果页面来自 file://,不建议长期使用宽泛的 CORS 配置放行。更稳妥的办法是把前端页面也放到本地 Web 服务中,或者为 hybrid application 设置明确的来源和白名单策略。
PHP 响应也可以稍作调整
接口返回的是 JSON,最好声明正确的响应类型:
function testPost()
{
if (isset($_POST['name'], $_POST['age'])) {
$result = array(
'name' => $_POST['name'],
'age' => $_POST['age']
);
sendResponse(
200,
json_encode($result),
'application/json; charset=UTF-8'
);
return;
}
sendResponse(
400,
json_encode(array('error' => 'Invalid request')),
'application/json; charset=UTF-8'
);
}
这个改动不会影响 $_POST 的解析,但可以让客户端正确识别响应格式。
还要注意 localhost 指向哪里
在 iOS Simulator 中,localhost 通常指向运行 Simulator 的 Mac,因此当前请求可以到达服务器。
如果以后改用真机测试,localhost 指向的是 iPhone 本身,而不是 Mac。这时需要使用 Mac 在局域网中的 IP 地址,例如:
http://192.168.1.10/testPost/
较新的 iOS 环境还可能通过 App Transport Security 限制明文 HTTP。不过当前服务器已经实际返回了 400,说明这次并不是 HTTP 连接单纯被 ATS 阻断。
排查时,先补上 URL 尾部的 /,删除由浏览器管理的请求头,并且只在 readyState === 4 时处理结果。完成这些修改后,再检查本地文件页面造成的跨源限制。
备注:内容仅供参考。
