Skip to content

GraphQL Federation: entity resolution fails for heterogeneous arrays where items have different nullable federated fields #7883

@dennissetiawan

Description

@dennissetiawan

Related Issue

This is a distinct reproduction of the same []null / empty URL symptom seen in #7653, but with a different trigger: truly heterogeneous arrays where items are different subtypes and carry different nullable federated fields — rather than a single entity type with multiple @key variants.


Description

When Tyk Gateway resolves federated entities via _entities queries for a field on a type that appears in an array, the federation planner uses array index 0 as the anchor to determine the subgraph URL for the entire entity resolution batch.

If item[0] has null for the federated field being resolved (because item[0] is of a different subtype and that field does not apply to it), Tyk generates an empty URL and the fetch fails with:

Get "": unsupported protocol scheme ""

This manifests specifically with heterogeneous arrays — arrays where items have different subtypes, and only some items carry a particular federated field (others have null for that field).


How This Differs from #7653

Issue #7653 involves a single entity type (Product) extended via two different @key fields (id vs sku) in two separate subgraphs. All array items are the same type.

This issue involves an array of items where each item can be one of two completely different entity types (e.g. TypeAlpha or TypeBeta), and only items of the matching type carry a non-null value for a given federated field. The planner still exhibits the same index-0 URL anchor behavior, but it is triggered by a structurally different schema pattern.


Steps to Reproduce

Schema Setup

Subgraph A ("catalog") — owns the Item array; extends two entity types from Subgraph B:

extend type TypeAlpha @key(fields: "id") {
    id: String @external
}

extend type TypeBeta @key(fields: "id") {
    id: String @external
}

type Item {
    itemType: ItemType!    # enum: ALPHA | BETA
    id: String!
    alphaDetail: TypeAlpha  # non-null only when itemType == ALPHA
    betaDetail: TypeBeta    # non-null only when itemType == BETA
}

enum ItemType {
    ALPHA
    BETA
}

type Query {
    listItems: [Item!]!
}

Subgraph B ("details") — owns both entity types:

type TypeAlpha @key(fields: "id") {
    id: String
    name: String
}

type TypeBeta @key(fields: "id") {
    id: String
    name: String
}

Query

query {
  listItems {
    itemType
    id
    alphaDetail { name }
    betaDetail { name }
  }
}

Data That Triggers the Bug

Subgraph A returns a mixed array where the first element is type ALPHA (so betaDetail is null at index 0):

[
  { "id": "alpha-001", "itemType": "ALPHA", "alphaDetail": { "id": "alpha-001" }, "betaDetail": null },
  { "id": "alpha-002", "itemType": "ALPHA", "alphaDetail": { "id": "alpha-002" }, "betaDetail": null },
  { "id": "beta-001",  "itemType": "BETA",  "alphaDetail": null,                  "betaDetail": { "id": "beta-001" } }
]

Observed Result

  • alphaDetail resolves successfully — item[0] has a non-null alphaDetail, Tyk correctly identifies the Subgraph B URL for TypeAlpha.
  • betaDetail resolution fails — item[0] has null for betaDetail, Tyk produces an empty URL string and errors.

Confirming the Index-0 Anchor

Reorder the array so a BETA item is first:

[
  { "id": "beta-001",  "itemType": "BETA",  "alphaDetail": null,                  "betaDetail": { "id": "beta-001" } },
  { "id": "alpha-001", "itemType": "ALPHA", "alphaDetail": { "id": "alpha-001" }, "betaDetail": null },
  { "id": "alpha-002", "itemType": "ALPHA", "alphaDetail": { "id": "alpha-002" }, "betaDetail": null }
]

After reordering:

  • betaDetail now resolves successfully.
  • alphaDetail now fails.

This confirms the planner anchors subgraph URL lookup to array index 0 regardless of which items actually carry the field.


Debug Logs

Enable debug logging (log_level: "debug"). The following pattern appears when the bug triggers:

# Successful — item[0] has non-null alphaDetail, URL correctly resolved
beforeFetchHook url="https://details-subgraph/query"
  representations=[{"__typename":"TypeAlpha","id":"alpha-001"},{"__typename":"TypeAlpha","id":"alpha-002"}]
# → 200 OK ✓

# Failed — item[0] has null betaDetail, URL is empty string
beforeFetchHook url=""
  representations=[]null
# → Get "": unsupported protocol scheme "" ✗

After reordering so item[0] is BETA:

# betaDetail now works
beforeFetchHook url="https://details-subgraph/query"
  representations=[{"__typename":"TypeBeta","id":"beta-001"}]
# → 200 OK ✓

# alphaDetail now fails
beforeFetchHook url=""
  representations=[]null
# → Get "": unsupported protocol scheme "" ✗

Expected Behavior

The federation planner should handle heterogeneous arrays where only a subset of items carry a particular federated field. It should either:

  1. Scan past null entries — find the first non-null value at any index to determine the subgraph URL, rather than always using index 0.
  2. Skip null representations — omit items with a null federated field from the _entities batch entirely, and only include items where the field is non-null.

Either approach aligns with the Apollo Federation spec, which requires entity resolution to be driven by __typename and key fields of each representation individually.


Actual Behavior

The planner uses index 0 to determine the subgraph URL for the entire batch. If item[0] has null for the field being resolved, Tyk generates an empty URL and issues a fetch to "", which fails immediately.


Environment

Component Version / Detail
Tyk Gateway v5.11.0
Federation spec Apollo Federation v1 (@key, @external)
Subgraph count 2

Workaround

Remove the GraphQL federation relationship for the affected field entirely. Resolve the details locally inside the owning subgraph by calling the details service directly via REST/gRPC. This bypasses the Tyk federation planner for that field but breaks the federation model and couples subgraphs at the network level.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions