diff --git a/packages/realm-server/tests/atomic-endpoints-test.ts b/packages/realm-server/tests/atomic-endpoints-test.ts index 9a65d614bb2..1548ece6812 100644 --- a/packages/realm-server/tests/atomic-endpoints-test.ts +++ b/packages/realm-server/tests/atomic-endpoints-test.ts @@ -554,8 +554,19 @@ module(basename(import.meta.filename), function () { }, ], }; + // This module batch is setup for the instance batch below, which + // adopts from Place/Country. /_atomic returns as soon as writes are + // durable — not indexed — and a subsequent /_atomic write does not + // wait for a prior write's indexing to settle. Without waitForIndex + // the modules can still be indexing when the instance batch's + // fileSerialization looks up the Place definition; that lookup then + // races the in-flight module index (generation bump discards the + // on-demand prerender result) and the batch fails with + // FilterRefersToNonexistentTypeError. waitForIndex makes the modules + // indexed before we move on, which is what a caller writing modules + // then dependent instances across separate batches must do. let response = await request - .post('/_atomic') + .post('/_atomic?waitForIndex=true') .set('Accept', SupportedMimeType.JSONAPI) .set( 'Authorization', @@ -637,7 +648,13 @@ module(basename(import.meta.filename), function () { `Bearer ${createJWT(testRealm, 'user', ['read', 'write'])}`, ) .send(JSON.stringify(instanceDoc)); - assert.strictEqual(instanceResponse.status, 201); + assert.strictEqual( + instanceResponse.status, + 201, + `expected 201, got ${instanceResponse.status}: ${JSON.stringify( + instanceResponse.body, + )}`, + ); assert.strictEqual(instanceResponse.body['atomic:results'].length, 2); }); test('can write new instance with new module', async function (assert) { diff --git a/packages/runtime-common/realm.ts b/packages/runtime-common/realm.ts index 67d3ca286af..96fa6facfe2 100644 --- a/packages/runtime-common/realm.ts +++ b/packages/runtime-common/realm.ts @@ -2462,9 +2462,21 @@ export class Realm { // Log the underlying exception before returning 500 — // otherwise callers only see "Write Error" and the original // stack trace is lost, making atomic-batch failures - // effectively undebuggable. + // effectively undebuggable. Include e.cause explicitly: errors + // like FilterRefersToNonexistentTypeError carry the actionable + // detail (which module/definition was missing, or that a + // concurrent invalidation discarded the lookup) in their cause, + // not their message, so without this the real reason is swallowed. + let cause = + e?.cause instanceof Error + ? `${e.cause.message}\n${e.cause.stack ?? '(no stack)'}` + : e?.cause != null + ? String(e.cause) + : undefined; this.#log.error( - `Atomic write failed: ${e.message}\n${e.stack ?? '(no stack)'}`, + `Atomic write failed: ${e.message}${ + cause ? `\ncause: ${cause}` : '' + }\n${e.stack ?? '(no stack)'}`, ); return createResponse({ body: JSON.stringify({