Skip to content

Commit 45aa8d2

Browse files
twmbclaude
andcommitted
fix(frontend): preserve \u00XX escapes in Avro JSON viewer
Avro's JSON encoding represents bytes/fixed as ISO-8859-1 code points (\u00XX for anything above 0x7F). Running the normalized payload through JSON.parse then JSON.stringify for display launders those escapes into literal Unicode glyphs (e.g. Û -> U+00DB -> "Û"), so "Copy Value" placed UTF-8 bytes on the clipboard instead of the original byte. Scoped to payload.encoding === 'avro' only. KowlJsonView now re-escapes code points in 0x80-0xFF back to \u00XX before display when called from an avro payload view, which is lossless for both Avro bytes fields (recovers the exact byte) and legitimate Latin-1 strings (valid JSON escape for the same Unicode character). Non-avro encodings (proto, json, text, etc.) are unchanged. Fixes #2421. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 94449f4 commit 45aa8d2

2 files changed

Lines changed: 20 additions & 6 deletions

File tree

frontend/src/components/misc/kowl-json-view.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,26 @@ const READ_ONLY_EDITOR_OPTIONS = {
4949
scrollBeyondLastLine: false,
5050
} as const;
5151

52-
export const KowlJsonView = (props: { srcObj: object | string | null | undefined; style?: CSSProperties }) => {
52+
export const KowlJsonView = (props: {
53+
srcObj: object | string | null | undefined;
54+
style?: CSSProperties;
55+
// escapeLatin1 re-escapes code points in 0x80-0xFF as \u00XX so bytes from
56+
// Avro's JSON bytes encoding survive copy-paste out of the viewer. Without
57+
// this, JSON.stringify emits the code point as a literal glyph and the
58+
// clipboard receives the UTF-8 encoding, not the original byte.
59+
escapeLatin1?: boolean;
60+
}) => {
5361
const containerRef = useRef<HTMLDivElement | null>(null);
5462
const editorRef = useRef<IStandaloneCodeEditor | null>(null);
5563
const frameRef = useRef<number | null>(null);
5664
const lastSizeRef = useRef({ width: 0, height: 0 });
57-
const str = useMemo(
58-
() => (typeof props.srcObj === 'string' ? props.srcObj : JSON.stringify(props.srcObj, undefined, 4)),
59-
[props.srcObj]
60-
);
65+
const str = useMemo(() => {
66+
const raw = typeof props.srcObj === 'string' ? props.srcObj : JSON.stringify(props.srcObj, undefined, 4);
67+
if (!props.escapeLatin1) {
68+
return raw;
69+
}
70+
return raw.replace(/[\u0080-\u00ff]/g, (c) => `\\u00${c.charCodeAt(0).toString(16).padStart(2, '0')}`);
71+
}, [props.srcObj, props.escapeLatin1]);
6172

6273
const scheduleLayout = useCallback(() => {
6374
if (frameRef.current !== null) {

frontend/src/components/pages/topics/Tab.Messages/message-display/payload-component.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,10 @@ export const PayloadComponent = (p: { payload: Payload; loadLargeMessage: () =>
182182
);
183183
}
184184
if (renderData.type === 'json') {
185-
return <KowlJsonView srcObj={renderData.content} />;
185+
// Avro JSON encodes bytes fields as \u00XX escape sequences. Re-escape
186+
// Latin-1 code points in the viewer so copy-paste yields the original
187+
// bytes rather than their UTF-8 encoding.
188+
return <KowlJsonView escapeLatin1={payload.encoding === 'avro'} srcObj={renderData.content} />;
186189
}
187190
return <span style={{ color: 'red' }}>Error in RenderExpandedMessage: {renderData.content}</span>;
188191
};

0 commit comments

Comments
 (0)