← Back to Blog

Advanced Flutter API Debugging on macOS with Rockxy

· 30 min read

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.

Rockxy capturing Flutter API traffic on macOS
The useful moment is not just seeing a row. It is being able to inspect the URL, headers, body, status, timing, and response contract behind that row.

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
RuntimeiOS Simulator / macOS desktop
Rockxy portThe active port from Rockxy, not the demo API port
Demo API base URLhttp://127.0.0.1:43210
ClientDart HttpClient first, then repeat with package:http and Dio
Proxy through RockxyEnabled

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 Flutter127.0.0.1:<Rockxy port>127.0.0.1:43210
iOS Simulator127.0.0.1:<Rockxy port>127.0.0.1:43210
Android Emulator10.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.

  1. Start capture before pressing Run Scenario in the Flutter sample.
  2. Filter the traffic table by /rockxy-demo/bootstrap or by the run ID shown in the sample.
  3. Open the captured row and keep the inspector on the request overview first.
  4. Check the method, full URL, query string, status code, response time, request headers, response headers, and response body.
  5. Switch between the request and response body views so you know Rockxy is capturing both sides of the exchange, not only the URL.
Rockxy showing a captured Flutter bootstrap request with request details open
Case 0 evidence in Rockxy: the known-good bootstrap request is captured before moving to failure cases.

Expected evidence.

  • The table has one GET row for http://127.0.0.1:43210/rockxy-demo/bootstrap when 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 unique X-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.

  1. Filter by X-Rockxy-Step-Id: runtime-diagnostic or search for /rockxy-demo/runtime-diagnostic.
  2. Open the row and inspect the request URL host, port, and query string.
  3. Open request headers and find X-Rockxy-Runtime; compare it with the runtime selected in the sample UI.
  4. 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.
  5. Repeat the same case after changing the runtime selector only when the Flutter app is actually running in that runtime.
Rockxy showing a captured Flutter runtime diagnostic request with runtime headers visible
Case 1 evidence in Rockxy: the runtime diagnostic request links the captured traffic to the selected Flutter runtime.

Expected evidence.

  • On macOS desktop and iOS Simulator, the runtime header should be local-apple-runtime and the local API path should resolve through 127.0.0.1:43210.
  • On Android Emulator, the runtime header should be android-emulator, and the proxy route back to the Mac should use 10.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, not 127.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.

  1. Run all sample scenarios once and copy the run ID shown in the sample result area.
  2. Paste the run ID into Rockxy search or create a header filter for X-Rockxy-Lab-Run-Id.
  3. Add a second filter for X-Rockxy-Scenario-Id when you want one scenario, such as auth-recovery or checkout-contract.
  4. Use status filters when you want to isolate failures such as 401, 409, and 500.
  5. Open Allow List and create a focused rule for the demo API, for example method ANY with 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.
  6. 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.
Rockxy Allow List focused rule for Flutter demo API traffic
Case 2 evidence in Rockxy: a focused Allow List rule keeps the Flutter demo API visible while reducing unrelated capture noise.
Rockxy Allow List narrow rule test for Flutter bootstrap traffic
Case 2 evidence in Rockxy: a deliberately narrow Allow List rule helps verify what Rockxy records versus what the Flutter app still receives.

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.

  1. 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.
Rockxy certificate trust installation flow for HTTPS interception
Case 3 evidence in Rockxy: installing the CA certificate is the first step before HTTPS traffic can be decrypted and inspected.
  1. 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.
  2. 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, add api.github.com. If your probe URL is https://httpbin.org/get, add httpbin.org. Do not put the full path there first.
  3. 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.
  4. Run the public probe. Paste https://api.github.com/zen or https://httpbin.org/get into 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.
  5. 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?
  6. 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. For https://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.

  1. Find the row. Filter by X-Rockxy-Scenario-Id: wrong-environment or search for /rockxy-demo/environment in the request list. You should see a single GET row with status 200.
  2. 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.
  3. 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, and X-Rockxy-Lab-Run-Id.
Rockxy request inspector showing X-App-Environment: production header on a staging-intended request
Case 4 evidence in Rockxy: the request headers expose X-App-Environment: production while the URL carries expected=staging — the environment mismatch is visible before any Map Remote rule is applied.
  1. 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.
Rockxy response body inspector showing query expected staging and remoteMappingCandidate true
Case 4 evidence in Rockxy: the response body echoes "expected": "staging" in the query field and flags "remoteMappingCandidate": true, confirming the server received the environment mismatch signal.
  1. 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:
    Matching Rule
    NameCase 4 – wrong environment
    Rule127.0.0.1:43210/rockxy-demo/environment
    MethodANY
    ModeUse Wildcard
    Subpaths☐ uncheck — this is an exact path, not a tree
    Map To
    Protocolhttps— the menu offers Keep Original, http, and https; pick https here because httpbin only accepts TLS
    Hosthttpbin.org— use this to confirm the rule fires without needing a real staging server
    Port443
    Path/get
    Queryleave blank — passes the original query string through

    💡 Tip: paste https://httpbin.org/get directly 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: production in the request headers. If you also need to fix the header value, add a separate Modify Header rule: set X-App-Environmentstaging.

    Click Add to save, then make sure the rule is toggled on before the next run.
  2. 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, not 127.0.0.1:43210/rockxy-demo/environment.
Rockxy capture list showing the remapped request hitting httpbin.org/get after Map Remote rule fires
Case 4 evidence in Rockxy: the second run row shows the request routed to httpbin.org/get — the Map Remote rule fired and rewrote the destination without any Flutter code change.
  1. 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.
Rockxy inspector showing the before row — original request to 127.0.0.1 demo server with X-App-Environment production header
Before: the raw production-config request goes to 127.0.0.1:43210/rockxy-demo/environment with X-App-Environment: production exposed in the request headers.
Rockxy inspector showing the after row — same request remapped to httpbin.org/get by Map Remote rule
After: the same Flutter request, same headers, same query string — now routed to 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=staging with status 200 and request header X-App-Environment: production clearly 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 correlation object 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.

  1. 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.
Rockxy capture list filtered by X-Rockxy-Scenario-Id auth-recovery showing three rows: 401 expired-token, 200 refresh-token, 200 profile-retry
Case 5 evidence in Rockxy: filtering by 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.
  1. Inspect the expired-token row. Open the 401 row. Confirm the request URL is /rockxy-demo/profile, the method is GET, and the request header Authorization: Bearer expired-demo-token is present. Open the response body — the demo server returns {"ok": false, "error": "Token expired or missing", "expectedHeader": "Authorization"}. Read this contract before fixing anything.
Rockxy inspector showing the 401 expired-token request with Authorization Bearer expired-demo-token header and response body explaining the token expiry contract
Case 5 evidence in Rockxy: the 401 row exposes 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.
  1. 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.
Rockxy inspector showing the 200 POST refresh-token row with refreshToken in the request body and accessToken plus expiresIn in the response body
Case 5 evidence in Rockxy: the refresh POST sends {"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.
  1. 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.
  2. Use Modify Headers to prove the source of the bug. Open Modify Headers and click + (or press ⌘N). Fill the matching rule with URL 127.0.0.1:43210/rockxy-demo/profile, method ANY, 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.
Rockxy Modify Header rule rewriting the Authorization header to Bearer demo-access-token so the expired-token profile call now returns 200
Case 5 evidence in Rockxy: the Modify Header rule rewrites 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.
  1. 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).

    1. 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").
    2. Click + (or ⌘N), then open the More menu and pick New Response Template.
    3. Set Name to Flutter profile 200 OK.
    4. 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"}}
    5. 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.
    6. Close the Template Manager. Templates persist across app restarts via Rockxy's local store.

    Phase B — Create the Breakpoint rule.

    1. Open Breakpoint Rules (Tools menu, or the toolbar icon).
    2. 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.
    3. Click + (or ⌘N) to open the Breakpoint Rule Editor. Fill it exactly like this:
      NameCase 5 – auth profile
      Matching Rule127.0.0.1:43210/rockxy-demo/profile
      MethodANY
      ModeUse Wildcard
      Subpaths☐ uncheck — pause only the exact profile call
      Breakpoint☑ Request  ☑ Response  — one rule pauses both sides
    4. 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.

    Rockxy Breakpoint Rule Editor filled in for the Case 5 auth profile rule with Matching Rule set to the demo profile endpoint and both Request and Response checkboxes ticked
    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.

    1. Switch to the Flutter sample. Select Expired token and retry in the Scenario dropdown and click Run Scenario.
    2. The first step (expired-token) fires GET /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.
    3. 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 exactly 127.0.0.1:43210/rockxy-demo/profile — no trailing space, no http:// prefix, no :43210/ typo.
    (4) Flutter is actually using Rockxy as proxy. Re-check the runtime headers on the captured row: X-Rockxy-Runtime should 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.

    1. In the right editor, confirm the Method dropdown shows GET and the URL field shows http://127.0.0.1:43210/rockxy-demo/profile.
    2. 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.
    3. Find the Authorization row. Click directly on the value cell (the Bearer expired-demo-token text) and type over it with Bearer demo-access-token.
    4. 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.

    Rockxy Breakpoint window paused on the outgoing Flutter profile request with the Authorization header being edited to Bearer demo-access-token in the Headers tab
    Phase D in Rockxy: the Breakpoint window holds the expired-token profile request mid-flight. Editing the Authorization cell directly in the Headers key/value table replaces Bearer expired-demo-token with Bearer demo-access-token before clicking Execute.

    Phase E — Edit the paused response.

    1. 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".
    2. The action bar swaps the Method dropdown for a Status Code dropdown listing 13 preset HTTP codes. Pick 200 OK.
    3. Click the Body tab and paste a synthetic profile JSON:
      {"profile": {"userId": "demo-user-001", "displayName": "Debug Shopper"}}
      Or click the Raw tab → Template menu → pick Flutter profile 200 OK (the template from Phase A) — that replaces status + headers + body in a single click.
    4. Click the Headers tab and confirm Content-Type: application/json is present. If not, add it via + Add Header.
    5. 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.

    Rockxy Breakpoint window paused on the incoming response with Status Code rewritten to 200 OK and a synthetic profile JSON injected into the Body tab
    Phase E in Rockxy: the response side of the same rule paused next. The Status Code dropdown is switched from 401 to 200 OK and 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.

    1. Switch back to the Flutter sample. The profile screen should now render with the synthetic user — proving the UI handles the success contract correctly.
    2. 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 original 401).
      • Response body containing the synthetic profile JSON.
      • Correlation headers (X-Rockxy-Scenario-Id, X-Rockxy-Step-Id, X-Rockxy-Lab-Run-Id) preserved unchanged.
    3. 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.
    Rockxy main capture window after the breakpoint replay showing the expired-token row with the edited Authorization header, overridden 200 status, and synthetic profile body — plus the Flutter sample rendering the profile screen successfully
    Phase F verification: back in Rockxy's main capture, the expired-token row now carries the rewritten Authorization: Bearer demo-access-token header and a 200 response 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-recovery shows exactly three rows with statuses 401, 200, 200.
  • Authorization header swap is visible. Step 2 shows Bearer expired-demo-token. Step 4 shows Bearer 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), not refresh_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, the X-Rockxy-Scenario-Id, X-Rockxy-Step-Id, and X-Rockxy-Lab-Run-Id headers 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.

  1. Filter by step. Search X-Rockxy-Step-Id: checkout-mismatch in the main capture window to isolate the failing POST row.
