C++ ときどき ごはん、わりとてぃーぶれいく☆

USAGI.NETWORKのなかのひとのブログ。主にC++。

C#/JSON/HTTP: .net で JSON-RPC-2.0 のクライアントを使ったら WebException が飛び ServerProtocolViolation で蹴られた件

Visual StudioC# プロジェクトをにゃんにゃんしている土曜日だった。JSON-RPC-2.0 のクライアントを実装する必要があったので NuGet を見ると導入が簡単そうなライブラリーが幾つも見つかる。

f:id:USAGI-WRP:20181201231619p:plain

このへんが使うの簡単で使ったらいいんじゃないかと思ったライブラリー:

ダウンロード数とアップデートを見ると Nethereum.JsonRpc.RpcClient とか EdjCase.JsonRpc.Client が良いかもしれないと思ったが、依存ライブラリーが多く、そもそも大きなライブラリーの小さな部品の1つとして設計されていて JSON-RPC-2.0 クライアントをさっくり簡単に使いたい場合には無駄に複雑な上にドキュメントも難解でした。

ライブラリー選びによらず、たいていの .net の HTTP クライアントは System.Net.Http をライブラリーの実装詳細に用いている。先に挙げた2つもそのタイプ。

使ってみると System.Net.WebException 例外が飛んできた。catch してみると

  • Status: ServerProtocolViolation
  • Message: The server committed a protocol violation. Section=ResponseStatusLine

だそうだ。

WebException 例外氏「オレは悪くない。なんかサーバーがおかしい。RFC違反のヘッダー返してくる。だからオレは知らん。」

みたいな。

さて、この例外が帰ってくるサーバーアプリは、 Chromecurl で目視確認する限りではヘッダー含め異常は無く、人間の目には正常に JSON-RPC-2.0 のレスポンスを返してくれていた。

curl --dump-header - http://127.0.0.1:50080/API/JSON-RPC-2.0/
HTTP/1.1 200 OK
Content-Length: 97
Content-Type: application/json
Access-Control-Allow-Origin: *

{"jsonrpc":"2.0","error":{"code":-32700,"message":"Parse error; an invalid JSON data"},"id":null}

どう見ても正常に JSON-RPC-2.0 のエラーレスポンスを 200 OK している。少なくとも HTTP レスポンスのレベルでは問題無さそうに見える。(本当は問題があるのだが😋)

この状況でもクライアントアプリのプロジェクトの App.config<httpWebRequest> 要素を追加し、 useUnsafeHeaderParsing = true を設定すると JSON-RPC-2.0 ライブラリーたちは取得結果のパースも正常に行え、期待動作してしまう。

<configuration>
  <!--他の要素は省略-->
  <system.net>
    <settings>
      <httpWebRequest useUnsafeHeaderParsing="true"/>
    </settings>
  </system.net>
</configuration>

しかし、キモい。

さて、見えない原因は何だろうか?

幸い WebException が問題があると言うバイナリーは現にそこにあるのだ。解析すればよい。私、プログラマー。バイナリー、トモダチ。

curl --trace - http://127.0.0.1:50080/API/JSON-RPC-2.0/
== Info:   Trying 127.0.0.1...
== Info: TCP_NODELAY set
== Info: Connected to localhost (127.0.0.1) port 50080 (#0)
=> Send header, 96 bytes (0x60)
0000: 47 45 54 20 2f 41 50 49 2f 4a 53 4f 4e 2d 52 50 GET /API/JSON-RP
0010: 43 2d 32 2e 30 2f 20 48 54 54 50 2f 31 2e 31 0d C-2.0/ HTTP/1.1.
0020: 0a 48 6f 73 74 3a 20 6c 6f 63 61 6c 68 6f 73 74 .Host: localhost
0030: 3a 35 30 30 38 30 0d 0a 55 73 65 72 2d 41 67 65 :50080..User-Age
0040: 6e 74 3a 20 63 75 72 6c 2f 37 2e 35 38 2e 30 0d nt: curl/7.58.0.
0050: 0a 41 63 63 65 70 74 3a 20 2a 2f 2a 0d 0a 0d 0a .Accept: */*....
<= Recv header, 16 bytes (0x10)

ここまで送信。問題はここから先、帰ってくるレスポンスのヘッダー領域にある事になっている。

0000: 48 54 54 50 2f 31 2e 31 20 32 30 30 20 4f 4b 0a HTTP/1.1 200 OK.
<= Recv header, 19 bytes (0x13)
0000: 43 6f 6e 74 65 6e 74 2d 4c 65 6e 67 74 68 3a 20 Content-Length:
0010: 39 37 0a                                        97.
<= Recv header, 31 bytes (0x1f)

…む??

0000: 43 6f 6e 74 65 6e 74 2d 54 79 70 65 3a 20 61 70 Content-Type: ap
0010: 70 6c 69 63 61 74 69 6f 6e 2f 6a 73 6f 6e 0a    plication/json.
<= Recv header, 31 bytes (0x1f)

…これは・w・

<= Recv header, 31 bytes (0x1f)
0000: 41 63 63 65 73 73 2d 43 6f 6e 74 72 6f 6c 2d 41 Access-Control-A
0010: 6c 6c 6f 77 2d 4f 72 69 67 69 6e 3a 20 2a 0a    llow-Origin: *.
<= Recv header, 1 bytes (0x1)

なるほど。

0000: 0a                                              .
<= Recv data, 97 bytes (0x61)

今回は原因が「見え」ました😃 よくバイナリーを見ると、レスポンスヘッダーのセパレーターの改行文字が 0x0a だけになっています。 0x0d どこ行った。と、言うわけで今回の原因はサーバーアプリが HTTP レスポンスヘッダーのセパレーターの改行文字を本来ならば RFC2616 §4 HTTP Message に準拠し "CR LF" ( 0x0D 0x0A ) を使わなければなりませんところを "LF" ( 0x0A ) のみで返している事らしいとわかる。

で、サーバーアプリの HTTP レスポンスヘッダーの生成箇所の実装ソースコードを確認してみると、

constexpr auto ret = TEXT( "\n" );

などと書いてありました。

constexpr auto ret = TEXT( "\r\n" );

このように修正すると、 System.Net.HttpuseUnsafeHeaderParsing = false でも期待動作するようになり WebException も飛ばなくなりました。こわいですね💀

References