Skip to content
Merged
Show file tree
Hide file tree
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
5 changes: 3 additions & 2 deletions apps/web/src/lib/rewriteModelResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { db } from '@/lib/drizzle';
import { KILO_ORGANIZATION_ID } from '@/lib/organizations/constants';
import { errorExceptInTest, logExceptInTest } from '@/lib/utils.server';
import { withRequestId } from '@/lib/ai-gateway/request-id';
import { sanitizeJsonbValue } from '@/lib/sanitize-jsonb';
import type { EventSourceMessage } from 'eventsource-parser';
import { createParser } from 'eventsource-parser';
import { after, NextResponse } from 'next/server';
Expand Down Expand Up @@ -109,9 +110,9 @@ async function createRequestLogCapture(
status_code: status,
model,
provider,
request: request.body,
request: sanitizeJsonbValue(request.body),
response: responseText,
error,
error: sanitizeJsonbValue(error),
})
.returning({ id: api_request_log.id });
logExceptInTest(
Expand Down
25 changes: 25 additions & 0 deletions apps/web/src/lib/sanitize-jsonb.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, expect, test } from '@jest/globals';
import { sanitizeJsonbValue } from './sanitize-jsonb';

describe('sanitizeJsonbValue', () => {
test('replaces JSONB-incompatible characters in nested values and object keys', () => {
const value = {
[`bad\ud800key`]: ['before\udc00after', { text: 'still\ud800broken' }],
[`nul\0key`]: 'nul\0value',
};

expect(sanitizeJsonbValue(value)).toEqual({
['bad\ufffdkey']: ['before\ufffdafter', { text: 'still\ufffdbroken' }],
['nul\ufffdkey']: 'nul\ufffdvalue',
});
});

test('preserves valid surrogate pairs and does not mutate the input', () => {
const value = { text: 'hello 😀' };

const sanitized = sanitizeJsonbValue(value);

expect(sanitized).toEqual(value);
expect(sanitized).not.toBe(value);
});
});
33 changes: 33 additions & 0 deletions apps/web/src/lib/sanitize-jsonb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* PostgreSQL JSONB rejects escaped NUL characters and lone UTF-16 surrogates.
* JavaScript strings can contain both, so repair them before sending values to
* a JSONB column.
*/
function sanitizeJsonbString(value: string): string {
if (value.isWellFormed() && !value.includes('\0')) {
return value;
}

return value.toWellFormed().replaceAll('\0', '\ufffd');
}

export function sanitizeJsonbValue(value: unknown): unknown {
if (typeof value === 'string') {
return sanitizeJsonbString(value);
}

if (Array.isArray(value)) {
return value.map(sanitizeJsonbValue);
}

if (value !== null && typeof value === 'object') {
return Object.fromEntries(
Object.entries(value).map(([key, nestedValue]) => [
sanitizeJsonbString(key),
sanitizeJsonbValue(nestedValue),
])
);
}

return value;
}