S
SolidJS•7d ago
Kehvyn

Debugging "Error: Can't render headers after they are sent to the client."

I'm trying to figure out how to debug the above error. This happens whenever I hit "Sign Out" on my Supabase Auth + Solid Start project. It's triggered by setCookie in my Supabase server client initialization function. I'm not sure where to start on this one. I've been able to confirm that the problematic cookie is the Supabase Auth Token by logging out the name and the value when it errors: { name: 'sb-<project>-auth-token', value: '' } Since the code is fairly small here, I'm just going to post it in full. The HTML
<form action={signOutAction} method="post">
<button formAction={signOutAction} class="border-b-2 mx-1.5 sm:mx-6" type="submit">Sign Out</button>
</form>
<form action={signOutAction} method="post">
<button formAction={signOutAction} class="border-b-2 mx-1.5 sm:mx-6" type="submit">Sign Out</button>
</form>
signOutAction:
export const signOutAction = action(async () => {
"use server";
const event = getRequestEvent()!
const supabase = createServerClient(event);
await supabase.auth.signOut();
return redirect("/sign-in");
}, "signOutAction");
export const signOutAction = action(async () => {
"use server";
const event = getRequestEvent()!
const supabase = createServerClient(event);
await supabase.auth.signOut();
return redirect("/sign-in");
}, "signOutAction");
createServerClient:
import { createServerClient as _createServerClient } from "@supabase/ssr";
import { RequestEvent } from "solid-js/web";
import { parseCookies, setCookie } from "vinxi/http";

export const createServerClient = (event: RequestEvent) => {
return _createServerClient(
process.env.VITE_SUPABASE_URL!,
process.env.VITE_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return Object.entries(parseCookies(event.nativeEvent)).map(([name, value]) => ({ name, value }));
},
setAll(cookiesToSet: { name: string, value: string }[]) {
cookiesToSet.forEach(({ name, value }: { name: string, value: string }) => {
setCookie(event.nativeEvent, name, value)
});
},
},
},
);
};
import { createServerClient as _createServerClient } from "@supabase/ssr";
import { RequestEvent } from "solid-js/web";
import { parseCookies, setCookie } from "vinxi/http";

