Skip to content

feat: send bytes= query param on upload measurements#120

Closed
devandrepascoa wants to merge 1 commit into
mainfrom
apascoa/upload-bytes-param
Closed

feat: send bytes= query param on upload measurements#120
devandrepascoa wants to merge 1 commit into
mainfrom
apascoa/upload-bytes-param

Conversation

@devandrepascoa
Copy link
Copy Markdown
Contributor

@devandrepascoa devandrepascoa commented Jun 5, 2026

Adds the upload size signal needed by the speed.cloudflare.com server cap.

Upload (POST /__up) requests now include bytes=<size>, matching download requests, so the server can enforce a byte ceiling on the upload body.

No user-visible behavior change for normal clients: bytes= matches the upload body size.

Copy link
Copy Markdown

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

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

Review summary

This PR adds the bytes=<size> query param to upload requests (previously only downloads sent it), and teaches LoggingBandwidthEngine to log the server-reported cf-meta-upload-bytes value when present, so capped uploads log the accepted byte count rather than the client-requested size.

What changed:

  • BandwidthEngine.ts: bytes= is now set unconditionally on the query string (both up and down) instead of download-only.
  • LoggingBandwidthEngine.ts: two new exported helpers (parseUploadBytesHeader, getLoggedBytes) plus #uploadBytes state captured in the response hook and consumed in #logMeasurement.
  • New unit test file covering both helpers (good — validates the safe-integer / regex guards).
  • tests/e2e/speedtest.test.ts: ~264 lines changed, but almost entirely reindentation from re-wrapping the it(...) call plus an eslint-disable compat/compat header — no substantive test-logic change.
  • Results.test.ts: unrelated type/formatting cleanup.

Overall: the core change is small, correct, and well-tested. The #uploadBytes state follows the existing #token/#requestTime convention and getLoggedBytes correctly guards on type === 'up', so download logging is unaffected. No security concerns. A few minor points inline.

Comment thread src/engines/BandwidthEngine/BandwidthEngine.ts
): number =>
measData.type === 'up' && uploadBytes !== undefined
? uploadBytes
: measData.bytes;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

After this change the logged bytes field is semantically asymmetric: for up it can be the server-accepted byte count, while for down it stays the requested count. That's intentional per the PR, but it's a non-obvious contract for log consumers. Consider a short doc comment on getLoggedBytes (or the logData.bytes line) noting that upload bytes may reflect the server cap, so future readers don't assume bytes is always the requested size.


// get request time
this.#requestTime = +r.headers.get(`cf-meta-request-time`)!;
this.#uploadBytes = parseUploadBytesHeader(r.headers);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

#uploadBytes is set here in the response hook and cleared in #logMeasurement — same lifecycle as #token/#requestTime, so this is consistent with the existing pattern. One thing to confirm: if a response hook fires for a request that never produces a measurement result (e.g. an error/abort path), #uploadBytes would persist until the next #logMeasurement. This is exactly the pre-existing behavior for #token/#requestTime, and getLoggedBytes guards on type === 'up', so a stale value can't leak into a download log. No change required — flagging for awareness.

Comment thread tests/e2e/speedtest.test.ts Outdated
import { describe, it, expect } from 'vitest';
import SpeedTest from '../../src/index.ts';

/* eslint-disable compat/compat */
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The bulk of this file's diff (~264 lines) is pure reindentation from re-wrapping the it(...) call, plus this eslint-disable compat/compat. That makes the upload-bytes change harder to review and risks merge-conflict noise. If the reindent isn't strictly required for the feature, consider splitting it into a separate formatting-only commit/PR. Also worth a brief comment explaining why compat/compat is disabled here (which API triggered it), since blanket file-level disables tend to mask future real violations.

@ask-bonk
Copy link
Copy Markdown

ask-bonk Bot commented Jun 5, 2026

Review posted successfully to PR #120 with 4 inline comments.

Review summary

I reviewed PR #120, verified the build/lint pass, and ran the unit tests (98 passing). Posted a review with a summary and 4 inline comments.

What the PR changes:

  • BandwidthEngine.ts — sends bytes=<size> on the query string for all requests instead of download-only. Since the value equals the upload body size from genContent(numBytes), it's a no-op for uncapped servers and only meaningful when the server caps uploads.
  • LoggingBandwidthEngine.ts — adds parseUploadBytesHeader (with \d+ regex + Number.isSafeInteger guards) and getLoggedBytes, captures the cf-meta-upload-bytes response header in #uploadBytes, and logs the server-accepted count for capped uploads.
  • New unit test — covers both helpers including invalid-header cases.
  • tests/e2e/speedtest.test.ts — 264 lines changed but almost entirely reindentation + an eslint-disable compat/compat; no real test-logic change.
  • Results.test.ts — unrelated type/formatting cleanup.

