encore icon indicating copy to clipboard operation
encore copied to clipboard

Generated Client `mustBeSet` check fails for `Set-Cookie`

Open tuckers-tech opened this issue 1 year ago • 7 comments

Hey all! Loving Encore TS! I ran into an issue with the generated client this evening.

I have an AuthResponse defined in the following way:

export interface AuthResponse {
  cookies: Header<'Set-Cookie'>
  user: User
}

When generated, it outputs the following:

public async login(params: LoginParams): Promise<AuthResponse> {
    // Now make the actual call to the API
    const resp = await this.baseClient.callAPI("POST", `/auth/login`, JSON.stringify(params))

    //Populate the return object from the JSON body and received headers
    const rtn = await resp.json() as AuthResponse
    rtn.cookies = mustBeSet("Header `set-cookie`", resp.headers.get("set-cookie"))
    return rtn
}

The mustBeSet call for set-cookie is failing because Set-Cookie is a forbidden response header. Encore itself is setting that header correctly, but the generated client should not expect to be able to access the Set-Cookie header.

tuckers-tech avatar Mar 20 '24 23:03 tuckers-tech

any solution for setting cookie headers from encore backend ?

theofrgs avatar Apr 17 '25 13:04 theofrgs

Also looking for solutions to setting cookies from encore.dev and reading same cookies in other api calls.

tribeless avatar May 02 '25 04:05 tribeless

@eandre

tribeless avatar May 02 '25 04:05 tribeless

Also waiting for fix, but here's our temporary solution:

import { middleware } from "encore.dev/api";

/**
 * Data type for storing Set-Cookie values.
 * Can be either a single string or an array of strings.
 */
export type SetCookie = string | string[];

/**
 * Middleware for correctly setting Set-Cookie headers in the response.
 * 
 * @param setCookieResponseFieldName - The name of the field in the payload from which Set-Cookie values will be extracted (default "setCookie")
 * @returns Middleware function that modifies the response by adding Set-Cookie headers
 * 
 * @example
 * ```ts
 * // Example of middleware usage
 * export default new Service("somService", {
 *   middlewares: [
 *     setCookieMiddleware(),
 *   ],
 * });
 * 
 * // Use the SetCookie type to declare the setCookie field in the response
 * export type SomeResponse = {
 *   setCookie?: SetCookie;
 * }
 * 
 * // In the controller code, simply add cookie data to the response
 * const refreshTokenCookie = serializeCookie('refreshToken', refreshToken, { maxAge: '2d' });
 * const deviceIdCookie: serializeCookie('deviceId', deviceId, { maxAge: '2d' });
 * 
 * const response = { setCookie: [refreshTokenCookie, deviceIdCookie]; };
 * 
 * ```
 * 
 * @remarks
 * The current implementation is necessary due to a known bug in the typing system (https://github.com/encoredev/encore/issues/1092).
 * After fixing the bug, it will be possible to use typing without middleware.
 */
export const setCookieMiddleware = (setCookieResponseFieldName: string = "setCookie") => {
  return middleware({ target: {} }, async (req, next) => {
    const resp = await next(req);
    
    if (setCookieResponseFieldName in resp.payload) {
      const setCookieValues = resp.payload[setCookieResponseFieldName] as SetCookie;  

      resp.header.set('Set-Cookie', setCookieValues);
      delete resp.payload[setCookieResponseFieldName];
    }

    return resp;
  })
}

And serializeCookie looks like:

import c from 'cookie';
import type { StringValue } from 'ms';
import ms from 'ms';

export type CookieOptions = Omit<c.SerializeOptions, 'maxAge'> & {
  maxAge?: string
}

const defaultOption: CookieOptions = {
  sameSite: 'lax',
  path: '/',
  secure: false,
  httpOnly: true,
  maxAge: '1d',
}

export const serializeCookie = (name: string, value: string, options?: CookieOptions) => {
  const maxAge = options?.maxAge ?? defaultOption.maxAge!;

  return c.serialize(name, value, {
    ...defaultOption,
    ...options,
    maxAge: Math.ceil(ms(maxAge as StringValue) / 1000),
  });
}

roman-komarov avatar May 02 '25 09:05 roman-komarov

Thanks for this @roman-komarov , seems I'll have to rely on the middleware the return and extract the cookies

tribeless avatar May 02 '25 09:05 tribeless

Hey guys, this is a pretty neat and simple approach to setting and reading cookies. Works for me, tested and approved https://github.com/encoredev/encore/issues/1895

tribeless avatar May 08 '25 05:05 tribeless

As of encore v1.48.0 we no longer run mustBeSet for the set-cookie header in a browser context. But we've also added support for adding cookies in your api schemas, see https://encore.dev/docs/ts/primitives/cookies

fredr avatar May 28 '25 09:05 fredr