fastify/fastify-websocket

WebsocketHandler Request generics don't work the same as http Handlers

mattbishop opened this issue ยท 7 comments

๐Ÿ› Bug Report

In a TypeScript project, I am using the new 3.0.0 WebsocketHandler interface that provides FastifyRequest. I want to declare the request Header type so I can access them without casting or using //@ts-ignore as I do with HTTP request handlers.

Here is an example

  server.get("/ws",  {
      websocket: true,
      schema: {
        headers: TenantHeaderSchema
      }
    },
    // compiler error, see below
    createWSHandler())

...

function createWSHandler() {
  return (conn:     SocketStream,
          request:  FastifyRequest<{Headers: TenantHTTPHeader}>) => {
    const {tenant} = request.headers
...

Here is the compiler error:

 Overload 1 of 4, '(path: string, opts: RouteShorthandOptions<Server, IncomingMessage, ServerResponse, RequestGenericInterface, unknown> & { ...; }, handler?: WebsocketHandler | undefined): FastifyInstance<...>', gave the following error.
    Argument of type '(conn: SocketStream, request: FastifyRequest<{    Headers: TenantHTTPHeader;}>) => void' is not assignable to parameter of type 'WebsocketHandler'.
      Types of parameters 'request' and 'request' are incompatible.
        Type 'FastifyRequest<RouteGenericInterface, Server, IncomingMessage>' is not assignable to type 'FastifyRequest<{ Headers: TenantHTTPHeader; }, Server, IncomingMessage>'.
          Type 'RouteGenericInterface' is not assignable to type '{ Headers: TenantHTTPHeader; }'.
            Types of property 'Headers' are incompatible.
              Type 'unknown' is not assignable to type 'TenantHTTPHeader'.

Expected behavior

I expect the FastifyRequest declaration to accept Headers declarations as they do in other get() handlers. An example that works with the same schema and types is below:

  server.get("/data", {
      schema: {
        headers: TenantHeaderSchema
      }
    },
    createGetDataHandler())

...

function createGetDataHandler() {
  return async (request:  FastifyRequest<{Headers: TenantHTTPHeader}>,
                reply:    FastifyReply) => {
    const {tenant} = request.headers

Your Environment

  • node version: 12.20.0
  • fastify version: >=3.11.0
  • fastify-websocket version: 3.0.0
  • os: Mac

cc @fastify/typescript

Would you like to send a Pull Request to address this issue? Remember to add unit tests.

Hmm this is a tough one. Can you provide a simple reproduction I could run locally?

Here is a standalone typescript module that illustrates the type error. Note the http and ws handlers both declare the Headers generic, but only the http handler is error-free.

import fastify, {FastifyRequest, FastifyReply} from "fastify"
import {SocketStream} from "fastify-websocket"


export interface TenantHTTPHeader {
  tenant: string;
}

const server = fastify()
server.register(require("fastify-websocket"))


const handler = async (request: FastifyRequest<{ Headers: TenantHTTPHeader }>,
                       reply:   FastifyReply): Promise<void> => {
  const {tenant} = request.headers
  reply.send(`Hi tenant ${tenant}`)
}

const wsHandler = (conn:    SocketStream,
                   request: FastifyRequest<{ Headers: TenantHTTPHeader }>): void => {
  const {tenant} = request.headers
  conn.socket.on("message", message => {
    conn.socket.send(`tenant ${tenant} sent message ${message}`)
  })
}

server.route({
  method: "GET",
  url: "/ws-hello",
  // No error
  handler,
  // error
  wsHandler
})

Hmm, I'm not sure -- consider looking into the type tests in this module and see if there's something missing.

I've been trying to figure out a solution for this, the following change seems to remove the specified error (however it introduces another one):

diff --git a/index.d.ts b/index.d.ts
index a450933..f66997d 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -1,10 +1,11 @@
 /// <reference types="node" />
 import { IncomingMessage, ServerResponse, Server } from 'http';
-import { FastifyRequest, FastifyPluginCallback, RawServerBase, RawServerDefault, RawRequestDefaultExpression, RawReplyDefaultExpression, RequestGenericInterface, ContextConfigDefault, FastifyInstance } from 'fastify';
+import { FastifyRequest, FastifyPluginCallback, FastifyLoggerInstance, RawServerBase, RawServerDefault, RawRequestDefaultExpression, RawReplyDefaultExpression, RequestGenericInterface, ContextConfigDefault, FastifyInstance } from 'fastify';
 import * as fastify from 'fastify';
 import * as WebSocket from 'ws';
 import { Duplex } from 'stream';
 import { FastifyReply } from 'fastify/types/reply';
+import { RouteGenericInterface } from 'fastify/types/route';
 
 interface WebsocketRouteOptions {
   wsHandler?: WebsocketHandler
@@ -17,6 +18,18 @@ declare module 'fastify' {
   }
 
   interface FastifyInstance<RawServer, RawRequest, RawReply> {
+    route<
+      RawServer extends RawServerBase = RawServerDefault,
+      RawRequest extends RawRequestDefaultExpression<RawServer> = RawRequestDefaultExpression<RawServer>,
+      RawReply extends RawReplyDefaultExpression<RawServer> = RawReplyDefaultExpression<RawServer>,
+      RouteGeneric extends RouteGenericInterface = RouteGenericInterface,
+      ContextConfig = ContextConfigDefault,
+      Logger extends FastifyLoggerInstance = FastifyLoggerInstance,
+    >(
+      opts: fastify.RouteOptions<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig> & { wsHandler: WebsocketHandler<RawServer, RawRequest, RouteGeneric> }
+      ): FastifyInstance<RawServer, RawRequest, RawReply, Logger>;
+  
+
     get: RouteShorthandMethod<RawServer, RawRequest, RawReply>
     websocketServer: WebSocket.Server,
   }
@@ -33,7 +46,7 @@ declare module 'fastify' {
     ): FastifyInstance<RawServer, RawRequest, RawReply>;
   }
 
-  interface RouteOptions extends WebsocketRouteOptions {}
 }
 
 declare const websocketPlugin: FastifyPluginCallback<WebsocketPluginOptions>;

In the types test file, this section now gives an error because RouteOptions is no longer extended inside the fastify module declaration to add the wsHandler property (But if I import it from the typings of this package, the error goes away):

const augmentedRouteOptions: RouteOptions = {
  method: 'GET',
  url: '/route-with-exported-augmented-route-options',
  handler: (request, reply) => {
    expectType<FastifyRequest>(request);
    expectType<FastifyReply>(reply);
  },
  wsHandler: (connection, request) => {
    expectType<SocketStream>(connection);
    expectType<FastifyRequest<RequestGenericInterface>>(request)
  },
};

Error:

Type '{ method: "GET"; url: string; handler: (this: FastifyInstance<...>, request: FastifyRequest<RouteGenericInterface, Server, IncomingMessage>, reply: FastifyReply<...>) => void; wsHandler: (connection: any, request: any) => void; }' is not assignable to type 'RouteOptions<Server, IncomingMessage, ServerResponse, RouteGenericInterface, unknown, FastifySchema>'.
  Object literal may only specify known properties, but 'wsHandler' does not exist in type 'RouteOptions<Server, IncomingMessage, ServerResponse, RouteGenericInterface, unknown, FastifySchema>'. Did you mean to write 'handler'?

@Ethan-Arrowood Is this an acceptable approach? I'm not that proficient with typescript so I'm not sure if this might introduce some unwanted behavior

Could you merge over the RouteOptions type instead?

This issue has been fixed by #112

Thanks!