Rockxy capture list filtered by X-Rockxy-Step-Id checkout-mismatch showing the failing POST /rockxy-demo/checkout row with status 409
Case 6 evidence in Rockxy: filtering by 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.
  1. 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 is expectedTotal: 59.50 — off by 5.00 against the cart subtotal returned by the earlier cart step.
  2. 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.
  3. 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/checkout row with status 409 (the one matching X-Rockxy-Step-Id: checkout-mismatch you 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 POST for 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.
Rockxy Compose window pre-filled from the captured POST /rockxy-demo/checkout row showing Method GET selector, URL field, Header / Query / Body / Raw tabs, all captured headers in a checkbox table, the No Response placeholder on the right, and the Template / History / more menu at the bottom
Case 6 evidence in Rockxy: the Compose window opens pre-filled with the original transaction. The Header tab shows every captured header with its own checkbox, and the Response panel waits on No Response until you click Send.
  1. Edit only the bad value. Click the Body tab. Find "expectedTotal": 59.50 in the JSON and change it to 64.5. Leave the Method, URL, every header, and every other body field untouched — single-variable experiment so the result is interpretable.
  2. 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 returned Content-Type: application/json and the orderId you expected.
Rockxy Compose window after Send: Response panel shows status 200 with the demo server's success body {ok:true, orderId:demo-order-2026, status:accepted} after editing expectedTotal from 59.50 to 64.5
Case 6 evidence in Rockxy: after changing only 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.
  1. 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 expectedTotal values, 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.
Rockxy Compose window History menu open at the bottom toolbar listing previous Send attempts with method, URL, status code, and relative timestamp per row
Case 6 evidence in Rockxy: the History menu records each Send with its method, URL, response status, and timestamp. Picking a row reloads that attempt into the editor so you can compare currency precisions, header variants, and body shapes without a scratchpad.
  1. 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, and X-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.
Rockxy Compose window Header tab with the X-Rockxy correlation header checkboxes unticked while the Authorization and other functional headers remain enabled, then Send returns 200 to prove those headers are optional
Case 6 evidence in Rockxy: unchecking the per-row checkboxes for 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.
  1. Compose a fresh request from scratch. Open the Compose window without a starting row (Menu → Compose, or close and reopen). Manually pick POST in the Method dropdown, paste http://127.0.0.1:43210/rockxy-demo/checkout into the URL field, switch to the Header tab and add only Content-Type: application/json via + 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.
Rockxy Compose window built from scratch with POST method, the demo checkout URL, only Content-Type application json header, the minimum cart payload in the Body tab, and the Response panel returning 200 with orderId demo-order-2026
Case 6 evidence in Rockxy: a freshly composed POST with only 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.
  1. 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.
Rockxy Compose Template menu open at the bottom of the window, saving the known-good checkout POST under the name Checkout known-good POST for reuse in future regressions
Case 6 evidence in Rockxy: saving the working POST as a Template named Checkout — known-good POST turns today's experiment into a reusable debugging recipe — load it next time instead of rebuilding the body by hand.
  1. Test currency precision edge cases. Use History to recall the working call, then re-send it with expectedTotal set to 64.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 Dart double to Decimal (package decimal) 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-Type or wrapping the body incorrectly. Dio defaults to JSON; HttpClient requires explicit jsonEncode().
  • Response body explains the mismatch with numbers. The demo server returns both expectedTotal and receivedTotal — 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.5 and 64.50 both 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.

  1. Capture the malformed response first. Run shape-mismatch once with no rules. Open the row and read the response body. The malformed shape is the baseline you'll compare against.
Rockxy inspector showing the baseline shape-mismatch response with price as the string 64.50 and currency null exposing the malformed product payload from the demo server
Case 7 baseline in Rockxy: the live 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.
  1. Prepare a fixture file on disk. Create a JSON file at a stable path, for example ~/Documents/rockxy-fixtures/products.json:
    {
      "product": {
        "id": "sku-debug-001",
        "price": 64.50,
        "currency": "USD",
        "available": true
      }
    }
    Use a path you can re-find from the file picker. Avoid Desktop or temporary download folders that get cleaned up.
  2. 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 to Use Regex only when you need character-class matching)
    • Include all subpaths: ☐ unchecked — this is an exact path
  3. 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.
