From 5b6fd4807485ea3745d4f6ca542525237d554b68 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 25 Nov 2025 14:10:34 +0000 Subject: [PATCH 1/3] Initial plan From 0b33783f7966d9266811ae7ee7f9728df428263d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 25 Nov 2025 14:17:03 +0000 Subject: [PATCH 2/3] Add REST API Tester Chrome extension with Manifest V3 Co-authored-by: freddycodes23 <123166882+freddycodes23@users.noreply.github.com> --- README.md | 61 +++++- background.js | 38 ++++ icons/icon128.png | Bin 0 -> 3354 bytes icons/icon16.png | Bin 0 -> 427 bytes icons/icon48.png | Bin 0 -> 1297 bytes manifest.json | 29 +++ popup.css | 455 ++++++++++++++++++++++++++++++++++++++++++ popup.html | 123 ++++++++++++ popup.js | 497 ++++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 1201 insertions(+), 2 deletions(-) create mode 100644 background.js create mode 100644 icons/icon128.png create mode 100644 icons/icon16.png create mode 100644 icons/icon48.png create mode 100644 manifest.json create mode 100644 popup.css create mode 100644 popup.html create mode 100644 popup.js diff --git a/README.md b/README.md index c46c989..b6752eb 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,59 @@ -# REST-API-TESTER -A lightweight browser extension that allows you to test REST API endpoints directly from your browser. Toggle between JSON and FORM Data, view formatted responses with status codes and timing and save your frequently-used endpoints for quick access. Send custom headers. +# REST API Tester + +A lightweight Chrome extension that allows you to test REST API endpoints directly from your browser. + +## Features + +- **HTTP Methods**: Support for GET, POST, PUT, PATCH, and DELETE requests +- **Request Body**: Toggle between JSON and Form Data formats +- **Custom Headers**: Add and manage custom request headers +- **Response Viewer**: View formatted responses with syntax highlighting +- **Status Codes**: See HTTP status codes with color-coded badges +- **Response Timing**: Track response times in milliseconds +- **Save Endpoints**: Store frequently-used endpoints for quick access + +## Installation + +1. Clone this repository or download the source code +2. Open Chrome and navigate to `chrome://extensions/` +3. Enable "Developer mode" in the top right corner +4. Click "Load unpacked" and select the extension directory +5. The REST API Tester icon will appear in your browser toolbar + +## Usage + +1. Click the REST API Tester icon in your browser toolbar +2. Enter the URL of the API endpoint you want to test +3. Select the HTTP method (GET, POST, PUT, PATCH, DELETE) +4. Optionally add headers and request body +5. Click "Send" to make the request +6. View the response with status code and timing + +### Saving Endpoints + +1. After configuring your request, click "Save Current" +2. Enter a name for the endpoint +3. The endpoint will appear in the saved endpoints list +4. Click on a saved endpoint to load it + +## Tech Stack + +- **Manifest V3**: Chrome extension configuration +- **Vanilla JavaScript**: No frameworks required +- **Chrome Storage API**: For storing saved endpoints +- **Fetch API**: For making HTTP requests + +## Architecture + +``` +├── manifest.json # Extension configuration +├── background.js # Service worker for window management +├── popup.html # Main UI structure +├── popup.css # UI styling +├── popup.js # Request builder and response viewer logic +└── icons/ # Extension icons +``` + +## License + +MIT License diff --git a/background.js b/background.js new file mode 100644 index 0000000..13a65d2 --- /dev/null +++ b/background.js @@ -0,0 +1,38 @@ +// Background service worker for REST API Tester extension +// Handles window management for the extension + +let popupWindowId = null; + +// Listen for extension icon click to manage popup window +chrome.action.onClicked.addListener(async () => { + // Check if popup window already exists + if (popupWindowId !== null) { + try { + const window = await chrome.windows.get(popupWindowId); + // If window exists, focus it + await chrome.windows.update(popupWindowId, { focused: true }); + return; + } catch (e) { + // Window no longer exists, reset the ID + popupWindowId = null; + } + } + + // Create a new popup window + const window = await chrome.windows.create({ + url: chrome.runtime.getURL('popup.html'), + type: 'popup', + width: 800, + height: 700, + focused: true + }); + + popupWindowId = window.id; +}); + +// Track when the popup window is closed +chrome.windows.onRemoved.addListener((windowId) => { + if (windowId === popupWindowId) { + popupWindowId = null; + } +}); diff --git a/icons/icon128.png b/icons/icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..10cadbb63afc60ab3f81901b8330ea829fdea070 GIT binary patch literal 3354 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 QK0Cl+J7-+gLI0%x0S`-&nE(I) literal 0 HcmV?d00001 diff --git a/icons/icon16.png b/icons/icon16.png new file mode 100644 index 0000000000000000000000000000000000000000..7bc39f45c59d777b1c2d3bec3d931989f37529b9 GIT binary patch literal 427 zcmV;c0aX5pP)37A!mfPhjgQ6k$rp zAPc2<0c%g-0Yr*Y!$K*HY?w)7YGyRo!gc2im&R<)x6?VFzJDYhkB2fDV-1lRpxs3P zKs-fL0q^||SN5JBPPZFOvtu2Rc_30F3`i8kq-A&Jq$4)^_z!3Q4uT75RWJN`N=)f+ zsi%+9z)(%_m#Qk~#wK1?b|?;y{ebgR7Wv6Y73_+j^w{RrCC#gIWEPVYM#cl3r=4dX zt|ZW$La7VvjU}=R>y!o@9}XYo2p_#sb}KLl>i5A2?0lGN4cI_~i_R$7g(UW70`ofc zqx1K~h7Oko9kPin?BpV$%X8kldnt@fc(Awg*$Kw&jc;n`v87O?d1bcBxokx=N3EMz zPujZ1-JrGqimm@szP(EnLy{3Tl)xtB^>JmbJi=f8t z#0T)!{!u+}bzpXOav4BECiB|_9s~!Hso*!ztpNl8s;U~lMsSB+>_d1nW@}~G;7V*h zDw>))4ZZC?WIBK~|2|thZZtpkIh1 zSJSRz?C@Y+LZwt2r3jyXkDI>xD%Tb7>1X-4!*cTSVh{sm`kY4QmIe;NV=vr z_emIS9i$=+=;LGF)yeW!uBJ)V2QhBRtpCvYe+do6X{$|g!AK2|V^z``E{f17e}=^bM! z5oBV%sw8+Q}41?}ROhiCh`I26!on9e>oBR!%A0*NJ2{!iR>}GZKngztnr^6&S6z zInlUsYL{j*ODIq7zDGpE-usoc!%gTeDL&?UZCEFL>35+5$&IZxZu-*hLS7P&F`EtV z{kxFQGjFbY3B)`d`!w@eTamz&tK)*qZ|#Q7`Om!<^AaJ>Iy&}u<^h2~jYw&zPl3dC z%DmYx0`kki>gvKuMR>0+psO&3zc#G3Ur9Cm*Uaci32=EX^d_k2c33vqmwyo2vyeLn zqJ|z=dmI=k8q^2y3i;ULbaV=`|I`C4FaMd`Q?u`LL#^!q@u2mYU*MHg!$|GX#hLSy zo)ID~`}b**r_*p)z^y=dOW+k(ln`?QF)9Llad~=V$Uf?S3;MjL2" + ], + "action": { + "default_popup": "popup.html", + "default_icon": { + "16": "icons/icon16.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + }, + "default_title": "REST API Tester" + }, + "icons": { + "16": "icons/icon16.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + }, + "background": { + "service_worker": "background.js" + } +} diff --git a/popup.css b/popup.css new file mode 100644 index 0000000..9901bd1 --- /dev/null +++ b/popup.css @@ -0,0 +1,455 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; + background-color: #1a1a2e; + color: #eaeaea; + min-width: 600px; + min-height: 500px; +} + +.container { + padding: 20px; + max-width: 800px; + margin: 0 auto; +} + +header { + margin-bottom: 20px; + text-align: center; +} + +header h1 { + font-size: 1.5rem; + color: #00d4ff; +} + +/* Buttons */ +.btn { + padding: 8px 16px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; + transition: background-color 0.2s, opacity 0.2s; +} + +.btn:hover { + opacity: 0.9; +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn-primary { + background-color: #00d4ff; + color: #1a1a2e; + font-weight: 600; +} + +.btn-secondary { + background-color: #3a3a5a; + color: #eaeaea; +} + +.btn-add { + background-color: transparent; + color: #00d4ff; + border: 1px dashed #00d4ff; + margin-top: 8px; +} + +.btn-remove { + background-color: #ff4757; + color: white; + padding: 4px 8px; + font-size: 1rem; +} + +/* Saved Endpoints */ +.saved-endpoints { + margin-bottom: 20px; + padding: 15px; + background-color: #16213e; + border-radius: 8px; +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} + +.section-header h2 { + font-size: 1rem; + color: #00d4ff; +} + +.endpoints-list { + display: flex; + flex-wrap: wrap; + gap: 8px; + max-height: 100px; + overflow-y: auto; +} + +.endpoint-item { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + background-color: #3a3a5a; + border-radius: 20px; + font-size: 0.85rem; + cursor: pointer; + transition: background-color 0.2s; +} + +.endpoint-item:hover { + background-color: #4a4a6a; +} + +.endpoint-item .method-badge { + font-size: 0.7rem; + padding: 2px 6px; + border-radius: 3px; + font-weight: 600; +} + +.endpoint-item .delete-endpoint { + background: none; + border: none; + color: #ff4757; + cursor: pointer; + padding: 0 4px; + font-size: 1rem; +} + +/* Method badge colors */ +.method-GET { background-color: #00c853; color: #1a1a2e; } +.method-POST { background-color: #2196f3; color: white; } +.method-PUT { background-color: #ff9800; color: #1a1a2e; } +.method-PATCH { background-color: #9c27b0; color: white; } +.method-DELETE { background-color: #f44336; color: white; } + +/* Request Builder */ +.request-builder { + margin-bottom: 20px; +} + +.request-line { + display: flex; + gap: 10px; + margin-bottom: 15px; +} + +.request-line select { + padding: 10px 15px; + border: none; + border-radius: 4px; + background-color: #16213e; + color: #eaeaea; + font-size: 0.9rem; + cursor: pointer; +} + +.request-line input { + flex: 1; + padding: 10px 15px; + border: 2px solid #3a3a5a; + border-radius: 4px; + background-color: #16213e; + color: #eaeaea; + font-size: 0.9rem; +} + +.request-line input:focus { + outline: none; + border-color: #00d4ff; +} + +/* Collapsible Sections */ +.collapsible-section { + margin-bottom: 10px; + border: 1px solid #3a3a5a; + border-radius: 4px; + overflow: hidden; +} + +.section-toggle { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 15px; + background-color: #16213e; + cursor: pointer; + user-select: none; +} + +.section-toggle:hover { + background-color: #1f2b4a; +} + +.toggle-icon { + transition: transform 0.2s; +} + +.toggle-icon.collapsed { + transform: rotate(-90deg); +} + +.section-content { + padding: 15px; + background-color: #0f0f23; +} + +.section-content.collapsed { + display: none; +} + +/* Headers */ +.header-row, .form-data-row { + display: flex; + gap: 10px; + margin-bottom: 8px; +} + +.header-row input, .form-data-row input { + flex: 1; + padding: 8px 12px; + border: 1px solid #3a3a5a; + border-radius: 4px; + background-color: #16213e; + color: #eaeaea; + font-size: 0.85rem; +} + +.header-row input:focus, .form-data-row input:focus { + outline: none; + border-color: #00d4ff; +} + +/* Body */ +.body-type-selector { + display: flex; + gap: 20px; + margin-bottom: 15px; +} + +.body-type-selector label { + display: flex; + align-items: center; + gap: 6px; + cursor: pointer; +} + +.body-type-selector input[type="radio"] { + accent-color: #00d4ff; +} + +#bodyInput { + width: 100%; + min-height: 120px; + padding: 12px; + border: 1px solid #3a3a5a; + border-radius: 4px; + background-color: #16213e; + color: #eaeaea; + font-family: 'Monaco', 'Menlo', 'Courier New', monospace; + font-size: 0.85rem; + resize: vertical; +} + +#bodyInput:focus { + outline: none; + border-color: #00d4ff; +} + +/* Response Section */ +.response-section { + padding: 15px; + background-color: #16213e; + border-radius: 8px; +} + +.response-header { + display: flex; + align-items: center; + gap: 15px; + margin-bottom: 15px; +} + +.response-header h2 { + font-size: 1rem; + color: #00d4ff; +} + +.status-badge { + padding: 4px 12px; + border-radius: 20px; + font-size: 0.85rem; + font-weight: 600; +} + +.status-badge.success { + background-color: #00c853; + color: #1a1a2e; +} + +.status-badge.redirect { + background-color: #ff9800; + color: #1a1a2e; +} + +.status-badge.client-error { + background-color: #ff4757; + color: white; +} + +.status-badge.server-error { + background-color: #9c27b0; + color: white; +} + +.response-time { + font-size: 0.85rem; + color: #888; +} + +.response-body { + background-color: #0f0f23; + border-radius: 4px; + padding: 15px; + overflow: auto; + max-height: 300px; +} + +.response-body pre { + margin: 0; + white-space: pre-wrap; + word-wrap: break-word; +} + +.response-body code { + font-family: 'Monaco', 'Menlo', 'Courier New', monospace; + font-size: 0.85rem; + color: #eaeaea; +} + +/* Loader */ +.loader { + display: flex; + align-items: center; + gap: 10px; + padding: 20px; + color: #888; +} + +.spinner { + width: 20px; + height: 20px; + border: 2px solid #3a3a5a; + border-top-color: #00d4ff; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* Error Message */ +.error-message { + padding: 12px; + background-color: rgba(255, 71, 87, 0.2); + border: 1px solid #ff4757; + border-radius: 4px; + color: #ff4757; + margin-bottom: 15px; +} + +/* Modal */ +.modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.7); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.modal-content { + background-color: #16213e; + padding: 25px; + border-radius: 8px; + min-width: 300px; +} + +.modal-content h3 { + margin-bottom: 15px; + color: #00d4ff; +} + +.modal-content input { + width: 100%; + padding: 10px 15px; + border: 1px solid #3a3a5a; + border-radius: 4px; + background-color: #0f0f23; + color: #eaeaea; + font-size: 0.9rem; + margin-bottom: 20px; +} + +.modal-content input:focus { + outline: none; + border-color: #00d4ff; +} + +.modal-actions { + display: flex; + justify-content: flex-end; + gap: 10px; +} + +/* Utility Classes */ +.hidden { + display: none !important; +} + +/* Scrollbar styling */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: #0f0f23; +} + +::-webkit-scrollbar-thumb { + background: #3a3a5a; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #4a4a6a; +} + +/* JSON Syntax Highlighting */ +.json-key { color: #ff79c6; } +.json-string { color: #f1fa8c; } +.json-number { color: #bd93f9; } +.json-boolean { color: #ff5555; } +.json-null { color: #6272a4; } diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..7c645f2 --- /dev/null +++ b/popup.html @@ -0,0 +1,123 @@ + + + + + + REST API Tester + + + +
+
+

