trpc-openapi icon indicating copy to clipboard operation
trpc-openapi copied to clipboard

Feat: Fastify adapter

Open SeanCassiere opened this issue 3 years ago • 9 comments

Not particularly the cleanest implementation, but I got an adapter working for Fastify.

https://github.com/SeanCassiere/fastify-trpc-openapi-adapter/blob/master/src/fastify.ts.

Let me know what you think. It's definitely a bit jank since the types for Fastify's request and reply objects don't exactly overlap all the types in NodeHTTPRequest & NodeHTTPResponse.

SeanCassiere avatar Jul 24 '22 12:07 SeanCassiere

Hi @SeanCassiere - thanks for this! We can add this fastify adapter to the trpc-openapi repo if you want to open an PR. Happy to work with you on this - we can use https://github.com/trpc/trpc/tree/main/packages/server/src/adapters/fastify as a battle tested pattern to follow.

jlalmes avatar Jul 26 '22 10:07 jlalmes

I actually forgot to mention sachinraja/uttp when I wrote my earlier message. This could be a good solution - any thoughts?

jlalmes avatar Jul 26 '22 15:07 jlalmes

@jlalmes I can try doing a PR with it if you want. I probably need to add some things before this package can use uttp anyway.

sachinraja avatar Jul 26 '22 18:07 sachinraja

Sorry was out of sick for the last couple of days. Will get on this tomorrow.

As mentioned in your comment above, I'll match the pattern being used.

Any thoughts on the uttp thing? Or shall I do what I'm currently doing by remapping the request and reply the methods to match what is used by the Node HTTP service (https://github.com/SeanCassiere/fastify-trpc-openapi-adapter/blob/master/src/fastify.ts#L37-L42). @jlalmes

SeanCassiere avatar Aug 18 '22 17:08 SeanCassiere

Hi @SeanCassiere, sorry for the super late reply!

Or shall I do what I'm currently doing by remapping the request and reply the methods to match what is used by the Node HTTP service

If you're still keen to work on this then I think this is the route we should take :)

jlalmes avatar Oct 14 '22 10:10 jlalmes

Hi @jlalmes, no sweat, I should have followed up on this as well.

I'll get on it 👍🏼.

SeanCassiere avatar Oct 14 '22 16:10 SeanCassiere

@jlalmes just for confirmation, as this is for the 1.0.0 release - #91, I'll be pulling a branch off of next and NOT master.

SeanCassiere avatar Oct 14 '22 16:10 SeanCassiere

... the road so far

  • Added the adapter in this branch.
  • Added an example project, with tRPC, trpc-openapi, and swagger UI being set up in examples/with-fastify.
  • For the time being, I've copied over the same tests being used for the Express adapter, and altered the set-up process to run them on a fastify server with the new fastify adapter (which they are passing all of them 😊).
  • Added a super minimal set-up in the README.

SeanCassiere avatar Oct 14 '22 20:10 SeanCassiere

@jlalmes in the testing of the fastify adapter, I've run into a blocker which requires more intimate knowledge on how the req and res objects are piped through the trpc-openapi plugin for the createContext function.

Scenarios

Please see the two scenarios below using Fastify:

1. Reading the headers in createContext

If I want to access the Authorization header using the @trpc/server/adapters/fastify plugin, I can access it using the following.

const createContext = async ({ req }: CreateFastifyContextOptions) => {
  console.log(req.headers.authorization);
  ...
}

However, in trpc-openapi, this is not possible since req.headers are undefined. As an alternative, the following has to be done to get to the header.

const createContext = async ({ req }: CreateFastifyContextOptions) => {
  console.log(req.raw.headers.authorization);
  ...
}

2. Settings headers in createContext

To set the x-request-id header with the @trpc/server/adapters/fastify plugin, it can be done using the following.

const createContext = async ({ res }: CreateFastifyContextOptions) => {
  const requestId = uuidv4();
  res.header('x-request-id', requestId);
  ...
}

However, in trpc-openapi, this is not possible since the res.header() function is not available, and instead the res.setHeader() has to be used.

const createContext = async ({ res }: CreateFastifyContextOptions) => {
  const requestId = uuidv4();
  res.setHeader('x-request-id', requestId);
  ...
}

Current fix/patch* in the createContext that works

Currently, in the createContext function, this can be worked around if you dropdown to using the raw objects stored in the req and res options.

const createContext = async ({ req, res }: CreateFastifyContextOptions) => {
  const requestId = uuidv4();
  res.raw.setHeader('x-request-id', requestId);
  if (req.raw.headers.authorization) {
    const user = jwt_decode(req.raw.headers.authorization);
  }
  ...
}

All-in-all, from what I can gather, the req and res objects being provided to the createContext function, are not the same as the ones being passed in for the primary tRPC fastify plugin.

SeanCassiere avatar Oct 14 '22 23:10 SeanCassiere

Hey @SeanCassiere, hows the blocker going? Would be awesome if we could use this fastify-adapter in an upcoming project soon. Anything I could help with maybe?

Asher-JH avatar Oct 20 '22 04:10 Asher-JH

Hey @SeanCassiere, hows the blocker going? Would be awesome if we could use this fastify-adapter in an upcoming project soon. Anything I could help with maybe?

