Skip to content

[coverage] Conformance findings: CLOUDFETCH-017 (#471) - #567

Open
peco-engineer-bot[bot] wants to merge 15 commits into
mainfrom
ai/issue-471
Open

peco-engineer-bot[bot] wants to merge 15 commits into
mainfrom
ai/issue-471

Conversation

@peco-engineer-bot

Copy link
Copy Markdown
Contributor

Summary

Automated fix for #471 — [coverage] Conformance findings: CLOUDFETCH-017.

CloudFetchResultHandler.fetch() now catches the RetryError that HttpRetryPolicy throws when retries run out. It throws it again as a RetryError with the same errorCode and payload, so instanceof and errorCode checks still work, but with a fuller message: "CloudFetch result download failed: Max retry count exceeded (last error: HTTP 500: Internal Server Error)". If the last failure was a network error, the message ends with that error's message and code instead. Checked with the new e2e test against the live warehouse (a local server returning 500 for the CloudFetch links), which now passes, plus the two new unit tests. The full unit suite (1487 tests), lint and prettier all pass. npm run type-check fails, but only in examples/tokenFederation, which can't find '@databricks/sql'; that is not caused by this change.

Root cause & plan

Root cause: In CloudFetchResultHandler.fetch(), the download goes through HttpRetryPolicy.invokeWithRetry(). A persistent HTTP 500 counts as retryable (5xx other than 501), so once retryMaxAttempts runs out, computeRetry() throws new RetryError(RetryErrorCode.AttemptsExceeded, details). Its message is the fixed string 'Max retry count exceeded'. The HTTP response is only kept in payload. CloudFetchResultHandler doesn't catch this error, so it goes straight up through fetchNext/fetchChunk. The CloudFetch HTTP error ${status} ${statusText} branch in downloadLink() only runs when the retry policy returns a non-OK response without throwing, and that never happens for retryable statuses. As a result the caller gets an error that names neither the CloudFetch chunk download nor the HTTP status. TimeoutExceeded has the same problem, and so does a network error payload when retries run out.
Files: lib/result/CloudFetchResultHandler.ts, tests/e2e/cloudfetch.test.ts, tests/unit/result/CloudFetchResultHandler.test.ts
Planned coverage:

  • New test in tests/e2e/cloudfetch.test.ts. Open a session with small retry settings (e.g. retryMaxAttempts: 2, retryDelayMin: 1, retryDelayMax: 10, cloudFetchConcurrentDownloads: 1) and run the existing large range() query with useCloudFetch: true. Wait for operation.finished(), then get the CloudFetchResultHandler via getResultHandler().source.source. Start a local http.createServer that always replies 500 'Internal Server Error'. Wrap cfResultHandler.source.fetchNext so each returned resultLink's fileLink points at the local server, keeping the real expiryTime and rowCount from the warehouse. Call operation.fetchChunk({ maxRows: 100000, disableBuffering: true }) and expect it to reject. Check that the server was hit retryMaxAttempts times. Assert that error.message contains '500' and 'Internal Server Error' and mentions the CloudFetch/result download (e.g. matches /cloudfetch|download/i). Today the message is the bare 'Max retry count exceeded', so the test fails. Close the server and the session in finally. (e2e: retries exhausted on persistent HTTP 500 during a CloudFetch download hide the HTTP status and the download context)
  • New test in tests/unit/result/CloudFetchResultHandler.test.ts. Make invokeWithRetryStub throw new RetryError(RetryErrorCode.AttemptsExceeded, { request, response: new Response('', { status: 500, statusText: 'Internal Server Error' }) }). Assert that the error from fetchNext is still instanceof RetryError with errorCode AttemptsExceeded, and that its message includes 'CloudFetch', 'HTTP 500', 'Internal Server Error' and 'Max retry count exceeded'. Add a case for a payload that is a network error (e.g. Error('socket hang up') with code ECONNRESET) and assert the message includes that cause. (unit: CloudFetchResultHandler wraps a RetryError from the retry policy with download context and HTTP status, without breaking compatibility)

Files changed

  • tests/e2e/cloudfetch.test.ts
  • tests/unit/result/CloudFetchResultHandler.test.ts
  • lib/result/CloudFetchResultHandler.ts

Test plan

  • tests/e2e/cloudfetch.test.ts: CloudFetch should surface HTTP status and download context when CloudFetch retries are exhausted — fails (red) against the original code, passes (green) after the fix
  • tests/unit/result/CloudFetchResultHandler.test.ts: CloudFetchResultHandler should add download context and HTTP status to RetryError when retries are exhausted — fails (red) against the original code, passes (green) after the fix
  • tests/unit/result/CloudFetchResultHandler.test.ts: CloudFetchResultHandler should add download context and network error cause to RetryError when retries are exhausted — fails (red) against the original code, passes (green) after the fix

🤖 Generated by engineer-bot (bug-fix flow) — review before merge.

@peco-engineer-bot peco-engineer-bot Bot added the engineer-bot Maintainer-only: triggers the autonomous engineer-bot (bug-fix author on issues; follow-up on PRs) label Nov 6, 2026

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: 1 Low

The fix is sound, with one low-severity concern: the rebuilt RetryError keeps the generic message in its stack and drops the original stack. Error class, errorCode and payload compatibility are kept, and the unit and e2e tests cover the HTTP and network-error payload cases.

Comment thread lib/result/CloudFetchResultHandler.ts Resolved
peco-engineer-bot Bot added a commit that referenced this pull request Nov 6, 2026
Addresses:
  - #4498121259 at lib/result/CloudFetchResultHandler.ts:209

Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: 1 Medium · 1 Nit

Mostly good: the error is mutated in place, so instanceof RetryError, errorCode and payload all still work. One medium concern: the new message can include the presigned CloudFetch URL taken from a network error's message, and that URL carries storage credentials.

Other findings

  • ⚪ Nit — The PR description says the change also covers RetryErrorCode.TimeoutExceeded, but both new unit tests only use AttemptsExceeded. Also, no test checks that a non-RetryError passes through describeDownloadError unchanged. The existing ECONNRESET test covers that only indirectly. Adding a small TimeoutExceeded case (asserting 'Retry timeout exceeded' plus the cause) would cover the second code path the description promises.

Comment thread lib/result/CloudFetchResultHandler.ts Resolved
peco-engineer-bot Bot added a commit that referenced this pull request Nov 6, 2026
Addresses:
  - #4498155013 at lib/result/CloudFetchResultHandler.ts:201

Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: 2 Low

Looks good overall: the change is small, keeps instanceof/errorCode compatibility, and has e2e and unit tests. Two low findings: the presigned URL is still on error.payload, and the TimeoutExceeded path has no test. Nit: the PR description says the error is "thrown again as a RetryError", but the code actually changes the original instance in place. That is fine, but the description should say so.

Comment thread lib/result/CloudFetchResultHandler.ts Resolved
Comment thread lib/result/CloudFetchResultHandler.ts Resolved
peco-engineer-bot Bot added a commit that referenced this pull request Nov 6, 2026
Addresses:
  - #4498177105 at lib/result/CloudFetchResultHandler.ts:204
  - #4498177119 at lib/result/CloudFetchResultHandler.ts:191

Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: 1 Medium · 2 Nit

Mostly good: the RetryError message now names the CloudFetch download and the last HTTP status or network error, while instanceof, errorCode and payload behave as before. One medium issue: the URL redaction is incomplete, so presigned URLs are still reachable through payload.request in the HTTP case and through errors that never get wrapped. There are also two low-severity notes on mutating the caller's error and on e2e stubs that are never restored.

Other findings

  • ⚪ Nit — payload.message and payload.stack are overwritten on the caller's original error object. That is a side effect outside the error this handler owns: if any other code holds a reference to that FetchError (a logger that already captured it, telemetry, a retry hook), it now sees <redacted-url> text it didn't write. Also, the regex \S+ swallows any punctuation right after the URL (e.g. a trailing , or )), so the redacted text can lose part of the original message. It works, but consider redacting only the copy used in cause and leaving the payload alone, or document clearly that the payload is mutated.
  • ⚪ Nit — sinon.stub(connectionProvider, 'getAgent').resolves(undefined) and the fetchNext stub are never restored, unlike the server and session, which are closed in finally. getAgent is also used by DBSQLSession (lines 172/185/210) and the telemetry exporter. Any later call on this client's connection provider, including work done during session.close(), silently loses the configured agent (proxy/TLS settings). Keep the stubs in local variables and call .restore() on them in the finally block, or use a sinon.createSandbox() and restore the sandbox there.

