Skip to content

0.3.2 --- download_to_file reports the writes the disk refused - #19

Merged
Sunrisepeak merged 3 commits into
mcpplibs:masterfrom
speak-agent:fix/download-write-errors
Sep 29, 2026
Merged

Sunrisepeak merged 3 commits into
mcpplibs:masterfrom
speak-agent:fix/download-write-errors

Conversation

@speak-agent

Copy link
Copy Markdown
Contributor

Problem

HttpClient::download_to_file (0.2.9 through 0.3.1) called ofs.write(...) for each body chunk without checking the stream, counted downloaded += n from the network, and set result.bytesWritten = downloaded after ofs.close() without checking that either. On a full disk (ENOSPC) the transfer therefore "succeeded". Measured on a real download: 420,831,054 bytes received, 220,979,200 bytes left on disk, bytesWritten == 420,831,054, ok() == true. The caller (xlings) then blamed the source ("sha256 mismatch") instead of the local disk.

Change (0.3.2)

  • Each chunk is flushed and the stream is checked. On the first failure the transfer stops reading, error becomes write <path>: <reason> (the reason is errno through std::generic_category, e.g. No space left on device), and the connection is dropped instead of being returned to the pool with the rest of the body still on it.
  • close() is checked the same way, since buffered bytes only reach the file at flush.
  • New API on DownloadToFileResult, both additive and placed after the existing fields:
    • bool writeFailed is true when the fault is the destination and not the source: a failed write, a failed close, or a file that could not be opened (that error string is unchanged: Cannot open file: <path>).
    • std::int64_t bytesReceived is the number of body bytes the connection delivered.
  • bytesWritten now counts bytes the file accepted, where it used to count bytes the network delivered. The two are equal for every transfer that did not fail to write. When a write fails, the chunk that failed is not counted, though the file may hold part of it, so the value is a floor on the file's size and never above it. This is the one meaning change.
  • http.cppm gains a global module fragment with <cerrno>; import std does not carry the errno macro.
  • Version 0.3.1 to 0.3.2 in mcpp.toml, a CHANGELOG entry, and the README install lines.

Tests

tests/test_pool.cpp uses the existing in-process TLS server:

  • ADownloadIntoAFullDeviceFailsAsALocalWriteAndCountsNothing (Linux only, skipped without /dev/full) downloads 64 KiB into /dev/full and asserts: not ok(), writeFailed, statusCode == 200, bytesWritten == 0, 0 < bytesReceived < 64 KiB (reading stopped), the error starts with write /dev/full: and carries the ENOSPC text, and a following download to a good path succeeds on a new connection (accepts() == 2), so the dirty connection was not reused.
  • ADestinationThatCannotBeOpenedIsALocalFailure pins writeFailed for the open failure.

Checked by mutation: with the stream checks removed, the first test fails on ok() (true) and bytesWritten (65536).

mcpp test: all four test binaries pass locally (test_framing, test_pool 27 tests, test_download and test_resolver, which reach the network). tools/template_smoke.sh passes.

download_to_file called ofs.write for each body chunk without looking at the
stream, added each chunk's size to bytesWritten from what the network delivered,
and set bytesWritten after ofs.close() without looking at that either. On a full
disk (ENOSPC) the transfer therefore succeeded: a 420,831,054 byte download left
220,979,200 bytes on disk while bytesWritten said 420,831,054 and ok() was true,
and the caller blamed the source (a checksum mismatch) for what was the local
disk.

Each chunk is now flushed and the stream checked. On the first failure the
transfer stops reading, error becomes "write <path>: <reason>" with the reason
taken from errno, and the connection is dropped instead of returned to the pool
with the rest of the body still on it. Closing the file is checked the same way.

Additive: DownloadToFileResult::writeFailed says the fault is the destination
and not the source (a failed write, a failed close, or a file that could not be
opened), and bytesReceived carries the network count. bytesWritten now counts
bytes the file accepted; the two are equal for every transfer that did not fail
to write.

Tests: a download into /dev/full (Linux) against the in-process TLS server
asserts not ok, writeFailed, bytesWritten == 0, the ENOSPC text, that reading
stopped after the first refused chunk, and that the next request opens a new
connection. Verified by mutation: with the stream checks removed it reports
ok() and bytesWritten == 65536.
Version, CHANGELOG entry, and the README install lines name this release.
The index now requires mcpp >= 2026.9.18.3, and MCPP_VERSION pinned 2026.8.29.1,
so every `mcpp` invocation that needed the index ended at

    error: index requires mcpp >= 2026.9.18.3 but this is mcpp 2026.8.29.1 [E0006]

That is the failure the "Smoke-test the project templates" step has reported on
master since the scheduled run of 2026-09-21. MCPP_VERSION is the only place the
version is named; both jobs' install steps and both cache keys read it.
@Sunrisepeak
Sunrisepeak merged commit f26a77f into mcpplibs:master Sep 29, 2026
2 checks passed
Sunrisepeak added a commit to mcpplibs/mcpp-index that referenced this pull request Sep 29, 2026
xpkg 0.0.60 (openxlings/libxpkg#45): ExecutionContext::hook_log -- a hook's
child processes write to a log file instead of the caller's terminal -- and
pkginfo.build_dep reads the variable xlings exports for a namespaced name.

tinyhttps 0.3.2 (mcpplibs/tinyhttps#19): download_to_file reports a write the
disk refused (writeFailed, bytesWritten counts what reached the file,
bytesReceived what the network delivered).

The GitHub tag tarballs and the GitCode mirror copies (mcpp-res/xpkg,
mcpp-res/tinyhttps) were downloaded in full and are byte-identical:
xpkg 348d78e8...ce5a, tinyhttps 6c9af965...fe5b. Added to all three platform
blocks of each recipe; checked by loading both files with lua.
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