Repository navigation
docs: Add ADR 0040 - API standards for request, response, and error payloads - #881
mike-gorman-bitwarden wants to merge 2 commits into
Conversation
…ayloads Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deploying contributing-docs with
|
| Latest commit: |
6700cfa
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://a83590bd.contributing-docs.pages.dev |
| Branch Preview URL: | https://docs-adr-0040-api-payload-st.contributing-docs.pages.dev |
|
|
||
| - **An array of error objects** in a top-level `errors` field. | ||
| - **[RFC 9457](https://www.rfc-editor.org/info/rfc9457/) Problem Details,** as published. | ||
| - **Problem Details with an `errors` extension member,** or ASP.NET Core's |
There was a problem hiding this comment.
We have already started adopting problem details. Here
There was a problem hiding this comment.
Thanks, good to know. That's Bit.HttpExtensions, used for Admin Console validation errors today. Problem Details was one of the options considered (lines 41–43), and lines 238–246 explain why the ADR chose error objects instead: one shape for one or many failures, source for parameters and headers, and per-item failures in best-effort bulk responses. These standards apply only to new APIs in new services, so existing responses like that one don't change.
| **Errors** | ||
|
|
||
| - **An array of error objects** in a top-level `errors` field. | ||
| - **[RFC 9457](https://www.rfc-editor.org/info/rfc9457/) Problem Details,** as published. |
There was a problem hiding this comment.
This RFC is still proposed, not accepted.
There was a problem hiding this comment.
RFC 9457 is published (July 2023, replacing RFC 7807). "Proposed Standard" is its IETF maturity level, the same as most RFCs in use, such as RFC 7396 for merge patch. Either way, the ADR doesn't adopt it; it's listed as a considered option.
| ### Errors | ||
|
|
||
| Errors `MUST` be returned as an array of error objects in a top-level `errors` field. Each error | ||
| has: | ||
|
|
||
| | Field | Description | | ||
| | ------------------ | ----------------------------------------------------------------------- | | ||
| | `code` | A stable, machine-readable error code. | | ||
| | `detail` | A human-readable explanation of this occurrence. May be localized. | | ||
| | `id` | A unique identifier for this occurrence. | | ||
| | `meta.resource` | In a bulk operation, the `type` and `id` of the resource that failed. | | ||
| | `source.header` | The request header that caused the error. | | ||
| | `source.parameter` | The query parameter that caused the error. | | ||
| | `source.pointer` | A JSON Pointer to the field in error, such as `/data/attributes/title`. | | ||
| | `status` | The HTTP status code that applies, as a string. | | ||
| | `title` | A short summary that does not change from occurrence to occurrence. | | ||
|
|
||
| At most one `source` member applies to any one error. | ||
|
|
||
| ```json | ||
| { | ||
| "errors": [ | ||
| { | ||
| "id": "9f3c1e2a-7d40-4c8b-9b17-2f5a1c6e83d1", | ||
| "status": "422", | ||
| "code": "resource-not-found", | ||
| "title": "Referenced resource does not exist", | ||
| "detail": "'e3d2eb3e-755c-41cd-86f0-0e0649043ef6' is not a group in this organization.", | ||
| "source": { | ||
| "pointer": "/data/attributes/groups/0" | ||
| } | ||
| } | ||
| ] | ||
| } | ||
| ``` |
There was a problem hiding this comment.
This should really be https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.problemdetails?view=aspnetcore-10.0 which is based on https://www.rfc-editor.org/info/rfc9457.
We get a lot of value leaning into the asp.net standard and it's quite painful to roll your own validation in asp.net.
There was a problem hiding this comment.
The validation stays ASP.NET's: annotations, model binding, and .NET 10's minimal-API validation all still run. Only the response format changes, and that's three standard hooks (InvalidModelStateResponseFactory, an IExceptionHandler, and UseStatusCodePages). Done once in Bitwarden.Server.Sdk, that's a few hundred lines, and no service writes any of it. These standards apply only to new APIs in new services, so nothing existing has to change. Lines 238–246 give the reasons for error objects over Problem Details. Happy to talk it through if those don't hold up for you.
| ### Status codes | ||
|
|
||
| Status codes follow [RFC 9110](https://www.rfc-editor.org/info/rfc9110/). The following are possible | ||
| for every API, and APIs `SHOULD NOT` document them individually: | ||
|
|
||
| | Status | Meaning | | ||
| | ------ | -------------------------------------------------------------------------------- | | ||
| | `400` | The request is malformed and cannot be parsed, such as invalid JSON. | | ||
| | `401` | The caller is not authenticated. | | ||
| | `403` | The caller is not authorized to invoke the API. | | ||
| | `404` | The resource named in the path does not exist, or the caller may not know of it. | | ||
| | `405` | The HTTP method is not allowed. | | ||
| | `406` | The API cannot return the requested media type. | | ||
| | `409` | The update conflicts with another update. | | ||
| | `415` | The API does not accept the request's media type. | | ||
| | `422` | The request is invalid in a way the caller can fix. | | ||
| | `429` | The caller has made too many requests. | | ||
| | `500` | An unexpected error the caller cannot fix. By definition, a bug. | | ||
| | `501` | The API is stubbed out and not yet implemented. | | ||
| | `503` | A dependency is unavailable or timed out. | |
There was a problem hiding this comment.
Our clients are notoriously terrible at handling error codes and we need to be very mindful of certain error codes ever routing into client handling.
There was a problem hiding this comment.
These standards only cover one service calling another, so these codes go to a service, never to a client. A service that calls another decides what its own callers see, and it shouldn't pass a downstream 403 straight through to a client.
|
|
||
| - **Treat an empty string, an absent field, and `null` alike,** and strip surrounding whitespace. | ||
| - **Preserve every string exactly,** and distinguish an empty string from `null`. | ||
|
|
There was a problem hiding this comment.
Is the empty string the same as null? This seems to say both, then the other.
There was a problem hiding this comment.
Yes, with one exception. Lines 47 and 48 were two opposing options, and the first was chosen, but the list didn't say so. I've marked the chosen option in each group to make that clearer. The rules are at lines 113–114: an empty string is treated as absent, and absent is treated as null. The exception is a partial update, where absent means "leave unchanged" (ADR-0044).
| "lastName": "Smith" | ||
| }, | ||
| "id": "62bed180-1f78-45d4-8a56-c996936a2947", | ||
| "type": "user" |
There was a problem hiding this comment.
Is type globally unique, unique per service?
There was a problem hiding this comment.
It is unique per service and, in practice, almost always globally unique.
|
|
||
| ### Standard fields | ||
|
|
||
| When relevant, resources `MUST` use these names: `createdAt`, `createdBy`, `updatedAt`, `updatedBy`, |
There was a problem hiding this comment.
I would suggest using CreationDate, RevisionDate, and DeletedDate instead of createdAt, updatedAt, and deletedAt. This would align with the existing fields in the database.
There was a problem hiding this comment.
These standards apply only to new APIs in new services whose database does not yet exist. Having said that, I suppose your suggested names are as good as any others but I feel like the names proposed are a bit more fluent as in 'created at [such-n-such time]' and 'created by [such-n-such user]'. Will leave this comment open to see if others have an opinion.
|
From the database side, four points:
|
…th migration consequence Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks.
|
🎟️ Tracking
Follow-up to ADR-0036 (#865).
📔 Objective
Adds ADR-0040, "API standards for request, response, and error payloads", with status Proposed. Sets what every request and response shares: the payload envelope (
data,meta, andid/type/attributeson every resource), standard fields, JSON value rules (dates, enumerations, whitespace, empty strings andnull), naming, content negotiation, method semantics, status codes, exhaustive request validation, and the error format, with the recorded departure from RFC 9457.