apollographql / federation

🌐  Build and scale a single data graph across multiple services with Apollo's federation gateway.
https://apollographql.com/docs/federation/
Other
668 stars 254 forks source link

Type resolver is invoked when reference resolver returns null for interface entity #3123

Closed aliosmanisikk closed 2 months ago

aliosmanisikk commented 3 months ago

Issue Description

When __resolveReference returns null for interface entity, there is no need to invoke __resolveType. However, it is being invoked and causing issues with resolving type for null value.

I tested the behavior for Union or Interface type. For those, it works as expected. Issue occurs for Interface Entity only.

Link to Reproduction

https://github.com/aliosmanisikk/apollo-subgraph-interface-entity-type-resolver

Reproduction Steps

Running query gives following output

query Product {
  _entities(representations: [{ __typename: "Product", slug: "frame" }]) {
    __typename
    ... on Product {
      type
      slug
    }
  }
}

returns

{
  "data": {
    "_entities": [
      {
        "__typename": "FrameProduct",
        "type": "FRAME",
        "slug": "frame"
      }
    ]
  }
}

And for another product

query Product {
  _entities(representations: [{ __typename: "Product", slug: "sunglass" }]) {
    __typename
    ... on Product {
      type
      slug
    }
  }
}

returns

{
  "data": {
    "_entities": [
      {
        "__typename": "SunglassProduct",
        "type": "SUNGLASS",
        "slug": "sunglass"
      }
    ]
  }
}

But running for unknown product (which is null) throws error

query Product {
  _entities(representations: [{ __typename: "Product", slug: "unknown" }]) {
    __typename
    ... on Product {
      type
      slug
    }
  }
}

returns

{
  "errors": [
    {
      "message": "Cannot read properties of null (reading 'type')",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "_entities",
        0
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "stacktrace": [
          "TypeError: Cannot read properties of null (reading 'type')",
          "    at /Users/ali/WKS/test/apollo-subgraph-interface-entity-type-resolver/src/subgraph-a.ts:52:22",
          "    at Generator.next (<anonymous>)",
          "    at /Users/ali/WKS/test/apollo-subgraph-interface-entity-type-resolver/src/subgraph-a.ts:8:71",
          "    at new Promise (<anonymous>)",
          "    at __awaiter (/Users/ali/WKS/test/apollo-subgraph-interface-entity-type-resolver/src/subgraph-a.ts:4:12)",
          "    at __resolveType (/Users/ali/WKS/test/apollo-subgraph-interface-entity-type-resolver/src/subgraph-a.ts:51:69)",
          "    at withResolvedType (/Users/ali/WKS/test/apollo-subgraph-interface-entity-type-resolver/node_modules/@apollo/subgraph/src/types.ts:166:23)",
          "    at processTicksAndRejections (node:internal/process/task_queues:95:5)"
        ]
      }
    }
  ],
  "data": {
    "_entities": [
      null
    ]
  }
}
dariuszkuc commented 2 months ago

Hello 👋,

Thanks for raising this but this is actually not a valid example for multiple reasons. While it is possible to hand craft custom subgraph queries that would trigger this behavior, _entities queries are handled by federation internals and they will not be exposed as a public API for end-users. Those queries should only be executed through a gateway/router as a result of a query planning process.

Issues with the provided example

  1. _entities representation would always include a concrete __typename information (unless it is an @interfaceObject which again in a given graph would be a concrete type) and never an interface (i.e. __typename: "Product" is invalid and instead should specify a concrete type).
  2. Furthermore, one of the fundamental properties of federation is that given entity should be resolvable to the same object across ALL subgraphs. Therefore, in a federated environment those entity queries will only be executed for a valid well known keys, i.e. slug = unknown implies that there was a parent query that resolved to this unknown entity and we are now invoking another subgraph to fetch some additional details through _entities query.

I'm closing this as won't fix. We can reopen if you can provide a reproduction of this problem that triggers in a federated environment through gateway/router.

aliosmanisikk commented 2 months ago

@dariuszkuc

I don't really understand why you closed this. This is of course observed in a router environment. I pinpointed the exact issue to make your investigation easier. I reproduced the exact queries that the router would invoke above.

_entities representation would always include a concrete typename information (unless it is an @interfaceObject which again in a given graph would be a concrete type) and never an interface (i.e. typename: "Product" is invalid and instead should specify a concrete type).

This is @interfaceObject and above query is exactly what the router invokes to the subgraph.

Furthermore, one of the fundamental properties of federation is that given entity should be resolvable to the same object across ALL subgraphs. Therefore, in a federated environment those entity queries will only be executed for a valid well known keys, i.e. slug = unknown implies that there was a parent query that resolved to this unknown entity and we are now invoking another subgraph to fetch some additional details through _entities query.

Parent entity does not have to be resolved with slug=unknown. I used the word unknown to make the example a bit more clear. Imagine you have a Cart entity which holds the skus of the Products which references Product entity with skus where one of the products is no longer in your catalog. Cart is resolved but the products it references will not be found in that case. With regular entities, this works perfectly. But with @interfaceObject the above error occurs due to a bug in subgraph code which I tried to illustrate here.

I thought I explained the gist of the issue here. I'll send you an example with Router and everything if that helps you.