The hard Flutter API bugs rarely look like “networking is broken.” They look like this: macOS works, Android Emulator times out, a physical device never reaches your local API, Dio sends an expired token, staging returns a different JSON shape than production, and checkout only fails after a retry. Logs help, but logs are still your app’s opinion. A proxy shows what actually crossed the boundary.
This guide is a case-by-case debugging workbook for the Rockxy Flutter sample. It starts with one safe HTTP request and gradually moves into localhost routing, HTTPS inspection, filtering, Allow List, Map Remote, Modify Header, Breakpoints, Map Local, Block List, Network Conditions, Replay, Compose, and scripting. It is deliberately practical: each case has a problem, a sample scenario, a Rockxy action, expected evidence, and a failure interpretation.
The debugging assignment
Imagine a Flutter storefront with this production-like incident:
- Startup works on macOS, but Android Emulator users see timeouts.
- A developer points the app at
localhost, but the request reaches the wrong runtime. - HTTPS rows appear in the proxy, but headers and bodies are not decrypted.
- The profile API returns 401 after a refresh should have happened.
- The app is accidentally calling a production-marked environment during staging QA.
- Checkout fails because the POST body total does not match the backend calculation.
- A product endpoint returns a schema the Flutter UI does not expect.
- An optional analytics endpoint fails and should not break the user flow.
- The team needs to retry edited requests without tapping through the whole app again.
The goal is not to “prove Rockxy can capture something.” The goal is to build a repeatable workflow a Flutter engineer can follow when a real app has the same class of API, HTTPS, proxy, environment, auth, timeout, and contract bugs.
The sample emits safe correlation headers on every request:
X-Rockxy-Lab: flutter-sample
X-Rockxy-Lab-Run-Id: run-...
X-Rockxy-Scenario-Id: auth-recovery
X-Rockxy-Step-Id: expired-token
X-Rockxy-Client: dio-5
X-Rockxy-Runtime: local-apple-runtime
X-Request-ID: run-...-auth-recovery-expired-token
Those headers are the backbone of the workflow. In Rockxy, filter by the run ID first, then drill down by scenario and step ID. That keeps the capture readable when your Mac is also producing browser, package manager, simulator, analytics, and background app traffic.
What success looks like
By the end of the workbook, you should have a capture where each row can answer one of these questions:
- Did the Flutter runtime actually use Rockxy as its HTTP proxy?
- Did the request go to the intended API host, path, scheme, and environment?
- Can Rockxy decrypt the HTTPS request and response body for the target host?
- Which header, token, query parameter, or body value caused the failure?
- Can a rule, fixture, replay, or script isolate the problem without changing app code?
- Is the failure in the Flutter client, the proxy setup, the backend contract, or the current Rockxy UX?
Prepare the lab
Before debugging HTTPS, auth, mapping, or replay, prove that one plain HTTP request reaches Rockxy. Start the sample API and app:
git clone https://github.com/RockxyApp/Rockxy-Flutter-Sample-Guidance.git
cd Rockxy-Flutter-Sample-Guidance
fvm flutter pub get
fvm dart run tool/rockxy_demo_api.dart --port 43210
# In another terminal
fvm flutter run -d macos
In Rockxy, start capture and copy the active proxy port. In the sample app, paste that port into Rockxy port. Keep Demo API base URL separate from the proxy target. For the first pass on macOS, use:
| Sample field | Value for the first pass |
|---|---|
| Runtime | iOS Simulator / macOS desktop |
| Rockxy port | The active port from Rockxy, not the demo API port |
| Demo API base URL | http://127.0.0.1:43210 |
| Client | Dart HttpClient first, then repeat with package:http and Dio |
| Proxy through Rockxy | Enabled |
Proxy host and API host are different
A lot of Flutter localhost API debugging gets derailed because two different addresses are mixed together: the proxy address and the API address.
| Runtime | Rockxy proxy host | Local API host |
|---|---|---|
| macOS Flutter | 127.0.0.1:<Rockxy port> | 127.0.0.1:43210 |
| iOS Simulator | 127.0.0.1:<Rockxy port> | 127.0.0.1:43210 |
| Android Emulator | 10.0.2.2:<Rockxy port> | 10.0.2.2:43210 when the app connects directly to the Mac API |
| Physical device | <Mac LAN IP>:<Rockxy port> | <Mac LAN IP>:43210 if the demo API is bound to a reachable address |
When the Flutter app explicitly routes through Rockxy, Rockxy opens the upstream connection. That means the API URL can be Mac-local for the desktop path. When you compare direct behavior from an emulator or device, the API host has to be reachable from that runtime too.
Case 0: capture a known-good HTTP request
Problem. You cannot debug advanced failures until you know the Flutter client is actually sending traffic through Rockxy.
Run in the sample. Select Known-good app startup and run the selected scenario. Start with the bootstrap step.
Use in Rockxy.
- Start capture before pressing Run Scenario in the Flutter sample.
- Filter the traffic table by
/rockxy-demo/bootstrapor by the run ID shown in the sample. - Open the captured row and keep the inspector on the request overview first.
- Check the method, full URL, query string, status code, response time, request headers, response headers, and response body.
- Switch between the request and response body views so you know Rockxy is capturing both sides of the exchange, not only the URL.
Expected evidence.
- The table has one GET row for
http://127.0.0.1:43210/rockxy-demo/bootstrapwhen you run the macOS path. - The request headers include
X-Rockxy-Lab: flutter-sample,X-Rockxy-Scenario-Id: app-startup,X-Rockxy-Step-Id: bootstrap, and a uniqueX-Rockxy-Lab-Run-Id. - The response status is 200 and the response body is JSON from the local demo API, not an HTML error page, proxy error, or empty body.
- The timing is small and stable on localhost; if it is already slow here, fix the baseline before moving to latency or timeout cases.
If it fails. If no row appears, do not debug backend code yet. Check that Rockxy capture is running, the active proxy port was copied correctly, the sample proxy toggle is enabled, and the selected runtime matches where Flutter is running.
Case 1: debug localhost and runtime confusion
Problem. localhost is not universal. On macOS it means the Mac. Inside Android Emulator, the route back to the Mac is usually 10.0.2.2. On a physical device, the Mac must be reachable by LAN IP.
Run in the sample. Select Known-good app startup and press Run Scenario. The sample runs runtime-diagnostic as the third request in that scenario; it is not a separate scenario in the dropdown. Then repeat the scenario on another runtime if you have one available.
Use in Rockxy.
- Filter by
X-Rockxy-Step-Id: runtime-diagnosticor search for/rockxy-demo/runtime-diagnostic. - Open the row and inspect the request URL host, port, and query string.
- Open request headers and find
X-Rockxy-Runtime; compare it with the runtime selected in the sample UI. - Compare the captured destination with the proxy host displayed by the sample summary. Do not mix up the upstream API host with the Rockxy proxy host.
- Repeat the same case after changing the runtime selector only when the Flutter app is actually running in that runtime.
Expected evidence.
- On macOS desktop and iOS Simulator, the runtime header should be
local-apple-runtimeand the local API path should resolve through127.0.0.1:43210. - On Android Emulator, the runtime header should be
android-emulator, and the proxy route back to the Mac should use10.0.2.2:<Rockxy port>. - On a physical device, the runtime header should be
physical-device, and both the proxy host and any directly reached local API host must use the Mac LAN IP, not127.0.0.1. - If Rockxy captures the row, the request reached Rockxy. That proves proxy routing, but it does not by itself prove which Flutter target emitted it; the correlation headers provide that clue.
If it fails. If Android Emulator times out while macOS works, suspect runtime routing first. If a physical device cannot connect, check Wi-Fi proxy settings, Mac firewall, same-network access, and whether the demo API is bound to a reachable interface.
Case 2: reduce noise with filters and Allow List
Problem. A real macOS capture quickly fills with browser tabs, package managers, simulator traffic, update checks, and app telemetry. If you inspect everything, you debug nothing.
Run in the sample. Run Run All once and copy the active run ID from the sample UI.
Use in Rockxy.
- Run all sample scenarios once and copy the run ID shown in the sample result area.
- Paste the run ID into Rockxy search or create a header filter for
X-Rockxy-Lab-Run-Id. - Add a second filter for
X-Rockxy-Scenario-Idwhen you want one scenario, such asauth-recoveryorcheckout-contract. - Use status filters when you want to isolate failures such as 401, 409, and 500.
- Open Allow List and create a focused rule for the demo API, for example method
ANYwith a wildcard pattern such as*127.0.0.1:43210/rockxy-demo/*for the macOS path. Turn the Allow List tool on, run Run All again, and watch whether Rockxy makes the active state obvious in the status bar or window chrome. This is the real UX test: a developer should be able to tell at a glance that capture is intentionally narrowed. - Test the failure mode deliberately. First make the rule too narrow, such as
*127.0.0.1:43210/rockxy-demo/bootstrap*, and run the sample again. The Flutter sample should still complete non-matching requests because Allow List forwards traffic; Rockxy should simply stop recording rows outside that pattern. Then relax the rule back to/rockxy-demo/*, disable it before third-party HTTPS or device traffic, and confirm the table repopulates without needing to restart capture. If the UI does not explain why rows disappeared, or if disabling the tool does not restore new rows immediately, that is a product bug or an immediate UX fix candidate.
Expected evidence.
- Filtering by run ID should leave only requests from that sample run, across all scenario groups.
- Filtering by scenario ID should reduce the view to a small, ordered set of rows that matches the sample timeline.
- Status filtering should immediately reveal the intentional failures: 401 for expired token, 409 for checkout mismatch, and 500 for server error.
- Allow List should reduce captured/displayed noise; it should not be described as a blocking mechanism. Use Block List later when the goal is to fail traffic intentionally.
- With a too-narrow Allow List rule, the sample UI can still show successful responses while Rockxy records fewer rows. That difference proves the current logic: non-matching traffic is forwarded but not saved to the session.
- The active Allow List state should be visible enough that a tired developer understands why the table is empty before they suspect the proxy, simulator, or Flutter code.
If it fails. If Allow List hides more than expected, verify the host, port, method, wildcard scope, and whether a normal search filter is also active. Also remember the current model: Allow List focuses what Rockxy captures and displays; Block List is the feature for intentionally blocking or dropping traffic. If users cannot recover by relaxing or disabling the rule and running a fresh request, prioritize that as a capture-state or UI feedback bug.
Case 3: inspect HTTPS traffic
For Flutter HTTPS debugging, two things have to be true. The runtime must trust Rockxy’s certificate path, and the target host must be inside Rockxy’s SSL Proxying scope. Global SSL Proxying by itself is not the whole story: include rules, exclude rules, bypass domains, certificate rejection, and app-level certificate pinning can all keep traffic tunneled.
Run in the sample. Paste one exact read-only HTTPS URL into the sample’s Advanced custom probe field. Use https://api.github.com/zen when you want a small plain-text response and https://httpbin.org/get when you want a JSON response that echoes request details. In this case, the host is only the domain name inside that URL: api.github.com or httpbin.org. That host value is what you will use in SSL Proxying rules. Start on macOS desktop or iOS Simulator first, then repeat the same exact URL on Android Emulator or a physical device. Keep the sample’s debug certificate bypass enabled only for local learning when you are proving proxy wiring; for a real HTTPS debugging pass, prefer proper platform certificate trust so you can confirm Rockxy is decrypting traffic the same way the runtime will behave without shortcuts.
Use in Rockxy.
- Install trust first. Open the certificate setup flow and install Rockxy’s CA for the runtime you are testing. For macOS this usually means trusting the root CA in Keychain once. For iOS Simulator or a physical iPhone/iPad, install the profile and enable full trust in system settings. For Android debug builds, make sure the app trusts user CAs through a debug-only network security configuration before you assume Rockxy is the problem.
- Verify proxy routing. Before testing the app flow, confirm that the selected runtime is really using Rockxy as its proxy. If the proxy host or port is wrong, the sample may still show that it ran, but Rockxy will never get a chance to present its generated certificate.
- Scope SSL Proxying narrowly. Open SSL Proxying and add an include rule for the host only. If your probe URL is
https://api.github.com/zen, addapi.github.com. If your probe URL ishttps://httpbin.org/get, addhttpbin.org. Do not put the full path there first. - Check bypass and exclude rules. Confirm that same host is not excluded or bypassed by another SSL Proxying or bypass rule. If the host is bypassed, Rockxy may still show connection metadata, but it will behave like a tunnel and you will not get a decrypted request body.
- Run the public probe. Paste
https://api.github.com/zenorhttps://httpbin.org/getinto the sample’s Advanced custom probe field and run it. Watch the request list from the start of the connection: a normal intercepted HTTPS flow often begins with CONNECT-style tunnel setup and then becomes an inspectable HTTP transaction once TLS is terminated inside Rockxy. - Inspect in order. Open the captured row and inspect it in order: request overview, request headers, request body, response headers, response body, then TLS details if available. The practical question is simple: did Rockxy decrypt a real HTTP exchange, or are you still looking at only tunnel-level evidence?
- Compare runtimes. Repeat the same exact URL on a second runtime, such as macOS first and Android Emulator second. When one runtime decrypts cleanly and another does not, the gap is usually in runtime trust or app-specific certificate handling rather than the public API itself.
Expected evidence.
- Decrypted row. The table should show the HTTPS request as an inspectable HTTP transaction for the included host, not only a raw tunnel entry.
- Real request headers. The inspector should show request headers beyond CONNECT metadata, including the sample correlation headers when the request comes from the Flutter sample.
- Readable public response body. For
https://api.github.com/zen, expect a small readable text response body. Forhttps://httpbin.org/get, expect a readable JSON body that echoes request details and visible request headers. - Runtime difference is evidence. If one runtime works and another fails on the same host, that is strong evidence that the certificate trust path or app-level TLS policy differs by runtime.
- Tunnel-only view means TLS is unresolved. If you see only CONNECT or tunnel metadata, or an empty decrypted view, treat that as trust, SSL scope, bypass-rule, or proxy-path evidence first, not as an API failure.
- Transport first, API second. If the request fails inside the app before Rockxy shows a decrypted row, treat that as a transport-layer problem first, not an API-contract bug.
If it fails. Separate the failure by layer. No row at all usually means the request never used Rockxy. CONNECT or tunnel-only row means the proxy path exists but decryption did not happen; check certificate trust, SSL Proxying scope, and bypass rules for the exact domain you tested, such as api.github.com or httpbin.org. Certificate warning or app rejection usually points to incomplete trust, certificate pinning, or runtime-specific TLS policy. One runtime works and another fails means the trust path is still incomplete on the failing runtime. Do not treat debug certificate bypass code as production guidance; use it only to learn where the failure boundary is.
Case 4: catch the wrong backend environment
Problem. The app reports it is in staging, but the client is sending X-App-Environment: production on every request. The mismatch is invisible in app logs until something breaks in production. You need Rockxy to surface the wrong header so you can use Map Remote to fix the routing without touching Flutter code.
Run in the sample. Open the Scenario dropdown and select Wrong backend environment. The scenario has one step, production-config: a GET to /rockxy-demo/environment that sends X-App-Environment: production while the query param says expected=staging. Click Run Scenario. Do not add any Map Remote rules on the first run — you want to capture the raw mismatch first.
Use in Rockxy.
- Find the row. Filter by
X-Rockxy-Scenario-Id: wrong-environmentor search for/rockxy-demo/environmentin the request list. You should see a single GET row with status 200. - Inspect the request URL. Open the row and check the full URL — it includes the query parameter
?expected=staging. That is the declared intent: the client wants staging behaviour. Note the mismatch before looking at headers. - Inspect request headers. Find
X-App-Environment: production. This is the wrong-environment marker — the client is identifying itself as production while the query says staging is expected. Also confirm the standard correlation headers:X-Rockxy-Scenario-Id: wrong-environment,X-Rockxy-Step-Id: production-config, andX-Rockxy-Lab-Run-Id.
X-App-Environment: production while the URL carries expected=staging — the environment mismatch is visible before any Map Remote rule is applied.- Read the response body. Open the response body tab and confirm two things:
"query": {"expected": "staging"}is echoed back, and"featureFlags": {"remoteMappingCandidate": true}is set. The server marks this endpoint as a Map Remote candidate. If either field is missing, the request did not reach the demo server correctly.
"expected": "staging" in the query field and flags "remoteMappingCandidate": true, confirming the server received the environment mismatch signal.-
Create a Map Remote rule. Open the Map Remote tool in Rockxy and click the + button to add a new rule. Fill the dialog as follows:
Click Add to save, then make sure the rule is toggled on before the next run.Matching RuleName
Case 4 – wrong environmentRule127.0.0.1:43210/rockxy-demo/environmentMethodANYModeUse WildcardSubpaths☐ uncheck — this is an exact path, not a treeMap ToProtocolhttps— the menu offers Keep Original, http, and https; pickhttpshere because httpbin only accepts TLSHosthttpbin.org— use this to confirm the rule fires without needing a real staging serverPort443Path/getQueryleave blank — passes the original query string through💡 Tip: paste
https://httpbin.org/getdirectly into the Host field — Rockxy auto-parses it into Protocol, Host, Port, and Path for you.⚠️ Map Remote rewrites the destination only. It does not change
X-App-Environment: productionin the request headers. If you also need to fix the header value, add a separate Modify Header rule: setX-App-Environment→staging. - Run the scenario again. Click Run Scenario a second time with the Map Remote rule active. Watch for a new row in the request list — it should show the request hitting
httpbin.org/get, not127.0.0.1:43210/rockxy-demo/environment.
httpbin.org/get — the Map Remote rule fired and rewrote the destination without any Flutter code change.- Compare before and after rows. Open both rows side by side. The before row shows the raw production-marked request going to the demo server. The after row shows the same Flutter request — same headers, same query — rerouted to the mapped target by Rockxy alone, with zero Flutter code change required.
production-config request goes to 127.0.0.1:43210/rockxy-demo/environment with X-App-Environment: production exposed in the request headers.
httpbin.org/get by the Map Remote rule. No Flutter code was changed between the two runs.Expected evidence.
- Raw mismatch row. The first run should show a GET to
/rockxy-demo/environment?expected=stagingwith status 200 and request headerX-App-Environment: productionclearly visible. - Response confirms mismatch. The response body should contain
"query": {"expected": "staging"}and"featureFlags": {"remoteMappingCandidate": true}, confirming the server received the wrong-environment signal. - Correlation block is correct. The response body's
correlationobject should show"scenarioId": "wrong-environment"and"stepId": "production-config", linking the captured row to this exact sample step. - Map Remote rewrites the destination. After adding the rule, the second run row should show the request URL pointing at the mapped target, not the original demo endpoint, with the same Flutter-emitted headers intact.
- No Flutter code change needed. The fix lives entirely in Rockxy's Map Remote rule. The Flutter sample does not change between runs; only the Rockxy rule is toggled. That is the point of the case.
If it fails. If no row appears, check that Rockxy capture is running and the sample proxy toggle is on. If Map Remote does not fire, the match pattern is likely wrong — check host, port, path prefix, and whether the rule is enabled and positioned before any conflicting rules. If the row appears but the destination URL is unchanged, the rule did not match; use Rockxy's rule test or match preview to verify the pattern against the exact URL you captured. If you also need to fix the header value and not just the routing, add a Modify Header rule alongside Map Remote — Map Remote rewrites the destination but does not mutate request headers by default. Do not reach for scripting as the first tool for host-level rewrites; Map Remote is the right feature here.
Case 5: diagnose a Flutter auth-token problem with Modify Header and Breakpoints
Problem. The Flutter profile screen returns HTTP 401 and the app logs only say "unauthorized." You need to know exactly which Authorization header was sent, whether the refresh-token call succeeded, whether the retried request actually used the new token, and where the bug lives — Flutter auth state, the Dio interceptor chain, secure-storage write timing, or backend auth validation. Without a proxy you guess. With Rockxy you see every byte of the Authorization header, the refresh POST body, and the retried request side by side.
Run in the sample. Open the Scenario dropdown and select Expired token and retry. The scenario runs three sequential steps: expired-token (GET /rockxy-demo/profile with Authorization: Bearer expired-demo-token, expected 401), refresh-token (POST /rockxy-demo/auth/refresh with body {"refreshToken": "demo-refresh-token"}, expected 200), and profile-retry (GET /rockxy-demo/profile with Authorization: Bearer demo-access-token, expected 200). Click Run Scenario once with no Rockxy rules active so you can see the raw flow first.
Use in Rockxy.
- Filter by scenario. In the search bar, filter by
X-Rockxy-Scenario-Id: auth-recovery. You should see exactly three rows in this order: 401 GET, 200 POST, 200 GET.
X-Rockxy-Scenario-Id: auth-recovery isolates the three-step auth flow — the 401 expired-token call, the 200 refresh POST, and the 200 retry — giving you a clean, ordered timeline before you dig into headers.- Inspect the expired-token row. Open the 401 row. Confirm the request URL is
/rockxy-demo/profile, the method is GET, and the request headerAuthorization: Bearer expired-demo-tokenis present. Open the response body — the demo server returns{"ok": false, "error": "Token expired or missing", "expectedHeader": "Authorization"}. Read this contract before fixing anything.
Authorization: Bearer expired-demo-token in the request headers and the structured error contract {"error": "Token expired or missing", "expectedHeader": "Authorization"} in the response body — the exact data a Flutter auth interceptor should read before deciding whether to refresh.- Inspect the refresh-token row. Open the 200 POST row. Confirm the URL is
/rockxy-demo/auth/refresh, method is POST,Content-Type: application/json, and the body is exactly{"refreshToken": "demo-refresh-token"}. The response body returns{"accessToken": "demo-access-token", "expiresIn": 900}. Verify nothing in the response logs a real secret — a refresh endpoint should never expose refresh tokens in headers or logs.
{"refreshToken": "demo-refresh-token"} and receives {"accessToken": "demo-access-token", "expiresIn": 900} — exactly the token-exchange contract a Flutter auth interceptor needs to honour before the retried profile call.- Inspect the profile-retry row. Open the second 200 row. The Authorization header should now be
Bearer demo-access-token— different from step 2's expired token. This is the proof that the Dio interceptor or the HttpClient retry path updated the token correctly between requests. - Use Modify Headers to prove the source of the bug. Open Modify Headers and click + (or press
⌘N). Fill the matching rule with URL127.0.0.1:43210/rockxy-demo/profile, methodANY, match type Use Wildcard. Then add a header operation: Action: Set, Header Name: Authorization, Header Value: Bearer demo-access-token, Phase: Request (each operation has its own Request / Response phase toggle, so one rule can mutate both sides if needed). Enable the rule, then re-run the scenario. The first profile call should now succeed with 200 — proving the bug is in Flutter auth state, not the backend.
Authorization to Bearer demo-access-token on the fly. The previously 401 profile call now returns 200 with no Flutter rebuild — proving the bug lives in the Flutter auth interceptor, not the backend.-
Use a Breakpoint to pause and edit the request and response live. Breakpoints are the most powerful Rockxy tool for interactive Flutter API debugging — you stop the request mid-flight, edit URL / method / headers / body, then either send the edited version upstream or rewrite the response on its way back to Flutter. The workflow runs in six small phases. If anything goes wrong, follow the "What to check if it doesn't fire" notes at the end of each phase.
Phase A — Prepare an optional response template (skip if you don't need it yet).
- Open Window menu → Breakpoint Template. The Template Manager opens with two grouped sections: Request Templates and Response Templates. Two seed templates ship by default ("JSON Request", "JSON Response").
- Click + (or
⌘N), then open the More menu and pick New Response Template. - Set Name to
Flutter profile 200 OK. - Paste this into the Raw HTTP message editor:
HTTP/1.1 200 OK Content-Type: application/json {"profile": {"userId": "demo-user-001", "displayName": "Debug Shopper"}} - The validation indicator next to the editor should turn green ("savedAndActive"). Red means the raw message did not parse — fix the blank line between headers and body, or the status line format.
- Close the Template Manager. Templates persist across app restarts via Rockxy's local store.
Phase B — Create the Breakpoint rule.
- Open Breakpoint Rules (Tools menu, or the toolbar icon).
- Confirm the Enable Breakpoint Tool checkbox at the top is ticked. If it's off, rules are not evaluated regardless of how many you create.
- Click + (or
⌘N) to open the Breakpoint Rule Editor. Fill it exactly like this:NameCase 5 – auth profileMatching Rule127.0.0.1:43210/rockxy-demo/profileMethodANYModeUse WildcardSubpaths☐ uncheck — pause only the exact profile callBreakpoint☑ Request ☑ Response — one rule pauses both sides - Click Add (or
⌘↩). The rule appears in the rules list with both Request and Response columns showing a tick.
⚠️ If the rule does not appear: the editor probably has a validation error (Matching Rule is empty, or both Request and Response are unchecked — at least one phase must be on). Re-check, click Add again.
Phase B in Rockxy: the Breakpoint Rule Editor with Name, Matching Rule, Method, Match Type, and both Request and Response checkboxes configured. One rule covers both sides of the call — no need for a separate response rule. Phase C — Trigger the breakpoint.
- Switch to the Flutter sample. Select Expired token and retry in the Scenario dropdown and click Run Scenario.
- The first step (
expired-token) firesGET /rockxy-demo/profile. Rockxy intercepts it. The Breakpoint window opens automatically with a banner reading "Request paused at breakpoint" and a live elapsed-time counter. - The left sidebar shows one queued item with a REQ badge. The right editor binds to that item.
⚠️ If the Breakpoint window never opens and the request completes normally in Flutter: the rule didn't match. Check, in order:
(1) Enable Breakpoint Tool at the top of Breakpoint Rules is ticked.
(2) The rule's own toggle (right side of the row) is on.
(3) The Matching Rule string is exactly127.0.0.1:43210/rockxy-demo/profile— no trailing space, nohttp://prefix, no:43210/typo.
(4) Flutter is actually using Rockxy as proxy. Re-check the runtime headers on the captured row:X-Rockxy-Runtimeshould be set. If the row never appears in the main capture list either, Flutter is bypassing Rockxy entirely.
(5) No earlier tool (Allow List, Block List, Map Local, Map Remote) is consuming the request before Breakpoint sees it.Phase D — Edit the paused request.
- In the right editor, confirm the Method dropdown shows
GETand the URL field showshttp://127.0.0.1:43210/rockxy-demo/profile. - Click the Headers tab. The two-column table lists every header:
Authorization,X-Rockxy-Scenario-Id,X-Rockxy-Step-Id,X-Rockxy-Lab-Run-Id, etc. - Find the
Authorizationrow. Click directly on the value cell (theBearer expired-demo-tokentext) and type over it withBearer demo-access-token. - Click Execute in the bottom-right of the action bar (or press
⌘↩). Rockxy forwards the edited request upstream to the demo server.
💡 Faster path with a template: click the Raw tab, click the Template menu, pick a saved Request template. It replaces method + URL + headers + body in one shot. (For Phase E you'll use the Response template you prepared in Phase A.)
⚠️ If Execute is greyed out: no item is selected in the sidebar. Click the queued row to select it.
Phase D in Rockxy: the Breakpoint window holds the expired-tokenprofile request mid-flight. Editing theAuthorizationcell directly in the Headers key/value table replacesBearer expired-demo-tokenwithBearer demo-access-tokenbefore clicking Execute.Phase E — Edit the paused response.
- After Phase D's Execute fires, the same rule pauses again — this time on the incoming response. The sidebar gains a second row with a RES badge. The banner now reads "Response paused at breakpoint".
- The action bar swaps the Method dropdown for a Status Code dropdown listing 13 preset HTTP codes. Pick
200 OK. - Click the Body tab and paste a synthetic profile JSON:
Or click the Raw tab → Template menu → pick{"profile": {"userId": "demo-user-001", "displayName": "Debug Shopper"}}Flutter profile 200 OK(the template from Phase A) — that replaces status + headers + body in a single click. - Click the Headers tab and confirm
Content-Type: application/jsonis present. If not, add it via + Add Header. - Click Execute. Rockxy delivers the rewritten response to Flutter.
⚠️ If the response phase never fires (Phase D's Execute completes, the request hits the server, but no second pause appears): your rule's Response checkbox is off. Open Breakpoint Rules, edit the rule, confirm ☑ Response is ticked.
Phase E in Rockxy: the response side of the same rule paused next. The Status Code dropdown is switched from 401to200 OKand the Body tab carries a synthetic profile JSON. Clicking Execute delivers this rewritten response to Flutter as if the backend itself had returned it.Phase F — Verify in Flutter and in Rockxy.
- Switch back to the Flutter sample. The profile screen should now render with the synthetic user — proving the UI handles the success contract correctly.
- Switch to Rockxy's main capture window. Filter by
X-Rockxy-Step-Id: expired-token. The row should show:- Request header
Authorization: Bearer demo-access-token(your edit, not the original expired token). - Response status
200(your override, not the server's original401). - Response body containing the synthetic profile JSON.
- Correlation headers (
X-Rockxy-Scenario-Id,X-Rockxy-Step-Id,X-Rockxy-Lab-Run-Id) preserved unchanged.
- Request header
- If the Flutter screen rendered correctly, the bug is in the Flutter auth interceptor (it sent the expired token instead of refreshing first). If the screen still fails despite the 200 + synthetic body, the bug is in the Flutter screen logic — not the API.
Phase F verification: back in Rockxy's main capture, the expired-tokenrow now carries the rewrittenAuthorization: Bearer demo-access-tokenheader and a200response with the synthetic profile body. Correlation headers (X-Rockxy-Scenario-Id,X-Rockxy-Step-Id,X-Rockxy-Lab-Run-Id) are preserved — and Flutter renders the profile screen, proving the UI can handle the success contract.Action bar reference (bottom of the Breakpoint editor).
- Cancel (
Esc) — dismisses the window. The paused item stays in the queue until you reopen the window and resolve it. Use when you want to step away briefly. - Templates — opens the Template Manager (same as Window → Breakpoint Template). Use this if you forgot to prepare a template in Phase A.
- Abort (503) — resolves the paused item by returning HTTP 503 to the client without contacting the upstream. Useful for testing Flutter's client-side outage handling and timeout UX.
- Execute (
⌘↩, default action) — sends the edited request upstream, or delivers the edited response to the client. The most common action.
Use Breakpoints for one-off interactive debugging where you want to react to the live message. Use Modify Headers (step 5) when you want the same edit to apply silently across every matching request without pausing.
Expected evidence.
- Three rows in scenario order. Filtering by
X-Rockxy-Scenario-Id: auth-recoveryshows exactly three rows with statuses 401, 200, 200. - Authorization header swap is visible. Step 2 shows
Bearer expired-demo-token. Step 4 showsBearer demo-access-token. If both show the same value, the Flutter auth state machine never updated after the refresh. - Refresh POST body shape is exact. The field name is
refreshToken(camelCase), notrefresh_token(snake_case). If your Flutter serializer sends the wrong field name, the contract mismatch is in the client, not the server. - Modify Header rule proves the suspect. When the rule fires and the call succeeds, the bug is in Flutter token storage or interceptor wiring — not the backend or network.
- Breakpoint editor preserves correlation headers. When you edit only
Authorization, theX-Rockxy-Scenario-Id,X-Rockxy-Step-Id, andX-Rockxy-Lab-Run-Idheaders stay intact in the executed request. Use this to verify the trace path didn't break.
If it fails. If profile-retry still sends the old token, Flutter auth state is not being persisted across the refresh — check whether your Dio interceptor stores the token in memory (lost on rebuild), SharedPreferences (async write race), or flutter_secure_storage (correct but slower). If the Modify Header rule fires but the call still returns 401, the demo server is reading a different header or the rule pattern didn't match — verify the host:port format. If the Breakpoint dialog never opens, confirm the rule is enabled, Rockxy capture is running, and Flutter is actually routing through the proxy (not a direct localhost bypass).
Case 6: inspect, edit, and replay a Flutter checkout POST body
Problem. Flutter checkout fails with HTTP 409 and a vague "amount mismatch" message in the UI. You don't know whether Flutter calculated the total wrong, serialized the body wrong, dropped currency precision, sent 59.5 instead of 59.50, or whether the backend's expected value is even reachable from the app. Replay-with-edit is the fastest experiment: change one field at a time, send the request directly from Rockxy, and watch the response — no rebuild, no hot-reload, no new app deploy.
Run in the sample. Select Checkout body and response contract and click Run Scenario. The scenario runs three steps: cart (GET /rockxy-demo/cart → 200, subtotal 64.50), checkout-mismatch (POST /rockxy-demo/checkout with body {"cartId": "demo-cart-2026", "paymentMethod": "debug-card", "expectedTotal": 59.50}, expected 409 because the backend expects 64.5), and shape-mismatch (covered in Case 7).
Use in Rockxy.
- Filter by step. Search
X-Rockxy-Step-Id: checkout-mismatchin the main capture window to isolate the failing POST row.
X-Rockxy-Step-Id: checkout-mismatch isolates the failing checkout POST so you can read the request body, the 409 response, and the backend's expected vs. received total before opening Compose.- Inspect the request body of the failing row. Open the row in the inspector and switch to the Request Body tab. Confirm
Content-Type: application/json, then read the JSON:cartId,paymentMethod,expectedTotal. The bad field isexpectedTotal: 59.50— off by 5.00 against the cart subtotal returned by the earliercartstep. - Inspect the response body of the failing row. The demo server returns HTTP 409 with
{"ok": false, "scenario": "checkout", "error": "Total mismatch", "expectedTotal": 64.5, "receivedTotal": 59.50, "cartId": "demo-cart-2026"}. The backend tells you the expected value — use it. -
Right-click the failing checkout row → Edit and Repeat (or Compose). In the capture list, right-click the same row you've been inspecting — the
POST http://127.0.0.1:43210/rockxy-demo/checkoutrow with status409(the one matchingX-Rockxy-Step-Id: checkout-mismatchyou filtered in step 1). From the context menu pick Edit and Repeat or Compose. Rockxy opens the Compose window pre-filled with the original transaction's method, URL, headers, and body. Walk the UI top-to-bottom:- Top bar: a Method dropdown (GET / POST / PUT / DELETE / PATCH / HEAD / OPTIONS — set to
POSTfor this row), the URL text field (http://127.0.0.1:43210/rockxy-demo/checkout), and the Send button on the right (⌘↩). - Request section (below the top bar): a tab bar with Header, Query, Body, Raw. A ⋯ menu on the right of the tab bar exposes extras like Copy as cURL, Import from cURL, and clear.
- The Header tab is a key/value table — each row has a checkbox to enable/disable that header without deleting it, a Name column, a Value column, and a red minus icon at the end to remove the row entirely. A + Add Header link sits below the last row.
- Response panel (right side): shows "No Response — Send a request to see the response here." until you click Send.
- Bottom toolbar: a Template button (save / load a request template), a History button (revisit previous Compose runs), and a ⋯ menu for extra session actions.
- Top bar: a Method dropdown (GET / POST / PUT / DELETE / PATCH / HEAD / OPTIONS — set to
- Edit only the bad value. Click the Body tab. Find
"expectedTotal": 59.50in the JSON and change it to64.5. Leave the Method, URL, every header, and every other body field untouched — single-variable experiment so the result is interpretable. - Send and read the response. Click Send at the top-right (or press
⌘↩). The Response panel fills in: status 200, body{"ok": true, "orderId": "demo-order-2026", "status": "accepted"}. Switch between the response Header / Body / Raw views to confirm the server returnedContent-Type: application/jsonand the orderId you expected.
expectedTotal to 64.5 and clicking Send, the Response panel returns 200 with {"ok": true, "orderId": "demo-order-2026", "status": "accepted"} — proving the original 409 was a Flutter-side calculation bug, not a backend contract problem.- Use History to iterate without losing the audit trail. Every Send is appended to the History menu at the bottom of the Compose window. Click History to revisit earlier attempts — different
expectedTotalvalues, different headers, different bodies — and pick one to re-load into the editor. Use this instead of a scratchpad when you're trying many values, currencies, or precisions.
- Toggle correlation headers off (don't delete them). Switch back to the Header tab. Uncheck the checkboxes next to
X-Rockxy-Scenario-Id,X-Rockxy-Step-Id,X-Rockxy-Lab-Run-Id, andX-Rockxy-Client— the rows stay visible but the headers won't be sent. Click Send again. If the response is still 200, the endpoint accepts the request based on body content alone — those headers are observability, not contract. If it now fails, you've found a hidden coupling worth documenting.
X-Rockxy-Scenario-Id, X-Rockxy-Step-Id, X-Rockxy-Lab-Run-Id, and X-Rockxy-Client keeps the rows visible but stops them being sent. A successful Send afterwards proves these are tracing headers, not part of the backend contract.- Compose a fresh request from scratch. Open the Compose window without a starting row (Menu → Compose, or close and reopen). Manually pick
POSTin the Method dropdown, pastehttp://127.0.0.1:43210/rockxy-demo/checkoutinto the URL field, switch to the Header tab and add onlyContent-Type: application/jsonvia + Add Header, then in Body paste the minimum payload:{"cartId": "demo-cart-2026", "paymentMethod": "debug-card", "expectedTotal": 64.5}. Click Send. A 200 here proves the backend contract is body-driven; the original failure was 100% Flutter-side.
Content-Type: application/json and the minimum body still returns 200. The backend contract is body-driven — confirming the original 409 was a Flutter-side calculation bug, not anything to do with correlation headers or request envelope.- Save the working request as a Template. Click the Template button at the bottom of the Compose window. Give it a name like
Checkout — known-good POST. Next time you debug a checkout regression you start from this template instead of rebuilding the body. Templates live alongside the History — use Template for canonical recipes, History for ad-hoc experimentation.
Checkout — known-good POST turns today's experiment into a reusable debugging recipe — load it next time instead of rebuilding the body by hand.- Test currency precision edge cases. Use History to recall the working call, then re-send it with
expectedTotalset to64.50,64.5,64.500, and"64.50"(string) one at a time. If any of these silently produce different statuses, your backend contract distinguishes between numeric forms and string forms — your Flutter formatter must match the canonical form exactly. Switch from DartdoubletoDecimal(packagedecimal) on the Flutter side if floating-point drift is the culprit.
Expected evidence.
- Request body is parsed as JSON in the inspector. If it appears as raw text or form-encoded, the Flutter HTTP client is sending the wrong
Content-Typeor wrapping the body incorrectly. Dio defaults to JSON;HttpClientrequires explicitjsonEncode(). - Response body explains the mismatch with numbers. The demo server returns both
expectedTotalandreceivedTotal— a real backend contract should do the same. If yours doesn't, file an API-design improvement. - Edited replay returns 200. Confirms the only wrong field was
expectedTotal. The bug is in Flutter total calculation or serialization, not the backend. - Compose without correlation headers still works. Proves the endpoint accepts the request based on body content, not on tracing headers. If it fails without correlation headers, the backend has a hidden coupling worth documenting.
- Replay does not appear as a new live capture row. Replays are sent directly from Rockxy, bypassing the proxy loop. Treat the replay result panel as the source of truth for that retry.
- Precision variants behave consistently. If
64.5and64.50both succeed but"64.50"fails, your contract distinguishes numbers from strings — your Flutter formatter must produce the right type.
If it fails. If the edited replay still returns 409, double-check the exact value — JSON treats 64.5 and 64.50 as equal but a string comparison on the server would differ. If Compose works but Edit-and-Repeat does not, the original Flutter request may have included a malformed header that broke server-side parsing — diff the two requests. If the request body shows as empty in the inspector, Flutter serialized to an empty stream — check that jsonEncode() is called before the body is set, especially in a Dio RequestTransformer. If currency precision is the culprit, switch from double to Decimal (package: decimal) on the Flutter side to avoid floating-point drift.
Current behavior to know. Replay and Compose requests bypass Rockxy's own proxy loop and are sent directly from the app. They do not become new ordinary capture rows; their results appear in the replay panel. This is intentional — it prevents infinite recursion if a Replay matches the same Map Remote rule that triggered it.
Case 7: mock a malformed backend response with Map Local fixtures
Problem. The Flutter product endpoint returns a payload the UI cannot parse — price came back as a string instead of a number, currency is null where the model expects a code, or a required field is missing entirely. The backend team will fix it next sprint, but you need to ship the Flutter UI now and prove it renders correctly against the documented contract. Map Local serves a local JSON file in place of the live response so you can validate the happy path without waiting for the API fix.
Run in the sample. In Checkout body and response contract, the shape-mismatch step calls GET /rockxy-demo/schema-mismatch. The demo server intentionally returns {"product": {"id": "sku-debug-001", "price": "64.50", "currency": null}, "contractNote": "Intentionally malformed for schema testing"} — price as a string, currency as null. This is your baseline failure.
Use in Rockxy.
- Capture the malformed response first. Run
shape-mismatchonce with no rules. Open the row and read the response body. The malformed shape is the baseline you'll compare against.
shape-mismatch response shows "price": "64.50" as a string and "currency": null — the exact contract violation the Flutter parser cannot handle. Capture this row first so you have a clean before/after when Map Local takes over in the next steps.- Prepare a fixture file on disk. Create a JSON file at a stable path, for example
~/Documents/rockxy-fixtures/products.json:
Use a path you can re-find from the file picker. Avoid Desktop or temporary download folders that get cleaned up.{ "product": { "id": "sku-debug-001", "price": 64.50, "currency": "USD", "available": true } } - Open Map Local and turn on Enable Map Local Tool. Click + (or press
⌘N) to open the Map Local Editor and fill the Matching Rule:- Name:
Flutter schema fixture - URL:
127.0.0.1:43210/rockxy-demo/schema-mismatch - Method:
ANY - Match Type:
Use Wildcard(switch toUse Regexonly when you need character-class matching) - Include all subpaths: ☐ unchecked — this is an exact path
- Name:
- Choose the target mode. In the editor, select Local File in the Target Mode segmented picker (the alternative is Local Directory, which is useful when you want one rule to serve many fixture files based on the request path). Toggle Enable Local File on and point the File Path field at your fixture.
- Edit the HTTP Message inline (optional). The editor includes an HTTP Message text area where you can paste a full HTTP response (status line, headers, blank line, body) or just a body. Add the response headers you want — at minimum
HTTP/1.1 200 OKandContent-Type: application/json. Without an explicit Content-Type some HTTP clients fall back totext/plainand the Flutter JSON decoder will refuse to parse. - Add an optional delay. Use the Delay Response menu to simulate slow backends — choose one of the presets (No Delay, 1s, 2s, 3s, 5s, 10s, 30s, 60s, Random 1–15s) or pick Custom and use the stepper to dial in 0–300 seconds. This combines Map Local with Network Conditions in one place when you only need delay on a single endpoint.
- Enable the rule and re-run. Click Run Scenario again. The capture row shows the same request URL but the response body should now match your fixture byte-for-byte.
shape-mismatch request URL now resolves to the fixture body — "price": 64.50 as a number and "currency": "USD". The Flutter parser sees the documented contract instead of the malformed live response.- Edit the fixture without leaving Rockxy. Right-click the rule → Show in Finder to confirm the file is the one you expect, or use Open with to launch the fixture in VS Code, Cursor, TextEdit, or Xcode. With Auto-Save enabled in the editor's Advanced settings, edits propagate to the next request immediately.
- Test schema variations. Create a second fixture with a different valid shape — for example one with
"available": falseor with the optionaldiscountPercentfield added. Duplicate the rule (⌘D), swap fixture paths, toggle one rule on at a time. This is how you prove the Flutter parser handles all documented variants without redeploying the backend. - Test failure shapes intentionally. Create a fixture that omits a required field. Re-run and confirm the Flutter UI surfaces a clear error — not a crash, not a silently blank screen. This validates your model's null-safety and error handling.
Expected evidence.
- Before rule: malformed payload visible. Response body shows
"price": "64.50"(string) and"currency": null. - After rule: fixture matches disk exactly. The response body in Rockxy matches the file content. The request URL and method stay the same; only the response source changes.
- Status and Content-Type reflect Map Local config. Status 200,
Content-Type: application/jsonboth appear in the response headers tab. - Correlation headers stay intact. The request still carries
X-Rockxy-Scenario-Id,X-Rockxy-Step-Id, andX-Rockxy-Lab-Run-Id— Map Local intercepts the response, not the request. - Flutter UI behaves deterministically. If the fixture makes the screen render correctly, the backend shape is the problem. If the UI still fails with a correct fixture, the parser, model class, or rendering code is the problem — and that's actionable.
If it fails. If the response body doesn't change, the rule pattern likely didn't match — use the Test your Rule link in the dialog to verify the captured URL hits the pattern. If the file is referenced but the response is empty, the path may have a typo, a tilde that wasn't expanded, or a permissions issue — Rockxy needs read access to the fixture. If the body shows binary garbage in the inspector, Content-Type is missing or wrong. If another rule (Map Remote, Scripting) also matches the URL, rule priority determines which wins — temporarily disable other rules to isolate.
Case 8: simulate optional-service failure with Block List
Problem. Your Flutter app sends analytics, experimentation, and error-reporting calls in the background. They are non-critical. But do you actually know that checkout still works if the analytics POST returns 403? What about a connection drop or DNS failure? Block List lets you simulate both failure modes — HTTP error response and transport failure — without disabling a real service, shipping a debug flag, or pulling your laptop off Wi-Fi.
Run in the sample. Select Slow and failed network calls and click Run Scenario. The scenario covers three steps: slow-checkout (Case 9), server-error (Case 10), and analytics (POST /rockxy-demo/analytics with body {"event": "checkout_failed", "source": "flutter-sample"}, expected HTTP 202 Accepted).
Use in Rockxy.
- Capture the baseline. Filter by
X-Rockxy-Step-Id: analyticsand confirm the POST returns HTTP 202. The analytics endpoint is the test target — it should not break the app even if it fails entirely.
- Open Block List and turn on Enable Block List Tool. Click + (or press
⌘N) and fill the matching rule:- Name:
Block analytics - Matching Rule:
127.0.0.1:43210/rockxy-demo/analytics - HTTP Method:
ANY - Match Type:
Wildcard - Include all subpaths: ☐ unchecked — exact path only
- Name:
- Choose the failure mode — Return 403 Forbidden first. The Action dropdown offers exactly two options: Return 403 Forbidden or Drop Connection. Pick Return 403 Forbidden. This simulates a backend that rejects the request — the TCP connection completes, the response is reachable, but the status is 403. It is how a real service typically fails: auth expiry, rate limit, geo block, IP ban.
- Re-run and watch the Flutter result. The analytics row should now show 403 in Rockxy with the Block List indicator on the row. The other scenarios (checkout, profile) should continue working. If they break, the app has an undocumented hard dependency on analytics.
- Switch the failure mode — Drop Connection. Edit the rule, change the Action dropdown to Drop Connection. This simulates a transport-layer failure — connection refused, network partition, DNS failure — by closing the socket immediately without sending any HTTP response.
- Re-run and observe error-class differences. Dart
HttpClientand Dio surface drop versus 403 differently. A 403 returns a normalHttpClientResponseyour code can branch on. A dropped connection throwsSocketException(Dart) orDioException(type: connectionError)(Dio). Each should be distinguishable in your error handler.
SocketException (Dart) or DioException(type: connectionError) (Dio), letting you confirm your error handler treats transport failures distinctly from HTTP error statuses.- Test multiple-host blocking. Duplicate the rule (
⌘D) and change the Matching Rule to*/v1/telemetry*using wildcards. This is closer to how you would block third-party telemetry SDKs (Sentry, Firebase Analytics, Amplitude) for testing — wildcards across hosts.
Expected evidence.
- Before rule: analytics returns 202. Baseline works.
- Return 403 Forbidden mode: row shows status 403 with the Block List indicator. Other scenarios still pass.
- Drop Connection mode: no response body, transport error in Flutter. The app surfaces
SocketException(Dart) orDioException(type: connectionError)instead of a status code. - Primary flows complete. Checkout, profile, and cart calls succeed even when analytics fails — proving the optional-dependency boundary holds.
- No cascading retries. If you see analytics retry 5+ times before giving up, the Flutter retry policy is too aggressive for non-critical traffic. Add exponential backoff and a max-attempts cap.
If it fails. If checkout breaks when analytics is blocked, your app has an undocumented hard dependency — check whether the analytics call is awaited in a critical path (it should be fire-and-forget). If Block List doesn't fire, the path pattern may be wrong — try *127.0.0.1:43210/rockxy-demo/analytics* with leading and trailing wildcards. If Drop mode behaves identically to 403 in your Flutter error handler, your code treats all failures the same — distinguish transport from HTTP errors for better telemetry and user messaging.
Case 9: validate Flutter timeout and retry behavior with Network Conditions throttling
Problem. Your Flutter checkout screen looks instant on Wi-Fi. It looks broken on a flaky 3G connection — a spinner that never resolves, a button that double-fires because the user thought the app was frozen, a 30-second timeout that surfaces as a generic "Something went wrong." You cannot reproduce this on a dev machine without throttling. Network Conditions adds artificial latency to specific hosts so you can see the slow-network behavior in seconds without leaving your desk or pulling cables.
Run in the sample. In Slow and failed network calls, the slow-checkout step calls /rockxy-demo/delay/2, which sleeps ~2 seconds server-side before responding. This is the baseline. The point of this case is to layer Rockxy's network throttling on top of the server delay until you cross your Flutter connectTimeout, receiveTimeout, or UI-patience threshold.
Use in Rockxy.
- Establish the baseline. Run
slow-checkoutonce with no rules. Note the duration shown in the Rockxy row (~2s plus minor proxy overhead).
slow-checkout row clocks in around 2 seconds — the demo server's intentional delay. Lock this number in as the unthrottled reference before layering 3G, 4G LTE, or Custom Network Conditions on top in the next steps.- Open Network Conditions and enable the tool. Click + (or press
⌘N) and fill the rule:- Name:
Flutter slow checkout - Host:
127.0.0.1(or toggle Apply System-wide to throttle all traffic regardless of host) - Network Profile: a preset menu — 3G, 4G LTE, WiFi, or Custom
- Name:
- Start with the 3G preset. Each preset configures Download Bandwidth, Upload Bandwidth, Packet Loss, and Latency together — for 3G that's roughly 3.0–6.0 Mbps down, 1.0–1.5 Mbps up, 0–2% packet loss, plus realistic mobile-network latency. Hover the preset to see the exact numbers.
- Re-run and confirm the math. Open the Rockxy row and verify timing increased noticeably above the 2s baseline. Bandwidth caps amplify the effect on responses with large payloads; small JSON responses feel mostly the latency hit. The Flutter sample's measured duration should agree with Rockxy's row timing within scheduling overhead.
slow-checkout request now takes noticeably longer than the 2-second baseline. Rockxy's row timing and the Flutter sample's duration agree to within scheduling overhead — confirming the throttle stacks on top of the server delay.- Push into 4G LTE territory. Switch the preset to 4G LTE and re-run. Compare durations — 4G LTE is faster than 3G but still slower than WiFi or unthrottled localhost. This is your "average user" benchmark.
- Dial in a stress profile with Custom. Change the preset to Custom to expose the individual fields — Download Bandwidth, Upload Bandwidth, Packet Loss %, and Latency (ms). Set Latency to
10000ms to deliberately exceed your Flutter timeout. Re-run and watch how the app surfaces the timeout — clear error message, silent retry, or frozen UI? - Validate the UI loading state. With the 3G preset still active, the spinner should be visible for the full wait. If the UI looks unresponsive (no spinner, frozen button, no skeleton screen), the app is missing loading-state feedback.
- Test double-tap protection. Tap the "Checkout" button twice quickly while a request is in flight. The second tap should be a no-op — if it fires a second request, you have a missing disabled-state on the button.
- Layer with Block List for flaky-network simulation. Keep Network Conditions at 4G LTE and add a Block List drop rule (Case 8) on a secondary endpoint. This simulates a request that crawls under throttling and then a follow-up request that drops — a common real-world cellular pattern.
Expected evidence.
- Baseline row shows ~2s duration. The demo server's intentional delay is visible without any rule.
- 3G / 4G LTE preset rows show a clear, repeatable slowdown. Rockxy timing and Flutter sample duration should agree within ~100ms. Different presets produce visibly different durations on the same endpoint.
- UI loading state visible during the slow phase. Spinner, skeleton, or disabled button appears for the entire wait — never a frozen screen.
- Timeout error is graceful and typed. When Custom latency exceeds the Dio
receiveTimeout, the app surfaces aDioException(type: receiveTimeout)with a clear user message — not a generic "Something went wrong" string. - No silent retry storms. A throttled request should retry at most 2–3 times with exponential backoff. Verify retry count by filtering Rockxy by step ID.
- Status column reads "Active" — not "Conflict". Only one Network Conditions rule should be active. A "Conflict" status (orange) means you toggled on two rules and Rockxy cannot apply both — disable one.
- Double-tap is suppressed. Multiple rapid taps produce only one request row in Rockxy.
If it fails. If Rockxy timing doesn't increase, the rule's Host pattern didn't match, the rule isn't active, or you have a Conflict between two rules — check the status column. If timing increases but the Flutter UI doesn't reflect loading, the app is missing user feedback — add a Stack with a translucent overlay or a LinearProgressIndicator. If the timeout fires earlier than you expect, your Dio connectTimeout or receiveTimeout is shorter than you think — log dio.options.receiveTimeout at app start to verify. If you see duplicate request rows from a single tap, the button isn't disabled during the in-flight call — wrap with AbsorbPointer or use a state-bound flag.
Case 10: triage a Flutter backend 500 from the proxy
Problem. The backend returns HTTP 500. Your Flutter app shows "Something went wrong." Engineering's bug queue fills with screenshots that have no actionable detail — no URL, no request ID, no response payload, no timing. The backend on-call cannot trace the failure without correlation data. Rockxy is the missing piece: every captured 500 row is a complete bug report waiting to be copied into Linear or Jira.
Run in the sample. In Slow and failed network calls, the server-error step calls GET /rockxy-demo/status/500. The demo server returns HTTP 500 with a structured error body that includes the full correlation block.
Use in Rockxy.
X-Request-ID, runtime header, client header, and timing. Copy it as cURL straight into Linear or Jira and the backend on-call can reproduce immediately.- Filter by status code. In the capture list, filter by status
500. If multiple unrelated 500s appear, add a second filter forX-Rockxy-Step-Id: server-erroror search for the literal path/rockxy-demo/status/500. - Open the row and inspect response headers. Confirm
Content-Type(the demo server returnsapplication/json), check forX-Request-IDechoed back, and noteDateand any cache headers. A real backend may also returnX-Trace-Idortraceparent— log them. - Read the response body. The demo server returns
{"ok": false, "scenario": "status", "statusCode": 500, "error": "Demo server error", "correlation": {"runId": "...", "scenarioId": "network-failure", "stepId": "server-error", "requestId": "..."}}. This is the contract for "what a useful 500 looks like." - Copy a complete error packet for the bug report. Right-click → Copy as cURL. The copied command should reproduce the same 500 against the backend. A useful Flutter bug report includes: exact URL with query string, HTTP method, status code, full response body,
X-Request-ID,X-Rockxy-Runtime(iOS Simulator / Android Emulator / physical device),X-Rockxy-Client(Dart HttpClient / Dio 5), and the row timestamp. - Compare timing with nearby successful calls. If the 500 returns in <100ms, the server failed fast — likely a validation or feature-flag check. If it takes 5+ seconds, the server did real work before throwing — likely a database, cache, or downstream service failure.
- Search the capture for related rows. A 500 is rarely isolated. Filter by the same
X-Rockxy-Lab-Run-Idto see what the Flutter app did before and after. The preceding rows often show the state that led to the failure (e.g., a successful auth, then a config load, then the 500 on checkout). - Pin the row for the post-mortem. Use Rockxy's row pin/star (or session save) so you don't lose the evidence when you clear the capture for the next reproduction attempt.
- Use Map Local to test the Flutter error UI. Create a Map Local rule that returns a synthetic 500 with a different error body shape (e.g., add
retryAfter: 30). Verify your Flutter UI surfaces the retry hint instead of a generic message. This is how you ship better error UX before the backend can.
Expected evidence.
- The row is HTTP 500, not a transport error. A 500 means the server reached your code and threw. If you see "connection refused" or "certificate error," the failure is transport, not application — different debugging path.
- Response body is structured JSON. A 500 with an empty body or an HTML error page indicates the backend lacks a proper error handler — file a server-side bug.
- Request ID propagates. The same
X-Request-IDshould appear in the request and (ideally) the response, so on-call can trace it across backend logs. - Correlation block is complete.
scenarioId,stepId,runId,client, andruntimeare all present — your bug report can name the exact device and run that produced the failure. - Copy as cURL works. The copied command reproduces the same 500 outside Flutter, confirming the bug is server-side and not Flutter-specific.
If it fails. If the app labels every non-200 response the same way ("Something went wrong"), the user-facing error mapping is broken — distinguish auth (401/403), validation (4xx), and server (5xx) with specific messages. If Rockxy shows an empty body where the server claims JSON, the backend is returning a generic gateway error page (likely from NGINX or a CDN) — file against backend infra. If X-Request-ID is missing, propagate it from the Flutter client by generating a UUID in your Dio interceptor and echoing it through to the backend.
Case 11: scripted response mutation for Flutter A/B and experiment testing
Problem. Your Flutter pricing experiment puts the user in the control bucket via X-Experiment-Bucket: control. You need to validate the treatment UI — the discount badge, the alternate paywall, the conversion analytics — without recompiling Flutter, flipping a feature flag in production, or shipping a debug build. Static Map Local fixtures can't do this because the response shape depends on request headers. Scripting lets you write a small JavaScript function that reads the live request and mutates the live response on the fly.
Run in the sample. Select Scripted response mutation and click Run Scenario. The scenario runs pricing-experiment — GET /rockxy-demo/pricing-experiment with header X-Experiment-Bucket: control, expected 200. The demo server returns {"experiment": {"bucket": "control", "paywall": "standard", "discountPercent": 0}, ...}.
Use in Rockxy.
- Capture the baseline. Run once with no script. Confirm the response shows
"bucket": "control","paywall": "standard","discountPercent": 0. This is the production state your Flutter UI currently renders.
pricing-experiment response shows "bucket": "control", "paywall": "standard", "discountPercent": 0. Lock this in as the unscripted reference before the JavaScript mutation in the next steps flips the bucket to treatment.- Open Scripting and turn on Enable Scripting Tool. Click + (or press
⌘N) to open the Script Editor in a new window. Fill the rule fields:- Name:
flutter-pricing-treatment - URL:
127.0.0.1:43210/rockxy-demo/pricing-experiment - HTTP Method:
ANY - Pattern Mode:
Wildcard - Include all subpaths: ☐ unchecked — exact path only
- Name:
- Choose where the script runs. The editor has three Run Script on toggles:
- Request — fires
onRequest(request)on the outgoing call - Response — fires
onResponse(response)on the incoming reply - Run as Mock API — the script returns a synthetic response without ever hitting the upstream server, useful when there is no backend at all
- Request — fires
- Write the response mutation. Rockxy's scripting runtime passes a single transaction-shaped object to each hook. Always check
X-Rockxy-Scenario-Idbefore mutating, so the script never accidentally fires on unrelated traffic during a longer session.function onResponse(response) { if (response.request.headers["X-Rockxy-Scenario-Id"] !== "scripted-mock") { return response; } var body = JSON.parse(response.body); body.experiment.bucket = "treatment"; body.experiment.paywall = "discount"; body.experiment.discountPercent = 20; response.headers["Content-Type"] = "application/json"; response.body = JSON.stringify(body); console.log("Mutated to treatment for run:", response.request.headers["X-Rockxy-Lab-Run-Id"]); return response; } - Save and verify the script is active. The Script Editor shows a green status dot and a "savedAndActive" message when the script is parsed without errors. If you see a red status, expand the Console panel — Rockxy logs the syntax error there.
- Re-run and confirm the mutation. The Flutter sample result should reflect the treatment bucket. In Rockxy, open the captured row and verify the response body shows
"bucket": "treatment","discountPercent": 20.
pricing-experiment URL now returns "bucket": "treatment", "paywall": "discount", "discountPercent": 20. The Flutter UI renders the treatment state — the experiment was flipped without redeploying the backend or recompiling the app.-
Read the scripting console. Toggle the Console panel open in the Script Editor. The
console.logoutput should appear once per matched response. If you see no log, the script didn't fire (URL pattern mismatch, Response toggle disabled, or scenario guard rejected the row).
Case 11 in Rockxy: the scripting console panel shows console.logoutput from theonResponsehandler — one entry per matched response. An empty console means the script didn't fire: check the URL pattern, confirm the Response toggle is enabled, and verify the scenario guard isn't rejecting the row. -
Add per-runtime branching to the same script (no new rule needed). Real A/B tests often vary by platform — you want one discount on iOS Simulator and a different one on Android Emulator from the same test bucket. Edit the script you wrote in step 4 (don't create a second one) and replace the static
discountPercentassignment with a runtime check.Background: the Flutter sample sets
X-Rockxy-Runtimeon every outgoing request to identify which runtime emitted it —local-apple-runtimefor macOS / iOS Simulator,android-emulatorfor Android Emulator,physical-devicefor a real device (see Case 1). Reading that header insideonResponsegives you the platform-aware branch point.function onResponse(response) { if (response.request.headers["X-Rockxy-Scenario-Id"] !== "scripted-mock") { return response; } var runtime = response.request.headers["X-Rockxy-Runtime"]; var body = JSON.parse(response.body); body.experiment.bucket = "treatment"; body.experiment.paywall = "discount"; body.experiment.discountPercent = runtime === "android-emulator" ? 15 : 20; response.body = JSON.stringify(body); console.log("Treatment for runtime", runtime, "→", body.experiment.discountPercent + "%"); return response; }How to test it:
- Save the updated script. The status indicator should still show green.
- In the Flutter sample, set the Runtime dropdown to iOS Simulator (or Local Apple Runtime) and click Run Scenario. Open the captured row in Rockxy — response body should show
discountPercent: 20and the Console should log"Treatment for runtime local-apple-runtime → 20%". - Stop the iOS run, set the Runtime dropdown to Android Emulator, start the Android Emulator, click Run Scenario again. The captured row should now show
discountPercent: 15and the Console should log"Treatment for runtime android-emulator → 15%". - The Flutter UI on each runtime should render the matching discount badge (20% on iOS, 15% on Android). Same code, same scenario, different treatment — proved with one script.
Case 11 in Rockxy (iOS Simulator run): the Console logs "Treatment for runtime local-apple-runtime → 20%"and the captured response body showsdiscountPercent: 20— the runtime branch resolved correctly for the Apple runtime.
Case 11 in Rockxy (Android Emulator run): same script, different runtime — the Console logs "Treatment for runtime android-emulator → 15%"and the response showsdiscountPercent: 15. One script handles both platforms without a second rule. -
Use a second script to redact secrets before sharing a capture (HAR export). When you file a backend bug you often attach the relevant capture as a HAR file — an HTTP Archive, a JSON format every proxy tool can import. Rockxy exports HAR via File menu → Export → HAR… (or right-click a session → Export → HAR). The problem: a HAR includes every header that flowed through the proxy —
Authorization: Bearer …,Cookie,Set-Cookie,X-Api-Key— so attaching one to a Jira ticket or Slack thread can leak production tokens.
Case 11 in Rockxy: a second script — Redact for HAR export— sits alongside the pricing-experiment script. Wildcard URL pattern, Request toggle on, kept disabled by default. Enable it only when replaying rows you intend to export as a HAR.Important behaviour to understand first: Rockxy's
onRequest(request)hook runs before the request goes upstream. If you strip theAuthorizationheader here, the backend rejects the request — your scenario fails. So this script is not "always on"; it's a deliberate redaction pass you toggle on right before exporting and off afterwards.The safer workflow:
- Create a new script in Rockxy (don't edit the pricing-experiment one). Name it
Redact for HAR export. URL pattern:*with Use Wildcard — it matches everything. - Toggle Request on. Leave Response off — response bodies rarely contain Authorization, but if you want to also redact
Set-Cookieon responses, mirror the same pattern inonResponse. - Keep the rule disabled by default. The script body:
function onRequest(request) { var sensitive = ["Authorization", "Cookie", "X-Api-Key", "X-Auth-Token"]; for (var i = 0; i < sensitive.length; i++) { if (request.headers[sensitive[i]]) { request.headers[sensitive[i]] = "[REDACTED]"; } } return request; } - Prepare the capture. Click Clear in the Rockxy toolbar to wipe existing rows — the export will only include what you replay next, so there are no unredacted rows mixed in.
-
Enable the Redact rule. Go to Tools → Scripting, find the
Redact for HAR exportrow, and tick its checkbox. The row turns fully opaque when enabled. You can also select it and press ↵. -
Replay the rows you want to share. Select each row in the capture list and choose right-click → Replay, or open it via Compose and resend. Each replay fires a fresh request through Rockxy — the
onRequesthook runs and rewrites the sensitive headers before the row is recorded. The new capture row shows[REDACTED]in place of any real token. -
Export. Choose File → Export → HAR… and save the
.harfile. To verify before attaching it anywhere, run:jq '.log.entries[].request.headers' < capture.har— everyAuthorization,Cookie, andX-Api-Keyvalue should read[REDACTED]. -
Disable the Redact rule immediately. Go back to Tools → Scripting and uncheck the
Redact for HAR exportcheckbox. Leaving it on will strip auth headers from your next real session and cause backend rejections.
If you forget the toggle dance, there's a fallback: redact after the export with a one-liner.
jq '(.log.entries[].request.headers[] | select(.name == "Authorization") | .value) = "[REDACTED]"' capture.har > clean.har. Slower but foolproof when you're tired.
Post-export redaction with jq: pipecapture.harthrough the one-liner and write toclean.har. EveryAuthorizationvalue is overwritten to[REDACTED]without touching anything else in the file. - Create a new script in Rockxy (don't edit the pricing-experiment one). Name it
- Review advanced toggles when you need them. In the panel's More menu you can enable Allow Scripts to read System Environment Variables (useful for parameterising fixtures by environment) and Allow Running Multiple Scripts for one Request (chain mutations across separate scripts instead of one giant function).
Expected evidence.
- Without script: control bucket visible. Response body has
bucket: "control",discountPercent: 0. - With script: treatment bucket visible. Response body has
bucket: "treatment",paywall: "discount",discountPercent: 20. - Only matching scenario rows are mutated. Run a different scenario (e.g.,
app-startup) and verify its responses are unchanged. - Console log fires once per matched response. Confirms the script ran without errors. If you see the log multiple times for one response, the pipeline is double-executing — file a bug.
- Flutter UI reflects the treatment state. The "20% off" badge, alternate copy, and conversion analytics fire as if the user were actually bucketed into treatment.
If it fails. If the script doesn't fire, check the URL pattern, that the Response toggle is on, and that the script status is green ("savedAndActive"). If the mutation appears in Rockxy's inspector but not in the Flutter UI, the response inspector display may differ from what the client actually receives — confirm by adding a Flutter-side log of the raw response. If the script errors silently, expand the Console panel — Rockxy's scripting runtime surfaces parse errors and uncaught exceptions there. Common pitfalls: response.body is a string and must be parsed with JSON.parse before mutation, and re-serialized with JSON.stringify before assignment. Always end onResponse with return response; — missing the return drops the response entirely.
Coexistence with other rules. Scripting runs after Map Local and Map Remote in the response path. If a Map Local fixture is already serving the response, scripting mutates the fixture, not the live server response. Use Run as Mock API only when there is no Map Local rule covering the same URL — otherwise the two will fight for ownership. Disable conflicting rules to isolate the script during development.
Case 12: repeat the Flutter debugging workbook with Dio 5
Problem. Dart's built-in HttpClient is the low-level API. Most production Flutter apps use Dio for interceptors, retry, auth refresh, request transformers, error mapping, and connection pooling. A debugging workflow that only works with HttpClient skips 80% of real Flutter codebases. Every case in this workbook needs a Dio re-run to validate that the same behavior holds — and to catch the cases where it doesn't, because Dio's adapter layer is where Flutter HTTPS debugging quietly breaks.
Run in the sample. Switch the client selector from Dart HttpClient to Dio 5. Re-run Expired token and retry (Case 5), then Checkout body and response contract (Case 6). These two scenarios cover the most common interceptor-driven workflows: auth refresh and POST body serialization. Then re-run any other case that depends on header injection, body shape, or retry — most of the workbook does.
Use in Rockxy.
- Run each scenario twice — once per client. First with Dart
HttpClient, then with Dio 5. Keep the runtime, proxy port, and SSL Proxying rules identical between runs. - Filter by run ID and split by client. Use
X-Rockxy-Lab-Run-Idto scope each run, then split rows byX-Rockxy-Client: dart-http-clientvsX-Rockxy-Client: dio-5. - Compare same-step rows side by side. For each step (e.g.,
expired-token), the URL, method, body, and demo-server-meaningful headers should match. Differences in incidental headers (User-Agent,Accept-Encoding, HTTP version) are normal. - Inspect Dio's adapter behavior. Dio's
IOHttpClientAdapterwraps Dart'sHttpClient. The Rockxy proxy must be configured on the adapter, not on Dio itself. If Dio rows are missing entirely, the adapter isn't routing through Rockxy. The minimum adapter setup is:final dio = Dio(); (dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () { final client = HttpClient(); client.findProxy = (uri) => 'PROXY 127.0.0.1:<rockxy-port>'; client.badCertificateCallback = (cert, host, port) => false; // never true in release return client; }; - Test interceptor ordering. Add a Modify Header rule from Case 5. Verify Dio's
onRequestinterceptor runs before Rockxy receives the request — anything the interceptor adds should be visible in Rockxy. If the interceptor mutates the body, Rockxy's request body should reflect the post-interceptor shape. - Compare error types under Block List. Re-run Case 8 with Dio. A 403 produces
DioException(type: badResponse). A connection drop producesDioException(type: connectionError). A timeout producesDioException(type: connectionTimeout)orreceiveTimeout. Each should map to a distinct user-facing message. - Compare timeout behavior under Network Conditions. Re-run Case 9 with Dio.
connectTimeoutandreceiveTimeoutfire independently — verify by setting each to different values and triggering one at a time.HttpClienthas a singleconnectionTimeout, which is one of the reasons Dio is preferred for fine-grained network control. - Validate Map Remote and Map Local with Dio. Re-run Case 4 and Case 7. Rules should apply identically — proxy interception happens below both libraries. If a rule works for
HttpClientbut not Dio, the adapter is bypassing the system proxy.
Expected evidence.
- Same scenario IDs visible across both runs. Both
X-Rockxy-Client: dart-http-clientandX-Rockxy-Client: dio-5rows appear under the same scenario filter, allowing a clean client-to-client comparison. - Header diffs are minor and explainable. Both clients send the same correlation headers, body, and auth. Differences in
Accept-Encoding(Dio addsgzipby default),User-Agent, or HTTP version are expected. - Interceptor mutations are visible in Rockxy. If the Dio auth interceptor adds
Authorization, Rockxy shows it. If it doesn't, the interceptor isn't running — verifydio.interceptors.add(...)is called before the request fires. - Dio errors are typed and distinguishable. A 500 produces
type: badResponse, a dropped connection producestype: connectionError, a timeout producestype: connectionTimeoutorreceiveTimeout. Each should be branched in yourErrorInterceptor. - Map Remote rules apply identically across clients. The interception happens at the system proxy layer, not inside Dio. If a rule works for HttpClient but not Dio, the adapter is misconfigured.
If it fails. If Dio rows don't appear, the adapter proxy isn't set — re-check the findProxy assignment above and confirm Dio is using the IOHttpClientAdapter on iOS/macOS or HttpClientAdapter on Android. If headers differ in unexpected ways, an interceptor is mutating them silently — log inside each interceptor's onRequest to find the source. If certificate errors appear only in Dio, the adapter's badCertificateCallback may be set incorrectly — never ship (_, __, ___) => true in release builds; that disables all TLS checks and is a security finding waiting to happen. If retries multiply requests beyond what your RetryInterceptor permits, check whether you also enabled retry at the HTTP layer — duplicate retry policies stack.
Decision table: what the evidence means
| Evidence in Rockxy | Most likely area to inspect |
|---|---|
| No row appears | Flutter proxy setup, runtime host, Rockxy port, capture state, network reachability |
| Row appears but HTTPS body is not readable | Certificate trust, SSL Proxying include rules, bypass domains, app certificate pinning |
| Wrong host, scheme, path, or environment | Flutter config, environment loader, Map Remote rule |
| 401 with stale token | Auth interceptor, token refresh state, request header mutation |
| 409 after POST body mismatch | Flutter calculation, JSON serialization, backend validation contract |
| Map Local fixture fixes the UI | Backend response shape or environment data mismatch |
| Block List breaks a noncritical flow | Flutter fallback handling and optional dependency boundaries |
| Replay succeeds after editing one value | Client-side request construction or retry path |
Common mistakes while following the workbook
- Putting the demo API port into the Rockxy proxy port field.
- Using
10.0.2.2while the app is running on macOS instead of inside Android Emulator. - Installing a certificate but forgetting the SSL Proxying include rule for the HTTPS host.
- Trusting a user CA in Android settings while the app’s debug network-security-config still rejects user CAs.
- Assuming Allow List blocks traffic. It focuses capture; Block List blocks.
- Assuming latency presets also simulate packet loss, bandwidth, DNS failure, or offline mode.
- Shipping debug certificate bypass callbacks in release builds.