BuilderIO / BuilderIO/agent-native

New page in Content shows a full-pane error until reload: draft recovery turns a create-time 403 into a dead editor

Open
#4,914 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
4.8k
Forks
449
Avg merge
10h 24m
Merged PRs (30d)
883

Description

# Content: new-page draft recovery turns a client-side create race into a full-pane error

**Version:** `@agent-native/core` 0.178.1, Content template (`templates/content`) installed as `apps/documents` in a four-app workspace on Vercel with Neon Postgres.

**What happens.** Creating a page from the Documents sidebar plus button shows "Something went wrong" over the editor until a full reload, even though the page was created successfully.

Reproduced by the workspace owner on both a preview deployment and production, on separate deployments and separate builds, so this is not a preview-only artifact.

The plus button's handler, `handleCreatePageInSpace` (`apps/documents/app/components/sidebar/DocumentSidebar.tsx:1603-1611`), generates the new document's id on the client with `nanoid()` (line 1605) before any request has gone out. `handleCreatePage` (lines 1342-1483) takes the optimistic branch for any normal space and calls `navigateToDocument(id)` at line 1428, which runs `navigate(\`/page/${id}\`, { flushSync: true })` (lines 1335-1340). That route change happens synchronously, before `createDocument.mutateAsync` is even called at line 1432, so the browser is on `/page/` while the create POST is still in flight.

On the new route, the editor's own `useDocument` query (`apps/documents/app/hooks/use-documents.ts:522-544`) is configured with `DOCUMENT_QUERY_FRESHNESS_OPTIONS` (lines 511-520), which sets `refetchOnMount: "always"`. That forces a background GET for a document the server may not consider visible yet, and it can come back 404. This blip is tolerated: `documentEditorLoadState` (`DocumentEditor.tsx:571-634`) checks `hasDocument && isDocumentCreationPending` first (lines 596-598) and keeps the editor mounted as long as the cache still holds the client-seeded `tempDoc`. This is not the visible failure.

The visible failure comes from a sibling query one level up. `PageDraftRecovery` (`apps/documents/app/components/editor/PageDraftRecovery.tsx`) wraps the editor whenever `document.canEdit === true` (`DocumentEditor.tsx:536-548`) and calls `usePreviewDocumentDraft`, which hits the `get-preview-document-draft` action (`use-documents.ts:557-563`) with no awareness that a creation is in flight. On `drafts.isError`, `PageDraftRecovery` unconditionally replaces the entire editor with `QueryErrorState` (lines 91-97), the "Something went wrong" panel with a Retry button.

It does not clear on its own, and the retry control does not clear it either for about a minute. `PageDraftRecovery` renders `QueryErrorState` with `onRetry={() => void drafts.refetch()}` (lines 91-97), so pressing Retry does re-issue the request. The workspace owner measured it twice, on a preview deployment and on production: Retry pressed immediately leaves the panel in place, and the panel clears on a retry roughly a minute after creation. The document id in the URL is the real one throughout, unchanged, and `create-document` accepts exactly this client-generated id (`create-document.ts:73-78`, "Optional pre-generated document ID for optimistic UI").

Cache invalidation is a contributing factor but not the whole story. `useCreateDocument` opts out of the framework's default broad invalidation (`skipActionQueryInvalidation: true`, `use-documents.ts:595-599`, versus the default `invalidateQueries({ queryKey: ["action"] })` in `use-action.js:690-692`), and the create flow re-invalidates only the `get-document` query via `documentQueryFilter` (`document-query.ts:23-36`, invoked at `DocumentSidebar.tsx:1449`). Nothing invalidates `get-preview-document-draft` or `list-document-versions`. That explains why the panel never refreshes itself. It does not explain why an explicit refetch keeps coming back 403.

