Skip to content

fix: Properly handle server errors on project pages#1792

Open
Bojun-Feng wants to merge 1 commit intolinuxfoundation:mainfrom
Bojun-Feng:bug/fix-not-onboarded-error-handling
Open

fix: Properly handle server errors on project pages#1792
Bojun-Feng wants to merge 1 commit intolinuxfoundation:mainfrom
Bojun-Feng:bug/fix-not-onboarded-error-handling

Conversation

@Bojun-Feng
Copy link
Copy Markdown

@Bojun-Feng Bojun-Feng commented Mar 31, 2026

Fix #1788
Fix sonic-net/SONiC#2271

When the backend has a transient error while serving a project page, the server API handler at frontend/server/api/project/[slug]/index.ts uses return createError() instead of throw createError() in its catch block. In H3 v2, this causes the error object to be treated as a normal successful JSON response rather than an HTTP error.

The error object {message: "Internal server error", statusCode: 500} then gets:

  • Stored in the TanStack Query cache with status "success"
  • Dehydrated and transferred to the client as valid data
  • Cached by Nitro's server-side response cache for up to 1 hour
  • Cached by the browser

Since the error object has no contributorCount or organizationCount properties, the page shows "This project hasn't been onboarded to LFX Insights" for a valid project. The client never retries because the query status is "success" and retry is set to false.

Evidence from browser console on an affected page:

// The dehydrated state has correct data:
> window.useNuxtApp()?.payload?.state?.['$svue-query']?.queries?.[0]?.state?.data?.contributorCount
5423

// But the live query cache has an error disguised as success:
> const qc = window.useNuxtApp()?.vueApp?._context?.provides?.VUE_QUERY_CLIENT;
> const q = qc?.getQueryCache()?.getAll()?.find(q => q.queryKey?.[0] === 'project');
> q?.state?.status
"success"
> q?.state?.data
{message: "Internal server error", statusCode: 500, statusMessage: "Internal server error"}
Root Cause Report on Issue #1788

Root Cause Analysis: "This project hasn't been onboarded to LFX Insights"

Summary

The "not onboarded" error on valid project pages is caused by the server API handler returning error objects instead of throwing them. This causes transient backend failures to be treated as successful responses, cached, and served to all users.

Evidence

I was able to reproduce the issue and inspect the live client state using the browser console. The key finding is that the TanStack Query cache contains the project query with status "success", but the data is actually a 500 error object.

1. The server HTML and API are fine

A direct curl to the API returns correct data:

$ curl -s "https://insights.linuxfoundation.org/api/project/sonic-foundation" | python3 -c "
import sys,json; d=json.load(sys.stdin)
print(f'contributorCount={d[\"contributorCount\"]}, organizationCount={d[\"organizationCount\"]}')"

contributorCount=5423, organizationCount=716

The dehydrated Nuxt state embedded in the HTML also has the correct data:

> const vqRef = window.useNuxtApp?.()?.payload?.state?.['$svue-query'];
> const raw = JSON.parse(JSON.stringify(vqRef));
> raw?.queries?.[0]?.state?.data?.contributorCount
5423

2. But the live query cache has an error object disguised as success

On the affected page, the TanStack Query cache tells a different story:

> const qc = window.useNuxtApp?.()?.vueApp?._context?.provides?.VUE_QUERY_CLIENT;
> const projectQuery = qc?.getQueryCache()?.getAll()?.find(q => q.queryKey?.[0] === 'project');
> console.log('status:', projectQuery?.state?.status);
> console.log('data:', JSON.parse(JSON.stringify(projectQuery?.state?.data)));

status: success
data: {message: "Internal server error", statusCode: 500, statusMessage: "Internal server error"}

The query status is "success", but the actual data is a 500 error response. This is why the page gets permanently stuck — the client believes the query succeeded and never retries.

3. Why this happens: return createError() vs throw createError()

The server API handler at frontend/server/api/project/[slug]/index.ts has:

  } catch (err: unknown) {
    if (err && typeof err === 'object' && 'statusCode' in err && err.statusCode === 404) {
      throw err;  // 404s are thrown correctly
    }
    console.error('Error fetching project:', err);
    return createError({ statusCode: 500, statusMessage: 'Internal server error' });
    // ^^^^^^ BUG: should be `throw`, not `return`
  }

When a Nuxt server handler returns a createError() result instead of throwing it, the framework treats it as a normal successful response with the error object as the body. The HTTP status code is 200, $fetch resolves normally, and TanStack Query stores the error object as valid data with status "success".

4. How this cascades into the permanent "not onboarded" state

The page component [slug].vue checks:

const projectIsOnboarded = computed(
  () => !!project.value?.contributorCount || !!project.value?.organizationCount
);

Since the error object {message: "Internal server error", statusCode: 500} has no contributorCount or organizationCount, this evaluates to false, and the template renders the "not onboarded" message.

Because the query status is "success" and retry is set to false, the client never retries. The staleTime of 5 minutes means it won't refetch even on re-render. And in production, the Nitro response cache (setup/caching.ts) caches /project/** responses for 1 hour in Redis, so a single transient Tinybird failure poisons the cache for all users.

5. Why incognito always works

Incognito bypasses the browser cache. If the Nitro server-side cache has expired or wasn't affected by the transient error, the fresh SSR works correctly. The original reporter also noted that "the page always loads normally when I open it in incognito," which is consistent with this being a caching issue around transient errors.

Suggested Fix

The primary fix is changing return createError(...) to throw createError(...) in the catch block of the server handler, so that 500 errors are properly surfaced as HTTP errors rather than treated as successful responses:

  } catch (err: unknown) {
    if (err && typeof err === 'object' && 'statusCode' in err && err.statusCode === 404) {
      throw err;
    }
    console.error('Error fetching project:', err);
-   return createError({ statusCode: 500, statusMessage: 'Internal server error' });
+   throw createError({ statusCode: 500, statusMessage: 'Internal server error' });
  }

Additionally, on the client side in [slug].vue, guarding the "not onboarded" message to only display when the query has returned valid data (not an error object) would prevent this class of issue from showing a misleading message:

  <div
-   v-else-if="!isLoading && !projectIsOnboarded"
+   v-else-if="data && !isError && !projectIsOnboarded"
  >

And changing retry: false to retry: 2 would allow the client to recover from transient failures even when the SSR didn't cache a successful response.

…project pages

* Change `return createError()` to `throw createError()` in the project
  API handler so 500 errors are properly surfaced as HTTP errors instead
  of being treated as successful responses
* Add `v-else-if="isError"` branch to show a proper error message when
  the project query fails
* Guard the "not onboarded" message with `data &&` so it only displays
  when the API has returned valid data confirming the project is not
  onboarded
* Change `retry: false` to `retry: 2` with a 1 second delay so
  transient backend failures are retried client-side

Signed-off-by: Bojun Feng <bojundf@gmail.com>
@Bojun-Feng Bojun-Feng force-pushed the bug/fix-not-onboarded-error-handling branch from 5f5acdd to 1e19b1d Compare March 31, 2026 18:34
@Bojun-Feng Bojun-Feng changed the title Fix: Properly handle server errors on project pages fix: Properly handle server errors on project pages Mar 31, 2026
@Bojun-Feng
Copy link
Copy Markdown
Author

Hi @mbani01, would you mind having a look when you have time?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LFX Insights Page for SONiC Foundation is Blank [Report issue] LFX Insights – Discover the world's most critical open source projects

1 participant