Read a response
Sending is one keystroke (Send your first request). Everything after it
happens in the response dock — ⌘J toggles it, and it can be popped out into its own OS
window.
This page covers that dock, and the transport settings that decided how the send was made in the first place.
The tabs
Section titled “The tabs”A tab appears only when it has something to say. A response with no redirects and no phase timings has no Timeline tab at all, rather than an empty one.
| Tab | Appears when |
|---|---|
| Preview | the body is renderable — image, PDF, audio, video, HTML, SVG — or it is binary and needs a hex/base64 view |
| Body | always. JSON is pretty-printed and foldable |
| Raw | always — the body as text, unformatted |
| Headers | always |
| Trailers | the response carried gRPC trailers |
| Timeline | the send was instrumented HTTP, or it followed a redirect |
| Diff | there is a text body to compare |
| Tests | the request declared tests |
| Console | a request script printed something, or set a variable |
| Request | the engine reported the request it actually sent |
In the Body tab every { and [ carries a fold caret with a … n items summary, and clicking a
key or a value copies it. Past 400 visible rows the list is virtualized — only the rows in the
viewport are in the DOM. That has one honest cost: the browser’s own find and “select all” see
only the rendered window. The pane’s own find does not (below), and Copy copies the whole text.
Find in the response
Section titled “Find in the response”⌘F opens a find bar over the current tab: a query, 3 of 17, prev/next, and a match-case
toggle.
It searches the model, not the DOM. Matches are counted over the whole document — including rows that are scrolled out and regions that are collapsed — so the count is honest, and navigating to a hidden match unfolds its ancestors first.
- Substring only, on purpose. Regex belongs to project-wide search (
⌘⇧F); a find bar over a 5,000-line body wants a predictable scan. - Searching stops at 5,000 matches, and the count then reads
3 of 5000+rather than a total that stopped being useful. - A very large flat body is still counted, but not highlighted — re-walking the whole document on every keystroke is what would make it stutter. The bar says which of the two it is doing.
- It is offered on Body, Raw and Headers of a buffered response. Preview, Tests, Request, Diff and Timeline are opted out (there is no single text buffer to step through), as is a live SSE stream and a binary body’s Body/Raw notice.
⌘F has three owners, in precedence order: a focused Monaco editor keeps its native find, then
the response find bar when the pane is showing findable content, then the sidebar’s endpoint
filter. See Keyboard shortcuts.
Filter the body
Section titled “Filter the body”The funnel button opens a filter bar, which is a different thing from find: find locates text
and steps through it; filter hides everything that did not match. The result is the body pruned
to the subtrees that matched, with each match’s ancestors kept (so it is still valid, navigable
JSON) and its whole subtree kept (so a match on user still shows the user).
data.items[*].id..emailstatus >= 400user.role = admin AND -deletedA term is a path, optionally followed by a comparison. Juxtaposition means AND; OR, -
(negation) and parentheses all work. The comparison operators are =, !=, >, >=, <, <=
and ~ (contains, case-insensitive). Comparisons are numeric when both sides look like numbers
and case-insensitive text otherwise.
Three things the bar does that are worth knowing:
- Completion comes from the spec, not from this body. Candidates are the field paths the bound spec source declares for the endpoint — a field the endpoint can return but this call did not is exactly the one you want offered.
- A parse error is underlined by offset, with a caret line under the exact characters the parser objected to, before you run anything.
- It reports what it cannot evaluate instead of quietly returning nothing, and says
Nothing in this body matched the filter.when the query was fine and the body simply did not match.
The filter bar is offered for JSON bodies. While a filter is narrowing, the Body tab, find and
Copy all work on the filtered document — what you see and what you search never diverge — and the
status reads 24 matches. Evaluation stops at 20,000 matches rather than running unbounded.
Use as chain extraction takes the first location the filter matched and offers to bind a chain variable to it, so “I found the id” becomes an extraction without retyping the path.
One path language
Section titled “One path language”The filter bar’s path half is the same grammar as Extraction.path in a chain, Assertion.path
in the Tests tab and the chain assert node, and the History filter box. One grammar, so
data.items[*].id means one thing everywhere.
| Spelling | Looks like | Picked when |
|---|---|---|
| JSONPath subset | data.token, items[0].id, items[*].id, ..id, ['odd key'], an optional leading $ | anything not starting with / |
| XPath subset | /order/line[2]/@sku, //token, /rss/channel/item/title/text() | the expression starts with / |
Deliberately not implemented in the JSONPath half: slices, filter predicates ([?(@.x)]),
unions, script expressions. That is where JSONPath libraries get big and where two implementations
start disagreeing — and this grammar has to agree with an evaluator written in Rust.
When the engine evaluates a path it picks the document from the body: JSON first, XML if that
fails. Neither the collection format nor you has to declare which it is, and both spellings work
over both models. An expression that is plural by construction (a wildcard, a ..) binds an
array, empty when nothing matched. One that is not binds the single node it addresses — and two or
more matches is an error naming the locations, never a silently-picked first.
The split worth naming: the response filter bar opens on a JSON body, so XPath is useful where the engine does the evaluating — extractions and assertions — rather than in the bar itself.
And one warning to disbelieve: a wildcard or .. typed into a chain extraction’s path field, or
into a source: body assertion, draws an amber “Wildcards and .. aren’t evaluated here” line
in this release. It is left over from an extractor that no longer exists. It warns, it does not
block, and the path resolves when the request runs — see
Path expressions.
Diff against a baseline
Section titled “Diff against a baseline”The Diff tab compares this response with one of four baselines, in order of what they cost you:
| Baseline | What it is |
|---|---|
| Spec example (default) | what the bound spec declares for the status that came back. Needs no second run — it works the first time you ever call the endpoint |
| Saved example | a response you kept on this endpoint (below) |
| Last passing run | the most recent passing run of this method + URL from this project’s history |
| Another environment | re-send the same request against a different environment, then diff. The only baseline that touches the network |
Two decisions shape what you see. JSON is compared pretty-printed with sorted keys, so a server that switched from compact to indented output, or re-ordered its keys, does not read as “every line changed”. (The Body tab keeps the server’s own order — there, it is information; here it is noise.) And the algorithm is bounded and says when it gave up: identical head and tail lines are stripped, the middle runs a plain LCS table, and past roughly four million cells the table is not built at all and the result says the diff is approximate rather than freezing the window.
One caveat the footer states for itself: run history keeps only the first slice of a body, so the last passing run baseline clips both sides to that length.
Keep a named example
Section titled “Keep a named example”The bookmark button saves the response on the endpoint it came from, under a name you choose.
It then becomes a Diff baseline, and it travels with the project in git — it is written into the
endpoint’s existing overlay file, .tinspec/overlays/<provider-id>.yaml (see
Project layout).
The limits follow from that file being committed and reviewed:
- text bodies only — a binary body is not offered, because base64 in a committed YAML file is not a diff anybody can read;
- 256 KB per example, and 20 per endpoint;
- the name is unique within the endpoint, and re-using one overwrites it;
- the endpoint has to be bound to a spec source — a hand-authored request has no endpoint to hang it on.
Each example records the status, the Content-Type and the body, so a diff can say when the shape
changed kind.
Save the body to a file
Section titled “Save the body to a file”The download button writes the bytes that came off the wire. They never cross into the UI — the backend holds them and writes them itself, which is the only way a non-UTF-8 body lands on disk intact. The dialog proposes a filename derived from the URL and the media type.
It is unavailable, with the reason in the button’s tooltip, when:
- the response is being shown in a detached panel window — that window mirrors a response without owning the tab it came from;
- the backend is not holding the bytes: the body was over the 32 MB keep ceiling, or it was assembled in the UI (a completed SSE stream) rather than buffered by the engine.
The same detached window has no spec binding, so Save as example and the spec-example Diff baseline are unavailable there too.
Large bodies
Section titled “Large bodies”Nothing expensive happens to a body until it is worth it. Pretty-printing means parsing, re-serialising and building a row per line — free for the 50 KB body you actually read, and a frozen window for a 50 MB one.
| Body size | What happens |
|---|---|
| up to 2 MB | pretty-printed, highlighted and foldable on arrival |
| 2–32 MB | the raw text, verbatim, plus an explicit Render anyway |
| over 32 MB | no render offered — the raw text and Save. Parsing it would cost several times its own size in memory before the first row was drawn |
Separately, a binary body’s exact bytes only reach the UI up to 8 MB. Past that the Preview tab still appears — to say so and point at Save — rather than rendering mojibake.
Preview
Section titled “Preview”Preview picks a renderer from the Content-Type: Image, PDF, Audio, Video, Rendered (HTML), and
Hex / Base64, which are always available once the bytes are in hand.
- HTML renders in an iframe with
sandbox=""— nothing granted: no scripts, no same-origin, no forms, no popups, no downloads, no top-level navigation. A response body is whatever some server chose to send; it gets rendered, never run. A Block external resources checkbox adds a content-security policy that stops the document fetching anything at all, so opening a preview cannot phone home or load a tracking pixel. Documents over 2 MB are truncated before framing, and the note under the frame says which of these applied. - PDF is probed, not assumed. The pane asks this webview whether it has a PDF viewer
(
navigator.pdfViewerEnabled). WebKitGTK on Linux ships none, and an empty grey frame with no error is the worst outcome — so when the answer is no it says so and offers Save. - Hex is laid out 16 KB at a time with a “show more”; Base64 shows a prefix and Copy takes all of it.
Picking a renderer means reading what the response claims to be, so this is also where a
media-type mismatch surfaces: when the spec declares application/pdf for this operation and
status and the response is text/html, that is recorded as contract drift — usually an error page
with a 200 on it. Structural relatives (application/hal+json against a declared
application/json) and wildcard declarations are never flagged.
Timeline
Section titled “Timeline”For an instrumented HTTP send the Timeline tab splits the round trip into the phases the engine measured:
| Phase | What it is |
|---|---|
| DNS | resolving the hostname |
| TCP + TLS | opening the connection, handshake included |
| Waiting | writing the request and waiting for the first response byte — the server’s own latency |
| Download | reading the body after the headers arrived |
Two refusals are baked in. There is no separate TLS number, because reqwest’s connector does
DNS, TCP and the handshake as one unit and exposes no way to split them — inventing a number
nobody measured would be worse than the missing one. And a phase that did not happen is not
zero: a reused pooled connection renders as reused connection, which is the most useful thing
the panel can say about a fast request. Time inside the round trip that no phase claims is shown as
Unaccounted rather than smeared across the phases to tidy the bar.
Only buffered HTTP sends are instrumented. gRPC, WebSocket and a completed SSE stream carry no timings, and the tab says that instead of drawing an empty bar.
The redirect chain
Section titled “The redirect chain”When the send followed a redirect, the same tab lists every hop: its status, its Location,
whether it was same-origin or cross-origin, and whether it was followed at all (not followed — redirect limit reached).
The reason this exists is the banner above it: a cross-origin hop strips credential headers, so the final request arrived unauthenticated. That is almost always the whole answer to “it works in curl but not here”. Only headers the request itself carried can be listed — a cookie the shared jar supplied is invisible to the redirect policy, and the panel does not pretend otherwise.
Trailers
Section titled “Trailers”gRPC metadata that arrives after the body gets its own tab rather than being folded into Headers,
because conflating the two is exactly the bug that hides a trailer-only error: for a failing call,
grpc-status and grpc-message are the whole story and they are trailers.
They are assertable from the request’s Tests tab with the ordinary header[name] source — the
engine falls back to trailers when no header matches, so header[grpc-status] works on success and
on failure alike. See gRPC.
Send settings, and where each value came from
Section titled “Send settings, and where each value came from”How a request is sent — timeout, redirect policy, TLS, proxy, HTTP version — is resolved from four layers, most specific first, field by field:
| Layer | Where it lives |
|---|---|
| This request | the editor’s Settings tab → Request.settings |
| The folder | folder settings → defaults.settings in the collection |
| The API provider | the provider’s Network tab |
| Global | Settings → Sending and Settings → Network — the floor |
The rule that shapes the editor is absent means inherit, never “default”. A field this layer
does not set shows the value that will apply and where it comes from — 30 s — from provider billing-staging — with one control to override it and one to hand it back. Pressing Override seeds
the value that applies right now, so it changes where a value comes from without changing the
value. Clearing an override removes the key entirely, so a committed collection never gains
timeoutMs: null and an all-empty block is not written as settings: {} at all. The tab badges
how many fields this layer actually pins.
The precedence itself is never computed in the UI: the app asks the engine for the effective settings, so there is exactly one implementation of the resolution order — the one that also applies when the request is sent.
What each layer can set:
| Setting | Meaning |
|---|---|
timeoutMs | Abort after this long; 0 is no timeout. For gRPC it is also the deadline the server is told about |
insecure | Accept an invalid or self-signed certificate — the one flaky staging endpoint, without turning verification off everywhere |
followRedirects, maxRedirects | Follow 3xx, and how many hops |
maxResponseBytes | Truncate the body past this many bytes; 0 is unlimited |
httpVersion | auto lets ALPN decide, http1 forces it, http2 assumes prior knowledge with no downgrade |
proxy | URL, bypass list, and username + a keychain reference for the password |
tls | client certificate and key references (mTLS), a CA bundle path, and whether to keep the OS trust store |
grpc | message-size limits and compression |
Proxy and TLS are not offered on a single request. They describe a network, not a call, so
they live on the folder, the API provider, or global Settings. And secret material stays a
reference: tls.clientKeyRef and proxy.auth.passwordRef are keychain names, never values —
the summaries you see name the ref, and the resolved bytes never enter the file or the UI. proxy,
tls and grpc are merged whole, by layer, rather than field by field: half a proxy from here
and half from a lower layer would describe a configuration no layer ever wrote.
Two limits worth stating out loud:
grpchas no editor yet. The message-size and compression fields are carried in the format and resolved by the engine, but nothing in the UI writes them.- Inside a chain, only a request’s own
settingsapplies, over the global floor. The folder and API-provider layers are not passed to a chain run — so a request behind a provider-configured corporate proxy will send correctly from the editor and fail in a chain. See Build a chain.
The fields, with their types, are in Collection file format.