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

Error when using server.inject(): "Cannot read private member #strategies from an object whose class did not declare it" #4119

Closed
chrisshiplet opened this issue Jul 22, 2020 · 4 comments
Assignees
Labels
non issue Issue is not a problem or requires changes

Comments

@chrisshiplet
Copy link

chrisshiplet commented Jul 22, 2020

Support plan

  • is this issue currently blocking your project? (yes/no): yes
  • is this issue affecting a production system? (yes/no): no

Context

  • node version: tested with 14.3.0 & 12.18.2
  • module version with issue: 19.2.0
  • last module version without issue: 18.4.1
  • environment (e.g. node, browser, native): node
  • used with (e.g. hapi application, another framework, standalone, ...): standalone, using @hapi/lab and server.inject()

What are you trying to achieve or the steps to reproduce?

I am trying to write @hapi/lab integration tests with the latest version of hapi and lab. If I use any authentication strategy, the tests fail with Cannot read private member #strategies from an object whose class did not declare it

The routes still work fine when accessed via an actual HTTP request. Also, it doesn't seem matter what strategy was used - I encountered the issue with hapi-auth-jwt2 but reproduced with the latest version of @hapi/basic as well.

I am running lab with lab tests from the npm test script. Minimal reproduction is available at chrisshiplet/hapi-inject-bug-repro and below:

tests/get-some-path.js
const Code = require("@hapi/code");
const Lab = require("@hapi/lab");

const getServer = require("../get-server");

const { expect } = Code;
const { it, describe, before } = (exports.lab = Lab.script());

describe("GET /", () => {
  before(async ({ context }) => {
    context.server = await getServer();

    await context.server.initialize();
  });

  it("responds with 200", async ({ context }) => {
    const res = await context.server.inject({
      method: "GET",
      url: `/some-path`,
    });

    expect(res.statusCode).to.equal(200);
  });
});
get-server.js
const hapi = require("@hapi/hapi");

module.exports = async () => {
  const server = hapi.server();

  await server.register(require("@hapi/basic"));

  server.auth.strategy("simple", "basic", {
    validate: () => ({ isValid: true, credentials: {} }),
  });

  server.route({
    path: "/some-path",
    method: "GET",
    handler: (request, h) => "hello world",
    config: {
      auth: {
        strategy: "simple",
      },
    },
  });

  return server;
};
package.json
{
  "name": "hapi-inject-bug-repro",
  "version": "1.0.0",
  "description": "",
  "scripts": {
    "test": "lab tests"
  },
  "license": "PUBLIC DOMAIN",
  "dependencies": {
    "@hapi/basic": "6.0.0",
    "@hapi/hapi": "19.2.0"
  },
  "devDependencies": {
    "@hapi/code": "8.0.1",
    "@hapi/lab": "22.0.5",
    "sinon": "9.0.2"
  }
}

What was the result you got?

GET /
  ✖ 1) responds with 200


Failed tests:

  1) GET / responds with 200:

      Cannot read private member #strategies from an object whose class did not declare it

      at module.exports.internals.Auth._authenticate (/Users/chris/Desktop/hapi-inject-bug-repro/node_modules/@hapi/hapi/lib/auth.js:254:35)
      at authenticate (/Users/chris/Desktop/hapi-inject-bug-repro/node_modules/@hapi/hapi/lib/auth.js:234:21)
      at Request._lifecycle (/Users/chris/Desktop/hapi-inject-bug-repro/node_modules/@hapi/hapi/lib/request.js:364:68)
      at Request._execute (/Users/chris/Desktop/hapi-inject-bug-repro/node_modules/@hapi/hapi/lib/request.js:273:9)


1 of 1 tests failed
Test duration: 38 ms

What result did you expect?

The tests should run successfully.

@chrisshiplet chrisshiplet added the support Questions, discussions, and general support label Jul 22, 2020
@chrisshiplet
Copy link
Author

chrisshiplet commented Jul 22, 2020

This may be related to #4013

According to https://2ality.com/2019/07/private-class-fields.html#this-and-private-static-fields when extending a class, the sub-class cannot access the super-class's private fields with this. I'm guessing something about inject() means the Auth lib is getting extended somehow? I'm not familiar enough with Hapi's codebase to understand the difference between a "real" request and an injected request.

@devinivy
Copy link
Member

devinivy commented Jul 22, 2020

I will admit this is a confusing one, but I see what is going on here and it is technically known/expected behavior. The trouble-maker isn't hapi: it's actually lab. Lab's context is cloned for each test, I believe to avoid tests mutating it and it becoming a place to share state across tests. This means you're getting a clone of your hapi server, which is breaking access to private fields. For now I'd say just avoid using that lab feature for this purpose.

I would guess that before hapi used private fields, using a clone of the hapi server wouldn't cause any issues, but I don't use this feature so I am unsure. It might be worth filing an issue in lab to see if there's a way we can account for this use case in the future.

Here's a diff demonstrating how to get it working again:

 const { it, describe, before } = (exports.lab = Lab.script());
 
 describe("GET /", () => {
-  before(async ({ context }) => {
+  const context = {};
+
+  before(async () => {
     context.server = await getServer();
 
     await context.server.initialize();
   });
 
-  it("responds with 401 when no credentials provided", async ({ context }) => {
+  it("responds with 401 when no credentials provided", async () => {
     const res = await context.server.inject({
       method: "GET",
       url: `/some-path`,

@robmcguinness
Copy link

@devinivy a bit unrelated by I believe Hapi using private fields internally so this would be something to keep in mind: https://twitter.com/jasnell/status/1276297531798896641

So... by all evidence, you should avoid using JavaScript private fields in hot code paths for the time being. Significant performance costs there. One way to approximate private fields that we use in Node.js core is to use non-exported Symbols.

@chrisshiplet
Copy link
Author

Thanks @devinivy! I filed this over on lab's issue tracker so I'll close this issue and keep future discussion over there.

@devinivy devinivy added non issue Issue is not a problem or requires changes and removed support Questions, discussions, and general support labels Jul 22, 2020
@devinivy devinivy self-assigned this Jul 22, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
non issue Issue is not a problem or requires changes
Projects
None yet
Development

No branches or pull requests

3 participants