Skip to content
This repository was archived by the owner on Jun 3, 2023. It is now read-only.

Exceptions

Jussi Saarivirta edited this page Mar 3, 2019 · 3 revisions

Enabling exception handling

Exception handling is currently available as a feature, but it's not enabled by default. This is because I think it's important to understand what the feature actually does behind the scenes before you commit yourself to using it. Enabling it requires you to add the Fody (at the time of writing, v4.0.2) Nuget package to your project and to register the provided Fody weaver via FodyWeavers.xml file to do the magic:

PM> Install-Package Fody

And then, either compile your project once (Fody will detect the HttpExtensions weaver and generate the file for you), or create the FodyWeavers.xml manually to your project root if it doesn't exist yet, with the following content:

<?xml version="1.0" encoding="utf-8" ?>
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
  <AzureFunctionsV2.HttpExtensions />
</Weavers>

This will enable the exception handling code injection as a compile step from here on.

If either step is missing, the exception handling will not be applied.

Customizing the exception handler

The job of an exception handler is to basically convert an Exception to an IActionResult, and it's provided the Exception as well as the HttpRequest to work with.

By default the DefaultHttpExceptionHandler class handles all exceptions. This class contains the basic exception handling code that returns 400, 401, 403 and 500 errors, simply handling the different HttpExtensions Exceptions and for anything else returning a generic 500 error.

The DefaultHttpExceptionHandler methods are virtual so you may override or supplement them as you like in your own implementation class.

To override the default handler you should register your implementation in the dependency injection provider, specifically to replace the default implementation. In practise you need to add the following line to your Function App startup code:

builder.Services.Replace(ServiceDescriptor.Singleton<IHttpExceptionHandler, TImplementation>())

Also see the IHttpExceptionHandler documentation.

Technical explanation

As said, I think it's important to understand how this implementation of exception handling works before starting to use it since it's technically a bit complex and generally an unordinary way of doing it. So without further ado, here's the explanation.

The Functions SDK provides Exception Filter functionality, which is meant to allow the developer to add custom exception handling for any thrown exception inside the Function code. While in theory this is exactly what we need, in practice it is not. It allows adding code to be executed upon exception but does not give any control over the return value of the Function (note: it is possible to do it in the emulator via a workaround but unfortunately this does not work in the native Azure environment).

So in the light of this there were two options:

  • Admit defeat, go home and wait for the Azure Functions team to maybe do something about it someday and accept that until then we can't have nice things.
  • Fight back, find an alternative solution, even if it's a bit harder to implement.

Option 2 felt much better, and after a poking around and looking if there were any ways of hooking into the system a somewhat reasonable solution was found. Trying to game the internal Functions runtime shenanigans would have been volatile and perhaps impossible, so the idea of IL manipulation (adding extra instructions, altering a few) actually sounded good. It was also possible to make this feature optional; if you don't like the idea of manipulating the generated IL code after compilation, you don't have to use it - just don't enable the provided Fody weaver that does the manipulation! You won't have the exception filter enabled but the rest of the framework is still all there and usable.

The solution: exception swallowing via Fody

So the dilemma without proper exception filter is this: we lose control once an exception is thrown, hence we can't throw them unless we handle them. For validation and authentication filters we can accommodate; we wrap the filters inside a try-catch and store the exceptions for a later use. However the issue of calling the handler still remains - you'd have to call it at the beginning of your Function by yourself. As for handling any other exceptions your code might throw, your custom Function code would need to be wrapped inside a try-catch - the whole idea is tedious; creating pointless wrapper code inside every function and calling the exception handling code inside every Function's catch block. This is literally the thing we are trying to avoid at all costs! This is why Exception Filters exist! But we can't use the provided one because we can't control the Function return value with it - so what to do?

Enter Fody: a painless way to run IL code manipulation after compilation. So instead of doing the aforementioned hassle yourself you let a compiler step do it for you. This takes care of the problem exactly the same way an Exception Filter would, only it goes to insert and change a few lines of the compiler generated Function IL code once it gets compiled.

Here's how it works:

  • Authentication and validation filters work normally, with the exception that in the case of the assembly being processed by Fody, we do not throw the exceptions and instead store them and stop running the filter.
  • We inject a method call to the very beginning of the Function that checks if there's a stored exception by one of the filters; if there is, the method rethrows it.
  • The entire Function code is wrapped around a try-catch block. Once any code inside it throws, the catch block activates and inside it we've replaced the standard compiler generated SetException() routine with a call to our custom exception handler method, followed by a call to SetResult() method which means that with the exception handler we convert the exception to an IActionResult and then set that as the result.
  • The method thinks no exception was ever thrown and happily returns the value the exception handling code had set.

