Skip to content

Add error handling and fallback for clipboard operations#8

Merged
tomiwa-a merged 4 commits intofeat/webfrom
copilot/sub-pr-5-another-one
Feb 7, 2026
Merged

Add error handling and fallback for clipboard operations#8
tomiwa-a merged 4 commits intofeat/webfrom
copilot/sub-pr-5-another-one

Conversation

Copy link
Contributor

Copilot AI commented Feb 7, 2026

Addresses unhandled promise rejections in clipboard write operations that fail in non-secure contexts, unsupported browsers, or when permissions are denied.

Changes

  • Error handling: Wrapped navigator.clipboard.writeText() in try-catch with proper rejection handling
  • Fallback mechanism: Implemented document.execCommand('copy') fallback for environments where Clipboard API is unavailable or fails
  • User feedback: Display "Failed!" message on copy failure instead of silent errors
  • Security fixes: Updated @modelcontextprotocol/sdk from 0.6.0 to 1.26.0 to address:
    • ReDoS vulnerability (CVE patched in 1.25.2)
    • DNS rebinding protection (CVE patched in 1.24.0)

Implementation

// Copy text to clipboard with fallback support
async function copyToClipboard(text: string): Promise<boolean> {
  if (navigator.clipboard && navigator.clipboard.writeText) {
    try {
      await navigator.clipboard.writeText(text);
      return true;
    } catch (err) {
      console.warn("Clipboard API failed, trying fallback method:", err);
      return fallbackCopyText(text);
    }
  }
  return fallbackCopyText(text);
}

Both copy button handlers now use the unified copyToClipboard() function and provide visual feedback for failures.


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI changed the title [WIP] Address feedback from review on original pull request Add error handling and fallback for clipboard operations Feb 7, 2026
Copilot AI requested a review from tomiwa-a February 7, 2026 20:41
@tomiwa-a tomiwa-a marked this pull request as ready for review February 7, 2026 20:41
Copilot AI review requested due to automatic review settings February 7, 2026 20:41
@tomiwa-a tomiwa-a merged commit 9dc8044 into feat/web Feb 7, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves clipboard copy UX in the website by handling Clipboard API failures and adding an execCommand('copy') fallback, and it upgrades @modelcontextprotocol/sdk to pick up security patches.

Changes:

  • Added copyToClipboard() with a legacy execCommand fallback and wired it into both copy button handlers.
  • Added explicit UI feedback for copy failures ("Failed!").
  • Updated @modelcontextprotocol/sdk dependency (and lockfile) to a patched 1.x version.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 4 comments.

File Description
package.json Fixes scripts JSON formatting by adding the missing comma after website:build.
package-lock.json Locks updated transitive dependency graph due to SDK upgrade and other minor bumps.
apps/website/src/components/preview.ts Introduces clipboard error handling + fallback copy logic and updates copy button handlers.
apps/mcp/package.json Bumps @modelcontextprotocol/sdk to 1.x range for security fixes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +64 to +86
copyBtn?.addEventListener("click", async () => {
const activePanel = document.querySelector(".preview-code.active");
if (!activePanel) return;

const text = activePanel.textContent || "";
const success = await copyToClipboard(text);

if (success) {
copyBtn.textContent = "Copied!";
copyBtn.classList.add("copied");
setTimeout(() => {
copyBtn.textContent = "Copy";
copyBtn.classList.remove("copied");
}, 2000);
} else {
copyBtn.textContent = "Failed!";
copyBtn.classList.add("error");
setTimeout(() => {
copyBtn.textContent = "Copy";
copyBtn.classList.remove("error");
}, 2000);
}
});
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copyBtn?.addEventListener(...) does not narrow the copyBtn type inside the async callback under strictNullChecks, so copyBtn.textContent / copyBtn.classList... will be flagged as possibly null. Wrap the listener registration in an if (copyBtn) block (or assign to a non-null local) so the callback closes over a non-null button reference.

