From 67266ba57b74d36df5c516f1a3059728639f7875 Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Mon, 8 Jun 2026 14:58:27 -0400 Subject: [PATCH] Fix notion-sync header crash on non-ASCII titles S3 user-metadata values are sent as HTTP headers, which must be US-ASCII. Notion page titles routinely contain em dashes and other non-ASCII characters, causing ERR_INVALID_CHAR and a daily sync failure. Sanitize the notion-title metadata to printable ASCII. Also treat a Bedrock ConflictException from StartIngestionJob as benign (an ingestion job is already running and will pick up the freshly uploaded files) instead of failing the whole invocation. --- lambda/notion-sync/index.ts | 45 ++++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 10 deletions(-) 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; + } }; -- 2.50.1