🌊

Invoke-WebRequest で 4xx を受け取ったときに投げられる例外とその対応

2024/02/25に公開

TL;DR

  • Invoke-WebRequest で 4xx などの応答を受け取ると例外を投げる
  • PowerShell 7 であれば -SkipHttpErrorCheck パラメーターを使うことでその応答を受け取っても例外を投げないようにできる
  • PowerShell 5.1 だと例外を処理するしかない

はじめに

PowerShell には Invoke-WebRequest という cmdlet があり、curl のように HTTP リクエストを送ることができます。

まず、以下のような HTTP request を送ってみます。

try {
  $response = Invoke-WebRequest -Uri "https://learn.microsoft.com/en-us/azure/expressroute/expressroute-faqs"
  $response.StatusCode
} catch {
  $_.exception
}

正しい URL へのアクセスであれば、$response にその応答が格納され、200 (HTTP status code で OK の意味) が出力されます。

> try {
>>   $response = Invoke-WebRequest -Uri "https://learn.microsoft.com/en-us/azure/expressroute/expressroute-faqs"
>>   $response.StatusCode
>> } catch {
>>   $_.exception
>> }
200

次に、少し URL を変えて、存在しないページにアクセスしてみます。
少しわかりづらいですが、expressroute-faqs から expressroute-faq に変えています。

try {
  $response = Invoke-WebRequest -Uri "https://learn.microsoft.com/en-us/azure/expressroute/expressroute-faq"
  $response.StatusCode
} catch {
  $_.exception
}

するとなんということでしょう、例外が投げられます。

> try {
>>   $response = Invoke-WebRequest -Uri "https://learn.microsoft.com/en-us/azure/expressroute/expressroute-faq"
>>   $response.StatusCode
>> } catch {
>>   $_.exception
>> }

Response         : StatusCode: 404, ReasonPhrase: 'Not Found', Version: 1.1, Content: System.Net.Http.HttpConnectionResponseContent, Headers:
                   {
                     Request-Context: appId=cid-v1:b1c5b6ea-7ff0-41d3-9862-84c5e1dc3be7
                     X-Datacenter: wus
                     X-Frame-Options: SAMEORIGIN
                     X-Content-Type-Options: nosniff
                     X-UA-Compatible: IE=edge
                     X-XSS-Protection: 1; mode=block
                     X-Rendering-Stack: Dynamic
                     nel: {"report_to":"network-errors","max_age":604800,"success_fraction":0.01,"failure_fraction":1.0}
                     report-to: {"group":"network-errors","max_age":604800,"endpoints":[{"url":"https://mdec.nelreports.net/api/report?cat=mdocs"}]}
                     X-Azure-Ref: 0ohTbZQAAAADdBV7XAaZaSp4kbLw3grWOVFlPMDFFREdFMjMxOAA3MTY4OTIwZS05ZjViLTRhNjItYjE2ZS1kNWJlNjNjZTYxZTc=
                     Cache-Control: public, max-age=600
                     Date: Sun, 25 Feb 2024 10:21:22 GMT
                     Connection: keep-alive
                     Akamai-Cache-Status: Miss from child, Miss from parent
                     Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
                     Content-Type: text/html
                     Content-Length: 26839
                     Expires: Sun, 25 Feb 2024 10:31:22 GMT
                   }
HttpRequestError : Unknown
StatusCode       : NotFound
TargetSite       : Void ThrowTerminatingError(System.Management.Automation.ErrorRecord)
Message          : Response status code does not indicate success: 404 (Not Found).
Data             : {}
InnerException   :
HelpLink         :
Source           : System.Management.Automation
HResult          : -2146233088
StackTrace       :    at System.Management.Automation.MshCommandRuntime.ThrowTerminatingError(ErrorRecord errorRecord)

4xx status code の意味とその対応

404 (Not Found) の応答が返ってきていること、それはサーバーからは正常に応答を受け取っており、その内容として「そのリソースは存在しない」ということを示しています。
ということは、この程度で例外を投げてほしいわけではなく、その応答を受け取って処理を続けたい、ということがあります。
今回、例として 404 (Not Found) を対象としていますが、429 (Too Many Requests) の場合にはその応答に含まれた時間だけ待ってからリトライする、ということはよくあります。

どうするか

Invoke-WebRequest には -SkipHttpErrorCheck というパラメーターがあり、これを使うことで 4xx などの応答を受け取っても例外を投げないようにできます。

> try {
>>   $response = Invoke-WebRequest -Uri "https://learn.microsoft.com/en-us/azure/expressroute/expressroute-faq" -SkipHttpErrorCheck
>>   $response.StatusCode
>> } catch {
>>   $_.exception
>> }
404

これにより、404 であればそのリソースがないと判断して別の処理を続けたり、429 であればどこかのヘッダーに示された時間だけ待ってからリトライする、といったことができます。

PowerShell 5.1 だとどうするか

例外を処理するしかないので、こんな感じになります。
status code は $_.Exception.Response.StatusCode.Value__ というちょっと深いところに格納されています。

$maxRetries = 10
$retryCount = 0

while ($retryCount -lt $maxRetries) {
    try {
        $response = Invoke-WebRequest -Uri $url -Method Post -Headers $headers -Body $body
        break
    } catch {
        # if return code is 429, wait for 10 seconds and retry
        if ($_.Exception.Response.StatusCode.Value__ -eq 429) {
            Start-Sleep -Seconds 10
            $retryCount++
        } else {
            throw $_
        }
    }
}

まとめ

既定では、Invoke-WebRequest は 4xx などの応答を受け取ると例外を投げます。
そこで、-SkipHttpErrorCheck パラメーターを使うことでその応答を受け取っても例外を投げないようにできます。

参考

  • Invoke-WebRequest

https://learn.microsoft.com/powershell/module/microsoft.powershell.utility/invoke-webrequest#-skiphttperrorcheck?wt.mc_id=MVP_391314

  • api - Invoke-Restmethod: how do I get the return code? - Stack Overflow
    value__ は typo じゃないよ、って書かれてます、普通にそう思っちゃいますよね

https://stackoverflow.com/questions/38622526/invoke-restmethod-how-do-i-get-the-return-code

Discussion