apollographql / federation

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

`@interfaceObject` error on interface inline fragments #2405

Open atomheartother opened 1 year ago

atomheartother commented 1 year ago

Introduction

I'm trying to get a complex graphql server running using Federation spec v2.3 as interfaces are an integral part of the data, which for the most part works, but I've encountered a pesky bug. I"m not sure if it is a subgraph bug, a router bug or an implementation error on my part (as interfaceObject is still not very documented).

In short it seems like when you use an inline fragment to specify an interface's implementation, you can confuse other subgraphs when you query the fields they've contributed to the interface.

The setup

# products.graphql
interface Product @key(fields: "id") {
  id: ID!
}

type Book implements Product @key(fields: "id") {
  id: ID!
  id: 
}

type Bike implements Product @key(fields: "id") {
  id: ID!
  wheels: Int!
}

Query {
  products: [Product!]!
}

And let's say you share this interface with the "reviews" subgraph:

# reviews.graphql
type Poduct @key(fields: "id") @interfaceObject {
  id: ID!
  reviews: [Review!]!
}

type Review {
  id: ID!
  productId: ID!
  score: Int!
}

Query {
  reviews: [Review!]!
}

Let's say for the sake of brevity you have resolvers for everything and __resolveReferences for all your entities.

The bug

Assuming products returns both Bikes and Books, this query will break:

query bugQuery {
  products {
    ... on Book {
      reviews { score }
    }
  }
}

It will print an error along the lines of:

