3 months ago
Our NestJS API sets two auth cookies on sign-in and refresh: at (access token) and rt (refresh token). Each is set with a separate res.cookie() call, so the response includes two distinct Set-Cookie headers.
Expected behavior
The response should contain two separate Set-Cookie headers, for example:
Set-Cookie: at=; Path=/; HttpOnly; Secure; SameSite=Lax
Set-Cookie: rt=; Path=/; HttpOnly; Secure; SameSite=Lax
Actual behavior
- Locally: both cookies are set correctly.
- On Railway: only one cookie reaches the browser (in our tests, only
rt). - With Cloudflare proxy disabled (DNS only) for the API domain, the same behavior occurs, so the issue appears to be in Railway’s proxy layer rather than Cloudflare.
Technical context
Per RFC 6265, multiple Set-Cookie headers must not be folded into a single header. Many proxies incorrectly merge headers with the same name into one comma-separated value, which breaks cookie parsing in browsers.
Steps to reproduce
- Deploy a NestJS (or Express) app that sets two cookies in one response.
- Call the endpoint (e.g.
/auth/sign-in) from a browser or Swagger. - Inspect the response headers and browser cookies.
- Observe that only one cookie is present.
Request
Please ensure Railway’s reverse proxy preserves multiple Set-Cookie headers as separate headers instead of merging or dropping them, so that applications can set more than one cookie per response.
9 Replies
2 months ago
Thanks for the detailed report. We've flagged this for our engineering team to investigate. The behavior you're describing, where only one of the two Set-Cookie headers reaches the client, does point to an issue in our proxy layer. We'll follow up here once we have more information.
Status changed to Awaiting User Response Railway • 2 months ago
sam-a
Thanks for the detailed report. We've flagged this for our engineering team to investigate. The behavior you're describing, where only one of the two Set-Cookie headers reaches the client, does point to an issue in our proxy layer. We'll follow up here once we have more information.
2 months ago
Thank you sam-a!
Status changed to Awaiting Railway Response Railway • 2 months ago
2 months ago
Hello,
I was not able to reproduce this. My test site was able to set two different Set-Cookie headers and have both headers maintained in full through to the end user.
Given that whatever you are running into that is causing your Set-Cookie headers to merge or drop is not on our end, I will open this thread to community help.
Best,
Brody
Attachments
2 months ago
This thread has been marked as public for community involvement, as it does not contain any sensitive or personal information. Any further activity in this thread will be visible to everyone.
Status changed to Open Railway • 2 months ago
Status changed to Awaiting User Response sam-a • 2 months ago
2 months ago
@the-skeleton what domain is the API on vs the frontend, and are you using withCredentials: true?
Status changed to Awaiting Railway Response Railway • 2 months ago
ilyass012
**@the-skeleton** what domain is the API on vs the frontend, and are you using `withCredentials: true`?
2 months ago
Hi ilyass012! same domain, different subdomain. Yes, we're using withCredentials: true
the-skeleton
Hi ilyass012! same domain, different subdomain. Yes, we're using `withCredentials: true`
2 months ago
can you share your res.cookie() calls in nestjs? specifically whether you're setting the domain attribute? because when api and frontend are on different subdomains without domain=.yourdomain.com set, browsers can silently drop or scope cookies incorrectly. that's the most likely culprit here but i don't want to say for sure without seeing your actual cookie config
2 months ago
We had set domain=.yourdomain.com too.
Test passing, local env working
async signIn(@Res({ passthrough: true }) res: Response): Promise<{ ok: boolean }> {
const command = new SignInCommand(body.email.trim().toLowerCase(), body.password, ip, userAgent)
const { accessToken, refreshToken } = await this.commandBus.execute(command)
setAuthCookies(res, accessToken, refreshToken)
return { ok: true }
}function setAuthCookies(res: Response, accessToken: string, refreshToken: string): void {
res.cookie(AUTH_COOKIES.accessToken, accessToken, getBaseOptions(COOKIE_MAX_AGE_ACCESS))
res.cookie(AUTH_COOKIES.refreshToken, refreshToken, getBaseOptions(COOKIE_MAX_AGE_REFRESH))
}ilyass012
can you share your res.cookie() calls in nestjs? specifically whether you're setting the domain attribute? because when api and frontend are on different subdomains without domain=.[yourdomain.com](http://yourdomain.com) set, browsers can silently drop or scope cookies incorrectly. that's the most likely culprit here but i don't want to say for sure without seeing your actual cookie config
2 months ago
Sorry, didn't tagged you ^
the-skeleton
Sorry, didn't tagged you ^
2 months ago
can you share what getBaseOptions() returns? specifically the full options object including domain, sameSite, secure, httpOnly, and maxAge values. that's the missing piece to figure out what's different between local and railway
Status changed to Open brody • 2 months ago