A deep dive into building DotFetch v2.1.0: Modular architecture in VS Code WebViews, RFC 6749 OAuth 2.0, backend-owned Basic Auth with environment variables, and safe credential isolation.
The Context Switching Problem
Context switching is one of the most frustrating parts of building backend services.
Every time you write an API endpoint in VS Code, you switch windows to a standalone API client, wait for it to load, configure environment variables, run the request, and switch back to your editor.
We created DotFetch to bring a lightweight, dark-themed HTTP client directly into your VS Code workspace โ native .env variable support, collections, cURL import/export, and zero UI bloat.
In v2.0.0, we built the initial request builder with structured parameter and header tables.
For v2.1.0, our focus shifted to engineering a hardened authentication and security layer designed for real-world API workflows. The challenge was resolving the architectural edge cases that appeared once authentication, persistence, and environment substitution started interacting.
Here is an inside look at how we designed and refactored DotFetch v2.1.0.
๐๏ธ 1. Architecture: Modular Frontend in a Sandboxed WebView
A common problem in VS Code extension development is stuffing DOM manipulation, business logic, and IPC handling into a single monolithic script.
In DotFetch v2.1.0, we structured the frontend into an MVC-inspired modular frontend architecture:
DotFetch Architecture
โโโ Extension Host (Node.js Runtime)
โ โโโ RequestService.ts โ Axios execution engine, SSL Agent, OAuth token fetcher
โ โโโ DataManager.ts โ globalState persistence (History & Collections)
โ โโโ EnvironmentManager.ts โ .env parsing & {{VAR}} substitution
โ โโโ webviewPanel.ts โ IPC Message Router & CSP builder
โ
โโโ Frontend WebView (Browser Runtime - src/webview/)
โโโ main.js โ Central Controller & event dispatcher
โโโ request.js โ Form state, live assembly & send logic
โโโ auth.js โ Auth rendering, live previews & RFC validation
โโโ ui.js โ Structured tables, response tabs & badges
โโโ shortcuts.js โ Configurable keyboard shortcuts
โโโ state.js โ Single source of truth & canonical schemas
โโโ api.js โ postMessage transport layer
The Bundling Strategy under Strict CSP
DotFetch applies a strict Content-Security-Policy to its VS Code WebView, using nonce-based script execution:
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'nonce-${nonce}'; ...">
Bundling simplifies WebView resource loading and CSP management while keeping the source code modular during development.
We maintain clean ES modules in src/webview/ during development, and use esbuild to bundle src/webview/main.js into a single IIFE bundle (media/script.js) in ~30ms local build time in our development setup:
"esbuild-base": "esbuild ./src/extension.ts --bundle --outfile=out/extension.js --external:vscode --format=cjs --platform=node && esbuild ./src/webview/main.js --bundle --outfile=media/script.js --format=iife --platform=browser"
This keeps the codebase modular and readable in source control while shipping a single, CSP-compliant script at runtime.
๐ 2. The Authentication Suite
๐ API Key Authentication (Header vs. Query Mode)
APIs accept keys differently. Some require custom HTTP headers (X-API-Key: secret), while others expect query parameters (?api_key=secret).
We built an interactive UI that lets developers switch modes with real-time visual previews and a copy button:
// src/webview/auth.js
export function updateApiKeyPreview() {
const preview = document.getElementById('auth-preview');
if (!preview) return;
const keyName = state.authConfig.keyName || '';
const keyValue = state.authConfig.keyValue || '';
const keyIn = state.authConfig.keyIn || 'header';
if (keyName && keyValue) {
const text = keyIn === 'header' ? `${keyName}: ${keyValue}` : `?${keyName}=${keyValue}`;
preview.innerHTML = `<span>${text}</span><button type="button" class="tool-btn" id="copy-auth-preview">Copy</button>`;
preview.classList.remove('hidden');
}
}
When sent in Query Parameter mode, the headers table remains untouched and RequestService injects the key into the URL string on the Node.js side right before execution.
โก OAuth 2.0 (Client Credentials Flow)
Machine-to-machine APIs (Spotify, Twitch, Azure, Auth0) frequently rely on OAuth 2.0 Client Credentials.
Instead of writing separate shell scripts:
- Enter your
Token URL,Client ID,Client Secret, andScope. - Click "โก Fetch & Inject Token".
- DotFetch contacts the OAuth provider, extracts the
access_token, displays an active token status card with expiration metadata, and injectsAuthorization: Bearer <token>into your live requests.
// src/requestService.ts - RFC 6749 ยง2.3.1 Compliance
public async fetchOAuthToken(message: any, webview: vscode.Webview) {
const tokenUrl = this.environmentManager.substituteVariables(message.tokenUrl, env).trim();
const clientId = this.environmentManager.substituteVariables(message.clientId, env).trim();
const clientSecret = this.environmentManager.substituteVariables(message.clientSecret, env).trim();
const scope = this.environmentManager.substituteVariables(message.scope, env).trim();
const params = new URLSearchParams();
params.append('grant_type', 'client_credentials');
if (scope) { params.append('scope', scope); }
const headers: Record<string, string> = {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json'
};
// RFC 6749 ยง2.3.1: client_id & client_secret MUST be URL-encoded before Base64 encoding
if (clientId && clientSecret) {
const formEncode = (value: string) => encodeURIComponent(value).replace(/%20/g, '+');
const basicCreds = Buffer.from(
`${formEncode(clientId)}:${formEncode(clientSecret)}`,
'utf8'
).toString('base64');
headers['Authorization'] = `Basic ${basicCreds}`;
}
const response = await axios.post(tokenUrl, params.toString(), { headers, timeout: 15000 });
// Returns access_token, token_type, expires_in to WebView...
}
๐ก๏ธ SSL/TLS Toggle for Local Development
When testing microservices locally with self-signed SSL certificates (https://localhost:8080), Node.js rejects the connection with certificate verification errors.
DotFetch v2.1.0 provides an SSL Toggle with a prominent toolbar warning badge (โ ๏ธ SSL Ignored):
const httpsAgent = message.sslVerify === false
? new https.Agent({ rejectUnauthorized: false })
: undefined;
To prevent accidental security gaps, every request load enforces a secure default (sslVerify: true).
๐ ๏ธ 3. Solving the Hard Architectural Edge Cases
The auth features were straightforward. The difficult part was keeping authentication, persistence, and request execution from contaminating each other.
๐งฉ 1. The "Draft vs. Wire" Headers Contamination
The Problem: When you send a request with Bearer Auth, the client injects Authorization: Bearer <token> into the outgoing wire headers. If the user clicked "Save to Collection", that injected header was saved into the raw headers table. Later, loading that request and switching to "No Auth" caused the old token to remain in manual headers and still be sent.
The Solution: We decoupled Draft Data (for storage) from Wire Data (for execution):
// src/webview/request.js
export function getRequestData({ forSend = false } = {}) {
const rawHeaders = serializeHeaders();
// Wire headers get auth injection; Saved draft headers stay clean
const headers = forSend ? applyAuthHeaderToRawHeaders(rawHeaders) : rawHeaders;
// Live send keeps in-memory secrets; Storage strips them
const auth = forSend ? { ...state.authConfig } : createPersistableAuthConfig();
return {
method: document.getElementById('method')?.value || 'GET',
url: constructFullUrl(),
headers,
auth,
sslVerify: state.settings.sslVerify !== false
};
}
๐งฉ 2. Basic Auth + Environment Variables Base64 Deadlock
The Problem: The UI allows Username: {{API_USER}} and Password: {{API_PASS}}.
If the WebView constructs Authorization: Basic ${btoa(${user}:${pass})}, it produces Base64 ciphertext of the literal string {{API_USER}}:{{API_PASS}}. When the Extension Host receives it, it cannot substitute the environment variables because they are locked inside Base64.
The Solution: Make Basic Auth 100% Backend-Owned:
- The WebView strips any conflicting manual
Authorizationheader. -
RequestServiceon the Extension Host performs variable substitution first, and then computes the Base64 header in Node.js runtime:
// src/requestService.ts
if (message.auth?.type === 'basic') {
const rawUser = message.auth.username || '';
const rawPass = message.auth.password || '';
const user = this.environmentManager.substituteVariables(rawUser, selectedEnv);
const pass = this.environmentManager.substituteVariables(rawPass, selectedEnv);
if (user || pass) {
const basicCreds = Buffer.from(`${user}:${pass}`, 'utf8').toString('base64');
substitutedHeaders['Authorization'] = `Basic ${basicCreds}`;
}
}
๐งฉ 3. Zero-Persistence Plaintext Secrets across Collections, Favorites & History
The Problem: vscode.ExtensionContext.globalState is used for persisting collections and history. Storing raw passwords, tokens, API key values, or OAuth secrets as plaintext JSON in globalState is an unnecessary security risk.
The Solution: Plaintext runtime credentials are stripped before DotFetch persists request metadata to globalState across Collections, Favorites, and History:
// src/webview/state.js
export function createPersistableAuthConfig(authConfig = state.authConfig) {
return {
...authConfig,
password: '',
token: '',
keyValue: '',
clientSecret: '',
accessToken: '',
expiresIn: null,
tokenReceivedAt: null
};
}
When a request executes, sendRequest() passes both requestData (live wire credentials) and historyData (sanitized snapshot). On success, webviewPanel.ts persists only the sanitized snapshot:
// src/webviewPanel.ts
if (capturedResponse) {
const safeRequest = message.historyData || {};
const historyEntry: RequestData = {
name: safeRequest.name,
url: safeRequest.url,
headers: safeRequest.headers, // Clean manual headers
auth: safeRequest.auth, // Stripped secrets
status: capturedResponse.status,
duration: capturedResponse.duration,
createdAt: new Date().toISOString()
};
await this.dataManager.addHistory(historyEntry);
}
๐งฉ 4. Guarding Against Memory-Exploding Payloads
The Problem: If an API endpoint returns a 50MB JSON payload, transferring that full payload across the VS Code postMessage IPC boundary will freeze the WebView and lag the editor.
The Solution: We implemented double-layered response protection:
-
Network Layer: Axios enforces
maxContentLength: 10 * 1024 * 1024(10 MB). -
Display Layer: If the response exceeds 1 MB (
DISPLAY_THRESHOLD), we compute accurate UTF-8 byte length viaBuffer.byteLength()and truncate the WebView preview to the first 1000 characters with a helpful badge:
// src/requestService.ts
const serializedResponse = typeof response.data === 'string'
? response.data
: JSON.stringify(response.data ?? '');
const responseSize = Buffer.byteLength(serializedResponse, 'utf8');
const isLarge = responseSize > RequestService.DISPLAY_THRESHOLD;
webview.postMessage({
type: 'response',
status: response.status,
duration,
size: responseSize,
headers: response.headers,
isLarge,
data: isLarge
? `[Response too large: ${(responseSize / 1024).toFixed(2)} KB]\n\nFirst 1000 characters:\n${serializedResponse.substring(0, 1000)}...`
: response.data
});
๐งช 4. How We Verified It
Before tagging v2.1.0, we run our quality gates:
npm run compilenpm run typechecknpm run lint
Then we validate the runtime test matrix manually across scenarios:
- No Auth: Raw headers sent unchanged; no injected tokens
- Basic Auth (Literal): Safe UTF-8 Base64 encoding; manual headers overridden
-
Basic Auth (Variables):
{{USER}}&{{PASS}}substituted before Base64 - Bearer Token: Injected on wire only; excluded from saved collections
- API Key (Header): Custom header injection; live preview works
- API Key (Query): Injected into URL query string; headers table untouched
- OAuth 2.0 Flow: RFC 6749 URL encoding; active token card display; expired token blocks send
- Save & Reload: Schema restored cleanly; zero header contamination
- History Sanitization: Plaintext secrets omitted from globalState
- SSL Toggle: Localhost bypasses SSL when disabled with active warning badge
- Memory Guard: Payloads > 1MB safely truncated in preview
- Cancel & Retry: AbortController halts execution; network retry with exponential backoff
๐ Summary of What DotFetch v2.1.0 Delivers
| Feature Area | What You Get |
|---|---|
| Authentication | Bearer Token, Basic Auth (env-aware), API Key (Header & Query), OAuth 2.0 Client Credentials |
| UI & Visibility | Interactive eye toggles (๐๏ธ / ๐), Response Headers Inspector table, Live previews |
| Security & Privacy | Zero-persistence plaintext secret policy across Collections, Favorites & History, SSL Toggle |
| Performance | 10MB payload size limits, 1MB large response truncation, modular WebView source bundled into a single CSP-compatible runtime |
| Code Health | Zero TypeScript typecheck errors, strict ESLint pass, clean IPC separation |
๐ Try DotFetch
DotFetch is free, open-source, and built for developers who appreciate clean tools.
๐ฆ VS Code Marketplace: Install DotFetch
๐ Open VSX Registry: Install DotFetch
โญ GitHub Repository: kareem2099/DotFetch
Have feedback or feature suggestions for v2.2? Drop a comment below or open an issue on GitHub!



Top comments (0)