I recently found myself wanting to standardize the error response from some HTTP-based Azure Functions. Depending on the error, the functions returned a 4xx or 5xx status code, with a response body that was at times a JSON object, other times a basic string. It was all over the place.
That’s where RFC7807, also known as http-problem, or problem details, comes in to play. Its goal is to standardize the body of an error response from an HTTP endpoint. The contract has five fields:
- Status: The status code of the response.
- Type: A URI-formed error identifier that uniquely identifies the error.
- Title: A short description of the error that could help developers understand what’s happened.
- Detail: A more verbose description of the error, with additional details that could help in debugging.
- Instance: The specific occurence of the problem.
Functions are meant to be short and sweet, so I wanted the approach to formatting problem details to be just as straightforward. Ultimately, the approach relies on three classes to get it done.
Dealing With Problem Details
The first class that is needed is ProblemDetails. It’s provided in the Microsoft.AspNetCore.Mvc namespace, requiring a dependency on ASP.NET Core. You can create your own class for it within your functions project if you’d rather avoid taking on such a large dependency.
public class ProblemDetails
{
public string Type { get; set; }
public string Title { get; set; }
public int? Status { get; set; }
public string Detail { get; set; }
public string Instance { get; set; }
}
The next step is to create a class that inherits from ObjectResult that accepts an instance of the ProblemDetails. ProblemObjectResult is what will be returned by the functions, serializing the ProblemDetails object into the response body.
public class ProblemObjectResult : ObjectResult
{
public ProblemObjectResult(ProblemDetails value) : base(value)
{
StatusCode = (int)value.Status;
}
}
Finally, the ErrorResponse class has a series of static methods to create common error responses:
public class ErrorResponse
{
public static ObjectResult CreateResponse(
HttpStatusCode statusCode,
string type,
string title = "",
string detail = "",
string instance = "")
{
var problem = new ProblemDetails()
{
Status = (int)statusCode,
Type = type,
Title = title,
Instance = instance,
Detail = detail
};
return new ProblemObjectResult(problem, statusCode);
}
public static ObjectResult BadRequest(
string type,
string title = "",
string detail = "",
string instance = "")
{
return CreateResponse(HttpStatusCode.BadRequest, type, title, detail, instance);
}
public static ObjectResult InternalServerError(
string type = "unexpected-error",
string title = "",
string detail = "",
string instance = "")
{
return CreateResponse(HttpStatusCode.InternalServerError, type, title, detail, instance);
}
public static ObjectResult NotFound(
string type,
string title = "",
string detail = "",
string instance = "")
{
return CreateResponse(HttpStatusCode.NotFound, type, title, detail, instance);
}
}
With that last piece in place, it’s now a matter of calling the appropriate method from within the functions. Here are a few sample calls from my functions project:
if (!validFlightId)
{
return ErrorResponse.BadRequest(type: "/invalid-flightid", instance: $"/flight/{unsanitizedflightId}");
}
...
return ErrorResponse.NotFound(type: "/invalid-flightid", instance: $"/flight/{unsanitizedflightId}");
...
return ErrorResponse.BadRequest(type: "/invalid-request", detail: errorMessages.Aggregate((i, j) => i + ", " + j));
The response that will be returned in the case of a 4xx or 5xx error will resemble the following:
400 BAD REQUEST
{
"type": "invalid-schema",
"title": "not-specified",
"status": 400,
"detail": "Required properties are missing from object: departing. Path '', line 1, position 1.",
"instance": "not-specified"
}
Possible Improvements
Right now, every property of the ProblemDetails object is serialized in the response. Ideally, I’d like to serialize only the properties that are set, but functions doesn’t expose a way to modify its JSON serializer settings.
Overall, I’m happy with the above solution. It’s lightweight to implement, and it provides a standard response for any failed HTTP requests. The next step would be to move it out into a reusable package that can be used across function projects.