LoginSignup
4
0

More than 3 years have passed since last update.

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

Posted at

何がおきたのか

仕事で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を戻してあげるなどの工夫が必要です
4
0
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0