Skip to content

fix(search): surface Google Flights ErrorResponse instead of silent empty results - #208

Open
bjgross10767 wants to merge 2 commits into
punitarani:mainfrom
bjgross10767:fix/surface-rate-limit-errors
Open

fix(search): surface Google Flights ErrorResponse instead of silent empty results#208
bjgross10767 wants to merge 2 commits into
punitarani:mainfrom
bjgross10767:fix/surface-rate-limit-errors

Conversation

@bjgross10767

@bjgross10767 bjgross10767 commented Jun 23, 2026

Copy link
Copy Markdown

Summary

Google Flights occasionally returns HTTP 200 with an ErrorResponse envelope (rate-limit / fingerprint reject / quota): a wrb.fr row whose inner-JSON slot is null and a protobuf-typed error blob in row[5]. parse_first_wrb_payload correctly returns None for that shape, but _fetch_flights then returned None -> SearchFlights.search() returned None -> the MCP layer reported {"success": true, "count": 0}.

Callers had no way to distinguish "rate-limited, retry with backoff" from "no flights match these filters". Symptoms surface for users as #142 (round-trip empty) and contribute to #200 (general empty results): each round-trip fires N+1 requests (one outbound + per-top-N expansion), so a few hit the per-IP quota first and ALL subsequent queries silently fail.

Reproduced consistently from a single residential IP: third or fourth round-trip search in a short window returns the error envelope, and one-way queries from the same client also start coming back empty. Switching IP or waiting ~60s restores normal results.

Changes

  • New exception GoogleFlightsRateLimited(SearchClientError) in fli/search/exceptions.py with optional session_id from the rejected envelope, exported from fli.search.

  • fli/search/_wire.py: adds is_rate_limit_response(body) (substring match against the stable protobuf type URL travel.frontend.flights.ErrorResponse) and extract_error_session_id(body) (best-effort session-id pull from row[5][2][0][1][0][3]).

  • fli/search/flights.py _fetch_flights: when parse_first_wrb_payload returns None, check is_rate_limit_response; if true, raise GoogleFlightsRateLimited instead of silently returning None. Otherwise behaviour unchanged.

  • fli/search/dates.py _fetch_dates: same treatment for SearchDates.

  • fli/mcp/server.py: _execute_flight_search, _execute_booking_options, and _execute_date_search each catch the new exception and return a structured payload:

    {
      "success": false,
      "error": "Google Flights rejected ...",
      "code": "RATE_LIMITED",
      "retry_after_s": 30,
      "session_id": "...",
      "flights": []
    }

    MCP clients can key off code == "RATE_LIMITED" for targeted backoff instead of treating the silent empty as "no matches".

Tests

  • tests/search/test_rate_limit_detection.py (6 cases): exercises one-way, round-trip, search_dates, and both MCP executors via the captured real-life rate-limit fixture using the project's existing fake-client pattern.
  • TestRateLimitDetection group in tests/search/test_wire.py (7 cases): wire-level detection + session-id extraction from the same fixture, plus negative cases (empty body, success body, bytes input).
  • tests/search/fixtures/flight_search_rate_limit_error.txt: real captured 325-byte response from a BZN -> SEA round-trip query when the client was rate-limited.

Full unit-test suite (213 tests, excluding live-network tests) passes locally on Windows + Python 3.14. The one pre-existing failure in test_snapshot_fixtures.py::test_jfk_fra_oneworld_has_results is a cp1252 encoding issue on Windows unrelated to this change (also fails on main).

Backwards compatibility

  • Public API: only adds (GoogleFlightsRateLimited exception, two helper functions, structured MCP error). No signature changes.
  • Existing tests' behaviour unchanged: the rejected envelope's parse_first_wrb_payload still returns None. The new raise only fires in the previously-unreachable branch where inner is None AND the body carries the ErrorResponse marker.
  • Library users who were catching Exception continue to work; users who were catching SearchClientError get a more specific exception type for free.

Closes / relates to

Generated with Claude Code

Greptile Summary

This PR fixes a silent-empty-result bug where Google Flights' HTTP 200 ErrorResponse envelope (rate-limit / fingerprint reject) was indistinguishable from "no matching flights." It introduces GoogleFlightsRateLimited, wires detection into _fetch_flights and _fetch_dates, and surfaces a structured code: "RATE_LIMITED" response at the MCP layer.

  • New exception and detection helpers: GoogleFlightsRateLimited(SearchClientError) is added to fli/search/exceptions.py and exported from fli.search; is_rate_limit_response (substring match on a stable protobuf type URL) and extract_error_session_id (best-effort path navigation) are added to _wire.py.
  • Search core changes: Both SearchFlights._fetch_flights and SearchDates._fetch_dates now raise GoogleFlightsRateLimited when parse_first_wrb_payload returns None and the body carries the error marker, replacing a silent return None.
  • MCP layer: All three executor functions catch the new exception and return {\"success\": false, \"code\": \"RATE_LIMITED\", \"retry_after_s\": 30, ...} so MCP clients can implement targeted backoff.

Confidence Score: 4/5

Safe to merge; the core detection logic is correct and the changes are additive. The one latent bug is confined to a test helper method that no current test path exercises.

The production logic is sound and well-tested via the captured fixture. The test helper _RateLimitedClient.get references the non-existent self._post, which will surface as an AttributeError the first time a GET-based search path is added and tested through this fake client. Both is_rate_limit_response and extract_error_session_id also carry an unreachable except UnicodeDecodeError block, and there is no test for the _execute_booking_options rate-limit handler.

tests/search/test_rate_limit_detection.py — fix the self._post typo in _RateLimitedClient.get and add a test for _execute_booking_options. fli/search/_wire.py — remove the dead except UnicodeDecodeError branches.

Important Files Changed

Filename Overview
fli/search/_wire.py Adds is_rate_limit_response and extract_error_session_id helpers. Both contain an unreachable except UnicodeDecodeError block because errors="ignore" is used. The session-id path navigation is fragile but correctly documented as best-effort.
fli/search/exceptions.py Adds GoogleFlightsRateLimited(SearchClientError) with an optional session_id attribute. Clean hierarchy, correct __init__, no issues.
fli/search/flights.py Integrates rate-limit detection in _fetch_flights — raises GoogleFlightsRateLimited when parse_first_wrb_payload returns None and the body contains the error marker. Logic is correct and only fires in the previously-unreachable inner is None branch.
fli/search/dates.py Mirrors the same rate-limit detection pattern in _fetch_dates. Symmetric and correct.
fli/mcp/server.py Adds GoogleFlightsRateLimited catch blocks to all three executors, returning code: "RATE_LIMITED" structured errors. The retry_after_s: 30 is hardcoded but acceptable.
tests/search/test_rate_limit_detection.py New end-to-end tests cover one-way, round-trip, date-search, and MCP executor paths. Contains a latent bug in _RateLimitedClient.get (calls non-existent self._post). Also missing a test for _execute_booking_options.
tests/search/test_wire.py Adds 7 wire-level unit tests for is_rate_limit_response and extract_error_session_id, including positive, negative, bytes, and empty-body cases.
fli/search/init.py Exports GoogleFlightsRateLimited from the public fli.search namespace. Correctly added to both the import and __all__.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[HTTP POST to Google Flights] --> B[response.raise_for_status]
    B --> C[parse_first_wrb_payload]
    C -->|non-None| D[Decode flight/date rows]
    C -->|None| E{is_rate_limit_response?}
    E -->|Yes| F[extract_error_session_id]
    F --> G[raise GoogleFlightsRateLimited]
    E -->|No| H[return None]
    D --> I[Return results]
    G --> J{MCP layer catch}
    J --> K[Return RATE_LIMITED structured error]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[HTTP POST to Google Flights] --> B[response.raise_for_status]
    B --> C[parse_first_wrb_payload]
    C -->|non-None| D[Decode flight/date rows]
    C -->|None| E{is_rate_limit_response?}
    E -->|Yes| F[extract_error_session_id]
    F --> G[raise GoogleFlightsRateLimited]
    E -->|No| H[return None]
    D --> I[Return results]
    G --> J{MCP layer catch}
    J --> K[Return RATE_LIMITED structured error]
Loading

Fix All in Cursor Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
Fix the following 4 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 4
tests/search/test_rate_limit_detection.py:55-58
**Typo in `_RateLimitedClient.get` — calls non-existent `self._post`**

The `get` method calls `self._post(url, **kwargs)` but no such attribute exists on the class (only `post` is defined). Any test path that exercises an HTTP GET through this fake client will immediately raise `AttributeError` instead of returning the mocked rate-limit response, silently hiding the intended coverage. The affected method is currently untriggered by the existing test suite since both `_fetch_flights` and `_fetch_dates` use `POST`, but the latent bug is one future `get`-based search path away from causing confusing failures.

### Issue 2 of 4
fli/search/_wire.py:142-147
**`except UnicodeDecodeError` is dead code after `errors="ignore"`**

`bytes.decode("utf-8", errors="ignore")` never raises `UnicodeDecodeError` — the `errors="ignore"` flag suppresses decode errors by silently dropping bad bytes. The `except` clause is therefore unreachable in both `is_rate_limit_response` and `extract_error_session_id`. This pattern appears twice in the new code.

```suggestion
    if isinstance(body, bytes):
        body = body.decode("utf-8", errors="ignore")
    return RATE_LIMIT_ERROR_MARKER in body
```

### Issue 3 of 4
fli/search/_wire.py:158-163
Same dead-code `except UnicodeDecodeError` in `extract_error_session_id``errors="ignore"` makes the clause unreachable.

```suggestion
    if isinstance(body, bytes):
        body = body.decode("utf-8", errors="ignore")
    if RATE_LIMIT_ERROR_MARKER not in body:
```

### Issue 4 of 4
tests/search/test_rate_limit_detection.py:153-212
**`_execute_booking_options` rate-limit path has no test coverage**

The PR adds a `GoogleFlightsRateLimited` catch block in `_execute_booking_options` (server.py lines ~766-777) but `TestMCPServerRateLimit` only exercises `_execute_flight_search` and `_execute_date_search`. A test analogous to `test_execute_flight_search_returns_rate_limit_code` is missing for the booking-options path, so any regression in its structured error response (e.g., wrong key name or missing `code` field) would go undetected.

Reviews (1): Last reviewed commit: "fix(search): surface Google Flights Erro..." | Re-trigger Greptile

Greptile also left 4 inline comments on this PR.

…mpty

Google Flights occasionally returns HTTP 200 with an `ErrorResponse`
envelope (rate-limit / fingerprint reject / quota): a `wrb.fr` row whose
inner-JSON slot is `null` and a protobuf-typed error blob in `row[5]`.
`parse_first_wrb_payload` correctly returns `None` for that shape, but
`_fetch_flights` then returned `None` -> `SearchFlights.search()` returned
`None` -> the MCP layer reported `{"success": true, "count": 0}`.

Callers had no way to distinguish "rate-limited, retry with backoff" from
"no flights match these filters". Symptoms surfaced for users as punitarani#142
(round-trip empty) and punitarani#200 (general empty results): each round-trip
fires N+1 requests (one outbound + per-top-N expansion), so a few hits
the per-IP quota first and ALL subsequent queries silently fail.

This change:

- Adds `GoogleFlightsRateLimited(SearchClientError)` exception with the
  captured shopping session id when extractable.
- Adds `is_rate_limit_response(body)` and `extract_error_session_id(body)`
  helpers in `_wire.py`, both based on the stable protobuf type URL
  `travel.frontend.flights.ErrorResponse`.
- `SearchFlights._fetch_flights` and `SearchDates._fetch_dates` raise
  `GoogleFlightsRateLimited` on the rejected envelope instead of
  swallowing it as `None`.
- MCP server's three executors catch the new exception and return a
  structured `{"success": False, "code": "RATE_LIMITED",
  "retry_after_s": 30, "session_id": "..."}` payload so MCP clients
  can implement targeted backoff instead of treating the silent empty
  as "no matches".

Tests:

- New `tests/search/test_rate_limit_detection.py` (6 cases) covers
  one-way, round-trip, search_dates, and both MCP executors using a
  captured real-life rate-limit fixture
  (`tests/search/fixtures/flight_search_rate_limit_error.txt`).
- New `TestRateLimitDetection` group in `tests/search/test_wire.py`
  (7 cases) covers the wire-level detection + session-id extraction
  from the same fixture.

No existing tests changed behaviour: the rejected envelope's
`parse_first_wrb_payload` still returns `None`; the new raise only fires
in the previously-unreachable branch where `inner is None` AND the body
carries the ErrorResponse marker.

Fixes punitarani#142, punitarani#200 partially (surfaces the failure instead of silencing it
— actual retry / backoff is left to callers but the structured error
makes that implementable).
Comment on lines +55 to +58
return self._post(url, **kwargs)


