SharePointVault ~/gotchas/spfx ⌕ Search 404 pages, error messages, scripts…Ctrl K ★ Star
M↓ README.mdM↓ share-text-via-url-hits-length-limits.md
sharepoint-vault›gotchas›spfx›share-text-via-url-hits-length-limits.mdhistoryedit on GitHub
● GOTCHAspfx· 5 min read

Sharing text through a URL (Teams /share, mailto) breaks at length limits

applies to: SharePoint Online / SPFx / any web app with "share to Teams" or "share via e-mail" buttonsreviewed: 2026-09-01#spfx#teams#deeplink#mailto#sharing#aad#ui

A "Send to Teams" (teams.microsoft.com/share?msgText=…) or "Share via e-mail" (mailto:?body=…) button that puts the whole content in the URL works for short text and fails for long: Teams redirects to sign-in and dies with AADSTS90015: Requested query string is too long, and mailto just never opens the mail client. Cap the URL payload hard, put the full text on the clipboard, and for mailto use window.location.href, not window.open('_self').

Tlačítko „Poslat do Teams“ (teams.microsoft.com/share?msgText=…) nebo „Sdílet e-mailem“ (mailto:?body=…), které cpe celý obsah do URL, funguje na krátkém textu a na dlouhém padá: Teams přesměruje na přihlášení a skončí AADSTS90015: Requested query string is too long, mailto mailového klienta prostě neotevře. Payload v URL tvrdě omez, plný text dej do schránky a pro mailto použij window.location.href, ne window.open('_self').

Symptom

Below each answer/record you have share buttons. They work in testing (short content).

Then someone shares a long answer:

  • Send to Teams lands on the Microsoft sign-in page with AADSTS90015: Requested query string is too long. (Request Id / Correlation Id in the error — a dead giveaway it's the AAD sign-in endpoint, not Teams.)
  • Share via e-mail does nothing at all — no draft, no error, no console message.

Cause

Both buttons serialize the entire content into a URL, and each URL has a limit you blow past.

Teams /share → AAD query-string limit. The share launcher https://teams.microsoft.com/share?msgText=<text>&href=<url> is fine once you're signed into Teams web. If you are not signed in, the launcher redirects to login.microsoftonline.com, carrying the entire share URL — including your msgText — as a parameter of the sign-in request. AAD caps the query-string length and rejects it with AADSTS90015. Two things make this worse than it looks:

  • encodeURIComponent inflates non-ASCII text and newlines ~3× (á → %C3%A1, \n → %0A).
  • AAD then wraps the already-encoded share URL and encodes it again (double-encoding).

So a "safe-looking" 1,800-character cap on msgText still overflows. And because it depends on Teams-web sign-in state, it passes on the dev's machine and fails on the user's.

mailto → URL length limit + swallowed window.open. mailto:?subject=…&body=<text> has a practical URL length limit (~2,000 chars; varies by mail client and OS). A long body silently fails to launch the client — no error. On top of that, window.open('mailto:…', '_self') is sometimes swallowed in an SPFx page; the canonical trigger is assigning window.location.href.

Fix

Cap the URL payload hard, and put the full text on the clipboard as the real carrier:

// Teams — msgText must survive a *double*-encoded AAD redirect, so keep it tiny.
const TEAMS_CAP = 350;
let text = full;
if (full.length > TEAMS_CAP) {
  text = full.substring(0, TEAMS_CAP) + '…\n\n(Full text copied to your clipboard — paste it with Ctrl+V.)';
  void navigator.clipboard?.writeText(full);   // full content preserved
}
const url = 'https://teams.microsoft.com/share?msgText=' + encodeURIComponent(text)
  + (href ? '&href=' + encodeURIComponent(href) : '');
window.open(url, '_blank', 'noopener,noreferrer');   // sync in the click = no popup block

// mailto — single encode, but still a URL-length limit; and use location.href.
const MAIL_CAP = 700;
let body = full;
if (full.length > MAIL_CAP) {
  body = full.substring(0, MAIL_CAP) + '…\n\n(Full text copied to your clipboard — paste it with Ctrl+V.)';
  void navigator.clipboard?.writeText(full);
}
window.location.href = 'mailto:?subject=' + encodeURIComponent(subject) + '&body=' + encodeURIComponent(body);

Notes:

  • The clipboard is the payload, the URL is the launcher. For long content, the URL only carries a preview + a note; the full text rides the clipboard. mailto via location.href does not navigate the page away, so the async clipboard write still completes; window.open('_blank') opens a new tab and leaves your page alive too.
  • Pick the caps for the encoded worst case. ~350 for Teams (double-encoded, non-ASCII heavy), ~700 for mailto (single-encoded). If your content is ASCII-only you can go higher, but conservative caps never fail.
  • The real fix for unlimited send is a different channel — Graph chatMessage (post to a chat/channel) or sendMail — but those need a delegated scope and admin consent, so they are a feature, not a hotfix.
  • Nothing catches this at build time. The compiler, linter, and unit tests all pass; the limit is a runtime property of the URL and, for Teams, of the user's sign-in state. Test with a genuinely long payload.

Platform pitfalls beyond length

Two environment traps hit the same buttons, independent of length:

  • window.open is blocked inside the Teams desktop webview for external URLs — the button simply "does nothing" (only in the Windows Teams app; a normal browser is fine). Use an anchor-click (<a target="_blank"> appended to the DOM, .click(), removed) instead of window.open — Teams desktop and browsers both honor it.

  • mailto: launches the OS default mail handler — on Windows where the default browser is Chrome with no registered mail handler, it just opens the browser and creates no draft. Prefer a Graph draft (POST /me/messages → open the returned webLink in OWA/Outlook); it also has no URL length limit, so the cap and clipboard fallback disappear. Async caveat: opening webLink after the Graph call trips the popup blocker → pre-open a blank tab synchronously in the click (const w = window.open('', '_blank')) and set w.location.href in the .then. Fall back to mailto: (via location.href) only when Graph is unavailable (e.g. a mailbox-less account).

  • Do not add noopener to the pre-opened blank tab. window.open('', '_blank', 'noopener') returns null even when the tab opens fine — with noopener the browser deliberately withholds the handle. Two things break at once: you can no longer assign w.location.href, which was the whole point of pre-opening, and null stops meaning "popup blocked", so a genuinely blocked tab gets reported to the user as success. Open it as window.open('', '_blank') and clear the back-reference yourself before navigating (w.opener = null). This is the trap in the security instinct: hardening the call is exactly what breaks it, and it breaks it silently — the button reports success and nothing opens. Note w.opener = null is not fully identical to noopener (which can also isolate the browsing-context group), so the rule is: plain external link with nothing to fill in afterwards → keep noopener and do not pre-open at all; deferred navigation after an await → pre-open without noopener and null the opener.

Neither is caught by the compiler or linter — only a manual matrix (mobile/desktop × browser/Teams-app) reveals them.

How to verify

  1. Share a short answer to Teams and via e-mail — both open with the text pre-filled.
  2. Share a long answer (a few thousand characters). Teams opens (preview + "on clipboard" note) instead of AADSTS90015; the mail client opens instead of doing nothing. Paste (Ctrl+V) to confirm the full text is on the clipboard.
  3. For the Teams case specifically, test while signed out of Teams web in that browser profile — that is the path that triggers the AAD redirect.
Was this helpful?ask or add your experience on GitHub →
Saved you an hour? A star on GitHub helps other SharePoint people find the vault.★ Star on GitHub
last reviewed: 2026-09-01last updated: 2026-09-24historyedit on GitHubview source