Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Bug Fixes

- Fix replication pull URL not URL-encoding the checkpoint `id`. When a document's primary key contained URL-reserved characters (for example `&`, `#`, `=`), the URL was parsed incorrectly on the server, causing the checkpoint to be truncated. With `batchSize: 1` this could make the pull loop never advance past such a document. The client now encodes the `id` with `encodeURIComponent`.
- Fix REST endpoint `/set` not protecting `serverOnlyFields` from client overwrites. Clients could include server-only fields in write requests to `/set`, and those values would be stored directly instead of being ignored. The handler now uses `mergeServerDocumentFields` (consistent with the replication endpoint) to ensure server-only field values are always preserved from the server-side document, not taken from client input.
- Fix missing `await` in `RxRestClient.get()`, `RxRestClient.set()`, and `RxRestClient.delete()` methods. The `postRequest()` call was not awaited before calling `handleError()`, which caused server errors (e.g. 403 Forbidden from `changeValidator`) to be silently swallowed instead of thrown to the caller.
- Fix conflict handling for new documents pushed via replication when `serverOnlyFields` are configured. `mergeServerDocumentFieldsMonad` incorrectly transformed a falsy `assumedMasterState` (used for new document inserts) into an object and set server-only fields to `null` on `newDocumentState`, causing schema validation failures and false conflicts.
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/replication-server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export function replicateServer<RxDocType>(
async handler(checkpointOrNull, batchSize) {
const lwt = checkpointOrNull && checkpointOrNull.lwt ? checkpointOrNull.lwt : 0;
const id = checkpointOrNull && checkpointOrNull.id ? checkpointOrNull.id : '';
const url = options.url + `/pull?lwt=${lwt}&id=${id}&limit=${batchSize}`;
const url = options.url + `/pull?lwt=${lwt}&id=${encodeURIComponent(id)}&limit=${batchSize}`;
const response = await fetch(url, {
method: 'GET',
credentials: 'include',
Expand Down
48 changes: 48 additions & 0 deletions test/unit/endpoint-replication.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,54 @@ describe('endpoint-replication.test.ts', () => {
clientCol.database.close();
});
});
describe('special characters in primary keys', () => {
it('should replicate documents whose primary key contains url-unsafe characters', async function () {
this.timeout(8000);

const col = await humansCollection.create(0);
// passport ids that contain characters which are relevant for URL query parsing.
await col.insert(schemaObjects.humanData('first-doc'));
await col.insert(schemaObjects.humanData('second&doc'));

const port = await nextPort();
const server = await createRxServer({
adapter: TEST_SERVER_ADAPTER,
database: col.database,
authHandler,
port
});
const endpoint = await server.addReplicationEndpoint({
name: randomToken(10),
collection: col
});
await server.start();

const clientCol = await humansCollection.create(0);
const url = 'http://localhost:' + port + '/' + endpoint.urlPath;
const replicationState = await replicateServer<HumanDocumentType>({
collection: clientCol,
replicationIdentifier: randomToken(10),
url,
headers,
live: false,
push: {},
// use batchSize=1 so the checkpoint id containing '&'
// is actually sent back to the server on a subsequent pull.
pull: { batchSize: 1 },
eventSource: EventSource
});
ensureReplicationHasNoErrors(replicationState);
await replicationState.awaitInSync();

const clientDocs = await clientCol.find().exec();
const clientIds = clientDocs.map(d => d.passportId).sort();
assert.deepStrictEqual(clientIds, ['first-doc', 'second&doc']);

await replicationState.cancel();
await col.database.close();
await clientCol.database.close();
});
});
describe('.serverOnlyFields', () => {
it('should not return serverOnlyFields to /pull requests', async () => {
const col = await humansCollection.create(3);
Expand Down