Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"env": { "node": true, "jest": true }
},
{
"files": ["generate-screenshots.js"],
"files": ["generate-screenshots.js", "scripts/**/*.js"],
"env": { "node": true }
}
]
Expand Down
27 changes: 27 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Validate extension
on:
push:
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm
- run: npm ci
- run: ./release.sh --no-bump
- run: npx playwright install --with-deps chromium
- run: npm run test:browser
- id: version
run: echo "value=$(node -p 'require("./manifest.json").version')" >> "$GITHUB_OUTPUT"
- uses: actions/upload-artifact@v4
with:
name: quick-api-client
path: dist/quick-api-client-v${{ steps.version.outputs.value }}.zip
if-no-files-found: error
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
## 1.1.5 - 2026-09-11

- Prevent duplicate sends during permission prompts and ignore stale callbacks after cancellation.
- Share request preparation across sending, preview and generated code, including GraphQL and environment values.
- Preserve raw request bodies and duplicate query parameters. Validate protocols, headers and import data.
- Bound request and response bodies to 5 MiB and skip syntax highlighting above 200,000 characters.
- Report actual response bytes and preserve raw response text when copying or downloading.
- Distinguish cancellations from timeouts and clean up request controllers and streams.
- Fix generated PHP, Java and C# quoting, empty Java POST bodies, and non-JSON Fetch responses.
- Preserve GraphQL variables during restoration and prevent settings reset from deleting favorites.
- Retain active environment selection when renaming, clear it when deleting, and report failed saves correctly.
- Include all popup modules in release archives and synchronize version metadata.
- Replace copied test implementations with source-based checks and add regression and browser package tests.

# Changelog - Quick API Client

Welcome to the **Quick API Client** release log. Below is the list of changes, optimizations, and new features introduced for the Chrome Store and Edge Add-ons.
Expand Down
73 changes: 16 additions & 57 deletions PRIVACY_POLICY.md
Original file line number Diff line number Diff line change
@@ -1,74 +1,33 @@
# Privacy Policy for Quick API Client

**Effective date:** 2025-01-01
Effective date 2026-09-11

This Privacy Policy describes how the browser extension **Quick API Client** (the "Extension") provided by **Hayk Jomardyan** ("we", "us", or "our") handles information when used in supported browsers, including Google Chrome and Microsoft Edge.
Quick API Client is provided by Hayk Jomardyan. This policy describes the extension's implemented data handling.

By installing or using the Extension, you acknowledge that you have read and understood this Privacy Policy.
## API requests

## 1. Scope
When you send a request, the extension transmits its URL, query parameters, headers and body directly to the selected API server. That server receives the connection's IP address and any credentials or personal information you include. Redirects follow browser networking rules. The extension omits browser cookies from its fetch requests.

This Privacy Policy applies solely to the Extension as distributed through:
- The Chrome Web Store for Google Chrome and other Chromium-based browsers.
- The Microsoft Edge Add-ons store for Microsoft Edge.
The extension does not send request data to a developer-operated relay, analytics service or advertising service. It does not monitor general browsing activity.

It does not apply to any websites, services, or applications that may be linked from the Extension but are operated by third parties under their own privacy policies.
## Browser storage

## 2. Data collection and processing
The extension saves the most recent request and enabled request history in local extension storage. These records may include URLs, headers, credentials and bodies entered by the user.

- The Extension does **not** collect, store, or process any personal data or personally identifiable information (PII).
- The Extension does **not** collect or transmit your browsing history, URLs, search queries, IP address, device identifiers, cookies, or similar identifiers.
- The Extension does **not** track your behavior across websites, and does **not** perform any profiling or analytics.
Settings, favorites and environment variables use browser sync storage. The browser provider may synchronize these records through the signed-in browser account, according to its settings and policies. The extension does not encrypt these values itself. The developer does not retrieve them.

All features of the Extension are designed to work without requiring any personal or sensitive user data, in line with data minimization expectations for Chrome and Edge extensions.
Response bodies are displayed in the request page. They are not added to saved history. You can copy or download a response explicitly.

## 3. Local processing and storage
## Sharing

