Skip to content

Count a firmware download once, not once per range request (#188) - #247

Merged
openipc-ai merged 2 commits into
masterfrom
count-a-download-once
Sep 21, 2026
Merged

openipc-ai merged 2 commits into
masterfrom
count-a-download-once

Conversation

@openipc-ai

Copy link
Copy Markdown
Collaborator

Closes #188. Unblocks #184 from using the downloads table.

Download.record ran once per HTTP request, and a browser or download manager fetching an 8–32 MB image asks for it in chunks — so the table counted requests, not downloads. Unevenly, too: the 16 MB ultimate images chunk hardest, so the chips people care most about were inflated most.

The error is bursty, which is why it was invisible

Confirmed against the nginx log before changing anything. Over the fourteen days the host retains, range responses on this path run from none to 99 in a day:

range responses status-200 table
10 Sep 99 55 read as a spike; wasn't one
11 Sep 76 90
12 Sep 39 75
20 Sep 0 156 recorded exactly 156 — no error at all

So on a given day the table is either perfect or 180% high, depending on whether a few clients happened to use range requests. A number that is sometimes right is harder to distrust than one that is always wrong.

The direction of the fix matters more than the fix

The first chunk is the download: a Range starting at byte 0 counts, so does no Range at all, and everything after is that same fetch continuing.

The bug this replaces made the table quietly high, which at least looks suspicious. Being too strict would make it quietly low — and an uncounted download is indistinguishable from nobody downloading. So the match tolerates surrounding and internal whitespace, and is anchored so a unit merely ending in bytes is not mistaken for the bytes unit.

That second point came out of revert-testing rather than design: my first anchoring guard could not be made to fail by any realistic header, so it was proving nothing. Working out why is what surfaced the whitespace case, which is a real way a legitimate first chunk could have gone uncounted.

Exercised against a real image, not only in the suite

On dev, one plain fetch of a 16 MB image followed by eight continuation chunks:

rows before: 2537
rows after:  2538
9 HTTP requests wrote 1 row

History is left alone

The 2,558 rows written before today meant something else. Rewriting them would make the table agree with itself and disagree with the nginx log, which is the only independent record of what actually happened. The date the meaning changed is recorded on Download itself, so a chart that crosses it can say what it is comparing.

Verification

774 tests. Three guards, each confirmed by reverting it: counting every request again, treating any Range as a continuation, and dropping the anchor. Fourteen cases covering plain requests, first chunks, continuations, suffix ranges, other units, whitespace, a full chunked fetch, and two separate downloads.

No rubocop offences added.

Not included

Counting the 302s this action answers for a combination that does not exist or does not fit — 44 yesterday, 116 on 16 September. #188 lists it as separately worth a count and it is; those are the wizard's dead ends and they leave no trace beyond the log. But it needs somewhere to go — a column or a table — and that is a wider change than the one blocking #184.

Download.record ran once per HTTP request, and a browser or download manager
fetching an 8-32MB image asks for it in chunks -- so the table counted
requests, not downloads. Unevenly, too: the 16MB ultimate images chunk hardest,
so the chips people care most about were inflated most.

Confirmed against the nginx log before changing anything. Over the fourteen
days the host retains, range responses on this path run from none to 99 in a
day: 10 September was 99 range responses against 55 real downloads, which read
as a spike in the table and was not one. Yesterday there were none at all and
the table's 156 rows matched the log's 156 status-200s exactly. The error is
bursty, driven by a handful of clients, which is what makes it hard to see.

The first chunk is the download. A Range starting at byte 0 counts, so does no
Range at all, and everything after is that same fetch continuing.

That direction matters. The bug this replaces made the table quietly high,
which at least looks suspicious; being too strict would make it quietly low,
and an uncounted download is indistinguishable from nobody downloading. So the
match tolerates surrounding and internal whitespace, and is anchored so a unit
merely ending in "bytes" is not mistaken for the bytes unit.

History is left alone. The 2,558 rows written before today meant something
else, and rewriting them would make the table agree with itself and disagree
with the nginx log -- the only independent record of what happened. The date
the meaning changed is recorded on Download itself, so a chart crossing it can
say what it is comparing.

774 tests. Three guards, each confirmed by reverting it -- after the first
version of the anchoring probe turned out to be untestable with any realistic
header, which is how the whitespace case was found.

Not included: counting the 302s this action answers for a combination that
does not exist or does not fit (44 yesterday, 116 on 16 September). #188 lists
it as separately worth a count and it is, but it needs somewhere to go -- a
column or a table -- and that is a wider change than the one this issue is
blocking.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Count firmware downloads once across range requests

🐞 Bug fix 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Count plain requests and byte ranges beginning at zero as firmware downloads.
• Ignore continuation, resumed, suffix, and non-byte ranges to prevent inflated metrics.
• Preserve historical rows and document when download-count semantics changed.
Diagram

graph TD
  A["Download request"] --> B{"First chunk?"} -->|Yes| C["Record download"] --> D["Send firmware"]
  B -->|No| D
Loading
High-Level Assessment

The request-header check is the best fit for this scope: it corrects counting at the existing write point without migrations or persistent deduplication state. Session-, IP-, or time-window-based deduplication would introduce collision and retry ambiguity, while log-based reconciliation would not keep the application table accurate in real time.

Files changed (3) +182 / -1

Bug fix (1) +32 / -1
socs_controller.rbRecord only the initial firmware range request +32/-1

Record only the initial firmware range request

• Guards Download.record with a new first_chunk? predicate. Plain requests and whitespace-tolerant bytes ranges starting at zero count, while continuation, resumed, suffix, and non-byte ranges do not.

app/controllers/cameras/socs_controller.rb

Tests (1) +136 / -0
download_counting_test.rbCover download counting across HTTP range variants +136/-0

Cover download counting across HTTP range variants

• Adds integration coverage for plain, initial, continuation, resumed, suffix, non-byte, and whitespace-bearing Range requests. It also verifies chunked and separate downloads produce correct counts without changing stored row attributes.

test/controllers/download_counting_test.rb

Documentation (1) +14 / -0
download.rbMark the download-counting semantics transition +14/-0

Mark the download-counting semantics transition

• Adds COUNTS_ONE_ROW_PER_DOWNLOAD_FROM for consumers interpreting historical metrics. Documents that pre-transition rows represent HTTP requests and remain intentionally unchanged.

app/models/download.rb

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 21, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Metadata probes inflate download totals ✓ Resolved 🐞 Bug ≡ Correctness
Description
first_chunk? classifies every request without a Range header as a download without checking
whether the request method is HEAD. Because the endpoint is a GET route that also handles its
corresponding metadata request, a client probing the file before fetching it records one row for the
probe and another for the download.
Code

app/controllers/cameras/socs_controller.rb[R313-316]

+    def first_chunk?
+      range = request.headers['Range'].to_s.strip
+
+      range.empty? || range.match?(/\Abytes\s*=\s*0-/i)
Evidence
The download endpoint is declared as a GET member route, while the action executes the new counting
condition before send_file; the predicate inspects only the Range value and treats its absence as
a first chunk. The added tests exercise only GET requests, leaving the metadata-request path
unguarded.

config/routes.rb[198-205]
app/controllers/cameras/socs_controller.rb[222-226]
app/controllers/cameras/socs_controller.rb[313-317]
test/controllers/download_counting_test.rb[45-54]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Metadata-only HEAD requests have no Range header and therefore satisfy `first_chunk?`, allowing a probe followed by its GET to create two download rows.
## Fix Focus Areas
- app/controllers/cameras/socs_controller.rb[225-225]
- app/controllers/cameras/socs_controller.rb[313-317]
- test/controllers/download_counting_test.rb[45-55]
## Recommended Fix
Require a body-fetching GET before calling `Download.record`, or make `first_chunk?` return false for HEAD requests. Add an integration test issuing HEAD followed by GET and assert that only the GET creates a row.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Ignored ranges hide full downloads ✓ Resolved 🐞 Bug ≡ Correctness
Description
first_chunk? returns false for every non-bytes range unit even though download_full_image
still proceeds to send_file rather than rejecting that request. When the file server ignores an
unsupported unit and sends the complete representation, the successful full download bypasses
Download.record, including the kbytes case the new test explicitly codifies.
Code

app/controllers/cameras/socs_controller.rb[R314-316]

+      range = request.headers['Range'].to_s.strip
+
+      range.empty? || range.match?(/\Abytes\s*=\s*0-/i)
Evidence
The controller condition suppresses recording unless the header is empty or starts with bytes=0-,
but it calls send_file for all other Range values. The new test proves that kbytes=0-4 follows
this uncounted path, yet it does not assert that the request is rejected or prevented from receiving
the file.

app/controllers/cameras/socs_controller.rb[225-226]
app/controllers/cameras/socs_controller.rb[313-317]
test/controllers/download_counting_test.rb[92-98]
deploy/nginx/sites-available/org.openipc[111-136]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Unsupported or invalid Range forms are classified as continuations even though the action still sends the file, allowing a full response to go uncounted.
## Fix Focus Areas
- app/controllers/cameras/socs_controller.rb[313-317]
- test/controllers/download_counting_test.rb[92-98]
## Recommended Fix
Return false only for a syntactically valid bytes continuation or suffix range. Treat unsupported units and malformed ranges as full-download candidates, and update the integration tests to assert both the response behavior and the resulting count.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Retries still inflate download totals 🐞 Bug ≡ Correctness
Description
first_chunk? records every request that starts again at byte zero because it has no identity or
deduplication for an in-progress fetch. If a failed first request is retried with no Range or
another zero-start range, each attempt reaches Download.record even though the client is
continuing one download.
Code

app/controllers/cameras/socs_controller.rb[R314-316]

+      range = request.headers['Range'].to_s.strip
+
+      range.empty? || range.match?(/\Abytes\s*=\s*0-/i)
Evidence
Every true result from first_chunk? invokes Download.record, and that method always creates a
fresh row without a uniqueness key. The tests independently establish that both an absent Range and
a zero-start Range increment the table, but do not exercise a repeated first request from the same
fetch.

app/controllers/cameras/socs_controller.rb[222-226]
app/controllers/cameras/socs_controller.rb[313-317]
app/models/download.rb[33-43]
test/controllers/download_counting_test.rb[51-68]
test/controllers/download_counting_test.rb[107-121]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Repeated requests beginning at byte zero are independently recorded, so retrying the first chunk can still produce multiple rows for one download.
## Fix Focus Areas
- app/controllers/cameras/socs_controller.rb[225-225]
- app/controllers/cameras/socs_controller.rb[313-317]
- app/models/download.rb[33-43]
- test/controllers/download_counting_test.rb[107-121]
## Recommended Fix
Introduce a fetch identifier that remains stable across requests belonging to one download and make recording idempotent on that identifier. Add tests for repeated zero-start requests while preserving separate counts for genuinely separate downloads.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can add REVIEW.md to your repo root and Qodo follows it on every PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread app/controllers/cameras/socs_controller.rb Outdated
Comment thread app/controllers/cameras/socs_controller.rb Outdated
Comment thread app/controllers/cameras/socs_controller.rb Outdated
Two real bugs, both confirmed by running them before fixing.

A HEAD wrote a row. Rails routes it to the same action, the reader gets no
body, and the table recorded a download that did not happen -- eighteen of
these reached the path in the fourteen days of log the host keeps, against
1,871 GETs. Small, and wrong in the direction that invents readers.

And a range in a unit the file server does not implement is not a
continuation. An unrecognised unit is ignored and the whole representation
sent: `Range: kbytes=0-4` comes back 200 with all 8,388,608 bytes, measured,
not assumed. The rule dropped it -- and the new test file asserted the drop,
which is how a bug gets written down as a rule. That test now asserts the
reader got the entire image and the row exists, for three unrecognised units.

Rewritten as a sequence rather than one condition, because there are now three
separate questions -- is this a fetch at all, is this a byte range, does it
start at zero -- and one boolean expression hid the second.

778 tests. Three guards, each confirmed by reverting it: counting HEAD again,
dropping unrecognised units again, and counting every request. No rubocop
offences added.
@openipc-ai
openipc-ai merged commit 518fd1a into master Sep 21, 2026
2 checks passed
@openipc-ai
openipc-ai deleted the count-a-download-once branch September 21, 2026 12:15
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.

Count a firmware download once, not once per range request

1 participant