Giter VIP home page Giter VIP logo

fastify-request-context's Introduction

@fastify/request-context

NPM Version NPM Downloads CI Coverage Status

Request-scoped storage support, based on Asynchronous Local Storage (which uses native Node.js ALS with fallback to cls-hooked for older Node.js versions)

Inspired by work done in @fastify/http-context.

This plugin introduces thread-local request-scoped http context, where any variables set within the scope of a single http call won't be overwritten by simultaneous calls to the api nor will variables remain available once a request is completed.

Frequent use-cases are persisting request-aware logger instances and user authorization information.

Getting started

First install the package:

npm i @fastify/request-context

Next, set up the plugin:

const { fastifyRequestContextPlugin } = require('@fastify/request-context')
const fastify = require('fastify');

fastify.register(fastifyRequestContextPlugin);

Or customize hook and default store values:

const { fastifyRequestContextPlugin } = require('@fastify/request-context')
const fastify = require('fastify');

fastify.register(fastifyRequestContextPlugin, { 
  hook: 'preValidation',
  defaultStoreValues: {
    user: { id: 'system' } 
  }
});

Default store values can be set through a function as well:

const { fastifyRequestContextPlugin } = require('@fastify/request-context')
const fastify = require('fastify');

fastify.register(fastifyRequestContextPlugin, {
  defaultStoreValues: request => ({
    log: request.log.child({ foo: 123 })
  })
});

This plugin accepts options hook and defaultStoreValues, createAsyncResource.

  • hook allows you to specify to which lifecycle hook should request context initialization be bound. Note that you need to initialize it on the earliest lifecycle stage that you intend to use it in, or earlier. Default value is onRequest.
  • defaultStoreValues / defaultStoreValues(req: FastifyRequest) sets initial values for the store (that can be later overwritten during request execution if needed). Can be set to either an object or a function that returns an object. The function will be sent the request object for the new context. This is an optional parameter.
  • createAsyncResource can specify a factory function that creates an extended AsyncResource object.

From there you can set a context in another hook, route, or method that is within scope.

Request context (with methods get and set) is exposed by library itself, but is also available as decorator on fastify.requestContext app instance as well as on req request instance.

For instance:

const { fastifyRequestContextPlugin, requestContext } = require('@fastify/request-context')
const fastify = require('fastify');

const app = fastify({ logger: true })
app.register(fastifyRequestContextPlugin, { 
  defaultStoreValues: {
    user: { id: 'system' } 
  },
  createAsyncResource: (req, context) => new MyCustomAsyncResource('custom-resource-type', req.id, context.user.id)
});

app.addHook('onRequest', (req, reply, done) => {
  // Overwrite the defaults.
  // This is completely equivalent to using app.requestContext or just requestContext 
  req.requestContext.set('user', { id: 'helloUser' });
  done();
});

// this should now get `helloUser` instead of the default `system`
app.get('/', (req, reply) => {
  // requestContext singleton exposed by the library retains same request-scoped values that were set using `req.requestContext`
  const user = requestContext.get('user');
  reply.code(200).send( { user });
});

app.get('/decorator', function (req, reply) {
  // requestContext singleton exposed as decorator in the fastify instance and can be retrieved:
  const user = this.requestContext.get('user'); // using `this` thanks to the handler function binding
  const theSameUser = app.requestContext.get('user'); // directly using the `app` instance
  reply.code(200).send( { user });
});

app.listen({ port: 3000 }, (err, address) => {
  if (err) throw err
  app.log.info(`server listening on ${address}`)
});

return app.ready()

TypeScript

In TypeScript you are expected to augment the module to type your context:

import {requestContext} from '@fastify/request-context'

declare module '@fastify/request-context' {
  interface RequestContextData {
    foo: string
  }
}

// Type is "string" (if "strictNullChecks: true" in your tsconfig it will be "string | undefined")
const foo = requestContext.get('foo')
// Causes a type violation as 'bar' is not a key on RequestContextData
const bar = requestContext.get('bar')

If you have "strictNullChecks": true (or have "strict": true, which sets "strictNullChecks": true) in your TypeScript configuration, you will notice that the type of the returned value can still be undefined even though the RequestContextData interface has a specific type. For a discussion about how to work around this and the pros/cons of doing so, please read this issue (#93).

fastify-request-context's People

Contributors

alfonsograziano avatar awlayton avatar darkgl0w avatar ddadaal avatar dependabot[bot] avatar eomm avatar fdawgs avatar gbalcewicz avatar github-actions[bot] avatar imjuni avatar jsumners avatar kibertoad avatar mcollina avatar salmanm avatar silenceisgolden avatar uzlopak avatar voxpelli avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

fastify-request-context's Issues

[FSTDEP006] FastifyDeprecation: You are decorating Request/Reply with a reference type. This reference is shared amongst all requests. Use onRequest hook instead.

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure the bug has not already been reported

Fastify version

3.22.0

Plugin version

websocket 4.0.0

Node.js version

16.4.2

Operating system

Windows

Operating system version (i.e. 20.04, 11.3, 10)

10

Description

I have this defined to augment FastifyRequest:

 declare module "fastify" {
  export interface FastifyRequest {
    config: {
      sessionParms: any;
    };
  }
}

I am getting this message:

[FSTDEP006] FastifyDeprecation: You are decorating Request/Reply with a reference type. This reference is shared amongst all requests. Use onRequest hook instead.

I have this defined in the onRequest hook :

server.addHook("onRequest", (request, reply, done) => {
  request.config.sessionParms = setSystem(apiParms, request, reply); // make sessionParms accessible 
  done();
});

This all looks to be correct according to the documentation.

Steps to Reproduce

See attached code above.

Expected Behavior

To not receive the deprecation error message..

Expose API for direct access to the context

๐Ÿš€ Feature Proposal

Expose request context (at least the CLS part of it) as a global object instead of (or as an alternative to) asking users to access it through req/app.

Motivation

The whole purpose of any CLS is to avoid having to pass a context object through application modules. Exposing request context through the req object kinda kills the whole idea. Having it available on app makes things a bit better as it could provide isolation guarantees for multiple Fastify instances running in a single node process, but it looks like the underlying asynchronous-local-storage library has a single global AsyncLocalStorage instance, which means no isolation as such.

Example

const { ctx } = require('fastify-request-context')

// Later in route handler
const foo = ctx.get('bar')

Customizable AsyncResource.type and create-resource

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure the feature has not already been requested

๐Ÿš€ Feature Proposal

  • pass AsyncResource factory
  • pass AsyncResource.type

index.js

fastify.addHook(hook, (req, res, done) => {
    const defaultStoreValues = hasDefaultStoreValuesFactory
      ? opts.defaultStoreValues(req)
      : opts.defaultStoreValues

    asyncLocalStorage.run({ ...defaultStoreValues }, () => {
      const asyncResource =
        opts.createResource != null
          ? opts.createResource(req, requestContext)
          : new AsyncResource(
              opts.resourceType != null ? opts.resourceType : 'fastify-request-context',
            )
      req[asyncResourceSymbol] = asyncResource
      asyncResource.runInAsyncScope(done, req.raw)
    })
  })

Motivation

When using TypeORM or Prisma, logging can be customized: there is a way to use the triggerId of the AsyncContext to find the AsyncResource in order to pass the request.id to the logger. This would require either changing the type of the AsyncResource, or a factory that allows developers to create AsyncResources in whatever form the developers want.

Example

// async_hooks.createHook
watcher
      .setupInitHook()
      .setupBeforeHook()
      .setupAfterHook()
      .setupDestroyHook()
      .setupPromiseResolveHook()
      .start()
      .on(
        'INIT',
        ({
          asyncId,
          type,
          resource,
          triggerAsyncId,
        }: {
          asyncId: number;
          type: string;
          triggerAsyncId: number;
          executionAsyncId: number;
          resource: object;
        }) => {
          idMap.set(asyncId, triggerAsyncId);

          // customized AsyncResource.type
          if (type === 'fastify-request-context' || type === 'tid-async-resource') {
            resourceMap.set(asyncId, resource);
          }
        },
      )
      
// getStore
getStore<T = AsyncResource>(asyncId: number) {
    let resource: object | undefined = resourceMap.get(asyncId);

    if (resource != null) {
      return resource as T;
    }

    let id = idMap.get(asyncId);
    let sentinel = 0;

    while (id != null && sentinel < 1000) {
      resource = resourceMap.get(id);

      if (resource != null) {
        return resource as T;
      }

      id = idMap.get(id);
      sentinel += 1;
    }

    return undefined;
  }

Node 16.2.0 with Jest break AsyncLocalStorage

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure it has not already been reported

Fastify version

3.x.x

Node.js version

16.2.0

Operating system

Windows

Operating system version (i.e. 20.04, 11.3, 10)

Linux, Windows, doesn't matter

Description

All tests are failing when being run on Node 16.2.0. Seems like all stored values are lost and are being resolved to undefined.

Considering that tests for https://github.com/kibertoad/asynchronous-local-storage are still passing with 16.2.0, looks like this particular plugin is doing something wrong.

Steps to Reproduce

Execute existing test suite on Node 16.2.0

Expected Behavior

Test suite passes.

lost context when use @fastify/multipart

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure the bug has not already been reported

Fastify version

4.15.0

Plugin version

4.2.0

Node.js version

18.14.0

Operating system

macOS

Operating system version (i.e. 20.04, 11.3, 10)

11.3

Description

when register multipart after context, the router frequently lost context info

Steps to Reproduce

const Fastify = require('fastify').default
const MultipartSupport = require('@fastify/multipart')
const { requestContext } = require('@fastify/request-context')
const RequestContextSupport = require('@fastify/request-context')

const server = Fastify({ logger: true })


server.register(RequestContextSupport, {
  defaultStoreValues: {
    user: { id: 0 },
  },
})

server.addHook('onRequest', async (req, reply) => {
  req.requestContext.set('user', { id: 1 })
})

server.register(MultipartSupport, {
  addToBody: true,
})

server.post('/', async (req, reply) => {
  console.log(req.requestContext.get('user'))

  return { message: 'ok' }
})

server.listen({ host: '0.0.0.0', port: 3002 })

use postman to send any file to http://127.0.0.1/,
console.log(req.requestContext.get('user')) sometimes are undefined

Expected Behavior

No response

ALS is deprecated and archived, Better to use AsyncLocalStorage class fom the async_hook std module

https://github.com/vicanso/async-local-storage is archived.

Working Example with the native AsyncLocalStorage:

// ctx.ts
import { AsyncLocalStorage } from "async_hooks";

const asyncLocalStorage = new AsyncLocalStorage<Map<string, unknown>>();

export const setInCtx = <T = unknown>(key: string, value: T): void => {
  asyncLocalStorage.getStore()?.set(key, value);
}

export const getFromCtx = <T = unknown>(key: string): T | undefined => asyncLocalStorage.getStore()?.get(key) as T;

export const initReqContext = (req: any, _res: any, next: any) => {
  asyncLocalStorage.run(new Map(), () => {
    const store = asyncLocalStorage.getStore();
    if (store){
      store.set("key",  "value");
    }
    next();
  });
}
// server.ts / index.ts 

import { initReqContext } from './ctx';

// this only works with preHandler hooks
server.addHook("preHandler", initReqContext)

you can start using get and set function to access the store

Default value persists if used wrongly

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure the issue has not already been raised

Issue

Hi,

When using a default value:

  fastify.register(fastifyRequestContextPlugin, 
  {
    hook: 'onRequest', 
    defaultStoreValues: {
      myDefault: {}
    }
  })

and later in the request handler doing this:

requestContext.get('myDefault').persistentData = "something between requests"

That persistentData property will "leak" between requests, I know this is not the intended use but if it is possible to prevent that, it could prevent a bug or exploitation.

Stricter default context interface?

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure the feature has not already been requested

๐Ÿš€ Feature Proposal

I would propose swapping this:

  export interface RequestContextData {
    [key: string]: any
  }

For plain:

  export interface RequestContextData {
  }

Motivation

This way someone like me who wants to get help ensuring I don't mistype any context names can use this plugin really strictly typed, and if someone wants to be lenient on the key names but strict with the types, then they could easily do:

declare module '@fastify/request-context' {
  interface RequestContextData {
    [key: string]: unknown
  }
}

And those who wants to have it like today can simply do:

declare module '@fastify/request-context' {
  interface RequestContextData {
    [key: string]: any
  }
}

By leaving out the default [key: string]: any it becomes possible to customize the type in ways otherwise not possible.

Example

No response

Using `strict` in a project `tsconfig.json` makes `requestContext.get(string)` possibly `undefined`

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure the bug has not already been reported

Fastify version

3.25.0

Plugin version

2.2.0

Node.js version

16.x

Operating system

macOS

Operating system version (i.e. 20.04, 11.3, 10)

12.0.1 Monterey

Description

With a project using fastify and fastify-request-context, the user could get confused because

declare module 'fastify-request-context' {
    interface RequestContextData {
       a: number;
    }
}

const value = request.requestContext.get('string');

and value is set to number | undefined. In the documentation it states that the value should be set to a type of number, but this is not always the case.

Steps to Reproduce

Create a new folder/nodejs project with these files.

package.json

{
  "name": "fastify-request-context-typescript",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "fastify": "^3.25.0",
    "fastify-request-context": "^2.2.0"
  },
  "devDependencies": {
    "@types/node": "^17.0.0"
  }
}

tsconfig.json

{
    "compilerOptions": {
        "strict": true // the cause of the issue
    }
}

app.ts

import fastify, { FastifyLoggerInstance } from 'fastify';
import { fastifyRequestContextPlugin } from 'fastify-request-context';

import { simpleController } from './a';

declare module 'fastify-request-context' {
    interface RequestContextData {
        logger: FastifyLoggerInstance;
    }
}

const server = fastify();

server.register((app) => {
    app.get('/', {
        handler: simpleController
    });
}, {
    prefix: '/helloworld',
})

server.register(fastifyRequestContextPlugin, {
    defaultStoreValues: {
        logger: server.log,
    },
});

a.ts

import { RouteHandler } from 'fastify';

export const simpleController: RouteHandler = async (req, reply) => {
    const logger = req.requestContext.get('logger');
    
    // logger is possibly undefined, so this is a typescript error
    logger.info('do a thing');

    reply.code(204);
};

The issue is due to this typing I think:

https://github.com/fastify/fastify-request-context/blob/master/index.d.ts#L8

Since the type is RequestContextData[K] | undefined, in strict typing mode I think that this causes logger above in a.ts to be FastifyLoggerInstance | undefined. This is not the case if you add "strictNullChecks": false to the tsconfig.json.

Expected Behavior