Rockxy Map Local Editor with Target Mode set to Local File, Enable Local File toggled on, and File Path pointing at the products.json fixture on disk
Case 7 in Rockxy: with Target Mode: Local File selected, the File Path field points at the on-disk fixture that will replace the live response. Local Directory mode is the alternative when you want one rule to serve many fixtures based on the request path.
  1. 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 OK and Content-Type: application/json. Without an explicit Content-Type some HTTP clients fall back to text/plain and the Flutter JSON decoder will refuse to parse.
  2. 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.
  3. 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.
Rockxy capture list with the Map Local rule active showing the re-run shape-mismatch row delivering the fixture payload with price as a number and currency as USD instead of the malformed baseline
Case 7 after Map Local fires in Rockxy: the same 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.
  1. 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.
Rockxy Map Local rule context menu open with Show in Finder and Open with VS Code Cursor TextEdit Xcode options so the fixture can be edited in place
Case 7 in Rockxy: right-clicking the Map Local rule exposes Show in Finder and Open with (VS Code, Cursor, TextEdit, Xcode). Combined with the Map Local editor's Auto-Save setting, every saved edit to the fixture file is picked up on the next request — no need to toggle the rule off and on.
  1. Test schema variations. Create a second fixture with a different valid shape — for example one with "available": false or with the optional discountPercent field 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.
  2. 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/json both appear in the response headers tab.
  • Correlation headers stay intact. The request still carries X-Rockxy-Scenario-Id, X-Rockxy-Step-Id, and X-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.

  1. Capture the baseline. Filter by X-Rockxy-Step-Id: analytics and confirm the POST returns HTTP 202. The analytics endpoint is the test target — it should not break the app even if it fails entirely.
Rockxy capture list filtered by X-Rockxy-Step-Id analytics showing the POST /rockxy-demo/analytics row returning HTTP 202 as the baseline before Block List is configured
Case 8 baseline in Rockxy: the analytics POST returns HTTP 202 while no Block List rule is active. Capture this row first so you can compare against the Return 403 Forbidden and Drop Connection failure modes the next steps inject.
  1. 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
  2. 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.
Rockxy Block List Editor Action dropdown open showing the two failure mode options Return 403 Forbidden and Drop Connection
Case 8 in Rockxy: the Block List Action dropdown exposes exactly two failure modes — Return 403 Forbidden simulates a backend rejection at the HTTP layer; Drop Connection simulates a transport-layer failure.
Rockxy Block List rule saved with Action set to Return 403 Forbidden matching the demo analytics endpoint
Case 8 in Rockxy: with the rule saved and Return 403 Forbidden selected, the analytics endpoint is now intercepted before reaching the demo server. The next Send replays the call so you can watch how Flutter handles a 403 from a non-critical service.
  1. 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.
  2. 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.
  3. Re-run and observe error-class differences. Dart HttpClient and Dio surface drop versus 403 differently. A 403 returns a normal HttpClientResponse your code can branch on. A dropped connection throws SocketException (Dart) or DioException(type: connectionError) (Dio). Each should be distinguishable in your error handler.
