C#/JSON/HTTP: .net で JSON-RPC-2.0 のクライアントを使ったら WebException が飛び ServerProtocolViolation で蹴られた件
Visual Studio で C# プロジェクトをにゃんにゃんしている土曜日だった。JSON-RPC-2.0
のクライアントを実装する必要があったので NuGet
を見ると導入が簡単そうなライブラリーが幾つも見つかる。
このへんが使うの簡単で使ったらいいんじゃないかと思ったライブラリー:
ダウンロード数とアップデートを見ると Nethereum.JsonRpc.RpcClient とか EdjCase.JsonRpc.Client が良いかもしれないと思ったが、依存ライブラリーが多く、そもそも大きなライブラリーの小さな部品の1つとして設計されていて JSON-RPC-2.0 クライアントをさっくり簡単に使いたい場合には無駄に複雑な上にドキュメントも難解でした。
ライブラリー選びによらず、たいていの .net の HTTP クライアントは System.Net.Http
をライブラリーの実装詳細に用いている。先に挙げた2つもそのタイプ。
使ってみると System.Net.WebException
例外が飛んできた。catch
してみると
Status
: ServerProtocolViolationMessage
: The server committed a protocol violation. Section=ResponseStatusLine
だそうだ。
WebException 例外氏「オレは悪くない。なんかサーバーがおかしい。RFC違反のヘッダー返してくる。だからオレは知らん。」
みたいな。
さて、この例外が帰ってくるサーバーアプリは、 Chrome
や curl
で目視確認する限りではヘッダー含め異常は無く、人間の目には正常に 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.Http
は useUnsafeHeaderParsing = false
でも期待動作するようになり WebException
も飛ばなくなりました。こわいですね💀