Could the .get method just be typed as get<K extends keyof RequestContextData>(key: K): RequestContextData[K]? This seems to work with "strictNullChecks" set to either true (implicitly by "strict": true), or with "strict": true and "strictNullChecks": false.

Plugin not working with fastify 3.x

Encountering this issue while trying to use fastify-request-context with fastify 3.x
Error: fastify-plugin: fastify-request-context - expected '2.x' fastify version, '3.1.1' is installed

Enable default context items derived from `request`

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure the feature has not already been requested

๐Ÿš€ Feature Proposal

In #136 / #137 defaultStoreValues was allowed to be a factory, but no parameters was actually sent to it.

I would like it if it was sent the request instance as an argument.

Motivation

Sending request as an argument to the defaultStoreValues factory allows one to easily bootstrap the request context with values derived from the request, which is something I have been doing for eg a logging function.

I will do a PR.

Example

fastify.register(fastifyRequestContextPlugin, { 
  hook: 'preValidation',
  defaultStoreValues: () => ({
    log: request.log.child({ foo: 'bar }),
  }),
});

FastifyDeprecation: You are decorating Request/Reply with a reference type

๐Ÿ› Bug Report

Using this plugin with fastify version 3.9.2 I get the the warning from the following warning:

[FSTDEP006] FastifyDeprecation: You are decorating Request/Reply with a reference type.
This reference is shared amongst all requests. Use onRequest hook instead. Property: requestContext

โ“ Possible Fix

I found a temporary solution mentioned in another plugin:
file: lib/requestContextPlugin.js

13:  fastify.decorateRequest('requestContext', requestContext)

replaced with:

13:  fastify.decorateRequest('requestContext', { getter: () => requestContext })

To Reproduce

  • fastify v3.9.2
  • fastify-request-context v2.1.2

Your Environment

  • node version: 15.2.1
  • fastify version: 3.9.2
  • fastify request context version: v2.1.2
  • os: Linux

Allow set the defaultStoreValues by factory

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure the feature has not already been requested

๐Ÿš€ Feature Proposal

The defaultStoreValue could accept a factory function.

Motivation

This is a similar approach used in different store libs (ie. vuex, pinia).

Currently when passing default values by the store, when the value is an object, it is shared between requests what could be not what we want. The side effects of this can be hard to find when someone is not aware of this and instead of setting the whole data by key mutates the members of object what user got from context (like in #125).

Example

fastify.register(fastifyRequestContextPlugin, { 
  hook: 'preValidation',
  defaultStoreValues: () => ({
    user: { id: 'system' } 
  })
});

In E2E tests, fastify-request-context is not closing properly after calling fastify.close()

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure the bug has not already been reported

Fastify version

3.28.0

Plugin version

2.2.0

Node.js version

16.3.2

Operating system

Linux

Operating system version (i.e. 20.04, 11.3, 10)

20.04.4 LTS

Description

When running E2E tests, even calling fastify.close(), Jest is reporting an open handle from fastify-request-context:

`
Jest has detected the following 1 open handle potentially keeping Jest from exiting:

โ— fastify-request-context

  35 |   it('test', (done) => {
  36 |     console.log('test');
> 37 |     fastify.inject({
     |             ^
  38 |         method: 'GET',
  39 |         url: '/'
  40 |       }, (err, response) => {

  at Object.ready (node_modules/fastify/fastify.js:481:13)
  at Object.inject (node_modules/fastify/fastify.js:459:12)
  at Object.<anonymous> (test.js:37:13)
  at TestScheduler.scheduleTests (node_modules/@jest/core/build/TestScheduler.js:333:13)
  at runJest (node_modules/@jest/core/build/runJest.js:404:19)
  at _run10000 (node_modules/@jest/core/build/cli/index.js:320:7)
  at runCLI (node_modules/@jest/core/build/cli/index.js:173:3)

`

Steps to Reproduce

package.json:

{
    "name": "test",
    "version": "1.0.0",
    "scripts": {
        "test": "jest  --detectOpenHandles"
    },
    "dependencies": {
        "fastify": "3.28.0",
        "fastify-request-context": "2.2.0"
    },
    "devDependencies": {
        "jest": "27.5.1"
    }
}

test.js:

const Fastify = require('fastify');
const { requestContext, fastifyRequestContextPlugin } = require('fastify-request-context');

async function buildFastify () {
  const fastify = Fastify();

  fastify.get('/', function (request, reply) {
    reply.send({ hello: 'world' });
  });

  // If we comment the following lines, no open handles
  await fastify.register(fastifyRequestContextPlugin, {
    hook: 'onRequest',
    defaultStoreValues: {},
  });
  fastify.addHook('preValidation', (request, _reply, done) => {
    requestContext.set('body', request.body);
    done();
  });

  return fastify;
};

describe('GET `/` route', () => {
  let fastify;

  beforeAll(async () => {
    fastify = await buildFastify();
  });

  afterAll(async () => {
    await fastify.close();
  });

  it('test', (done) => {
    fastify.inject({
        method: 'GET',
        url: '/'
      }, (err, response) => {
        expect(response.statusCode).toEqual(200);
        done();
      });
  });  
})

Steps to reproduce:

  1. Create the files above
  2. npm install
  3. npm run test
  4. Most of the times Jest will say there is an open handle related to fastify-request-context

Expected Behavior

There shouldn't be any open handles in an E2E test after calling fastify.close()

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.