HTTP Status Codes and Problem+JSON
We should never use any 1xx or 3xx status codes without a specific and planned reason. Those codes are almost entirely about HTTP protocol details and will be used by the system. They should not be needed for the microservice responses.
Success
When our service responds successfully to a request, we should use one of these:
2xx:
- For most successful responses use 200 “OK”.
- If successfully create a new resource we should use 201 “Created”.
Failure
When our service has trouble responding to a request, we should use one of these:
4xx:
- If a request has a JSON syntax or validation error use 400 “Bad Request”. I think this is the default behavior for fastify’s OpenAPI validator. This is unlikely to give a different result if we submit the same request again.
- If a secure endpoint request is missing an auth token use 401 “Unauthorized”. This will probably get handled by the APIM and it will probably send whatever code it normally uses. 401 usually implies that trying again might work if the request contains new credentials, or the account administrator updates the use information in a way that makes the failed credentials start working. This latter can happen for example if the administrator helps by creating or unlocking a user account.
- If a secure endpoint request has a token but not permission use 403 “Forbidden”. The most likely reason for this one is that the RBAC roles do not provide the needed permissions for the request. This is not likely to be resolved without changes to the roles assigned to the user OR permissions assigned to the roles.
- If a resource request by ID does not find an item use 404 “Not Found”. If we get a request to list resources and we find none that match the request, we should not return 404 but instead 200 with an empty array in the response body. It is possible that a 404 could be resolved by a repeated request IF the resource is created in the meantime.
- If a request has valid JSON but cannot be fulfilled use 422 “Unprocessable Entity”. This is different from 400 because we had a request that was valid based on the schema but is somehow not able to be handled. Possible causes include requesting to change a read-only value, attempt to remove a value that is not removable, trying to use a date in the past when it can only be in the future or simply trying to perform an action that does not make sense at this time. Because some of these reasons are time or state sensitive, they may be successful without any other changes beside sending again at a later time.
5xx:
- For any unexpected error on the server use 500 “Internal Server Error”. This is most often not going to succeed for the same request at a later time. It might, but it is most likely going to require a redeployment of the service or some changes to the request at least.
- If the server is temporarily offline use 503 “Service Unavailable”. As mentioned, this is a temporary condition so trying later makes sense here.
We use application/problem+json (RFC9457)
When one of our microservice REST API endpoints has trouble servicing a request we will send problem details in a documented and standardized format in addition to using an appropriate HTTP error status code. The specification, RFC9457, defines the format of the message we will use to communicate details about the problem. This response format has a content type of application/problem+json which should also be set in the response header to clarify that the error details are in the problem+json format. The problem+json is defines a JSON response body with properties for each of the things we would like in the error responses.
We specifically want to send these details in for errors encountered by the microservice while processing the request:
- An error code that categorizes the type of problem
- A human readable general description of the problem
- A human readable description of the specific problem with details
- A reference to the specific instance of the problem
For error codes, problem+json specifies that we should use a URI value in the “type” property of the response JSON. Examples of these often look like an HTTP URL. Each URI should start with https://claconnect.com/platform/autoextract/problems/ and end with an error code name using “kebab-case”. This could be something like “uploaded-file-too-large”
The human readable, general description should be related to the “type” values in a 1-to-1 relationship. So for each URI we should have a simple description to put in the “title” property of the JSON message. An example “title” could be something like “The uploaded file was too large to accept”.
The description with specific details should be assigned to a property named “detail”. It is different than the “title” in that it should give more details about this specific occurrence of the problem. It could be different for each problem occurrence of the same “type”. An example might look like The file named "my_really_cool_file.pdf" was 2Gb but the system can only accept file of 1Gb or less in length.
And finally, we want to identify the occurrence so that we can look for additional information in the logs or data to help understand the problem if it is unexpected or difficult to explain. Our platform creates a unique ID for each file that enters the system so that we can easily say which file was involved in the problem. We also have a trace or span ID from telemetry that can be appended to that which tells the system which user action was originally the cause of the problem. For further identification purposes the system should also include a timestamp indicating the system time when the problem was encountered as milliseconds since the epoch using GMT times. We will concatenate those things into a path-like string with the form: /platform/autoextract/<fileId>/<correlationId>/<epochTimestamp>
A full example of what a problem message should look like would be:
Requests should Accept: application/json, application/problem+json to be as correct as possible. Requests will probably work correctly with only Accept: application/json in most situations and with most clients. Endpoints are even likely to work without an Accept header at all. We recommend using the full Accept header as shown above.