export const createServerClient = (event: RequestEvent) => {
return _createServerClient(
process.env.VITE_SUPABASE_URL!,
process.env.VITE_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return Object.entries(parseCookies(event.nativeEvent)).map(([name, value]) => ({ name, value }));
},
setAll(cookiesToSet: { name: string, value: string }[]) {
cookiesToSet.forEach(({ name, value }: { name: string, value: string }) => {
setCookie(event.nativeEvent, name, value)
});
},
},
},
);
};
This is part of a whole template that I've been working on, so I can also submit the whole thing to GitHub in PR form. 🙂
14 Replies
peerreynders
peerreynders•7d ago
I suspect that the response of the action is starting to stream before your code has a chance to access/update the cookie headers. I ran into a similar situation https://discord.com/channels/722131463138705510/1329164943878258739/1329546586572984391 It's only because I started to look at what was starting to stream in the response payload that I was able to figure out how to rearrange the code so that I could stop it from happening.
Kehvyn
KehvynOP•7d ago
Oooh that is deviously difficult. By pulling out all other queries, I was able to get it to work. So clearly I've got the same kind of race condition you dealt with. I'll have to do a deep dive on the responses. I'm not sure how I can slow this down though. Maybe the server-side client needs to be some kind of singleton.
Kehvyn
KehvynOP•7d ago
GitHub
solid-start/examples/with-supabase-auth/src/util/supabase/actions.t...
SolidStart, the Solid app framework. Contribute to kjrocker/solid-start development by creating an account on GitHub.
peerreynders
peerreynders•7d ago
My understanding of Supabase is tenuous at best but isn't createServerClient only needed for SSR? In other words sign out with the client directly? Or is this a case of the cookies being buried inside the client request which the server client then turns around to use against supabase?
Kehvyn
KehvynOP•7d ago
Yeah that matches my thoughts as well. All these actions hit Supabase's API anyway, they don't need to be server only
peerreynders
peerreynders•7d ago
That said, client side actions are still useful for remote async operations, especially detached from a specific component because you'll often want to throw a redirect to the router; useNavigate can get a bit glitchy in those situations.
Kehvyn
KehvynOP•6d ago
I'm not sure, but I think I ran into a problem with Vinxi not tree-shaking. I'm still testing it. Hopefully when the example repo is available to the world, people can learn from all these painful lessons 😆 I keep seeing errors in the console about AsyncLocalStorage not having a browser polyfill, which tells me something has gone very wrong. That, and it's just generally not stable, which usually means some kind of import error or library issue. Moving things to the client simplified things a lot. I can't make an isomorphic client, so my actions need to be either client-side or server-side. Now I'm having a new problem. This might be worth it's own thread. This feels like the simplest thing. An async function returning an object, but it's fighting me.
export const getUser = query(async () => {
"use client";
const supabase = createClient();
const { data: { user } } = await supabase.auth.getUser();
return user;
}, "user");
export const getUser = query(async () => {
"use client";
const supabase = createClient();
const { data: { user } } = await supabase.auth.getUser();
return user;
}, "user");
I've tried that basic function in a query and as a createResource fetcher, and neither Show nor Suspense give me consistent results.
export function Navigation() {
const location = useLocation();
const [user] = createResource(async () => {
"use client";
const supabase = createClient();
const { data: { user } } = await supabase.auth.getUser();
return user;
});

const signOut = useAction(signOutAction);

const active = (path: string) =>
path == location.pathname ? "border-sky-600" : "border-transparent hover:border-sky-600";

return (
<nav class="bg-sky-800">
<ul class="container flex items-center p-3 text-gray-200">
<li class={`border-b-2 ${active("/sign-up")} mx-1.5 sm:mx-6`}>
<A href="/sign-up">Sign Up</A>
</li>
<li class={`border-b-2 ${active("/sign-in")} mx-1.5 sm:mx-6`}>
<A href="/sign-in">Sign In</A>
</li>
<Suspense fallback={<li>Loading...</li>}>
{user() ? (
<li class={`border-b-2 ${active("nonsense-route")} mx-1.5 sm:mx-6`}>
<button onClick={() => signOut()}>Sign Out</button>
</li>
) : null}
</Suspense>
</ul>
</nav>
);
}
export function Navigation() {
const location = useLocation();
const [user] = createResource(async () => {
"use client";
const supabase = createClient();
const { data: { user } } = await supabase.auth.getUser();
return user;
});

const signOut = useAction(signOutAction);

const active = (path: string) =>
path == location.pathname ? "border-sky-600" : "border-transparent hover:border-sky-600";

return (
<nav class="bg-sky-800">
<ul class="container flex items-center p-3 text-gray-200">
<li class={`border-b-2 ${active("/sign-up")} mx-1.5 sm:mx-6`}>
<A href="/sign-up">Sign Up</A>
</li>
<li class={`border-b-2 ${active("/sign-in")} mx-1.5 sm:mx-6`}>
<A href="/sign-in">Sign In</A>
</li>
<Suspense fallback={<li>Loading...</li>}>
{user() ? (
<li class={`border-b-2 ${active("nonsense-route")} mx-1.5 sm:mx-6`}>
<button onClick={() => signOut()}>Sign Out</button>
</li>
) : null}
</Suspense>
</ul>
</nav>
);
}
I can tell it's not working because I have a Protected page that does work. So if I can be on the protected page, but not see the Sign Out, then something is wrong. And when I log user it's always null/undefined.
peerreynders
peerreynders•6d ago
query and as a createResource fetcher, and neither Show nor Suspense give me consistent results.
query starts a transition while createResource doesn't. So for query Suspense will only show the fallback the very first time, while it does have a previous render under it. For createResource the Suspense fallback should show anytime it has a pending promise under it. Have you done a console.log(user) before return user on the server side? (In case it's some weird serialization issue. I'm assuming you did a createEffect log on the client side).
Kehvyn
KehvynOP•5d ago
I realized it probably didn't know the session had updated, because it was happening over in the supabase client and NOT within Solid's reactivity. So I threw the Supabase session into a signal, subscribed to updates with supabases's built in helper, put that in a context, and moved all my actions that affect auth unto the client. That seems to have worked, and now my PR is live https://github.com/solidjs/solid-start/pull/1830 😄
GitHub
Supabase Auth Example by kjrocker · Pull Request #1830 · solidjs/so...
PR Checklist Please check if your PR fulfills the following requirements: Addresses an existing open issue: fixes #000 Tests for the changes have been added (for bug fixes / features) What is t...
peerreynders
peerreynders•5d ago
GitHub
solid-start/examples/with-supabase-auth/src/util/supabase/session-c...
SolidStart, the Solid app framework. Contribute to kjrocker/solid-start development by creating an account on GitHub.
Kehvyn
KehvynOP•5d ago
I wasn't sure if I could force the actions to only run on the client. It does seem to work without it, but is there a better way to tell solid that the action is absolutely 100% client only? The createEffect is taken right out of the supabase docs for SolidJS. https://supabase.com/docs/guides/getting-started/tutorials/with-solidjs#launch but it doesn't seem to be necessary.
Build a User Management App with SolidJS | Supabase Docs
Learn how to use Supabase in your SolidJS App.
peerreynders
peerreynders•5d ago
"use client" isn't a thing with SolidStart; action and query are client side router concepts; "use server" wraps the section under it in an RPC call.
out of the supabase docs
It seems a bit of React-ism. createEffect exists to leave the reactive graph; that code enters the reactive graph. createEffect will run at least once in order to capture the reactive dependencies but in this case there aren't any. As a control enhusiast I'd been inclined to just try:
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session);

supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
});
});
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session);

supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
});
});
Kehvyn
KehvynOP•3d ago
Yeah it worked fine without it, so I removed it. This is my first major project with Solid, and I'm coming from React, so the React-isms will inevitably leak in. I think I've got everything working now. Thank for your help, and the Solid crash course. Now I just need someone to officially pick up the PR on Github. I assume someone has seen it, I'm not in a hurry. That said, any tips on next steps would be appreciated 🙂

Did you find this page helpful?