During your time building and debugging your botframework chatbot, you’ve probably already come across the annoyance of seeing a huge error message with a ridiculous stack trace that doesn’t seem to relate to your error, or even just a plain old HTTP 500 and big red exclamation mark..
Perhaps you’ve deployed to a production environment and are stuck with the stock error message:
Sorry, my bot code is having an issue
In this article I’ll show you how to 1) display useful exception information, and 2) override the default error message sent to the user
In order to capture the short, top level, exception only, we need to wrap the root Dialog with an exception handling one; let’s start with that.
Exception Handler Dialog
By default, when you start a conversation your MessagesController
will look a bit like this:
public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
if (activity.Type == ActivityTypes.Message)
{
await Conversation.SendAsync(activity, () => new MyRootDialog());
}
else
{
HandleSystemMessage(activity);
}
var response = Request.CreateResponse(HttpStatusCode.OK);
return response;
}
When an exception occurs within your root dialog you’ll either see nothing (i.e. an HTTP 500 error) or a huge stack trace. To try to handle both of these scenarios, we need to wrap the root dialog with an exception catching one.
This dialog wrapping code isn’t mine – it came from a GitHub issue, but I use it a lot so am sharing the love:
This dialog will enable us to wrap our root dialog’s StartAsync
and ResumeAsync
methods in try
catch
blocks:
[Serializable]
public class ExceptionHandlerDialog<T> : IDialog<object>
{
private readonly IDialog<T> _dialog;
private readonly bool _displayException;
private readonly int _stackTraceLength;
public ExceptionHandlerDialog(IDialog<T> dialog, bool displayException, int stackTraceLength = 500)
{
_dialog = dialog;
_displayException = displayException;
_stackTraceLength = stackTraceLength;
}
public async Task StartAsync(IDialogContext context)
{
try
{
context.Call(_dialog, ResumeAsync);
}
catch (Exception e)
{
if (_displayException)
await DisplayException(context, e).ConfigureAwait(false);
}
}
private async Task ResumeAsync(IDialogContext context, IAwaitable<T> result)
{
try
{
context.Done(await result);
}
catch (Exception e)
{
if (_displayException)
await DisplayException(context, e).ConfigureAwait(false);
}
}
private async Task DisplayException(IDialogContext context, Exception e)
{
var stackTrace = e.StackTrace;
if (stackTrace.Length > _stackTraceLength)
stackTrace = stackTrace.Substring(0, _stackTraceLength) + "…";
stackTrace = stackTrace.Replace(Environment.NewLine, " \n");
var message = e.Message.Replace(Environment.NewLine, " \n");
var exceptionStr = $"**{message}** \n\n{stackTrace}";
await context.PostAsync(exceptionStr).ConfigureAwait(false);
}
}
Now we use this dialog in our MessagesController
instead of our previous “root” dialog. Change the conversation initiation from (assuming your main dialog is called MyRootDialog
and implements IDialog<object>
):
await Conversation.SendAsync(activity, () => new MyRootDialog());
to
await Conversation.SendAsync(activity, () =>
new ExceptionHandlerDialog<object>(
new MyRootDialog(),
displayException: true));
This will actually capture the top level exception, and optionally return it as a message; just configure displayException
to be true
when debugging and false
when deployed.
You can also add in logging in the
catch
section of theExceptionHandlerDialog
.
Now your real exception should be both exposed and summarised.
Sorry, my bot code is having an issue
Next let’s override the default error message. This requires a little detective work, digging into the code in the GitHub repo. Let’s get our Sherlock hats on!
The exception message is sent by the PostUnhandledExceptionToUserTask
class, which has this method:
async Task IPostToBot.PostAsync<T>(T item, CancellationToken token)
{
try
{
await this.inner.PostAsync<T>(item, token);
}
catch (Exception error)
{
try
{
if (Debugger.IsAttached)
{
await this.botToUser.PostAsync($"Exception: {error}");
}
else
{
await this.botToUser.PostAsync(this.resources.GetString("UnhandledExceptionToUser"));
}
}
catch (Exception inner)
{
this.trace.WriteLine(inner);
}
throw;
}
}
This class can be found in the
DialogTask
file in the BotBuilder github repository
Notice the reference to this. resources. GetString("UnhandledExceptionToUser")
, which gets the message from the resource file for the current context’s language; for English, this is set to:
<data name="UnhandledExceptionToUser" xml:space="preserve">
<value>Sorry, my bot code is having an issue.</value>
</data>
As can be found in the Resources.resx
file
The PostUnhandledExceptionToUserTask
class is wired up using Autofac for IoC (Inversion of Control, out of scope for this article, but essentially allows resolution of an interface to an implementation via configuration in the “hosting” code) – notice the last few lines of the following code from the DialogModule.cs
Autofac module:
builder
.Register(c =>
{
var cc = c.Resolve<IComponentContext>();
Func<IPostToBot> makeInner = () =>
{
var task = cc.Resolve<DialogTask>();
IDialogStack stack = task;
IPostToBot post = task;
post = new ReactiveDialogTask(post, stack, cc.Resolve<IStore<IFiberLoop<DialogTask>>>(), cc.Resolve<Func<IDialog<object>>>());
post = new ExceptionTranslationDialogTask(post);
post = new LocalizedDialogTask(post);
post = new ScoringDialogTask<double>(post, stack, cc.Resolve<TraitsScorable<IActivity, double>>());
return post;
};
IPostToBot outer = new PersistentDialogTask(makeInner, cc.Resolve<IBotData>());
outer = new SerializingDialogTask(outer, cc.Resolve<IAddress>(), c.Resolve<IScope<IAddress>>());
// -- we want to override the next line:
outer = new PostUnhandledExceptionToUserTask(outer, cc.Resolve<IBotToUser>(), cc.Resolve<ResourceManager>(), cc.Resolve<TraceListener>());
outer = new LogPostToBot(outer, cc.Resolve<IActivityLogger>());
return outer;
})
.As<IPostToBot>()
.InstancePerLifetimeScope();
We need to override this and have our own class resolved for the IPostToBot
interface instead. However, we still need all of the other “wrapper” dialog tasks, so it’s actually easier to copy and paste this into our own AutoFac module, just changing that one line.
Default Exception Message Override Module
Let’s create a DefaultExceptionMessageOverrideModule
AutoFac module that looks like this:
public class DefaultExceptionMessageOverrideModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder
.Register(c =>
{
var cc = c.Resolve<IComponentContext>();
Func<IPostToBot> makeInner = () =>
{
var task = cc.Resolve<DialogTask>();
IDialogStack stack = task;
IPostToBot post = task;
post = new ReactiveDialogTask(post, stack, cc.Resolve<IStore<IFiberLoop<DialogTask>>>(),
cc.Resolve<Func<IDialog<object>>>());
post = new ExceptionTranslationDialogTask(post);
post = new LocalizedDialogTask(post);
post = new ScoringDialogTask<double>(post, stack,
cc.Resolve<TraitsScorable<IActivity, double>>());
return post;
};
IPostToBot outer = new PersistentDialogTask(makeInner, cc.Resolve<IBotData>());
outer = new SerializingDialogTask(outer, cc.Resolve<IAddress>(), c.Resolve<IScope<IAddress>>());
// --- our new class here
outer = new PostUnhandledExceptionToUserOverrideTask(outer, cc.Resolve<IBotToUser>(),
cc.Resolve<ResourceManager>(), cc.Resolve<TraceListener>());
outer = new LogPostToBot(outer, cc.Resolve<IActivityLogger>());
return outer;
})
.As<IPostToBot>()
.InstancePerLifetimeScope();
}
}
Notice the line which previously registered
PostUnhandledExceptionToUserTask
now uses our ownPostUnhandledExceptionToUserOverrideTask
.
Post Unhandled Exception To User Override Task
That new file looks like this – should be very familiar!
public class PostUnhandledExceptionToUserOverrideTask : IPostToBot
{
private readonly ResourceManager resources;
private readonly IPostToBot inner;
private readonly IBotToUser botToUser;
private readonly TraceListener trace;
public PostUnhandledExceptionToUserOverrideTask(IPostToBot inner, IBotToUser botToUser, ResourceManager resources, TraceListener trace)
{
SetField.NotNull(out this.inner, nameof(inner), inner);
SetField.NotNull(out this.botToUser, nameof(botToUser), botToUser);
SetField.NotNull(out this.resources, nameof(resources), resources);
SetField.NotNull(out this.trace, nameof(trace), trace);
}
async Task IPostToBot.PostAsync<T>(T item, CancellationToken token)
{
try
{
await inner.PostAsync(item, token);
}
catch (Exception)
{
try
{
await botToUser.PostAsync("OH NOES! Put your own message here, or logic to decide which message to show.", cancellationToken: token);
}
catch (Exception inner)
{
trace.WriteLine(inner);
}
throw;
}
}
}
Wiring it all up
Now that we’ve got a new implementation for IPostToBot
it has to be wired up in order to override the default one from DialogModule.cs
. To do this, put the following code wherever you already do your IoC, or in the Global.asax.cs
if you’re not doing it anywhere else yet.
var builder = new ContainerBuilder();
builder.RegisterModule(new DefaultExceptionMessageOverrideModule());
builder.Update(Conversation.Container);
Summary
With these changes in place you can finally avoid scrolling through many screens of stack trace that doesn’t give you any useful information, and customise the friendly error message sent to your user.
Great post and just what I was looking for. Thank you!
Hey Robin, i was trying to implement this feature. But am getting below error message. Can you please help?
Exception: Cannot create an instance of ExceptionHandlerDialog`1[T] because Type.ContainsGenericParameters is true.
[File of type ‘text/plain’]
man is this code still valid?
Not completely, no; the IPostToUser has changed, and the ability to override IoC registrations more easily has been added too.