- All processing performed by the Extension occurs locally within your browser.
- Any settings, preferences, or cached data used by the Extension (if any) are stored only in your browser's local storage or synchronized storage associated with your browser account.
- We do not have access to this locally stored information and do not retrieve it to our own servers.
Request exports include the request URL, headers, query parameters, body and GraphQL settings. Base64 encoding is not encryption. Copying an export to the clipboard and sharing it can disclose credentials or other information contained in that request.

## 4. No use of third-party services
## Permissions and controls

- The Extension does **not** use third-party analytics tools, advertising networks, or tracking pixels.
- The Extension does **not** embed third-party SDKs or scripts for the purpose of collecting user data.
- The Extension does **not** share any information with third parties, because no information is collected in the first place.
The extension uses the storage permission and asks for optional access to API hosts. It does not require access to browsing history or all open tabs.

## 5. Permissions
You can disable or clear request history, edit or delete favorites and environments, revoke host access in the browser, or uninstall the extension. Disabling history prevents new history entries but does not remove the separate last-request draft. Resetting preferences preserves favorites. Browser synchronization and API-server retention are controlled by their respective providers.

The Extension may request certain browser permissions that are technically required for its functionality (for example, access to specific sites, tabs, or storage).
Requesting a permission does **not** mean that personal data is collected; permissions are only used to provide the core features of the Extension and not for data harvesting or tracking, in accordance with Chrome Web Store and Microsoft Edge Add-ons policies.
## Policy updates and contact

## 6. Children's privacy

Because the Extension does not collect, store, or process any personal information, it does not knowingly collect information from children of any age.
If this changes in a future version, this Privacy Policy will be updated to reflect the applicable requirements for children's data protection.

## 7. User rights and controls

Since the Extension does not collect or store personal data on our servers, there is no personal data for us to access, modify, export, or delete on your behalf.
You may at any time:
- Disable the Extension from your browser's extensions/add-ons page.
- Uninstall the Extension, which will remove it and any associated local data stored by the browser.

For Chrome and Edge, you can manage or remove extensions from the respective extensions management pages provided by the browser.

## 8. International data transfers

Because we do not collect or store personal data, there are no international transfers of personal data related to the Extension.
If future versions begin to collect data, this section will be updated to describe any applicable transfers and safeguards.

## 9. Changes to this Privacy Policy

We may update this Privacy Policy from time to time, for example to reflect changes in:
- The functionality or behavior of the Extension.
- Applicable laws, regulations, or store policies for Chrome or Edge extensions.

When changes are made, the "Effective date" at the top of this document will be updated. Material changes will be reflected in the latest version of the policy made available via the Chrome Web Store and Microsoft Edge Add-ons listing pages.

## 10. Contact

If you have any questions or concerns about this Privacy Policy or the Extension, you can contact us at:

