Per-request global context accessible everywhere

So what I currently miss in Adonis is some “Request Context” or “Thread context” which can be accessed inside any function called in terms of the same request.

Something like https://github.com/othiym23/node-continuation-local-storage or already deprecated Domains in Node.

I do know that in Adonis 4.0 I can just pass context in every function by hand but It becomes very annoying to pass the same context object along with real function params in pretty much every function which uses, for example, logging or error reporting or localisation.

For example, here are the similar questions (and examples) for nodejs in general:


So my question:
Is there already a good practice or an already existing way of retrieving request context without explicitly passing context in every function?

Okay so it’s long story divided into 2 parts, One is what we can achieve and what we really should do.

There is another library called zone.js introduced by Angular team, which can be used within Node.js as well.

It basically gives you the current context where your code is running at. A related post on same https://medium.com/@amcdnl/zones-for-nodejs-apis-670281ac5aa5

Do we really need it?

Atleast as of today, all these libraries has huge performance impact + there are 100% solid to support all use cases.

So we all end up fighting in fixing the Context issues over doing the real work.

Passing the Context manually at each level

I believe you don’t have to do that. I also fought against the nature or Javascript or Node.js and realized that there is a different way too. Lemme share by principles around that ( Yes, they can be 100% wrong )

I divide my application into multiple layers.

  1. Controllers
  2. Services
  3. Factories
  4. Or whatever

The only layer that stores the outside state ( means the request state ) is my controllers. So it’s simple, a route get’s called, which invokes the controller method and you get the context.

Everything below this layer are pure functions or classes with their own local state. Which means a Service should never have access to request object directly. I just pass the required data to service methods.

class SomeService {
  constructor () {
     // feel free to have local state
  }
 
  performTask () {
    // take inputs and give output
  }
}

Let’s do it this way

I have created some decent apps using it approach, just by controlling myself to not share the outside state with my other layers and everything works fine. Also

  1. These services are easy to test
  2. Also easy to re-use in different areas. For example: Using the same service for a command line command.

If you still believe it’s not possible, I can say, share a problem with me, where you are frustrated passing the context. I will share a more elegant solution to that problem ( just by eliminating the need of passing the context )

@virk , thanks for a very detailed answer, I appreciate it a lot!

So here are a few use-cases where context would be useful:

  1. I sometimes throw errors with localized hints in services. So for that I have to pass ctx.antl into every service’s function which can throw such a error. Would be really nice to be able to get request’s language or antl instance without passing it to the service/Error explicitly;

For example, currently it looks something like this (simplified):

LocalizedError extends NE.LogicalException {
      constructor({ errorCode, antl, params }) {
          super(errorCode, 500)
          this.details =  antl.formatMessage(`errors.${errorCode.toLowerCase()}.details`, params)
          this.hint =  antl.formatMessage(`errors.${errorCode.toLowerCase()}.hint`, params)
      })
   }

// Example
throw new LocalizedError({ antl, code: "E_ALREADY_FINISHED" })

Because same error code can be thrown inside many functions and hint/details stay same, it is cleaner to localize/add these params inside error constructor itself. But I would really love to avoid passing antl to error constructor every time.

  1. Second use-case is logging. I would prefer to be able to filter logs on per-request-basis on papertrail or whatever logging system. So for this my Logger needs some common parameter which it can access anywhere. So currently I end up creating a logger instance in request context (and hence passing this logger instance in every function which uses logging).

  2. There are a few more usecases, like, error tracking (e. g. sentry) or a few more things.

So most of these use-cases need a very tiny common object. Like language string (for using antl everywhere) or some unique request id (for logging) or request params (for error tracking).

On one side, I don’t like globals. On the other side, getting these tiny things without explicitly passing them to the functions would make the code cleaner.

P. S. By the way, currently zone.js does not work with async/await functions. See this github issue (as also stated in medium article)

is_liked attribute for each Post model ?

  1. I must add a afterFind or afterFetch hook since I have to use await
  2. I need to get current user via auth.user, but there is no way to get current context.

Relevant to this conversation, I just published a Context provider package using async_hooks. Any feedback is appreciated. https://github.com/brentburg/adonis-context

@brentburg Looks nice, I will give it shot and see how it behaves :+1:

I solved this problem using the library express-http-context.
I used to create a wrap of Winston library in order to print a unique request id in all logs of every request. You can see the code here:
express-logger-unique-req-id
Hope it helps