Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,34 @@ 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)](https://github.com/fastify/fastify-request-context/issues/93).

## Sharing context across multiple request sources

You can share a single `AsyncLocalStorage` instance across multiple request sources (HTTP, queues, scheduled tasks, etc.) by using `setAsyncLocalStorage()` before plugin registration:

```js
const { AsyncLocalStorage } = require('node:async_hooks');
const { fastifyRequestContext, setAsyncLocalStorage, requestContext } = require('@fastify/request-context');

const sharedAsyncLocalStorage = new AsyncLocalStorage();

// Set the global instance before plugin registration
setAsyncLocalStorage(sharedAsyncLocalStorage);

// Register plugin
app.register(fastifyRequestContext, {
defaultStoreValues: { /* ... */ }
});

// Use in other parts of your application with the same context
messageQueue.consume(async (message) => {
await sharedAsyncLocalStorage.run({ messageId: message.id }, async () => {
await processMessage(message); // requestContext.get/set work here
});
});
```

This enables unified request context across different entry points in your application without coupling non-HTTP code to Fastify.

## Usage outside of a request

If functions depend on requestContext but are not called in a request, i.e. in tests or workers, they can be wrapped in the asyncLocalStorage instance of requestContext:
Expand Down
7 changes: 6 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ const fp = require('fastify-plugin')

const asyncResourceSymbol = Symbol('asyncResource')

const asyncLocalStorage = new AsyncLocalStorage()
let asyncLocalStorage = new AsyncLocalStorage()

function setAsyncLocalStorage(it) {
asyncLocalStorage = it
}

const requestContext = {
get: (key) => {
Expand Down Expand Up @@ -67,6 +71,7 @@ module.exports = fp(fastifyRequestContext, {
module.exports.default = fastifyRequestContext
module.exports.fastifyRequestContext = fastifyRequestContext
module.exports.asyncLocalStorage = asyncLocalStorage
module.exports.setAsyncLocalStorage = setAsyncLocalStorage
module.exports.requestContext = requestContext

// Deprecated
Expand Down
42 changes: 40 additions & 2 deletions test-tap/requestContextPlugin.e2e.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ const {
initAppPostWithAllPlugins,
initAppGetWithDefaultStoreValues,
} = require('../test/internal/appInitializer')
const { fastifyRequestContext } = require('..')
const { fastifyRequestContext, setAsyncLocalStorage } = require('..')
const { TestService } = require('../test/internal/testService')
const { test, afterEach } = require('node:test')
const { CustomResource, AsyncHookContainer } = require('../test/internal/watcherService')
const { executionAsyncId } = require('node:async_hooks')
const { AsyncLocalStorage, executionAsyncId } = require('node:async_hooks')

let app
afterEach(() => {
Expand Down Expand Up @@ -392,3 +392,41 @@ test('returns the store', (t) => {
})
})
})

test('uses external AsyncLocalStorage when provided', (t) => {
t.plan(3)

const externalALS = new AsyncLocalStorage()
setAsyncLocalStorage(externalALS)

app = fastify({ logger: true })
app.register(fastifyRequestContext, {
defaultStoreValues: { userId: 'default' },
})

const route = (req) => {
// Set value directly on the external ALS store
const store = externalALS.getStore()
store.userId = 'test-user'

// Verify the value is accessible through both APIs
const valueFromPlugin = req.requestContext.get('userId')
const valueFromExternalALS = store.userId

t.assert.strictEqual(valueFromPlugin, 'test-user')
t.assert.strictEqual(valueFromExternalALS, 'test-user')

return { userId: valueFromExternalALS }
}

app.get('/', route)

return app.listen({ port: 0, host: '127.0.0.1' }).then(() => {
const { address, port } = app.server.address()
const url = `${address}:${port}`

return request('GET', url).then((response) => {
t.assert.strictEqual(response.body.userId, 'test-user')
})
})
})
1 change: 1 addition & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ declare namespace fastifyRequestContext {

export const requestContext: RequestContext
export const asyncLocalStorage: AsyncLocalStorage<RequestContext>
export function setAsyncLocalStorage(als: AsyncLocalStorage<RequestContextData>): void
/**
* @deprecated Use FastifyRequestContextOptions instead
*/
Expand Down