Multiple Set-Cookie headers are merged/dropped by Railway proxy; only one cookie reaches the client
the-skeleton
PROOP

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

  1. Deploy a NestJS (or Express) app that sets two cookies in one response.
  2. Call the endpoint (e.g. /auth/sign-in) from a browser or Swagger.
  3. Inspect the response headers and browser cookies.
  4. 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.

$20 Bounty

9 Replies

sam-a
EMPLOYEE

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.

the-skeleton
PROOP

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


Railway
BOT

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


ilyass012
FREE

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`?

the-skeleton
PROOP

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`

ilyass012
FREE

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


the-skeleton
PROOP

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

the-skeleton
PROOP

2 months ago

Sorry, didn't tagged you ^


the-skeleton

Sorry, didn't tagged you ^

ilyass012
FREE

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


Welcome!

Sign in to your Railway account to join the conversation.

Loading...