Skip to content

Commit

Permalink
Merge pull request #309 from share/error-codes
Browse files Browse the repository at this point in the history
Replace numeric error codes with strings
  • Loading branch information
alecgibson authored Nov 20, 2019
2 parents b44acc2 + c0e9e0b commit 1a483b7
Show file tree
Hide file tree
Showing 21 changed files with 351 additions and 171 deletions.
140 changes: 78 additions & 62 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -651,69 +651,85 @@ ShareDB only supports the following logger methods:
- `error`


## Error codes
## Errors

ShareDB returns errors as plain JavaScript objects with the format:
ShareDB returns errors as an instance of `ShareDBError`, with a machine-parsable `code`, as well as more details in the human-readable `message`.

### Common error codes

#### `ERR_OP_SUBMIT_REJECTED`

The op submitted by the client has been rejected by the server for a non-critical reason.

When the client receives this code, it will attempt to roll back the rejected op, leaving the client in a usable state.

This error might be used as part of standard control flow. For example, consumers may define a middleware that validates document structure, and rejects operations that do not conform to this schema using this error code to reset the client to a valid state.

#### `ERR_OP_ALREADY_SUBMITTED`

The same op has been received by the server twice.

This is non-critical, and part of normal control flow, and is sent as an error in order to short-circuit the op processing. It is eventually swallowed by the server, and shouldn't need further handling.

#### `ERR_SUBMIT_TRANSFORM_OPS_NOT_FOUND`

The ops needed to transform the submitted op up to the current version of the snapshot could not be found.

If a client on an old version of a document submits an op, that op needs to be transformed by all the ops that have been applied to the document in the meantime. If the server cannot fetch these ops from the database, then this error is returned.

The most common case of this would be ops being deleted from the database. For example, let's assume we have a TTL set up on the ops in our database. Let's also say we have a client that is so old that the op corresponding to its version has been deleted by the TTL policy. If this client then attempts to submit an op, the server will not be able to find the ops required to transform the op to apply to the current version of the snapshot.

Other causes of this error may be dropping the ops collection all together, or having the database corrupted in some other way.

#### `ERR_MAX_SUBMIT_RETRIES_EXCEEDED`

The number of retries defined by the `maxSubmitRetries` option has been exceeded by a submission.

#### `ERR_DOC_ALREADY_CREATED`

The creation request has failed, because the document was already created by another client.

This can happen when two clients happen to simultaneously try to create the same document, and is potentially recoverable by simply fetching the already-created document.

#### `ERR_DOC_WAS_DELETED`

The deletion request has failed, because the document was already deleted by another client.

This can happen when two clients happen to simultaneously try to delete the same document. Given that the end result is the same, this error can potentially just be ignored.

#### `ERR_DOC_TYPE_NOT_RECOGNIZED`

The specified document type has not been registered with ShareDB.

This error can usually be remedied by remembering to register any types you need:

```javascript
var ShareDB = require('sharedb');
var richText = require('rich-text');

ShareDB.types.register(richText.type);
```
{
code: 5000,
message: 'ShareDB internal error'
}

#### `ERR_DEFAULT_TYPE_MISMATCH`

The default type being used by the client does not match the default type expected by the server.

This will typically only happen when using a different default type to the built-in `json0` used by ShareDB by default (eg if using a fork). The exact same type must be used by both the client and the server, and should be registered as the default type:

```javascript
var ShareDB = require('sharedb');
var forkedJson0 = require('forked-json0');

// Make sure to also do this on your client
ShareDB.types.defaultType = forkedJson0.type;
```

Additional fields may be added to the error object for debugging context depending on the error. Common additional fields include `collection`, `id`, and `op`.

### 4000 - Bad request

* 4001 - Unknown error type
* 4002 - Database adapter does not support subscribe
* 4003 - Database adapter not found
* 4004 - Missing op
* 4005 - Op must be an array
* 4006 - Create data in op must be an object
* 4007 - Create op missing type
* 4008 - Unknown type
* 4009 - del value must be true
* 4010 - Missing op, create or del
* 4011 - Invalid src
* 4012 - Invalid seq
* 4013 - Found seq but not src
* 4014 - op.m invalid
* 4015 - Document does not exist
* 4016 - Document already exists
* 4017 - Document was deleted
* 4018 - Document was created remotely
* 4019 - Invalid protocol version
* 4020 - Invalid default type
* 4021 - Invalid client id
* 4022 - Database adapter does not support queries
* 4023 - Cannot project snapshots of this type
* 4024 - Invalid version
* 4025 - Passing options to subscribe has not been implemented

### 5000 - Internal error

The `41xx` and `51xx` codes are reserved for use by ShareDB DB adapters, and the `42xx` and `52xx` codes are reserved for use by ShareDB PubSub adapters.

* 5001 - No new ops returned when retrying unsuccessful submit
* 5002 - Missing snapshot
* 5003 - Snapshot and op version don't match
* 5004 - Missing op
* 5005 - Missing document
* 5006 - Version mismatch
* 5007 - Invalid state transition
* 5008 - Missing version in snapshot
* 5009 - Cannot ingest snapshot with null version
* 5010 - No op to send
* 5011 - Commit DB method unimplemented
* 5012 - getSnapshot DB method unimplemented
* 5013 - getOps DB method unimplemented
* 5014 - queryPollDoc DB method unimplemented
* 5015 - _subscribe PubSub method unimplemented
* 5016 - _unsubscribe PubSub method unimplemented
* 5017 - _publish PubSub method unimplemented
* 5018 - Required QueryEmitter listener not assigned
* 5019 - getMilestoneSnapshot MilestoneDB method unimplemented
* 5020 - saveMilestoneSnapshot MilestoneDB method unimplemented
* 5021 - getMilestoneSnapshotAtOrBeforeTime MilestoneDB method unimplemented
* 5022 - getMilestoneSnapshotAtOrAfterTime MilestoneDB method unimplemented
#### `ERR_OP_NOT_ALLOWED_IN_PROJECTION`

The submitted op is not valid when applied to the projection.

This may happen if the op targets some property that is not included in the projection.

#### `ERR_TYPE_CANNOT_BE_PROJECTED`

The document's type cannot be projected. `json0` is currently the only type that supports projections.
17 changes: 10 additions & 7 deletions lib/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ var hat = require('hat');
var types = require('./types');
var util = require('./util');
var logger = require('./logger');
var ShareDBError = require('./error');

var ERROR_CODE = ShareDBError.CODES;

