【VBA】GASへのPOSTが405になる原因と通信オブジェクト比較

GASへのPOSTが405になる原因と通信オブジェクト比較(VBAのコード画面風のアイキャッチ) VBA

VBAからGoogle Apps Script(GAS)のWebアプリを呼ぶと、GETは通るのにPOSTだけ405になることがあります。私も、業務で使っているGASへのPOSTで、通信オブジェクトを替えるたびに結果が変わり、原因を追うのに苦労しました。

当時は、「POSTはMSXML2.XMLHTTP、GETはWinHttpが安定する」と整理していました。ただ、その根拠は確かめられる形で残していませんでした。そこで、同じGASに3種類の通信オブジェクトでGETとPOSTを5回ずつ送り、結果を実測しました。

想定読者

  • VBAからGAS Webアプリへ送受信している人
  • POSTで405やアクセス拒否が出て、原因が分からない人

用語の確認:405・302・307

本文で使うHTTPの用語を、先に短く整理します。

  • GET・POST:GETはデータを取得する要求、POSTはデータを送る要求です
  • 200:成功です
  • 405(Method Not Allowed):そのURLは、送ったメソッド(GETやPOSTなど)に対応していません。レスポンスのAllowヘッダに、使えるメソッドが入ります
  • 302(Found):このURLは一時的に移動している、という応答です。移動先のURLはLocationヘッダに入り、通信側が自動で追いかけます(リダイレクト)
  • 307(Temporary Redirect):302と同じ一時的な移動ですが、メソッドを変えずに移動先へ送り直すよう求めます

302のあと、POSTをGETに変えて送り直すことは、仕様上許されています。変えるかどうかは、通信するオブジェクトによって違いました(後述)。詳しくは、302 Found(MDN)を参照してください。

先に結論

今回の環境では、WinHttp.WinHttpRequest.5.1だけがGET・POSTとも通りました。使い分けは不要でした。

オブジェクトGETPOST
MSXML2.XMLHTTP失敗(アクセス拒否)失敗(アクセス拒否)
MSXML2.ServerXMLHTTP200405
WinHttp.WinHttpRequest.5.1200200

各5回とも同じ結果でした。XMLHTTPの失敗は、エラー番号-2147024891(アクセス拒否)です。検証環境はExcel 16.0(64bit Office)、Windows(64bit)、コードページ932です。別のPCやMac版、32bit Officeでは確かめていません。

ServerXMLHTTPで405になる原因は、次の2つの組み合わせでした。GASが302でGET専用のURLへ飛ばすことと、ServerXMLHTTPが302のあともPOSTを送り直すことです。

検証の方法

  • 検証用に、受け取った内容を返すだけのGASを用意しました
  • GETは?q=abcを付けて、POSTは小さなJSONを送りました
  • 判定は、ステータスと、GASが返したメソッド名・受信文字数で行いました
  • 302のあとの動きは、自分のPCで立てた検証サーバーでも確かめました(通信先はこのPC内だけ)
  • 通信先のURLは、記事にもコードにも載せていません
  • GASは、doGet・doPostが何回動いたかを数える形に差し替え、実行回数も確かめました。XMLHTTPの切り分けには、自分のサイトへのhttps通信も使いました(各5回)。

405になる理由

WinHttpで自動追従を切ってGASのWebアプリにPOSTすると、5回とも302が返りました。Locationのホストはscript.googleusercontent.comでした。

このLocationに対して、POSTすると、XMLHTTP・ServerXMLHTTPのどちらでも、WinHttpで手動で再POSTしても、405になりました。GETすると200で、POSTの結果が返りました。405のレスポンスのAllowヘッダはHEAD, GETでした。Location側はGETしか受け付けません。

では、なぜオブジェクトによって結果が分かれるのか。302のあとの動きの違いを、自分のPCで立てた検証サーバーで確かめました。POSTを受けると302で別のURLへ飛ばし、飛び先はGETなら200、POSTなら405を返すサーバーです。

オブジェクト302のあとに飛び先へ送ったメソッド結果
MSXML2.XMLHTTPGET200
MSXML2.ServerXMLHTTPPOST405
WinHttp.WinHttpRequest.5.1GET200

各3回とも同じ結果でした。同じホスト名への302でも、別のホスト名への302でも同じでした。メソッドを変えない転送(307)では、3つともPOSTのまま送られて405になりました。GETは、302のあともGETのままで200でした。

ServerXMLHTTPは、302のあともPOSTを送り直します。そのため、GET専用のLocationで405になります。XMLHTTPとWinHttpはGETに切り替えるので、200になります。GASで確認した結果(ServerXMLHTTPのPOSTは405、WinHttpの手動再POSTも405)は、この動きと合います。GAS側では、doPostとdoGetの実行回数で確かめました。ServerXMLHTTPのPOSTが405になったときも、doPostは1回のままで、doGetは動きませんでした。追従を切ったPOSTのLocationをGETしても、doGetは動かず、doPostも1回のままでした。405を返したLocationの先では、GASのコードは動いていません。ServerXMLHTTPがPOSTを送り直すことは、検証サーバーで確かめた結果です。