{
    "data": null,
    "errors": [
        {
            "message": "Subgraph response from 'reviews' was missing key `_entities`",
            "path": [
                "products",
                "@"
            ],
            "extensions": {
                "code": "PARSE_ERROR"
            }
        }

When you'd expect it to return a mix of empty objects (the Bikes) and Books with their attached reviews. After some investigating, this is due to the fact that the Bikes returned by products aren't being queried for reviews. Somehow that breaks federation.

Similarly this will break:

query bugQuery {
  products {
    ... on Book {
      reviews { score }
    }
    ... on Bike {
      wheels
    }
  }
}

Example of working similar queries

This works and returns reviews for all products:

query validQuery {
   products {
    reviews { score }
  }
}

This also effectively produces the same output:

query validQuery {
   products {
    ... on Book {
      reviews { score }
    }
    ... on Bike {
      reviews { score }
    }
  }
}

Even this works and also produces the same output:

query validQuery {
   products {
    reviews { score }
    ... on Bike {
      reviews { score }
    }
  }
}

Package versions

I'm using the router docker image v1.10.3 with @apollo/server 4.3.3 and @apollo/subgraph 2.3.2.

atomheartother commented 1 year ago

The more I poke at this bug the more I lean towards implementation error. It's happening for some data types but not all, so it's likely depending on some unknown implementation detail my example above would not reproduce the bug. I'll try and find the time to make a reproducible example and get back to this report, but if someone can provide any insight as to what the problem might be from this error message I'd be grateful.

korinne commented 1 year ago

Hi @atomheartother, thanks for raising this issue! We're looking into this now to see if it's an implementation problem or a bug -- which version of the Federation 2.3 spec are you using (importing via @link)? We did release some patch updates that solved similar bugs (trying to deduce if it's the same one) in version 2.3.2, so I'd recommend using that version. I know you're using 2.3.2 of @apollo/subgraph, which is great.

atomheartother commented 1 year ago

@korinne Thanks for the quick reply :)

I'm using 2.3 like so (2.3.2 isn't a valid spec version to @link against as far as I understand it):

extend schema
  @link(
    url: "https://specs.apollo.dev/federation/v2.3"
    import: [
      "@key"
      "@tag"
      "@shareable"
      "@inaccessible"
      "@override"
      "@external"
      "@provides"
      "@requires"
      "@interfaceObject"
    ]
  )

This is in both subgraphs. In the router I had this in my supergraph-config.yaml: federation_version: =2.3.0 I updated it to =2.3.2 just now to no avail.

korinne commented 1 year ago

Oh you're completely right about the @link -- sorry about that, too early in the morning :) . One last question for you -- are you using managed Federation with Apollo Studio, or rover supergraph compose ? If using the latter, it's very easy to forget to recompose once you bump the version.

Either way, we've added this to our backlog to investigate asap, and thanks again for the extra information!

atomheartother commented 1 year ago

It's local composition with rover supergraph compose, but I did triple-check that i had done that and restarted my router before replying 😄 The supergraph was simply left unchanged after upgrading to =2.3.2 and re-composing so it should have had no effect anyway.

Like I said I'm still pretty sure this is an implementation error but it's hard to pin down where I went wrong...

korinne commented 1 year ago

Alright cool, thank you for the extra information -- I've forgotten to recompose before so it's always good to check haha. Looking into it, and will get back to you shortly. It could be implementation specific, but it's always good for us to dive into any issues. Thanks again for reaching out!

korinne commented 1 year ago

@atomheartother Hey there, do you happen to have that repro available for us? We can try to reproduce on our end, but it would be great to look at what you have. Thanks!

atomheartother commented 1 year ago

@korinne I'm afraid I'm extremely busy at the moment, but I'll try and figure something out reasonably soon.

I however have been poking at and tweaking my schema and I've found that the error only occurs when the interface's federated field has an @external dependency. Here's a simplified schema of my error case:

# limit.graphql

## Limit only has those two implementations
interface Limit @key(fields: "id") {
  id: ID!
  strategyId: ID!
}

type StandaloneLimit implements Limit @key(fields: "id") {
  id: ID!
  strategyId: ID!
}

type ProfileLimit implements Limit @key(fields: "id") {
  id: ID!
  strategyId: ID!
}

# Federated
type Strategy @interfaceObject @key(fields: "id") {
  id: ID!
  limits: [Limit!]!
}
#strategy.graphql
interface Strategy @key(fields: "id") {
  id: ID!
}

type XStrategy implements Strategy @key(fields: "id") {
  id: ID!
}

type YStrategy implements Strategy @key(fields: "id") {
  id: ID!
}

# Federated
type Limit @interfaceObject @key(fields: "id") {
  id: ID!
  strategyId: Int! @external
  strategy: Strategy! @requires(fields: "strategyId")
}
query test {
    # Doesn't work
    limits {
        ...on StandaloneLimit {
            strategy {id}
        }
    }
    # Doesn't work
    limits {
        ...on ProfileLimit {
            strategy {id}
        }
    }
       # Works
    limits {
        strategy {id}
    }
    # Works
    limits {
        ...on StandaloneLimit {
            strategy {id}
        }
        ...on ProfileLimit {
            strategy {id}
        }
    }
    # Doesn't work (this queries a Limit which we know is a StandaloneLimit)
    limit(id: "001a5bea-dd8b-4bbc-836a-6bf676fa20be") {
        ...on ProfileLimit {
            strategy {id}
        }
    }
    # Works
    strategies {
        ...on XStrategy {
            limits {id}
        }
    }
    # Works
    strategies {
        ...on YStrategy {
            limits {id}
        }
    }
    # Works (id:10 is an XStrategy)
    strategy(id: "10") {
        ...on YStrategy {
            limits {id}
        }
    }
}

I also doubt this is linked but the schema version differs in other schemas (these two are v2.3, but they are being composed with other schemas which are still on 2.0).

aliosmanisikk commented 10 months ago

I'm experiencing a similar issue and my investigation showed me that the query planner is over-fetching on conditional fragments.

While over-fetching in itself is an issue, worse thing is, it tries to fetch __typename from reviews graph @interfaceObject which does not match with the one resolved by the products graph, so it returns null since __typename do not match.

I think it is closely related to this issue https://github.com/apollographql/federation/issues/2759