Findings (no blockers):

  • Correctness: solid. getLoggedBytes guards on type === 'up', so a stale header can't contaminate download logs; #uploadBytes follows the existing #token/#requestTime lifecycle convention.
  • Security: none.
  • Style/minor: the logged bytes field is now semantically asymmetric (server-accepted for uploads, requested for downloads) — worth a doc comment. The large reindent-only e2e diff and the blanket compat/compat disable add review noise and would be better split out / annotated.

github run

@devandrepascoa
Copy link
Copy Markdown
Contributor Author

/bonk

Copy link
Copy Markdown

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

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

Review summary

This PR sends the bytes=<size> query param on upload requests (previously download-only) and teaches LoggingBandwidthEngine to log the server-reported cf-meta-upload-bytes value when present, so capped uploads log the server-accepted byte count instead of the client-requested size.

What changed (verified against main):

  • BandwidthEngine.ts:348qsParams.bytes = ${numBytes} is now set unconditionally instead of isDown && (...). Since numBytes equals the body produced by genContent(numBytes), this is a no-op signal for uncapped servers and only matters when the server caps.
  • LoggingBandwidthEngine.ts — two new exported pure helpers (parseUploadBytesHeader, getLoggedBytes), plus #uploadBytes state captured in #loggingResponseHook and consumed/cleared in #logMeasurement following the existing #token/#requestTime lifecycle.
  • New unit test file covering both helpers, including invalid-header and download-guard cases.
  • tests/e2e/speedtest.test.ts2 lines added (an eslint-disable compat/compat + explanatory comment for the Promise usage).
  • Results.test.ts — replaces as any with a typed ResultsConfig alias; unrelated cleanup.

Verification: pnpm build, pnpm lint, and pnpm test:unit (98 passing) all pass on the head commit.

Correctness: Requests are issued strictly sequentially (#nextMeasurement only re-fires after the current request resolves), so the single-field #uploadBytes state cannot race across concurrent requests, and getLoggedBytes guards on type === 'up' so a stale upload value can never leak into a download log. The change is small, correct, and well-tested.

Security: None.

A few minor, non-blocking points inline.

Note: I could not reproduce a prior characterization of the e2e diff as ~264 lines of reindentation — the actual diff there is a 2-line addition.


const bytes = Number(value);
return Number.isSafeInteger(bytes) ? bytes : undefined;
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

parseUploadBytesHeader accepts '0' (it passes /^\d+$/ and Number.isSafeInteger(0)), so a cf-meta-upload-bytes: 0 response would make getLoggedBytes return 0 for an upload (since 0 !== undefined). If the server only ever sends this header with a positive accepted size that's fine, but if 0 can mean "rejected / no accepted bytes," logging bytes: 0 may be misleading versus falling back to the requested size. Worth confirming the server contract; a bytes > 0 guard would make the intent explicit if 0 is not a meaningful logged value.

};

