Repository navigation
fix: refuse Prometheus endpoint responses over 8 MB - #3541
karl-power merged 1 commit into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🦋 Changeset detectedLatest commit: a78cfa6 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
220a244 to
a78cfa6
Compare
Duplicate code analysisNo new duplicate code introduced by this PR. jscpd compared this branch against |
| text = await readBoundedBody(resp, PROMETHEUS_MAX_RESPONSE_BYTES); | ||
| } catch (err) { | ||
| const tooLargeMessage = `Prometheus at ${redactedTarget} returned more than ${PROMETHEUS_MAX_RESPONSE_BYTES / (1024 * 1024)} MB. Narrow the query or time range.`; | ||
| const genericMessage = `Failed to read the response from Prometheus at ${redactedTarget}`; |
There was a problem hiding this comment.
🔵 minor — A timeout while reading the body is reported as "Failed to read the response", not as a timeout
The 30 s AbortSignal.timeout passed to fetch also covers the body stream. If a slow Prometheus hits it partway through the body, reader.read() rejects with a TimeoutError, and the catch here maps that to the generic message. The fetch catch just above maps the same error to Prometheus request to ${redactedTarget} timed out. Check err.name === 'TimeoutError' here too and reuse that message. Add a test that rejects mid-stream, because the generic Failed to read branch has no test at all.
Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond
PR Review
2 finding(s): 🔴 0 critical · 🟠 1 major · 🔵 1 minor 2 posted as inline comment(s) on the changed lines. Severity is the reviewer's own estimate and is used for ordering, not filtering. No finding blocks merge automatically; a maintainer decides. |
Deep Review✅ No critical issues found. The 8 MB cap is sound: the streaming counter bounds a lying or absent 🟡 P2 -- recommended
🔵 P3 nitpicks (6)
Reviewers (10): correctness, security, reliability, performance, adversarial, testing, maintainability, project-standards, kieran-typescript, previous-comments. Testing gaps:
No finding blocks merge automatically. A maintainer will expect P0/P1 findings in code this PR changes to be fixed; P2/P3 are your call -- fix or reply. Never fix findings about surrounding code here; reply instead. Do not widen the PR. How to respond |
E2E Test Results✅ All tests passed • 462 passed • 1 skipped • 1178s
Tests ran across 4 shards in parallel. |
a78cfa6 to
1c37c28
Compare
🔵 Tier 2 — Low RiskSmall, isolated change with no API route or data model modifications. Why this tier:
Review process: AI review + quick human skim (target: 5–15 min). Reviewer validates AI assessment and checks for domain-specific concerns. Stats
|
| ); | ||
| let text: string; | ||
| try { | ||
| text = await readBoundedBody(resp, PROMETHEUS_MAX_RESPONSE_BYTES); |
There was a problem hiding this comment.
🟠 major — The 8 MB cap was sized for label lists, but it also applies to query_range matrices and to the ClickHouse HTTP backend
fetchPrometheusData also serves /query and /query_range, for kind: 'prometheus' and for clickhouse-http (around line 607). The clickhouse-http path is already bounded by max_result_rows, which is set in clickhousePrometheusUpstream. Each matrix sample is about 28 bytes of JSON, so 8 MB holds roughly 300k samples. That is only about 300 series × 1,000 points, far under PROMETHEUS_MAX_RESOLUTION (11,000 points per series) × 100k rows. A rate(...) by (pod) tile that works on the table-function backend now fails here with 'Narrow the query'. Fix: give range and instant queries their own larger cap, or skip the byte cap for clickhouse-http since max_result_rows already bounds it. Then correct the doc comment's claim that 'both backends allow about the same amount of data'.
Nothing blocks merge automatically, but a maintainer will expect this fixed if it is a real defect in code this PR changes. If it is about surrounding code, reply and say so instead of patching. Do not widen the PR. How to respond
| let text: string; | ||
| try { | ||
| text = await readBoundedBody(resp, PROMETHEUS_MAX_RESPONSE_BYTES); | ||
| } catch (err) { |
There was a problem hiding this comment.
🔵 minor — A timeout or abort while reading the body is reported as a generic read failure, not as a timeout
The fetch catch above checks for err.name === 'TimeoutError' and reports 'timed out'. The new body-read catch turns the same AbortSignal.timeout firing mid-stream (for example a slow chunked response) into 'Failed to read the response'. Fix: apply the same TimeoutError check here, and rethrow AbortError from the caller's own signal instead of reporting it as a server error.
Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond
1c37c28 to
dc0d776
Compare
dc0d776 to
1bc40a2
Compare
fetchPrometheusData buffered and parsed whatever an external Prometheus endpoint sent. A label or metric-name list on a server with hundreds of thousands of series, or a wide query_range result, could be tens of megabytes, all held in the API process at once. The ClickHouse paths were already bounded by max_result_rows; an external endpoint has no equivalent we can set (older servers ignore `limit` on the label APIs, and the query APIs have none). Read the body through a new utils/readBoundedBody helper: a Content-Length over the cap is refused before any of the body is read, and a chunked body is read up to the cap and then cancelled. Either way the caller gets a server error naming the cap. describe_source keeps returning the source config with partial: true, as it does for any lookup failure.
1bc40a2 to
4171c32
Compare
Summary
Caps Prometheus endpoint responses that MCP buffers at 8 MB. Follow-up to the review on #3496:
fetchPrometheusDataread whatever the server sent, and a label list from a large Prometheus can be tens to hundreds of MB.utils/readBoundedBody.ts: reads a fetchResponseas text, refusing aContent-Lengthover the cap before reading and cancelling a chunked body at the cap. General helper; the webhook transport and the ClickHouse Prometheus-API probe could adopt it later.serverPromQLQueryError: "Prometheus at returned more than 8 MB. Narrow the query or time range."describe_sourcereturnspartial: truewith the config intact;clickstack_promqlandquery_tilereturn the error.PROMETHEUS_MAX_RESULT_ROWS(100,000) label values, matching the bound the ClickHouse paths already had frommax_result_rows. Only the Prometheus-endpoint backend changes.How to test
Manual, with PromQL enabled. Both stubs below listen on port 9999; run one at a time.
Start a stub that declares a body over the cap:
Stub: Content-Length over the cap
Team settings → Sources: add a connection with host
http://localhost:9999and "Prometheus endpoint" ticked, then a PromQL source on it.Through MCP, call
clickstack_describe_sourceon that source: the result haspartial: trueanderrorcontains "returned more than 8 MB"; the source config is still returned. Callclickstack_promqlwith expressionup: a server error with the same text. Neither call reads the body.Swap in a stub that streams a chunked body with no
Content-Length:Stub: chunked body, no Content-Length
Repeat step 3. Both calls fail with the same error well before the 30 s fetch timeout, and the stub prints
client gave up after 8 MB.Point the connection at a real Prometheus and repeat step 3:
describe_sourcelists labels and metric names,clickstack_promqlwithupreturns one series per scrape target, as before.References