概览
操作流程
记录限制和耗时
在故障环境执行一次设置有限超时的请求,记录各阶段耗时。
修复已定位的延迟
根据失败阶段选择修复动作,然后重复相同的受控请求。
cURL error 28 表示操作因为达到某项时间限制而停止。它可能发生在连接尚未建立时、等待首个响应字节时,也可能发生在下载过程中。决定增加超时或更换代理前,应先阅读完整错误,并收集请求各阶段耗时。
这个消息可能来自 curl 命令、PHP/libcurl、WordPress 或其他应用。请在失败请求实际运行的环境中测试;笔记本上的请求成功,并不能证明托管服务器具有相同线路。
下一步
哪项限制触发了 cURL error 28?

连接超时限制连接建立阶段;max-time 限制一次完整传输。图中各阶段宽度仅作示意。
控制项 | 限制什么 | 诊断用途 |
|---|---|---|
| 连接建立,包括相关名称解析和协议握手 | 识别无法在规定时间内建立连接的请求 |
| 整个单次传输尝试 | 为停滞或下载过慢的操作设置上限 |
| 持续低于指定值的传输速度 | 停止长时间低速传输 |
应用自身的期限 | 外层任务或请求 | 需要单独检查;应用可能比 curl 的限制更早停止操作 |
curl 超时指南介绍了前三项控制。错误 28 表示超时;证书校验失败、连接被拒绝等其他情况有各自的错误。应保留实际错误码,不要把所有网络故障都当成超时。
1. 从失败的运行环境收集耗时
将 TARGET_URL 替换为目标地址。下面的 macOS/Linux 命令不会把响应正文输出到终端,而是打印累计耗时。命令没有增加重试,也不跟随重定向,以便保留第一次请求的结果。
curl --silent --show-error --output /dev/null \
--connect-timeout 10 --max-time 30 \
--write-out 'http=%{http_code} dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} first_byte=%{time_starttransfer} total=%{time_total}\n' \
'TARGET_URL'Windows 请使用 curl.exe 和 NUL。这些数值是从开始到各个节点的累计时间,不是可以直接相加的独立阶段时长。某项为零,可能表示该阶段未完成,也可能表示本次请求没有使用该阶段。应结合标准错误输出和进程退出码解读。
如果运行环境本来就需要使用代理,请按照 MIYAIP 连接教程补充协议、主机、端口和鉴权字段。只有在直连符合预期且被允许时,才进行直连对照。保持目标、请求方法和时间限制一致,才能让比较回答清楚的问题。
2. 将现象对应到具体检查项
现象 | 接下来检查什么 |
|---|---|
DNS 一直未完成 | 从同一主机或容器解析端点;检查所用解析器和网络可达性 |
连接建立阶段未完成 | 确认主机、端口、路由和出站规则。对于原生代理,还应检查所需的海外网络路径 |
连接已经就绪,但一直没有响应 | 与另一个允许访问的目标进行比较;检查隧道协商、目标服务状态和服务器端处理 |
已经收到部分正文,随后超时 | 检查响应大小、持续传输速度,以及总时限是否符合任务需求。只有应用能够校验并继续下载时,才保留部分数据 |
只有部署后的任务失败 | 将运行环境的代理变量、DNS、容器路由和应用期限与本地环境比较 |
只有并发任务失败 | 先降低并发并测量失败率,再考虑增加时限。等待更久不会增加连接容量 |
对于代理路径,代理测速工具提供的是从 MIYAIP 测试服务器得到的测量。应将它与本地命令的结果对照;两者的发起位置不同,目标也可能不同。
3. 修复发起请求的应用,再重新测试
使用 WordPress 时,记录站点健康检查的信息,并确认失败的请求属于 REST 请求、回环请求还是外部服务调用。请托管服务管理员在 PHP 运行环境中复现完全相同的请求。如果怀疑插件冲突,应在预发布环境中对照测试,测试后恢复配置。泛泛地更换代理并不能修复损坏的回环线路。
对于定时采集器或 API 客户端,应根据预期工作负载选择时间限制。小型状态响应和大型导出需要不同的时间预算。只有确认操作本身有效、只是需要更久时,才增加时限;同时保留有限的任务总期限。
仅在失败可能是暂时性的情况下,才对只读请求重试,并限制尝试次数和等待预算。对于超时的购买、提交或其他会改变状态的请求,不要自动重新发送:即使响应丢失,远程操作也可能已经完成。收到 429 时,应遵循 Retry-After,而不是立刻轮换 IP。
在可控示例中识别超时
在本地启动随附的 network_demo.py 后,/slow 会等待两秒再响应。下面的命令刻意只允许等待 0.2 秒:
curl --noproxy '*' --max-time 0.2 'http://127.0.0.1:8766/slow'这个本地示例以退出码 28 结束,展示传输时限到期的情况。将参数改为 --max-time 3 再运行,就能允许两秒后的响应完成。
常见问题
错误 28 和 HTTP 504 是一回事吗?
不是。错误 28 是 curl 操作的结果,HTTP 504 是网关返回的响应。应同时记录进程结果,以及实际收到的任何 HTTP 状态。
--insecure 能解决这个问题吗?
禁用证书校验不能解决一般性的传输超时。如果实际结果是证书错误,应单独排查该错误。
是否应该不断增加超时,直到请求成功?
先找到延迟发生的位置。错误线路或停滞的服务可能耗尽任何更大的等待预算。
将端点测试与自己的运行环境对照
检查连接、比较服务器端测速结果,并从应用运行环境重复相同请求。
下一步
运行本地演示
下载 network_demo.py,或将以下完整代码保存为同名文件。使用 Python 3 运行 python network_demo.py。服务仅监听 127.0.0.1:8766,使用完按 Ctrl+C 退出。
"""Local HTTP demonstration. No external requests, credentials or MIYAIP service calls."""
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlsplit
import json
import time
class DemoHandler(BaseHTTPRequestHandler):
def log_message(self, *_):
pass
def reply(self, code, body=b"", headers=None):
self.send_response(code)
for key, value in (headers or {}).items():
self.send_header(key, value)
self.send_header("Content-Length", str(len(body)))
try:
self.end_headers()
self.wfile.write(body)
except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
pass
def do_CONNECT(self):
self.reply(407, headers={"Proxy-Authenticate": 'Basic realm="local-demo"'})
def do_GET(self):
path = urlsplit(self.path).path
if path == "/redirect":
return self.reply(302, headers={"Location": "/headers"})
if path == "/loop":
return self.reply(302, headers={"Location": "/loop"})
if path == "/slow":
time.sleep(2)
if path in {"/headers", "/slow"}:
payload = {"method": self.command, "headers": dict(self.headers)}
return self.reply(200, json.dumps(payload, indent=2).encode(),
{"Content-Type": "application/json"})
self.reply(404, b"Use /headers, /redirect, /loop or /slow")
def do_POST(self):
body = self.rfile.read(int(self.headers.get("Content-Length", "0")))
if urlsplit(self.path).path == "/redirect307":
return self.reply(307, headers={"Location": "/headers"})
if urlsplit(self.path).path == "/redirect302":
return self.reply(302, headers={"Location": "/headers"})
payload = {"method": self.command, "headers": dict(self.headers),
"body": body.decode("utf-8", errors="replace")}
self.reply(200, json.dumps(payload, indent=2).encode(),
{"Content-Type": "application/json"})
if __name__ == "__main__":
print("Local demonstration: http://127.0.0.1:8766 (Ctrl+C to stop)", flush=True)
ThreadingHTTPServer(("127.0.0.1", 8766), DemoHandler).serve_forever()