How Lab Deployments Work Part 3 - Durable Starter Function
Now let me acquaint you with how a POST request at the front door to the Azure Durable Functions workflow. Below I’ll focus on the Durable Functions Starter function and move onto the app’s orchestrator and activity functions in the next post. The role of the starter function is to accept the POST request and trigger the rest of the workflow. In order to do this successfully, it must:
- Read the POST request it receives with its JSON payload
- Validate that the data it receives is in the correct format
- Pass that data to and activate the orchestrator
- Create status URLs that will act as query endpoints for checking on the status of the function apps orchestration
- Return a 202 accepted response to the client with the query URLs
- Perform various error handling So although the starter function, in essence, only acts as a trigger for the rest of the orchestration, it must perform many tasks in order to fulfil that role.
Starter Function Operations
Request Body Parsing
An HTTP POST request lands at the Azure Function endpoint something like this:
Content-Type: application/json{ "subscriptionId": "12345678-1234-1234-1234-123456789012", "templateUrl": "https://example.com/template.json", "roleDefinitionName": "Contributor"}However, that request body can arrive in many forms depending on how it was sent. For example, for testing purposes, I will usually use cURL, PowerShell’s invoke-webrequest or the Azure portal’s test panel. However, in production, the website’s own endpoints will send the request with JavaScript’s fetch command. This means that the payload could arrive already as a PSCustomObject or as a JSON string or even other formats which I have used in the past (direct Bicep file uploads would need to be parsed as streams for example). For safety, a number of possible formats are expected so therefore parsed, as per the below examples.
Body Type Detection - PSCustomObject or IDictionary
This is the simplest format to deal with as it is essentially ready to pass to the rest of the workflow. In the testing phase, this gave me great relief as I was using PowerShell to send my invocations. Essentially, payload is already parsed before it reaches the starter so no further parsing is even required in this circumstance.
if ($Request.Body -is [System.Collections.IDictionary] -or $Request.Body -is [PSCustomObject]) { Write-DetailedLog -Message "Request body is already an object" -Level "DEBUG" $inputData = $Request.Body}Body Type Detection - JSON String Processing
If the body is a JSON string, it will be parsed as a PSObject by converting from JSON with ConvertFrom-Json. This is so we can access its properties with dot notation (e.g. input.subscriptionID). However, during this process PowerShell will attempt to infer the type of each value; meaning that the subscriptionID may become an integer if the JSON value is numeric, and templateUrl will be a string but could cause issues if not handled as a string in downstream functions (such as serialization or type mismatch errors). To guarantee type consistency, we need to be able to control how the types are parsed. We do this by then converting the PSCustomObject to a hashtable and explicitly converting each property value to a string (performing type coercion).
elseif ($Request.Body -is [string] -and $Request.Body.Trim().StartsWith('{')) { # Parse as regular PSObject first $jsonObject = $Request.Body | ConvertFrom-Json
# Convert to hashtable, ensuring string properties remain as strings $inputData = @{} foreach ($prop in $jsonObject.PSObject.Properties) { # Ensure all URL values and IDs are explicitly converted to strings if ($prop.Name -eq "subscriptionId" -or $prop.Name -eq "templateUrl" -or $prop.Name -eq "roleDefinitionName") { $inputData[$prop.Name] = "$($prop.Value)"Body Type Detection - Stream Handling
For reference, stream-based requests are handled as such:
elseif ($Request.Body -is [System.IO.Stream]) { # Reset stream position if possible if ($Request.Body.CanSeek) { $Request.Body.Position = 0 }
# Read the stream $reader = New-Object System.IO.StreamReader($Request.Body) $bodyContent = $reader.ReadToEnd()Required Parameter Validation
Certain parameters are essential. This excerpt focuses on the subscription ID, but there are several parameters that must be included in the request such as the RBAC permission level, template URL and more. Note the use of the Push-OutputBinding command. This is part of a set of specialised PowerShell commands designed for Azure Durable Functions runtime. This provides the HTTP response back to the client - in this case an error response stating it received a bad request.
if (-not $inputData.subscriptionId) { Write-DetailedLog -Message "Missing required parameter: subscriptionId" -Level "WARNING"
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::BadRequest Headers = @{ "Content-Type" = "application/json" } Body = ConvertTo-Json @{ error = "subscriptionId is required" message = "Please provide a valid Azure subscription ID to deploy the lab environment" } }) return}The Orchestrator Is Activated
The starter function can now fulfil its true task which is to launch the orchestrator. A log is written here to note that orchestration is starting which helps a lot with debugging. Next an instanceID is generated by invoking the orchestrator function by it’s name and providing the parsed and validated input data. The returned $instanceId uniquely identifies this orchestration instance, which can be used to track its status or interact with it later.
$orchestratorFunctionName = "`Lab_Orchestrator`"
Write-DetailedLog -Message "Starting orchestration" -Data @{ orchestratorFunction = $orchestratorFunctionName inputDataKeys = ($inputData.Keys -join ", ")}
$instanceId = Start-NewOrchestration -FunctionName $orchestratorFunctionName -Input $inputDataStatus URL Creation
The function creates management URLs for the orchestration using the InstanceId generated in the preceding code block. There are three endpoints generated here; statusQueryGetUri is for checking the progress and final result of the workflow; sendEventPostUri is for sending external events to the orchestration, for situations where external input is needed; terminatePostUri is for terminating the current orchestration, useful for aborting the workflow. Note that defining these URLs is part of the custom function CreateCheckStatusResponse.
$statusQueryGetUri = "$baseUrl/runtime/webhooks/durabletask/instances/$InstanceId`?taskHub=$taskhubName"$sendEventPostUri = "$baseUrl/runtime/webhooks/durabletask/instances/$InstanceId/raiseEvent/{eventName}?taskHub=$taskhubName"$terminatePostUri = "$baseUrl/runtime/webhooks/durabletask/instances/$InstanceId/terminate?reason={text}&taskHub=$taskhubName"Structured Logging Throughout
You may have noticed the Write-DetailedLog command used in the above code snippets. These refer to an PowerShell function which is defined at the beginning of the Starter Function. Every step is logged with detailed context. This is only a small excerpt from the code but gives you an idea of what is logged:
function Write-DetailedLog { $logEntry = @{ timestamp = $timestamp level = $Level message = $Message functionName = $TriggerMetadata.FunctionName invocationId = $TriggerMetadata.InvocationId }}Response to client
Finally, the custom function CreateCheckStatusResponse generates the URLs and defines the $response, returning the data to the client with the Push-OutputBinding cmdlet.
# Create a response with a 202 Accepted status and orchestration status URLWrite-DetailedLog -Message "Creating check status response for orchestration" -Level "DEBUG"$response = CreateCheckStatusResponse -Request $Request -InstanceId $instanceId -InputData $inputData
Write-DetailedLog -Message "HTTP Starter function completed successfully" -Data @{ instanceId = $instanceId statusCode = 202}
# Return the responsePush-OutputBinding -Name Response -Value $responseThe End of The Starter
That about covers the role of the starter function in the workflow of the Durable Functions app. These code snippets are simplified and much of the code is left out due to its length, but the spirit of the starter is captured here fully. If you read the Microsoft documentation on the starter function (sometimes called client function) you may be deceived into thinking that this function would be the simplest part of the Function App to implement. In my experience, it was probably one of the hardest. So much validation and logging was needed along the way that it ended up ballooning to 500 lines of code at one point. The custom functions for logging and generating the status response helped, but a lot of my personal confusion came from dealing with data types in PowerShell that I just was not that familiar with. Serialisation errors were in abundance due to how PowerShell parsed the JSON input values and that required a lot of research on how PSCustomObjects and hashtables worked and how to manipulate them in PowerShell. It was not fun, but it was a learning opportunity and a good way to learn about data types in PowerShell which I had not thought enough about until that time. The orchestrator’s job is essentially to pass data between the functions constantly, which was very painful given the learning curve I was on. Join me in the next post wherein I detail the torture of implementing the orchestrator!