Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
23 changes: 23 additions & 0 deletions apps/web/src/lib/sanitize-jsonb.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { describe, expect, test } from '@jest/globals';
import { sanitizeJsonbValue } from './sanitize-jsonb';

describe('sanitizeJsonbValue', () => {
test('replaces lone surrogates in nested values and object keys', () => {
const value = {
[`bad\ud800key`]: ['before\udc00after', { text: 'still\ud800broken' }],
};

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

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);
});
});
51 changes: 51 additions & 0 deletions apps/web/src/lib/sanitize-jsonb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* PostgreSQL JSONB rejects JSON strings containing escaped lone UTF-16
* surrogates. JavaScript can receive those values from JSON.parse, so repair
* them before sending values to a JSONB column.
*/
function replaceUnpairedSurrogates(value: string): string {
Comment thread
chrarnoldus marked this conversation as resolved.
Outdated
let result = '';
let changed = false;

for (let index = 0; index < value.length; index++) {
Comment thread
chrarnoldus marked this conversation as resolved.
Outdated
const code = value.charCodeAt(index);
if (code >= 0xd800 && code <= 0xdbff) {
const nextCode = value.charCodeAt(index + 1);
if (nextCode >= 0xdc00 && nextCode <= 0xdfff) {
result += value[index] + value[index + 1];
index++;
} else {
result += '\ufffd';
changed = true;
}
} else if (code >= 0xdc00 && code <= 0xdfff) {
result += '\ufffd';
changed = true;
} else {
result += value[index];
}
}

return changed ? result : value;
}

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

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

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

return value;
}
Loading