Skip to content

Inspect traffic

Streaming responses

Since 0.4.0 Requires Cloak Proxy 0.4.0 or later.

Watch Server-Sent Events, JSON-lines streams, multipart GraphQL responses and long chunked bodies event by event in Cloak Proxy, live, with when each event arrived. Streams pass through unbuffered.

Some responses arrive a piece at a time: an AI assistant’s reply token by token, live scores, order updates, build logs, a GraphQL subscription. Cloak Proxy passes these streams on to the app as they arrive, and the inspector shows them event by event in a Messages tab, updating while the stream is open. Each event shows when it arrived and how long it took after the one before, so a slow first token or a stalled stream is easy to spot.

An assistant's reply streamed as SSE. Event 9 arrived after a 1.42 s stall.

  1. Live, Ended, Closed by app or Broke
  2. Show one event type
  3. Event count, time to the first event, longest gap
  4. When each event arrived; click a tick to select it
  5. Wait since the previous event, orange from 1 s
  6. The selected event's fields and data
The Messages tab of a Server-Sent Events response: an Ended badge, a filter field, an All types menu and the summary 16 events, first after 381 ms, longest gap 1.418 s. Below it, a strip with a tick for each event, then a table of events with number, time, gap, type and data: message_start at 381 ms, ping, content_block_start, then content_block_delta events. Event 9 is selected, arriving 1.42 s after the previous one, and its JSON data opens in a tree below.
Response Shown as Example
text/event-stream Messages: one row per Server-Sent Event Most AI APIs, live feeds, notifications
application/x-ndjson, application/jsonl, application/json-seq and similar Messages: one row per JSON line Local model servers, log and export APIs
multipart/mixed Messages: one row per part GraphQL @defer and @stream (incremental delivery)
Any other response with no Content-Length that is still arriving after half a second Chunks: one row per network read Long polling, progress output, chunked feeds

The traffic list marks these requests with a teal badge before the URL: SSE, JSON lines, Multipart or Streamed, and Live · in front while the stream is still open.

Cloak Proxy forwards a stream’s bytes to the app as soon as the server sends them, instead of waiting for the whole response. Event sources, chat UIs and progress bars keep working through the proxy, and closing the stream in the app closes it at the server too.

  1. Make sure the connection is decrypted. For HTTPS, turn on SSL Proxying for the host or the app.
  2. Select the request in the traffic list. A stream that’s still open shows Live · in its badge.
  3. The response pane opens on the Messages tab, with the event count. Events are added as they arrive, and the list scrolls to the newest one.
  4. Click an event to see its data below the table. Selecting an event stops the list following new ones. Click the follow button at the right of the toolbar to follow again.

Streams remember their own tab: if you switch a stream to Body or Raw, the next stream you select opens on that tab too, while ordinary responses keep theirs.

A stream that's still open. The two long waits are orange.

The Messages tab of an order-updates stream that is still open: a green Live badge, five events with ids 1 to 4, a retry event that sets the reconnect delay to 5000 ms, status and location events with gaps of 6.28 s and 8.50 s in orange, and Waiting for more… under the last row.
Column Shows
# The event’s position in the stream
Time When the event finished arriving, counted from the start of the request
Gap How long after the previous event it arrived. Orange from 1 second, red from 10 seconds.
Type The SSE event: field (message when there is none), or a multipart part’s Content-Type. Blocks that only set the reconnect delay show as retry. Not shown for JSON lines and chunks.
ID The SSE id: field, when any event has one
Size The size of the event’s data
Data The data on one line. Line breaks inside it show as ⏎.

Two small badges can follow the data:

Badge Meaning
Cut off The stream ended in the middle of this event: it had no blank line (SSE), no newline and wasn’t valid JSON (JSON lines), or no closing boundary (multipart).
No data An SSE block with fields but no data: line. Browsers’ EventSource doesn’t deliver these, so the app never sees them.
Control What it does
State badge Live while the response is streaming. Ended when the server finished it, Closed by app when the app hung up first, Broke when the connection to the server dropped (hover to see why), Buffered when a tool held it.
Filter events Shows events whose data, type or id contains your text, ignoring case
All types Shows one event type. Each type lists how many events it has. Only there when the stream has more than one type.
Summary How many events, how long after the request the first one arrived, and the longest gap. times estimated appears for compressed streams (see below).
Follow button While the stream is live: keeps the newest event in view

Under the toolbar, a strip runs from the start of the request (0) to the last event. The gray bar at its start is the wait for the response headers, and each tick is an event, so bursts and stalls show at a glance. Ticks of events hidden by a filter are fainter. Click anywhere on the strip to select the nearest event.

The lower half shows the selected event’s number, type, id, retry value, when it arrived, the gap, and its size. Its data opens in the body viewer: JSON as a tree you can filter and query with JSONPath or jq, other text as Raw, binary chunks in Hex. Click the copy button to copy the data.

