🐛(y-provider) return empty output when converting empty Yjs document#2328
Conversation
WalkthroughThis PR updates the y-provider convert handler to gracefully handle empty Yjs documents by returning HTTP 200 with empty output, instead of returning HTTP 500 when blocks are missing or empty. The core change modifies Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Why calling this webservice with an empty body ? |
|
@AntoLC because our CMS fetches all pages without knowing in advance if some are empty or not :) (the parent pages are often empty) |
44ca7a1 to
036feee
Compare
The convert handler returned 500 "No valid blocks were generated" whenever the reader produced an empty blocks array, which is the normal state of a freshly-created Yjs document. Treat empty input as a valid case and return 200 with an empty body for every supported output format. Signed-off-by: Sylvain Zimmer <sylvain@sylvainzimmer.com>
036feee to
5bd2d1b
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/frontend/servers/y-provider/src/handlers/convertHandler.ts (1)
319-344: 🧹 Nitpick | 🔵 Trivial | 💤 Low valueConsider testing additional output formats for empty documents.
The test validates the core fix by checking HTML and Markdown outputs. For more comprehensive coverage, consider also testing:
- Empty Yjs → JSON output
- Empty Yjs → YJS output
- Empty BlockNote input (
[]) → various outputsThis would ensure all writers handle empty blocks consistently.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/frontend/servers/y-provider/src/handlers/convertHandler.ts` around lines 319 - 344, Add unit tests that exercise additional serialization paths for empty documents beyond HTML and Markdown to ensure all writers handle empty blocks consistently: specifically add tests that feed an empty Yjs document into the conversion flow used by convertHandler and assert JSON output (Yjs → JSON) and YJS serialized output (Yjs → YJS) are correct/empty, and add tests that convert an empty BlockNote input ([]) through the same convertHandler/writers and assert HTML, Markdown, JSON and YJS outputs. Reference the convertHandler conversion entrypoint and the writer functions used there (e.g., the JSON writer, YJS writer and BlockNote input handling) so the new tests invoke the exact code paths you fixed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/frontend/servers/y-provider/__tests__/convert.test.ts`:
- Around line 319-344: Add a third sub-test in the 'POST /api/convert empty Yjs
document returns 200 with empty content' test: after creating yjsUpdate via
Y.encodeStateAsUpdate(new Y.Doc()) and the existing HTML/Markdown requests, send
a POST to '/api/convert' using initApp() with headers 'content-type:
application/vnd.yjs.doc' and 'accept: application/json', sending
Buffer.from(yjsUpdate); then assert the response status is 200 and the parsed
JSON body equals an empty object ({}), mirroring the non-empty JSON conversion
test pattern.
- Around line 319-344: The test "POST /api/convert empty Yjs document returns
200 with empty content" is missing the established spy on
Y.Doc.prototype.destroy; add a jest.spyOn(Y.Doc.prototype, 'destroy') before
making the two requests, run the two requests as-is, then assert the spy was
called twice (to verify both decodings cleaned up), and finally restore the spy
with mockRestore to avoid polluting other tests.
---
Outside diff comments:
In `@src/frontend/servers/y-provider/src/handlers/convertHandler.ts`:
- Around line 319-344: Add unit tests that exercise additional serialization
paths for empty documents beyond HTML and Markdown to ensure all writers handle
empty blocks consistently: specifically add tests that feed an empty Yjs
document into the conversion flow used by convertHandler and assert JSON output
(Yjs → JSON) and YJS serialized output (Yjs → YJS) are correct/empty, and add
tests that convert an empty BlockNote input ([]) through the same
convertHandler/writers and assert HTML, Markdown, JSON and YJS outputs.
Reference the convertHandler conversion entrypoint and the writer functions used
there (e.g., the JSON writer, YJS writer and BlockNote input handling) so the
new tests invoke the exact code paths you fixed.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 95738a87-3706-438d-905a-449333f8b79c
📒 Files selected for processing (3)
CHANGELOG.mdsrc/frontend/servers/y-provider/__tests__/convert.test.tssrc/frontend/servers/y-provider/src/handlers/convertHandler.ts
| test('POST /api/convert empty Yjs document returns 200 with empty content', async () => { | ||
| const app = initApp(); | ||
| const yjsUpdate = Y.encodeStateAsUpdate(new Y.Doc()); | ||
|
|
||
| const htmlResponse = await request(app) | ||
| .post('/api/convert') | ||
| .set('origin', origin) | ||
| .set('authorization', `Bearer ${apiKey}`) | ||
| .set('content-type', 'application/vnd.yjs.doc') | ||
| .set('accept', 'text/html') | ||
| .send(Buffer.from(yjsUpdate)); | ||
|
|
||
| expect(htmlResponse.status).toBe(200); | ||
| expect(htmlResponse.text).toBe(''); | ||
|
|
||
| const markdownResponse = await request(app) | ||
| .post('/api/convert') | ||
| .set('origin', origin) | ||
| .set('authorization', `Bearer ${apiKey}`) | ||
| .set('content-type', 'application/vnd.yjs.doc') | ||
| .set('accept', 'text/markdown') | ||
| .send(Buffer.from(yjsUpdate)); | ||
|
|
||
| expect(markdownResponse.status).toBe(200); | ||
| expect(markdownResponse.text).toBe('\n'); | ||
| }); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | 💤 Low value
Consider adding JSON output format coverage.
For completeness, consider also testing conversion of an empty Yjs document to JSON format, similar to how the test at line 261 validates non-empty Yjs to JSON conversion.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/frontend/servers/y-provider/__tests__/convert.test.ts` around lines 319 -
344, Add a third sub-test in the 'POST /api/convert empty Yjs document returns
200 with empty content' test: after creating yjsUpdate via
Y.encodeStateAsUpdate(new Y.Doc()) and the existing HTML/Markdown requests, send
a POST to '/api/convert' using initApp() with headers 'content-type:
application/vnd.yjs.doc' and 'accept: application/json', sending
Buffer.from(yjsUpdate); then assert the response status is 200 and the parsed
JSON body equals an empty object ({}), mirroring the non-empty JSON conversion
test pattern.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Add Y.Doc.destroy spy to follow established pattern.
Every test in this file that converts FROM Yjs format verifies that Y.Doc.prototype.destroy is called (lines 262, 304). Since this test makes two requests that decode Yjs input, you should add the spy and expect it to be called twice to ensure resource cleanup is tested.
♻️ Proposed addition to verify cleanup
test('POST /api/convert empty Yjs document returns 200 with empty content', async () => {
+ const destroySpy = vi.spyOn(Y.Doc.prototype, 'destroy');
const app = initApp();
const yjsUpdate = Y.encodeStateAsUpdate(new Y.Doc());
const htmlResponse = await request(app)
.post('/api/convert')
.set('origin', origin)
.set('authorization', `Bearer ${apiKey}`)
.set('content-type', 'application/vnd.yjs.doc')
.set('accept', 'text/html')
.send(Buffer.from(yjsUpdate));
expect(htmlResponse.status).toBe(200);
expect(htmlResponse.text).toBe('');
const markdownResponse = await request(app)
.post('/api/convert')
.set('origin', origin)
.set('authorization', `Bearer ${apiKey}`)
.set('content-type', 'application/vnd.yjs.doc')
.set('accept', 'text/markdown')
.send(Buffer.from(yjsUpdate));
expect(markdownResponse.status).toBe(200);
expect(markdownResponse.text).toBe('\n');
+ expect(destroySpy).toHaveBeenCalledTimes(2);
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| test('POST /api/convert empty Yjs document returns 200 with empty content', async () => { | |
| const app = initApp(); | |
| const yjsUpdate = Y.encodeStateAsUpdate(new Y.Doc()); | |
| const htmlResponse = await request(app) | |
| .post('/api/convert') | |
| .set('origin', origin) | |
| .set('authorization', `Bearer ${apiKey}`) | |
| .set('content-type', 'application/vnd.yjs.doc') | |
| .set('accept', 'text/html') | |
| .send(Buffer.from(yjsUpdate)); | |
| expect(htmlResponse.status).toBe(200); | |
| expect(htmlResponse.text).toBe(''); | |
| const markdownResponse = await request(app) | |
| .post('/api/convert') | |
| .set('origin', origin) | |
| .set('authorization', `Bearer ${apiKey}`) | |
| .set('content-type', 'application/vnd.yjs.doc') | |
| .set('accept', 'text/markdown') | |
| .send(Buffer.from(yjsUpdate)); | |
| expect(markdownResponse.status).toBe(200); | |
| expect(markdownResponse.text).toBe('\n'); | |
| }); | |
| test('POST /api/convert empty Yjs document returns 200 with empty content', async () => { | |
| const destroySpy = vi.spyOn(Y.Doc.prototype, 'destroy'); | |
| const app = initApp(); | |
| const yjsUpdate = Y.encodeStateAsUpdate(new Y.Doc()); | |
| const htmlResponse = await request(app) | |
| .post('/api/convert') | |
| .set('origin', origin) | |
| .set('authorization', `Bearer ${apiKey}`) | |
| .set('content-type', 'application/vnd.yjs.doc') | |
| .set('accept', 'text/html') | |
| .send(Buffer.from(yjsUpdate)); | |
| expect(htmlResponse.status).toBe(200); | |
| expect(htmlResponse.text).toBe(''); | |
| const markdownResponse = await request(app) | |
| .post('/api/convert') | |
| .set('origin', origin) | |
| .set('authorization', `Bearer ${apiKey}`) | |
| .set('content-type', 'application/vnd.yjs.doc') | |
| .set('accept', 'text/markdown') | |
| .send(Buffer.from(yjsUpdate)); | |
| expect(markdownResponse.status).toBe(200); | |
| expect(markdownResponse.text).toBe('\n'); | |
| expect(destroySpy).toHaveBeenCalledTimes(2); | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/frontend/servers/y-provider/__tests__/convert.test.ts` around lines 319 -
344, The test "POST /api/convert empty Yjs document returns 200 with empty
content" is missing the established spy on Y.Doc.prototype.destroy; add a
jest.spyOn(Y.Doc.prototype, 'destroy') before making the two requests, run the
two requests as-is, then assert the spy was called twice (to verify both
decodings cleaned up), and finally restore the spy with mockRestore to avoid
polluting other tests.

The convert handler returned 500 "No valid blocks were generated" whenever the reader produced an empty blocks array, which is the normal state of a freshly-created Yjs document. Treat empty input as a valid case and return 200 with an empty body for every supported output format.