Skip to content

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.

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.

TabAppears when
Previewthe body is renderable — image, PDF, audio, video, HTML, SVG — or it is binary and needs a hex/base64 view
Bodyalways. JSON is pretty-printed and foldable
Rawalways — the body as text, unformatted
Headersalways
Trailersthe response carried gRPC trailers
Timelinethe send was instrumented HTTP, or it followed a redirect
Diffthere is a text body to compare
Teststhe request declared tests
Consolea request script printed something, or set a variable
Requestthe 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.

⌘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.

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
..email
status >= 400
user.role = admin AND -deleted

A 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.

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.

SpellingLooks likePicked when
JSONPath subsetdata.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.

The Diff tab compares this response with one of four baselines, in order of what they cost you:

BaselineWhat 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 examplea response you kept on this endpoint (below)
Last passing runthe most recent passing run of this method + URL from this project’s history
Another environmentre-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.

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.

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.

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 sizeWhat happens
up to 2 MBpretty-printed, highlighted and foldable on arrival
2–32 MBthe raw text, verbatim, plus an explicit Render anyway
over 32 MBno 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 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.

For an instrumented HTTP send the Timeline tab splits the round trip into the phases the engine measured:

PhaseWhat it is
DNSresolving the hostname
TCP + TLSopening the connection, handshake included
Waitingwriting the request and waiting for the first response byte — the server’s own latency
Downloadreading 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.

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.

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:

LayerWhere it lives
This requestthe editor’s Settings tab → Request.settings
The folderfolder settings → defaults.settings in the collection
The API providerthe provider’s Network tab
GlobalSettings → 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:

SettingMeaning
timeoutMsAbort after this long; 0 is no timeout. For gRPC it is also the deadline the server is told about
insecureAccept an invalid or self-signed certificate — the one flaky staging endpoint, without turning verification off everywhere
followRedirects, maxRedirectsFollow 3xx, and how many hops
maxResponseBytesTruncate the body past this many bytes; 0 is unlimited
httpVersionauto lets ALPN decide, http1 forces it, http2 assumes prior knowledge with no downgrade
proxyURL, bypass list, and username + a keychain reference for the password
tlsclient certificate and key references (mTLS), a CA bundle path, and whether to keep the OS trust store
grpcmessage-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:

  • grpc has 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 settings applies, 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.