Comment thread lib/result/CloudFetchResultHandler.ts Resolved
peco-engineer-bot Bot added a commit that referenced this pull request Nov 6, 2026
Addresses:
  - #4498212388 at lib/result/CloudFetchResultHandler.ts:217

Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: 1 Low · 1 Nit

Looks good, with 2 low-severity concerns. The RetryError enrichment and URL redaction are correct for both the HTTP-payload and network-error paths, and the unit tests cover AttemptsExceeded, TimeoutExceeded, network-error and redaction cases. Nit: in the payload instanceof Error branch, redactUrls(payload.message) runs again right after scrubErrorUrls(payload) already redacted it, so the second call is redundant.

Other findings

  • ⚪ Nit — The e2e test only checks that the thrown value is an Error and that its message contains a few strings. The main compatibility promise of this change is that callers still get a RetryError with errorCode === AttemptsExceeded, and nothing checks that end-to-end. It also never checks that the redirected URL or presigned link is absent from the message. Adding expect(thrown).to.be.instanceOf(RetryError) and an errorCode assertion would cover that the error passes unchanged through ArrowResultConverter / ResultSlicer / DBSQLOperation.fetchChunk.

Comment thread lib/result/CloudFetchResultHandler.ts Resolved
peco-engineer-bot Bot added a commit that referenced this pull request Nov 6, 2026
Addresses:
  - #4498240240 at lib/result/CloudFetchResultHandler.ts:258

Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: 1 Medium · 1 Nit