Rockxy capture list after re-running with Drop Connection enabled showing the analytics row terminating without a response body while the Flutter sample surfaces a SocketException or DioException connectionError
Case 8 evidence in Rockxy: with Drop Connection active, the analytics row has no response body — Rockxy closed the socket before any HTTP exchange completed. Flutter surfaces this as SocketException (Dart) or DioException(type: connectionError) (Dio), letting you confirm your error handler treats transport failures distinctly from HTTP error statuses.
  1. 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) or DioException(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.

  1. Establish the baseline. Run slow-checkout once with no rules. Note the duration shown in the Rockxy row (~2s plus minor proxy overhead).
Rockxy capture list showing the baseline slow-checkout row with a duration around 2 seconds reflecting the demo server's intentional delay before any Network Conditions throttling is applied
Case 9 baseline in Rockxy: the 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.
  1. 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
    Only one Network Conditions rule can be active at a time — the status column shows Active, Inactive, Paused, or Conflict (orange) if multiple rules are toggled on.
  2. 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.
  3. 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.
Rockxy capture list with the 3G Network Conditions preset active showing the slow-checkout row duration jump well above the 2 second baseline, proving the throttle is layering on top of the demo server's intentional delay
Case 9 evidence in Rockxy: with the 3G preset active, the same 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.
  1. 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.
  2. 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?
  3. 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.
  4. 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.
  5. 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 a DioException(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.

Rockxy capture list filtered to a 500 row showing the inspector open with response status 500, the demo server's structured JSON error body, request and response headers including X-Request-ID, and the full correlation block — everything a developer needs to file an actionable backend bug report
Case 10 in Rockxy: a single captured 500 row contains everything an actionable bug report needs — URL, method, status, response body, 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.
  1. Filter by status code. In the capture list, filter by status 500. If multiple unrelated 500s appear, add a second filter for X-Rockxy-Step-Id: server-error or search for the literal path /rockxy-demo/status/500.
  2. Open the row and inspect response headers. Confirm Content-Type (the demo server returns application/json), check for X-Request-ID echoed back, and note Date and any cache headers. A real backend may also return X-Trace-Id or traceparent — log them.
  3. 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."
  4. 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.
  5. 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.
  6. Search the capture for related rows. A 500 is rarely isolated. Filter by the same X-Rockxy-Lab-Run-Id to 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).
  7. 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.
  8. 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-ID should 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, and runtime are 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.

  1. 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.
Rockxy response inspector showing the baseline pricing-experiment response with bucket control, paywall standard, and discountPercent zero before any Scripting rule is applied
Case 11 baseline in Rockxy: the live 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.
  1. 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
  2. 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
    For this case enable Response only. Leave Request off so you don't accidentally rewrite the experiment header before it reaches the server.
  3. Write the response mutation. Rockxy's scripting runtime passes a single transaction-shaped object to each hook. Always check X-Rockxy-Scenario-Id before 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;
    }
  4. 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.
Rockxy Script Editor showing the treatment-mutation JavaScript saved with a green status indicator and the savedAndActive label, with the Console panel collapsed and ready to expand on error
Case 11 in Rockxy: the Script Editor confirms the response mutation script parsed cleanly — green status dot, savedAndActive label. If you see red instead, expand the Console panel below the editor to read the parse error.
  1. 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.
