Skip to content

Commit

Permalink
refactor(storage): added some cloudflare KV 413 error management
Browse files Browse the repository at this point in the history
  • Loading branch information
CorentinTh committed Nov 16, 2024
1 parent c4dd574 commit e228e29
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 31 deletions.
70 changes: 40 additions & 30 deletions packages/app-server/src/modules/notes/notes.repository.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type { Storage } from '../storage/storage.types';
import type { DatabaseNote, Note } from './notes.types';
import { injectArguments } from '@corentinth/chisels';
import { isCustomError } from '../shared/errors/errors';
import { generateId } from '../shared/utils/random';
import { createNoteNotFoundError } from './notes.errors';
import { KV_VALUE_LENGTH_EXCEEDED_ERROR_CODE } from '../storage/factories/cloudflare-kv.storage';
import { createNoteNotFoundError, createNotePayloadTooLargeError } from './notes.errors';
import { getNoteExpirationDate } from './notes.models';

export { createNoteRepository };
Expand Down Expand Up @@ -52,38 +54,46 @@ async function saveNote(
isPublic: boolean;
},
): Promise<{ noteId: string }> {
const noteId = generateNoteId();
const baseNote = {
payload,
deleteAfterReading,
encryptionAlgorithm,
serializationFormat,
isPublic,
};

if (!ttlInSeconds) {
await storage.setItem(noteId, baseNote);
try {
const noteId = generateNoteId();
const baseNote = {
payload,
deleteAfterReading,
encryptionAlgorithm,
serializationFormat,
isPublic,
};

if (!ttlInSeconds) {
await storage.setItem(noteId, baseNote);

return { noteId };
}

const { expirationDate } = getNoteExpirationDate({ ttlInSeconds, now });

await storage.setItem(
noteId,
{
...baseNote,
expirationDate: expirationDate.toISOString(),
},
{
// Some storage drivers have a different API for setting TTLs
ttl: ttlInSeconds,
// Cloudflare KV Binding - https://developers.cloudflare.com/kv/api/write-key-value-pairs/#create-expiring-keys
expirationTtl: ttlInSeconds,
},
);

return { noteId };
}
} catch (error) {
if (isCustomError(error) && error.code === KV_VALUE_LENGTH_EXCEEDED_ERROR_CODE) {
throw createNotePayloadTooLargeError();
}

const { expirationDate } = getNoteExpirationDate({ ttlInSeconds, now });

await storage.setItem(
noteId,
{
...baseNote,
expirationDate: expirationDate.toISOString(),
},
{
// Some storage drivers have a different API for setting TTLs
ttl: ttlInSeconds,
// Cloudflare KV Binding - https://developers.cloudflare.com/kv/api/write-key-value-pairs/#create-expiring-keys
expirationTtl: ttlInSeconds,
},
);

return { noteId };
throw error;
}
}

async function getNoteById({ noteId, storage }: { noteId: string; storage: Storage<DatabaseNote> }): Promise<{ note: Note }> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { describe, expect, test } from 'vitest';
import { looksLikeCloudflare413Error } from './cloudflare-kv.storage.models';

describe('cloudflare-kv storage models', () => {
describe('looksLikeCloudflare413Error', () => {
test('a cloudflare 413 error starts with "KV PUT failed: 413 Value length of", everything else is not a cloudflare 413 error', () => {
expect(looksLikeCloudflare413Error({
error: new Error('KV PUT failed: 413 Value length of 41943339 exceeds limit of 26214400.'),
})).to.eql(true);

expect(looksLikeCloudflare413Error({
error: new Error('KV PUT failed: 429 Too many requests.'),
})).to.eql(false);

expect(looksLikeCloudflare413Error({ error: undefined })).to.eql(false);
expect(looksLikeCloudflare413Error({ error: 'foo' })).to.eql(false);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { isError } from 'lodash-es';

export { looksLikeCloudflare413Error };

function looksLikeCloudflare413Error({ error }: { error: unknown }) {
if (isError(error)) {
return error?.message?.startsWith('KV PUT failed: 413 Value length of') ?? false;
}

return false;
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import type { Driver } from 'unstorage';
import { safely } from '@corentinth/chisels';
import { createStorage } from 'unstorage';
import cloudflareKVBindingDriver from 'unstorage/drivers/cloudflare-kv-binding';
import { createError } from '../../shared/errors/errors';
import { defineBindableStorageFactory } from '../storage.models';
import { looksLikeCloudflare413Error } from './cloudflare-kv.storage.models';

export const KV_VALUE_LENGTH_EXCEEDED_ERROR_CODE = 'storage.kv.value_length_exceeds_limit';

export const createCloudflareKVStorageFactory = defineBindableStorageFactory(() => {
return {
Expand All @@ -28,7 +32,22 @@ export const createCloudflareKVStorageFactory = defineBindableStorageFactory(()
// Current : https://github.com/unjs/unstorage/blob/v1.10.2/src/drivers/cloudflare-kv-binding.ts
// Future : https://github.com/unjs/unstorage/blob/main/src/drivers/cloudflare-kv-binding.ts
async setItem(key, value, options) {
return binding.put(key, value, options);
const [result, error] = await safely(binding.put(key, value, options));

if (looksLikeCloudflare413Error({ error })) {
throw createError({
message: 'Value length exceeds limit',
code: KV_VALUE_LENGTH_EXCEEDED_ERROR_CODE,
statusCode: 413,
isInternal: true,
});
}

if (error) {
throw error;
}

return result;
},
};

Expand Down
5 changes: 5 additions & 0 deletions packages/app-server/wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,8 @@ pages_build_output_dir = "./dist"
[[kv_namespaces]]
binding = "notes"
id = "b1329cb8560e49d392bed877c9ac48a2"

[vars]
# Cloudfare KV value max size is 25Mib
# https://developers.cloudflare.com/kv/platform/limits/
NOTES_MAX_ENCRYPTED_PAYLOAD_LENGTH = 26214400

0 comments on commit e228e29

Please sign in to comment.