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
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,23 @@ export function TopicMessage({
// (error-only rows). In that case we render the alerts but no content/toolbar.
const hasMessage = !isNil(message);

const {preparedMessage, decodedMessage, isJson} = React.useMemo(() => {
const {preparedMessage, decodedMessage, isJson, rawBytes} = React.useMemo(() => {
// A schematized value is already a parsed JSON value (object/array or a
// primitive such as a string) and must be used as-is. Only legacy,
// non-schematized string values are base64-encoded and need decoding.
// A raw string value alone is ambiguous, so we rely on the
// `isSchematized` signal derived from the response schema context rather
// than guessing from the type.
let preparedMessage: string | unknown = message;
// decodedMessage is always a string, suitable for download/clipboard.
// decodedMessage is always a string, suitable for preview/clipboard. It is
// a best-effort UTF-8 rendering and MUST NOT be used for downloading the
// message, since non-UTF-8 binary payloads get lossily mangled (invalid
// byte sequences become U+FFFD) - use `rawBytes` for that instead.
let decodedMessage: string =
typeof message === 'string' ? message : JSON.stringify(message, null, 2);
// Raw decoded bytes of a non-schematized base64 message, exactly as sent
// by the backend - the only safe source for downloading binary messages.
let rawBytes: Uint8Array | undefined;

if (typeof message === 'string' && !isSchematized) {
try {
Expand All @@ -64,6 +70,7 @@ export function TopicMessage({
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
rawBytes = bytes;
decodedMessage = utf8Decoder.decode(bytes);
} catch (e) {
console.warn(e);
Expand All @@ -89,7 +96,7 @@ export function TopicMessage({
preparedMessage = JSON.stringify(preparedMessage, null, 2);
}

return {preparedMessage, decodedMessage, isJson};
return {preparedMessage, decodedMessage, isJson, rawBytes};
}, [message, size, isSchematized]);

const renderMessageContent = () => {
Expand Down Expand Up @@ -132,10 +139,11 @@ export function TopicMessage({
<ActionTooltip title={i18n('label_download')}>
<Button
view="flat-secondary"
aria-label={i18n('label_download')}
onClick={(e) => {
e.stopPropagation();
createAndDownloadFile(
decodedMessage,
rawBytes ?? decodedMessage,
Comment thread
astandrik marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Narrow raw byte type before passing it as BlobPart

With the repo's TypeScript 5.8 DOM typings, the rawBytes variable is annotated as plain Uint8Array, which widens to Uint8Array<ArrayBufferLike>, but createAndDownloadFile now requires BlobPart and DOM BlobPart only accepts an ArrayBufferView<ArrayBuffer>. This makes npm run typecheck reject the new download call (Uint8Array<ArrayBufferLike> is not assignable to BlobPart) even though the runtime path is valid; keep the byte array typed as Uint8Array<ArrayBuffer> or otherwise narrow/convert it before passing it here.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a false positive — the code type-checks clean. npx tsc --noEmit over the whole project reports 0 errors on TS 5.8.3 with lib: ["dom","dom.iterable","esnext"] (the exact setup flagged here). rawBytes ?? decodedMessage is Uint8Array<ArrayBufferLike> | string, and in this DOM lib ArrayBufferView defaults to ArrayBufferView<ArrayBufferLike>, so a Uint8Array<ArrayBufferLike> already satisfies BufferSource ⊂ BlobPart. No narrowing needed.

`topic-message-${offset ?? 'unknown-offset'}`,
);
}}
Expand Down
2 changes: 1 addition & 1 deletion src/utils/downloadFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export function downloadFile(url: string, filename: string) {
document.body.removeChild(link);
}

export const createAndDownloadFile = (data: string, fileName: string, type?: string) => {
export const createAndDownloadFile = (data: BlobPart, fileName: string, type?: string) => {
const blob = new Blob([data], {
type,
});
Expand Down
Loading