This is basically what happens code-wise:

This original code...

[FunctionName("AuthTest1")]
[HttpAuthorize(Scheme.Jwt)]
public static async Task<IActionResult> AuthTest1(
    [HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequest req,
    [HttpToken]HttpUser user,
    ILogger log)
{
    // Your code.
    return new OkResult();
    // End your code.
}

...basically turns into this:

[FunctionName("AuthTest1")]
[HttpAuthorize(Scheme.Jwt)]
public static async Task<IActionResult> AuthTest1(
    [HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequest req,
    [HttpToken]HttpUser user,
    ILogger log)
{
    try {
        ILFunctionExceptionHandler.RethrowStoredException(req);
        // Your code.
        return new OkResult();
        // End your code.
    }
    catch(Exception e)
    {
        return ILFunctionExceptionHandler.HandleExceptionAndReturnResult(e, req);
    }
}

When you compile the code, you'll bee seeing output such as:

2>Target FodyTarget:
2>    Fody: Fody (version 4.0.2.0 @ file:///C:/Users/x/.nuget/packages/fody/4.0.2/netclassictask/Fody.dll) Executing
2>      Fody/AzureFunctionsV2.HttpExtensions:   Executing Weaver
2>      Fody/AzureFunctionsV2.HttpExtensions:   Will attempt to find any Azure Functions (static async methods in static classes with 'FunctionNameAttribute'), and then augment them with IL instructions that call exception handling code if an exception gets thrown.
2>      Fody/AzureFunctionsV2.HttpExtensions:   Found Function Hello
2>      Fody/AzureFunctionsV2.HttpExtensions:     - Corresponding compiler generated state machine: HttpExtensionsExamples.HttpExtensionFunctions/<Hello>d__2
2>      Fody/AzureFunctionsV2.HttpExtensions:   Found Function GetSecrets
2>      Fody/AzureFunctionsV2.HttpExtensions:     - Corresponding compiler generated state machine: HttpExtensionsExamples.HttpExtensionFunctions/<GetSecrets>d__3
2>      Fody/AzureFunctionsV2.HttpExtensions:   Found Function MyWebhook
2>      Fody/AzureFunctionsV2.HttpExtensions:     - Corresponding compiler generated state machine: HttpExtensionsExamples.HttpExtensionFunctions/<MyWebhook>d__4
2>      Fody/AzureFunctionsV2.HttpExtensions:   Found Function UserData
2>      Fody/AzureFunctionsV2.HttpExtensions:     - Corresponding compiler generated state machine: HttpExtensionsExamples.HttpExtensionFunctions/<UserData>d__5
2>      Fody/AzureFunctionsV2.HttpExtensions:   Found Function Swagger
2>      Fody/AzureFunctionsV2.HttpExtensions:     - Corresponding compiler generated state machine: HttpExtensionsExamples.HttpExtensionFunctions/<Swagger>d__6
2>      Fody/AzureFunctionsV2.HttpExtensions:   Discovery completed.
2>      Fody/AzureFunctionsV2.HttpExtensions:   Augmenting function 'Hello' with exception handling logic.
2>      Fody/AzureFunctionsV2.HttpExtensions:   Augmenting function 'GetSecrets' with exception handling logic.
2>      Fody/AzureFunctionsV2.HttpExtensions:   Augmenting function 'MyWebhook' with exception handling logic.
2>      Fody/AzureFunctionsV2.HttpExtensions:   Augmenting function 'UserData' with exception handling logic.
2>      Fody/AzureFunctionsV2.HttpExtensions:   Augmenting function 'Swagger' with exception handling logic.
2>      Fody/AzureFunctionsV2.HttpExtensions:   All methods processed.
2>    Fody:   Finished Fody 115ms.

This can be useful for checking that the methods have been successfully processed.

This explanation of course, is still a simplified version; in reality there's a bit more to it as we're touching the compiler generated code. The description of the AsyncErrorHandler Fody AddIn explains it quite well. The process is almost the same, except we inject method calls to the beginning of a Function and we replace the SetException() call with the exception handler call and a SetResult() call after that.

Clone this wiki locally