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.
This commit is contained in:
Adam Moussa 2026-06-08 14:58:27 -04:00
parent de55c0eeca
commit 67266ba57b

View file

@ -44,6 +44,17 @@ function getPageTitle(page: Record<string, any>): 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<void> {
let continuationToken: string | undefined;
const toDelete: { Key: string }[] = [];
@ -128,7 +139,7 @@ export const handler = async (): Promise<void> => {
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<void> => {
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;
}
};