ROBOT PAYMENT Engineers Blog

株式会社ROBOT PAYMENTのテックブログです

Invoke-WebRequestでエラーレスポンスを取得

こんにちは。決済システムでエンジニアをやっております hoshino33 です。 今回はPowerShellのInvoke-WebRequestを実行した際に2XX系以外でHTTPステータスコードが返ってきた際にレスポンスボディが取れなかったので、その際に対応した内容になります。

はじめに

Invoke-WebRequestを使用しつつ、curlでもどのようになるか比較しながら見てみようと思います。 Invoke-WebRequestとcurlは以下のバージョンで確認を行っています。

> $PSVersionTable
Name                           Value
----                           -----
PSVersion                      5.1.19041.1682
PSEdition                      Desktop
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
BuildVersion                   10.0.19041.1682
CLRVersion                     4.0.30319.42000
WSManStackVersion              3.0
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
> curl -V
curl 7.58.0 (x86_64-pc-linux-gnu) libcurl/7.58.0 OpenSSL/1.1.1 zlib/1.2.11 libidn2/2.0.4 libpsl/0.19.1 (+libidn2/2.0.4) nghttp2/1.30.0 librtmp/2.3
Release-Date: 2018-01-24
Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtmp rtsp smb smbs smtp smtps telnet tftp
Features: AsynchDNS IDN IPv6 Largefile GSS-API Kerberos SPNEGO NTLM NTLM_WB SSL libz TLS-SRP HTTP2 UnixSockets HTTPS-proxy PSL

サンプルで使用しているAPIは以下を使用させいただいております。 - https://httpbin.org/#/Status_codes - HTTPステータスコードを確認のため使用 - http://developer.hatena.ne.jp/ja/documents/blog/apis/atom - エラー時のレスポンスを確認のため使用(例で使用しているはてなIDとブログIDはダミーです)

2XX系のレスポンス(レスポンスボディなし)

それではHTTPステータスコードに200が返されるパターンを試してみます。

  • Invoke-WebRequest
> Invoke-WebRequest https://httpbin.org/status/200
StatusCode        : 200
StatusDescription : OK
Content           :
RawContent        : HTTP/1.1 200 OK
                    Connection: keep-alive
                    Access-Control-Allow-Origin: *
                    Access-Control-Allow-Credentials: true
                    Content-Length: 0
                    Content-Type: text/html; charset=utf-8
                    Date: Tue, 07 Jun 2022 05:53...
Forms             : {}
Headers           : {[Connection, keep-alive], [Access-Control-Allow-Origin, *], [Access-Control-Allow-Credentials, true], [Content-Length, 0]...}
Images            : {}
InputFields       : {}
Links             : {}
ParsedHtml        : mshtml.HTMLDocumentClass
RawContentLength  : 0
  • curl
> curl -i https://httpbin.org/status/200
HTTP/2 200
date: Tue, 07 Jun 2022 05:55:17 GMT
content-type: text/html; charset=utf-8
content-length: 0
server: gunicorn/19.9.0
access-control-allow-origin: *
access-control-allow-credentials: true

どちらも想定通りに返ってきているのがわかります。

2XX系以外のレスポンス(レスポンスボディなし)

次はHTTPステータスコードに400が返されるパターンを試してみます。

  • Invoke-WebRequest
> Invoke-WebRequest https://httpbin.org/status/400
Invoke-WebRequest : リモート サーバーがエラーを返しました: (400) 要求が不適切です
発生場所 行:1 文字:1
+Invoke-WebRequest https://httpbin.org/status/400
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    +CategoryInfo          : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest]、WebException
    +FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand
  • curl
> curl -i https://httpbin.org/status/400
HTTP/2 400
date: Tue, 07 Jun 2022 06:03:41 GMT
content-type: text/html; charset=utf-8
content-length: 0
server: gunicorn/19.9.0
access-control-allow-origin: *
access-control-allow-credentials: true

Invoke-WebRequestでは2XX系以外なので例外が発生してしまいます。 内容をみるとHTTPステータスコードが400で返ってきているのはわかりますが例外が気になる方は以下のようにすると良さそうです。

> try {
>     Invoke-WebRequest https://httpbin.org/status/400
> } catch {
>     $_.Exception.Response
> }
IsMutuallyAuthenticated : False
Cookies                 : {}
Headers                 : {Connection, Access-Control-Allow-Origin, Access-Control-Allow-Credentials, Content-Length...}
SupportsHeaders         : True
ContentLength           : 0
ContentEncoding         :
ContentType             : text/html; charset=utf-8
CharacterSet            : utf-8
Server                  : gunicorn/19.9.0
LastModified            : 2022/06/07 15:12:41
StatusCode              : BadRequest
StatusDescription       : BAD REQUEST
ProtocolVersion         : 1.1
ResponseUri             : https://httpbin.org/status/400
Method                  : GET
IsFromCache             : False

2XX系以外のレスポンス(レスポンスボディあり)

今回の本題です。 認証を行っていないので、HTTPステータスコードが401で何かしらのレスポンスボディが返ってくる想定です。

  • Invoke-WebRequest
