in libraries/Microsoft.Bot.Builder/Streaming/StreamingRequestHandler.cs [235:388]
public override async Task<StreamingResponse> ProcessRequestAsync(ReceiveRequest request, ILogger<RequestHandler> logger = null, object context = null, CancellationToken cancellationToken = default)
{
var response = new StreamingResponse();
// We accept all POSTs regardless of path, but anything else requires special treatment.
if (!string.Equals(request?.Verb, StreamingRequest.POST, StringComparison.OrdinalIgnoreCase))
{
return HandleCustomPaths(request, response);
}
// Convert the StreamingRequest into an activity the adapter can understand.
string body;
try
{
body = await request.ReadBodyAsStringAsync().ConfigureAwait(false);
}
#pragma warning disable CA1031 // Do not catch general exception types (we log the exception and continue execution)
catch (Exception ex)
#pragma warning restore CA1031 // Do not catch general exception types
{
response.StatusCode = (int)HttpStatusCode.BadRequest;
response.SetBody($"Request body missing or malformed");
_logger.LogError("Request body missing or malformed: " + ex.Message);
return response;
}
try
{
var activity = JsonConvert.DeserializeObject<Activity>(body, SerializationSettings.DefaultDeserializationSettings);
var conversationId = activity?.Conversation?.Id;
// An Activity with Conversation.Id is required.
if (activity == null || string.IsNullOrEmpty(conversationId))
{
response.StatusCode = (int)HttpStatusCode.BadRequest;
if (activity == null)
{
response.SetBody($"activity is null. Body: {body}");
}
else
{
response.SetBody($"conversationId is null. Body: {body}");
}
return response;
}
// All activities received by this StreamingRequestHandler will originate from the same channel, but we won't
// know what that channel is until we've received the first request.
if (string.IsNullOrWhiteSpace(ServiceUrl))
{
ServiceUrl = activity.ServiceUrl;
}
// If this is the first time the handler has seen this conversation it needs to be added to the dictionary so the
// adapter is able to route requests to the correct handler.
if (!HasConversation(conversationId))
{
// Ignore the result, since the goal is simply to ensure the Conversation.Id is in _conversations.
// If some other thread added it already, does not matter.
_conversations.TryAdd(conversationId, DateTime.Now);
}
/*
* Any content sent as part of a StreamingRequest, including the request body
* and inline attachments, appear as streams added to the same collection. The first
* stream of any request will be the body, which is parsed and passed into this method
* as the first argument, 'body'. Any additional streams are inline attachments that need
* to be iterated over and added to the Activity as attachments to be sent to the Bot.
*/
if (request.Streams.Count > 1)
{
var streamAttachments = new List<Attachment>();
for (var i = 1; i < request.Streams.Count; i++)
{
streamAttachments.Add(new Attachment() { ContentType = request.Streams[i].ContentType, Content = request.Streams[i].Stream });
}
if (activity.Attachments != null)
{
activity.Attachments = activity.Attachments.Concat(streamAttachments).ToArray();
}
else
{
activity.Attachments = streamAttachments.ToArray();
}
}
// Populate Activity.CallerId given the Audience value.
string callerId = null;
switch (Audience)
{
case AuthenticationConstants.ToChannelFromBotOAuthScope:
callerId = CallerIdConstants.PublicAzureChannel;
break;
case GovernmentAuthenticationConstants.ToChannelFromBotOAuthScope:
callerId = CallerIdConstants.USGovChannel;
break;
default:
if (!string.IsNullOrEmpty(Audience))
{
if (Guid.TryParse(Audience, out var result))
{
// Large assumption drawn here; any GUID is an AAD AppId. This is prohibitive towards bots not using the Bot Framework auth model
// but still using GUIDs/UUIDs as identifiers.
// It's also indicative of the tight coupling between the Bot Framework protocol, authentication and transport mechanism in the SDK.
// In R12, this work will be re-implemented to better utilize the CallerId and Audience set on BotFrameworkAuthentication instances
// and decouple the three concepts mentioned above.
callerId = $"{CallerIdConstants.BotToBotPrefix}{Audience}";
}
else
{
// Fallback to using the raw Audience as the CallerId. The auth model being used by the Adapter using this StreamingRequestHandler
// is not known to the SDK, therefore it is assumed the developer knows what they're doing. The SDK should not prevent
// the developer from extending it to use custom auth models in Streaming contexts.
callerId = Audience;
}
}
// A null Audience is an implicit statement indicating the bot does not support skills.
break;
}
activity.CallerId = callerId;
// Now that the request has been converted into an activity we can send it to the adapter.
var adapterResponse = await _activityProcessor.ProcessStreamingActivityAsync(activity, _bot.OnTurnAsync, cancellationToken).ConfigureAwait(false);
// Now we convert the invokeResponse returned by the adapter into a StreamingResponse we can send back to the channel.
if (adapterResponse == null)
{
response.StatusCode = (int)HttpStatusCode.OK;
}
else
{
response.StatusCode = adapterResponse.Status;
if (adapterResponse.Body != null)
{
response.SetBody(adapterResponse.Body);
}
}
}
#pragma warning disable CA1031 // Do not catch general exception types (we logging the error and we send it back in the body of the response)
catch (Exception ex)
#pragma warning restore CA1031 // Do not catch general exception types
{
response.StatusCode = (int)HttpStatusCode.InternalServerError;
response.SetBody(ex.ToString());
_logger.LogError(ex.ToString());
}
return response;
}