Skip to content

fix: preserve raw bytes when downloading topic messages#4130

Open
krasnovdm wants to merge 2 commits into
ydb-platform:mainfrom
krasnovdm:fix/topic-message-binary-download
Open

fix: preserve raw bytes when downloading topic messages#4130
krasnovdm wants to merge 2 commits into
ydb-platform:mainfrom
krasnovdm:fix/topic-message-binary-download

Conversation

@krasnovdm

@krasnovdm krasnovdm commented Jul 15, 2026

Copy link
Copy Markdown

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.

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

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.

Please add a regression test for this byte-preserving branch. The fix prevents silent corruption, but the PR adds no test; reverting this line to decodedMessage would still leave the current suite green.

Repro:

  1. Use legacy/non-schematized payload //4AYYA= (bytes ff fe 00 61 80).
  2. Click Download.
  3. Compare the downloaded Blob bytes with the source.

Expected: ff fe 00 61 80. The lossy string path produces ef bf bd ef bf bd 00 61 ef bf bd.

A focused component test can mock createAndDownloadFile, click Download, and assert that its first argument is Uint8Array([255, 254, 0, 97, 128]).

Links:

  • #4129 — original corruption report and end-user reproduction.
  • Unit Tests for this HEAD — current npm test run passes without covering this Download path.

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