Suggested change
copyBtn?.addEventListener("click", async () => {
const activePanel = document.querySelector(".preview-code.active");
if (!activePanel) return;
const text = activePanel.textContent || "";
const success = await copyToClipboard(text);
if (success) {
copyBtn.textContent = "Copied!";
copyBtn.classList.add("copied");
setTimeout(() => {
copyBtn.textContent = "Copy";
copyBtn.classList.remove("copied");
}, 2000);
} else {
copyBtn.textContent = "Failed!";
copyBtn.classList.add("error");
setTimeout(() => {
copyBtn.textContent = "Copy";
copyBtn.classList.remove("error");
}, 2000);
}
});
if (copyBtn) {
const nonNullCopyBtn = copyBtn;
nonNullCopyBtn.addEventListener("click", async () => {
const activePanel = document.querySelector(".preview-code.active");
if (!activePanel) return;
const text = activePanel.textContent || "";
const success = await copyToClipboard(text);
if (success) {
nonNullCopyBtn.textContent = "Copied!";
nonNullCopyBtn.classList.add("copied");
setTimeout(() => {
nonNullCopyBtn.textContent = "Copy";
nonNullCopyBtn.classList.remove("copied");
}, 2000);
} else {
nonNullCopyBtn.textContent = "Failed!";
nonNullCopyBtn.classList.add("error");
setTimeout(() => {
nonNullCopyBtn.textContent = "Copy";
nonNullCopyBtn.classList.remove("error");
}, 2000);
}
});
}

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +20
textArea.focus();
textArea.select();

try {
const successful = document.execCommand("copy");
document.body.removeChild(textArea);
return successful;
} catch (err) {
document.body.removeChild(textArea);
return false;
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fallbackCopyText removes the temporary textarea in both the try and catch blocks, but if an exception occurs before entering the try (e.g., during focus() / select()), the element won’t be cleaned up. Consider moving the focus/select into the try and using a finally block to guarantee removal.

Suggested change
textArea.focus();
textArea.select();
try {
const successful = document.execCommand("copy");
document.body.removeChild(textArea);
return successful;
} catch (err) {
document.body.removeChild(textArea);
return false;
try {
textArea.focus();
textArea.select();
const successful = document.execCommand("copy");
return successful;
} catch (err) {
return false;
} finally {
if (textArea.parentNode === document.body) {
document.body.removeChild(textArea);
}

Copilot uses AI. Check for mistakes.
Comment on lines +79 to +112
copyBtn.textContent = "Failed!";
copyBtn.classList.add("error");
setTimeout(() => {
copyBtn.textContent = "Copy";
copyBtn.classList.remove("error");
}, 2000);
}
});

// Quick start copy button
document.querySelectorAll(".qs-copy-btn").forEach((btn) => {
btn.addEventListener("click", () => {
btn.addEventListener("click", async () => {
const codeBlock =
btn.closest(".qs-code-window")?.querySelector("pre") ??
btn.parentElement?.querySelector("code");
if (!codeBlock) return;

const text = codeBlock.textContent || "";
navigator.clipboard.writeText(text).then(() => {
const success = await copyToClipboard(text);

if (success) {
(btn as HTMLElement).textContent = "Copied!";
btn.classList.add("copied");
setTimeout(() => {
(btn as HTMLElement).textContent = "Copy";
btn.classList.remove("copied");
}, 2000);
});
} else {
(btn as HTMLElement).textContent = "Failed!";
btn.classList.add("error");
setTimeout(() => {
(btn as HTMLElement).textContent = "Copy";
btn.classList.remove("error");
}, 2000);
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new failure path adds an error CSS class, but there doesn’t appear to be any .error styling in the website CSS. Either add styling for the error state (e.g., red text/border) or remove the class to avoid dead/unused UI states.

Copilot uses AI. Check for mistakes.
"@modelcontextprotocol/sdk": "^0.6.0",
"@modelcontextprotocol/sdk": "^1.25.2",
"dotenv": "^16.3.1",
"zod": "^3.22.4"
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@modelcontextprotocol/sdk@^1.25 declares a peer dependency on zod of ^3.25 || ^4.0 (and also depends on the same range). This package.json currently allows installing zod versions <3.25 via ^3.22.4, which can lead to peer-dep warnings or runtime incompatibilities if a lockfile changes. Update the declared zod range to at least ^3.25.0 (or move to ^4).

Suggested change
"zod": "^3.22.4"
"zod": "^3.25.0"

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants