Docs / ArchitectureKit 2.0 / Guides / Mapping Errors to Status Codes

Mapping Errors to Status Codes

Every error has a matching HTTP status code, which the httpapi package derives from the kind of the error.

To get the status code that matches an error, call the StatusFor function:

status := httpapi.StatusFor(err)

It checks the categories in this order:

Error Status code
nil 200 OK
httpapi.ErrUnauthorized 401 Unauthorized
httpapi.ErrForbidden 403 Forbidden
httpapi.ErrTooLarge 413 Request Entity Too Large
httpapi.ErrUnsupportedMediaType 415 Unsupported Media Type
httpapi.ErrMalformed 400 Bad Request
httpapi.ErrNotFound, query.ErrNoItems 404 Not Found
architecturekit.ErrDomain 422 Unprocessable Entity
architecturekit.ErrOutcomeUnknown 500 Internal Server Error
architecturekit.ErrNotCaughtUp 500 Internal Server Error
architecturekit.ErrConflict 409 Conflict
architecturekit.ErrTransient 503 Service Unavailable
context.Canceled 499 Client Closed Request
context.DeadlineExceeded 503 Service Unavailable
architecturekit.ErrPermanent 500 Internal Server Error
architecturekit.ErrNotARevision, architecturekit.ErrEmptyRange 400 Bad Request
any other error 500 Internal Server Error

Note that context.Canceled means that the caller went away before it got an answer. HTTP has no status code for that, so 499 is the one that nginx introduced, and which logs and metrics commonly know. Since nothing failed, it is not logged. Its message is request canceled, because the error may name internals, such as the subject that was read.

Note that ErrOutcomeUnknown means that a write may or may not have stored its events (see Handling Errors). So it comes before ErrConflict, ErrTransient, and the end of the context, and an error that wraps one of them as well is answered with 500 Internal Server Error all the same: trying again, which 409 and 503 invite, may store the events twice, and 499 is not logged. Its message is outcome unknown: the request may have succeeded, so that the caller does not simply try again, while the error, which may name internals, such as the subject, is logged at level Error. Only if it comes from the function that returns a command, from the one that returns a query, or from the one that determines the user, the command or the query has not run, so its message is internal server error, as for any other internal failure. Handle, Ask, and UserOf return such an error marked for that, with the same text, which errors.Is and errors.As see through.

Note that ErrNotCaughtUp means that a write has succeeded, but that a view did not catch up with it (see Waiting for Written Events). Trying again would store the events twice, so it comes right after ErrOutcomeUnknown, and is answered with 500 Internal Server Error the same way, also if it wraps ErrConflict, ErrTransient, ErrPermanent, or the end of the context as well, as it does when one of them kept the view from catching up. Its message is the request succeeded, but its result is not visible yet, while the error is logged at level Error. A handler that waits after a command usually answers it with success, though, since the write has succeeded. Only if it comes from the function that returns a command, from the one that returns a query, or from the one that determines the user, the command or the query has not run, so its message is internal server error, and Handle, Ask, and UserOf return it marked for that, as they do for ErrOutcomeUnknown.

Note that ErrNotARevision means that a value that was handed over is not a revision, such as a bound of Read, the event ID of OnEventID, a value for CompareRevisions or ParseRevision, or the revision a view is to wait for, which usually comes from the request. So it is answered with 400 Bad Request and the error as the message, like any other mistake in the request, also if it is the function that answers a query that finds it. An ID that the server stored or made itself and that is broken is a failure of the server, though, so an error of the category ErrPermanent is answered with 500 Internal Server Error, even if it wraps ErrNotARevision as well. Both come last, so that an error that belongs to another category as well keeps its status code.

Note that ErrEmptyRange means that the bounds of Read leave no room for any event, which usually come from the request as well. So it is answered the same way, with 400 Bad Request and the error as the message, such as empty range: no event can lie before "0", unless the error belongs to the category ErrPermanent as well (see Reading Events).

Note that an error of the function that returns a command, of the one that returns a query, or of the one that determines the user keeps its status code only if it has one of its own, belongs to the category ErrPermanent, or wraps ErrOutcomeUnknown or ErrNotCaughtUp. Any other error is answered with 400 Bad Request for the first two, and with 401 Unauthorized for the last (see Authorizing Commands and Setting Up an HTTP API).

Note that a panic while a route handles a request is answered with 500 Internal Server Error as well (see Handling Commands over HTTP).

Note that the status code says nothing about what to tell the caller. If you answer in a format of your own, leave out the error for 401, 409, 499, and 500 and above, as Respond, RespondResult, and RespondError do, since it may name internals, say not found for query.ErrNoItems, tell the caller for ErrOutcomeUnknown that the request may have succeeded, and for ErrNotCaughtUp that it has succeeded, but that its result is not visible yet (see Handling Commands over HTTP). Respond, RespondResult, and RespondError answer such an error from before the command or the query with internal server error instead, by the mark that Handle, Ask, and UserOf give it. Only they can tell it apart, so in a format of your own, answer both kinds the same way, or answer failures with RespondError.