/** Upload logs use server-accepted bytes when the server reports a cap. */
export const getLoggedBytes = (
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nice that this is a pure, exported helper. Doc nit: the logged bytes field is now semantically asymmetric — up may be the server-accepted count while down stays the requested count. The existing one-line comment helps; consider extending it to state explicitly that downloads always log the requested size, so log consumers don't assume bytes is uniform across directions.


// get request time
this.#requestTime = +r.headers.get(`cf-meta-request-time`)!;
this.#uploadBytes = parseUploadBytesHeader(r.headers);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

#uploadBytes is set here and cleared in #logMeasurement, mirroring #token/#requestTime. I confirmed requests are sequential (#nextMeasurement only re-fires after the current request's .then), so there is no cross-request race. Small inconsistency: #token/#requestTime reset to null while #uploadBytes resets to undefined. That's correct given its number | undefined type and the !== undefined check downstream — just noting the deliberate divergence.

Comment thread tests/e2e/speedtest.test.ts Outdated

// Run the speed test and wait for completion
// The test environment supports Promise; compat checks target browser consumers.
// eslint-disable-next-line compat/compat
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The disable is reasonable (the test runs in Node where Promise is available; compat/compat targets browser consumers). The comment could name the specific construct triggering the lint (the Promise constructor) so a future reader doesn't have to guess which API this line-scoped disable covers. Minor.

@ask-bonk
Copy link
Copy Markdown

ask-bonk Bot commented Jun 5, 2026

Review posted successfully to PR #120 with 4 inline comments.

Review summary

I checked out PR #120, verified pnpm build, pnpm lint, and pnpm test:unit (98 passing) all pass on the head commit, and read through the actual diff against main.

What the PR changes:

  • BandwidthEngine.ts:348 — sends bytes=<size> on the query string for all requests (replacing the download-only isDown && (...)). Since the value equals the upload body from genContent(numBytes), it's a no-op for uncapped servers and only meaningful when the server caps uploads.
  • LoggingBandwidthEngine.ts — adds two pure exported helpers (parseUploadBytesHeader with /^\d+$/ + Number.isSafeInteger guards, and getLoggedBytes), captures the cf-meta-upload-bytes response header in #uploadBytes, and logs the server-accepted count for capped uploads.
  • New unit test — covers both helpers, including invalid-header and download-guard cases.
  • tests/e2e/speedtest.test.ts — a 2-line addition (line-scoped eslint-disable compat/compat for the Promise usage).
  • Results.test.ts — replaces as any with a typed ResultsConfig alias; unrelated cleanup.

Findings (all non-blocking):

  • Correctness: Solid. I verified requests are strictly sequential (#nextMeasurement only re-fires after the current request resolves), so the single-field #uploadBytes can't race; and getLoggedBytes guards on type === 'up' so a stale value can't contaminate download logs.
  • Inline Test PR automation #1 (correctness edge case): parseUploadBytesHeader('0') returns 0, which getLoggedBytes would log for an upload — worth confirming whether 0 is a meaningful server-accepted value or means "rejected."
  • Inline Fix build in GH actions #2 (style): the logged bytes field is now asymmetric (server-accepted for up, requested for down) — extend the doc comment.
  • Inline CI npm publish #3 (style): #uploadBytes resets to undefined while #token/#requestTime reset to null; correct but noted.
  • Inline Ci npm publish auth #4 (style): name the specific API (Promise constructor) in the compat/compat disable comment.
  • Security: None.

One correction to the prior review thread: I could not reproduce the claim that the e2e diff is ~264 lines of reindentation — the actual change there is a 2-line addition.

No commits pushed.

github run

Include the bytes= query parameter on upload (POST /__up) requests, not
just downloads, so the server can enforce a byte ceiling on the upload
body.
@devandrepascoa
Copy link
Copy Markdown
Contributor Author

/bonk

Copy link
Copy Markdown

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

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

Review summary

This PR makes a single, focused change to BandwidthEngine.ts: the bytes=<size> query param, previously sent only on download requests, is now sent on all bandwidth requests (download and upload).

-    isDown && (qsParams.bytes = `${numBytes}`);
+    qsParams.bytes = `${numBytes}`;

Verification

  • The value numBytes is exactly the upload body size: the POST body is genContent(numBytes) (BandwidthEngine.ts:360), and genContent returns '0'.repeat(numBytes) (BandwidthEngine.ts:61-67). So bytes= accurately reflects the upload body length — matching the PR description.
  • qsParams is a shallow copy of #qsParams (BandwidthEngine.ts:347), so the instance config is not mutated.
  • For uncapped servers this is a no-op signal; it only becomes meaningful when the server enforces a byte ceiling on the upload body.

Correctness: Looks correct. The change does what it claims.

Security: No concerns. No new inputs, no new network surface, no secret handling.

Style: Replacing the isDown && (...) short-circuit with a plain assignment is cleaner. Fine.

Note on prior review comments in this thread

The earlier automated reviews on this PR described a substantially different and much larger changeset — new parseUploadBytesHeader/getLoggedBytes helpers in LoggingBandwidthEngine.ts, a cf-meta-upload-bytes response-header capture, a new unit test file, and changes to tests/e2e/speedtest.test.ts and Results.test.ts. None of those changes exist in PR #120. The actual diff is exactly one line in src/engines/BandwidthEngine/BandwidthEngine.ts (+1/-1, 1 file, 1 commit). The inline findings in those prior reviews (e.g. the bytes=0 edge case, the #uploadBytes lifecycle, the compat/compat disable) do not apply to any code in this PR and can be disregarded.

@devandrepascoa
Copy link
Copy Markdown
Contributor Author

Superseded by a recreated PR from the same branch (to re-trigger Bonk on the slimmed-down change).

@ask-bonk
Copy link
Copy Markdown

ask-bonk Bot commented Jun 5, 2026

@devandrepascoa Bonk workflow was cancelled.

View workflow run · To retry, trigger Bonk again.

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.

1 participant