Skip to content

cookies/headers does not work in NestJS (Fastify Adapter Only) #992

@ctjhoa

Description

@ctjhoa

Environment

orpc@1.8.8, Node v22.18.0

Reproduction

https://stackblitz.com/edit/github-ztmtcix4-ijyupsmj?file=src%2Fauth%2Fauth.controller.ts

Describe the bug

After having setup the ResponseHeadersPlugin to be able to set a cookie response in Nestjs, the cookie header is not set in the Nestjs response.

Image

Additional context

I tried 2 workarounds:

  • Create a NestInterceptor to set the response on the request object and give the request as ORPC context. Without success.
  • Using outputStructure: 'detailed' on route declaration allows me to manually define the shape of the response but it shouldn't be such burden. Specially when you need to combine it CORSPlugin.
export const loginContract = oc
  .route({
    method: 'POST',
    path: '/auth/login',
    tags: ['Auth', 'Login'],
    outputStructure: 'detailed',
  })
  .input(loginInputSchema)
  .output(
    z.object({
      headers: z.object({
        'Set-Cookie': z.string(),
      }),
      status: z.literal(200),
      body: loginOutputSchema,
    }),
  );
  @Implement(appRouter.auth.login)
  login() {
    return implement(appRouter.auth.login).handler(async ({ input }) => {
      this.logger.log('ORPC login request');
      const user = await this.authService.validateUser(input.email, input.password);
      if (!user) {
        throw new ORPCError('BAD_REQUEST');
      }
      const accessToken = await this.authService.login(user);
      return {
        status: 200,
        headers: {
          'Set-Cookie': serializeCookie('access_token', accessToken, {
            httpOnly: true,
            path: '/',
          }),
        },
        body: user,
      };
    });
  }

Logs

{
  "log": {
    "version": "1.2",
    "creator": {
      "name": "Firefox",
      "version": "141.0.3"
    },
    "browser": {
      "name": "Firefox",
      "version": "141.0.3"
    },
    "pages": [
      {
        "id": "page_1",
        "pageTimings": {
          "onContentLoad": -58483,
          "onLoad": -58216
        },
        "startedDateTime": "2025-09-12T19:50:15.972+02:00",
        "title": "https://githubztmtcix4ijyupsmj-a335--3000--96435430.local-corp.webcontainer.io/#tag/authentication/post/auth/signin"
      }
    ],
    "entries": [
      {
        "startedDateTime": "2025-09-12T19:50:15.972+02:00",
        "request": {
          "bodySize": 71,
          "method": "POST",
          "url": "http://localhost:3000/auth/signup",
          "httpVersion": "HTTP/1.1",
          "headers": [
            {
              "name": "Host",
              "value": "localhost:3000"
            },
            {
              "name": "User-Agent",
              "value": "Mozilla/5.0 (X11; Linux x86_64; rv:141.0) Gecko/20100101 Firefox/141.0"
            },
            {
              "name": "Accept",
              "value": "*/*"
            },
            {
              "name": "Accept-Language",
              "value": "en-US,en;q=0.5"
            },
            {
              "name": "Accept-Encoding",
              "value": "gzip, deflate, br, zstd"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Content-Length",
              "value": "71"
            },
            {
              "name": "Origin",
              "value": "https://githubztmtcix4ijyupsmj-a335--3000--96435430.local-corp.webcontainer.io"
            },
            {
              "name": "Sec-Fetch-Dest",
              "value": "empty"
            },
            {
              "name": "Sec-Fetch-Mode",
              "value": "cors"
            },
            {
              "name": "Sec-Fetch-Site",
              "value": "cross-site"
            },
            {
              "name": "authorization",
              "value": "Bearer default-token"
            },
            {
              "name": "Connection",
              "value": "keep-alive"
            }
          ],
          "cookies": [],
          "queryString": [],
          "headersSize": 0,
          "postData": {
            "mimeType": "application/json",
            "params": [],
            "text": "{\"name\": \"John Doe\",\n  \"email\": \"john@doe.com\",\n  \"password\": \"123456\"}"
          }
        },
        "response": {
          "status": 200,
          "statusText": "OK",
          "httpVersion": "HTTP/1.1",
          "headers": [
            {
              "name": "connection",
              "value": "keep-alive"
            },
            {
              "name": "content-type",
              "value": "application/json"
            },
            {
              "name": "cross-origin-embedder-policy",
              "value": "require-corp"
            },
            {
              "name": "cross-origin-opener-policy",
              "value": "same-origin"
            },
            {
              "name": "cross-origin-resource-policy",
              "value": "cross-origin"
            },
            {
              "name": "date",
              "value": "Fri, 12 Sep 2025 17:50:15 GMT"
            },
            {
              "name": "keep-alive",
              "value": "timeout=5"
            },
            {
              "name": "transfer-encoding",
              "value": "chunked"
            },
            {
              "name": "x-powered-by",
              "value": "Express"
            }
          ],
          "cookies": [],
          "content": {
            "mimeType": "application/json",
            "size": 86,
            "text": "{\"id\":\"28aa6286-48e9-4f23-adea-3486c86acd55\",\"name\":\"John Doe\",\"email\":\"john@doe.com\"}"
          },
          "redirectURL": "",
          "headersSize": 0,
          "bodySize": 0
        },
        "cache": {},
        "timings": {
          "blocked": 0,
          "dns": 0,
          "connect": 0,
          "ssl": 0,
          "send": 0,
          "wait": 0,
          "receive": 0
        },
        "time": 0,
        "_securityState": "insecure",
        "pageref": "page_1"
      }
    ]
  }
}

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions