Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(storage): added some cloudflare KV 413 error management #342

Merged
merged 1 commit into from
Nov 16, 2024
Merged
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
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 };
@@ -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 }> {
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 {
@@ -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;
},
};

5 changes: 5 additions & 0 deletions packages/app-server/wrangler.toml
Original file line number Diff line number Diff line change
@@ -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