Skip to content
Open
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/runtime/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ export function queryCollectionSearchSections<T extends keyof PageCollections>(c

async function executeContentQuery<T extends keyof Collections, Result = Collections[T]>(event: H3Event | undefined, collection: T, sql: string) {
if (import.meta.client && window.WebAssembly) {
if (window.sessionStorage?.getItem('previewToken')) {
try {
Comment on lines +42 to +43
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cat -n src/runtime/client.ts | sed -n '34,51p'

Repository: nuxt/content

Length of output: 1053


🌐 Web query:

sessionStorage access throws error restricted browser context private browsing

💡 Result:

Accessing sessionStorage throws errors like "Failed to read the 'sessionStorage' property from 'Window': Access is denied for this document" or SecurityError/QuotaExceededError in restricted browser contexts, primarily due to privacy protections in private/incognito browsing or strict cookie settings.[1][2][3] Key causes: - Private browsing/incognito mode: Browsers restrict storage to prevent tracking. Data is session-only and cleared on close, but access may be limited.[4][5] Modern browsers (Chrome, Firefox, Safari post-2017) generally allow sessionStorage with reduced quotas (~5MB), but older Safari threw QuotaExceededError with 0 quota.[6][7][8] - Third-party/iframe contexts: Cross-origin iframes or embedded scripts (third-party) are blocked in incognito or when "Block third-party cookies" is enabled, especially Chrome.[9][10][11] - User-configured restrictions: Blocking cookies/site data interprets as "prevent persisting data," throwing SecurityError per spec.[1][2] - Other: Opaque origins (file:/data:), sandboxed iframes, enterprise policies.[1] Browser differences (as of 2026): - Chrome/Edge: Works in top-level incognito; fails in third-party iframes/incognito with cookie blocking.[9][10] - Firefox: Generally allows; some tab duplication quirks in private mode.[12] - Safari: Improved since Safari 11; now allows with quota limits in private mode.[6][7] Solution: Always wrap access in try-catch for graceful degradation. Use in-memory fallback. try { sessionStorage.setItem('key', 'value'); } catch (e) { if (e.name === 'SecurityError' || e.name === 'QuotaExceededError') { // Fallback: in-memory storage, cookies, or disable features console.warn('sessionStorage unavailable; using fallback'); // e.g., const fallbackStorage = {}; } } Test availability first: function isStorageAvailable(type = 'sessionStorage') { try { const storage = window[type]; const test = 'storage_test'; storage.setItem(test, test); storage.removeItem(test); return true; } catch (e) { return false; } } For iframes/third-party, request Storage Access API if supported: document.requestStorageAccess({ sessionStorage: true }).[13][14] Users may need to adjust cookie settings for full access, but don't rely on it.[15][10]

Citations:


Guard preview-token reads so fallback still works.

At Line 41, sessionStorage.getItem is evaluated outside the try. In restricted browser contexts (private browsing, third-party iframes, or cookie-blocking settings), this access can throw SecurityError and skip both the WASM path and fetchQuery fallback, causing the function to fail entirely.

Proposed fix
-    if (window.sessionStorage?.getItem('previewToken')) {
+    let hasPreviewToken = false
+    try {
+      hasPreviewToken = !!window.sessionStorage?.getItem('previewToken')
+    }
+    catch {
+      // Ignore storage access errors and continue with non-preview behavior.
+    }
+
+    if (hasPreviewToken) {
       try {
         const result = await queryContentSqlClientWasm<T, Result>(collection, sql)
         if (result && (Array.isArray(result) ? result.length > 0 : true)) {
           return result as Result[]
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/client.ts` around lines 41 - 42, Read access to sessionStorage
can throw in restricted contexts because it's performed outside the try, so move
the sessionStorage read into the try block and guard it there: inside the
function where you currently check
window.sessionStorage?.getItem('previewToken'), wrap the access in try { const
previewToken = window.sessionStorage.getItem('previewToken'); if (previewToken)
{ /* existing WASM path using previewToken */ } } catch (e) { /* allow existing
fetchQuery fallback to run */ } ensuring you reference the same previewToken
variable and preserve the existing WASM vs fetchQuery control flow.

const result = await queryContentSqlClientWasm<T, Result>(collection, sql)
if (result && (Array.isArray(result) ? result.length > 0 : true)) {
return result as Result[]
}
}
catch {
// WASM query failed — fall through to server fetch
}
return fetchQuery(event, String(collection), sql) as Promise<Result[]>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Empty-result fallback cannot distinguish "DB not synced" from "genuinely no matches".

When WASM is fully synced and a preview-mode query legitimately returns zero results (e.g., filtering by a tag that matches no content), the condition on line 39 treats it identically to a missing-draft scenario and fires an extra fetchQuery round-trip unconditionally. In an active Studio session this can happen on every navigation to a page with no matching content.

There is no robust client-side signal to distinguish the two cases without additional metadata (e.g., a sync-complete flag written to sessionStorage after the WASM DB is hydrated). Consider documenting this as a known trade-off or adding a sync-state guard if the extra requests become noticeable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/client.ts` around lines 39 - 46, The current WASM-to-server
fallback treats an empty array result as a missing DB and always calls
fetchQuery; change the logic to only fall back when the WASM query actually
failed or returned null/undefined (i.e., treat Array.isArray(result) with
length===0 as a valid result and return it), or alternatively gate the fallback
with a sync-state flag written/read from sessionStorage (e.g., a "wasmSynced"
boolean set when hydration completes) so fetchQuery is only invoked when the DB
isn't known-synced; update the block around the Result check and the fetchQuery
call accordingly to use one of these two approaches and keep fetchQuery, result,
and the try/catch intact.

}
return queryContentSqlClientWasm<T, Result>(collection, sql) as Promise<Result[]>
}
else {
Expand Down
Loading