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
- Confirm the id is from a fresh `connector.list` response for the current workspace.
- On the client, treat NOT_FOUND from getForEdit by closing the edit form and refreshing the list.
- Ensure the workspace scope in the request matches the connector's workspace.
- 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
- Source edit-form ids from a fresh `connector.list` for the current workspace.
- Handle NOT_FOUND in the edit modal by closing and refreshing.
- Reopen the edit form only after confirming the caller is still the connector owner.
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)