【C#】WebAPIがJSONではなく、HTMLのエラーを返すことがある
バックエンドがC#(.NET Framework 4.8.1)、フロントがReactのアプリを保守している。
WebAPIアプリのC#が、エラー(Exception?)発生時にJSONではなく、HTMLを返すことがあり、困る。
ログ作成処理はどれも正常だった。
表示されるエラーは以下
Unexpected token '<', "<!DOCTYPE "... is not valid JSON
response.json()
でこのエラーが起きていることと、response
の中身がHTMLとなっていることは推測している。
通信がなぜHTMLを返すのかが分からない(C#だけではなく、CORS回りも原因の可能性?)
C#, Reactの実装は以下。
C#
System.Web.Http.HttpPostAttribute;
public class DefaultController : ApiController
{
[HttpGet]
public IHttpActionResult Get()
{
if (IsAuth())
{
// エラーログ作成
return BadRequest("認証してください。");
}
// 正常ログ作成
return Ok(new {
// いろいろ
});
}
[HttpPost]
public IHttpActionResult Post(dynamic d)
{
if (IsAuth())
{
// エラーログ作成
return BadRequest("認証してください。");
}
// 正常ログ作成
return Ok(new {
// いろいろ
});
}
}
React
function get() {
fetch("https://localhost:12345/api/Default", {
mode: 'cors', credentials: 'include',
})
.then(response => {
if (!response.ok) {
return response.json();
.then(e => {
throw new Error(e.Message);
});
}
return response;
})
.then(response => response.json())
.then(json => {
// いろいろ必要な処理
})
.catch(e => {
console.error(e.message);
});
}
function post() {
fetch("https://localhost:12345/api/Default", {
mode: 'cors', method: 'POST', credentials: 'include',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
})
.then(response => {
if (!response.ok) {
return response.json();
.then(e => {
throw new Error(e.Message);
});
}
return response;
})
.then(response => response.json())
.then(json => {
// いろいろ必要な処理
})
.catch(e => {
console.error(e.message);
});
}
とりあえず、Request headerにAccept
を必ず追加するようにすればよい?
実際にUnexpected token '<', "<!DOCTYPE "... is not valid JSON
のエラーが出た箇所を確認したが、'Accept': 'application/json'
が書いてあったので関係なさそう
参考になりそうなもの
1.
1を試してみた。結果は無理だった。
そもそも追加するTokenValidationAttribute
が.NET Frameworkが対象外なので。
2を試す。でも無理そう。
ExceptionFilterAttribute
が.NET Coreしか対応してない
C#はreturn Ok();
とかreturn BadRequest()
の直前でログをDBに作る処理を入れているのだが、そのログをみると、エラーになっているHTTP Requestは問題なさそうなので、エラーの発生個所が
-
return Ok(object);
の箇所 - React側の受け取った時
- CORS回り
が候補になる。1はあまり考えられないから2,3の方が可能性高いかな?
こんな記事を見つけた
本番環境にもポートってあるのだろうか?
でも常にHTMLが返ってくるわけじゃなくて、稀にだから違う原因?
IIS Expressの設定回りで内部で別ポートに回したりとかがあるのだろうか?
アドバイスをもらって、以下を調査することにした
- ログを作成してから
return Ok()
するまでに何か特殊なことをしていないか - CORS、プロキシ設定周りで変なリダイレクト入っていないか?
- IISのログ確認
- 文字コード確認
1. ログを作成してからreturn Ok()するまでに何か特殊なことをしていないか
結果:特筆するようなことはなさそう
HTMLが返る状況は再現できていないが、JSONが返される状況で何の処理が動くかを確認。
POST, PATCHは、return Ok()
で返しているデータのモデルのget
が動いていた。
GETは、常にではないがGlobal.asax.cs
のMvcApplication_PostAuthenticateRequest()
に入ったりしていた。POSTの直後に実行されるGETで多かった印象。
2. CORS、プロキシ設定周りで変なリダイレクト入っていないか
結果:多分入っていない
CORSの方は、same-origin
になるから変なことはあまりないだろうし、軽く調べて、CORSのリダイレクトはほぼほぼ動かないという情報を見たので、無いとする
プロキシ設定はよくわからないけど、本当にアクセスしたいところにアクセスする前に通るゲートみたいなもの?という印象。
プログラムの方は"proxy"という単語が全く引っかからなかったので無さそう。
AWSとかIISの設定はよくわからなかったけど、たぶんしてない。
3. IISのログ確認
結果:IISが原因かもしれない
C#で作成したログが200
だったのに対し、IISのログが500
のものがあった。
IISがなぜか勝手に500エラーとして、用意されていた500.html
を返している可能性が高い?
理由は不明
4. 文字コード確認
結果:どちらもデフォルトがUTF-8っぽい
JavaScriptはデフォルトがUTF-16だとかUTF-8だとか、いろいろな話が書いてあった
VS Codeでファイルを開いたときはUTF-8だったから、今回はそれがデフォルト?
受け付ける文字コードの指定としてAccept-Charset
がある(あった?)ようだが、標準実装はされていないそう。なんでも通信で使う文字コードはUTF-8にしようという圧倒的支持があるよう?(リンク先のMDN Web Docの先が消えていた)
C#はよくわからなかったけど、VS CodeでControllerファイル開いたらUTF-8 with BOMだった。
軽く調べた感じだと、Responseの文字コード指定とかもありそう
↑これを見ると、以下のように書かれている。
HTTP 応答のコンテンツ エンコーディング。 既定は UTF-8 です。
ということで、デフォルトはUTF-8なのでは?
でもMVCで返すHTMLファイルのエンコーディングの話かもしれない
「HTTP応答」にはWebAPIアプリのHTTP requestも含まれるのか?
Web.config
に文字コードを指定するコードがある。
断続的に発生する場合、負荷がかかりすぎている可能性があるよう
EC2なので、イベントビューアーのログも見てみたら、管理イベントのログにぽいものがあった。原因はMailKit
?
見せない方が良さそうなものは削除。
Event code: 3005
Event message: An unhandled exception has occurred.
Event time: (削除)
Event time (UTC): (削除)
Event ID: (削除)
Event sequence: 570
Event occurrence: 5
Event detail code: 0
Application information:
Application domain: (削除)
Trust level: Full
Application Virtual Path: /
Application Path: (削除)
Machine name: (削除)
Process information:
Process ID: 5084
Process name: (削除)
Account name: (削除)
Exception information:
Exception type: IOException
Exception message: An existing connection was forcibly closed by the remote host
at MailKit.Net.NetworkStream.Read(Byte[] buffer, Int32 offset, Int32 count) in (削除)
at System.Net.FixedSizeReader.ReadPacket(Byte[] buffer, Int32 offset, Int32 count)
at System.Net.Security.SslState.StartReceiveBlob(Byte[] buffer, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslState.CheckCompletionBeforeNextReceive(ProtocolToken message, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslState.ForceAuthentication(Boolean receiveFirst, Byte[] buffer, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslState.ProcessAuthentication(LazyAsyncResult lazyResult)
at MailKit.Net.Smtp.SmtpClient.<ConnectAsync>d__100.MoveNext() in (削除)
An existing connection was forcibly closed by the remote host
at MailKit.Net.NetworkStream.Read(Byte[] buffer, Int32 offset, Int32 count) in (削除)
Request information:
Request URL: (削除)
Request path: (削除)
User host address: (削除)
User: (削除)
Is authenticated: True
Authentication Type: (削除)
Thread account name: (削除)
Thread information:
Thread ID: 21
Thread account name: (削除)
Is impersonating: False
Stack trace: at MailKit.Net.NetworkStream.Read(Byte[] buffer, Int32 offset, Int32 count) in (削除)
at System.Net.FixedSizeReader.ReadPacket(Byte[] buffer, Int32 offset, Int32 count)
at System.Net.Security.SslState.StartReceiveBlob(Byte[] buffer, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslState.CheckCompletionBeforeNextReceive(ProtocolToken message, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslState.ForceAuthentication(Boolean receiveFirst, Byte[] buffer, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslState.ProcessAuthentication(LazyAsyncResult lazyResult)
at MailKit.Net.Smtp.SmtpClient.<ConnectAsync>d__100.MoveNext() in (削除)
Custom event details:
どうしようもないかも
C#に入れているのはMailKit v3.1.1
とりあえずメール送信全体をtry-catchで囲むことにした