@pytest.fixture

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Typo in _RateLimitedClient.get — calls non-existent self._post

The get method calls self._post(url, **kwargs) but no such attribute exists on the class (only post is defined). Any test path that exercises an HTTP GET through this fake client will immediately raise AttributeError instead of returning the mocked rate-limit response, silently hiding the intended coverage. The affected method is currently untriggered by the existing test suite since both _fetch_flights and _fetch_dates use POST, but the latent bug is one future get-based search path away from causing confusing failures.

Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/search/test_rate_limit_detection.py
Line: 55-58

Comment:
**Typo in `_RateLimitedClient.get` — calls non-existent `self._post`**

The `get` method calls `self._post(url, **kwargs)` but no such attribute exists on the class (only `post` is defined). Any test path that exercises an HTTP GET through this fake client will immediately raise `AttributeError` instead of returning the mocked rate-limit response, silently hiding the intended coverage. The affected method is currently untriggered by the existing test suite since both `_fetch_flights` and `_fetch_dates` use `POST`, but the latent bug is one future `get`-based search path away from causing confusing failures.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Cursor Fix in Claude Code Fix in Codex

Comment thread fli/search/_wire.py
Comment on lines +142 to +147
if isinstance(body, bytes):
try:
body = body.decode("utf-8", errors="ignore")
except UnicodeDecodeError:
return False
return RATE_LIMIT_ERROR_MARKER in body

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 except UnicodeDecodeError is dead code after errors="ignore"