Rockxy response inspector after the script fires showing pricing-experiment response body mutated to bucket treatment, paywall discount, and discountPercent 20
Case 11 evidence in Rockxy: the same 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.
  1. Read the scripting console. Toggle the Console panel open in the Script Editor. The console.log output 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.log output from the onResponse handler, confirming the script fired once per matched response.
    Case 11 in Rockxy: the scripting console panel shows console.log output from the onResponse handler — 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.
  2. 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 discountPercent assignment with a runtime check.

    Background: the Flutter sample sets X-Rockxy-Runtime on every outgoing request to identify which runtime emitted it — local-apple-runtime for macOS / iOS Simulator, android-emulator for Android Emulator, physical-device for a real device (see Case 1). Reading that header inside onResponse gives 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:

    1. Save the updated script. The status indicator should still show green.
    2. 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: 20 and the Console should log "Treatment for runtime local-apple-runtime → 20%".
    3. 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: 15 and the Console should log "Treatment for runtime android-emulator → 15%".
    4. 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: the scripting console shows 'Treatment for runtime local-apple-runtime → 20%' after the iOS Simulator run, confirming the per-runtime branch fired correctly.
    Case 11 in Rockxy (iOS Simulator run): the Console logs "Treatment for runtime local-apple-runtime → 20%" and the captured response body shows discountPercent: 20 — the runtime branch resolved correctly for the Apple runtime.
    Case 11 in Rockxy: the scripting console shows 'Treatment for runtime android-emulator → 15%' after the Android Emulator run, confirming the per-runtime branch fires a different discount for Android.
    Case 11 in Rockxy (Android Emulator run): same script, different runtime — the Console logs "Treatment for runtime android-emulator → 15%" and the response shows discountPercent: 15. One script handles both platforms without a second rule.
  3. 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 named 'Redact for HAR export' is created in the Script Editor with a wildcard URL pattern, ready to strip Authorization, Cookie, and API key headers before a HAR export.
    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 the Authorization header 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:

    1. 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.
    2. Toggle Request on. Leave Response off — response bodies rarely contain Authorization, but if you want to also redact Set-Cookie on responses, mirror the same pattern in onResponse.
    3. 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;
      }
    4. 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.
    5. Enable the Redact rule. Go to Tools → Scripting, find the Redact for HAR export row, and tick its checkbox. The row turns fully opaque when enabled. You can also select it and press .
    6. 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 onRequest hook runs and rewrites the sensitive headers before the row is recorded. The new capture row shows [REDACTED] in place of any real token.
    7. Export. Choose File → Export → HAR… and save the .har file. To verify before attaching it anywhere, run: jq '.log.entries[].request.headers' < capture.har — every Authorization, Cookie, and X-Api-Key value should read [REDACTED].
    8. Disable the Redact rule immediately. Go back to Tools → Scripting and uncheck the Redact for HAR export checkbox. 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.

    Terminal showing the jq one-liner rewriting Authorization header values to [REDACTED] in a HAR file, producing a clean.har safe to share.
    Post-export redaction with jq: pipe capture.har through the one-liner and write to clean.har. Every Authorization value is overwritten to [REDACTED] without touching anything else in the file.
  4. 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.

  1. 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.
  2. Filter by run ID and split by client. Use X-Rockxy-Lab-Run-Id to scope each run, then split rows by X-Rockxy-Client: dart-http-client vs X-Rockxy-Client: dio-5.
  3. 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.
  4. Inspect Dio's adapter behavior. Dio's IOHttpClientAdapter wraps Dart's HttpClient. 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;
    };
  5. Test interceptor ordering. Add a Modify Header rule from Case 5. Verify Dio's onRequest interceptor 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.
  6. Compare error types under Block List. Re-run Case 8 with Dio. A 403 produces DioException(type: badResponse). A connection drop produces DioException(type: connectionError). A timeout produces DioException(type: connectionTimeout) or receiveTimeout. Each should map to a distinct user-facing message.
  7. Compare timeout behavior under Network Conditions. Re-run Case 9 with Dio. connectTimeout and receiveTimeout fire independently — verify by setting each to different values and triggering one at a time. HttpClient has a single connectionTimeout, which is one of the reasons Dio is preferred for fine-grained network control.
  8. 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 HttpClient but not Dio, the adapter is bypassing the system proxy.

Expected evidence.

  • Same scenario IDs visible across both runs. Both X-Rockxy-Client: dart-http-client and X-Rockxy-Client: dio-5 rows 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 adds gzip by 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 — verify dio.interceptors.add(...) is called before the request fires.
  • Dio errors are typed and distinguishable. A 500 produces type: badResponse, a dropped connection produces type: connectionError, a timeout produces type: connectionTimeout or receiveTimeout. Each should be branched in your ErrorInterceptor.
  • 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 appearsFlutter proxy setup, runtime host, Rockxy port, capture state, network reachability
Row appears but HTTPS body is not readableCertificate trust, SSL Proxying include rules, bypass domains, app certificate pinning
Wrong host, scheme, path, or environmentFlutter config, environment loader, Map Remote rule
401 with stale tokenAuth interceptor, token refresh state, request header mutation
409 after POST body mismatchFlutter calculation, JSON serialization, backend validation contract
Map Local fixture fixes the UIBackend response shape or environment data mismatch
Block List breaks a noncritical flowFlutter fallback handling and optional dependency boundaries
Replay succeeds after editing one valueClient-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.2 while 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.
Rockxy — native macOS proxy debugger

Try Rockxy — Free and Open Source

A native macOS proxy debugger for HTTP, HTTPS, WebSocket, and API traffic. No cloud account. No subscription. Download the app or audit the source directly on GitHub.

Open source · AGPL-3.0 · Native macOS · No account required · Source available to audit