プロキシ環境下でBot Framework から QnA Makerを呼び出せない問題への対処

# TL;DR

- Bot Frameworkを利用し[Azure Bot Service](https://docs.microsoft.com/ja-jp/azure/bot-service/?view=azure-bot-service-4.0)し、[QnA Maker を使用した開発](https://docs.microsoft.com/ja-jp/azure/bot-service/bot-builder-tutorial-add-qna?view=azure-bot-service-4.0&tabs=csharp)を行った場合、プロキシ環境下では、QnA Makerのサービスに接続できない問題への対処方法

# 環境

- Visual Studio 2019 16.4.3
- .NET Core 2.1
- Microsoft.Bot.Builder.AI.QnA 4.7.1
- Microsoft.Bot.Builder.Integration.AspNet.Core 4.7.1

# 事象

Visual StudioでBot Serviceを起動し、Bot Framework EmulatorでQnA Makerを利用したBotをテストした場合、以下のようなメッセージが表示されサービスに接続できない

## コード

```cs:EchoBot.cs
private async Task AccessQnAMaker(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
   var results = await EchoBotQnA.GetAnswersAsync(turnContext);
   if (results.Any())
   {
      await turnContext.SendActivityAsync(MessageFactory.Text("QnA Maker Returned: " + results.First().Answer), cancellationToken);
   }
   else
   {
      await turnContext.SendActivityAsync(MessageFactory.Text("Sorry, could not find an answer in the Q and A system."), cancellationToken);
   }
}
```

## エラー

``var results = await EchoBotQnA.GetAnswersAsync(turnContext);``の行で以下のエラーが発生する

### Bot Framework Emulator

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204150/b1484259-76bf-5841-6352-aa27d195a9cd.png)

```
The bot encounted an error or bug.
To continue to run this bot, please fix the bot source code.
```

### AdapterWithErrorHandler.csでの例外（Exception）

```:StacTrace
   at System.Net.Http.ConnectHelper.<ConnectAsync>d__2.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Threading.Tasks.ValueTask`1.get_Result()
   at System.Net.Http.HttpConnectionPool.<CreateConnectionAsync>d__44.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Threading.Tasks.ValueTask`1.get_Result()
   at System.Net.Http.HttpConnectionPool.<WaitForCreatedConnectionAsync>d__49.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Threading.Tasks.ValueTask`1.get_Result()
   at System.Net.Http.HttpConnectionPool.<SendWithRetryAsync>d__39.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
   at System.Net.Http.RedirectHandler.<SendAsync>d__4.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
   at System.Net.Http.HttpClient.<FinishSendAsyncBuffered>d__62.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.Bot.Builder.AI.QnA.HttpRequestUtils.<ExecuteHttpRequestAsync>d__4.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.Bot.Builder.AI.QnA.GenerateAnswerUtils.<QueryQnaServiceAsync>d__13.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.Bot.Builder.AI.QnA.GenerateAnswerUtils.<GetAnswersRawAsync>d__9.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.Bot.Builder.AI.QnA.QnAMaker.<GetAnswersRawAsync>d__22.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.Bot.Builder.AI.QnA.QnAMaker.<GetAnswersAsync>d__21.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at EchoBot3QnA.Bots.EchoBot.<AccessQnAMaker>d__10.MoveNext() in C:\Users\User1\～
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at EchoBot3QnA.Bots.EchoBot.<OnMessageActivityAsync>d__8.MoveNext() in C:\Users\User1\～
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.Bot.Builder.BotFrameworkAdapter.TenantIdWorkaroundForTeamsMiddleware.<OnTurnAsync>d__0.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.Bot.Builder.MiddlewareSet.<ReceiveActivityWithStatusAsync>d__3.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.Bot.Builder.BotAdapter.<RunPipelineAsync>d__15.MoveNext()
```

```:InnerException
NativeErrorCode = 10060	System.Exception {System.Net.Sockets.SocketException}
```

```:Message
"接続済みの呼び出し先が一定の時間を過ぎても正しく応答しなかったため、接続できませんでした。または接続済みのホストが応答しなかったため、確立された接続は失敗しました。"	string
```

# 原因

[ここ](https://social.msdn.microsoft.com/Forums/silverlight/ro-RO/2b7f0be2-b997-4408-8e41-7640b5e08ff0/qnamakergetanswersasync407-proxy-authentication-required?forum=azureiotcognitivebotja)にヒントがあるが、QnA Maker接続用のクラス`Microsoft.Bot.Builder.AI.QnA.QnAMaker`内部で使用しているHttpClientでプロキシが使用されていないため。

[ここ](https://docs.microsoft.com/ja-jp/archive/blogs/shozoa/httpclient)によれば、HttpClientHandlerを一度初期化すれば、以降はどのようにHttpClientでコネクションしても接続できるとある。しかし、.NET Core 2.1の問題なのか仕様が変更されたのか、QnAMakerモジュールの仕様かは不明だが、HttpClientHandlerを一度初期化するだけでは解決しない。

.NET Core 3.1で改善している可能性もあるが、検証できていない。

# 対策

`Microsoft.Bot.Builder.AI.QnA.QnAMaker`のインスタンスを作成する際に、プロキシを設定した HttpClientを渡すように変更する。

また、環境でプロキシのアドレスが異なる場合も考えられるため、設定ファイルにプロキシのアドレスを持たせる。

- .NET Core 3.1では`HttpClient.DefaultProxy`を使用できる[可能性もある](（https://docs.microsoft.com/ja-jp/dotnet/api/system.net.http.httpclient.defaultproxy?view=netcore-3.1）)が、検証できていないため、設定ファイルに持たせる形としている
- 開発時の環境の相違を考慮し、`appsetting.json`の「新しい場合はコピーする」プロパティを`新しい場合はコピーする`としている

## 変更前

```json:appsetting.json
{
  "MicrosoftAppId": "",
  "MicrosoftAppPassword": "",
  "ScmType": "None",
  "QnAKnowledgebaseId": "knowledge-base-id",
  "QnAAuthKey": "qna-maker-resource-key",
  "QnAEndpointHostName": "your-hostname" // This is a URL ending in /qnamaker
}
```

```cs:EchoBot.cs
public QnAMaker EchoBotQnA { get; private set; }
public EchoBot(QnAMakerEndpoint endpoint)
{
   // connects to QnA Maker endpoint for each turn
   EchoBotQnA = new QnAMaker(endpoint);
}
```
## 変更後

```json:appsetting.json
{
  "MicrosoftAppId": "",
  "MicrosoftAppPassword": "",
  "ScmType": "None",
  "ProxyAddress": "http://（プロキシのアドレス）:（プロキシのポート）",
  "QnAKnowledgebaseId": "knowledge-base-id",
  "QnAAuthKey": "qna-maker-resource-key",
  "QnAEndpointHostName": "your-hostname" // This is a URL ending in /qnamaker
}
```

```cs:EchoBot.cs
public QnAMaker EchoBotQnA { get; private set; }
public EchoBot(IConfiguration configuration, QnAMakerEndpoint endpoint)
{
    Configuration = configuration;

    // connects to QnA Maker endpoint for each turn
    var proxyAddress = Configuration.GetValue<string>($"ProxyAddress");
    if (string.IsNullOrEmpty(proxyAddress))
    {
        EchoBotQnA = new QnAMaker(endpoint);
    }
    else
    {
        var httpClientHandler = new HttpClientHandler
        {
            // .NET Core 2.1では（HttpClient.DefaultProxy）がない（https://docs.microsoft.com/ja-jp/dotnet/api/system.net.http.httpclient.defaultproxy?view=netcore-3.1）
            Proxy = new WebProxy(proxyAddress, true),
            UseProxy = true
        };
        var client = new HttpClient(httpClientHandler);
        EchoBotQnA = new QnAMaker(endpoint, null, client);
    }
}
```