PAYATHON 2026

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 时处理结果。完成这些修改后,再检查本地文件页面造成的跨源限制。

备注:内容仅供参考。