SharePointVault ~/gotchas/rest-api ⌕ Search 404 pages, error messages, scripts…Ctrl K ★ Star
M↓ README.mdM↓ actions-must-precede-the-alias-query-string.md
sharepoint-vault›gotchas›rest-api›actions-must-precede-the-alias-query-string.mdhistoryedit on GitHub
● GOTCHArest-api· 3 min read

Actions must go before the alias query string, not after the URL

applies to: SharePoint Online, SharePoint Serverreviewed: 2026-08-30#rest-api#lists#permissions#spfx

Once you resolve a list with GetList(@u)?@u='...', the URL ends in a query string — so appending /breakroleinheritance(...) puts the action inside the parameter value. The request returns a shrug instead of an error, and whatever you thought you secured was never secured.

Jakmile seznam adresuješ přes GetList(@u)?@u='…', končí URL query stringem – a připojení /breakroleinheritance(…) tím vloží akci dovnitř hodnoty parametru. Požadavek neselže hlasitě, jen neudělá nic, a to, co sis myslel, že zabezpečuješ, zabezpečené není.

Symptom

You switched to resolving lists by URL (see Get lists by URL, not by title) and your permission bootstrap stopped working. No exception in the browser console beyond a warning you wrapped it in. The lists still inherit permissions from the web. Running the same break manually in the UI works fine.

Cause

The two helper shapes look interchangeable, but they are not:

by title:  /_api/web/lists/getbytitle('Docs')          ← path ends here
by URL:    /_api/web/GetList(@u)?@u='/sites/x/Lists/Docs'   ← query string

String-concatenating an action onto the second one produces this:

POST /_api/web/GetList(@u)?@u='/sites/x/Lists/Docs'/breakroleinheritance(copyRoleAssignments=false,clearSubscopes=true)

The server sees the resource GetList(@u) and an @u value of '/sites/x/Lists/Docs'/breakroleinheritance(...). The action is now data, not a path segment. Depending on the endpoint you get a 400, or — worse — a 2xx for a call that did nothing you wanted.

Everything after the ? is parameters. This bites every action segment equally:

  • /breakroleinheritance(...)
  • /resetroleinheritance
  • /roleassignments/addroleassignment(principalid=…,roledefid=…)
  • /roleassignments/removeroleassignment(principalid=…)
  • /items(1)/AttachmentFiles/add(FileName='x.pdf')

Why it hides so well

Three things conspire:

  1. Permission bootstraps are usually wrapped in try/catch — they must not break provisioning for a user who lacks ManagePermissions. That same catch swallows a malformed-URL failure.
  2. Failure looks like the normal no-op. A list that still inherits permissions is exactly what you'd see if the code had correctly decided there was nothing to do.
  3. It works from an admin account in the UI. Nobody re-checks the REST path once the feature "exists".

A codebase can carry this for a long time while doing it correctly elsewhere — the manual splice is easy to get right when you're thinking about it, and easy to forget when you're writing the third call in a loop.

Fix

Splice the action in before ?@u=:

// WRONG — action lands inside the parameter value
const url = `${listBase(webUrl, name)}/breakroleinheritance(copyRoleAssignments=false,clearSubscopes=true)`;

// RIGHT — action is a path segment, alias stays a parameter
const url = listBase(webUrl, name)
  .replace('?@u=', '/breakroleinheritance(copyRoleAssignments=false,clearSubscopes=true)?@u=');

Resulting in:

POST /_api/web/GetList(@u)/breakroleinheritance(copyRoleAssignments=false,clearSubscopes=true)?@u='/sites/x/Lists/Docs'

Do not leave this as a manual .replace() at each call site. That is precisely how the inconsistency arises — some calls get it, some don't. Give the alias form a helper that owns the splice, and make the plain base function the one you never concatenate onto:

/** List root — safe to GET, never concatenate an action onto this. */
export function listBase(webUrl: string, name: string): string {
  const alias = serverRelPath(webUrl) + '/Lists/' + name;
  return `${webUrl}/_api/web/GetList(@u)?@u='${alias}'`;
}

/** URL for an ACTION on the list. Inserts the segment before the query string. */
export function listActionUrl(webUrl: string, name: string, action: string): string {
  return listBase(webUrl, name).replace('?@u=', '/' + action + '?@u=');
}

How to check whether you already have the bug

Grep for a closing template-literal brace followed by a slash — that is concatenation onto a base URL:

grep -rnE "listBase\([^)]*\)\}/" src/

Then verify the intent actually took effect, rather than trusting the absence of errors:

GET /_api/web/GetList(@u)/HasUniqueRoleAssignments?@u='/sites/x/Lists/Docs'

false after a break you believe succeeded means the request never did what you think. Assert on state, not on the absence of an exception — a permission routine that only logs failures will report success forever.

Related

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-08-30last updated: 2026-09-24historyedit on GitHubview source