Skip to content

Commit

Permalink
Improved XRPC error protocol (bluesky-social#194)
Browse files Browse the repository at this point in the history
* Add lexicon doc

* Update error-handling spec

* Implement new error-behaviors in xrpc and xrpc-server packages

* Update lexicon and lex-cli packages to add xrpc error behaviors

* Generate new API and test an error behavior
  • Loading branch information
pfrazee authored Sep 28, 2022
1 parent 31bd54e commit a21417b
Show file tree
Hide file tree
Showing 72 changed files with 1,032 additions and 415 deletions.
39 changes: 1 addition & 38 deletions docs/specs/adx/repo.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,44 +133,7 @@ To fetch a schema, a request must be sent to the xrpc [`getSchema`](../xrpc.md#g

### Schema structure

Record schemas are encoded in JSON and adhere to the following interface:

```typescript
interface RecordSchema {
adx: 1
id: string
revision?: number // a versioning counter
description?: string
record: JSONSchema
}
```

Here is an example schema:

```json
{
"adx": 1,
"id": "com.example.post",
"record": {
"type": "object",
"required": ["text", "createdAt"],
"properties": {
"text": {"type": "string", "maxLength": 256},
"createdAt": {"type": "string", "format": "date-time"}
}
}
}
```

And here is a record using this example schema:

```json
{
"$type": "com.example.post",
"text": "Hello, world!",
"createdAt": "2022-09-15T16:37:17.131Z"
}
```
Record schemas are encoded in JSON using [Lexicon Schema Documents](../lexicon.md).

### Reserved field names

Expand Down
111 changes: 111 additions & 0 deletions docs/specs/lexicon.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Lexicon Schema Documents

Lexicon is a schemas document format used to define [XRPC](./xrpc.md) methods and [ATP Repository](./adx/repo.md) record types. Every Lexicon schema is written in JSON and follows the interface specified below. The schemas are identified using [NSIDs](./nsid.md) which are then used to identify the methods or record types they describe.

## Interface

```typescript
interface LexiconDoc {
lexicon: 1
id: string // an NSID
type: 'query' | 'procedure' | 'record'
revision?: number
description?: string
}

interface RecordLexiconDoc extends LexiconDoc {
record: JSONSchema
}

interface XrpcLexiconDoc extends LexiconDoc {
parameters?: Record<string, XrpcParameter>
input?: XrpcBody
output?: XrpcBody
errors?: XrpcError[]
}

interface XrpcParameter {
type: 'string' | 'number' | 'integer' | 'boolean'
description?: string
default?: string | number | boolean
required?: boolean
minLength?: number
maxLength?: number
minimum?: number
maximum?: number
}

interface XrpcBody {
encoding: string|string[]
schema: JSONSchema
}

interface XrpcError {
name: string
description?: string
}
```

## Examples

### XRPC Method

```json
{
"lexicon": 1,
"id": "todo.adx.createAccount",
"type": "procedure",
"description": "Create an account.",
"parameters": {},
"input": {
"encoding": "application/json",
"schema": {
"type": "object",
"required": ["email", "username", "password"],
"properties": {
"email": {"type": "string"},
"username": {"type": "string"},
"inviteCode": {"type": "string"},
"password": {"type": "string"}
}
}
},
"output": {
"encoding": "application/json",
"schema": {
"type": "object",
"required": ["jwt", "name", "did"],
"properties": {
"jwt": { "type": "string" },
"name": {"type": "string"},
"did": {"type": "string"}
}
}
},
"errors": [
{"name": "InvalidEmail"},
{"name": "InvalidUsername"},
{"name": "InvalidPassword"},
{"name": "InvalidInviteCode"},
{"name": "UsernameTaken"},
]
}
```

### ATP Record Type

```json
{
"lexicon": 1,
"id": "todo.social.repost",
"type": "record",
"record": {
"type": "object",
"required": ["subject", "createdAt"],
"properties": {
"subject": {"type": "string"},
"createdAt": {"type": "string", "format": "date-time"}
}
}
}
```
130 changes: 11 additions & 119 deletions docs/specs/xrpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,82 +73,7 @@ net.users.bob.ping

#### Method schemas

Method schemas are encoded in JSON and adhere to the following interface:

```typescript
interface MethodSchema {
xrpc: 1
id: string
type: 'query' | 'procedure'
description?: string
parameters?: Record<string, MethodParam> // a map of param names to their definitions
input?: MethodBody
output?: MethodBody
}

interface MethodParam {
type: 'string' | 'number' | 'integer' | 'boolean'
description?: string
default?: string | number | boolean
required?: boolean
minLength?: number // string only
maxLength?: number // string only
minimum?: number // number and integer only
maximum?: number // number and integer only
}

interface MethodBody {
encoding: string | string[] // must be a valid mimetype
schema?: JSONSchema // json only
}
```

An example query-method schema:

```json
{
"xrpc": 1,
"id": "io.social.getFeed",
"type": "query",
"description": "Fetch the user's latest feed.",
"parameters": {
"limit": {"type": "integer", "minimum": 1, "maximum": 50},
"cursor": {"type": "string"},
"reverse": {"type": "boolean", "default": true}
},
"output": {
"encoding": "application/json",
"schema": {
"type": "object",
"required": ["entries", "totalCount"],
"properties": {
"entries": {
"type": "array",
"items": {
"type": "object",
"description": "Entry items will vary and are not constrained at the method level"
}
},
"totalCount": {"type": "number"}
}
}
}
}
```

An example procedure-method schema:

```json
{
"xrpc": 1,
"id": "io.social.setProfilePicture",
"type": "procedure",
"description": "Set the user's avatar.",
"input": {
"encoding": ["image/png", "image/jpg"],
}
}
```
Method schemas are encoded in JSON using [Lexicon Schema Documents](./lexicon.md).

#### Schema distribution

Expand Down Expand Up @@ -211,10 +136,7 @@ The request has succeeded. Expectations:

#### `400` Invalid request

The request is invalid and was not processed. Expecations:

- `Content-Type` header must be `application/json`.
- Response body must match the [InvalidRequest](#invalidrequest) schema.
The request is invalid and was not processed.

#### `401` Authentication required

Expand Down Expand Up @@ -244,32 +166,23 @@ The client has sent too many requests. Rate-limits are decided by each server. E

#### `500` Internal server error

The server reached an unexpected condition during processing. Expecations:

- `Content-Type` header must be `application/json`.
- Response body must match the [InternalError](#internalerror) schema.
The server reached an unexpected condition during processing.

#### `501` Method not implemented

The server does not implement the requested method.

#### `502` A request to upstream failed

The execution of the procedure depends on a call to another server which has failed. Expecations:

- `Content-Type` header must be `application/json`.
- Response body must match the [UpstreamError](#upstreamerror) schema.
The execution of the procedure depends on a call to another server which has failed.

#### `503` Not enough resources

The server is under heavy load and can't complete the request.

#### `504` A request to upstream timed out

The execution of the procedure depends on a call to another server which timed out. Expecations:

- `Content-Type` header must be `application/json`.
- Response body must match the [UpstreamError](#upstreamerror) schema.
The execution of the procedure depends on a call to another server which timed out.

#### Remaining codes

Expand All @@ -285,36 +198,15 @@ Any response code not explicitly enumerated should be handled as follows:

TODO

### Response schemas
### Custom error codes and descriptions

The following schemas are used within the XRPC protocol.

#### `InvalidRequest`
In non-200 (error) responses, services may respond with a JSON body which matches the following schema:

```typescript
interface InvalidRequest {
error: true
type: 'InvalidRequest'
message: string
interface XrpcErrorDescription {
error?: string
message?: string
}
```

#### `InternalError`

```typescript
interface InternalError {
error: true
type: 'InternalError'
message: string
}
```

#### `UpstreamError`

```typescript
interface UpstreamError {
error: true
type: 'UpstreamError'
message: string
}
```
The `error` field of the response body should map to an error name defined in the method's [Lexicon schema](./lexicon.md). This enables more specific error-handling by client software. This is especially advised on 400, 500, and 502 responses where further information will be useful.
Loading

0 comments on commit a21417b

Please sign in to comment.