Skip to content

ctx.state is empty before await next() in last-registered global middleware #19529

@th0rgall

Description

@th0rgall

Bug report

Required System information

  • Node.js version: 18.19.0
  • NPM version: 10.2.3
  • Strapi version: 4.19.1
  • Database: SQLite
  • Operating system: Debian-based Node & TS Docker devcontainer on arm64 macOS
  • Is your project Javascript or Typescript: Typescript

Describe the bug

These two forum posts (not mine) have described the problem already:

I'm quoting the first one here:

module.exports = (config, { strapi }) => {
    return async (ctx, next) => {
        console.log(ctx.state.user);

        return await next();
    };
};

I’ve tried to access it even in some lifecycle files, but it’s the same thing.

When I console.log(ctx.state), it returns {} an empty object so I cannot access to ctx.state.user

Steps to reproduce the behavior

  1. Add a global middleware with the above code
  2. Register it at the end:
// config/middlewares.ts
export default [
  'strapi::logger',
  'strapi::errors',
  'strapi::security',
  'strapi::cors',
  'strapi::poweredBy',
  'strapi::query',
  'strapi::body',
  'strapi::session',
  'strapi::favicon',
  'strapi::public',
  'global::my-global-middleware' // <-- register here
];
  1. Observe {} being logged

Expected behavior

Since the global middleware is registered last, I would expect the authentication state to be available already.

I might have some gap in understanding regarding how global MW should function.

What part of the internal Strapi stack sets the state? And when does it run? Is it supposed to run after the global middlewares? (but before Policies?)

If this isn't a bug, it could perhaps be pointed out in the documentation that global MW's can not access authentication state before they pass control to Policies/Controllers/..., for example here https://docs.strapi.io/dev-docs/backend-customization

Additional context

Motivation

The reason I want to check this is to be able to modify the populate and fields parameters of requests, to restrict/filter out population of certain fields based on auth state without blocking the request, before the query engine executes the query.

Route MWs are less attractive for my case, because I want to restrict fields on a content type that is related to many other content types, and hence, you can access it "through" all those other APIs as well (using population parameters). I would need to keep careful track of which route MWs to add, and there would be many. For the same reason, this interesting plugin did not fit my needs: https://github.com/strapi-community/strapi-plugin-protected-populate

Policies are read only, and can't modify the request.

I was hoping to dynamically check the model schema behind the currently requested route in global MW, then figure out if the request would return fields that the user is not allowed to see (based on the role). This would be more efficient performance-wise, because I could prevent some levels of (attempted) population for unauthorized clients.

Workaround

I will be able to work around this by removing the restricted fields from the response based on auth state after the query has run. But this is less performant.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions