Server Actions and Cookies sending error: Cannot set headers after they are sent to the clien

Maybe I am misunderstanding something here. I am following the docs for Actions. Basically, what this component is supposed to do is check for the pressence of a session cookie. And, if the user is authenticated, redirects the client to the dashboard page. And if not, it redirects the client to the login page.
export default function Home() {
const loadAuthenticatedUser = useAction(getAuthenticatedUserAction);
const navigate = useNavigate();
loadAuthenticatedUser();
const authenticatedUser = useSubmission(getAuthenticatedUserAction);
const [_, { setUser, clearUser }] = useUserContext();
const [serverError, setServerError] = createSignal("");

createEffect(() => {
if (authenticatedUser.result) {
// If the user is authenticated, we redirect them to the dashboard page.
// otherwise, we show them the main page.
const result = authenticatedUser.result;
if (result.success) {
setUser(result.data as User);
navigate("/dashboard");
} else {
if (result.data instanceof AppException) {
setServerError(result.message);
} else {
// for now, we ust redirect them to the login page. as soon as we get more traction,
// we will have a proper home page deidicated for marketing.
clearUser();
navigate("/login");
}
}
authenticatedUser.clear();
}
});

const handleRetry = () => {
setServerError("");
authenticatedUser.retry();
};

return (
<Show
when={!serverError()}
fallback={
<ConfirmationMessage
title="Something went wrong."
description={serverError()}
buttonText="Try Again"
onClick={handleRetry}
image="img/logo.svg"
/>
}
>
<></>
</Show>
);
}
export default function Home() {
const loadAuthenticatedUser = useAction(getAuthenticatedUserAction);
const navigate = useNavigate();
loadAuthenticatedUser();
const authenticatedUser = useSubmission(getAuthenticatedUserAction);
const [_, { setUser, clearUser }] = useUserContext();
const [serverError, setServerError] = createSignal("");

createEffect(() => {
if (authenticatedUser.result) {
// If the user is authenticated, we redirect them to the dashboard page.
// otherwise, we show them the main page.
const result = authenticatedUser.result;
if (result.success) {
setUser(result.data as User);
navigate("/dashboard");
} else {
if (result.data instanceof AppException) {
setServerError(result.message);
} else {
// for now, we ust redirect them to the login page. as soon as we get more traction,
// we will have a proper home page deidicated for marketing.
clearUser();
navigate("/login");
}
}
authenticatedUser.clear();
}
});

const handleRetry = () => {
setServerError("");
authenticatedUser.retry();
};

return (
<Show
when={!serverError()}
fallback={
<ConfirmationMessage
title="Something went wrong."
description={serverError()}
buttonText="Try Again"
onClick={handleRetry}
image="img/logo.svg"
/>
}
>
<></>
</Show>
);
}
I included the action definition in the comments.
17 Replies
Je Suis Un Ami
Je Suis Un AmiOP7mo ago
Here is the definition of the action.
export const getAuthenticatedUserAction = action(
async (): Promise<ActionResult<User | null | AppException>> => {
"use server";
let user: User | null = null;
const session = await getUserSession();

if (session.data.access_token) {
try {
user = await getAuthenticatedUserForToken(session.data.access_token);

if (!user && session.data.refresh_token) {
// refresh the token
const tokens = await refreshAuthTokens(session.data.refresh_token);
await session.update(() => {
return {
access_token: tokens!.access_token,
refresh_token: tokens!.refresh_token,
};
});
user = await getAuthenticatedUserForToken(session.data.access_token);
}
} catch (e) {
const error = e as AppException;
return {
success: false,
message: error.message,
data: error,
} as ActionResult<AppException>;
}
}
return {
success: user !== null,
data: user,
message: "",
} as ActionResult<User | null>;
},
);
export const getAuthenticatedUserAction = action(
async (): Promise<ActionResult<User | null | AppException>> => {
"use server";
let user: User | null = null;
const session = await getUserSession();

if (session.data.access_token) {
try {
user = await getAuthenticatedUserForToken(session.data.access_token);

if (!user && session.data.refresh_token) {
// refresh the token
const tokens = await refreshAuthTokens(session.data.refresh_token);
await session.update(() => {
return {
access_token: tokens!.access_token,
refresh_token: tokens!.refresh_token,
};
});
user = await getAuthenticatedUserForToken(session.data.access_token);
}
} catch (e) {
const error = e as AppException;
return {
success: false,
message: error.message,
data: error,
} as ActionResult<AppException>;
}
}
return {
success: user !== null,
data: user,
message: "",
} as ActionResult<User | null>;
},
);
So, can I not call a server action from the component manually instead of from a form submission? Here's the definition of the getUserSession() function.
export const getUserSession = async () => {
return await useSession<UserSession>({
password: process.env.SESSION_SECRET || "password",
cookie: {
httpOnly: true,
secure: true,
path: "/",
},
maxAge: 60 * 60 * 60 * 24 * 90, // 90 days
sessionHeader: "x-perivel-identity-session",
name: "perivel_aid",
});
};
export const getUserSession = async () => {
return await useSession<UserSession>({
password: process.env.SESSION_SECRET || "password",
cookie: {
httpOnly: true,
secure: true,
path: "/",
},
maxAge: 60 * 60 * 60 * 24 * 90, // 90 days
sessionHeader: "x-perivel-identity-session",
name: "perivel_aid",
});
};
I just took it from the SolidStart vDocs. To summarize, the problem I am getting is when this component (the home page) is loaded, the app crashes saying that I am trying to set the session after the cookie has been sent to the client. Thanks for the help.
peerreynders
peerreynders7mo ago
Grasping at straws here, but: The underlying library function from h3 useSession() takes the h3 event in the first parameter position. vinxi creates a wrapper around that which goes on some extensive detour into async context to obtain that event (which contains the request) if it isn't supplied. So I would try
import { getRequestEvent } from 'solid-js/web';

