Search code examples
c#botsbotframeworkfacebook-messenger-bot

Multiple messages to the bot in quick succession crash it


Setup

I have a a bot that runs on .NET + Bot Framework + Azure + Facebook Messenger.

Initial Problem

I was trying to solve a problem when sending several messages to the bot triggers an exception and HTTP error 412. Microsoft describes this problem here: https://learn.microsoft.com/en-us/bot-framework/troubleshoot-general-problems#what-causes-an-error-with-http-status-code-412-precondition-failed-or-http-status-code-409-conflict

First Solution

In the page above, Microsoft provides an outdated sample code to resolve this issue. In this github issue, there is a revised version of that code that is supposed to work. I put it inside the constructor of my MessageController:

    static MessagesController()
    {
        // Prevent exception in the bot and HTTP error 412 when the user
        // sends multiple messages in quick succession. This may cause 
        // potential problems with consistency of getting/setting user
        // properties. 
        // See https://learn.microsoft.com/en-us/bot-framework/troubleshoot-general-problems#what-causes-an-error-with-http-status-code-412-precondition-failed-or-http-status-code-409-conflict
        // for details. The above link contains wrong code sample, revised
        // code is from here: https://github.com/Microsoft/BotBuilder/issues/2345
        var builder = new ContainerBuilder();
        builder
            .Register(c => new CachingBotDataStore(c.ResolveKeyed<IBotDataStore<BotData>>(typeof(ConnectorStore)), CachingBotDataStoreConsistencyPolicy.LastWriteWins))
            .As<IBotDataStore<BotData>>()
            .AsSelf()
            .InstancePerLifetimeScope();
        builder.Update(Conversation.Container);
    } 

Second Problem

Now, the exception still occurs when I send several messages to the bot in a quick succession. However, it changed from HTTP error 412 to something else:

One or more errors occurred. at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions) at System.Threading.Tasks.Task1.GetResultCore(Boolean waitCompletionNotification) at System.Threading.Tasks.Task1.get_Result() at MyBot.SetUserDataProperty(Activity activity, String PropertyName, String ValueToSet) in C:\Users\xxx.cs:line 230

Update: I've checked the InnerException of the above and it turns out to be the same old HTTP error 412:

The remote server returned an error: (412) Precondition Failed.

The offending code is a function that writes to the bot storage. The line 230 referenced above is the last line of this function:

    public static void SetUserDataProperty(Activity activity, string PropertyName, string ValueToSet)
    {
        StateClient client = activity.GetStateClient();
        BotData userData = client.BotState.GetUserData(activity.ChannelId, activity.From.Id);
        userData.SetProperty<string>(PropertyName, ValueToSet);

        //client.BotState.SetUserDataAsync(activity.ChannelId, activity.From.Id, userData);
        // Await async call without making the function asynchronous:
        var temp = Task.Run(() => client.BotState.SetUserDataAsync(activity.ChannelId, activity.From.Id, userData)).Result;
    }

Question

What else can I do to make sure that the user is able to send multiple messages in quick succession without triggering an exception when writing to the BotState storage?


Solution

  • I think there are a few issues here

    The way you are trying to do this activity.GetStateClient(); is a only intended to be used for prototyping. We do no reccomend this method for production level code. You can set user data like context.UserData.SetValue("food", "Nachos" ); in the dialog and the values will automagically get saved when the dialog is serialized.

    Most likely you are calling this method SetUserDataProperty from a dialog so when you do this var temp = Task.Run(() => client.BotState.SetUserDataAsync(activity.ChannelId, activity.From.Id, userData)).Result; it is conflicting and causing the error.

    please review this blog post to learn more

    Here is how to implement your follow up question:

            if (activity.Type == ActivityTypes.Message)
            {
    
                var message = activity as IMessageActivity;
                using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, message))
                {
                    var botDataStore = scope.Resolve<IBotDataStore<BotData>>();
                    var key = new AddressKey()
                    {
                        BotId = message.Recipient.Id,
                        ChannelId = message.ChannelId,
                        UserId = message.From.Id,
                        ConversationId = message.Conversation.Id,
                        ServiceUrl = message.ServiceUrl
                    };
                    ConversationReference r = new ConversationReference();
                    var userData = await botDataStore.LoadAsync(key, BotStoreType.BotUserData, CancellationToken.None);
    
                    userData.SetProperty("key 1", "value1");
                    userData.SetProperty("key 2", "value2");
    
                    await botDataStore.SaveAsync(key, BotStoreType.BotUserData, userData, CancellationToken.None);
                    await botDataStore.FlushAsync(key, CancellationToken.None);
                }
                await Conversation.SendAsync(activity, () => new Dialogs.RootDialog());
            }
    

    you will need to implement this class or something similar:

    public class AddressKey : IAddress
    {
        public string BotId { get; set; }
        public string ChannelId { get; set; }
        public string ConversationId { get; set; }
        public string ServiceUrl { get; set; }
        public string UserId { get; set; }
    }