XMLHTTPは、GASの元のURLでは失敗しました。GAS側では、失敗したPOSTでもdoPostが、失敗したGETでもdoGetが1回ずつ動いていて、リクエストはGASまで届いています。そのうえでSendがエラー-2147024891(アクセス拒否)になり、Statusは0でした。送り先を変えて、XMLHTTPで試した結果は次のとおりです。

送り先結果
自分のサイトのhttps(リダイレクトなし)200(5回)
自分のサイトのhttps→httpsの301(同じホスト)200(5回)
自分のサイトのhttp→httpsの301失敗(アクセス拒否)(5回)
GASのLocationへ直接GET(httpsの別ホスト、リダイレクトなし)200(3回)
検証サーバーのhttpの302(同じホスト・別のホスト)200(各3回)
GASの元URL(GET・POST)失敗(アクセス拒否)(各5回)

リダイレクトのない通信は、httpsでも別のホストでも通りました。失敗したのは、GASの元URLと、httpからhttpsへの301でした。XMLHTTPのProgIDをMSXML2.XMLHTTP.6.0・.3.0・Microsoft.XMLHTTPに替えても、GASの元URLは失敗し、自分のサイトのhttps GETは200で、同じでした。

使い分けの目安

GAS Webアプリが相手なら、WinHttpにそろえます。

  • GET:WinHttp(ServerXMLHTTPも通りました)
  • POST:WinHttp

ServerXMLHTTPは、302のあともPOSTを送り直すため、飛び先がGET専用だとPOSTは通りません。XMLHTTPは、今回の環境ではGET・POSTとも通りませんでした(条件別の結果は、405になる理由の節のとおりです)。WinHttpの仕様は、WinHttpRequest object(Microsoft Learn)を参照してください。

よくある失敗

1. ステータスだけを見て、オブジェクトを替える

ServerXMLHTTPの405は、AllowがHEAD, GETでした。ヘッダーを読むと、「POSTを受け付けないURLに送っている」と分かります。

2. Locationへ手動で再POSTする

自動追従を切って、302のLocationへPOSTし直すと、5回とも405でした。POSTの結果が欲しいときは、自動追従に任せます。

3. 日本語をURLにそのまま入れる

WinHttpとServerXMLHTTPでは、未エンコードの「テスト」を送っても200でしたが、GASには9文字(UTF-8のバイト値)で届きました。UTF-8でパーセントエンコードすると、3文字で正しく届きました。XMLHTTPは400でした。エンコードの組み立て方は、【VBA】ScriptControlが64bit Officeで使えない時の代替コードを参照してください。

JSONの本文は、UTF-8のバイト列にして送れば、日本語の「テスト」は3文字で届きました。Content-Typeにcharsetを付けた場合も、付けない場合も同じでした。

応用例:GAS宛の送受信関数

動作を確認した通信部分を、関数にまとめます。

Option Explicit

Public Function GasPost(ByVal url As String, ByVal json As String, _
                        ByRef status As Long) As String
    Dim http As Object
    Set http = CreateObject("WinHttp.WinHttpRequest.5.1")
    http.SetTimeouts 5000, 5000, 30000, 30000
    http.Open "POST", url, False
    http.SetRequestHeader "Content-Type", "application/json"
    http.Send json
    status = http.Status
    GasPost = http.ResponseText
End Function

Public Function GasGet(ByVal url As String, ByRef status As Long) As String
    Dim http As Object
    Set http = CreateObject("WinHttp.WinHttpRequest.5.1")
    http.SetTimeouts 5000, 5000, 30000, 30000
    http.Open "GET", url, False
    http.Send
    status = http.Status
    GasGet = http.ResponseText
End Function

同じGASへ、WinHttpのGET・POSTと、ServerXMLHTTPのPOSTを1回ずつ送った結果です。

イミディエイトウィンドウの出力。WinHttpはGET・POSTとも200、ServerXMLHTTPのPOSTは405でAllowがHEAD, GETと表示されている
再現用の最小コードの実行結果

受け側のGASがエラーをJSONで返す作りなら、ステータスが200でも応答にエラーが入ります。ステータスだけでなく、本文も確認します。

関連する別の現象

POSTのStatusが0になる場合は、405とは別の現象です。3つの通信オブジェクトとサイズ別の結果は、【VBA】GAS宛POSTのStatus 0をサイズ別に実測で扱っています。

受け側のGASがシートへ書き込む方式の違いは、【VBA】GASへの書き込み方の違い|全消しと追記のみを実測で扱っています。

まとめ

  • GAS宛なら、WinHttpでGET・POSTとも通りました
  • ServerXMLHTTPは、302のあともPOSTを送り直すため、GET専用の飛び先で405になりました
  • XMLHTTPは、GASの元URLでは失敗しました。リダイレクトのないhttps通信は通りました
  • 405はAllowヘッダを見ると原因を絞れます
  • 日本語はUTF-8でエンコードしてから送ります

コメント

タイトルとURLをコピーしました