Skip to content

fix(antd): resolve shrunk DataMap before sizing streaming downloads - #243

Merged
Nic-dorman merged 3 commits into
mainfrom
bug/stream-content-length-child-datamap
Sep 4, 2026
Merged

fix(antd): resolve shrunk DataMap before sizing streaming downloads#243
Nic-dorman merged 3 commits into
mainfrom
bug/stream-content-length-child-datamap

Conversation

@Nic-dorman

Copy link
Copy Markdown
Member

Summary

Bug report from SDK users: streaming download of larger files returns only a tiny prefix ("only a single chunk"). Root cause: files over 3 × MAX_CHUNK_SIZE (~12.5 MB) upload as a shrunk (child) DataMap, and DataMap::original_file_size() on a child map describes the serialized parent map (a few hundred bytes), not the plaintext. All three streaming paths sized their response from the caller-supplied map:

Site Feeds Impact
rest/data.rs stream_response Content-Length Data loss — hyper frames the body at the declared length, so the download truncates with a "successful" 200
rest/data.rs stream_response_ndjson meta.total_size Wrong byte denominator
grpc/service.rs data_chunk_stream_response x-content-length Wrong byte denominator

The truncation also breaks the documented short-read failure signal that antd-go/antd-js rely on. The buffered path was unaffected because ant-core's data_download resolves child maps internally — but that resolver is private, so antd now resolves on its own.

Fix

  • New antd/src/datamap.rs: resolve a child DataMap to its root form via the public Client::chunk_get + self_encryption::get_root_data_map (block_in_place bridge, mirroring ant-core's private resolver, including its error-taxonomy preservation — missing wrapper chunk stays NotFound, network failures keep their Timeout/Network classification). Note: from_core has no NotFound arm, so a missing chunk still maps to 500/INTERNAL_ERROR today, same as the buffered path — a dedicated 404 mapping would be a separate, all-endpoints change.
  • All three stream response builders resolve up front, size from the root map, and hand the resolved map to file_download_to_sender (which then skips its internal resolution — wrapper chunks are fetched exactly once).
  • Side effect: resolution failures now surface as a proper error response before the stream opens, instead of a truncated 200. The resolving_map NDJSON/gRPC progress phase no longer occurs (resolution completes before the stream opens); proto comment updated.

Verification

  • 2 new unit tests in datamap.rs: a >12.5 MB in-memory encrypt yields a child map that misreports its size and resolves back to the true size/chunk count; a flat map passes through without any fetch. cargo test 52/52, clippy -D warnings clean, fmt clean.
  • Live devnet A/B (25-node local devnet + anvil, 20 MB upload → 9 chunks: 6 content + 3 wrapper, child DataMap):
    • Buggy build (main @ 518db55, dev1): POST /v1/data/stream → HTTP 200, Content-Length: 490 — client received 490 bytes of 20,971,520 as a "successful" download. NDJSON meta.total_size: 490.
    • This branch (e748073 = this HEAD minus a comment fix, dev2): POST /v1/data/stream → HTTP 200, Content-Length: 20971520, 20,971,520 bytes received, sha256 identical to the uploaded file. NDJSON meta.total_size: 20971520, full payload sha256-identical, phases resolved → fetching (no more resolving_map, as documented).
    • Buffered POST /v1/data/get control returned the full file, sha256-identical, in both runs — confirming only the stream sizing was broken.

Linear: V2-1104

🤖 Generated with Claude Code

Nic-dorman and others added 2 commits August 31, 2026 09:05
Files over 3 x MAX_CHUNK_SIZE (~12.5 MB) upload as a shrunk (child)
DataMap, and original_file_size() on such a map describes the serialized
parent map (a few hundred bytes), not the plaintext. All three streaming
paths sized their response from it, so the REST raw stream truncated
every larger download at a bogus Content-Length, and the NDJSON
meta.total_size / gRPC x-content-length denominators were wrong.

Resolve the map to its root form up front (ant-core keeps its own
resolver private, so antd resolves via the public chunk_get +
self_encryption::get_root_data_map), size from the root map, and hand
the resolved map to file_download_to_sender, which then skips its
internal resolution — wrapper chunks are fetched exactly once.
Resolution failures now surface as a proper error response before the
stream opens instead of a truncated 200.

Linear: V2-1104

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stable Rust moved to 1.98 since the last green run and its
result_large_err lint now fires on functions returning
Result<_, tonic::Status> (Status is >=176 bytes): 24 hits in
antd-rust's tonic-generated client stubs (would fail on main too) and
1 on the new antd stream helper. The Status type is fixed by tonic's
service contract and the generated code can't be reshaped, so allow the
lint at those two sites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Nic-dorman

Copy link
Copy Markdown
Member Author

CI red was clippy: stable moved to 1.98 since the last green main run (08-20), and its result_large_err lint now fires on Result<_, tonic::Status> returns (Status ≥176 bytes). 24 hits were in antd-rust's tonic-generated client stubs — that half would fail on main today too — plus 1 on this PR's new stream helper. Fixed with two scoped #[allow(clippy::result_large_err)]s (generated proto module + the one antd fn whose error type tonic's contract fixes). Verified locally with cargo +1.98.0 clippy --all-targets -- -D warnings clean on both crates; fmt + 52/52 tests still green.

…ngth-child-datamap

# Conflicts:
#	antd-rust/src/grpc_client.rs

@dirvine dirvine left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Verdict: APPROVE recommendation — no blocking findings at 96f88a497e204e7af5cdda07aeee7cc4900375a0.

The fix addresses the truncation mechanism rather than papering over the stream: each REST/gRPC handler resolves a child DataMap before deriving Content-Length, NDJSON meta.total_size, or gRPC x-content-length, then passes the resolved map into file_download_to_sender. Because ant-core only resolves when is_child(), that does not fetch wrapper chunks twice. Resolution failure also occurs before the response/stream opens.

Independent review consensus found no correctness blocker. Local validation passed:

  • antd: fmt, 68 tests, clippy (-D warnings), docs build (one pre-existing broken PaymentProof rustdoc-link warning)
  • antd-rust: fmt, 82 unit + 2 integration tests, clippy (-D warnings), docs build
  • targeted DataMap regressions: 2/2 passed
  • GitHub checks: Check (antd), Check (antd-rust), and Security audit all green

Non-blocking follow-ups:

  • The new unit test proves child-map size restoration, but an offline handler-level regression asserting full emitted byte count/header metadata would tighten coverage.
  • The wrapper resolver uses the sequential get_root_data_map path rather than ant-core's parallel resolver. That is a small pre-stream latency cost (normally three wrapper chunks), not a correctness issue.
  • Missing wrapper chunks preserve ant_core::data::Error::NotFound internally, but the existing AntdError::from_core catch-all still exposes this as 500. This is pre-existing/out of scope and is already disclosed in the PR.

Submitted as a comment-only review under the repository review-authority policy; recommendation is APPROVE.

@Nic-dorman
Nic-dorman merged commit 94c89de into main Sep 4, 2026
3 checks passed
@Nic-dorman
Nic-dorman deleted the bug/stream-content-length-child-datamap branch September 4, 2026 09:22
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.

2 participants