**Open question for the maintainer, stated as an open question rather than a diagnosis.** `get-preview-document-draft` does nothing but `assertAccess("document", documentId, "editor")` before reading the draft (`get-preview-document-draft.ts:17-24`), and `resolveAccessImpl` loads the resource straight from the registered resource table by id (`sharing/access.js:397` and `loadResourceForAccess` above it). The document row is readable through `get-document` 549 ms after creation, yet editor access for the same id keeps resolving to forbidden for roughly a minute of explicit retries. We could not find a cache or TTL in `sharing/access.js` to account for that interval, and we did not chase it further into the framework's internals. Whatever holds that state for a minute is the thing worth fixing; the 403-instead-of-404 and the unconditional error panel are what make it user-visible.

Observed on a Vercel preview, all within one page visit. The three failures carry an identical millisecond timestamp in the runtime log, so they are one render's worth of requests, not a sequence:

```
1789322362183 GET /documents/_agent-native/actions/get-document 404
1789322362183 GET /documents/_agent-native/actions/list-document-versions 403
1789322362183 GET /documents/_agent-native/actions/get-preview-document-draft 403
1789322362732 GET /documents/_agent-native/actions/get-document 200
```

The successful read lands 549 ms after the failures. The row was created correctly the whole time; only the draft and version-history reads stayed latched on the earlier 403.

**Why.** `get-document.ts` uses the app's own document-aware resolver, `resolveDocumentAccess` (`apps/documents/actions/_document-access.ts:7-33`), and explicitly converts "no access record" into a 404 (`get-document.ts:75-83`). `list-document-versions.ts:25` and `get-preview-document-draft.ts:21` instead call the framework's generic `assertAccess` helper directly. `assertAccess` (`node_modules/@agent-native/core/dist/sharing/access.js:488-497`) cannot distinguish "row does not exist yet" from "row exists, no grant"; both produce a null from `resolveAccessImpl`, and either way it throws a `ForbiddenError`, hardcoded to `statusCode = 403` (line 41). So a document that is only momentarily not yet visible to the caller answers 403 from these two actions instead of the 404 that `get-document` would give it.

**Expected.** Two things are wrong independently. First, editor access for a document the caller just created should not resolve to forbidden for a minute; that is the open question above. Second, whatever the access layer decides, `get-preview-document-draft` and `list-document-versions` should answer 404 rather than 403 for a row that is not visible, and `PageDraftRecovery` should not replace the whole editor on a draft lookup that fails.

Of two plausible fixes for the second half, the smaller one is making `get-preview-document-draft` and `list-document-versions` call the same document-aware resolver `get-document` already uses, so a row that exists but is not yet visible answers 404 like `get-document` does, instead of 403. That is a resolver swap in two action files, with no new state to plumb. The other option, having `PageDraftRecovery` treat a not-found or forbidden result as "no draft" rather than an error whenever a creation is pending, needs a pending-creation signal threaded from `DocumentEditor` into `PageDraftRecovery`, and separately into whatever consumes `list-document-versions`, which is more surface to change. Neither has been tried; this is not a claim that either one is correct once implemented.

**Impact.** Every page created from the sidebar plus button in a deployed Content app shows a full-pane error over a document that was in fact created correctly, and the user has to reload before they can work in it. This is a race between the client-side navigation and the server-side create, so it will not reproduce reliably against a fast local database; it was only caught on a Vercel preview against Neon Postgres.

**Workaround.** None available locally. Every file in the trace, `DocumentSidebar.tsx`, `DocumentEditor.tsx`, `PageDraftRecovery.tsx`, `use-documents.ts`, `document-query.ts`, `optimistic-document.ts`, the three actions, and the shared `assertAccess` helper, is unmodified Content template or `@agent-native/core` framework code. There is no local change that fixes this without forking the template.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with PageDraftRecovery.tsx and the create/navigation flow in DocumentSidebar.tsx and DocumentEditor.tsx, then compare the document-aware resolver with the generic access calls in get-preview-document-draft.ts and list-document-versions.ts. Reproduce the creation race using the request trace and inspect the pending-query behavior. Done means a newly created page stays usable without reload and invisible resources return the intended not-found behavior without replacing the editor with an error panel.

Written by the indexing model from the issue text.

Assessment

Tech stack
postgresql, react, typescript
Domain
authorization, backend-api-design, full-stack
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.