何がおきたのか
仕事でVBAからWeb APIに向けてデータを投げ込む必要があったのでWinHTTPRequestを使った実装をやっていたのですが、テストコードだと動くのに本番コードに落としていくとなぜかAPIからBad Requestで蹴られてしまうという事象に悩まされました。
いろいろ試行錯誤しているうちに、どうやらWinHTTPRequestのSendメソッドに渡すオブジェクトの種類により挙動が違うことに気が付きました。
再現するためのテスト準備
ポンコツコードではありますが、投げ込まれているデータを正確に把握するためのCGIを即席で書きます。
#!/bin/sh
echo "Content-Type: text/plain"
echo
/usr/bin/od -t x1c
このCGIにPOSTデータを投げると、単純にodの結果を出してくれます。読みやすさを考えてオプションは次のようにしました。
- x1 ... 16進1バイトごとに出力
- c ... 対応するASCIIコード (表示不可はエスケープ表示)
たとえば、こんな感じになります。
% curl -X POST -d "This is a test." localhost/~yaizawa/posttest.cgi
0000000 54 68 69 73 20 69 73 20 61 20 74 65 73 74 2e
T h i s i s a t e s t .
0000017
実験1: Stringオブジェクトで渡す
こんなテストコードを実行してみます (要参照設定「Microsoft WinHTTP Services, version 5.1」)。
Sub test1()
Dim whr As WinHttpRequest
Dim poststr As String
Set whr = New WinHttpRequest
poststr = "test"
whr.Open "POST", "http://localhost/~yaizawa/posttest.cgi", False
whr.Send poststr
If whr.Status <> 200 Then
'Error
Debug.Assert "Error"
Else
Debug.Print whr.ResponseText
End If
End Sub
実行するとイミディエイトウィンドウには次のように出力されます。
0000000 74 65 73 74
t e s t
0000004
まぁ、素直な結果ですよね。こうなってほしい。ちなみに文字列リテラルも同じ結果になります。
実験2: Byte()配列で渡す
テキストデータと一緒にバイナリデータを渡したい場合など、POSTデータを組み立ててから渡さなければなりません。こういった場合は一旦Byte()配列に組み立ててあげる必要があります。
テストコードは次のようになります。
Sub test1()
Dim whr As WinHttpRequest
Dim poststr As String
Dim postdata() As Byte
Set whr = New WinHttpRequest
poststr = "test"
postdata = poststr
whr.Open "POST", "http://localhost/~yaizawa/posttest.cgi", False
whr.Send postdata
If whr.Status <> 200 Then
'Error
Debug.Assert "Error"
Else
Debug.Print whr.ResponseText
End If
End Sub
これを実行すると…
0000000 74 00 65 00 73 00 74 00
t \0 e \0 s \0 t \0
0000010
なぜか1バイトごとに0x00が挿入されています…。いろいろ確認した結果を総合すると、VBAの文字列は内部的にUTF-16LEで保持されており、String→Byte()の変換をするとUTF-16LEのバイト列として格納されるようです。
為念で日本語の文字を扱ってみましょう。poststrを「令和」にして実行するとこうなります。
0000000 e4 4e 8c 54
344 N 214 T
0000004
「令」はU+4EE4、「和」はU+548C。リトルエンディアンなのでLSBが先、MSBが後ろになりますから、しっかり符号しています。
回避方法
「Stringで渡す」のが一番楽ですが、Byte()を使わざるを得ない場合もあるかと思います (というか自分はソレでハマった)。VBA側でString→Byte()のキャストを行うとUTF-16LEになってしまうのですから、別の方法でキャストしてあげれば回避できます。
私はADODB.Streamを使って回避しました (要参照設定「Microsoft ActiveX Data Objects 6.1 Library」)。この方法であれば好きな文字コードのバイト列を生成できますので、UTF-8だろうがShift_JISだろうが、EUC-JPだろうが出力できます。
Sub test1()
Dim whr As WinHttpRequest
Dim buf As ADODB.Stream
Dim poststr As String
Dim postdata() As Byte
Set whr = New WinHttpRequest
Set buf = New ADODB.Stream
poststr = "令和"
' ADODB.Streamをテキストモードで開き、文字コードをShift_JISに指定。ストリームに書き出したい文字列を流し込む
buf.Open
buf.Type = adTypeText
buf.Charset = "Shift_JIS"
buf.WriteText poststr
' ストリームのカーソル位置を先頭に戻した上でバイナリモードに変更、読み取った上でByte()配列に格納
buf.Position = 0
buf.Type = adTypeBinary
postdata = buf.Read
whr.Open "POST", "http://localhost/~yaizawa/posttest.cgi", False
whr.Send postdata
If whr.Status <> 200 Then
'Error
Debug.Assert "Error"
Else
Debug.Print whr.ResponseText
End If
End Sub
実行するとこうなります。経産省の出している令和の文字コードを見て答え合わせすると、ちゃんと「97DF」「9861」になっていますね。
0000000 97 df 98 61
227 337 230 a
0000004
余談
- ADODB.StreamのReadText・WriteTextはType = adTypeTextのとき、Read・WriteはType = adTypeBinaryのときだけ成功します。逆だとエラーが起きます。テキストで流し込んでからバイナリで抜くときも、いちいちTypeを入れ替える必要があります
- ADODB.StreamのTypeを切り替えるときはPositionを0まで巻き戻す必要があります。なおテキストとバイナリが混じったデータを作る場合はType切り替えをたびたびする必要がありますので、切り替える前にPositionをどこかに退避しておいてから0に巻き戻し、対比させたPositionを戻してあげるなどの工夫が必要です