Jonathan's Blog

Accessing Fastify Sessions via tRPC Websockets

Posted in Javascript, Tips, Frontend, Programming

This is a quick post to point out a potential issue that might catch you out with using Fastify’s sessions mechanism alongside tRPC’s websockets transport, and how I’ve fixed it in my projects.

The problem happens with an application that looks something like this:

const app = Fastify();

app.register(ws);
app.register(fastifyCookie);
app.register(fastifySession, { secret: "..." });

app.register(fastifyTRPCPlugin, {
  prefix: "/trpc",
  useWSS: true,
  trpcOptions: {
    router,
    onError,
    createContext: ({ req }) => {
      console.log(req.session); // logs "undefined"
      return {};
    },
  },
});

The useWSS parameter passed to the tRPC plugin means that it can handle both standard HTTP requests and a persistent websocket connection. Theoretically, both of these kinds of requests get handled by Fastify first, and therefore both should be initialised with a session object1. In practice, though, the session field is missing on all websocket-based connections, but present for all HTTP-based connections.

The cause is that the Fastify adapter for tRPC delegates to the non-Fastify-specific websocket adapter. When that creates a context, the incoming req object is an instance of IncomingMessage, i.e. the underlying NodeJS request abstraction, which does not have the Fastify session details attached.

This probably should be better documented, ideally directly in the types (although that would probably be a fairly large breakage), or at least in the Fastify documentation.

The best solution I found is a WeakMap mapping IncomingMessage requests to FastifyRequest values, which have the expected session attribute. That looks something like this:

// create a new scope so that the hook we add later will only
// affect tRPC-specific requests
app.register((app) => {
  // use a WeakMap to avoid leaking memory by holding on to
  // requests longer than necessary
  const REQS = new WeakMap<
    FastifyRequest | IncomingMessage,
    FastifyRequest
  >();

  app.addHook("onRequest", async (req) => {
    // associate each raw `IncomingMessage` (`req.raw`) with
    // the original `IncomingMessage`
    REQS.set(req.raw, req);
  });

  app.register(fastifyTRPCPlugin, {
    prefix: "/trpc",
    useWSS: true,
    trpcOptions: {
      router,
      onError,
      createContext: ({ req }) => {
        // given either a `FastifyRequest` or an
        // `IncomingMessage`, fetch the related
        // `FastifyRequest` that we saved earlier
        const realReq = REQS.get(req.raw ?? req);
        if (!realReq)
          throw new Error("This should never happen");

        console.log(realReq.session); // logs the session object
        return {};
      },
    },
  });
});

Because the tRPC types aren’t correct for the req parameter of the createContext callback, you might need to fiddle with the Typescript types to get this to work properly. Specifically, the WeakMap type here is technically incorrect, but means that I can do REQS.get(req.raw ?? req) without Typescript complaining. This does cause an ESLint error (because according to the type definitions, req.raw can never be null), but I’d rather silence an ESLint error than a Typescript error.

I hope this helps you, or myself in six months’ time when I run into this issue again having forgotten about it completely.


  1. Indeed, this is also what the types will tell you — if you are using Typescript, req.session will have the type of the Fastify session object in the example above. ↩︎


Share this article on Reddit, X/Twitter, Bluesky, Hacker News, or Lobsters, or .

Comments, thoughts, or corrections? Send me an email at or contact me on social media.