HTTP Function Output Binding Gotchas

Posted by

In theory, there is no difference between theory and practice. In practice, however, there is.

someone, at some point

The above quote is attributed to any number of different people, depending on who you ask. While it’s unclear who first said it, I’ve been dealing with the reality of this quote over the last few days, wrestling between what I thought was possible and what actually is.

The premise of what I’m trying to do is straight-forward: receive a request by HTTP Trigger function, validate the request body, and send a message to a queue if it’s valid. If any of the validations fail, I want to return an error response. And it’s that last bit where I got stuck.

You typically see this pattern implemented when a request is going to take longer than a few seconds to complete – making it impractical to keep the HTTP connection open that long. Instead, a message is enqueued for a background worker, and the response tells the client where it can get progress status on the job.

The code in this post builds on a previous post that focused on validating the JSON body of HTTP-based Azure Functions. It could be useful to review it before going further.

The Failed Attempt

The code that I planned to implement was straight to the point:

  1. The function receives an HTTP POST request that contains a JSON message body.
  2. The function validates the body parameters and returns a 400 Bad Request if any of the parameters are invalid.
  3. The function returns a 200 Ok when the body parameters are valid and enqueues a message for further processing by another function.

I used a feature of Azure Functions called output bindings to enqueue the message. Input and output bindings help you avoid writing boiler-plate code to read or write from other Azure services. An attribute here, a return statement there, and you’re interacting with things like blobs, queues, and Service Bus with next to no code.

Conceptually, it all made sense, so I started coding. It took no more than 5 minutes to put together, and I ended up with the following:

[return: Queue("flightscheduled")]
[FunctionName("Scheduler")]
public static async Task<IActionResult&gt; Run(
    [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req,
    [Blob("schemas/SchedulerSchema.json", FileAccess.Read)] Stream validationSchema,
    ILogger log)
{

    try
    {
        string schemaJson = await new StreamReader(validationSchema).ReadToEndAsync();
        JSchema parsedSchema = JSchema.Parse(schemaJson);

        string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
        var parsedRequest = JObject.Parse(requestBody);

        IList<string&gt; errorMessages = new List<string&gt;();
        bool validRequest = parsedRequest.IsValid(parsedSchema, out errorMessages);

        if (!validRequest)
        {
            return new BadRequestObjectResult(errorMessages);
        }

        var flight = parsedRequest.ToObject<Flight&gt;();

        return (ActionResult)new OkObjectResult(flight);
    }
    catch (Exception ex)
    {
        log.LogError(ex.Message);

        return (ActionResult)new InternalServerErrorObjectResult();
    }

}

With that out of the way, it was time to do some initial acceptance testing. The happy path seemed to be working as expected. But I quickly noticed a problem when the validations failed: instead of a 400 Bad Request, the client was receiving a 200 Ok.

Even worse, a message was being written to the queue even when the validations failed. There was a clue to this strange behaviour lurking in the queue message’s body:

{
  "Value": [
    "Required properties are missing from object: departing. Path '', line 1, position 1."
  ],
  "Formatters": [],
  "ContentTypes": [],
  "StatusCode": 400,
  "$AzureWebJobsParentId": "95dd34ad-d544-4294-ab79-355c6b76ebd2"
}

This was the HTTP response I was expecting to see in Postman, where I made the call from! As I thought about it more, what I was seeing began to make sense.

An HTTP Trigger needs to return a response to the client. But by specifying a Queue output binding, the runtime also needs to return something to the queue. It seems like the output binding trumped the HTTP binding, causing the return value, in this case an ActionResult, to be written to the queue instead. As for the HTTP response, it always returned a 200 Ok, no matter what I did.

Clearly, this is not what I wanted the function to do.

The Successful Alternative

Old habits die hard, and I was tempted to solve the problem as I would have in a traditional .NET application: create a QueueClient class to abstract away the details of writing to the queue, passing in the message in question.

A bit of googling pointed me to a far easier solution. It takes advantage of a WebJobs SDK feature, upon which Azure Functions is built.

The ICollector interface is exactly what it sounds like: a collector of objects, which are then written to the specified output binding. It makes it trivial to add as many messages as neccessary to a queue.

Here’s what the modified code looks like:

[FunctionName("Scheduler")]
public static async Task<IActionResult&gt; Run(
    [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req,
    [Blob("schemas/SchedulerSchema.json", FileAccess.Read)] Stream validationSchema,
    [Queue("flightscheduled")]ICollector<Flight&gt; queueCollector,
    ILogger log)
{
    try
    {
        string schemaJson = await new StreamReader(validationSchema).ReadToEndAsync();
        JSchema parsedSchema = JSchema.Parse(schemaJson);

        string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
        var parsedRequest = JObject.Parse(requestBody);

        IList<string&gt; errorMessages = new List<string&gt;();
        bool validRequest = parsedRequest.IsValid(parsedSchema, out errorMessages);

        if (!validRequest)
        {
            return new BadRequestObjectResult(errorMessages);
        }

        var flight = parsedRequest.ToObject<Flight&gt;();

        queueCollector.Add(flight);

        return (ActionResult)new OkObjectResult(flight);
    }
    catch (Exception ex)
    {
        log.LogError(ex.Message);

        return (ActionResult)new InternalServerErrorObjectResult();
    }

}

With this tiny refactoring in place, the queue message is only written when the body parameters are valid, and the correct HTTP response is sent to the client. VoilĂ !

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s