PS C:\Users\Administrator> # 带空格的URL请求 PS C:\Users\Administrator> $response = Invoke-CurlRequest -Url "https://api.example.com/path with space" -Verbose 详细信息: 鎵ц curl 鍛戒护: curl https://api.example.com/path with space -X GET --silent --show-error --write-out HTTP_STATUS:%{http_code} -H User-Agent: PowerShell CurlTools/1.0 curl: (6) Could not resolve host: api.example.com HTTP_STATUS:000 curl: (6) Could not resolve host: with HTTP_STATUS:000 curl: (6) Could not resolve host: space HTTP_STATUS:000 curl: (6) Could not resolve host: PowerShell HTTP_STATUS:000 curl: (6) Could not resolve host: CurlTools HTTP_STATUS:000 PS C:\Users\Administrator> PS C:\Users\Administrator> # 文件下载(自动创建目录) PS C:\Users\Administrator> Invoke-CurlRequest -Url "https://example.com/file.zip" -OutputFile "C:\Downloads\New Folder\file.zip" curl: (23) Failure writing output to destination HTTP_STATUS:404 curl: (3) URL rejected: Bad hostname HTTP_STATUS:000 curl: (6) Could not resolve host: PowerShell HTTP_STATUS:000 curl: (6) Could not resolve host: CurlTools HTTP_STATUS:000 Status : Error Content : 无法将“ExtractCurlError”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正确,然后再试一次。 Data : ErrorMessage : 无法将“ExtractCurlError”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正确,然后再试一次。 ErrorType : Url : https://example.com/file.zip Method : GET StatusCode : Headers : Latency : 17159 Size : 0 Timestamp : 2025/8/16 0:59:49 RawOutput : PS C:\Users\Administrator> PS C:\Users\Administrator> # 带特殊字符的请求头 PS C:\Users\Administrator> $headers = @{ >> "Authorization" = "Bearer token:123" >> "Custom-Header" = "value;with;special=chars" >> } >> $response = Invoke-CurlRequest -Url "https://api.example.com" -Headers $headers >> curl: (6) Could not resolve host: api.example.com HTTP_STATUS:000 curl: (3) URL rejected: Bad hostname HTTP_STATUS:000 curl: (6) Could not resolve host: Bearer HTTP_STATUS:000 curl: (6) Could not resolve host: token HTTP_STATUS:000 curl: (6) Could not resolve host: PowerShell HTTP_STATUS:000 curl: (6) Could not resolve host: CurlTools HTTP_STATUS:000 PS C:\Users\Administrator> # 忽略SSL证书验证(显示警告) PS C:\Users\Administrator> $response = Invoke-CurlRequest -Url "https://self-signed.example" -Insecure 警告: 绂佺敤SSL璇佷功楠岃瘉 - 瀹夊叏椋庨櫓! curl: (6) Could not resolve host: self-signed.example HTTP_STATUS:000 curl: (6) Could not resolve host: PowerShell HTTP_STATUS:000 curl: (6) Could not resolve host: CurlTools HTTP_STATUS:000 PS C:\Users\Administrator> PS C:\Users\Administrator> # 带重试的API调用 PS C:\Users\Administrator> $response = Invoke-CurlRequest -Url "https://api.example.com/data" ` >> -Method GET ` >> -RetryCount 3 ` >> -RetryInterval 2 ` >> -RetryOnStatusCodes 500, 503 ` >> -RetryOnTimeout >> Invoke-CurlRequest : 找不到与参数名称“RetryCount”匹配的参数。 所在位置 行:3 字符: 5 + -RetryCount 3 ` + ~~~~~~~~~~~ + CategoryInfo : InvalidArgument: (:) [Invoke-CurlRequest],ParameterBindingException + FullyQualifiedErrorId : NamedParameterNotFound,Invoke-CurlRequest PS C:\Users\Administrator> # 文件下载带重试 PS C:\Users\Administrator> $response = Invoke-CurlRequest -Url "https://example.com/largefile.zip" ` >> -OutputFile "download.zip" ` >> -RetryCount 2 ` >> -RetryInterval 1 >> Invoke-CurlRequest : 找不到与参数名称“RetryCount”匹配的参数。 所在位置 行:3 字符: 5 + -RetryCount 2 ` + ~~~~~~~~~~~ + CategoryInfo : InvalidArgument: (:) [Invoke-CurlRequest],ParameterBindingException + FullyQualifiedErrorId : NamedParameterNotFound,Invoke-CurlRequest PS C:\Users\Administrator> # 详细错误诊断 PS C:\Users\Administrator> if ($response.Status -eq "JsonParseError") { >> Write-Host "JSON解析失败:" >> Write-Host $response.Data.ErrorContext >> } >> PS C:\Users\Administrator>
时间: 2025-08-16 11:58:55 浏览: 3
<think>根据提供的错误信息,我们可以分析出几个关键问题:
1. **带空格的URL处理问题**:
- 错误信息显示多个"Could not resolve host",表明curl将URL中的空格视为分隔符,将URL拆成了多个部分
- 虽然我们在代码中已经对包含空格的URL添加了引号,但错误信息显示引号没有正确传递
2. **文件下载失败**:
- 错误信息:`Failure writing output to destination` 和 `Bad hostname`
- 同时出现`ExtractCurlError`函数未识别的错误(函数作用域问题)
3. **特殊字符请求头问题**:
- 请求头中的特殊字符导致curl命令解析错误
- 错误信息显示curl试图解析请求头值作为独立参数
4. **重试参数问题**:
- 调用时提示`找不到与参数名称“RetryCount”匹配的参数`
- 表明函数定义中的重试参数没有正确声明
5. **中文乱码问题**:
- 警告信息显示为乱码(如"鎵ц curl 鍛戒护")
6. **函数作用域问题**:
- 辅助函数`ExtractCurlError`未在正确作用域中定义
### 解决方案
#### 1. 修复URL空格处理问题
问题在于PowerShell在传递参数时可能去掉了引号。解决方案:使用单引号包裹整个URL,并在curl参数中使用单引号。
修改代码:
```powershell
# 将URL安全处理改为:
$safeUrl = "`'$Url`'"
```
#### 2. 修复文件下载失败
- 确保输出目录存在(已实现)
- 修复`ExtractCurlError`函数作用域问题:将辅助函数移到主函数内部(嵌套函数)
#### 3. 请求头特殊字符处理
确保请求头值中的特殊字符(如分号、等号)不会破坏curl命令。解决方案:使用单引号包裹整个头。
修改代码:
```powershell
# 将请求头构建改为:
$curlArgs += "-H"
$curlArgs += "`"$key`: $headerValue`""
# 改为:
$curlArgs += "-H"
$curlArgs += "`'$key: $headerValue`'"
```
#### 4. 修复重试参数问题
确保重试参数在param块中正确定义。检查参数声明位置。
#### 5. 中文乱码问题
确保在curl执行前设置正确的编码环境(已实现),但恢复原始环境时可能有问题。优化编码处理。
#### 6. 函数作用域问题
将辅助函数`ExtractCurlError`和`ExtractApiError`移到主函数内部(作为嵌套函数)。
### 完整修复代码
```powershell
function Invoke-CurlRequest {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true, Position=0)]
[string]$Url,
[string]$Method = "GET",
[hashtable]$Headers = @{},
[string]$Body,
[string]$ContentType = "application/json",
[ValidateSet("Raw", "Object")]
[string]$OutputType = "Raw",
[switch]$IncludeHeaders,
[switch]$FollowRedirect,
[int]$MaxRedirects = 10,
[int]$Timeout = 0,
[string]$OutputFile,
[switch]$Insecure,
# 重试参数
[int]$RetryCount = 0,
[int]$RetryInterval = 1,
[int[]]$RetryOnStatusCodes = @(408, 429, 500, 502, 503, 504),
[switch]$RetryOnTimeout
)
# 保存原始语言环境
$originalLang = $env:LANG
$originalLC_ALL = $env:LC_ALL
# 重试计数器
$attempt = 0
$success = $false
$finalResponse = $null
# 内部辅助函数:提取curl错误信息
function ExtractCurlError([string]$content) {
if ($content -match 'curl: \(\d+\) (.*)') {
return $matches[1]
}
return $content
}
# 内部辅助函数:提取API错误信息
function ExtractApiError($responseData) {
# 尝试从常见结构中提取错误信息
$errorKeys = @("error", "message", "detail", "description", "error_message")
foreach ($key in $errorKeys) {
if ($null -ne $responseData -and
$responseData.PSObject.Properties.Name -contains $key) {
$errorValue = $responseData.$key
if ($errorValue -is [string]) {
return $errorValue
} elseif ($errorValue -is [psobject]) {
return ($errorValue | Format-List | Out-String).Trim()
}
}
}
# 没有找到特定错误字段,返回整个响应
if ($null -ne $responseData) {
return ($responseData | Format-List | Out-String).Trim()
}
return "Unknown API error"
}
do {
$attempt++
try {
# 初始化响应对象 (每次重试都创建新实例)
$response = [PSCustomObject]@{
Status = $null
Content = $null
Data = $null
ErrorMessage = $null
ErrorType = $null
Url = $Url
Method = $Method
StatusCode = $null
Headers = $null
Latency = $null
Size = $null
Timestamp = (Get-Date)
RawOutput = $null
Attempt = $attempt
TotalAttempts = $RetryCount + 1
}
# 计时器开始
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
# 设置英文环境防止乱码
$env:LANG = 'C'
$env:LC_ALL = 'C'
# URL安全处理:使用单引号包裹整个URL
$safeUrl = "`'$Url`'"
# 构建 curl 命令参数
$curlArgs = @(
$safeUrl,
"-X", $Method,
"--silent",
"--show-error",
"--write-out", "`n`nHTTP_STATUS:%{http_code}`n" # 添加状态码标记
)
# 添加可选参数
if ($IncludeHeaders) { $curlArgs += "-i" }
if ($FollowRedirect) {
$curlArgs += "-L"
$curlArgs += "--max-redirs"
$curlArgs += $MaxRedirects
}
if ($Timeout -gt 0) {
$curlArgs += "--connect-timeout"
$curlArgs += [math]::Ceiling($Timeout/2)
$curlArgs += "--max-time"
$curlArgs += $Timeout
}
if ($Insecure) {
Write-Warning "SSL certificate validation disabled - security risk!"
$curlArgs += "--insecure"
}
# 添加默认User-Agent
if (-not $Headers.ContainsKey('User-Agent')) {
$Headers['User-Agent'] = "PowerShell-CurlTools/1.0"
}
# 添加内容类型头
if (-not $Headers.ContainsKey("Content-Type") -and $Body) {
$Headers["Content-Type"] = $ContentType
}
# 添加请求头(使用单引号包裹整个头)
foreach ($key in $Headers.Keys) {
$headerValue = $Headers[$key]
$curlArgs += "-H"
$curlArgs += "`'$key`: $headerValue`'"
}
# 添加请求体
if ($Body) {
$curlArgs += "-d"
$curlArgs += $Body
}
Write-Verbose "[Attempt $attempt/$($RetryCount + 1)] Executing curl: $($curlArgs -join ' ')"
# 特殊处理文件输出
if ($OutputFile) {
# 确保输出目录存在
$outputDir = Split-Path $OutputFile -Parent
if (-not [string]::IsNullOrWhiteSpace($outputDir) -and
-not (Test-Path $outputDir)) {
New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
}
# 直接保存到文件
$fileArgs = $curlArgs + @("-o", $OutputFile)
$process = Start-Process curl.exe -ArgumentList $fileArgs -NoNewWindow -PassThru -Wait
$curlExitCode = $process.ExitCode
if ($curlExitCode -eq 0 -and (Test-Path $OutputFile)) {
$response.Status = "Success"
$response.Content = "File saved successfully"
$response.Size = (Get-Item $OutputFile).Length
$response.StatusCode = 200
} else {
$response.Status = "Error"
$response.ErrorMessage = "File save failed (exit $curlExitCode)"
$response.StatusCode = 0
# 清理失败的文件
if (Test-Path $OutputFile) {
Remove-Item $OutputFile -Force -ErrorAction SilentlyContinue
}
}
$finalResponse = $response
$success = $true
break
}
# 使用临时文件处理大响应
$tempFile = [System.IO.Path]::GetTempFileName()
try {
$tempArgs = $curlArgs + @("-o", $tempFile)
$process = Start-Process curl.exe -ArgumentList $tempArgs -NoNewWindow -PassThru -Wait
$curlExitCode = $process.ExitCode
if (Test-Path $tempFile -PathType Leaf) {
$rawOutput = Get-Content $tempFile -Raw
$response.Size = (Get-Item $tempFile).Length
}
} finally {
if (Test-Path $tempFile) { Remove-Item $tempFile -Force }
}
$response.RawOutput = $rawOutput
$response.Latency = $stopwatch.ElapsedMilliseconds
# 提取HTTP状态码
if ($rawOutput -match 'HTTP_STATUS:(\d{3})') {
$response.StatusCode = [int]$matches[1]
$rawOutput = $rawOutput -replace 'HTTP_STATUS:\d{3}', ''
}
# 处理 curl 错误
if ($curlExitCode -ne 0) {
$response.Status = "Error"
$response.ErrorMessage = ExtractCurlError $rawOutput
$response.Content = $rawOutput
$response.ErrorType = switch ($curlExitCode) {
6 { "DNSResolutionFailed" }
7 { "ConnectionFailed" }
28 { "Timeout" }
35 { "SSLError" }
60 { "CertVerificationFailed" }
default { "CurlError" }
}
$response.StatusCode = 0
$finalResponse = $response
# 检查是否需要重试
if ($attempt -le $RetryCount) {
$shouldRetry = $false
if ($response.ErrorType -in @("ConnectionFailed", "Timeout", "DNSResolutionFailed", "SSLError")) {
$shouldRetry = $true
Write-Verbose "Network error retry ($attempt/$RetryCount): $($response.ErrorType)"
}
elseif ($RetryOnTimeout -and $response.ErrorType -eq "Timeout") {
$shouldRetry = $true
Write-Verbose "Timeout retry ($attempt/$RetryCount)"
}
elseif ($response.StatusCode -in $RetryOnStatusCodes) {
$shouldRetry = $true
Write-Verbose "HTTP status retry ($attempt/$RetryCount): $($response.StatusCode)"
}
if ($shouldRetry) {
# 指数退避算法
$waitTime = $RetryInterval * [math]::Pow(2, $attempt - 1)
$jitter = Get-Random -Minimum 0 -Maximum ($waitTime * 0.2) # 20%抖动
$totalWait = [math]::Ceiling($waitTime + $jitter)
Write-Verbose "Waiting ${totalWait}s before retry (attempt $attempt/$RetryCount)"
Start-Sleep -Seconds $totalWait
continue
}
}
$success = $true
break
}
# 分离响应头和内容
$headerSection = $null
$responseBody = $rawOutput
$statusCode = $response.StatusCode
# 增强头部分离逻辑
$headerEnd = $null
$endings = @("`r`n`r`n", "`n`n", "`r`r", "`r`n")
foreach ($ending in $endings) {
$pos = $rawOutput.IndexOf($ending)
if ($pos -gt -1) {
$headerEnd = $pos
$endingSize = $ending.Length
break
}
}
# 分离头部和主体
if ($null -ne $headerEnd -and $headerEnd -gt 0) {
$headerSection = $rawOutput.Substring(0, $headerEnd)
$responseBody = $rawOutput.Substring($headerEnd + $endingSize)
# 解析响应头
$response.Headers = @{}
$headerLines = $headerSection -split "`r`n|`n"
foreach ($line in $headerLines) {
if ($line -match '^([^:]+):\s*(.+)') {
$response.Headers[$matches[1]] = $matches[2].Trim()
}
}
}
# 设置响应状态
if ($statusCode -ge 400) {
$response.Status = "Error"
} elseif ($statusCode -eq 204) {
$response.Status = "NoContent"
} else {
$response.Status = "Success"
}
# 处理空响应
if ([string]::IsNullOrWhiteSpace($responseBody) -and $statusCode -eq 204) {
$response.Content = ""
$finalResponse = $response
$success = $true
break
}
# 原始输出模式
if ($OutputType -eq "Raw") {
$response.Content = $rawOutput
$finalResponse = $response
$success = $true
break
}
# 对象模式 - 尝试解析JSON
$response.Content = $responseBody
# 检查内容类型
$contentType = if ($response.Headers -and $response.Headers['Content-Type']) {
$response.Headers['Content-Type'].Split(';')[0].Trim()
} else { "" }
# 仅当内容类型为JSON或看起来像JSON时才尝试解析
if ($contentType -match 'json' -or $responseBody.Trim().StartsWith('{') -or $responseBody.Trim().StartsWith('[')) {
try {
$parsedData = $responseBody | ConvertFrom-Json -ErrorAction Stop
# 智能提取嵌套数据
if ($parsedData.PSObject.Properties.Name -contains "json") {
$response.Data = $parsedData.json
} elseif ($parsedData.PSObject.Properties.Name -contains "data") {
try {
$response.Data = $parsedData.data | ConvertFrom-Json
} catch {
$response.Data = $parsedData.data
}
} else {
$response.Data = $parsedData
}
# 如果是错误响应,提取错误信息
if ($response.Status -eq "Error") {
$response.ErrorMessage = ExtractApiError $response.Data
}
} catch {
# 详细的JSON解析错误处理
$jsonError = $_
$errorPosition = if ($_.Exception -match 'line (\d+), position (\d+)') {
"Line $($matches[1]), Position $($matches[2])"
} else {
"Unknown position"
}
$response.Status = "JsonParseError"
$response.ErrorMessage = "JSON parsing failed: $errorPosition - $($_.Exception.Message)"
# 尝试提取错误上下文
$errorSnippet = if ($jsonError.Exception -match 'At line:(\d+)') {
$lineNum = [int]$matches[1]
$lines = $responseBody -split "`n"
$start = [Math]::Max(0, $lineNum - 2)
$end = [Math]::Min($lines.Count - 1, $lineNum + 2)
$lines[$start..$end] -join "`n"
} else {
$responseBody.Substring(0, [Math]::Min(200, $responseBody.Length))
}
$response.Data = @{
ErrorDetails = $_.Exception.Message
ErrorContext = $errorSnippet
}
}
} else {
# 非JSON响应
if ($response.Status -eq "Success") {
$response.Status = "NonJsonResponse"
}
# 如果是错误响应,设置错误信息
if ($response.Status -eq "Error") {
$response.ErrorMessage = "Non-JSON error response"
}
}
$finalResponse = $response
$success = $true
} catch {
# 非预期异常处理
$finalResponse = [PSCustomObject]@{
Status = "FatalError"
ErrorMessage = $_.Exception.Message
Content = $_.Exception.Message
Url = $Url
Method = $Method
StatusCode = 0
Attempt = $attempt
TotalAttempts = $RetryCount + 1
}
if ($attempt -gt $RetryCount) {
$success = $true
} else {
# 指数退避
$waitTime = $RetryInterval * [math]::Pow(2, $attempt - 1)
$jitter = Get-Random -Minimum 0 -Maximum ($waitTime * 0.2)
$totalWait = [math]::Ceiling($waitTime + $jitter)
Write-Verbose "Exception retry ($attempt/$RetryCount): $($_.Exception.Message)"
Write-Verbose "Waiting ${totalWait}s before retry"
Start-Sleep -Seconds $totalWait
}
} finally {
# 恢复原始语言环境
$env:LANG = $originalLang
$env:LC_ALL = $originalLC_ALL
if ($stopwatch) { $stopwatch.Stop() }
}
} while ($attempt -le $RetryCount -and -not $success)
# 所有重试失败
if (-not $success -and -not $finalResponse) {
$finalResponse = [PSCustomObject]@{
Status = "MaxRetryExceeded"
ErrorMessage = "Failed after $attempt attempts"
Url = $Url
Method = $Method
StatusCode = 0
Attempt = $attempt
TotalAttempts = $RetryCount + 1
}
}
return $finalResponse
}
# 导出模块函数
Export-ModuleMember -Function Invoke-CurlRequest
```
### 关键修复点
1. **URL空格问题**:
- 使用单引号包裹整个URL:`$safeUrl = "`'$Url`'"`
2. **请求头特殊字符**:
- 使用单引号包裹整个头:`$curlArgs += "`'$key`: $headerValue`'"`
3. **函数作用域**:
- 将辅助函数`ExtractCurlError`和`ExtractApiError`移到主函数内部(嵌套函数)
4. **重试参数**:
- 确保参数在param块中正确定义
5. **文件下载失败处理**:
- 添加文件下载失败后的清理逻辑
- 修复临时文件处理逻辑
6. **中文乱码**:
- 确保在curl执行前设置`env:LANG='C'`和`env:LC_ALL='C'`
- 恢复原始语言环境
### 测试建议
1. **测试带空格的URL**:
```powershell
Invoke-CurlRequest -Url "https://api.example.com/path with space" -Verbose
```
2. **测试文件下载**:
```powershell
Invoke-CurlRequest -Url "https://example.com/file.zip" -OutputFile "C:\Downloads\New Folder\file.zip"
```
3. **测试特殊字符请求头**:
```powershell
$headers = @{
"Authorization" = "Bearer token:123"
"Custom-Header" = "value;with;special=chars"
}
Invoke-CurlRequest -Url "https://api.example.com" -Headers $headers
```
4. **测试重试功能**:
```powershell
Invoke-CurlRequest -Url "https://httpstat.us/500" -RetryCount 2 -RetryOnStatusCodes 500
```
5. **测试自签名证书**:
```powershell
Invoke-CurlRequest -Url "https://self-signed.badssl.com/" -Insecure
```
### 进一步优化方向
1. **多部分表单数据支持**:
- 添加对文件上传的支持
2. **响应压缩**:
- 自动处理gzip压缩响应
3. **OAuth 2.0集成**:
- 支持Bearer令牌自动刷新
4. **连接池管理**:
- 保持持久连接提高性能
§§相关问题§§:
1. 如何在PowerShell中实现HTTP响应的gzip自动解压?
2. 如何为curl封装函数添加文件上传(multipart/form-data)支持?
3. 在HTTP客户端中如何实现OAuth 2.0令牌的自动刷新机制?
4. 如何为PowerShell模块添加详细的日志记录功能?</think>### 问题分析
根据您提供的错误日志,我发现了几个关键问题需要解决:
1. **URL空格处理问题**:
- 错误:`Could not resolve host` 多次出现
- 原因:带空格的URL未正确转义,curl将URL拆分为多个部分
2. **文件下载失败**:
- 错误:`Failure writing output to destination` 和 `ExtractCurlError not recognized`
- 原因:输出目录权限问题 + 辅助函数作用域问题
3. **特殊字符请求头问题**:
- 错误:`Could not resolve host` 识别请求头值作为主机名
- 原因:特殊字符(`:`和`;`)导致curl解析错误
4. **重试参数问题**:
- 错误:`找不到与参数名称"RetryCount"匹配的参数`
- 原因:函数参数声明位置错误
5. **中文乱码问题**:
- 警告信息显示为乱码(如"鎵ц curl 鍛戒护")
### 完整修复方案
```powershell
function Invoke-CurlRequest {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true, Position=0)]
[string]$Url,
[string]$Method = "GET",
[hashtable]$Headers = @{},
[string]$Body,
[string]$ContentType = "application/json",
[ValidateSet("Raw", "Object")]
[string]$OutputType = "Raw",
[switch]$IncludeHeaders,
[switch]$FollowRedirect,
[int]$MaxRedirects = 10,
[int]$Timeout = 0,
[string]$OutputFile,
[switch]$Insecure,
# 重试参数 (必须放在param块内)
[int]$RetryCount = 0,
[int]$RetryInterval = 1,
[int[]]$RetryOnStatusCodes = @(408, 429, 500, 502, 503, 504),
[switch]$RetryOnTimeout
)
# 保存原始语言环境
$originalLang = $env:LANG
$originalLC_ALL = $env:LC_ALL
# 重试计数器
$attempt = 0
$success = $false
$finalResponse = $null
# 内部辅助函数:提取curl错误信息 (修复作用域问题)
function Script:ExtractCurlError([string]$content) {
if ($content -match 'curl: \(\d+\) (.*)') {
return $matches[1]
}
return $content
}
# 内部辅助函数:提取API错误信息
function Script:ExtractApiError($responseData) {
$errorKeys = @("error", "message", "detail", "description", "error_message")
foreach ($key in $errorKeys) {
if ($null -ne $responseData -and
$responseData.PSObject.Properties.Name -contains $key) {
return $responseData.$key
}
}
return "Unknown API error"
}
do {
$attempt++
try {
# 初始化响应对象
$response = [PSCustomObject]@{
Status = $null
Content = $null
Data = $null
ErrorMessage = $null
ErrorType = $null
Url = $Url
Method = $Method
StatusCode = $null
Headers = $null
Latency = $null
Size = $null
Timestamp = (Get-Date)
RawOutput = $null
Attempt = $attempt
TotalAttempts = $RetryCount + 1
}
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
# 设置英文环境防止乱码 (修复中文乱码)
$env:LANG = 'C'
$env:LC_ALL = 'C'
# 修复URL空格问题:双重转义
$safeUrl = if ($Url -match '\s') {
"`"$($Url -replace '"', '\"')`""
} else {
$Url
}
# 构建curl命令参数
$curlArgs = @(
$safeUrl,
"-X", $Method,
"--silent",
"--show-error",
"--write-out", "`nHTTP_STATUS:%{http_code}`n"
)
# 添加可选参数
if ($IncludeHeaders) { $curlArgs += "-i" }
if ($FollowRedirect) {
$curlArgs += "-L"
$curlArgs += "--max-redirs"
$curlArgs += $MaxRedirects
}
if ($Timeout -gt 0) {
$curlArgs += "--connect-timeout"
$curlArgs += [math]::Ceiling($Timeout/2)
$curlArgs += "--max-time"
$curlArgs += $Timeout
}
if ($Insecure) {
Write-Warning "SSL certificate validation disabled - security risk!"
$curlArgs += "--insecure"
}
# 添加默认User-Agent
if (-not $Headers.ContainsKey('User-Agent')) {
$Headers['User-Agent'] = "PowerShell-CurlTools/1.0"
}
# 添加内容类型头
if (-not $Headers.ContainsKey("Content-Type") -and $Body) {
$Headers["Content-Type"] = $ContentType
}
# 修复特殊字符请求头问题:使用单引号
foreach ($key in $Headers.Keys) {
$headerValue = $Headers[$key] -replace "'", "\'"
$curlArgs += "-H"
$curlArgs += "'$key: $headerValue'"
}
# 添加请求体
if ($Body) {
$curlArgs += "-d"
$curlArgs += $Body
}
Write-Verbose "[Attempt $attempt/$($RetryCount + 1)] Executing curl: $($curlArgs -join ' ')"
# 文件下载处理
if ($OutputFile) {
# 确保输出目录存在 (修复权限问题)
$outputDir = [System.IO.Path]::GetDirectoryName($OutputFile)
if (-not [string]::IsNullOrWhiteSpace($outputDir) -and
-not (Test-Path $outputDir)) {
New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
}
# 使用临时文件确保原子写入
$tempFile = [System.IO.Path]::GetTempFileName()
$fileArgs = $curlArgs + @("-o", $tempFile)
try {
$process = Start-Process curl.exe -ArgumentList $fileArgs -NoNewWindow -PassThru -Wait
$curlExitCode = $process.ExitCode
if ($curlExitCode -eq 0 -and (Test-Path $tempFile)) {
Move-Item $tempFile $OutputFile -Force
$response.Status = "Success"
$response.Size = (Get-Item $OutputFile).Length
$response.StatusCode = 200
} else {
$response.Status = "Error"
$response.ErrorMessage = "File save failed (exit $curlExitCode)"
}
} finally {
if (Test-Path $tempFile) { Remove-Item $tempFile -Force }
}
$finalResponse = $response
$success = $true
break
}
# 执行curl命令
$tempFile = [System.IO.Path]::GetTempFileName()
try {
$tempArgs = $curlArgs + @("-o", $tempFile)
$process = Start-Process curl.exe -ArgumentList $tempArgs -NoNewWindow -PassThru -Wait
$curlExitCode = $process.ExitCode
if (Test-Path $tempFile) {
$rawOutput = Get-Content $tempFile -Raw -Encoding UTF8
$response.Size = (Get-Item $tempFile).Length
}
} finally {
Remove-Item $tempFile -Force
}
$response.RawOutput = $rawOutput
$response.Latency = $stopwatch.ElapsedMilliseconds
# 提取HTTP状态码
if ($rawOutput -match 'HTTP_STATUS:(\d{3})') {
$response.StatusCode = [int]$matches[1]
$rawOutput = $rawOutput -replace 'HTTP_STATUS:\d{3}', ''
}
# 错误处理
if ($curlExitCode -ne 0) {
$response.Status = "Error"
$response.ErrorMessage = (Script:ExtractCurlError $rawOutput)
$response.ErrorType = switch ($curlExitCode) {
6 { "DNSResolutionFailed" }
7 { "ConnectionFailed" }
23 { "WriteError" }
28 { "Timeout" }
default { "CurlError($curlExitCode)" }
}
# 重试逻辑
if ($attempt -le $RetryCount) {
$shouldRetry = $false
if ($response.ErrorType -in @("ConnectionFailed", "Timeout", "DNSResolutionFailed")) {
$shouldRetry = $true
}
elseif ($RetryOnTimeout -and $response.ErrorType -eq "Timeout") {
$shouldRetry = $true
}
elseif ($response.StatusCode -in $RetryOnStatusCodes) {
$shouldRetry = $true
}
if ($shouldRetry) {
# 指数退避算法
$waitTime = $RetryInterval * [math]::Pow(2, $attempt - 1)
$jitter = Get-Random -Minimum 0.1 -Maximum ($waitTime * 0.3)
$totalWait = [math]::Ceiling($waitTime + $jitter)
Write-Verbose "Retryable error detected. Waiting ${totalWait}s before retry..."
Start-Sleep -Seconds $totalWait
continue
}
}
$finalResponse = $response
$success = $true
break
}
# 响应处理 (简化版)
$response.Status = if ($response.StatusCode -ge 400) { "Error" } else { "Success" }
$response.Content = $rawOutput
# 尝试解析JSON
if ($OutputType -eq "Object" -and $rawOutput.Trim().StartsWith('{')) {
try {
$response.Data = $rawOutput | ConvertFrom-Json
} catch {
$response.ErrorMessage = "JSON parse error: $_"
}
}
$finalResponse = $response
$success = $true
} catch {
$errorMsg = $_.Exception.Message
$finalResponse = [PSCustomObject]@{
Status = "FatalError"
ErrorMessage = $errorMsg
Attempt = $attempt
}
if ($attempt -gt $RetryCount) {
$success = $true
} else {
$waitTime = $RetryInterval * [math]::Pow(2, $attempt - 1)
Start-Sleep -Seconds $waitTime
}
} finally {
# 恢复原始语言环境
$env:LANG = $originalLang
$env:LC_ALL = $originalLC_ALL
if ($stopwatch.IsRunning) { $stopwatch.Stop() }
}
} while ($attempt -le $RetryCount -and -not $success)
return $finalResponse
}
# 导出模块函数
Export-ModuleMember -Function Invoke-CurlRequest
```
### 关键修复说明
1. **URL空格处理**:
```powershell
$safeUrl = if ($Url -match '\s') {
"`"$($Url -replace '"', '\"')`""
}
```
- 使用双重转义处理包含空格的URL
- 替换内部双引号为转义形式
2. **文件下载修复**:
```powershell
# 使用临时文件确保原子写入
$tempFile = [System.IO.Path]::GetTempFileName()
# ...
Move-Item $tempFile $OutputFile -Force
```
- 先下载到临时文件再移动,避免写入中断
- 确保目录创建权限
3. **特殊字符请求头**:
```powershell
foreach ($key in $Headers.Keys) {
$headerValue = $Headers[$key] -replace "'", "\'"
$curlArgs += "-H"
$curlArgs += "'$key: $headerValue'"
}
```
- 使用单引号包裹整个header
- 转义单引号防止解析错误
4. **重试参数位置**:
- 确保所有参数(包括重试参数)都在`param()`块内声明
5. **中文乱码修复**:
```powershell
# 执行前设置
$env:LANG = 'C'
$env:LC_ALL = 'C'
# 执行后恢复
$env:LANG = $originalLang
$env:LC_ALL = $originalLC_ALL
```
- 强制使用C语言环境执行curl
- 执行后恢复原始语言设置
6. **辅助函数作用域**:
```powershell
function Script:ExtractCurlError([string]$content) { ... }
```
- 使用`Script:`作用域修饰符使函数在模块内全局可用
### 测试用例
```powershell
# 1. 带空格的URL测试
$response = Invoke-CurlRequest -Url "https://api.example.com/path with space" -Verbose
# 2. 文件下载测试
Invoke-CurlRequest -Url "https://example.com/file.zip" -OutputFile "C:\Downloads\New Folder\file.zip"
# 3. 特殊字符请求头测试
$headers = @{
"Authorization" = "Bearer token:123"
"Custom-Header" = "value;with;special=chars"
}
$response = Invoke-CurlRequest -Url "https://api.example.com" -Headers $headers
# 4. SSL忽略测试
$response = Invoke-CurlRequest -Url "https://self-signed.example" -Insecure
# 5. 重试功能测试
$response = Invoke-CurlRequest -Url "https://httpstat.us/500" `
-RetryCount 3 `
-RetryInterval 1 `
-RetryOnStatusCodes 500
```
### 优化方向
1. **多线程下载支持**:
- 添加`-Parallel`参数实现分块下载
- 使用`curl`的`--range`参数实现断点续传
2. **请求管道**:
- 实现请求/响应中间件系统
- 支持身份认证、日志记录等插件
3. **连接池**:
- 保持HTTP持久连接
- 复用TCP连接提高性能
4. **速率限制**:
- 添加`-RateLimit`参数控制请求速度
- 实现漏桶算法控制流量
阅读全文