export const getUserSession = async () => {
return await useSession<UserSession>(getRequestEvent().nativeEvent, {
password: process.env.SESSION_SECRET || 'password',
cookie: {
httpOnly: true,
secure: true,
path: '/',
},
maxAge: 60 * 60 * 60 * 24 * 90, // 90 days
sessionHeader: 'x-perivel-identity-session',
name: 'perivel_aid',
});
};
import { getRequestEvent } from 'solid-js/web';

export const getUserSession = async () => {
return await useSession<UserSession>(getRequestEvent().nativeEvent, {
password: process.env.SESSION_SECRET || 'password',
cookie: {
httpOnly: true,
secure: true,
path: '/',
},
maxAge: 60 * 60 * 60 * 24 * 90, // 90 days
sessionHeader: 'x-perivel-identity-session',
name: 'perivel_aid',
});
};
and see if that changes things. (For all I know that does exactly the same thing.)
Handle Session - h3
Remember your users using a session.
Je Suis Un Ami
Je Suis Un AmiOP7mo ago
Seems to do the same thing. Still getting the same error. I’m wondering… maybe it’s because I can’t run a server-only action outside a form submission? The docs don’t say anything about that though… so, no idea. My first attempt was just doing the check with a data loader function. However, the data loader only executes during the first load. I need this check to run every time the user goes to the route though, which is why I opted for an action. Maybe I need to take a different approach?
peerreynders
peerreynders7mo ago
The error is suggesting that something is trying to modify headers after the response body has already started flushing. Typically that happens when a await is missing somewhere.
Je Suis Un Ami
Je Suis Un AmiOP7mo ago
Hmm. Let me check.
peerreynders
peerreynders7mo ago
I assume that getAuthenticatedUserForToken or refreshAuthTokens don't do anything header or cookie related?
Je Suis Un Ami
Je Suis Un AmiOP7mo ago
Nope. They just make a fetch() call and return the respective data (the authenticated user and the auth tokens respectively). Updating the session is happening in the call to user session.update() in the action function itself. The only un-await-ed promise I’m seeing is the call to loadAuthenticatedUser() action in the main component. However, that’s the same thing that the example in the actions section in the docs is doing…
SolidStart Release Candidate Documentation
SolidStart Release Candidate Documentation
Early release documentation and resources for SolidStart Release Candidate
peerreynders
peerreynders7mo ago
Have you tried to implement a simple action and got it to work? I doubt it's a form vs. no form issue. I'd be tempted to start with the simplest possible "fake" version just to get it to work end to end and then build it out from there to see what makes it break. Just by reviewing the code it's difficult to judge what may be accessing the request at an inappropriate time.
Je Suis Un Ami
Je Suis Un AmiOP7mo ago
I just did. I created this basic action that just clears the session and then returns. It is still throwing the same error.
const dummyAction = action(async () => {
const session = await getUserSession();
await session.clear();
return {
success: false,
message: "Test",
data: null,
} as ActionResult<null>;
});
const dummyAction = action(async () => {
const session = await getUserSession();
await session.clear();
return {
success: false,
message: "Test",
data: null,
} as ActionResult<null>;
});
Here's the updated component. I basically just commented out the original useAction() and replaced it with the dummy.
export default function Home() {
//const loadAuthenticatedUser = useAction(getAuthenticatedUserAction);
const loadAuthenticatedUser = useAction(dummyAction);
const navigate = useNavigate();
loadAuthenticatedUser();
const authenticatedUser = useSubmission(getAuthenticatedUserAction);
const [_, { setUser, clearUser }] = useUserContext();
const [serverError, setServerError] = createSignal("");

createEffect(() => {
if (authenticatedUser.result) {
// If the user is authenticated, we redirect them to the dashboard page.
// otherwise, we show them the main page.
const result = authenticatedUser.result;
if (result.success) {
setUser(result.data as User);
navigate("/dashboard");
} else {
if (result.data instanceof AppException) {
setServerError(result.message);
} else {
// for now, we ust redirect them to the login page. as soon as we get more traction,
// we will have a proper home page deidicated for marketing.
clearUser();
navigate("/login");
}
}
authenticatedUser.clear();
}
});

const handleRetry = () => {
setServerError("");
authenticatedUser.retry();
};

return (
<Show
when={!serverError()}
fallback={
<ConfirmationMessage
title="Something went wrong."
description={serverError()}
buttonText="Try Again"
onClick={handleRetry}
image="img/logo.svg"
/>
}
>
<></>
</Show>
);
}
export default function Home() {
//const loadAuthenticatedUser = useAction(getAuthenticatedUserAction);
const loadAuthenticatedUser = useAction(dummyAction);
const navigate = useNavigate();
loadAuthenticatedUser();
const authenticatedUser = useSubmission(getAuthenticatedUserAction);
const [_, { setUser, clearUser }] = useUserContext();
const [serverError, setServerError] = createSignal("");

createEffect(() => {
if (authenticatedUser.result) {
// If the user is authenticated, we redirect them to the dashboard page.
// otherwise, we show them the main page.
const result = authenticatedUser.result;
if (result.success) {
setUser(result.data as User);
navigate("/dashboard");
} else {
if (result.data instanceof AppException) {
setServerError(result.message);
} else {
// for now, we ust redirect them to the login page. as soon as we get more traction,
// we will have a proper home page deidicated for marketing.
clearUser();
navigate("/login");
}
}
authenticatedUser.clear();
}
});

const handleRetry = () => {
setServerError("");
authenticatedUser.retry();
};

return (
<Show
when={!serverError()}
fallback={
<ConfirmationMessage
title="Something went wrong."
description={serverError()}
buttonText="Try Again"
onClick={handleRetry}
image="img/logo.svg"
/>
}
>
<></>
</Show>
);
}
Let me test it if it with an action that just returns immediately. If this works, my best guess is the error is somehow in how I set up the getUserSession() function? Okay. So it looks like the error is only being thrown whenever the session is being updated. So, my guess is the session is what is causing the error…
peerreynders
peerreynders7mo ago
In combination with the error message the suggestion is that by the time session.update is reached the response headers have already been sent—which is counterintuitive given that we haven't reached a return statement of any sort. However if the response started streaming this is the type of error that could occur. This isn't happening during SSR by any chance? https://github.com/nksaraf/vinxi/issues/208
GitHub
Error: Cannot set headers after they are sent to the client · Issue...
It seems it's not possible to set the session at present in my solid-start project. All request are returning: Error: Cannot set headers after they are sent to the client at ServerResponse.setH...
Je Suis Un Ami
Je Suis Un AmiOP7mo ago
Yes. This is using SSR. I scattered some logs around the action. And it seems the error occurs during the getUserSession() call, which indicates I am experiencing the same issue as you linked. So, I guess the solution to this error would be to do the checks from within a middleware instead of an action. Let me try that out really quick. It’s still not updated in the docs… at least from what I see. So, hopefully that changes soon.
peerreynders
peerreynders7mo ago
GitHub
solid-start-sse-chat/src/middleware.ts at 5ade3b77d9cca0bbdc3459cc4...
Basic Chat demonstration with server-sent events (SSE) - peerreynders/solid-start-sse-chat
Je Suis Un Ami
Je Suis Un AmiOP7mo ago
This helps a lot. Thanks. Here I thought SolidStart “just works”. Lmao.
peerreynders
peerreynders7mo ago
The cost of bleeding edge
Je Suis Un Ami
Je Suis Un AmiOP7mo ago
Anyways. Thanks for the help my friend. Really appreciate it. Just in case someone else is reading this with the same issue. It seems this issue only arises in actions involving SSR. For server actions invoked via form action, this solution is not necessary. At least as far as I can tell. My authentication form action seems to work just fine
Katja (katywings)
Quick tip on this: the h3/vinxi session utilities always create a session cookie if it doesnt exist, which is suboptimal during streaming ssr render, because the response might already be sent. You can avoid this by first checking if the session cookie exists in the request, and only if does exist you run getSession/useSession.
Want results from more Discord servers?
Add your server