Skip to content

fix(app-shell): gate /meta/* on a resolved session and identify HTTP failures in the log (#4042) - #4078

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-4042-login-meta-401-noise
Aug 10, 2026
Merged

fix(app-shell): gate /meta/* on a resolved session and identify HTTP failures in the log (#4042)#4078
yinlianghui merged 2 commits into
mainfrom
claude/issue-4042-login-meta-401-noise

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #4042

Opening a logged-out console painted ~30 red HTTP request failed lines before the login form was drawn. The card's triage split this into two independently deliverable halves; both are here.

Premise check

Verified against origin/main before implementing. Both halves still hold, and the "extra trigger" the card asked me to fix or explain turned out to be two distinct mechanisms, one of which is not an unauthenticated artefact at all.

A recording harness on the real provider stack reproduced the card's per-round table exactly — one mount, one consumer reading objects:

before: ["object", "view", "app", "view", "object"]   <- 5 requests, object x2, view x2
after:  ["object", "view", "app"]                      <- 3 requests, one per type

Half 1 — no /meta/* before a session exists

ConnectedShellInner now withholds the metadata tree until GET /auth/get-session resolves. useAuth() outside an AuthProvider reports isLoading: false, so a provider-less embed is untouched; every protected route already sat behind an AuthGuard that resolves auth first, so the signed-in flow is unchanged.

The actual entry point for the anonymous burst was the console's landing route: < Route path="/" > mounted ConnectedShell with no AuthGuard above it, so simply opening /_console/ mounted the whole data layer as an anonymous visitor. It is now guarded, which also means an unauthenticated visitor reaches /login without a single doomed request. examples/console-starter had the identical shape and got the identical fix.

Half 1b — the duplicate trigger, in two parts

Part one, and it was never an auth problem. Consumers read metadata during the FIRST render — useActionModal reads objects, whose getter kicks ensureType('object') and ensureType('view') from the render phase — which is before any effect runs. MetadataProvider's preview-mode effect then cleared the whole cache on mount, discarding those two entries while their requests were in flight; the next render found them idle and refetched both. The effect now skips its mount run: on mount the cache is empty and there was never anything to drop, so the clear only ever meant something on a later previewDrafts change. This doubled meta/object and meta/view on every mount, signed in included — the reporter saw it as 401 noise, but it was costing two wasted round trips per mount in normal authenticated use.

Part two, failures only. entry.promise collapses callers that arrive while a request is in flight, but callers arriving just after a failure found status: 'error' with promise: null and each started a fresh attempt. That is a real sequence rather than a hypothetical: the mount effect walks EAGER_TYPES serially, so by the time it reached view the render-phase read of view had already failed, and it re-requested it. A failed type now stays un-retried for ~1s, which collapses one mount's burst of callers into a single attempt. Deliberately not the 5-minute ttlMs — a later caller still retries on its own, and refresh() / invalidate() retry immediately and unconditionally, which is pinned by its own test.

Half 2 — a failure that says which request failed

@objectstack/client reports every non-2xx as logger.error("HTTP request failed", undefined, { method, url, status, error }). The console's logger forwarded that verbatim, so the identifying fields lived only in the third argument — and anything that flattens a console record to text (a headless/CDP capture, a log shipper, a copied DevTools line) renders them [object Object] / Object. A screenful of failures could not tell you a single URL or status; the reporter had to diff the network panel by hand to establish that all 30 lines were one benign pre-login burst.

The identifying fields now go into the message string itself:

HTTP request failed: GET /api/v1/meta/object -> 401 [UNAUTHORIZED]

The structured bag is still passed alongside for DevTools to expand — text for the flatteners, object for the inspectors, neither at the other's expense. formatHttpFailureMessage and createQuietHttpLogger are exported so the contract is testable and an app wiring its own ObjectStackClient gets the same identified failures.

Nothing is newly silenced, which the card called out explicitly. The only demotion remains 404-on-an-optional-collection (sys_presence, sys_activity) — an expected outcome of a request we still mean to make. A 401 that survives the session gate, e.g. a mid-session expiry, stays a visible, fully-identified error, pinned by a dedicated test. The cure for doomed requests is not issuing them, never hiding them once issued.

Reverse verification

Direction predicted before running: revert the source, keep the new tests, expect red. Done with a patch-file revert (never git stash — shared stack).

14 of 15 assertions went red, each in the predicted direction:

AssertionError: expected [ 'object', 'view', 'app', ...(2) ] to deeply equal []
AssertionError: expected [ 'app', 'object', 'object', ...(3) ] to deeply equal [ 'app', 'object', 'view' ]
AssertionError: expected [ 'object', 'object' ] to have a length of 1 but got 2
 Test Files  3 failed (3)
      Tests  14 failed | 1 passed (15)

The one that stayed green is renders through once auth resolves with NO session — that test asserts the non-regression direction (a consumer mounting ConnectedShell outside an AuthGuard must still render once the answer is known), so it is correctly green both before and after. Reporting it rather than reshaping it to fit the red/green template.

Tests

New: MetadataProvider.requestBudget.test.tsx (3), ConnectedShell.sessionGate.test.tsx (3), httpFailureLogging.test.ts (9). Both directions are pinned throughout — no /meta/* while auth is pending, and the same three requests once each after it resolves.

# affected packages, post-merge with origin/main
npx vitest run --maxWorkers=2 packages/app-shell packages/data-objectstack apps/console
 Test Files  371 passed (371)
      Tests  3612 passed | 1 skipped (3613)

# type-check
packages/data-objectstack type-check: Done
packages/app-shell type-check: Done
apps/console type-check: Done

# lint
packages/data-objectstack lint: 344 problems (0 errors, 344 warnings)
packages/app-shell lint:       2237 problems (0 errors, 2237 warnings)
apps/console lint:              193 problems (0 errors, 193 warnings)

origin/main merged in before opening, per the region-exclusivity note against #4047.


Generated by Claude Code

claude added 2 commits August 10, 2026 04:24
…failures in the log (#4042)

Opening a logged-out console painted ~30 red `HTTP request failed` lines
before the login form was drawn. Two independent causes.

1. Requests fired before the session was known. ConnectedShellInner now
   withholds the metadata tree until GET /auth/get-session resolves, and
   the console's `/` route — which mounted ConnectedShell with no
   AuthGuard above it — is now guarded, so an anonymous visitor reaches
   /login without a single doomed request. Same fix in console-starter.

2. Two requests per type per mount, signed in as well. Consumers read
   metadata during the FIRST render, before any effect runs;
   MetadataProvider's preview-mode effect then cleared the cache on
   mount, discarding those entries mid-flight so the next render
   refetched them. That effect now skips its mount run. A second
   duplicate appeared only after a failure, where callers arriving just
   after the rejection each started a fresh attempt — a failed type now
   stays un-retried for ~1s, which collapses one mount's burst without
   touching refresh()/invalidate().

3. `HTTP request failed` now names the request. The client passes
   method/url/status as a third argument, which every console-flattener
   renders as `[object Object]`; those fields now go into the message
   string too, alongside the structured bag.

Nothing is newly silenced: the only demotion remains 404-on-an-optional-
collection, and a 401 surviving the gate stays a visible, identified
error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 10, 2026 4:44am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.3 KB 350 KB
Entry file index-DWVXen9m.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.66KB 3.13KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 483.91KB 106.75KB
core (index.js) 3.04KB 1.15KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 140.66KB 36.25KB
fields (index.js) 228.51KB 56.69KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.65KB 1.06KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 9.48KB 3.27KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 4.52KB 1.96KB
layout (index.js) 38.84KB 10.80KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 45.23KB 12.45KB
plugin-charts (index.js) 61.49KB 17.48KB
plugin-chatbot (index.js) 180.33KB 42.79KB
plugin-dashboard (index.js) 118.50KB 30.66KB
plugin-designer (index.js) 210.51KB 42.51KB
plugin-detail (index.js) 237.80KB 59.48KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 113.81KB 27.53KB
plugin-gantt (index.js) 162.79KB 39.67KB
plugin-grid (index.js) 187.97KB 49.79KB
plugin-kanban (index.js) 48.53KB 13.38KB
plugin-list (index.js) 109.96KB 26.64KB
plugin-map (index.js) 17.00KB 5.32KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.58KB 10.58KB
plugin-timeline (index.js) 26.21KB 7.52KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.03KB 20.55KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 23.71KB 7.95KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.23KB 0.66KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 2.71KB 1.34KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghui
yinlianghui marked this pull request as ready for review August 10, 2026 04:54
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 10, 2026
Merged via the queue into main with commit 41d6022 Aug 10, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4042-login-meta-401-noise branch August 10, 2026 04:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

objectui: 登录页在无会话时仍拉 /meta/*,登录前刷 30 条 401 报错,且日志打成 [object Object] 无法定位

2 participants