toeverything/AFFiNE · error · NotFound

not_found

not_found

Error message

Doc not found

What it means

Thrown by the internal RPC handler `GET /rpc/workspaces/:workspaceId/docs/:docId` when `docReader.getDoc(workspaceId, docId)` returns null. The endpoint is `@Internal()` (server-to-server) and maps the missing doc to `NotFound` with code `not_found`, which the global filter turns into HTTP 404.

Source

Thrown at packages/backend/server/src/core/doc-service/controller.ts:33

import { DatabaseDocReader } from '../doc';

@Controller('/rpc')
export class DocRpcController {
  private readonly logger = new Logger(DocRpcController.name);

  constructor(private readonly docReader: DatabaseDocReader) {}

  @SkipThrottle()
  @Internal()
  @Get('/workspaces/:workspaceId/docs/:docId')
  async getDoc(
    @Param('workspaceId') workspaceId: string,
    @Param('docId') docId: string,
    @Res() res: Response
  ) {
    const doc = await this.docReader.getDoc(workspaceId, docId);
    if (!doc) {
      throw new NotFound('Doc not found');
    }
    this.logger.debug(
      `get doc ${docId} from workspace ${workspaceId}, size: ${doc.bin.length}`
    );
    res.setHeader('x-doc-timestamp', doc.timestamp.toString());
    if (doc.editor) {
      res.setHeader('x-doc-editor-id', doc.editor);
    }
    res.send(doc.bin);
  }

  @SkipThrottle()
  @Internal()
  @Get('/workspaces/:workspaceId/docs/:docId/markdown')
  async getDocMarkdown(
    @Param('workspaceId') workspaceId: string,
    @Param('docId') docId: string,
    @Query('aiEditable') aiEditable?: string

View on GitHub (pinned to 26c515e050)

Solutions

  1. On the caller side, treat HTTP 404 from this RPC as 'doc absent' and trigger first-create or skip, not as a hard failure.
  2. Verify the doc row exists in the workspace's `doc` table before issuing the RPC for known-existing docs.
  3. Confirm `workspaceId`/`docId` are well-formed and paired (doc belongs to that workspace).
  4. If the doc was just created, retry shortly to allow the snapshot write to land.

Example fix

// before
const res = await fetch(`/rpc/workspaces/${wid}/docs/${did}`);
const bin = await res.arrayBuffer();

// after
const res = await fetch(`/rpc/workspaces/${wid}/docs/${did}`);
if (res.status === 404) {
  // doc absent — caller decides: seed it or skip
  return null;
}
if (!res.ok) throw new Error(`doc rpc failed: ${res.status}`);
const bin = await res.arrayBuffer();
Defensive patterns

Strategy: try-catch

Validate before calling

// Internal RPC caller: confirm the snapshot row exists before requesting
async function docSnapshotExists(workspaceId: string, docId: string): Promise<boolean> {
  const row = await models.doc.get(workspaceId, docId);
  return row != null;
}

if (!(await docSnapshotExists(workspaceId, docId))) {
  // doc not seeded yet — handle as 'absent', not an error
  return null;
}

Type guard

function isDocBytes(value: unknown): value is { bin: Uint8Array; timestamp: number } {
  return typeof value === 'object' && value !== null &&
    (value as { bin?: unknown }).bin instanceof Uint8Array &&
    typeof (value as { timestamp?: unknown }).timestamp === 'number';
}

Try / catch

try {
  const res = await fetch(`/rpc/workspaces/${wid}/docs/${did}`);
  if (res.status === 404) return null; // doc absent — caller decides
  if (!res.ok) throw new Error(`doc rpc failed: ${res.status}`);
  return await res.arrayBuffer();
} catch (e) {
  // network-level errors: retry/backoff as appropriate
  throw e;
}

Prevention

When it happens

Trigger: A peer service (sync/collab) requesting a doc that was never created, was deleted, or whose binary snapshot row is missing in the `doc` table for that workspace.

Common situations: Sync requests arriving before the doc snapshot is written; a deleted doc still referenced by cached routing; workspace migration that dropped rows; a malformed `docId`.

Related errors


AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12). Data as JSON: /api/errors/72c8300fd24b2d73. Report an issue: GitHub.