> try {
>     Invoke-WebRequest https://blog.hatena.ne.jp/hoge/hoge.hatenablog.com/atom/entry
> } catch {
>     $_.Exception.Response
> }
IsMutuallyAuthenticated : False
Cookies                 : {b=$1$Jshjbbb9$vRpgU3FHO9mvRwbF8u9ln0, ek=, sk=198fc3de6792854527b3b3f164bd8ce4b90ad14d}
Headers                 : {Transfer-Encoding, Connection, Vary, Cache-Control...}
SupportsHeaders         : True
ContentLength           : -1
ContentEncoding         :
ContentType             : text/html; charset=utf-8
CharacterSet            : utf-8
Server                  : nginx
LastModified            : 2022/06/07 15:35:52
StatusCode              : Unauthorized
StatusDescription       : Unauthorized
ProtocolVersion         : 1.1
ResponseUri             : https://blog.hatena.ne.jp/hoge/hoge.hatenablog.com/atom/entry
Method                  : GET
IsFromCache             : False
  • curl
> curl -i https://blog.hatena.ne.jp/hoge/hoge.hatenablog.com/atom/entry
HTTP/1.1 401 Unauthorized
Server: nginx
Date: Tue, 07 Jun 2022 06:32:13 GMT
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Set-Cookie: b=$1$X0LE8DWV$K/v8SszSGiZ2iTFqi5iMd/; expires=Mon, 02 Jun 2042 06:32:13 GMT; domain=hatena.ne.jp; path=/
Cache-Control: private
Vary: Accept-Language,Cookie,Accept-Encoding
WWW-Authenticate: Basic realm="Hatena Blog"
WWW-Authenticate: WSSE profile="UsernameToken"
WWW-Authenticate: OAuth realm="Hatena Blog"
Content-Security-Policy-Report-Only: block-all-mixed-content; report-uri https://blog.hatena.ne.jp/api/csp_report
P3P: CP="OTI CUR OUR BUS STA"
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-Revision: 1fab4f40deb38548e9beab265c9917
X-XSS-Protection: 1
Set-Cookie: ek=; path=/; expires=Tue, 07-Jun-2022 05:32:13 GMT
Set-Cookie: sk=a226416ed0f178838a50818cd5b73e44f651c928; path=/
X-Runtime: 0.007296
X-Proxy-Revision: 453c0d2
<p class="error-box">Authorization required</p>

Invoke-WebRequestではHTTPステータスコード(Unauthorized = 401)はわかりますが、レスポンスボディがありません。 curlでの<p class="error-box">Authorization required</p> の部分。

2XX系以外のレスポンス(レスポンスボディあり)を対応してみる

レスポンスボディを取得するためにはResponseStreamから読み込む必要があるようです。 そのため以下を実行してみます。 ※HTTPステータスコードとレスポンスボディが欲しいため出力時は一部加工しています。

> try {
>     Invoke-WebRequest https://blog.hatena.ne.jp/hoge/hoge.hatenablog.com/atom/entry
> } catch {
>     $ExceptionResponse = $_.Exception.Response
>     $Stream = $ExceptionResponse.GetResponseStream()
>     $Stream.Position = 0
>     $Reader = [System.IO.StreamReader]::new($Stream)
>     $ErrResponse = $Reader.ReadToEnd()
>     "StatusCode : {0}, Response : {1}" -f $ExceptionResponse.StatusCode.Value__ ,$ErrResponse
> }
StatusCode : 401, Response : <p class="error-box">Authorization required</p>

SkipHttpErrorCheckについて

5系ではなかったのですが、7系からはSkipHttpErrorCheckオプションが追加され、 以下のようにするとエラーレスポンスまで取得できるようです。

> $PSVersionTable
Name                           Value
----                           -----
PSVersion                      7.2.4
PSEdition                      Core
GitCommitId                    7.2.4
OS                             Microsoft Windows 10.0.19043
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0
> Invoke-WebRequest -SkipHttpErrorCheck https://blog.hatena.ne.jp/hoge/hoge.hatenablog.com/atom/entry
StatusCode        : 401
StatusDescription : Unauthorized
Content           : <p class="error-box">Authorization required</p>
RawContent        : HTTP/1.1 401 Unauthorized
                    Server: nginx
                    Date: Tue, 14 Jun 2022 08:13:52 GMT
                    Transfer-Encoding: chunked
                    Connection: keep-alive
                    Set-Cookie: b=$1$0x52Lrkc$8Bj6eU1PPtlJ8KbVxEJel1; expires=Mon, 09 Jun…
Headers           : {[Server, System.String[]], [Date, System.String[]], [Transfer-Encoding, System.String[]], [Connection, System.String[]]…}
Images            : {}
InputFields       : {}
Links             : {}
RawContentLength  : 48
RelationLink      : {}

まとめ

7系以前のInvoke-WebRequestを使用する場合は一手間必要でしたが、7系からはSkipHttpErrorCheckオプションを指定することにより簡単に取得できるようになったようです。 また、なんらかの理由で7系に上げられない場合は、WSLをサクッとインストールしてcurlを使用した方が楽そうです。