Right-click an event for Copy Data, Copy Data as Pretty JSON, Copy Event as Sent (with its event:, id: and data: lines), or Show Only type Events.

JSON lines from a local model server, one event per line.

A JSON-lines stream from a local model server: nine events 31 ms apart, each a JSON object with model, created_at and message, and event 4 open in a tree.

When a stream carries GraphQL, the response pane adds a GraphQL tab that groups the traffic by operation. It recognizes:

Transport How it’s recognized
graphql-transport-ws (the graphql-ws library) A WebSocket with that subprotocol, or whose messages use subscribe and next
graphql-ws (the older subscriptions-transport-ws) A WebSocket with that subprotocol, or whose messages use start and data
GraphQL over SSE A GraphQL request answered with text/event-stream (next and complete events)
Incremental delivery A GraphQL request answered with multipart/mixed (@defer, @stream)

Two subscriptions on one WebSocket, and the results of the selected one.

  1. Whether the server acknowledged the connection
  2. Active, Completed, Failed or Stopped
  3. Results, Query, Variables and Errors
The GraphQL tab of a WebSocket: graphql-transport-ws, Acknowledged, 2 operations, 1 active, 6 results. The subscription OnMessage (id 1, Active, 4 results, first result at 4.000 s) is selected above subscription OrderStatus (Completed, 2 results). Below, its Results tab lists four results 7.30 s apart, and the last one opens as a JSON tree with messageAdded text It left the warehouse this morning.
Column Shows
Operation The kind (subscription, query, mutation) and name, or Anonymous
ID The id the protocol gave the operation
Status Active while results may follow, Completed when the server finished it, Failed when it sent an error, Stopped when the app stopped it or the socket closed
Results How many results arrived
First result When the first result arrived, from the start of the connection

Select an operation to see its Results (each with its time and gap, and its payload as JSON), its Query, its Variables and any Errors. Results that carry errors have a red errors badge. The tab’s header names the transport and, for WebSockets, whether the server answered connection_init with connection_ack.

The Messages tab still lists every frame or event, including protocol messages such as ping, pong and connection_init.

Detail How it’s measured
Event time When the network read containing the event’s last byte reached Cloak Proxy, counted from when the request started
Response headers When the response head arrived (the gray bar on the arrival strip, and headersMs in MCP)
Gap The difference between an event’s time and the previous event’s
Compressed streams Cloak Proxy decompresses gzip, deflate and Brotli streams to read the events, then places each one in time by its position in the stream. The summary says times estimated.
Very long streams Up to 20,000 network reads keep their own times. Beyond that, neighbouring reads are merged, so some events take the time of the read after them, never an earlier one.
Body size Events past the Max captured body size aren’t recorded, but the stream keeps flowing to the app.

Arrival times are saved in Cloak Proxy session files. HAR files have no place for them, so a stream opened from a HAR shows its events without times.

The get_stream_events MCP tool returns a streamed response event by event, with each event’s type, id, data (parsed when it’s JSON), time and gap, plus the time to the first event, the average and longest gap, and how the stream ended. For GraphQL streams it adds each operation with its status, result count and last result. Pass after with the lastIndex of the previous call to get only newer events while a stream is still open. See MCP tools reference.

Use Cloak Proxy to look at request #42’s stream. How long until the first token, and where did it stall?

cloakctl events prints a stream one event per line: index, time, gap, type, id and data.

Terminal
cloakctl events 42 # from the running app
cloakctl events 42 --follow # keep printing until the stream ends
cloakctl events 42 --type message_delta
cloakctl events 42 --session run.cloaksession
Output
# https://api.assistant.example/v1/messages · SSE · 16 events
1 381 ms message_start {"type":"message_start",…}
2 382 ms +1 ms ping {"type":"ping"}
9 2.121 s +1.418 s content_block_delta {"type":"content_block_delta",…}
Option What it does
--follow Keeps printing new events every half second until the stream ends
--type TYPE Only events of this type
--json Prints the get_stream_events result instead
--session FILE / --har FILE Reads the request from a saved session instead of the running app
--port N The app’s port, when it isn’t the configured one

It exits 0 when the stream ended cleanly or is still live, 2 when the server broke the stream or its last event was cut off, and 1 when the request isn’t a stream. In CI, record with cloakctl run --record run.cloaksession, then check a streaming endpoint with cloakctl events ID --session run.cloaksession.

  • Streams over WebSocket are shown in the WebSocket Messages tab; only GraphQL ones also get the GraphQL tab.
  • Editing a stream’s events in flight (dropping, delaying or injecting events) isn’t available yet. A Breakpoint on the response holds the whole stream and lets you edit it as one body.
  • gRPC streaming calls are shown in the body viewer, not as events.