**Name:** Hayk Jomardyan
**Website:** https://github.com/jomardyan/Quick-API-Client
This file is updated when implemented data handling changes. For questions, contact Hayk Jomardyan through the [project repository](https://github.com/jomardyan/Quick-API-Client).
50 changes: 39 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,49 @@
# Quick API Client

A rapid API testing and debugging tool for developers.
A Chrome and Edge extension for testing REST and GraphQL APIs.

## Features

- Quick API testing
- Request/response visualization
- Multiple HTTP methods support
- HTTP methods, headers, repeated query parameters and raw request bodies
- GraphQL query and variables editor
- Named environments, favorites and local request history
- Cancellation, configurable timeouts and response timing
- Request sharing and code generation for eight targets
- JSON, XML, HTML and CSS parsing checks

## 👨‍💻 Author
## Install

**Hayk Jomardyan**
Download the latest versioned ZIP from `dist`, extract it, and use Load unpacked in your browser's extension management page with developer mode enabled. Grant access to the API host when sending a request. Use the open-in-tab control for requests that should remain visible while switching browser tabs.

- 🌐 Website: [lolino.pl](https://lolino.pl)
- 📧 Email: [hayk.jomardyan@outlook.com](mailto:hayk.jomardyan@outlook.com)
- 💼 GitHub: [@jomardyan](https://github.com/jomardyan)
## Development and verification

## License
Requires Node.js 22, npm, Bash, jq, zip and unzip.

MIT License - See LICENSE file for details
```sh
npm ci
npm run lint
npm test -- --runInBand
./release.sh --no-bump
npx playwright install chromium
npm run test:browser
```

The browser smoke test extracts the release ZIP and starts a local fixture server. Its temporary manifest pregrants only that fixture host. The distributed manifest continues to request optional permissions. The test exercises real extension messaging and HTTP requests. It requires an environment that permits Chromium processes and local sockets.

The release script validates source before bumping versions. It updates the manifest, package and lockfile together and includes all runtime modules. GitHub Actions runs lint, regression tests, packaging and the Chromium smoke test, then provides the current ZIP as an artifact.

## Operating limits

Request and decoded response bodies are limited to 5 MiB. Larger responses fail explicitly. JSON responses above 200,000 characters display as plain text to keep the interface responsive. Copy and download retain the raw response text. Responses are text-oriented and do not provide lossless binary downloads.

Timeouts range from 1 to 60 seconds. Chrome can terminate an extension service worker if response headers take over 30 seconds to arrive. The UI reports a disconnected background or recovers after the configured timeout plus five seconds. Failed or interrupted requests are never automatically retried because the server may already have processed them. See the [Chrome service worker lifecycle documentation](https://developer.chrome.com/docs/extensions/develop/concepts/service-workers/lifecycle).

Browser networking still controls restricted request headers, redirects and TLS validation. Cookies are omitted and the HTTP cache is bypassed. Closing the request page attempts to cancel its active request. Cancellation cannot reverse an action already performed by an API.

Favorites, settings and environments use browser sync storage and remain subject to its quota. History and the last request use local extension storage. Request exports contain the entered headers and body. See [PRIVACY_POLICY.md](PRIVACY_POLICY.md) for details.

## Author and license

Hayk Jomardyan - [GitHub](https://github.com/jomardyan)

The repository contains the Creative Commons Attribution-NoDerivatives 4.0 International license. See [LICENSE](LICENSE).
147 changes: 70 additions & 77 deletions background.js
Original file line number Diff line number Diff line change
@@ -1,97 +1,90 @@
const DEFAULT_TIMEOUT_MS = 15000;
const DEBUG_LOGGING = false;
const MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
const pendingRequests = new Map();

// Track in-flight requests so cancellation can abort them
const pendingRequests = new Map(); // requestId -> AbortController
async function readResponse(res) {
if (!res.body) return { body: "", bodyBytes: 0 };
const reader = res.body.getReader();
const decoder = new TextDecoder();
const chunks = [];
let bodyBytes = 0;
try {
for (;;) {
const { done, value } = await reader.read();
if (done) break;
bodyBytes += value.byteLength;
if (bodyBytes > MAX_RESPONSE_BYTES) {
await reader.cancel();
throw new Error("Response exceeds the 5 MiB limit");
}
chunks.push(decoder.decode(value, { stream: true }));
}
chunks.push(decoder.decode());
return { body: chunks.join(""), bodyBytes };
} finally {
reader.releaseLock();
}
}

chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
// Handle cancellation from popup
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
// Only extension pages may use the network bridge.
if (sender.id !== chrome.runtime.id || !sender.url?.startsWith(chrome.runtime.getURL(""))) return;
if (message?.type === "cancel-request") {
const controller = pendingRequests.get(message.payload?.requestId);
if (controller) {
controller.abort();
pendingRequests.delete(message.payload.requestId);
const request = pendingRequests.get(message.payload?.requestId);
if (request && request.owner === sender.url) {
request.cancelled = true;
request.controller.abort();
}
return; // no async response needed
sendResponse({ ok: true });
return;
}

if (message?.type !== "api-request") return;

const { url, method, headers, body, timeoutMs, requestId } = message.payload;
const effectiveTimeout =
typeof timeoutMs === "number" && timeoutMs > 0 ? timeoutMs : DEFAULT_TIMEOUT_MS;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), effectiveTimeout);

if (requestId) pendingRequests.set(requestId, controller);

const payload = message.payload;
let parsedUrl;
try {
if (!payload || typeof payload.requestId !== "string" || !payload.requestId || payload.requestId.length > 128) throw new Error("Invalid request ID");
if (pendingRequests.has(payload.requestId)) throw new Error("Request ID already in use");
if (pendingRequests.size >= 8) throw new Error("Too many active requests");
parsedUrl = new URL(payload.url);
if (!["http:", "https:"].includes(parsedUrl.protocol) || parsedUrl.username || parsedUrl.password) throw new Error("Only HTTP and HTTPS URLs without embedded credentials are supported");
if (!["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"].includes(payload.method)) throw new Error("Unsupported HTTP method");
if (payload.body != null && typeof payload.body !== "string") throw new Error("Invalid request body");
if (payload.body && new TextEncoder().encode(payload.body).length > MAX_RESPONSE_BYTES) throw new Error("Request exceeds the 5 MiB limit");
new Headers(payload.headers); // Validate before allocating request resources.
} catch (err) {
sendResponse({ ok: false, error: err.message });
return;
}
const { method, headers, body, requestId } = payload;
const timeoutMs = Number.isFinite(payload.timeoutMs) && payload.timeoutMs > 0
? Math.min(60000, Math.max(1000, payload.timeoutMs)) : DEFAULT_TIMEOUT_MS;
const request = { controller: new AbortController(), cancelled: false, owner: sender.url };
pendingRequests.set(requestId, request);
const timeout = setTimeout(() => request.controller.abort(), timeoutMs);
const started = performance.now();

(async () => {
try {
// Validate URL
try {
new URL(url);
} catch (err) {
sendResponse({
ok: false,
error: "Invalid URL format",
});
return;
}

const fetchOptions = {
method,
headers,
signal: controller.signal,
mode: 'cors',
credentials: 'omit'
};

if (body && !['GET', 'HEAD'].includes(method)) {
fetchOptions.body = body;
}

if (DEBUG_LOGGING) console.log("Fetching", url, fetchOptions);
const res = await fetch(url, fetchOptions);
const text = await res.text();
const elapsed = Math.round(performance.now() - started);
const headersEntries = Array.from(res.headers.entries());

if (DEBUG_LOGGING) console.log("Fetch success", res.status);
const options = { method, headers, signal: request.controller.signal, credentials: "omit", cache: "no-store" };
if (!["GET", "HEAD"].includes(method) && body != null) options.body = body;
const res = await fetch(parsedUrl.href, options);
const result = await readResponse(res);
sendResponse({
ok: true,
status: res.status,
statusText: res.statusText,
url: res.url,
type: res.type,
elapsed,
headers: headersEntries,
body: text,
ok: true, status: res.status, statusText: res.statusText, url: res.url,
type: res.type, elapsed: Math.round(performance.now() - started),
headers: Array.from(res.headers.entries()), ...result,
});
} catch (err) {
console.error('API request error:', err);
const elapsed = Math.round(performance.now() - started);
sendResponse({
ok: false,
error:
err.name === "AbortError"
? `Request timed out after ${Math.round(effectiveTimeout / 1000)}s`
: err.name === "TypeError" && err.message.includes("fetch")
? "Network error: Unable to reach the server. Check your connection or the URL."
: err.message || "Unknown error occurred",
elapsed,
ok: false, cancelled: request.cancelled,
error: request.cancelled ? "Request cancelled" : request.controller.signal.aborted
? `Request timed out after ${timeoutMs / 1000}s`
: err.message || "Network request failed",
elapsed: Math.round(performance.now() - started),
});
} finally {
clearTimeout(timeout);
if (requestId) pendingRequests.delete(requestId);
if (pendingRequests.get(requestId) === request) pendingRequests.delete(requestId);
}
})();

return true; // keep the message channel open for async response
return true;
});

// Log extension startup
if (DEBUG_LOGGING) {
console.log("Quick API Client: Background service worker started");
}
6 changes: 5 additions & 1 deletion defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@
function clampHistorySize(size) {
const num = Number(size);
if (!Number.isFinite(num)) return defaults.historySize;
return Math.max(0, Math.min(50, num));
return Math.max(0, Math.min(50, Math.floor(num)));
}

window.clampTimeoutMs = (value) => {
const number = Number(value);
return Number.isFinite(number) && number > 0 ? Math.max(1000, Math.min(60000, Math.round(number))) : defaults.timeoutMs;
};
window.DEFAULT_OPTIONS = defaults;
window.clampHistorySize = clampHistorySize;
})();
Binary file added dist/quick-api-client-v1.1.5.zip
Binary file not shown.
Loading
Loading