mirror of
https://github.com/tiennm99/rplace.git
synced 2026-10-15 10:15:33 +00:00
fix(canvas): make multi-chunk pixel writes atomic and refund cooldown on failure
- wrap writePixels in state.storage.transactionSync so a partial multi-chunk failure doesn't leave the canvas half-written or out of sync with the WS broadcast - size new chunk buffer against chunkSize(chunkId) instead of the persisted blob's length so writes after a canvas-grow no longer silently drop OOB bytes in the formerly-last short chunk - refund the cooldown row when writePixels throws so transient storage errors stop soft-DOSing the user (and halving image-uploader throughput) - bound readAllChunks by chunk_id < CHUNK_COUNT and trim oversized blobs so orphan rows from a future shrink no longer crash GET /api/canvas - require a positive Content-Length on /api/place (411) and reject above the pre-parse cap (413); previously a missing or zero header bypassed the cap - drop String(err) from the 500 response body - drain the INSERT cursor symmetrically with the UPDATE branch in tryAcquire - assert CHUNK_BYTES <= 2 MB at module load (DO SQLite per-cell BLOB cap) - correct the inverted webSocketClose comment and guard the re-close call - add tests for missing / zero / oversized Content-Length Plan: plans/260510-0232-fix-do-migration-followups/phase-01-do-storage-atomicity.md
This commit is contained in:
1 parent
a977adc62d
commit
3c1263add6
15 files changed
+1686
-24
No files matched your search
@@ -10,12 +10,18 @@ vi.mock('../src/durable-objects/canvas-room.js', () => ({
|
||||
|
||||
import app from '../src/worker.js';
|
||||
|
||||
/** Helper to create POST request */
|
||||
/** Helper to create POST request. Computes Content-Length explicitly because
|
||||
* the synthetic Request constructor in this environment doesn't auto-set it. */
|
||||
function postPlace(body) {
|
||||
const bodyStr = typeof body === 'string' ? body : JSON.stringify(body);
|
||||
const bodyBytes = new TextEncoder().encode(bodyStr).byteLength;
|
||||
return new Request('http://localhost/api/place', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: typeof body === 'string' ? body : JSON.stringify(body),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': String(bodyBytes),
|
||||
},
|
||||
body: bodyStr,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -35,6 +41,7 @@ describe('POST /api/place validation', () => {
|
||||
it('rejects invalid JSON', async () => {
|
||||
const req = new Request('http://localhost/api/place', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Length': '8' },
|
||||
body: 'not json',
|
||||
});
|
||||
const res = await app.fetch(req, env);
|
||||
@@ -43,6 +50,38 @@ describe('POST /api/place validation', () => {
|
||||
expect(data.error).toBe('invalid_json');
|
||||
});
|
||||
|
||||
it('rejects POST without Content-Length', async () => {
|
||||
const req = new Request('http://localhost/api/place', {
|
||||
method: 'POST',
|
||||
body: '{"pixels":[]}',
|
||||
});
|
||||
const res = await app.fetch(req, env);
|
||||
expect(res.status).toBe(411);
|
||||
expect((await res.json()).error).toBe('content_length_required');
|
||||
});
|
||||
|
||||
it('rejects POST with zero Content-Length', async () => {
|
||||
const req = new Request('http://localhost/api/place', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Length': '0' },
|
||||
body: '',
|
||||
});
|
||||
const res = await app.fetch(req, env);
|
||||
expect(res.status).toBe(411);
|
||||
expect((await res.json()).error).toBe('content_length_required');
|
||||
});
|
||||
|
||||
it('rejects POST with Content-Length above the cap', async () => {
|
||||
const req = new Request('http://localhost/api/place', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Length': String(MAX_BATCH_SIZE * 64 + 1) },
|
||||
body: '{}',
|
||||
});
|
||||
const res = await app.fetch(req, env);
|
||||
expect(res.status).toBe(413);
|
||||
expect((await res.json()).error).toBe('body_too_large');
|
||||
});
|
||||
|
||||
it('rejects missing pixels array', async () => {
|
||||
const res = await app.fetch(postPlace({}), env);
|
||||
expect(res.status).toBe(400);
|
||||
|
||||
Reference in new issue
Block a user