Skip to content

fix: preserve raw bytes when downloading topic messages#4130

Merged
astandrik merged 4 commits into
ydb-platform:mainfrom
krasnovdm:fix/topic-message-binary-download
Jul 23, 2026
Merged

fix: preserve raw bytes when downloading topic messages#4130
astandrik merged 4 commits into
ydb-platform:mainfrom
krasnovdm:fix/topic-message-binary-download

Conversation

@krasnovdm

@krasnovdm krasnovdm commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Topic message Download button reused the UTF-8-decoded preview string, so non-UTF-8 binary payloads were silently corrupted (invalid byte sequences → U+FFFD) in the downloaded file
  • Download now uses the raw decoded Uint8Array directly; preview/clipboard still use the best-effort UTF-8 string

Fixes #4129

Test plan

  • tsc --noEmit passes
  • eslint on changed files — no new warnings/errors
  • Manual: write a binary (non-UTF-8) message to a topic, download it via Embedded UI, verify downloaded bytes match the original

Greptile Summary

This PR fixes silent data corruption when downloading binary (non-UTF-8) topic messages. Previously, the download reused the lossy UTF-8-decoded preview string, replacing invalid byte sequences with U+FFFD; now the raw Uint8Array from atob is passed directly to Blob, preserving every original byte.

  • TopicMessage.tsx: rawBytes (a Uint8Array) is captured in the useMemo alongside decodedMessage; the download button uses rawBytes ?? decodedMessage, while preview and clipboard continue to use the best-effort UTF-8 string. All fallback paths (schematized messages, non-string values, failed atob) safely fall back to decodedMessage.
  • downloadFile.ts: The data parameter of createAndDownloadFile is widened from string to BlobPart, the minimal typing change needed to accept Uint8Array.

Confidence Score: 5/5

Safe to merge — the change is narrowly scoped to the download path and preserves all existing preview/clipboard behaviour unchanged.

The fix is correct and complete: rawBytes is populated from the same atob/Uint8Array decode that already feeds the UTF-8 preview, every fallback path (schematized messages, non-string payloads, failed atob) continues to work via the ?? decodedMessage guard, and the only typing change in downloadFile.ts is a minimal widening of BlobPart that does not alter any other callers.

No files require special attention.

Important Files Changed

Filename Overview
src/containers/Tenant/Diagnostics/TopicData/TopicMessageDetails/components/TopicMessage.tsx Adds rawBytes (Uint8Array) to useMemo output and uses it for downloads instead of the lossy UTF-8 decoded string; clipboard/preview still use decodedMessage. All fallback paths (schematized, non-string, atob failure) correctly fall back to decodedMessage.
src/utils/downloadFile.ts Widens the data parameter type from string to BlobPart, enabling Uint8Array to be passed directly to Blob without any other logic changes.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant UI as TopicMessage Component
    participant Memo as useMemo (decode logic)
    participant DL as createAndDownloadFile
    participant Blob as Browser Blob API

    UI->>Memo: message (base64 string), isSchematized
    alt non-schematized base64 string
        Memo->>Memo: atob(message) → binary string
        Memo->>Memo: Uint8Array from binary (rawBytes)
        Memo->>Memo: utf8Decoder.decode(bytes) → decodedMessage (lossy)
    else schematized / non-string
        Memo->>Memo: "rawBytes = undefined"
        Memo->>Memo: "decodedMessage = string/JSON"
    end
    Memo-->>UI: "{preparedMessage, decodedMessage, isJson, rawBytes}"

    note over UI: Preview & Clipboard use decodedMessage (UTF-8 string)
    note over UI: Download uses rawBytes ?? decodedMessage

    UI->>DL: rawBytes (Uint8Array) OR decodedMessage (string)
    DL->>Blob: new Blob([data])
    Blob-->>DL: blob URL
    DL->>DL: trigger anchor click → download
    DL->>Blob: revokeObjectURL
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant UI as TopicMessage Component
    participant Memo as useMemo (decode logic)
    participant DL as createAndDownloadFile
    participant Blob as Browser Blob API

    UI->>Memo: message (base64 string), isSchematized
    alt non-schematized base64 string
        Memo->>Memo: atob(message) → binary string
        Memo->>Memo: Uint8Array from binary (rawBytes)
        Memo->>Memo: utf8Decoder.decode(bytes) → decodedMessage (lossy)
    else schematized / non-string
        Memo->>Memo: "rawBytes = undefined"
        Memo->>Memo: "decodedMessage = string/JSON"
    end
    Memo-->>UI: "{preparedMessage, decodedMessage, isJson, rawBytes}"

    note over UI: Preview & Clipboard use decodedMessage (UTF-8 string)
    note over UI: Download uses rawBytes ?? decodedMessage

    UI->>DL: rawBytes (Uint8Array) OR decodedMessage (string)
    DL->>Blob: new Blob([data])
    Blob-->>DL: blob URL
    DL->>DL: trigger anchor click → download
    DL->>Blob: revokeObjectURL
Loading

Reviews (2): Last reviewed commit: "Merge branch 'main' into fix/topic-messa..." | Re-trigger Greptile

…F-8 text

Topic message download reused the UTF-8-decoded preview string for the
Download button. For non-UTF-8 binary payloads this silently corrupted
the data (invalid byte sequences replaced with U+FFFD), so the
downloaded file no longer matched the original message bytes.

Download now uses the raw decoded Uint8Array directly; the preview
string is still used for JSON/text rendering and clipboard copy.

Fixes ydb-platform#4129
@astandrik astandrik changed the title fix: download raw bytes for binary topic messages instead of lossy UTF-8 text fix: preserve raw bytes when downloading topic messages Jul 15, 2026
@astandrik

Copy link
Copy Markdown
Collaborator

@greptile review

@astandrik astandrik left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The implementation looks correct, but the binary download path needs focused regression coverage before merge.

@krasnovdm
krasnovdm requested a review from astandrik July 21, 2026 08:28
Adds a component test asserting the Download button receives the raw
Uint8Array bytes for non-UTF-8 payloads (and unchanged bytes for plain
UTF-8 text), so reverting to the lossy decodedMessage string would fail
the suite.

Also gives the Download button an explicit aria-label (matching how
ClipboardButton already labels itself) so the test - and screen readers -
can target it without relying on the hover/focus-only tooltip, and
polyfills the Encoding API in jsdom test setup, which TopicMessage needs
at module load time.

Addresses review feedback on ydb-platform#4130.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 486cda798f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

e.stopPropagation();
createAndDownloadFile(
decodedMessage,
rawBytes ?? decodedMessage,

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.

@astandrik
astandrik added this pull request to the merge queue Jul 23, 2026
Merged via the queue into ydb-platform:main with commit 3ad3ce9 Jul 23, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Topic message download saves UTF-8-decoded text instead of raw bytes for binary payloads

3 participants