bytes.decode("utf-8", errors="ignore") never raises UnicodeDecodeError — the errors="ignore" flag suppresses decode errors by silently dropping bad bytes. The except clause is therefore unreachable in both is_rate_limit_response and extract_error_session_id. This pattern appears twice in the new code.

Suggested change
if isinstance(body, bytes):
try:
body = body.decode("utf-8", errors="ignore")
except UnicodeDecodeError:
return False
return RATE_LIMIT_ERROR_MARKER in body
if isinstance(body, bytes):
body = body.decode("utf-8", errors="ignore")
return RATE_LIMIT_ERROR_MARKER in body
Prompt To Fix With AI
This is a comment left during a code review.
Path: fli/search/_wire.py
Line: 142-147

Comment:
**`except UnicodeDecodeError` is dead code after `errors="ignore"`**

`bytes.decode("utf-8", errors="ignore")` never raises `UnicodeDecodeError` — the `errors="ignore"` flag suppresses decode errors by silently dropping bad bytes. The `except` clause is therefore unreachable in both `is_rate_limit_response` and `extract_error_session_id`. This pattern appears twice in the new code.

```suggestion
    if isinstance(body, bytes):
        body = body.decode("utf-8", errors="ignore")
    return RATE_LIMIT_ERROR_MARKER in body
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Cursor Fix in Claude Code Fix in Codex

Comment thread fli/search/_wire.py
Comment on lines +158 to +163
if isinstance(body, bytes):
try:
body = body.decode("utf-8", errors="ignore")
except UnicodeDecodeError:
return None
if RATE_LIMIT_ERROR_MARKER not in body:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Same dead-code except UnicodeDecodeError in extract_error_session_iderrors="ignore" makes the clause unreachable.

Suggested change
if isinstance(body, bytes):
try:
body = body.decode("utf-8", errors="ignore")
except UnicodeDecodeError:
return None
if RATE_LIMIT_ERROR_MARKER not in body:
if isinstance(body, bytes):
body = body.decode("utf-8", errors="ignore")
if RATE_LIMIT_ERROR_MARKER not in body:
Prompt To Fix With AI
This is a comment left during a code review.
Path: fli/search/_wire.py
Line: 158-163

Comment:
Same dead-code `except UnicodeDecodeError` in `extract_error_session_id``errors="ignore"` makes the clause unreachable.

```suggestion
    if isinstance(body, bytes):
        body = body.decode("utf-8", errors="ignore")
    if RATE_LIMIT_ERROR_MARKER not in body:
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Cursor Fix in Claude Code Fix in Codex

