fix: preserve raw bytes when downloading topic messages#4130
Open
krasnovdm wants to merge 2 commits into
Open
Conversation
…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
Collaborator
|
@greptile review |
astandrik
requested changes
Jul 15, 2026
astandrik
left a comment
Collaborator
There was a problem hiding this comment.
The implementation looks correct, but the binary download path needs focused regression coverage before merge.
| e.stopPropagation(); | ||
| createAndDownloadFile( | ||
| decodedMessage, | ||
| rawBytes ?? decodedMessage, |
Collaborator
There was a problem hiding this comment.
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:
- Use legacy/non-schematized payload
//4AYYA=(bytesff fe 00 61 80). - Click Download.
- 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 testrun passes without covering this Download path.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Uint8Arraydirectly; preview/clipboard still use the best-effort UTF-8 stringFixes #4129
Test plan
tsc --noEmitpasseseslinton changed files — no new warnings/errorsGreptile 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
Uint8Arrayfromatobis passed directly toBlob, preserving every original byte.TopicMessage.tsx:rawBytes(aUint8Array) is captured in theuseMemoalongsidedecodedMessage; the download button usesrawBytes ?? decodedMessage, while preview and clipboard continue to use the best-effort UTF-8 string. All fallback paths (schematized messages, non-string values, failedatob) safely fall back todecodedMessage.downloadFile.ts: Thedataparameter ofcreateAndDownloadFileis widened fromstringtoBlobPart, the minimal typing change needed to acceptUint8Array.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
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%%{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: revokeObjectURLReviews (2): Last reviewed commit: "Merge branch 'main' into fix/topic-messa..." | Re-trigger Greptile