Skip to content

fix: Correct conditional-request header and sanitize network error log in FDv1 polling requestor#263

Merged
kinyoklion merged 3 commits into
mainfrom
rlamb/sdk-2322/fdv1-requestor-fixes
May 6, 2026
Merged

fix: Correct conditional-request header and sanitize network error log in FDv1 polling requestor#263
kinyoklion merged 3 commits into
mainfrom
rlamb/sdk-2322/fdv1-requestor-fixes

Conversation

@kinyoklion
Copy link
Copy Markdown
Member

@kinyoklion kinyoklion commented May 4, 2026

Two FDv1 polling bugs surfaced during review of the FDv2 polling work. The FDv2 sibling already handles both correctly; this PR mirrors that pattern in packages/common_client/lib/src/data_sources/requestor.dart.

Jira: SDK-2322

1. Conditional-request header

The requestor was sending the stored ETag back as etag: <value>. The conditional-request header per RFC 7232 is If-None-Match, not etag (which is a response header). The server didn't recognize the request as conditional, so 304s were never emitted for our polling traffic and the SDK paid full bandwidth on every successful poll.

If a 304 ever did arrive (non-compliant proxy), the same code path fell through to DataEvent('put', '', environmentId) with an empty body, which downstream parsed as empty JSON and surfaced as ErrorKind.invalidData — the opposite of the intended "data is current" semantics.

Fix: send if-none-match, treat 304 as null (no change), treat 200 as fresh body.

2. Network-error log message

The catch block passed err.toString() into both the warn log and the public StatusEvent.message. For http.ClientException, toString() formats as 'ClientException: <msg>, uri=<full-url>', so the request URL ended up in both places — and that URL contains the base64url-encoded context.

Fix: categorize the exception via minification-safe is checks (TimeoutException, http.ClientException, plus a substring fallback for dart:io's TlsException / HandshakeException) into a fixed sanitized message, and use that for both the log line and the StatusEvent.

Testing

  • ETag round-trip: response with etag: abc-123 results in if-none-match: abc-123 on the next request, and the wrong etag header is not sent.
  • 304 path: a 304 response no longer surfaces as ErrorKind.invalidData and leaves FlagManager untouched.
  • Log content: a network-error path's captured log records do not contain the encoded context (verified via mocktail MockLogAdapter).

dart format, dart analyze lib test, and the polling test suite all pass.


Note

Medium Risk
Adjusts polling HTTP semantics (ETag/304 handling) and the polling loop scheduling, which could change request frequency and cache behavior if incorrect. Mitigated by added regression tests covering header behavior, 304 handling, and log sanitization.

Overview
Fixes FDv1 polling conditional requests by sending cached ETags as if-none-match (instead of etag), treating 304 Not Modified as no change, and avoiding storing invalid/empty ETags (while preserving the previous ETag when the header is omitted).

Hardens polling failure reporting by replacing raw exception toString() with a sanitized, categorized message (timeout/network/TLS/unknown) for both logs and StatusEvents, preventing encoded context leakage. Adds regression tests for ETag round-tripping, 304 behavior (including continuing to poll), and sanitized network-error logging.

Reviewed by Cursor Bugbot for commit ad22a25. Bugbot is set up for automated code reviews on this repo. Configure here.

…ssage in FDv1 polling requestor

Two fixes in `packages/common_client/lib/src/data_sources/requestor.dart`:

1. Conditional-request header

The requestor was sending the stored ETag back as `etag: <value>` (a
response header name), not `If-None-Match: <value>`. The server didn't
recognize it as a conditional request and never emitted 304s, so every
poll paid full bandwidth. If a 304 *did* arrive (non-compliant proxy),
the same code path fell through to `DataEvent('put', '', ...)` with an
empty body, which downstream then parsed as empty JSON and reported as
`ErrorKind.invalidData` -- the opposite of the intended "data is
current" semantics.

The fix sends `if-none-match` correctly, treats 304 as null (no
change to apply), and treats 200 as a fresh body that parses and
updates the stored ETag.

2. Network-error log message

The catch block was passing `err.toString()` into both the warn log
and the public `StatusEvent.message`. For `http.ClientException`,
`toString()` formats as `'ClientException: <msg>, uri=<full-url>'`,
so the request URL was ending up in both places -- and that URL
embeds the base64url-encoded context.

The fix categorizes the exception with minification-safe `is` checks
(TimeoutException, http.ClientException, plus a substring fallback
for dart:io's TlsException / HandshakeException) into a fixed
sanitized message and uses that for both the log line and the
StatusEvent.

Tests added: ETag round-trip (response then If-None-Match on the
next request, with the wrong `etag` header NOT sent), 304 does not
surface as `ErrorKind.invalidData`, and warn-level log records on a
network error do not contain the encoded context.
@kinyoklion kinyoklion requested a review from a team as a code owner May 4, 2026 22:50
@kinyoklion kinyoklion changed the title fix(SDK-2322): correct conditional-request header and sanitize network error log in FDv1 polling requestor fix: Correct conditional-request header and sanitize network error log in FDv1 polling requestor May 4, 2026
Comment thread packages/common_client/lib/src/data_sources/requestor.dart Outdated
@tanderson-ld
Copy link
Copy Markdown
Contributor

Is there end to end test coverage in the flutter repo or via contract tests for this case?

…on omitted header

Match the FDv2 sibling: only update the stored ETag when the response
carries a non-null, non-empty value. This protects against two
edge cases:

- Empty-string ETag: the previous code stored '' verbatim and would
  send `if-none-match: ` on the next request. An unquoted empty
  token is invalid per RFC 7232, and lenient servers can interpret
  it as "match anything" and pin the SDK to permanent 304s.
- Missing ETag header on a 200: the previous code cleared
  `_lastEtag` to null. The fix preserves the previously stored
  value so the next request is still conditional.

Tests pin both: an empty-string ETag is not stored (next request
sends no `if-none-match`), and a 200 without an ETag header
preserves the prior value (third request still uses the original).
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b632cd1. Configure here.

Comment thread packages/common_client/lib/src/data_sources/requestor.dart
`_doPoll` early-returned on `case null:` without calling
`_schedulePoll()`, so the first no-change response halted the loop.
This was previously dead code -- the requestor's wrong
conditional-request header meant 304s never arrived. Fixing the
header (earlier in this PR) made 304s the expected response when
flag data hasn't changed, which immediately exposed the latent bug:
the SDK stopped receiving updates after the first unchanged-data
poll.

The fix is to fall through the null arm to `_schedulePoll()` so the
next poll cycle is set up regardless of whether a value, no-change,
or status was received.

Test pins the regression: three 304 responses in a row must produce
at least three requests.
@kinyoklion kinyoklion merged commit 18d78ce into main May 6, 2026
7 checks passed
@kinyoklion kinyoklion deleted the rlamb/sdk-2322/fdv1-requestor-fixes branch May 6, 2026 21:55
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.

2 participants