Comment on lines +153 to +212
class TestMCPServerRateLimit:
"""MCP layer should convert the exception into a structured error.

Not a vanilla ``success=False, error="Search failed: ..."`` — callers
keying off the ``code`` field can implement targeted backoff.
"""

def test_execute_flight_search_returns_rate_limit_code(
self, rate_limit_body: str, monkeypatch: pytest.MonkeyPatch
) -> None:
from fli.mcp import server

rate_limited = _RateLimitedClient(rate_limit_body)
original_init = SearchFlights.__init__

def _patched_init(self, *args: Any, **kwargs: Any) -> None:
original_init(self, *args, **kwargs)
self.client = rate_limited

# Patch SearchFlights to ship the fake client on construction so the
# MCP layer's own `SearchFlights()` picks it up without extra wiring.
monkeypatch.setattr(SearchFlights, "__init__", _patched_init, raising=True)
params = server.FlightSearchParams(
origin="BZN",
destination="SEA",
departure_date="2026-07-15",
currency="USD",
)
result = server._execute_flight_search(params)
assert result["success"] is False
assert result["code"] == "RATE_LIMITED"
assert result["retry_after_s"] == 30
assert result["session_id"] == "avU5aqPqKqioj8oP77Xy6Qc"
assert result["flights"] == []

def test_execute_date_search_returns_rate_limit_code(
self, rate_limit_body: str, monkeypatch: pytest.MonkeyPatch
) -> None:
from fli.mcp import server

rate_limited = _RateLimitedClient(rate_limit_body)
original_init = SearchDates.__init__

def _patched_init(self, *args: Any, **kwargs: Any) -> None:
original_init(self, *args, **kwargs)
self.client = rate_limited

monkeypatch.setattr(SearchDates, "__init__", _patched_init, raising=True)
params = server.DateSearchParams(
origin="BZN",
destination="SEA",
start_date="2026-07-10",
end_date="2026-07-25",
currency="USD",
)
result = server._execute_date_search(params)
assert result["success"] is False
assert result["code"] == "RATE_LIMITED"
assert result["retry_after_s"] == 30
assert result["dates"] == []

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 _execute_booking_options rate-limit path has no test coverage

The PR adds a GoogleFlightsRateLimited catch block in _execute_booking_options (server.py lines ~766-777) but TestMCPServerRateLimit only exercises _execute_flight_search and _execute_date_search. A test analogous to test_execute_flight_search_returns_rate_limit_code is missing for the booking-options path, so any regression in its structured error response (e.g., wrong key name or missing code field) would go undetected.

Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/search/test_rate_limit_detection.py
Line: 153-212

Comment:
**`_execute_booking_options` rate-limit path has no test coverage**

The PR adds a `GoogleFlightsRateLimited` catch block in `_execute_booking_options` (server.py lines ~766-777) but `TestMCPServerRateLimit` only exercises `_execute_flight_search` and `_execute_date_search`. A test analogous to `test_execute_flight_search_returns_rate_limit_code` is missing for the booking-options path, so any regression in its structured error response (e.g., wrong key name or missing `code` field) would go undetected.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Cursor Fix in Claude Code Fix in Codex

…surface

Google Flights' throttle keys off the client TLS / HTTP-2 fingerprint
(JA3 + ALPN + h2 frame settings + priority frames), not just IP.
A tight burst of flight queries from a single fingerprint (the prior
hardcoded `impersonate="chrome"`) exhausts the per-fingerprint quota
and Google returns the ErrorResponse envelope surfaced by PR punitarani#208.