REST API Tester

+
+ + +
+
+

Saved Endpoints

+ +
+
+
+ + +
+
+ + + +
+ + +
+
+ Headers + +
+
+
+
+ + + +
+
+ +
+
+ + +
+
+ Body + +
+
+
+ + + +
+ + +
+
+
+ + +
+
+

Response

+ + +
+ + +
+
Send a request to see the response here
+
+
+
+ + + + + + + diff --git a/popup.js b/popup.js new file mode 100644 index 0000000..a8ccba8 --- /dev/null +++ b/popup.js @@ -0,0 +1,497 @@ +// REST API Tester - Main UI Logic + +// DOM Elements +const httpMethod = document.getElementById('httpMethod'); +const urlInput = document.getElementById('urlInput'); +const sendBtn = document.getElementById('sendBtn'); +const headersToggle = document.getElementById('headersToggle'); +const headersContent = document.getElementById('headersContent'); +const headersContainer = document.getElementById('headersContainer'); +const addHeaderBtn = document.getElementById('addHeaderBtn'); +const bodyToggle = document.getElementById('bodyToggle'); +const bodyContent = document.getElementById('bodyContent'); +const bodyTypeRadios = document.querySelectorAll('input[name="bodyType"]'); +const bodyInputContainer = document.getElementById('bodyInputContainer'); +const bodyInput = document.getElementById('bodyInput'); +const formDataContainer = document.getElementById('formDataContainer'); +const formDataFields = document.getElementById('formDataFields'); +const addFormFieldBtn = document.getElementById('addFormFieldBtn'); +const responseStatus = document.getElementById('responseStatus'); +const responseTime = document.getElementById('responseTime'); +const responseLoader = document.getElementById('responseLoader'); +const responseError = document.getElementById('responseError'); +const responseBody = document.getElementById('responseBody'); +const responseContent = document.getElementById('responseContent'); +const savedEndpointsList = document.getElementById('savedEndpointsList'); +const saveEndpointBtn = document.getElementById('saveEndpointBtn'); +const saveModal = document.getElementById('saveModal'); +const endpointNameInput = document.getElementById('endpointName'); +const cancelSaveBtn = document.getElementById('cancelSaveBtn'); +const confirmSaveBtn = document.getElementById('confirmSaveBtn'); + +// Initialize the application +document.addEventListener('DOMContentLoaded', () => { + loadSavedEndpoints(); + setupEventListeners(); +}); + +// Setup all event listeners +function setupEventListeners() { + // Toggle sections + headersToggle.addEventListener('click', () => toggleSection(headersToggle, headersContent)); + bodyToggle.addEventListener('click', () => toggleSection(bodyToggle, bodyContent)); + + // Add header row + addHeaderBtn.addEventListener('click', addHeaderRow); + + // Add form data field + addFormFieldBtn.addEventListener('click', addFormDataRow); + + // Body type change + bodyTypeRadios.forEach(radio => { + radio.addEventListener('change', handleBodyTypeChange); + }); + + // Send request + sendBtn.addEventListener('click', sendRequest); + + // Enter key on URL input + urlInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + sendRequest(); + } + }); + + // Save endpoint modal + saveEndpointBtn.addEventListener('click', openSaveModal); + cancelSaveBtn.addEventListener('click', closeSaveModal); + confirmSaveBtn.addEventListener('click', saveEndpoint); + + // Initial header row remove button + setupRemoveButtons(); +} + +// Toggle collapsible section +function toggleSection(toggle, content) { + const icon = toggle.querySelector('.toggle-icon'); + icon.classList.toggle('collapsed'); + content.classList.toggle('collapsed'); +} + +// Add new header row +function addHeaderRow() { + const row = document.createElement('div'); + row.className = 'header-row'; + row.innerHTML = ` + + + + `; + headersContainer.appendChild(row); + setupRemoveButtons(); +} + +// Add new form data row +function addFormDataRow() { + const row = document.createElement('div'); + row.className = 'form-data-row'; + row.innerHTML = ` + + + + `; + formDataFields.appendChild(row); + setupRemoveButtons(); +} + +// Setup remove buttons for headers and form data +function setupRemoveButtons() { + document.querySelectorAll('.btn-remove').forEach(btn => { + btn.onclick = function() { + const row = this.parentElement; + const container = row.parentElement; + // Keep at least one row + if (container.children.length > 1) { + row.remove(); + } else { + // Clear the inputs instead of removing + row.querySelectorAll('input').forEach(input => input.value = ''); + } + }; + }); +} + +// Handle body type change +function handleBodyTypeChange(e) { + const type = e.target.value; + bodyInputContainer.classList.add('hidden'); + formDataContainer.classList.add('hidden'); + + if (type === 'json') { + bodyInputContainer.classList.remove('hidden'); + bodyInput.placeholder = '{\n "key": "value"\n}'; + } else if (type === 'form') { + formDataContainer.classList.remove('hidden'); + } +} + +// Get headers from the form +function getHeaders() { + const headers = {}; + document.querySelectorAll('.header-row').forEach(row => { + const key = row.querySelector('.header-key').value.trim(); + const value = row.querySelector('.header-value').value.trim(); + if (key && value) { + headers[key] = value; + } + }); + return headers; +} + +// Get body content based on type +function getBody() { + const bodyType = document.querySelector('input[name="bodyType"]:checked').value; + + if (bodyType === 'none') { + return null; + } + + if (bodyType === 'json') { + const jsonBody = bodyInput.value.trim(); + return jsonBody || null; + } + + if (bodyType === 'form') { + const formData = new URLSearchParams(); + document.querySelectorAll('.form-data-row').forEach(row => { + const key = row.querySelector('.form-key').value.trim(); + const value = row.querySelector('.form-value').value.trim(); + if (key) { + formData.append(key, value); + } + }); + return formData.toString() || null; + } + + return null; +} + +// Get content type based on body type +function getContentType() { + const bodyType = document.querySelector('input[name="bodyType"]:checked').value; + if (bodyType === 'json') { + return 'application/json'; + } + if (bodyType === 'form') { + return 'application/x-www-form-urlencoded'; + } + return null; +} + +// Send the HTTP request +async function sendRequest() { + const url = urlInput.value.trim(); + + if (!url) { + showError('Please enter a URL'); + return; + } + + // Validate URL + try { + new URL(url); + } catch (e) { + showError('Please enter a valid URL'); + return; + } + + // Show loader + responseLoader.classList.remove('hidden'); + responseError.classList.add('hidden'); + responseStatus.classList.add('hidden'); + responseTime.classList.add('hidden'); + responseContent.textContent = ''; + sendBtn.disabled = true; + + const method = httpMethod.value; + const headers = getHeaders(); + const body = getBody(); + const contentType = getContentType(); + + if (contentType) { + headers['Content-Type'] = contentType; + } + + const options = { + method, + headers + }; + + // Only add body for methods that support it + if (['POST', 'PUT', 'PATCH'].includes(method) && body) { + options.body = body; + } + + const startTime = performance.now(); + + try { + const response = await fetch(url, options); + const endTime = performance.now(); + const duration = Math.round(endTime - startTime); + + // Hide loader + responseLoader.classList.add('hidden'); + sendBtn.disabled = false; + + // Show status + showStatus(response.status, response.statusText); + showResponseTime(duration); + + // Get response body + const contentType = response.headers.get('content-type'); + let responseText; + + if (contentType && contentType.includes('application/json')) { + const json = await response.json(); + responseText = formatJSON(json); + } else { + responseText = await response.text(); + } + + responseContent.innerHTML = responseText; + responseBody.classList.remove('hidden'); + + } catch (error) { + // Hide loader + responseLoader.classList.add('hidden'); + sendBtn.disabled = false; + + showError(`Request failed: ${error.message}`); + } +} + +// Show status badge +function showStatus(status, statusText) { + responseStatus.textContent = `${status} ${statusText}`; + responseStatus.classList.remove('hidden', 'success', 'redirect', 'client-error', 'server-error'); + + if (status >= 200 && status < 300) { + responseStatus.classList.add('success'); + } else if (status >= 300 && status < 400) { + responseStatus.classList.add('redirect'); + } else if (status >= 400 && status < 500) { + responseStatus.classList.add('client-error'); + } else if (status >= 500) { + responseStatus.classList.add('server-error'); + } +} + +// Show response time +function showResponseTime(duration) { + responseTime.textContent = `${duration}ms`; + responseTime.classList.remove('hidden'); +} + +// Show error message +function showError(message) { + responseError.textContent = message; + responseError.classList.remove('hidden'); + responseLoader.classList.add('hidden'); +} + +// Format JSON with syntax highlighting +function formatJSON(json) { + const jsonString = JSON.stringify(json, null, 2); + return jsonString + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?)/g, (match) => { + let cls = 'json-string'; + if (/:$/.test(match)) { + cls = 'json-key'; + } + return `${match}`; + }) + .replace(/\b(true|false)\b/g, '$1') + .replace(/\bnull\b/g, 'null') + .replace(/\b(-?\d+\.?\d*)\b/g, '$1'); +} + +// Load saved endpoints from Chrome storage +function loadSavedEndpoints() { + if (typeof chrome !== 'undefined' && chrome.storage) { + chrome.storage.local.get(['savedEndpoints'], (result) => { + const endpoints = result.savedEndpoints || []; + renderSavedEndpoints(endpoints); + }); + } +} + +// Render saved endpoints list +function renderSavedEndpoints(endpoints) { + savedEndpointsList.innerHTML = ''; + + if (endpoints.length === 0) { + savedEndpointsList.innerHTML = 'No saved endpoints'; + return; + } + + endpoints.forEach((endpoint, index) => { + const item = document.createElement('div'); + item.className = 'endpoint-item'; + item.innerHTML = ` + ${endpoint.method} + ${endpoint.name} + + `; + + // Load endpoint on click (except delete button) + item.addEventListener('click', (e) => { + if (!e.target.classList.contains('delete-endpoint')) { + loadEndpoint(endpoint); + } + }); + + // Delete endpoint + item.querySelector('.delete-endpoint').addEventListener('click', (e) => { + e.stopPropagation(); + deleteEndpoint(index); + }); + + savedEndpointsList.appendChild(item); + }); +} + +// Load endpoint into the form +function loadEndpoint(endpoint) { + httpMethod.value = endpoint.method; + urlInput.value = endpoint.url; + + // Load headers + headersContainer.innerHTML = ''; + if (endpoint.headers && Object.keys(endpoint.headers).length > 0) { + Object.entries(endpoint.headers).forEach(([key, value]) => { + const row = document.createElement('div'); + row.className = 'header-row'; + row.innerHTML = ` + + + + `; + headersContainer.appendChild(row); + }); + } else { + addHeaderRow(); + } + + // Load body type and content + const bodyType = endpoint.bodyType || 'none'; + document.querySelector(`input[name="bodyType"][value="${bodyType}"]`).checked = true; + + bodyInputContainer.classList.add('hidden'); + formDataContainer.classList.add('hidden'); + + if (bodyType === 'json') { + bodyInputContainer.classList.remove('hidden'); + bodyInput.value = endpoint.body || ''; + } else if (bodyType === 'form') { + formDataContainer.classList.remove('hidden'); + formDataFields.innerHTML = ''; + if (endpoint.formData && endpoint.formData.length > 0) { + endpoint.formData.forEach(({ key, value }) => { + const row = document.createElement('div'); + row.className = 'form-data-row'; + row.innerHTML = ` + + + + `; + formDataFields.appendChild(row); + }); + } else { + addFormDataRow(); + } + } + + setupRemoveButtons(); +} + +// Open save modal +function openSaveModal() { + const url = urlInput.value.trim(); + if (!url) { + showError('Please enter a URL first'); + return; + } + endpointNameInput.value = ''; + saveModal.classList.remove('hidden'); + endpointNameInput.focus(); +} + +// Close save modal +function closeSaveModal() { + saveModal.classList.add('hidden'); +} + +// Save endpoint to Chrome storage +function saveEndpoint() { + const name = endpointNameInput.value.trim(); + if (!name) { + return; + } + + const bodyType = document.querySelector('input[name="bodyType"]:checked').value; + const formData = []; + + if (bodyType === 'form') { + document.querySelectorAll('.form-data-row').forEach(row => { + const key = row.querySelector('.form-key').value.trim(); + const value = row.querySelector('.form-value').value.trim(); + if (key) { + formData.push({ key, value }); + } + }); + } + + const endpoint = { + name, + method: httpMethod.value, + url: urlInput.value.trim(), + headers: getHeaders(), + bodyType, + body: bodyType === 'json' ? bodyInput.value : '', + formData + }; + + if (typeof chrome !== 'undefined' && chrome.storage) { + chrome.storage.local.get(['savedEndpoints'], (result) => { + const endpoints = result.savedEndpoints || []; + endpoints.push(endpoint); + chrome.storage.local.set({ savedEndpoints: endpoints }, () => { + renderSavedEndpoints(endpoints); + closeSaveModal(); + }); + }); + } +} + +// Delete endpoint from Chrome storage +function deleteEndpoint(index) { + if (typeof chrome !== 'undefined' && chrome.storage) { + chrome.storage.local.get(['savedEndpoints'], (result) => { + const endpoints = result.savedEndpoints || []; + endpoints.splice(index, 1); + chrome.storage.local.set({ savedEndpoints: endpoints }, () => { + renderSavedEndpoints(endpoints); + }); + }); + } +} + +// Escape HTML to prevent XSS +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} From a1ca67dec35993903c04c6697f8d0a002e1a1e20 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 25 Nov 2025 14:19:34 +0000 Subject: [PATCH 3/3] Fix XSS vulnerability and add JSON parse error handling Co-authored-by: freddycodes23 <123166882+freddycodes23@users.noreply.github.com> --- popup.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/popup.js b/popup.js index a8ccba8..54bdd27 100644 --- a/popup.js +++ b/popup.js @@ -250,15 +250,28 @@ async function sendRequest() { // Get response body const contentType = response.headers.get('content-type'); let responseText; + let isFormattedJson = false; if (contentType && contentType.includes('application/json')) { - const json = await response.json(); - responseText = formatJSON(json); + try { + const json = await response.json(); + responseText = formatJSON(json); + isFormattedJson = true; + } catch (parseError) { + // If JSON parsing fails, treat as plain text + responseText = await response.text(); + } } else { responseText = await response.text(); } - responseContent.innerHTML = responseText; + // Use innerHTML only for our formatted JSON (which is safely escaped in formatJSON) + // Use textContent for plain text to prevent XSS + if (isFormattedJson) { + responseContent.innerHTML = responseText; + } else { + responseContent.textContent = responseText; + } responseBody.classList.remove('hidden'); } catch (error) {