diff --git a/lambda/notion-sync/index.ts b/lambda/notion-sync/index.ts index 696e5eb..7837152 100644 --- a/lambda/notion-sync/index.ts +++ b/lambda/notion-sync/index.ts @@ -44,6 +44,17 @@ function getPageTitle(page: Record): string { return page.id as string; } +// S3 user-metadata values travel as HTTP headers, which must be US-ASCII with no +// control characters. Notion titles routinely contain em dashes and other +// non-ASCII characters (ERR_INVALID_CHAR otherwise). Strip to safe printable +// ASCII and cap at the 256-char metadata limit. +function sanitizeMetadataValue(value: string): string { + return value + .replace(/[^\x20-\x7E]/g, '') // drop non-printable / non-ASCII + .trim() + .slice(0, 256); +} + async function clearOldFiles(bucket: string): Promise { let continuationToken: string | undefined; const toDelete: { Key: string }[] = []; @@ -128,7 +139,7 @@ export const handler = async (): Promise => { ContentType: 'text/markdown', Metadata: { 'notion-page-id': pageId, - 'notion-title': title.slice(0, 256), // S3 metadata value limit + 'notion-title': sanitizeMetadataValue(title), }, }), ); @@ -144,13 +155,27 @@ export const handler = async (): Promise => { console.log(`Upload complete. ${synced} synced, ${failed} failed.`); console.log('Triggering Bedrock KB ingestion job...'); - const ingestionRes = await bedrockAgent.send( - new StartIngestionJobCommand({ - knowledgeBaseId: kbId, - dataSourceId: dsId, - }), - ); - console.log( - `Ingestion job started: ${ingestionRes.ingestionJob?.ingestionJobId}`, - ); + try { + const ingestionRes = await bedrockAgent.send( + new StartIngestionJobCommand({ + knowledgeBaseId: kbId, + dataSourceId: dsId, + }), + ); + console.log( + `Ingestion job started: ${ingestionRes.ingestionJob?.ingestionJobId}`, + ); + } catch (err) { + // A ConflictException means an ingestion job is already running for this data + // source (e.g. an overlapping run). The freshly uploaded files will be picked + // up by that in-flight job, so this is benign — log and exit cleanly rather + // than failing the whole invocation. + if (err instanceof Error && err.name === 'ConflictException') { + console.log( + 'Ingestion job already in progress; uploaded files will be picked up by the running job. Skipping.', + ); + return; + } + throw err; + } };