fix(group,template,status): bound participant arrays and template renders, and reconcile status media orphans - #964
Merged
Merged
Conversation
…ders, and reconcile status media orphans Group: cap participant arrays at 256 per create/batch request (@ArrayMaxSize; the engine works the list sequentially, so an unbounded array turned one HTTP call into unbounded serial engine work). A settings patch that fails midway now throws an error naming the failed field and the ones already applied, keeping the underlying HTTP status, instead of leaving a silently half-applied patch. Template: cap the final rendered text of a send-template request at 64 KiB (TEMPLATE_RENDER_MAX_CHARS). Caller-supplied variables could inflate a small template unboundedly toward the engine/DB; an over-cap render is now rejected with a 400 naming the limit, never silently truncated. Status store: ingest now saves the row BEFORE writing its media file, so a crash mid-ingest leaves a consistent media-omitted row instead of a permanent orphan file. The TTL purge keeps a row whose media delete fails (retried on the next sweep) instead of orphaning its file. A new hourly reconciliation sweep (STATUS_ORPHAN_SWEEP_INTERVAL_MS) deletes statuses/ files no row references after a grace window (STATUS_ORPHAN_GRACE_MS, default 1h each).
rmyndharis
force-pushed
the
fix/bounds-leftovers
branch
from
July 27, 2026 08:07
e094985 to
3cb2e3c
Compare
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
Three bounds/consistency hardenings across the group, template, and status-store modules.
1. Group: bound participant arrays, report partially-applied settings patches
CreateGroupDto.participantsand the sharedParticipantsDto(add / remove / promote / demote) now enforce@ArrayMaxSize(256). The engine works a participant list sequentially — one WhatsApp round-trip per participant with an honest per-participant result — so an unbounded array turned a single HTTP request into unbounded serial engine work. 256 stays well inside WhatsApp's own group-size limit; larger imports batch across requests. Over-limit bodies now fail validation with a 400 before any engine call. The OpenAPI schema gainsmaxItems: 256on both DTOs (snapshot regenerated).GroupService.updateGroupSettingsapplied fields sequentially and, on a mid-patch engine failure, propagated the bare engine error — leaving the group silently half-patched (e.g.ephemeralSecondsapplied,announcefailed). A failure on a later field now throws an error that names the failed field and the ones already applied, preserving the underlying HTTP status (a 403 refusal stays 403, a 501 stays 501). A failure on the first applied field propagates unchanged — nothing was applied, so there is no partial state to report. The existing ordering rule (ephemeral first, so wwjs's deterministic 501 never half-applies a patch) is preserved.2. Template: cap the post-render length
MessageService.sendTemplatesubstituted caller-supplied variables into the stored template with no bound on the final text, so a small template plus a huge variable inflated the payload toward the engine and the messages table unboundedly. The joined rendered text is now capped at 64 KiB (TEMPLATE_RENDER_MAX_CHARS, also WhatsApp's own text-message ceiling). An over-cap render is rejected with a 400 naming the limit — never silently truncated; at-or-under renders are byte-identical to before.3. Status store: safe media write ordering, purge retry, orphan reconciliation
ingestpreviously wrote the media file before saving the row, so a crash in between leaked a permanent orphan file (and the unique-race loser needed bespoke reaping). The row is now saved first — media-less outcomes settled up front, a keepable blob saved as omitted-for-now — and the file write plus row update happen after. A crash mid-ingest now leaves a consistent media-omitted row (worst case: a missing attachment); the only remaining orphan source is the narrow window between the file write and the row update. The race loser no longer writes a file at all, so the reaping-on-save-failure logic is gone.purgeExpiredswallowed adeleteFilefailure and deleted the row anyway, permanently orphaning the file. A row whose media delete fails is now kept and retried on the next sweep; file-less rows in the same batch are still purged. (An already-missing file counts as a successful delete, so a gone file never wedges its row.)STATUS_ORPHAN_SWEEP_INTERVAL_MS) listsstatuses/media keys viaStorageService.listFiles()and deletes ones no row references — scoped to thestatuses/prefix so shared chat media is never touched. A file is deleted only after being seen unreferenced for a grace window (STATUS_ORPHAN_GRACE_MS, default 1h; first-seen tracked in memory, so a restart fails safe by restarting the clock), which keeps mid-ingest files safe.Tests
TEMPLATE_RENDER_MAX_CHARSoverride honored.deleteFilekeeps the media row (text row still purged) and a later healthy sweep finishes; orphan sweep reaps an unreferenced file only after the grace window, never touches row-referenced or non-statuses/files, honors the grace override; a file that becomes referenced between sightings is never reaped; race tests re-pinned to row-first semantics (no file is written before the row lands).npx jest src/modules/group src/modules/template src/modules/status-store src/modules/message— 15 suites / 284 tests pass;npx eslintclean on the touched modules;npm run buildclean;npm run openapi:checkpasses (snapshot regenerated for the DTOmaxItems).Risks
.env.example; the template cap is env-tunable, the participant cap is a DTO constant.status.receivedwebhook fires.