-
Notifications
You must be signed in to change notification settings - Fork 2.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
How to set multiple Set-Cookie
headers in a single response?
#231
Comments
I get an interesting result if I try this: // import {Headers} from 'remix'
const headers = new Headers()
headers.append('Set-Cookie', destroyCookie)
headers.append('Set-Cookie', sessionIdCookie)
return redirect('/me', {headers}) I still only get a single
I'm pretty sure that's exactly the same result as using But I'm thinking remix has a bug in handling this situation. It should be sending multiple |
I got it working. Now we're properly setting two cookies :) However, to make it work, I needed to create these patches 😬 diff --git a/node_modules/@remix-run/express/server.js b/node_modules/@remix-run/express/server.js
index 4fd2142..246a32e 100644
--- a/node_modules/@remix-run/express/server.js
+++ b/node_modules/@remix-run/express/server.js
@@ -73,8 +73,10 @@ function createRemixRequest(req) {
function sendRemixResponse(res, response) {
res.status(response.status);
- for (let [key, value] of response.headers.entries()) {
- res.set(key, value);
+ for (let [key, values] of Object.entries(response.headers.raw())) {
+ for (const value of values) {
+ res.append(key, value);
+ }
}
if (Buffer.isBuffer(response.body)) { And one in diff --git a/node_modules/@remix-run/node/server.js b/node_modules/@remix-run/node/server.js
index af7406e..93cf72d 100644
--- a/node_modules/@remix-run/node/server.js
+++ b/node_modules/@remix-run/node/server.js
@@ -58,7 +58,7 @@ async function handleDataRequest(request, loadContext, build, routes) {
try {
response = isActionRequest(request) ? await data.callRouteAction(build, routeMatch.route.id, clonedRequest, loadContext, routeMatch.params) : await data.loadRouteData(build, routeMatch.route.id, clonedRequest, loadContext, routeMatch.params);
} catch (error) {
- return responses.json(errors.serializeError(error), {
+ response = responses.json(errors.serializeError(error), {
status: 500,
headers: {
"X-Remix-Error": "unfortunately, yes"
@@ -66,17 +66,29 @@ async function handleDataRequest(request, loadContext, build, routes) {
});
}
+ if (build.entry.module.handleDataRequest) {
+ try {
+ response = await build.entry.module.handleDataRequest(clonedRequest, response, loadContext, routeMatch.params);
+ } catch (error) {
+ return responses.json(errors.serializeError(error), {
+ status: 500,
+ headers: {
+ "X-Data-Request-Handler-Error": "Handle your errors, yo."
+ }
+ });
+ }
+ }
+
if (isRedirectResponse(response)) {
// We don't have any way to prevent a fetch request from following
// redirects. So we use the `X-Remix-Redirect` header to indicate the
// next URL, and then "follow" the redirect manually on the client.
let locationHeader = response.headers.get("Location");
response.headers.delete("Location");
+ response.headers.set("X-Remix-Redirect", locationHeader);
return new nodeFetch.Response("", {
status: 204,
- headers: { ...Object.fromEntries(response.headers),
- "X-Remix-Redirect": locationHeader
- }
+ headers: response.headers,
});
} As discussed on discord, |
Another thing that will need to happen is all the templates and docs should update so instead of this: headers: {
...Object.fromEntries(responseHeaders),
// ...etc
} They simply set values on the responseHeaders instead. Otherwise multiple header values will be lost. I just looked and noticed that this doc is correct, so maybe you've already made this change. responseHeaders.set("Content-Type", "text/html");
return new Response("<!DOCTYPE html>" + markup, {
status: responseStatusCode,
headers: responseHeaders
}); 👍 |
FYI, I had to add another patch for the |
Thanks for all the detailed info here, @kentcdodds. I think you've uncovered a significant bug in the way we currently handle headers in Remix. We'll get this fixed very soon 👍 |
…headers x-ref #231 Signed-off-by: Logan McAnsh <[email protected]>
* feat(express): add support for multiple 'Set-Cookie' and other multi headers x-ref #231 Signed-off-by: Logan McAnsh <[email protected]> * test(express): add tests for createRemixHeaders Signed-off-by: Logan McAnsh <[email protected]> * test(express): add basic test for createRemixRequest Signed-off-by: Logan McAnsh <[email protected]> * test(architect): add tests for headers Signed-off-by: Logan McAnsh <[email protected]> * test(vercel): extract headers conversion function, start adding tests around it Signed-off-by: Logan McAnsh <[email protected]> * test(vercel): shutdown server after running test Signed-off-by: Logan McAnsh <[email protected]> * chore(architect): use requestContent protocol; add test for arc createRemixRequest Signed-off-by: Logan McAnsh <[email protected]> * test(architect): remove tes as I was handling it in the wrong direction Signed-off-by: Logan McAnsh <[email protected]> * chore: remove `Object.fromEntries(responseHeaders)` spreading Signed-off-by: Logan McAnsh <[email protected]> * test(express): update createRemixHeaders to support multiple headers with the same name Signed-off-by: Logan McAnsh <[email protected]> * test(vercel): add more tests Signed-off-by: Logan McAnsh <[email protected]> * chore: ignore test directories when using tsc * feat(architect): make multi set-cookie headers work Signed-off-by: Logan McAnsh <[email protected]> * feat(vercel): make multi set-cookie headers work Signed-off-by: Logan McAnsh <[email protected]> * test(architect): add tests for createRemixRequest Signed-off-by: Logan McAnsh <[email protected]> * test(vercel): add test for createRequestHandler vercel requests are regular http IncomingMessages but with various methods on it to make it more like express, so we'll just treat it like express.. Signed-off-by: Logan McAnsh <[email protected]> * test(vercel): use supertest for better header testing Signed-off-by: Logan McAnsh <[email protected]> * chore(deps/vercel): add @types/supertest Signed-off-by: Logan McAnsh <[email protected]> * test(express,vercel): bring tests to parity Signed-off-by: Logan McAnsh <[email protected]> * fix(express): read port from request app settings Signed-off-by: Logan McAnsh <[email protected]> * fix(express): req.get("host") returns the port Signed-off-by: Logan McAnsh <[email protected]> * test(express): update request mocking Signed-off-by: Logan McAnsh <[email protected]> * chore: update notes * fix(express): im dumb Signed-off-by: Logan McAnsh <[email protected]> * feat: actually make multiple set-cookie headers work Signed-off-by: Logan McAnsh <[email protected]> * test: add test for multiple set cookie headers Signed-off-by: Logan McAnsh <[email protected]> * test: update config snapshots due to new route Signed-off-by: Logan McAnsh <[email protected]> * chore(vercel): remove extraneous setting of status code Signed-off-by: Logan McAnsh <[email protected]> * Add space after comma * Code formatting * feat(getDocumentHeaders): re-add support for multiple set-cookie headers Signed-off-by: Logan McAnsh <[email protected]> * test(arc,vercel): update mock import Signed-off-by: Logan McAnsh <[email protected]> * chore: update changelog Signed-off-by: Logan McAnsh <[email protected]> Co-authored-by: Michael Jackson <[email protected]>
this is fixed and included once 0.18 ships! |
This doesn't seem to be working when you have a parent loader and nested loader both trying to How do I handle this situation? Or am I missing something? |
@shripadk This is a closed issue. I don't think your issue is a bug, but a support question. Can you create a new discussion? |
@kiliman Wasn't sure if this is a bug or a discussion so I posted both. A discussion in Discord as well as a report here. But I am now inclined to think this is definitely a bug. Tested it out in the root loader (the Response headers contains only 1 Network tab: |
@kiliman Interestingly it only works with Network tab: |
this still seems to be an issue in the import type { ActionArgs } from "@remix-run/node";
import { createCookie, json } from "@remix-run/node";
import { AUTH_COOKIE, AUTH_EXP_COOKIE } from "app/cookies"
export const setCookie = (key: string, value: any) => {
const createdCookie = createCookie(key, {
path: "/",
sameSite: "lax",
});
return createdCookie.serialize(value);
};
export async function action({ request }: ActionArgs) {
return json(
{},
{
headers: {
"Set-Cookie": await setCookie(AUTH_COOKIE, ""),
// @ts-expect-error
"Set-Cookie": await setCookie(AUTH_EXP_COOKIE, ""),
},
}
);
} and here are my response headers: |
This way it seems to work return json(
{ success: true },
{
headers: [
["Set-Cookie", await destroyFirstSession(firstSession)],
["Set-Cookie", await commitSecondSession(secondSession)]
]
}
) |
I tried both Cookies: const idTokenCookie = createCookie("id_token", {
path: "/",
httpOnly: true,
secrets: [process.env.COOKIE_SECRET],
secure: process.env.NODE_ENV === "production",
});
const refreshTokenCookie = createCookie("refresh_token", {
path: "/",
httpOnly: true,
secrets: [process.env.COOKIE_SECRET],
secure: process.env.NODE_ENV === "production",
}); And nor this: class {
async getLogoutHeaders() {
return [
["Set-Cookie", await idTokenCookie.serialize("", { maxAge: 0 })],
["Set-Cookie", await refreshTokenCookie.serialize("", { maxAge: 0 })],
];
}
} And nor this: class {
async getLogoutHeaders() {
const headers = new Headers();
headers.append(
"Set-Cookie",
await idTokenCookie.serialize("", { maxAge: 0 })
);
headers.append(
"Set-Cookie",
await refreshTokenCookie.serialize("", { maxAge: 0 })
);
return headers;
}
} |
A temporary solution from my side was to merge cookies: class {
async getLogoutHeaders() {
const headers = new Headers();
headers.append(
"Set-Cookie",
await idTokenCookie.serialize("", { maxAge: 0 })
);
headers.append(
"Set-Cookie",
await refreshTokenCookie.serialize("", { maxAge: 0 })
);
return new Headers([["Set-Cookie", headers.get("Set-Cookie") || ""]]);
}
} Yet, I do not think the solution is great. |
I haven't found a stable solution for this issue which works in production mode yet. I don't fully understand why, my best guess is that is because of race conditions. My case: I tried to delete a cookie1 and create a new cookie2 in the same header. My workaround was to delete cookie1 on one route, redirect to another route where cookie2 is added and redirect again to the final target route. That worked for me, but your workaround seems more elegant than mine. What I can't really explain is that the solution I posted previously, worked in production for about a month and after updating the project, it broke. |
@wenzf have you tried combining them, as I did? I also noticed that despite being combined, on local it still sends cookies separately, and in production it does indeed split them apart. Seems like it could indeed be the difference in server implementation, I am using |
@alfredsgenkins I haven't tried your solution combining the cookies yet, but I'll give it a try for sure in future! I'm using SST looks very interesting btw., I need to have a closer look at that. Would be great having an alternative to |
@alfredsgenkins this might be related: sst/ion#702 |
I tried copying
concatSetCookieHeaders
from remix but that... eh, doesn't work... Doing that I end up with a single set-cookie header.Here's the relevant code:
What I need is two
Set-Cookie
headers, and this gives me a singleSet-Cookie
header that doesn't end up updating either one.If I use either of these cookie values by itself then they each work as expected. But I need to set them both in the same response.
The text was updated successfully, but these errors were encountered: