pear-devs/pear-desktop · info

Forbidden

Error message

Forbidden

What it means

'Forbidden' is the HTTP 403 body returned by the auth route's register() handler when the user declines the authorization dialog shown for a new client. The handler shows a dialog (with buttons and defaultId/cancelId of 1); if result.response === 1 (the cancel/decline button), it sets status 403 and returns a null body. This is the deliberate user-rejection path during client pairing, not a bug.

Source

Thrown at src/plugins/api-server/backend/routes/auth.ts:70

    if (config.authorizedClients.includes(id)) {
      // SKIP CHECK
    } else if (config.authStrategy === AuthStrategy.AUTH_AT_FIRST) {
      const result = await dialog.showMessageBox({
        title: t('plugins.api-server.dialog.request.title'),
        message: t('plugins.api-server.dialog.request.message', {
          origin: getConnInfo(ctx).remote.address,
          ID: id,
        }),
        buttons: [
          t('plugins.api-server.dialog.request.buttons.allow'),
          t('plugins.api-server.dialog.request.buttons.deny'),
        ],
        defaultId: 1,
        cancelId: 1,
      });

      if (result.response === 1) {
        ctx.status(403);
        return ctx.body(null);
      }
    } else if (config.authStrategy === AuthStrategy.NONE) {
      // SKIP CHECK
    }

    if (!config.authorizedClients.includes(id)) {
      setConfig({
        authorizedClients: [...config.authorizedClients, id],
      });
    }

    const token = await sign(
      {
        id,
        iat: ~~(Date.now() / 1000),
      } satisfies JWTPayload,
      config.secret,

View on GitHub (pinned to 1e2aac5706)

Solutions

  1. Retry registration from the client and click 'Allow' in the dialog this time.
  2. If the client is trusted, pre-add its id to authorizedClients in the config so registration isn't needed.
  3. If running headless/automated, switch config.authStrategy to AuthStrategy.NONE or pre-authorize clients to avoid interactive prompts.
Defensive patterns

Strategy: fallback

Type guard

const isForbidden = (res: Response): boolean => res.status === 403;

Try / catch

const res = await fetch(registerUrl, opts);
if (res.status === 403) {
  // user declined the pairing dialog — inform user; only retry on explicit consent
  throw new Error('Pairing declined by user');
}

Prevention

When it happens

Trigger: A client attempts to register/pair via the auth route while authStrategy is not NONE, and the user clicks the 'Cancel'/'Deny' button in the popped-up dialog.

Common situations: User accidentally dismissing the pairing prompt, pairing attempt from an unrecognized machine the user intentionally rejects, or an automation script triggering the dialog while the user is away so it times out to the cancel action.

Understand the failure class

Related errors


AI-assisted analysis of pear-devs/pear-desktop@1e2aac5706 (2026-08-27). Data as JSON: /api/errors/86851725bb5676e4. Report an issue: GitHub.