@Asher-JH, In it's current state the adapter is certainly usable, however, it wouldn't have the 'feel' of interacting with the first party Fastify Request and Response objects, rather you'd have to drop down to using the raw ones instead.

I've tried a few things to give it a shot, but I cannot understand why the Fastify methods are being stripped out when they arrive at the createContext function.

Need help from @jlalmes , as he's both the library author and quite a thorough understanding of how the official tRPC works.

SeanCassiere avatar Oct 20 '22 06:10 SeanCassiere

Looks great, thanks @SeanCassiere 🙌. Just opened this PR (#170) on your behalf - I will take a closer look over the weekend!

jlalmes avatar Oct 21 '22 18:10 jlalmes

I've tried a few things to give it a shot, but I cannot understand why the Fastify methods are being stripped out when they arrive at the createContext function.

Fastify's request and reply object are very funky when you try to clone them in any way.

Spreading the request and reply object removes all inherited/prototype stuff from it, so that didn't work.

Using Object.assign has some weird behavior if trustProxy is enabled, but it does work fine for the reply.

And for the request, we can pass it the original reference to openApiHttpHandler but set request.raw.url before (request.url is just a getter for request.raw.url)

Working snippet:

fastify.all(`${prefix}/*`, async (request, reply) => {
    const prefixRemovedFromUrl = request.url.replace(prefix, '')
    request.raw.url = prefixRemovedFromUrl
    return await openApiHttpHandler(
      request,
      Object.assign(reply, {
        setHeader: (key: string, value: string | number | readonly string[]) => {
          if (Array.isArray(value)) {
            value.forEach((v) => reply.header(key, v))
            return reply
          }

          return reply.header(key, value)
        },
        end: (body: any) => reply.send(body)
      }),
    )
  })

keifufu avatar Nov 02 '22 16:11 keifufu

Thanks, @keifufu, I'll be pushing up your version of the call as it does look better without the spread syntax.


Overall, this Fastify Request object is proving to be quite fickle. I've also tried using structuredClone() for the Request as well, but it too leads to the same results.

It is truly quite frustrating, since accessing headers and cookies are something that could be done in the createContext as well as in the trpc-router procedures as well, and as such need to be consistent.

As of now, in its current state, the fastify-adapter for the plugin can be used in a project, HOWEVER, you'd have to ensure that all your queries, mutations, and middleware, access the request object items using req.raw.

SeanCassiere avatar Nov 02 '22 18:11 SeanCassiere

It is truly quite frustrating, since accessing headers and cookies are something that could be done in the createContext as well as in the trpc-router procedures as well, and as such need to be consistent.

Just making sure you understand that with the snippet I posted the objects don't get stripped anymore. Accessing headers, cookies and such works as expected. The changes I did were not done for looks :^)

keifufu avatar Nov 03 '22 06:11 keifufu

@keifufu I appreciate the snippet you posted is not just for aesthetic purposes, but I think I may be missing something for the request object being held intact.

Could you confirm in the examples/with-fastify/src/router.ts file, that you are able to access the headers in the createContext function using req.headers, without dropping down to req.raw.headers?

This is the one I'm using to test it.

export const createContext = async ({
  req,
  res,
}: // eslint-disable-next-line @typescript-eslint/require-await
CreateFastifyContextOptions): Promise<Context> => {
  const requestId = uuid();
  res.raw.setHeader('x-request-id', requestId);

  let user: User | null = null;

  console.log('createContext req.headers', req.headers);
  console.log('createContext req.raw.headers', req.raw.headers);
  try {
    if (req.raw.headers.authorization) {
      const token = req.raw.headers.authorization.split(' ')[1];
      const userId = jwt.verify(token, jwtSecret) as string;
      if (userId) {
        user = database.users.find((_user) => _user.id === userId) ?? null;
      }
    }
  } catch (cause) {
    console.error(cause);
  }

  return { user, requestId };
};

You can test it using first the Swagger Docs http://localhost:3000/docs (this pipes the OpenAPI request into the trpc router), and then directly accessing the trpc query via the browser http://localhost:3000/trpc/posts.getPosts?batch=1&input=%7B%220%22%3A%7B%22json%22%3Anull%2C%22meta%22%3A%7B%22values%22%3A%5B%22undefined%22%5D%7D%7D%7D.

SeanCassiere avatar Nov 03 '22 07:11 SeanCassiere

@SeanCassiere The request doesn't get stripped anymore because we don't clone it (or spread, or anything). We just pass the reference from fastify.

I can't do any tests right now, or for the next few days, but I was able to access headers, cookies and any other variable from inside createContext just fine with the snippet I provided!

Edit: Yes, without using req.raw

keifufu avatar Nov 03 '22 08:11 keifufu

@SeanCassiere The request doesn't get stripped anymore because we don't clone it (or spread, or anything). We just pass the reference from fastify.

I can't do any tests right now, or for the next few days, but I was able to access headers, cookies and any other variable from inside createContext just fine with the snippet I provided!

Edit: Yes, without using req.raw

Thanks!

I'll nuke my node_modules and maybe see if anything's going on with my install.

SeanCassiere avatar Nov 03 '22 09:11 SeanCassiere

Hello, Is there any news in regards to this issue/PR? Seems like the issues with headers were solved by @keifufu. I'll happily test it out if required :)

LucasLundJensen avatar Nov 25 '22 17:11 LucasLundJensen