Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[useResponseCache] Provide server context to define session Id #3282

Open
santino opened this issue May 19, 2024 · 3 comments · May be fixed by #3285
Open

[useResponseCache] Provide server context to define session Id #3282

santino opened this issue May 19, 2024 · 3 comments · May be fixed by #3285

Comments

@santino
Copy link

santino commented May 19, 2024

Hello folks,
I am struggling to properly implement session-based caching for useResponseCache Yoga plugin.

I don't have any usable header with a fully formed user id passed within the client request. Instead, I have cookies which are handled by my Fastify server instance through an onRequest hook.
Ultimately if a valid user session can be derived from the cookies, a user/session id will be decorated within the request object of my server.

Here comes the problem with useResponseCache. The session function only gets passed the yoga request object which is a PonyfillRequest instance.
Since this does not receive the req object from my server, I have no chance to read my decorated object containing information about the user session.
This is effectively a blocker that makes this plugin unusable for me.

To work around this issue I had to create a custom useResponseCache plugin in order to provide the serverContext to the session function. In this case, I am duplicating the Yoga plugin and I will need to keep it up to date.
Maybe you can consider passing a more complete request object to the session function or more arguments to include the server context in its entirety.

I am pasting the code I used for my custom implementation of the plugin. I literally added 3 lines of code to just pass the serverContext between onRequest and onParams so it can be provided to the sessions function.

export function useResponseCache (options) {
  const buildResponseCacheKey =
    options?.buildResponseCacheKey || defaultBuildResponseCacheKey
  const cache = options.cache ?? createInMemoryCache()
  const enabled = options.enabled ?? (() => true)
  let logger
  let servContext

  return {
    onYogaInit ({ yoga }) {
      logger = yoga.logger
    },
    onPluginInit ({ addPlugin }) {
      addPlugin(
        useEnvelopResponseCache({
          ...options,
          enabled ({ request }) {
            return enabled(request)
          },
          cache,
          getDocumentString: getDocumentStringForEnvelop,
          session: sessionFactoryForEnvelop,
          buildResponseCacheKey: cacheKeyFactoryForEnvelop,
          shouldCacheResult ({ cacheKey, result }) {
            let shouldCache
            if (options.shouldCacheResult) {
              shouldCache = options.shouldCacheResult({ cacheKey, result })
            } else {
              shouldCache = !result.errors?.length
              if (!shouldCache) {
                logger.debug(
                  '[useResponseCache] Decided not to cache the response because it contains errors'
                )
              }
            }
            if (shouldCache) {
              const extensions = (result.extensions ||= {})
              const httpExtensions = (extensions.http ||= {})
              const headers = (httpExtensions.headers ||= {})
              headers.ETag = cacheKey
              headers['Last-Modified'] = new Date().toUTCString()
            }
            return shouldCache
          }
        })
      )
    },
    async onRequest ({ request, fetchAPI, endResponse, serverContext }) {
      servContext = serverContext
      if (enabled(request)) {
        const operationId = request.headers.get('If-None-Match')
        if (operationId) {
          const cachedResponse = await cache.get(operationId)
          if (cachedResponse) {
            const lastModifiedFromClient = request.headers.get(
              'If-Modified-Since'
            )
            const lastModifiedFromCache =
              cachedResponse.extensions?.http?.headers?.['Last-Modified']
            if (
              // This should be in the extensions already but we check it here to make sure
              lastModifiedFromCache != null &&
              // If the client doesn't send If-Modified-Since header, we assume the cache is valid
              (lastModifiedFromClient == null ||
                new Date(lastModifiedFromClient).getTime() >=
                  new Date(lastModifiedFromCache).getTime())
            ) {
              const okResponse = new fetchAPI.Response(null, {
                status: 304,
                headers: {
                  ETag: operationId
                }
              })
              endResponse(okResponse)
            }
          }
        }
      }
    },
    async onParams ({ params, request, setResult }) {
      const sessionId = await options.session(servContext.req)
      const operationId = await buildResponseCacheKey({
        documentString: params.query || '',
        variableValues: params.variables,
        operationName: params.operationName,
        sessionId,
        request
      })
      operationIdByRequest.set(request, operationId)
      sessionByRequest.set(request, sessionId)
      if (enabled(request)) {
        const cachedResponse = await cache.get(operationId)
        if (cachedResponse) {
          const responseWithSymbol = {
            ...cachedResponse,
            [Symbol.for('servedFromResponseCache')]: true
          }
          if (options.includeExtensionMetadata) {
            setResult(resultWithMetadata(responseWithSymbol, { hit: true }))
          } else {
            setResult(responseWithSymbol)
          }
        }
      }
    }
  }
}
@ardatan
Copy link
Collaborator

ardatan commented May 20, 2024

We can change the sessionId factory to pass the server context as the second parameter but I am curious what do you need from the server context specifically?

@santino
Copy link
Author

santino commented May 20, 2024

The request object from my server context has a number of decorators that can be useful to define the session.
In my specific case, I add an authenticatedUser object containing information retrieved and validated from an authentication cookie.

Within the opaque Yoga request object I just have the parameters passed with the request. So I can see the plain cookie string which also contains my authentication cookie. But then I'd need to split it; extract the authentication cookie, parse the JWT, validate it, and finally extract the userId.

I am doing all this stuff within my Fastify decorator and I don't certainly want to process the cookie string manually and process the authentication cookie for the second time; considering all the data I need is available in the server context.

@santino
Copy link
Author

santino commented May 31, 2024

Hello folks,
what's your plan on #3285 can I help with anything to get the PR merged?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants