SharePointVault ~/gotchas/rest-api ⌕ Search 404 pages, error messages, scripts…Ctrl K ★ Star
M↓ README.mdM↓ hubsites-returns-empty-list-not-403.md
sharepoint-vault›gotchas›rest-api›hubsites-returns-empty-list-not-403.mdhistoryedit on GitHub
● GOTCHArest-api· 3 min read

/_api/HubSites returns an empty list (HTTP 200) when the caller cannot see the hub sites

applies to: SharePoint Onlinereviewed: 2026-08-29#rest-api#hub-sites#permissions#governance#diagnostics

GET /_api/HubSites is security-trimmed: it lists only hubs whose hub site the caller may read. Missing permission shows up as value: [] with HTTP 200, not as 403 — so "the tenant has no hubs" and "this account sees no hubs" are indistinguishable from the response alone. Never derive "this site has no hub" or "nothing is inherited from a hub" from an empty list.

GET /_api/HubSites je ořezaný podle oprávnění: vrátí jen huby, na jejichž web volající vidí. Chybějící právo se projeví jako value: [] se stavem 200, ne jako 403 – z odpovědi samotné tedy nerozlišíš „tenant huby nemá“ od „tenhle účet na ně nevidí“. Z prázdného seznamu nikdy nevyvozuj, že web žádný hub nemá.

Symptom

A governance view tells you a site is not connected to any hub and therefore "inherits no owner/steward from a hub". The same view, opened by a different account on the same tenant, draws the full hub tree correctly.

Measured on the very site the view was describing:

GET /_api/site?$select=HubSiteId,IsHubSite
→ 200  { "HubSiteId": "63c5ce9c-…", "IsHubSite": true }

GET /_api/HubSites
→ 200  { "value": [] }

The site is a hub, and it does carry a HubSiteId — yet the hub list is empty, with a success status.

Cause

/_api/HubSites enumerates hub sites the caller is allowed to see; entries whose hub site is not readable for that account are filtered out. Trimming produces an empty set, not an error, so code written as

const hubs = await getHubs();               // 200 → []
const hub  = hubs.find(h => h.ID === site.HubSiteId);   // undefined
if (!hub) { /* "the site has no hub" */ }   // ← wrong conclusion

silently turns "we could not see it" into a factual claim. In a governance report that claim then propagates: the site is rendered as a plain site instead of a hub, the inheritance chain comes out empty, and a field that should read unknown reads missing — a number people act on.

Fix

1. Ask the object itself whether it is a hub. IsHubSite and HubSiteId come from _api/site of the site you are already reading, so they survive wherever the rest of the detail survives:

GET /_api/site?$select=HubSiteId,IsHubSite

2. Treat "belongs to a hub, but that hub is not in the list" as a contradiction, not a fact.

const hubs = await getHubs();                 // may be [] or null
const known = !!hubs && (!site.hubSiteId
  || hubs.filter(h => normGuid(h.ID) === site.hubSiteId).length > 0);

if (!known) {
  // The inheritance chain is UNKNOWN. Report "not determined" plus the reason —
  // never "nothing is inherited".
}

3. Say why in the UI. "The hub this site belongs to could not be read, so we claim nothing about inherited ownership" is a different message from "this site has no hub", and only one of them is true here.

Why it matters

The failure is invisible to types, linters and tests: the call succeeds, the array is a valid array, and the wrong branch is ordinary code. It only surfaces when someone with different permissions opens the same screen — or when a person named in the report objects.

Applies more widely

Any tenant-wide collection read as the user can come back trimmed to nothing with HTTP 200 — hub sites, search results over sites, followed sites, tenant properties. When your logic needs "the set is complete" (counting, inheritance, "nobody is responsible"), cross-check against the object the statement is about, or degrade to unknown. See also: deleted-site-vs-site-you-cannot-see.md (404 ≠ 403) and dont-cache-a-throttled-permission-probe.md.

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