PAYATHON 2026

如何处理 Safari 中 Ajax call 的 timeout

支付小周

结论

timeout 不会让已经成功或失败的请求继续等待 30 秒。服务器一旦返回有效响应,jQuery 就会立即调用 success 或 error。只有请求在规定时间内没有完成,才会以 textStatus === 'timeout' 进入 error。

当前代码有一个明确的问题:always 不是 $.ajax() 的配置项。应该在配置对象中使用 complete,或者在 $.ajax() 返回的 jqXHR 上调用 .always()。

推荐修改

performAuthorizePaymentRequest: function (payment) {
    return new Promise(function (resolve, reject) {
        $.ajax({
            type: 'POST',
            url: encodedContextPath + '/checkout/authorise_order',
            data: JSON.stringify(payment),
            dataType: 'json',
            contentType: 'application/json',
            timeout: 30000,

            success: function (data) {
                resolve(data);
            },

            error: function (jqXHR, textStatus, errorThrown) {
                if (textStatus === 'timeout') {
                    reject(new Error('Payment API request timed out'));
                    return;
                }

                reject(new Error(
                    errorThrown ||
                    jqXHR.responseText ||
                    'Payment API request failed'
                ));
            },

            complete: function () {
                pageSpinner.end();
            }
        });
    });
}

调用方也要同时处理成功和失败:

performAuthorizePaymentRequest(payment)
    .then(function (response) {
        showSuccessMessage(response);
    })
    .catch(function (error) {
        showErrorMessage(error.message);
    });

修改后,请求成功会立即执行 resolve();遇到 HTTP 错误、网络错误或 JSON 解析错误时,会立即执行 reject()。只有请求超过 30 秒仍未完成,才会报告 timeout。无论请求结果如何,complete 都会关闭 spinner。

更简洁的写法

$.ajax() 本身返回一个类似 Promise 的 jqXHR,通常不需要再套一层 new Promise():

performAuthorizePaymentRequest: function (payment) {
    return $.ajax({
        type: 'POST',
        url: encodedContextPath + '/checkout/authorise_order',
        data: JSON.stringify(payment),
        dataType: 'json',
        contentType: 'application/json',
        timeout: 30000
    }).always(function () {
        pageSpinner.end();
    });
}

调用时可以使用 jQuery 的回调:

performAuthorizePaymentRequest(payment)
    .done(function (response) {
        showSuccessMessage(response);
    })
    .fail(function (jqXHR, textStatus, errorThrown) {
        if (textStatus === 'timeout') {
            showErrorMessage('Payment API request timed out');
        } else {
            showErrorMessage(
                errorThrown ||
                jqXHR.responseText ||
                'Payment API request failed'
            );
        }
    });

如果后续代码要求这个函数返回原生 Promise,保留前一种包装方式更合适。

为什么看起来总是在等待 timeout

如果修改后问题仍然存在,可以重点检查下面几项。

后端是否真正结束了 HTTP 响应

“后端抛出异常”不等于浏览器已经收到错误响应。如果服务端捕获异常后既没有发送响应,也没有关闭连接,浏览器就只能继续等待,直到最终触发 timeout。

后端应及时返回明确的 HTTP 状态,例如 400、422 或 500,并结束响应。

返回内容是否为有效 JSON

代码中指定了:

dataType: 'json'

因此,响应内容必须是有效 JSON。空响应、HTML 错误页或格式错误的 JSON 都可能进入 error,此时 textStatus 通常是 parsererror。

可以暂时记录完整的错误信息来确认:

error: function (jqXHR, textStatus, errorThrown) {
    console.error({
        status: jqXHR.status,
        textStatus: textStatus,
        errorThrown: errorThrown,
        responseText: jqXHR.responseText
    });
}

调用方是否延迟了 UI 更新

resolve() 只表示 Promise 已经完成。成功消息何时显示,还取决于调用方的 .then()、.done() 以及后续业务代码。如果后面还有定时器或尚未完成的 Promise 链,延迟未必来自 $.ajax()。

注意事项

  • timeout: 30000 的单位是毫秒,也就是 30 秒。
  • 前端 timeout 只会停止等待并中止客户端请求,不能保证后端已经停止处理付款。支付接口必须使用幂等键或查询订单状态,避免超时重试导致重复扣款。
  • Safari 不会改变 jQuery 的基本回调规则。如果这里还涉及 Apple Pay 的 completePayment() 时限,需要在成功、失败和超时三个分支中及时结束对应的支付流程。
  • pageSpinner.end() 应放在 complete 中,或者通过返回值调用 .always()。不能把 always 直接写进 $.ajax() 的配置对象。

备注:内容仅供参考。