Switching to random per-request rotation across a small diverse pool
(2 Chrome variants + Firefox + Safari + one stable Chrome anchor)
spreads requests across multiple browser identities. In a smoke test
of 5 cold queries to different routes immediately after the prior
session burned the per-fingerprint quota, 3 of 5 succeeded with real
flight rows; the other 2 returned the now-explicit RATE_LIMITED code
so callers can retry, likely landing on an unblocked fingerprint.

This is the second half of the rate-limit resilience pair:

  - PR punitarani#208: surface the rejected-envelope path as
    `GoogleFlightsRateLimited` instead of silently returning empty
    so callers can detect + back off.

  - This commit: reduce how often callers hit the wall in the first
    place by widening the fingerprint surface.

Together they shift the rate-limit experience from "all flight queries
inexplicably return zero results until the IP cools down" to "most queries
work; a minority surface a typed retryable error".

Pool: chrome120, chrome124, chrome131, firefox133, safari17_0. All
verified against `curl_cffi.requests.BrowserType` in the unit test, so
a future curl_cffi release that drops one of these surfaces at test
time rather than at HTTP time.

Tests:

  - `tests/search/test_impersonate_rotation.py` (5 cases): pool diversity
    (at least 2 browser families), every member is a valid curl_cffi
    BrowserType, `pick_impersonate` returns pool members, statistically
    visits every member across 500 picks, distribution is roughly uniform.

  - All 38 unit tests in the touched modules pass (33 prior + 5 new),
    no regressions.

The rotation reduces but does not eliminate rate-limit hits — the
`GoogleFlightsRateLimited` path remains the safety net. Callers should
still handle `code == "RATE_LIMITED"` and back off / retry as the
upstream behaviour can change at any time.
@alexechoi

Copy link
Copy Markdown

Heads-up: four open PRs now address this same envelope (#201, #205, #224, and this one) — I posted a comparison with live-captured evidence on #201. Two things from this PR are worth preserving whichever base lands: the structured MCP error payloads + dates.py coverage, and the captured fixture. I can independently corroborate extract_error_session_idrow[5][2][0][1][0][3] yields the request id on my own GetExploreDestinations capture too (#226), so that path is confirmed against a second sample.

One caution on framing: the code-13 envelope isn't always rate limiting — I can reproduce it deterministically with malformed requests (missing date, wrong location type, missing same-origin header), so surfacing it as RATE_LIMITED with a hardcoded retry_after_s: 30 may mislead callers into backing off when the request itself is the problem. A neutral BACKEND_ERROR with the code + detail attached would stay honest about what's actually known.

Also, the impersonate-rotation commit might get more traction as its own PR — it's a separate (and more contestable) change, and it would be a shame for it to hold up the error surfacing.

Whirlywack added a commit to Whirlywack/skrendam that referenced this pull request Aug 21, 2026
Investigated four plausible "maybe we're using fli wrong" hypotheses. All
negative, all worth recording:

- Stale vendored fork: our fli/ is at upstream daf9e9a and the only later
  upstream commit is TypeScript-only. Nothing to pull.
- Wrong surface: CLI, MCP and library share one client.py and the same three
  RPCs, so no surface can be less blocked. There is no REPL.
- Retry: 12 identical requests, 3s apart, 0/12. The block is deterministic
  per request, so upstream PRs punitarani#201/punitarani#205/punitarani#208 would not help.
- Byte-vs-char chunk framing (upstream PR punitarani#224): reproduced synthetically,
  then REFUTED against the 7 real captured Google bodies in
  tests/search/fixtures/ — 6 contain multi-byte UTF-8 and all decode
  correctly. Google counts bytes; our decoder is right. We are not
  discarding decodable European responses.

Also records two untested leads (force-IPv4 per issue punitarani#200, curl_cffi string
proxy per issue punitarani#50) and an independent A/B whose no-bgr baseline of 1/6
matches our measured 6/40.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014DZNr5u3Jbx34RkbJbq7YY
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.

Round-trip search missing results

2 participants