Test it before you req it!
A local-first VS Code API workbench for testing HTTP services — with first-class auth (mTLS, JWT, OAuth2) and a path to near-Postman-complete workflows without a Reqit cloud or separate desktop shell.
Reqit targets Postman-class workflow and capability parity for creating, organizing, editing, sending, inspecting, testing, importing/exporting, and automating API requests. It is not a pixel-for-pixel clone. .requests/ workspace files remain the portable source of truth. Reqit is delivered today as a VS Code extension with pure TypeScript core modules; the roadmap extracts those modules for a small CLI and optional local MCP package rather than adding another desktop shell.
Non-negotiable boundaries:
- no Reqit accounts, first-party backend, hosted sync/collaboration/monitors/mocks, telemetry, crash upload, or opaque cloud storage;
- no bundled Electron shell, always-on daemon, or heavyweight/native data store without a measured, approved architecture exception;
- the architecture requires secret values to stay in VS Code SecretStorage or an explicitly configured local provider and to be redacted/excluded from logs, history, exports, clipboard, Git, errors, fixtures, and MCP outputs by default; the sole storage exception is an explicitly selected user-managed mTLS private-key/PFX file accessed in place through the fail-closed read-only secret-file capability; known implementation gaps must close before the affected roadmap capabilities claim compliance;
- optional backup uses a Git repository selected by the user and system Git credentials; MCP defaults to local stdio.
See ADR 0001, the local data contract and threat model, and the performance and footprint budgets.
The existing landscape of VS Code REST extensions handles GET /api/users great and falls apart the moment you need:
- mTLS / client certificate auth
- JWT generation from claims + signing key (not just paste-in)
- OAuth2 with PKCE and token caching
- Per-environment secrets that aren't stored in plaintext on disk
Reqit fixes that.
.httprequest files — plain-text, version-controllable, REST Client–compatible syntax (### request separators, method/URL line, headers, blank line, body).- Environments + variables —
.requests/.http-env.jsonwith per-environment values,{{var}}substitution everywhere, and built-ins ({{$guid}},{{$timestamp}},{{$randomInt min max}},{{$datetime iso|rfc1123}}). - Secrets done right — per-environment values flagged
{ "$secret": true }are stored in VS CodeSecretStorage. Never written to disk in plaintext, never logged. - First-class auth —
@auth <name>directive resolves a profile from.requests/.http-auth.json:basic,bearer,apiKey(header or query)jwt— paste-in OR generated from claims (HS256/HS384/HS512)clientCert(mTLS) — PEM (cert+key+ optionalca) or PFX with passphraseoauth2—clientCredentialsandauthorizationCodew/ PKCE; tokens cached inSecretStorage
- Send Request codelens — one-click execution backed by
undici; response opens in a side panel. - Request chaining — name a request with
# @name, pull values out of its response with# @capture name[: type [secret]] = $.path, and reference them from later requests ({{login.response.body.$.token}},{{login.response.status}},{{tok}}). Chain references resolve before sending; unresolved ones block the send with an actionable error. Captures and recorded exchanges live in memory for the window session only — never persisted — andsecretcaptures never appear in the rendered request echo. Seedocs/chain-grammar.mdand the scaffolded.requests/chain.httpsample. - Copy as curl — POSIX-safe, secrets redacted by default. Opt in to
revealSecrets: truewhen you really need it. - Inline assertions —
# @test status === 200,# @test json.id != null, etc. (M6, runner UI shipping in follow-up). - GraphQL — mark a request with
# @graphqlorX-Request-Kind: graphqland write the query naturally; Reqit serializes{ query, variables, operationName }and pretty-printsdata/errorsseparately. See### GraphQLbelow. - Local-only, zero telemetry — no first-party server, no "check for updates" pings. CI enforces a no-telemetry import scan on every build.
Reqit isn't published to the Marketplace yet (M7 in progress — see #7). To try it now:
git clone https://github.com/rwrife/reqit.git
cd reqit
npm install && npm run buildThen press F5 in VS Code to launch an Extension Development Host with Reqit loaded, or run npx vsce package to build a .vsix you can code --install-extension reqit-0.0.1.vsix.
- Open a workspace and run Reqit: Init Workspace — scaffolds
.requests/with a samplehello.httpand an empty.http-env.json. - Open the sample file. Click Send Request above any request.
- Pick an environment from the status-bar item.
In Reqit Requests, use the filter icon or run Reqit: Filter Requests by Method from the command palette. Choose an HTTP method or GRPC; All methods clears the filter, and Escape leaves it unchanged.
Use the search icon or Reqit: Search Requests by Name to apply a case-insensitive literal substring match over explicit ### name labels in parsed .http/.grpc requests. Reqit: Search Requests by URL matches the literal URL in parsed HTTP requests (not gRPC targets); it never expands variables or resolves secrets. Reqit: Search Requests by Tag matches one exact tag token from an HTTP request's existing # @tag directive (comma-separated values, case-insensitive; a comma in the query is rejected rather than silently matching nothing). Reqit: Search Request Text matches literal text inside a request's own section (directives, URL, headers, body, assertions; the preceding ### name line is excluded), unresolved and case-insensitive, for HTTP and gRPC sections. URL, tag, and text input use masked native input boxes because any of them can carry sensitive text. Empty input clears a search; Escape preserves it. Each input is bounded to 128 characters. A file longer than 1,048,576 UTF-16 code units reports File too large for text search rather than scanning or falsely reporting no matches. The view title shows only generic active-filter indicators (Method: ..., Name search active, URL search active, Tag search active, Text search active), never search text.
Use Reqit: New Collection Folder from the command palette (choose a workspace in multi-root windows) or an existing folder row to add a nested .requests/ folder. It creates a .gitkeep marker in one undoable VS Code edit, making empty folders portable in Git. Names are single safe segments; existing folders are refused. Symlink ancestors are checked before the edit, not protected from concurrent swaps at final I/O (#56). Reqit: Add Folder Documentation on a folder row opens an untitled Markdown template. To add README.md beside the requests, use VS Code Save As, inspect the destination, and avoid overwriting existing notes. Reqit does not read or write a notes path, render Markdown, or upload it. This manual fallback is not race-safe collection file management; final-I/O confinement remains #56.
Use Reqit: New Request File from the command palette (choose a workspace in multi-root windows) or the new-file action on an existing .requests/ folder row to create a .http file with a starter GET request. Names are restricted to a single safe filename; existing files are never overwritten. The create and initial content insertion form one VS Code WorkspaceEdit. It does not create folders or silently initialize .requests/; run Init Workspace first. Symlink ancestors are checked before the edit, but final-I/O race-safe confinement and external-edit conflict handling remain open in #56.
When an external tool creates, changes, or deletes an open .requests/*.http or .grpc file with unsaved edits, Reqit warns once per dirty editor and offers Review Editor. Closing the editor or observing a clean disk event resets the warning. Reqit does not read/merge disk content or overwrite unsaved bytes; VS Code remains responsible for save conflicts. This is warning-only coverage, not conflict resolution or final-I/O confinement (#56).
Use Reqit: Add Request to File (+ icon on an existing .http file row) to append a named GET {{baseUrl}}/ request to the open document. The change is a reviewable, undoable VS Code edit; save it to persist. Existing file bytes remain unchanged, and a unique New Request section name is chosen. This action does not create files or cover other collection CRUD operations; race-safe final-I/O confinement and external-edit conflict handling remain open in #56.
Use Reqit: Rename Request (edit icon on a named HTTP request row) to update its ### name separator line in the open document. The change is a reviewable, undoable VS Code edit; save the file to persist it. Unnamed requests, duplicate names within the file, names with control characters, and requests under symbolic links are rejected without modifying the document. Other collection operations (move) and final-I/O confinement remain open in #56.
Use Reqit: Duplicate Request (copy icon on a named HTTP request row) to insert a copy of that request section immediately after it with a unique ### name Copy label (or numbered suffix). The insertion is one undoable VS Code edit; save the document to persist it. The action rejects stale rows, invalid or oversized files, and symbolic links during preflight. It does not duplicate unrelated sections. Document-version checks do not provide race-safe final-I/O confinement or detect concurrent external disk edits; those remain open in #56.
Use Reqit: Delete Request (trash icon on a named HTTP request row) to remove just that section after modal confirmation. The deletion remains an undoable, reviewable unsaved editor change; save only after inspecting it. Stale rows, oversized files, and symbolic links fail closed during preflight. This does not delete files or folders. External disk edits and race-safe final-I/O confinement remain open in #56.
Use Reqit: Move Request Up/Down from a named HTTP request row's context menu to swap it with the adjacent named section in the same .http file. This produces one undoable editor change; save after review. Edges, unnamed/malformed sections, stale rows, and sections lacking a trailing newline are refused. Cross-file moves, external disk edits, and race-safe final-I/O confinement remain open in #56.
Use Reqit: Show Favorite Requests (star icon in explorer title) to toggle favorites-only view. Add # @favorite true before the request line inside a named .http request section to mark it; edit or remove that directive to unmark it. A Toggle Favorite Request star action on each HTTP request row now inserts/removes the directive in the open .http document as a reviewable, undoable edit; save the file to persist it. Unsupported directive placements in the header block must be edited manually. There is no hidden favorites database or automatic workspace write. Only exact true is recognized; gRPC favorites are not supported. Other explorer filters still compose with favorites.
Filtering/search affects request children inside expanded .http/.grpc files in every VS Code-approved workspace folder's .requests/ tree. Single-folder windows retain the flat tree; multi-folder windows group files by workspace folder. Explorer discovery does not grant access to files outside those roots. Folders and files stay visible (no recursive index); unnamed fallback labels (METHOD request / target-derived gRPC labels) are excluded from name search. HTTP URLs are omitted entirely from request labels and tooltips because credentials can occur anywhere in a URL; use explicit request names to distinguish unnamed entries. URL search still matches the original source URL, and send anchors are unchanged. Open editors are parsed from their current text instead of stale disk bytes, matching the Send Request codelens source; edits and closes refresh the tree, while closed files are read lazily. Explicit names, file/folder labels, and gRPC targets are not secret-classified by this display policy. A parsed file with no matching requests shows a non-actionable message. Selecting a matching request keeps its original source location and existing send/preview action. Refresh keeps active filters, while reloading the extension resets them.
Method, name, URL, and tag filters compose in-memory (AND). Use Reqit: Search Requests by Tag with a single tag token from # @tag smoke, regression in an HTTP request; matching is case-insensitive and exact per comma-separated token. Empty input clears; Escape preserves the filter. Tags remain optional portable .http text — not hidden metadata — and are never inferred from URLs, headers, bodies, or gRPC labels. This feature does not resolve variables/secrets, write workspace files, or send requests. It reuses existing lazy file reads and parsers without building a new body/response index. The explorer skips entries reported as symbolic links during directory discovery, including links to request files and folders. This is presentation filtering, not path confinement: a link can replace an entry after discovery, and the existing editor/send/import paths still lack final-I/O no-follow checks. Text search, CRUD, race-safe path-confinement hardening, and 1,000-request performance validation remain tracked in #56; no measured indexing/search-speed claim is made here.
.requests/.http-auth.json:
{
"prod-mtls": {
"type": "clientCert",
"cert": "./certs/client.pem",
"key": "./certs/client.key",
"ca": "./certs/ca.pem"
},
"signed-jwt": {
"type": "jwt",
"alg": "HS256",
"secret": { "$secret": true },
"claims": { "sub": "svc-account", "aud": "https://api.example.com" },
"ttlSeconds": 300
},
"github": {
"type": "oauth2",
"flow": "authorizationCode",
"clientId": "...",
"authorizationUrl": "https://github.com/login/oauth/authorize",
"tokenUrl": "https://github.com/login/oauth/access_token",
"scopes": ["repo", "read:user"]
}
}# @auth prod-mtls
GET https://api.example.com/me
###
# @auth signed-jwt
POST https://api.example.com/events
Content-Type: application/json
{ "kind": "ping" }Reqit can convert requests from formats you've already got lying around.
Reqit: Import from cURL— paste anycurl ...command (multi-line with\continuations is fine). Reqit writes a new file under.requests/with the URL, method, headers and body filled in.-ubecomes a BasicAuthorizationheader,--cert/--keybecome a# @auth clientCert (...)directive so you know to wire up an mTLS profile in.http-auth.json.Reqit: Import Postman Collection— point at a Postman v2.1.jsonexport. Reqit writes one.httpfile per top-level folder under.requests/(nested folders are flattened into prefixed request names like### Admin / Users / invite). Supported bodies:raw(Content-Type inferred fromoptions.raw.language),urlencoded,graphql. Supported auth: basic, bearer, apikey (header), oauth2 (token attached if present). Multipart/file bodies andapikeyin query are surfaced as# unsupported:notes so you can finish them by hand. Collection-level variables are offered for merge into.http-env.jsonunder an env name of your choice;{{var}}tokens are preserved verbatim since reqit uses the same syntax.Reqit: Import OpenAPI— pick a local.yaml/.yml/.jsonspec or paste an http(s) URL. Reqit writes one.httppertags[0](untagged operations land indefault.http), with{pathParams}rewritten to{{pathParams}}and query/header params surfaced as{{var}}placeholders. Request bodies preferapplication/json(then form-urlencoded, then anything else) and are filled with the spec'sexample/examplesvalue, falling back to a schema-derived sample ($refs resolved within the same doc, cycles are handled).servers[0].urlbecomes{{baseUrl}}in.http-env.json.securitySchemesmap toAuthorizationheaders where possible — bearer/basic/apiKey-header are auto-wired; oauth2 / mTLS produce a warning so you can finish them in.http-auth.json.
Imports never silently overwrite an existing file — you'll be asked to confirm.
Going the other way — sharing .http files with teammates on Postman/Insomnia/Bruno, or with a CI job that only speaks curl — is being landed in slices tracked under #42:
- cURL bundle — pure exporter in
src/core/export/curl.tsturns a set of parsed.httpfiles into a single runnable bash script (onecurlper request, grouped by source file). Secret redaction is first-class: values you pass inredactare replaced with***REDACTED***(or a caller-supplied placeholder) in every URL, header, body and in the request-label comment above each command — no partial-token reassembly..grpcand.wsinputs in a mixed tree are skipped with a warning rather than failing the export. TheReqit: Export to cURL bundle…command that wires this into VS Code is the next slice. - Postman v2.1 collection — coming next.
- OpenAPI 3.x stub from history — coming after that.
🚧 Pre-alpha. Following the plan in PLAN.md.
M1 landed: TypeScript + esbuild scaffold, .http parser, Reqit: Init Workspace command, Send Request codelens that fires undici and renders the response in a webview.
M2 in progress: .requests/.http-env.json with named environments, status-bar env picker, {{var}} substitution (URL + headers + body), built-ins {{$guid}} / {{$timestamp}} / {{$datetime iso|rfc1123}} / {{$randomInt min max}}, and per-secret values via VS Code SecretStorage ({ "$secret": true }).
Define .requests/.http-env.json:
{
"default": { "baseUrl": "https://api.example.com" },
"staging": {
"baseUrl": "https://staging.example.com",
"apiKey": { "$secret": true }
}
}- Switch the active env from the status-bar item (
Reqit: <env>). - Reference vars anywhere with
{{baseUrl}},{{apiKey}}, etc. - Secrets are prompted on first use and stored in VS Code
SecretStorage— never written to disk in plaintext. Re-prompt with Reqit: Set Secret.
Every request gets a Copy as curl codelens next to Send Request. The generated command is POSIX-safe (single-quoted) and resolved secrets — active-environment secret values and request-chaining secret captures, including their environment/builtin-expanded forms — are replaced with ***REDACTED*** before the command hits the clipboard, using the same canonical masker as the render echo (raw and JSON-escaped forms, longest-first). Run the reqit.copyAsCurl command with revealSecrets: true if you explicitly need the unredacted version.
You can attach inline assertions to a request with # @test <expr> (or // @test <expr>). The expression is evaluated against the response and pass/fail is reported by the runner (M6 — runner UI + CLI ship in follow-up PRs).
Available bindings: status, statusText, headers (lower-cased), header(name), body, text (alias of body), json (parsed when content-type is JSON), durationMs. Safe globals: Math, Date, JSON, Number, String, Boolean, Array, Object, RegExp, parseInt, parseFloat, isNaN, isFinite. Things like process, require, and globalThis are deliberately shadowed.
# @test status === 200
# @test json.id != null
# @test header("content-type").includes("json")
# @test durationMs < 1000
GET https://example.com/api/meAttach a JSON Schema to a request with # @schema <ref> and Reqit will validate the response body against it. Three ref forms are supported:
- Inline JSON —
# @schema { "type": "object", "required": ["id"] } - OpenAPI pointer —
# @schema openapi:./api.yaml#/components/schemas/User(walks$refs inside the same doc) - File —
# @schema file:./user.schema.json[#/definitions/User]
Failures are reported with a JSON Pointer path (e.g. /id), the failing rule (required, type, format…), and a plain-English message. Non-JSON responses are skipped with a warning, not treated as failures. The response-panel Schema tab and CLI exit-code wiring land in follow-up PRs — this PR ships the pure core (src/core/schemaValidator.ts + src/core/schemaRef.ts) so the parser, importer, and future runner all share the same validation path.
### get user
# @schema { "type": "object", "required": ["id", "name"], "properties": { "id": { "type": "integer" }, "name": { "type": "string" } } }
GET https://example.com/api/users/1Mark a request as GraphQL with either a # @graphql directive or an X-Request-Kind: graphql header. The body is the GraphQL document, optionally followed by a blank line and a JSON variables block:
### Get user
POST https://api.example.com/graphql
X-Request-Kind: graphql
query GetUser($id: ID!) {
user(id: $id) { id name email }
}
{ "id": "{{userId}}" }Reqit serializes the outgoing body as { query, variables, operationName? } (defaulting variables to {}), auto-detects operationName from the first named query|mutation|subscription, strips the X-Request-Kind marker, sets Content-Type: application/json if you didn't, and runs {{var}} substitution across both the query and the variables block. The response viewer pretty-prints data and errors separately when the response looks like GraphQL.
When a request returns Content-Type: text/event-stream, Reqit treats it as
a live event stream instead of a single body. Under the hood this is driven
by a pure, spec-compliant SSE parser + transport (src/core/sse/) that has
no dependency on VS Code or undici, so the same code runs in unit tests
and in the extension host.
Core capabilities available today:
-
Full WHATWG SSE frame parsing:
event:,data:,id:,retry:, multi-linedata:joining,:comments, blank-line dispatch, CR / LF / CRLF terminators, leading BOM tolerated. -
# @sse-until <expr>directive: a single-line JS predicate evaluated against every dispatched event. When it returns truthy, the transport stops draining and closes the stream. Useful for LLM-style streams:# @auth openai # @sse-until event.data.includes("[DONE]") POST https://api.openai.com/v1/chat/completions Content-Type: application/json { "model": "gpt-4o-mini", "stream": true, "messages": [{ "role": "user", "content": "Say hi." }] }
-
Reconnect state (
lastEventId, server-suggestedretry:) is tracked so callers can sendLast-Event-IDon the next attempt. A helper clamps absurdretry:values into a sane range (default 100ms–30s). -
Guardrails:
maxEvents,maxDurationMs,idleMs, and anAbortSignal— all validated up front withzodwhen they come from user data. Configurable per-request via directives:# @sse-max-events 50 # @sse-max-duration-ms 30000 # @sse-idle-ms 5000 # @sse-until event.data.includes("[DONE]")
Any non-positive-integer value is surfaced as a warning banner in the response panel and dropped — the stream keeps running with the defaults.
-
Transcript format:
formatSseTranscriptLineemits one JSON object per line (.sse.jsonl), safe totail -f. Auth material is never logged by the parser or transport; callers must not funnel secrets into event bodies.
The VS Code response-viewer wiring for SSE now lives on top of this
core: when Send Request fires a request whose response has
Content-Type: text/event-stream, the response panel switches into
streaming mode and appends events (with type, id, elapsed time and
pretty-printed data) live as they arrive. While a stream is live the panel
shows a dedicated Stop stream button that aborts exactly that session
(its markup comes from the pure src/core/sse/streamView.ts builder — a
strict CSP with a per-render script nonce, a single validated stop message,
and no other script). When a stream ends, Reqit caches the
captured event transcript and offers Save transcript; you can also run
Reqit: Save Last SSE Transcript from the command palette to write
.sse.jsonl into .requests/.history/ (or another path you choose).
Closing the panel alone does not stop a live stream — use the Stop button
or Reqit: Stop SSE Stream.
Reconnect + Last-Event-ID retry landed in
#68. A palette-wide Stop stream action
(Reqit: Stop SSE Stream) aborts every live session through a pure
SseStreamRegistry (src/core/sse/streamControl.ts): the transport races the
next-chunk wait against the stop signal, so a stalled connection returns
aborted promptly, late chunks can never dispatch late events, in-flight and
reconnect sockets are destroyed, and live sessions are also aborted on
deactivation. Remaining #46 polish is tracked in
#46.
npm install
npm run build # esbuild → dist/extension.js
npm run lint # eslint
npm run typecheck # tsc --noEmit
npm run test:unit # vitest (pure parser tests)
Press F5 in VS Code to launch the Extension Development Host.
See CHANGELOG.md.
MIT — see LICENSE.