lobehub/lobehub · error · TRPCError

NOT_FOUND

NOT_FOUND

Error message

Connector not found

What it means

Thrown by `connector.getForEdit` when `ctx.connectorModel.findById(input.id)` returns null. The input is validated as a UUID, so this is a genuine 'no such connector (in the caller's scope)' condition. Because `findById` is workspace/user-scoped, a connector owned by another user/workspace is invisible and reads as not-found. A viewer-role caller is blocked earlier by `connectorWriteProcedure`; this check fires for an owner whose connector was deleted or whose scope is wrong.

Solutions

  1. Confirm the id is from a fresh `connector.list` response for the current workspace.
  2. On the client, treat NOT_FOUND from getForEdit by closing the edit form and refreshing the list.
  3. Ensure the workspace scope in the request matches the connector's workspace.
  4. If the connector was shared, verify the caller is still its owner (downgrade to viewer is blocked earlier).

Example fix

// before
const connector = await trpc.connector.getForEdit.query({ id });

// after
try { const connector = await trpc.connector.getForEdit.query({ id }); }
catch (e) {
  if (e.data?.code === 'NOT_FOUND') { closeEditModal(); refreshList(); }
  else throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Confirm the connector is still in the current workspace list
const list = await trpc.connector.list.query();
if (!list.some((c) => c.id === id)) { closeEditModal(); return; }

Type guard

const isTrpcNotFound = (e: unknown): boolean =>
  typeof e === 'object' && e !== null && (e as any).data?.code === 'NOT_FOUND';

Try / catch

try {
  await trpc.connector.getForEdit.query({ id });
} catch (e) {
  if (isTrpcNotFound(e)) { closeEditModal(); refreshList(); return; }
  throw e;
}

Prevention

When it happens

Trigger: Opening the connector edit form for an id deleted since the list loaded, or for a connector in a different workspace. A creator later downgraded to viewer is stopped at the write-procedure gate first.

Common situations: Stale edit deep-link; concurrent deletion of the connector; switching workspace while the edit modal is open.

Understand the failure class

Background: NOT_FOUND error code: why tRPC, Harbor, Nacos and other libraries return 404 "not found" errors for resources that may still exist — this error's family across 11 libraries.

Related errors


AI-assisted analysis of lobehub/lobehub@10f24d7ade (2026-08-12). Data as JSON: /api/errors/743519e8ae4e4fe0. Report an issue: GitHub.

Appendix: source

Thrown at apps/server/src/routers/lambda/connector.ts:264

  /**
   * Return the connector record with decrypted user-set credentials so the
   * edit form can pre-fill accurately. Only the connector owner can call this
   * (enforced by connectorProcedure ownership check).
   *
   * Machine-managed secrets are intentionally excluded:
   * - OAuth access/refresh tokens (type 'oauth2') → stripped, returned as null
   * - oidcConfig.clientSecret (DCR-registered secret)  → stripped
   * User-set credentials (bearer token, custom headers) are returned as-is so
   * the edit form can display them.
   */
  // Member-gated even though it's a query: it returns decrypted credentials
  // for edit prefill, so a creator later downgraded to viewer must not reach it.
  getForEdit: connectorWriteProcedure
    .input(z.object({ id: z.string().uuid() }))
    .query(async ({ input, ctx }) => {
      const connector = await ctx.connectorModel.findById(input.id);
      if (!connector) throw new TRPCError({ code: 'NOT_FOUND', message: 'Connector not found' });
      // Edit-read returns decrypted credentials — same creator/owner gate as
      // the mutations that this edit view feeds.
      assertWorkspaceRowManageable(ctx, connector.userId, 'connector');

      const { oidcConfig, credentials, ...rest } = connector;
      const safeOidcConfig = oidcConfig ? { ...oidcConfig, clientSecret: undefined } : oidcConfig;
      // OAuth tokens are machine-managed — don't return them; the UI only needs
      // to know an OAuth flow is configured (reflected via oidcConfig presence).
      const safeCredentials = credentials?.type === 'oauth2' ? null : credentials;

      return { ...rest, credentials: safeCredentials, oidcConfig: safeOidcConfig };
    }),

  /**
   * The exact redirect URI the server will send to the OAuth/DCR endpoints.
   * The Add modal must display THIS value (not a client-derived origin) so the
   * URI the user registers matches the one used at authorize time.
   */

View on GitHub (pinned to 10f24d7ade)