Azure Functions support custom handlers written in any programming language . Your handler acts as a web server and receives input bindings forwarded from the Azure functions host. And your handler responses are received by the host and forwarded to the endpoints defined in the bindings file function.json
.
Your endpoints should normally send a 20X
even for non-http bindings, such as queues. Your response payload should look like this as per the docs:
{
"Outputs": {
"res": {
"body": "Message enqueued"
},
"myQueueOutput": [
"queue message 1",
"queue message 2"
]
},
"Logs": [
"Log message 1",
"Log message 2"
],
"ReturnValue": "{\"hello\":\"world\"}"
}
This all works fine unless there's an error. If your error is unhandled then there's no logs because the response was not sent. However, if you want to control your error response you might wish to set the http response code to 500
or 40X
and still include the logs. For example:
{
"Outputs": {},
"Logs": [
"an error",
"it is bad"
]
}
But this does not work. Sending a non success response causes the Azure Functions host to reject the whole response, including the logs. The Azure Functions invocation logs looks something like this:
14/02/2025, 09:51:09 Information
Executing 'Functions.v1-topic-actionstep-datalake-tasks' (Reason='(null)', Id=5d166d54-ce1a-4263-a501-db025fc4caff)
14/02/2025, 09:51:09 Information
Trigger Details: MessageId: 7afcc1fa627f42dab325f5ebfcdd27a5, SequenceNumber: 2, DeliveryCount: 2, EnqueuedTimeUtc: 2025-02-13T22:51:08.6120000+00:00, LockedUntilUtc: 2025-02-13T22:52:09.0340000+00:00, SessionId: (null)
14/02/2025, 09:51:09 Error
Response status code does not indicate success: 500 (Internal Server Error).
14/02/2025, 09:51:09 Error
Executed 'Functions.v1-topic-actionstep-datalake-tasks' (Failed, Id=5d166d54-ce1a-4263-a501-db025fc4caff, Duration=64ms)
14/02/2025, 09:51:09 Error
Response status code does not indicate success: 500 (Internal Server Error).
This is unfortunate because a non-success response causes retries, which is a good thing. But without app-level logging, one is in the dark as to what the problem is.
Does anyone know how to return a controlled error response to the Azure Function host so that retries are used but the invocation log still contains your app-level logs? Basically how it normally works when using the SDKs.
Azure Functions support custom handlers written in any programming language https://learn.microsoft/en-gb/azure/azure-functions/functions-custom-handlers. Your handler acts as a web server and receives input bindings forwarded from the Azure functions host. And your handler responses are received by the host and forwarded to the endpoints defined in the bindings file function.json
.
Your endpoints should normally send a 20X
even for non-http bindings, such as queues. Your response payload should look like this as per the docs:
{
"Outputs": {
"res": {
"body": "Message enqueued"
},
"myQueueOutput": [
"queue message 1",
"queue message 2"
]
},
"Logs": [
"Log message 1",
"Log message 2"
],
"ReturnValue": "{\"hello\":\"world\"}"
}
This all works fine unless there's an error. If your error is unhandled then there's no logs because the response was not sent. However, if you want to control your error response you might wish to set the http response code to 500
or 40X
and still include the logs. For example:
{
"Outputs": {},
"Logs": [
"an error",
"it is bad"
]
}
But this does not work. Sending a non success response causes the Azure Functions host to reject the whole response, including the logs. The Azure Functions invocation logs looks something like this:
14/02/2025, 09:51:09 Information
Executing 'Functions.v1-topic-actionstep-datalake-tasks' (Reason='(null)', Id=5d166d54-ce1a-4263-a501-db025fc4caff)
14/02/2025, 09:51:09 Information
Trigger Details: MessageId: 7afcc1fa627f42dab325f5ebfcdd27a5, SequenceNumber: 2, DeliveryCount: 2, EnqueuedTimeUtc: 2025-02-13T22:51:08.6120000+00:00, LockedUntilUtc: 2025-02-13T22:52:09.0340000+00:00, SessionId: (null)
14/02/2025, 09:51:09 Error
Response status code does not indicate success: 500 (Internal Server Error).
14/02/2025, 09:51:09 Error
Executed 'Functions.v1-topic-actionstep-datalake-tasks' (Failed, Id=5d166d54-ce1a-4263-a501-db025fc4caff, Duration=64ms)
14/02/2025, 09:51:09 Error
Response status code does not indicate success: 500 (Internal Server Error).
This is unfortunate because a non-success response causes retries, which is a good thing. But without app-level logging, one is in the dark as to what the problem is.
Does anyone know how to return a controlled error response to the Azure Function host so that retries are used but the invocation log still contains your app-level logs? Basically how it normally works when using the SDKs.
Share Improve this question asked Feb 14 at 1:13 Neil DobsonNeil Dobson 1,2591 gold badge12 silver badges25 bronze badges 1- Did you check the logs in application insights? – Pravallika KV Commented Feb 18 at 12:03
1 Answer
Reset to default 0As per my observation, in case of 500
errors the detailed error can be seen under Invocation logs.
Configure Application Insights to ensure function logs are being captured regardless of the function status.
Below is the code I have used to handle the errors in basic GO
function code with bindings, this helps to capture the logs and retry the failed invocations:
Code Snippet:
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
)
type InvokeRequest struct {
Data map[string]json.RawMessage
Metadata map[string]interface{}
}
type InvokeResponse struct {
Outputs map[string]interface{} `json:"Outputs"`
Logs []string `json:"Logs"`
ReturnValue interface{} `json:"ReturnValue"`
}
func orderHandler(w http.ResponseWriter, r *http.Request) {
var invokeRequest InvokeRequest
d := json.NewDecoder(r.Body)
err := d.Decode(&invokeRequest)
if err != nil {
logs := []string{"Failed to decode request", err.Error()}
invokeResponse := InvokeResponse{
Outputs: make(map[string]interface{}),
Logs: logs,
ReturnValue: "{\"error\":\"Failed to decode request\"}",
}
responseJson, _ := json.Marshal(invokeResponse)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(responseJson)
return
}
var reqData map[string]interface{}
err = json.Unmarshal(invokeRequest.Data["req"], &reqData)
if err != nil {
logs := []string{"Failed to process request data", err.Error()}
invokeResponse := InvokeResponse{
Outputs: make(map[string]interface{}),
Logs: logs,
ReturnValue: "{\"error\":\"Failed to process request data\"}",
}
responseJson, _ := json.Marshal(invokeResponse)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(responseJson)
return
}
// If everything is fine, return the success response
outputs := make(map[string]interface{})
outputs["message"] = reqData["Body"]
resData := make(map[string]interface{})
resData["body"] = "Order enqueued"
outputs["res"] = resData
invokeResponse := InvokeResponse{
Outputs: outputs,
Logs: nil,
ReturnValue: "{\"status\":\"success\"}",
}
responseJson, _ := json.Marshal(invokeResponse)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(responseJson)
}
func main() {
customHandlerPort, exists := os.LookupEnv("FUNCTIONS_CUSTOMHANDLER_PORT")
if !exists {
customHandlerPort = "8080"
}
mux := http.NewServeMux()
mux.HandleFunc("/order", orderHandler)
fmt.Println("Go server Listening on:", customHandlerPort)
log.Fatal(http.ListenAndServe(":"+customHandlerPort, mux))
}
host.json:
{
"version": "2.0",
"customHandler": {
"description": {
"defaultExecutablePath": "handler.exe"
}
},
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[4.*, 5.0.0)"
}
}
function.json:
{
"bindings": [
{
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": ["get","post"]
},
{
"type": "http",
"direction": "out",
"name": "res"
},
{
"type": "queue",
"name": "message",
"direction": "out",
"queueName": "orders",
"connection": "AzureWebJobsStorage"
}
]
}
Output:**
2025-02-18T13:35:20Z [Verbose] AuthenticationScheme: WebJobsAuthLevel was successfully authenticated.
2025-02-18T13:35:20Z [Verbose] Authorization was successful.
2025-02-18T13:35:20Z [Information] Executing 'Functions.order' (Reason='This function was programmatically called via the host APIs.', Id=56cb6c2d-cbf4-4cae-8dd2-a8b2e7f40d13)
2025-02-18T13:35:20Z [Verbose] Sending invocation id: '56cb6c2d-cbf4-4cae-8dd2-a8b2e7f40d13
2025-02-18T13:35:20Z [Verbose] Sending invocation for function: 'order' invocationId: '56cb6c2d-cbf4-4cae-8dd2-a8b2e7f40d13'
2025-02-18T13:35:21Z [Verbose] Received invocation response for function: 'order' invocationId: '56cb6c2d-cbf4-4cae-8dd2-a8b2e7f40d13'
2025-02-18T13:35:21Z [Error] Executed 'Functions.order' (Failed, Id=56cb6c2d-cbf4-4cae-8dd2-a8b2e7f40d13, Duration=231ms)
Invocation Logs:
Expand the error log to see the detailed error of 500
response under FunctionApp's Invocations
: