PowerShell 7:用本地回环区分 HTTP 错误、超时和连接失败

举报
yd_235245466 发表于 2026/10/30 08:26:21 2026/10/30
【摘要】 Invoke-WebRequest 请求失败时,并非都能拿到 HTTP 状态码。服务端返回 503 与连接尚未建立就失败,是两类不同的结果。下面用一个只监听 127.0.0.1 的临时服务,让它们和超时在同一次测试中出现。环境:Windows 11、PowerShell 7.6.7、.NET 10.0.13。示例要求 PowerShell 7.4 或更高版本及 Start-ThreadJob...

Invoke-WebRequest 请求失败时,并非都能拿到 HTTP 状态码。服务端返回 503 与连接尚未建立就失败,是两类不同的结果。下面用一个只监听 127.0.0.1 的临时服务,让它们和超时在同一次测试中出现。
环境:Windows 11、PowerShell 7.6.7、.NET 10.0.13。示例要求 PowerShell 7.4 或更高版本及 Start-ThreadJob;仅在上述版本实测。无需外网、账号或安装依赖,不要把本地演示服务暴露到公网。
在 PowerShell 7 中一次性粘贴运行:

& {
    $ErrorActionPreference = 'Stop'
    if ($PSVersionTable.PSVersion -lt [version]'7.4') {
        throw 'Requires PowerShell 7.4 or later; verified with PowerShell 7.6.7'
    }
    $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0)
    $listener.ExclusiveAddressUse = $true
    $job = $null
    try {
        # Bind on loopback only, with an automatically selected port.
        $listener.Start()
        $port = ([System.Net.IPEndPoint]$listener.LocalEndpoint).Port
        $job = Start-ThreadJob -ArgumentList $listener -ScriptBlock {
            param($server)
            $ErrorActionPreference = 'Stop'
            # Test order: first response 200, second 503, third deliberately stalls.
            foreach ($status in @('200 OK', '503 Service Unavailable', 'slow')) {
                $client = $server.AcceptTcpClient()
                try {
                    if ($status -eq 'slow') {
                        Start-Sleep -Seconds 3
                        continue
                    }
                    $stream = $client.GetStream()
                    $reader = [System.IO.StreamReader]::new($stream, [System.Text.Encoding]::ASCII, $false, 1024, $true)
                    try {
                        while (($line = $reader.ReadLine()) -ne '') { if ($null -eq $line) { break } }
                        $data = [System.Text.Encoding]::ASCII.GetBytes("HTTP/1.1 $status`r`nContent-Length: 2`r`nConnection: close`r`n`r`nok")
                        $stream.Write($data, 0, $data.Length)
                    } finally { $reader.Dispose() }
                } finally { $client.Dispose() }
            }
        }
        foreach ($name in @('ok', 'error', 'slow', 'down')) {
            if ($name -eq 'down') {
                # Wait for the stalled connection to close before shutting down the listener.
                $null = $job | Wait-Job -Timeout 6
                if ($job.State -ne 'Completed') { throw 'Local server did not finish normally' }
                $listener.Stop()
            }
            $timeoutSeconds = if ($name -eq 'down') { 5 } else { 1 }
            # Labels identify requests; the tiny server responds in this order, not by routing.
            try {
                $response = Invoke-WebRequest -Uri "http://127.0.0.1:$port/$name" -NoProxy -TimeoutSec $timeoutSeconds -ErrorAction Stop
                [pscustomobject]@{ Case = $name; Outcome = 'success'; HTTP = [int]$response.StatusCode; Kind = 'None'; Detail = '-' }
            } catch {
                $kind = $_.Exception.GetType().Name
                $inner = $_.Exception.InnerException
                $detail = if ($inner -is [System.Net.Sockets.SocketException]) { $inner.SocketErrorCode.ToString() } else { '-' }
                if ($null -ne $_.Exception.Response) {
                    [pscustomobject]@{ Case = $name; Outcome = 'http-error'; HTTP = [int]$_.Exception.Response.StatusCode; Kind = $kind; Detail = $detail }
                } elseif ($_.Exception -is [System.OperationCanceledException]) {
                    [pscustomobject]@{ Case = $name; Outcome = 'timeout'; HTTP = $null; Kind = $kind; Detail = $detail }
                } elseif ($_.Exception -is [System.Net.Http.HttpRequestException]) {
                    [pscustomobject]@{ Case = $name; Outcome = 'request-failed'; HTTP = $null; Kind = $kind; Detail = $detail }
                } else { throw }
            }
        }
    } finally {
        $listener.Stop()
        if ($null -ne $job) { $job | Stop-Job; $job | Remove-Job -Force }
    }
} | Format-Table -AutoSize

一次实际输出如下(空白的 HTTP 列表示没有收到可用状态码):

Case  Outcome        HTTP Kind                  Detail
----  -------        ---- ----                  ------
ok    success         200 None                  -
error http-error      503 HttpResponseException -
slow  timeout             TaskCanceledException -
down  request-failed      HttpRequestException  ConnectionRefused

ok 收到 200;error 收到 503,默认进入 catch,可以从 Exception.Response.StatusCode 获取状态码。slow 的服务端接受连接后暂停 3 秒,不发送响应;客户端的 1 秒限制先到,出现 TaskCanceledException。down 等待慢连接结束、关闭监听器,再请求同一端口;本次内层 SocketException 的代码为 ConnectionRefused。它是本机服务未监听时的连接失败,不代表所有不可达网络都会返回同一个错误。
这里的 HTTP 服务刻意简化:前三次连接按顺序处理,路径只是标签,不根据路径路由。脚本只监听回环地址,-NoProxy 防止本地请求绕到代理,端口由系统临时分配;finally 停止监听器和线程任务。关闭监听器与重新请求之间理论上仍有其他进程占用同一端口的竞态,极少数情况下应重跑,不应把意外成功当作连接失败。
down 使用 5 秒限制,是因为本机实测较短限制可能先超时,来不及得到拒绝连接错误;其他机器、版本或防火墙的表现可能不同。示例把 OperationCanceledException 归为超时只适合本次没有外部取消的场景,生产代码还要考虑主动取消;HttpRequestException 则可能来自 DNS、TLS 等其他阶段,不能一概判定为“服务未启动”。实际响应还可能包含重定向或读取流中断,本文没有覆盖。
PowerShell 7.4 起 -TimeoutSec 是 -ConnectionTimeoutSeconds 的别名;-OperationTimeoutSeconds 控制流读取中的等待间隔,两者不等同。示例仅模拟等待响应头时的超时,没有验证流读取超时。详情见 Microsoft Learn 的 Invoke-WebRequest 文档。
AI 协助说明: 本文和示例由 AI 协助起草与编写,代码已在上述本机环境实际执行并核对四种结果;这些结果不是云服务的实测数据。

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。