Files
stats/apps/start/src/integrations/tanstack-query/root-provider.tsx
Carl-Gerhard Lindesvärd 729722bf85 fix: potential fix for #301
wip
2026-03-21 13:12:54 +01:00

121 lines
3.5 KiB
TypeScript

import type { AppRouter } from '@openpanel/trpc';
import { QueryClient } from '@tanstack/react-query';
import { createIsomorphicFn } from '@tanstack/react-start';
import { getRequestHeaders } from '@tanstack/react-start/server';
import { createTRPCClient, httpLink } from '@trpc/client';
import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query';
import { useMemo } from 'react';
import superjson from 'superjson';
import { TRPCProvider } from '@/integrations/trpc/react';
export const getIsomorphicHeaders = createIsomorphicFn()
.server(() => {
const headers = getRequestHeaders();
const result: Record<string, string> = {};
// Only forward the cookie header so the API can validate the session.
// Forwarding all headers causes problems with hop-by-hop headers like
// `Connection: upgrade` (common in NGINX WebSocket configs) which makes
// Node.js undici throw UND_ERR_INVALID_ARG ("fetch failed").
const cookie = headers.get('Cookie');
if (cookie) {
result.cookie = cookie;
}
return result;
})
.client(() => {
return {};
});
// Create a function that returns a tRPC client with optional cookies
export function createTRPCClientWithHeaders(apiUrl: string) {
return createTRPCClient<AppRouter>({
links: [
httpLink({
transformer: superjson,
url: `${apiUrl}/trpc`,
headers: () => getIsomorphicHeaders(),
fetch: async (url, options) => {
try {
const response = await fetch(url, {
...options,
mode: 'cors',
credentials: 'include',
});
// Log HTTP errors on server
if (!response.ok && typeof window === 'undefined') {
const text = await response.clone().text();
console.error('[tRPC SSR Error]', {
url: url.toString(),
status: response.status,
statusText: response.statusText,
body: text,
options,
});
}
return response;
} catch (error) {
// Log fetch errors on server
if (typeof window === 'undefined') {
console.error('[tRPC SSR Error]', {
url: url.toString(),
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
options,
});
}
throw error;
}
},
}),
],
});
}
export function getContext(apiUrl: string) {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5,
gcTime: 1000 * 60 * 10,
refetchOnReconnect: false,
},
dehydrate: { serializeData: superjson.serialize },
hydrate: { deserializeData: superjson.deserialize },
},
});
// Create a tRPC client with cookies if provided
const client = createTRPCClientWithHeaders(apiUrl);
const serverHelpers = createTRPCOptionsProxy({
client,
queryClient,
});
return {
queryClient,
trpc: serverHelpers,
};
}
export function Provider({
children,
queryClient,
apiUrl,
}: {
children: React.ReactNode;
queryClient: QueryClient;
apiUrl: string;
}) {
const trpcClient = useMemo(
() => createTRPCClientWithHeaders(apiUrl),
[apiUrl]
);
return (
<TRPCProvider queryClient={queryClient} trpcClient={trpcClient}>
{children}
</TRPCProvider>
);
}