Media errors without the link; session files replaced whole - #1
Merged
Merged
Conversation
POST /chat/getBase64FromMediaMessage answers a failed download with the
error's text, Baileys' "Failed to fetch stream from <link>", so the HTTP
answer carries WhatsApp's signed CDN link (oh, oe, the _nc_ parameters and
the directPath). The same object is what the error handler posts to the
errors webhook and what the S3 upload logs at ERROR.
The new file drives the real route against WhatsApp's CDN host, answered by
the undici mock agent: 404 with the phone refusing the re-upload, 410 with
no answer in time, 403 on an expired link, a re-upload whose new link fails
too, and a 403 on a valid link with and without reupload: false. It asserts
the whole answer, its headers and the output meanwhile, the thrown object,
and the S3 upload's error log. All eight fail on the code as it is.
media-skip-reupload.test.ts asserted the old text ("TypeError: fetch failed")
for a fallback that fails without an HTTP status; it now expects the stable
reason text (error kind and network code), and fails until the fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
getBase64FromMediaMessage answered a failed download with the error's own
text, which for Baileys' download error is "Failed to fetch stream from"
and the signed CDN link. Its 400 now carries a stable reason instead: the
CDN's HTTP status ("The media could not be downloaded (HTTP 404)"), or,
with no status, the kind of error and its network code. reupload and
reuploadReason are unchanged, and a text Evolution threw itself (it names
no link) stands. The object is built fresh, so no Boom data (data.url)
travels with it: not to the HTTP answer, not to the errors webhook main.ts
posts, and not to the S3 upload's error log.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…concurrent one The Prisma auth store writes each signal key over its file in place (fs.writeFile truncates, then writes), and reads a file that does not parse as no key at all. Four cases, on the real store and a real filesystem: - Crash: a child process (the real store, bundled with esbuild) writes one key over and over with 4 MB values that name their write, and is SIGKILLed at random moments, 64 times across 4 folders. After each kill the file must hold one whole value. On the code as it is, one run: 32 of 64 whole, 26 torn, 6 empty. - A write that fails partway: the child runs under a file size limit (ulimit -f), so the second write fails with EFBIG after 1 MiB. The error reaches keys.set, and the previous value must survive; it is torn. - Concurrency: twelve writes of the same key at once must end with the last one, whole; they end torn. - A value written is read back by the store and by a new one after a restart (passes today, kept as the guard for the fix). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Prisma auth store now writes a key through writeFileAtomic (src/utils/atomic-file.ts): the value goes to a temp file in the same directory (opened exclusively), is flushed with fsync, and is renamed over the key file, which POSIX makes atomic; the directory is then flushed, so the rename survives a power loss (renames that land together share one directory flush). A failed write removes its temp file, leaves the previous value and rejects, so keys.set fails as saveCreds does. Writes of the same file run one at a time in call order, so the last one asked for stays. A killed writer can leave its temp file behind; the store removes those when it opens its folder (a temp file of a process that no longer runs, or this process's own that it is not writing), and leaves one of another running process alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…marker The marker next to the session's key files is written in place (fs.writeFile). A write that fails partway (injected here as a full disk: half the bytes land, then ENOSPC) rejects as it should, but leaves half a marker over the whole one, which reads as pending with no instance name. The previous marker must survive, with no temp file left. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
writeLogoutMarker writes through writeFileAtomic, as the key files do: a temp file in the same directory, fsync, rename over the marker, fsync of the directory. A write that fails keeps the previous marker, removes its temp file and rejects, so the logout or delete still answers 500 as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…laced whole Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Two hardening fixes, each a failing test commit then its fix, verified red then green commit by commit.
A failed media download answers without the media link.
getBase64FromMediaMessageput the raw download error in its 400 answer, and that text is Baileys'Failed to fetch stream from https://mmg.whatsapp.net/...?oh=...&oe=..., so the signed media link left the process (in the 404/410/403 cases and after a re-upload). The same thrown object reached the errors webhook and the S3 upload's error log line. The answer now readsThe media could not be downloaded (HTTP <status>);reuploadandreuploadReasonare unchanged.Session files are replaced whole, never written in place. Signal key files and the pending-logout marker were written with an in-place write, so a crash, a full disk or two overlapping writes left a torn or empty file, which reads as no key. They now go through
src/utils/atomic-file.ts: a temp file in the same directory, fsync, rename over the target, directory fsync; writes of one file are queued in call order, and temp files left by a killed writer are removed when the store opens.The crash test runs the real store in a child process writing 4 MB values and SIGKILLs it at random moments, 64 times. On the old code one run gave 32 whole, 26 torn and 6 empty files; with the fix all 64 are whole. A real partial write (
ulimit -f, EFBIG) and 12 concurrent writes also left torn files before the fix.Not covered: the provider-files store (it writes on its own server) and Redis (no files).
🤖 Generated with Claude Code