/**
* Agent deserializes the wire protocol messages received from the stream and
Expand Down Expand Up @@ -228,7 +231,7 @@ Agent.prototype._sendOpsBulk = function(collection, opsMap) {
function getReplyErrorObject(err) {
if (typeof err === 'string') {
return {
code: 4001,
code: ERROR_CODE.ERR_UNKNOWN_ERROR,
message: err
};
} else {
Expand Down Expand Up @@ -283,7 +286,7 @@ Agent.prototype._open = function() {
if (agent.closed) return;

if (typeof chunk !== 'object') {
var err = {code: 4000, message: 'Received non-object message'};
var err = new ShareDBError(ERROR_CODE.ERR_MESSAGE_BADLY_FORMED, 'Received non-object message');
return agent.close(err);
}

Expand Down Expand Up @@ -326,7 +329,7 @@ Agent.prototype._checkRequest = function(request) {
Agent.prototype._handleMessage = function(request, callback) {
try {
var errMessage = this._checkRequest(request);
if (errMessage) return callback({code: 4000, message: errMessage});
if (errMessage) return callback(new ShareDBError(ERROR_CODE.ERR_MESSAGE_BADLY_FORMED, errMessage));

switch (request.a) {
case 'qf':
Expand All @@ -350,14 +353,14 @@ Agent.prototype._handleMessage = function(request, callback) {
case 'op':
// Normalize the properties submitted
var op = createClientOp(request, this.clientId);
if (!op) return callback({code: 4000, message: 'Invalid op message'});
if (!op) return callback(new ShareDBError(ERROR_CODE.ERR_MESSAGE_BADLY_FORMED, 'Invalid op message'));
return this._submit(request.c, request.d, op, callback);
case 'nf':
return this._fetchSnapshot(request.c, request.d, request.v, callback);
case 'nt':
return this._fetchSnapshotByTimestamp(request.c, request.d, request.ts, callback);
default:
callback({code: 4000, message: 'Invalid or unknown message'});
callback(new ShareDBError(ERROR_CODE.ERR_MESSAGE_BADLY_FORMED, 'Invalid or unknown message'));
}
} catch (err) {
callback(err);
Expand Down Expand Up @@ -603,10 +606,10 @@ Agent.prototype._submit = function(collection, id, op, callback) {
// Message to acknowledge the op was successfully submitted
var ack = {src: op.src, seq: op.seq, v: op.v};
if (err) {
// Occassional 'Op already submitted' errors are expected to happen as
// Occasional 'Op already submitted' errors are expected to happen as
// part of normal operation, since inflight ops need to be resent after
// disconnect. In this case, ack the op so the client can proceed
if (err.code === 4001) return callback(null, ack);
if (err.code === ERROR_CODE.ERR_OP_ALREADY_SUBMITTED) return callback(null, ack);
return callback(err);
}

Expand Down
22 changes: 17 additions & 5 deletions lib/backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ var MemoryPubSub = require('./pubsub/memory');
var ot = require('./ot');
var projections = require('./projections');
var QueryEmitter = require('./query-emitter');
var ShareDBError = require('./error');
var Snapshot = require('./snapshot');
var StreamSocket = require('./stream-socket');
var SubmitRequest = require('./submit-request');
Expand All @@ -22,6 +23,8 @@ var AFTER_SUBMIT_ACTION_DEPRECATION_WARNING = 'DEPRECATED: "after submit" and "a
'Use "afterWrite" instead. Pass `disableSpaceDelimitedActions: true` option to ShareDB to ' +
'disable the "after submit" and "afterSubmit" actions and this warning.';

var ERROR_CODE = ShareDBError.CODES;

function Backend(options) {
if (!(this instanceof Backend)) return new Backend(options);
emitter.EventEmitter.call(this);
Expand Down Expand Up @@ -482,7 +485,10 @@ Backend.prototype.subscribe = function(agent, index, id, version, options, callb
// add the ability to SubmitRequest.commit to optionally pass the metadata to other clients on
// PubSub. This behaviour is not needed right now, but we have added an options object to the
// subscribe() signature so that it remains consistent with getOps() and fetch().
return callback({code: 4025, message: 'Passing options to subscribe has not been implemented'});
return callback(new ShareDBError(
ERROR_CODE.ERR_DATABASE_METHOD_NOT_IMPLEMENTED,
'Passing options to subscribe has not been implemented'
));
}
var start = Date.now();
var projection = this.projections[index];
Expand Down Expand Up @@ -593,7 +599,7 @@ Backend.prototype.queryFetch = function(agent, index, query, options, callback)
// Options can contain:
// db: The name of the DB (if the DB is specified in the otherDbs when the backend instance is created)
// skipPoll: function(collection, id, op, query) {return true or false; }
// this is a syncronous function which can be used as an early filter for
// this is a synchronous function which can be used as an early filter for
// operations going through the system to reduce the load on the DB.
// pollDebounce: Minimum delay between subsequent database polls. This is
// used to batch updates to reduce load on the database at the expense of
Expand All @@ -604,7 +610,10 @@ Backend.prototype.querySubscribe = function(agent, index, query, options, callba
backend._triggerQuery(agent, index, query, options, function(err, request) {
if (err) return callback(err);
if (request.db.disableSubscribe) {
return callback({code: 4002, message: 'DB does not support subscribe'});
return callback(new ShareDBError(
ERROR_CODE.ERR_DATABASE_DOES_NOT_SUPPORT_SUBSCRIBE,
'DB does not support subscribe'
));
}
backend.pubsub.subscribe(request.channel, function(err, stream) {
if (err) return callback(err);
Expand Down Expand Up @@ -650,7 +659,7 @@ Backend.prototype._triggerQuery = function(agent, index, query, options, callbac
// Set the DB reference for the request after the middleware trigger so
// that the db option can be changed in middleware
request.db = (options.db) ? backend.extraDbs[options.db] : backend.db;
if (!request.db) return callback({code: 4003, message: 'DB not found'});
if (!request.db) return callback(new ShareDBError(ERROR_CODE.ERR_DATABASE_ADAPTER_NOT_FOUND, 'DB not found'));
request.snapshotProjection = backend._getSnapshotProjection(request.db, projection);
callback(null, request);
});
Expand Down Expand Up @@ -731,7 +740,10 @@ Backend.prototype._fetchSnapshot = function(collection, id, version, callback) {
if (error) return callback(error);

if (version > snapshot.v) {
return callback({code: 4024, message: 'Requested version exceeds latest snapshot version'});
return callback(new ShareDBError(
ERROR_CODE.ERR_OP_VERSION_NEWER_THAN_CURRENT_SNAPSHOT,
'Requested version exceeds latest snapshot version'
));
}

callback(null, snapshot);
Expand Down
19 changes: 15 additions & 4 deletions lib/client/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ var types = require('../types');
var util = require('../util');
var logger = require('../logger');

var ERROR_CODE = ShareDBError.CODES;

function connectionState(socket) {
if (socket.readyState === 0 || socket.readyState === 1) return 'connecting';
return 'disconnected';
Expand Down Expand Up @@ -186,15 +188,21 @@ Connection.prototype.handleMessage = function(message) {
case 'init':
// Client initialization packet
if (message.protocol !== 1) {
err = new ShareDBError(4019, 'Invalid protocol version');
err = new ShareDBError(
ERROR_CODE.ERR_PROTOCOL_VERSION_NOT_SUPPORTED,
'Unsupported protocol version: ' + message.protocol
);
return this.emit('error', err);
}
if (types.map[message.type] !== types.defaultType) {
err = new ShareDBError(4020, 'Invalid default type');
err = new ShareDBError(
ERROR_CODE.ERR_DEFAULT_TYPE_MISMATCH,
message.type + ' does not match the server default type'
);
return this.emit('error', err);
}
if (typeof message.id !== 'string') {
err = new ShareDBError(4021, 'Invalid client id');
err = new ShareDBError(ERROR_CODE.ERR_CLIENT_ID_BADLY_FORMED, 'Client id must be a string');
return this.emit('error', err);
}
this.id = message.id;
Expand Down Expand Up @@ -303,7 +311,10 @@ Connection.prototype._setState = function(newState, reason) {
this.state !== 'connecting'
)
) {
var err = new ShareDBError(5007, 'Cannot transition directly from ' + this.state + ' to ' + newState);
var err = new ShareDBError(
ERROR_CODE.ERR_CONNECTION_STATE_TRANSITION_INVALID,
'Cannot transition directly from ' + this.state + ' to ' + newState
);
return this.emit('error', err);
}

Expand Down
Loading

0 comments on commit 1a483b7

Please sign in to comment.