JSX: clean up dead WKWebView fallback paths

Now that we've confirmed the direct in-page fetch() POST to
/api/nsk/v4/availability/search/simple works end-to-end on real iOS
devices (and is the only thing that does — simulator is blocked at
the transport layer by Akamai per-endpoint fingerprinting), delete
the dead simulator-era attempts that were kept around as hopeful
fallbacks:

- Delete nativePOSTSearchSimple and all the URLSession+cookie-replay
  plumbing. URLSession can't reach /search/simple from iOS Simulator
  either (TLS fingerprint same as WKWebView), and on real device the
  in-page fetch already works so the URLSession path is never useful.
- Delete the ~150 lines of SPA state-harvest JavaScript that walked
  __ngContext__ to find the parsed availability payload inside
  Angular services as an attempt-2 fallback. The state-harvest was a
  proxy for "maybe the POST went through but our interceptor
  swallowed the response" — that theory is dead now that we know the
  POST itself is what's blocked in the simulator.
- Delete the capturedBody instance property that only nativePOST
  wrote to.

Step 17 is now exactly what it claims to be: read the sessionStorage
token, fire a single direct fetch() POST from the page context, return
the body on success. ~400 lines removed from JSXWebViewFetcher.swift
(2148 -> 1748).

Step 18's low-fare fallback stays as graceful degradation when the
POST fails (which happens on iOS Simulator). The fallback cabin is now
labeled "Route day-total (fallback)" instead of "Route (day total)"
so the UI clearly distinguishes a per-flight seat count from a route
estimate.

JSX_NOTES.md corrected: removed the inaccurate claim that WKWebView
POSTs to /search/simple just work. The anti-bot-surface table now
separates iOS Simulator (fails) from real iOS device (works) with
the specific error modes for each. TL;DR adds a visible caveat at
the top that the working path requires a real device; develop with
the low-fare fallback in the simulator.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-04-11 13:50:54 -05:00
parent 4d46b836a1
commit 5f19e48172
3 changed files with 66 additions and 451 deletions

View File

@@ -21,6 +21,13 @@ hardware against the live production site in April 2026.
4. Driving the on-page UI to submit the search form is a dead end in
**WKWebView** (but works in Playwright) because of a subtle trusted-event
issue described below.
5. **Environment caveat**: the in-page `fetch()` POST (#3) works on a
**real iOS device** but **fails in the iOS Simulator**. Simulator
WebKit runs on macOS's CFNetwork stack, whose TLS/H2 fingerprint
Akamai's per-endpoint protection tier on `/search/simple` treats as
untrusted. Other `api.jsx.com` endpoints (tokens, bootstrap graph
calls, low-fare estimate) work from both. **Test on device; develop
with the low-fare fallback in the simulator.**
The working pattern is therefore:
@@ -28,7 +35,7 @@ The working pattern is therefore:
Wait for SPA bootstrap (station buttons exist, token call finishes)
Read token from sessionStorage
POST /api/nsk/v4/availability/search/simple ← from inside the page
Parse the response
Parse the response (real device only)
---
@@ -346,6 +353,12 @@ via a direct API call whose request shape matches exactly what
This is what `Flights/Services/JSXWebViewFetcher.swift` step 17 does.
**Important environment caveat:** this workaround works on a **real iOS
device** but **fails in the iOS Simulator**. See the anti-bot surface
table below for the specific failure modes. Develop with a real device
plugged in, or accept the low-fare day-total fallback when running
against the simulator.
---
## Anti-bot surface (Akamai)
@@ -357,8 +370,9 @@ JSX fronts `api.jsx.com` with Akamai Bot Manager. Observed behavior:
| Plain `curl`, `fetch` from Node, any external HTTP client | Blocked. Almost all endpoints return HTML challenge page or Akamai error. |
| Playwright's built-in `chromium.launch()` (both bundled chromium and `channel: "chrome"`) | GET requests succeed, but `POST /availability/search/simple` specifically returns `ERR_HTTP2_PROTOCOL_ERROR`. Playwright injects enough automation bits for Akamai to flag the TLS/H2 fingerprint. |
| Real Chrome spawned as a plain process + Playwright attached via `chromium.connectOverCDP()` | **Works reliably.** Chrome has the expected fingerprint and Playwright is only driving it via CDP, not altering it. |
| WKWebView on macOS / iOS | GET requests succeed. Direct `POST /availability/search/simple` from inside a loaded `jsx.com` page via `fetch()` also succeeds. The browser session's cookies and TLS fingerprint are trusted. |
| WKWebView with UI-driven Find Flights click | Fails — but for an unrelated reason (the trusted-events trap above). Angular never fires the POST in the first place, so Akamai never sees it. |
| **WKWebView on iOS Simulator** | GETs to `api.jsx.com` succeed (`/lowfare/estimate`, `/nsk/v1/token`, `/graph/*`). **`POST /availability/search/simple` fails** with `TypeError: Load failed` (in-page `fetch()`/`XHR`) or `NSURLErrorNetworkConnectionLost` (-1005) (native `URLSession` with cookies forwarded from the WKWebView store). iOS Simulator WebKit runs against macOS's CFNetwork stack, whose TLS/H2 fingerprint Akamai's per-endpoint protection tier treats as untrusted for this specific endpoint. No configuration (custom UA, XHR vs fetch, credentials mode, content type, cookie replay into URLSession) makes it work. |
| **WKWebView on real iOS hardware** | **Works.** Direct `POST /availability/search/simple` from inside a loaded `jsx.com` page via `fetch()` returns `status=200` with the full per-flight availability payload. Real iOS WebKit has the iOS-specific network stack whose TLS/H2 fingerprint Akamai accepts. The fetcher in `Flights/Services/JSXWebViewFetcher.swift` step 17 does exactly this and produces correct per-class seat counts. |
| WKWebView with UI-driven Find Flights click (any platform) | Fails for an unrelated reason: WKWebView's synthetic `MouseEvent` has `isTrusted === false`, and JSX's custom datepicker only commits its day-cell selection to the Angular form model on a trusted user gesture. Result: the input visually shows the selected date but the underlying model stays null, `form.invalid === true`, and Angular's `search()` silently returns without firing a request. The direct-fetch POST bypasses this entirely. |
Observations: