FAQ and troubleshooting

Answers to the questions that come up most when signing in with UNET and calling the API: what an error means, the usual cause, and the fix.

Each answer gives the usual cause and the fix. For the full list of errors, see Errors.

Why does unet.social say the return URL is not allowed?

Cause. The redirectUri you pass to the SDK is not one of the return URLs registered for the app. The match is exact: scheme, host, port, path, and a trailing slash all count.

Fix. Open the app in the console, compare the return URLs with the redirectUri in your code, and add the one you use, for example http://localhost:5173/auth/callback for development. Remember that production and development are different addresses.

Why do I get StateMismatch?

Cause. The return page has no sign-in waiting for it. Typical reasons: the page was opened twice or reloaded after the code was used; the sign-in was started in one tab and finished in another (the default storage, sessionStorage, belongs to one tab); the sign-in took longer than 15 minutes; the browser cleared its storage in between.

Fix. Start the sign-in again. If you need the return to work in a different tab, set storage: "local". In React's strict mode in development, an effect runs twice, so guard the callback so that it runs once (see the React recipe).

What does invalid_client mean?

Cause. UNET does not accept the App ID. It is mistyped, the app was turned off or deleted, or it belongs to the other server: apps exist per server, so an app created in the console against the test server does not exist on production.

Fix. Check the App ID, that the app is active in the console, and that issuer points at the server the app was created on. From verifyAccessToken, invalid_client also means the App Secret is wrong or the app has no secret (a single-page or mobile app is a public app; use verifyIdToken for it).

What does discovery_invalid mean?

Cause. The server's description of itself (the discovery document) does not match what you configured: it names another issuer, or an endpoint is missing, is not https, or is on another origin. The SDK refuses to send a sign-in code or tokens to such a place.

Fix. Check the issuer option. Fetch https://<issuer>/.well-known/openid-configuration and compare. If you run the server, its issuer must be its public https address. See Upgrading from 0.2 to 0.3.

What does invalid_scope mean?

Cause. The sign-in asked for a permission that the app did not register. New apps are held to the permissions they registered. The error comes back on the return URL and handleRedirectCallback() throws it.

Fix. Register the permission in the app's settings in the console, or remove it from scopes. After adding permissions, sign in again with prompt: "consent" so the person can allow them. See Permissions.

What is the difference between insufficient_scope and not_allowed?

Both are a 403, and they mean different things.

  • InsufficientScope means a permission is missing: your app was not granted the scope the call needs. Its scope field names it. Register it and sign in again.
  • ApiError with code: "not_allowed" means a rule forbids it: you are blocked, the account is private, comments are off on that post, it is your own post. More permissions do not help.

Why does SessionExpired happen after a while?

Cause. The SDK could not renew the session. Common reasons: the person removed your app in their settings; they signed out of UNET everywhere or changed their password; your app did not ask for offline_access, so there is no refresh token and the session ends with the first access token (about an hour); the refresh token was not used for 60 days, or the person's permission passed its 180 days (server defaults); the app was turned off.

Fix. Ask for offline_access if you want a long session, and when SessionExpired arrives, send the person back to sign in. See Sessions and storage.

I see a CORS error

Cause. UNET's sign-in and API accept requests from any origin without cookies, so a real origin problem is rare. A CORS error in the console usually hides something else: the address is wrong or the request never reached UNET (a wrong issuer or apiBase), a proxy or firewall answered with an error page that has no CORS headers (such as a 502), or a custom fetch sends cookies (credentials: "include"), which UNET does not allow.

Fix. Open the Network tab and look at the failing request: its address and its status. Remove credentials: "include" from a custom fetch. If the address is right and it still fails, use onTrace (see below) and check your own proxy.

Why does sign-in do nothing on an http page?

Cause. The sign-in uses Web Crypto, which browsers provide only on https pages and on localhost. On a plain http address signIn() fails in the browser with an error, and no redirect happens.

Fix. Use https, or http://localhost for development. For a phone or another device on your network, use an https tunnel.

Why is the person signed in separately in each tab, or signed out in another?

Cause. The default storage is "session", which is private to one tab and cleared when it closes.

Fix. Use storage: "local" to share the session between tabs. Tabs then follow each other's sign-in and sign-out, and refreshes are serialised across tabs so a rotating refresh token is never spent twice. Weigh the cost: with "local" any script on your origin can read the tokens, so use a strict content security policy.

How do I get the UNET user id?

If your app has a REST API permission, the API shows real ids: Profile.id and the id of every author. For example, (await unet.api.profile(user.username)).profile.id with content:read. The sub of a sign-in is a per-site id by default, not the UNET id, and an app that only signs in cannot get the UNET id. See User ids.

Why is there no email address?

UNET accounts have no email address, so there is none to give. The identity of a person is sub, and username and displayName are labels. If your service needs email, ask for it yourself. See Privacy.

The person's avatar does not load

Cause. avatarUrl is null when the person has no picture. For a private account, UNET does not serve the picture to the public, so an image tag on your site can fail. Your content security policy may also block the image.

Fix. Show a placeholder when avatarUrl is null or the image fails to load, and allow https://api.unet.ovh in img-src.

I get RateLimited

Cause. The app made more requests than its budget for that person allows. Budgets are per app and per person, and writes have tighter ones.

Fix. Wait for retryAfterSeconds, or set retries so the SDK waits and retries for you. Cache what you can, load only what is on screen, and use realtime instead of polling. The numbers are on Limits.

My app works on the test server but not on production, or the other way round

Cause. Apps exist per server. The App ID, its return URLs and its permissions were created on one server only, and the SDK's issuer decides which server it talks to.

Fix. Create the app on each server you use, put the right App ID in each environment's configuration, and set issuer to match. The default is the production server.

How do I debug a request?

Pass onTrace to see every request the SDK makes, with the call that caused it:

import { UnetId } from "@unet/id-web";

const unet = new UnetId({
  clientId: "unet_xxx",
  redirectUri: `${location.origin}/auth/callback`,
  onTrace: (t) => console.log(t.sdk, t.method, t.url, t.status, `${t.ms} ms`, t.requestBody, t.responseBody),
});

Tokens, codes and verifiers are shortened in a trace, so it is safe to paste into a bug report. See Testing.