Handle ASP.NET Core exceptions gracefully in TwiML webhooks
Time to read: 7 minutes
A lot of things can go wrong in your application leading to exceptions, and when these exceptions are not caught, ASP.NET Core will respond with an 500 Internal Server Error
and, depending on your project and environment, render an HTML error page for you. However, when you're implementing an incoming message or voice call webhook, Twilio expects you to respond with HTTP status 200 and TwiML instructions.
In this tutorial, you'll learn how to extend ASP.NET Core to respond with TwiML instructions to respond that an error occurred.
Prerequisites
Here’s what you will need to follow along:
- .NET 7 SDK (earlier and versions may work too)
- A code editor or IDE (I recommend JetBrains Rider, Visual Studio, or VS Code with the C# plugin)
- A free Twilio account (sign up with Twilio for free)
- A Twilio Phone Number
- The ngrok CLI and, optionally, a free ngrok account
- Git to clone the sample projectPrior experience with Twilio Phone Numbers, the message webhook, and the voice webhooks
You can find the source code for this tutorial on GitHub. Use it if you run into any issues, or submit an issue, if you run into problems.
Clone the sample project
To illustrate the problem, I have provided you with a sample ASP.NET Core project that handles the incoming message and voice call webhooks using controllers and minimal API endpoints, but they have errors in them.
Open a shell and clone the sample project using the following command:
Next, open the project using your preferred IDE and inspect the following files:
Controllers/TwilioController.cs:
TwilioEndpoints.cs:
All the controller actions and minimal API endpoints are supposed to respond with the result of dividing 1 by 0. However, as my father used to teach in his math classes, "Delen door null is flauwekul", which loosely translates to "Dividing by zero is bulls**t". So none of these endpoints will succeed as all of them will throw a DivideByZeroException
.
Test the project before handling exceptions gracefully
To test the project, run the application using the following command:
Then, open the browser and browse to the application URL with the /twilio/message path appended. During development, you will see a nice error page with exception details to help you debug the problem. In production, no page is returned, only a "HTTP ERROR 500". You will see the same result for paths /twilio/voice, /minimal-message, and /minimal-voice.
To see how this would behave in your Twilio application, you'll need to configure the message and voice webhook on your Twilio Phone Number. But before you can do that, you'll need to make your locally running project accessible to the internet. You can quickly do this using ngrok which creates a secure tunnel to the internet for you.
In the following command, replace [YOUR_LOCALHOST_URL]
with the URL that is printed to the console when executing dotnet run
. Then, leave your .NET application running, and run the following command in a separate shell:
ngrok will print the Forwarding URL you'll need to publicly access your local application.
Now, go to the Twilio Console in your browser, use the left navigation to navigate to Phone Numbers > Manage > Active Numbers, and then select the Twilio Phone Number you want to test with.
On the phone number configuration page, locate the "A CALL COMES IN" section. Underneath that, set the first dropdown to Webhook, the text box to the ngrok Forwarding URL, adding on the /twilio/voice path. Then, set the second dropdown to "HTTP POST". Follow the same steps at the "A MESSAGE COMES IN" section, but use the /twilio/message path instead, and then click Save.
Now call and text the Twilio Phone Number. When calling, you should hear "We're sorry, an application error has occurred. Goodbye.". When texting, you should not get any response at all.
Now let's see how you can handle these errors gracefully by responding with HTTP status 200 and TwiML instructions.
Handling TwiML webhooks gracefully
The easiest way to solve this problem, would be to wrap all your code in a try/catch block and, inside the catch block, return a user-friendly message, like this:
While this works, it's not a good solution and considered a code smell. It indents all your code, making the code less readable, you have to repeat the same logic in every endpoint.
This is why ASP.NET Core has some built-in APIs to handle exceptions for your entire application. While there are many ways to do this, I'll share two solutions with you, one that applies specifically to MVC, and one that applies to all endpoints including MVC. Let's start with the one that applies to all endpoints, as that's the one I recommend the most.
Use the Exception Handler
Open the Program.cs file and add the following lines of code:
The UseExceptionHandler("/error")
method will catch any uncaught exceptions in your application, log it as an error, and then reroute the request to the given path of /error. Currently, nothing handles requests going to /error, which is why you'll add an endpoint for /error next, that will be mapped inside MapErrorEndpoint()
.
Create a new file called ErrorEndpoint.cs and add the following code:
First, take note of the MapErrorEndpoint
method which is an extension method, extending IEndpointRouteBuilder
. This is the method that you're calling inside of Program.cs. MapErrorEndpoint
maps the /error path to the OnError
method.
OnError
retrieves the details of the exception that occurred and checks if the endpoint metadata contains the CatchWithMessageTwimlAttribute
or the CatchWithVoiceTwimlAttribute
.
If the metadata contains the CatchWithMessageTwimlAttribute
, the TwimlMessageError
method will respond with the following TwiML:
If the metadata contains the CatchWithVoiceTwimlAttribute
, the TwimlVoiceError
method will respond with the following TwiML:
For any other endpoint, the StatusCodeError
method will respond with HTTP status 500, just as before.
The CatchWithMessageTwimlAttribute
and CatchWithVoiceTwimlAttribute
are defined at the bottom of the file and are empty attributes without any logic. You need to use these attributes to mark your endpoints, such as areas, controllers, actions, and Minimal APIs. So let's do that next.
Update the Controllers/TwilioController.cs file as follows:
Next, update the TwilioEndpoints.cs file as follows:
I wrote these two endpoints inside the TwilioEndpoints
class with dedicated methods to organize my code the way I prefer. However, you can also map these endpoints using lambdas directly in the Program.cs file. If you prefer that, you can apply these attributes using two different options.
The first option is to add the attribute in front of the lambda:
The second option is to use the .WithMetadata
method:
To test this out, leave the ngrok tunnel running, switch back to the shell where your app is running, and stop your application by pressing ctrl + c
. Then start it again using the dotnet run
command. Now, give your Twilio Phone Number another call, and send it another message.
Now you'll be responded with, "An unexpected error occurred. Please try again.".
Use exception filters in MVC
MVC uses the concept of filters, which lets you run code at different stages of the MVC pipeline. One of the types of filters is the exception filter, which is called when an exception is not caught within your controller. Let's create an exception filter to catch the exception and respond with the appropriate TwiML.
First, create a new file TwilioExceptionFilters.cs and add the following code:
The GenericErrorTwimlExceptionFilter
will need to know whether it should respond with messaging TwiML or voice TwiML, hence the need for the ErrorTwimlType
enum. The GenericErrorTwimlExceptionFilter
receives the ErrorTwimlType
via its constructor, and also receives a logger that is injected by ASP.NET Core's built-in dependency injection container.
To develop an exception filter, you need to implement the IExceptionFilter
interface, (or the IAsyncExceptionFilter
interface if you need to do async work). To implement IExceptionFilter
, you need to add the OnException
method where you can add your error handling logic.
OnException
will log the exception as an error, then generate either messaging or voice TwiML which is set as the result using context.Result
. Thus, ASP.NET Core will respond with HTTP status 200 and the generic error TwiML.
To use this exception filter, you need to apply it to your areas, controllers, or actions like this:
This works, but having to specify the type and passing in a new array with an enum isn't very elegant, so let's make it cleaner.
Go back to TwilioExceptionFilters.cs and add the following code at the end of the file:
These two attributes essentially alias what you were doing directly inside your controller. So now you can use [GenericErrorTwimlMessage]
and [GenericErrorTwimlVoice]
as attributes, like this:
Much more elegant!
Which is better?
The exception handler solution works for all of ASP.NET Core, not just MVC, and the logging is handled by ASP.NET Core. You also have to handle the case where the endpoint is not supposed to respond with TwiML, such as responding with an empty error 500, or a nice HTML error 500 page.
The exception filter solution only works for MVC, but it fits in and integrates well within MVC, which may be your preferred framework. You don't have to handle cases where you shouldn't respond with TwiML, because it'll be handled by another exception filter or the exception handler.
Technically, you don't have to chose, as you can use both in the same project. They both get the job done in a similar way, so it's up to your and your teams' preference.
Fallback webhook
Both the messaging and the voice webhook have a fallback webhook that is called in case the primary webhook returns an error. You can configure this in the phone number configuration page under the "PRIMARY HANDLER FAILS" sections. How you handle the fallback webhook is not prescribed by Twilio, so you could use it as you see fit.
You could configure the fallback webhook to send the HTTP request back to your application, and your application could respond with the same generic error message TwiML. However, if your application is completely down, the fallback webhook would also fail. I recommend pointing the fallback webhook to a different host, maybe running the same application, maybe running a different application, or even better, you could use a TwiML bin, a Twilio Function, or a Studio Flow.
Next steps
You learned how to extend ASP.NET Core to handle uncaught exceptions and respond with the appropriate TwiML. Inside the exception handler and the exception filter, you have access to the original HTTP request, which you could use to generate a different response. So, if the preferred language is stored in the request URL or in a cookie, you could retrieve the language and respond using that language instead of hard coding it.
Here are a couple more resources to further your learning on ASP.NET Core and Twilio:
- Forward Voicemails with Transcript to your Email using C# and ASP.NET Core
- Find your U.S. Representatives and Congressional Districts with SMS and ASP.NET Core
- Respond to Twilio Webhooks using Azure Functions
We can't wait to see what you build. Let us know!
Niels Swimberghe is a Belgian American software engineer and technical content creator at Twilio. Get in touch with Niels on Twitter @RealSwimburger and follow Niels’ personal blog on .NET, Azure, and web development at swimburger.net.
Related Posts
Related Resources
Twilio Docs
From APIs to SDKs to sample apps
API reference documentation, SDKs, helper libraries, quickstarts, and tutorials for your language and platform.
Resource Center
The latest ebooks, industry reports, and webinars
Learn from customer engagement experts to improve your own communication.
Ahoy
Twilio's developer community hub
Best practices, code samples, and inspiration to build communications and digital engagement experiences.