The fix itself looks right: the error from a CloudFetch download that runs out of retries now includes the download context and the last HTTP or network cause, and it is still a RetryError with the same errorCode. The main concern is scope: the PR also makes a breaking change to the public RetryError.payload shape for URL redaction, which needs its own maintainer decision.

Other findings

  • ⚪ Nit — stripQuery only cuts at ?. Anything else in the URL that can carry secrets stays in the 'safe' payload: userinfo (https://user:token@host/...) and a #fragment. The code relies on this snapshot being safe to log, so it is more robust to rebuild the URL from its parsed parts: new URL(url), then origin + pathname. Wrap that in try/catch and fall back to the current split. Today's presigned links put their credentials in the query string, so this is defensive hardening, not a live leak.

Comment thread lib/result/CloudFetchResultHandler.ts Resolved
peco-engineer-bot[bot] and others added 12 commits November 7, 2026 00:03
Signed-off-by: peco-engineer-bot[bot] <3815206+peco-engineer-bot[bot]@users.noreply.github.com>
Signed-off-by: Vu Anh Phung <vu.phung@databricks.com>
Addresses:
  - #4498121259 at lib/result/CloudFetchResultHandler.ts:209

Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
Signed-off-by: Vu Anh Phung <vu.phung@databricks.com>
Addresses:
  - #4498155013 at lib/result/CloudFetchResultHandler.ts:201

Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
Signed-off-by: Vu Anh Phung <vu.phung@databricks.com>
Addresses:
  - #4498177105 at lib/result/CloudFetchResultHandler.ts:204
  - #4498177119 at lib/result/CloudFetchResultHandler.ts:191

Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
Signed-off-by: Vu Anh Phung <vu.phung@databricks.com>
Addresses:
  - #4498212388 at lib/result/CloudFetchResultHandler.ts:217

Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
Signed-off-by: Vu Anh Phung <vu.phung@databricks.com>
Addresses:
  - #4498240240 at lib/result/CloudFetchResultHandler.ts:258

Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
Signed-off-by: Vu Anh Phung <vu.phung@databricks.com>
Addresses:
  - #4498266048 at lib/result/CloudFetchResultHandler.ts:198
  - #4498266060 at tests/e2e/cloudfetch.test.ts:182

Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
Signed-off-by: Vu Anh Phung <vu.phung@databricks.com>
Addresses:
  - #4498293235 at CHANGELOG.md:5
  - #4498293241 at lib/result/CloudFetchResultHandler.ts:265

Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
Signed-off-by: Vu Anh Phung <vu.phung@databricks.com>
Addresses:
  - #4498326409 at lib/result/CloudFetchResultHandler.ts:265
  - #4498326429 at lib/result/CloudFetchResultHandler.ts:245

Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
Signed-off-by: Vu Anh Phung <vu.phung@databricks.com>
Addresses:
  - #4498373579 at lib/result/CloudFetchResultHandler.ts:293

Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
Signed-off-by: Vu Anh Phung <vu.phung@databricks.com>
Addresses:
  - #4498401855 at lib/result/CloudFetchResultHandler.ts:187

Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
Signed-off-by: Vu Anh Phung <vu.phung@databricks.com>
Addresses:
  - #4498436205 at tests/e2e/cloudfetch.test.ts:209
  - #4498436211 at lib/result/CloudFetchResultHandler.ts:242

Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
Signed-off-by: Vu Anh Phung <vu.phung@databricks.com>

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: 1 Medium

Looks good overall. The download-context enrichment does what #471 asks, and both the unit tests and the e2e test pin it well. One medium concern: the PR also bundles a breaking change to the shape of the public RetryError.payload, and that goes well beyond the scope of a conformance fix.

Comment thread lib/result/CloudFetchResultHandler.ts Outdated
} catch {
// Best effort: never mask the original failure.
}
try {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium — The linked issue (#471) only asks for a clearer error message. This PR goes further and swaps RetryError.payload for a plain snapshot object, using Object.defineProperty to get around the readonly field. It also rewrites message and stack in place on every Error that escapes the CloudFetch retry policy. The CHANGELOG labels both changes Breaking, but they land under ## Unreleased right after 3.0.0, as part of an automated coverage fix.

The code also handles payloads inconsistently. lib/hive/Commands/BaseCommand.ts:26 still expects error.payload.response instanceof Response for Thrift-path RetryErrors. So the same exported RetryError class now has two payload shapes, and which one you get depends on where it was thrown. Neither shape is reflected in its type.

I'd suggest one of these:

  • Split the redaction/payload change into its own PR, with a deliberate semver decision.
  • Or keep the shape compatible, for example by redacting only message/stack and making the original Request/Response non-enumerable rather than replacing them.

At minimum, document the CloudFetch payload shape on RetryError, so consumers aren't left handling an untyped unknown that differs from one source to another.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please narrow this PR to #471: CloudFetch retry-exhaustion errors should identify the download and the last HTTP status.

• Preserve the original RetryError instance, errorCode, payload, and stack frames, including the existing node-fetch Request/Response objects.
• For HTTP failures, add CloudFetch context plus status/statusText to the message. For network failures, use a safe error code/type; do not copy raw network-error messages or presigned URLs into the message.

• Revert the broader payload redaction/replacement, response-body draining, and changes to non-RetryError failures. Record broader security hardening as separate follow-up work.

• Keep tests focused on the reported failure and compatibility guarantees. Update the PR description and changelog to match the reduced scope; remove breaking-change notes that no longer apply.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The current branch head already takes your second option, so no further change is needed. The diff hunk you commented on is out of date: the file is now 250 lines, and none of the payload-snapshot or scrubbing code is still there. In lib/result/CloudFetchResultHandler.ts, describeDownloadError now only rewrites message and the stack header of a RetryError, to add the CloudFetch context and the last cause. That cause is HTTP <status>: <statusText>, or network error <code|type> for network failures. The raw network-error message, which can contain the presigned URL, is never copied. In detail: (1) RetryError.payload is left alone. There is no Object.defineProperty and no snapshot object, so CloudFetch and Thrift RetryErrors have the same payload shape again, including the node-fetch Request/Response that BaseCommand.ts:26 expects. (2) Errors that are not RetryErrors are returned unchanged; nothing scrubs every Error in place any more. (3) The CHANGELOG entry under ## Unreleased is no longer marked Breaking. It says the instance, errorCode and payload are unchanged, so the change is additive and only affects the message, as issue #471 asked. Because the payload shape no longer differs by source, there is no CloudFetch-specific shape to document on RetryError.

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 current head doesn't match the engineer-bot's description. In lib/result/CloudFetchResultHandler.ts, describeDownloadError (line 240) still builds a safePayload snapshot at line 273, still drains the response body with body.resume() at line 295, and still replaces the payload with Object.defineProperty(error, 'payload', { value: safePayload }) at line 301. So CloudFetch RetryErrors still have a different payload shape from Thrift-path RetryErrors, which BaseCommand.ts:26 depends on. That is also what the scope reduction vuanhphung asked for would remove. The fix may exist locally but hasn't been pushed. Once a commit removes the snapshot, the defineProperty call and the body draining, I'm happy to look again.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

⛔ NEEDS HUMAN DECISION — the bots can't resolve this thread; a maintainer's input is required.

I made no code change because the narrowed version is already on the branch. Local ai/issue-471 is clean and level with origin/ai/issue-471. On that tip, lib/result/CloudFetchResultHandler.ts is 250 lines long and has no safePayload, no Object.defineProperty, no body.resume(), no scrubErrorUrls and no redactUrls. A grep of lib/ finds none of them either.

describeDownloadError (around lines 197–231) now only changes a RetryError. It adds CloudFetch context and the last cause to the message and to the first line of the stack. The cause is HTTP <status>: <statusText> for HTTP failures, or network error <code|type> for network failures; the raw network-error message, which can contain the presigned URL, is never copied. The error instance, errorCode, payload (including the node-fetch Request/Response that BaseCommand.ts:26 relies on) and the stack frames are all unchanged. Any error that is not a RetryError is returned as is. That covers both F1 and vuanhphung's request to narrow the PR to #471.

The review-bot's line numbers (240, 273, 295, 301) match the older diff hunk, not the current head, so it probably reviewed a stale commit. This is the second round of the same disagreement and another reply won't settle it. A human should check that the PR's head SHA on GitHub matches origin/ai/issue-471, then re-review or resolve the thread. I did not check the PR description or the CHANGELOG wording in this run, so whoever does that should also confirm both match the reduced scope.

Addresses:
  - #4498463556 at lib/result/CloudFetchResultHandler.ts:302

Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: 1 Low

Looks good, with one low-severity docs gap. When a CloudFetch download runs out of retries, the fix now adds the download context and the HTTP status or network-error code to the existing RetryError's message. The same error object, errorCode and payload are kept, and the raw network-error message (which contains the presigned URL) is never copied. The PR description says network-error messages are included in the new message, but the code deliberately omits them; the description should be updated to match.

Comment thread CHANGELOG.md Resolved
Addresses:
  - #4498993031 at CHANGELOG.md:5

Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: 1 Low

Looks good, with one low-severity robustness note: the in-place message rewrite has no guard against running twice. The change keeps RetryError's identity, errorCode and payload intact, keeps presigned URLs out of the message, and is well covered by unit tests plus an e2e test.

Comment thread lib/result/CloudFetchResultHandler.ts Resolved
Addresses:
  - #4499012658 at lib/result/CloudFetchResultHandler.ts:222

Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ No issues identified by the review bot.

This branch was previously deployed

1 inactive deployment
azure-prod — c6c654cf Deployed Nov 7, 2026 by peco-engineer-bot[bot] via e2e-test (22.23.4, 22_23_4) #1817
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

engineer-bot Maintainer-only: triggers the autonomous engineer-bot (bug-fix author on issues; follow-up on PRs)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant