VBAからWinHTTPRequestで文字列データを投げ込んだら化けて困った話

# 何がおきたのか

仕事でVBAからWeb APIに向けてデータを投げ込む必要があったのでWinHTTPRequestを使った実装をやっていたのですが、テストコードだと動くのに本番コードに落としていくとなぜかAPIからBad Requestで蹴られてしまうという事象に悩まされました。
いろいろ試行錯誤しているうちに、どうやらWinHTTPRequestのSendメソッドに渡すオブジェクトの種類により挙動が違うことに気が付きました。

# 再現するためのテスト準備

ポンコツコードではありますが、投げ込まれているデータを正確に把握するためのCGIを即席で書きます。

```sh

#!/bin/sh

echo "Content-Type: text/plain"
echo
/usr/bin/od -t x1c
```

このCGIにPOSTデータを投げると、単純にodの結果を出してくれます。読みやすさを考えてオプションは次のようにしました。

* x1 ... 16進1バイトごとに出力
* c ... 対応するASCIIコード (表示不可はエスケープ表示)

たとえば、こんな感じになります。

```shell-session
% 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」)。

```vb
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()配列に組み立ててあげる必要があります。
テストコードは次のようになります。

```vb
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
```

これを実行すると&hellip;

```
0000000  74  00  65  00  73  00  74  00
          t  \0   e  \0   s  \0   t  \0
0000010

```

なぜか1バイトごとに0x00が挿入されています&hellip;。いろいろ確認した結果を総合すると、VBAの文字列は内部的にUTF-16LEで保持されており、String&rarr;Byte()の変換をするとUTF-16LEのバイト列として格納されるようです。
為念で日本語の文字を扱ってみましょう。poststrを「令和」にして実行するとこうなります。

```
0000000  e4  4e  8c  54
        344   N 214   T
0000004

```

「令」はU+4EE4、「和」はU+548C。リトルエンディアンなのでLSBが先、MSBが後ろになりますから、しっかり符号しています。

# 回避方法

「Stringで渡す」のが一番楽ですが、Byte()を使わざるを得ない場合もあるかと思います (というか自分はソレでハマった)。VBA側でString&rarr;Byte()のキャストを行うとUTF-16LEになってしまうのですから、別の方法でキャストしてあげれば回避できます。
私はADODB.Streamを使って回避しました (要参照設定「Microsoft ActiveX Data Objects 6.1 Library」)。この方法であれば好きな文字コードのバイト列を生成できますので、UTF-8だろうがShift_JISだろうが、EUC-JPだろうが出力できます。

```vb
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
```

実行するとこうなります。経産省の出している[令和の文字コード](https://www.meti.go.jp/policy/it_policy/kaigen/20190405_kaigen_code.pdf)を見て答え合わせすると、ちゃんと「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を戻してあげるなどの工夫が必要です