Apollo-server: [Federation] How to merge schema from gateway itself.

Created on 7 Jun 2019  ยท  7Comments  ยท  Source: apollographql/apollo-server

In short, is there a way to implement Apollo Federation such that the gateway itself has it's own schema?

(EDIT) TLDR No, the gateway can't also be a federated service.

Federating Apollo Server where the gateway itself has a schema

@apollo/[email protected]
@apollo/[email protected]

Expected Behavior

When I am instantiating my Apollo Gateway server, I expect that I should be able to merge schemas from federated services, as well as the schema from the gateway itself.

Actual Behavior

The gateway server fails to mount the /graphql route because it is expecting all of the services to be currently running before it does so.

On run time, the following error is printed to the console:

POST /graphql 404 11.251 ms - 147
Encountered error when loading gateway at http://localhost:8000/graphql: invalid json response body at http://localhost:8000/graphql reason: Unexpected token < in JSON at position 0
[DEBUG] Fri Jun 07 2019 12:11:07 GMT-0400 (Eastern Daylight Time) apollo-gateway: Configuration loaded for Gateway
[DEBUG] Fri Jun 07 2019 12:11:07 GMT-0400 (Eastern Daylight Time) apollo-gateway: Composing schema from service list:
  remote-svc
TypeError: schema.toConfig is not a function
    at Object.composeServices (/gateway-app/node_modules/@apollo/federation/dist/composition/compose.js:191:67)
    at Object.composeAndValidate (/gateway-app/node_modules/@apollo/federation/dist/composition/composeAndValidate.js:13:41)
    at ApolloGateway.createSchema (/gateway-app/node_modules/@apollo/gateway/dist/index.js:90:47)
    at ApolloGateway.<anonymous> (/gateway-app/node_modules/@apollo/gateway/dist/index.js:81:22)
    at Generator.next (<anonymous>)
    at fulfilled (/gateway-app/node_modules/@apollo/gateway/dist/index.js:4:58)
    at process._tickCallback (internal/process/next_tick.js:68:7)

Source Code

const url = new URL(`redis://${REDIS_URL}:${REDIS_PORT}/${REDIS_DATABASE}`).toString()

const cache = new RedisCache({ url })

const context = ({ req }) => {
  if (!(req.user || req.headers.authorization === ROUTE_AUTH)) {
    throw new AuthenticationError('Not authenticated')
  }
  return { user: req.user, req: req }
}

try {
  const loadGateway = async () => {
    try {
      const { schema, executor } = await gateway.load()
      return { schema, executor }
    } catch (err) {
      console.error(err)
      return null
    }
  }

  const gateway = new ApolloGateway({
    debug: process.env.ENV !== 'prod',
    serviceList: [
      { name: 'gateway', url: `${GATEWAY_HOSTNAME}/graphql` },
      { name: 'remote-svc', url: `${REMOTE_SERVICE_HOSTNAME}/graphql` },
    ],
  })

  const { schema, executor } = loadGateway()
  const server = new ApolloServer({
    schema,
    executor,
    cache,
    dataSources,
    engine: { apiKey: ENGINE_API_KEY },
    tracing: true,
    context,
  })
  server.applyMiddleware({ app })
} catch (err) {
  console.error(err)
}

Justification

The reason for implementing our gateway server this way is to primarily for ease of authentication.

Our gateway server performs all authentication so that we do not have to expend an unnecessary round trip to authenticate a user before executing their request.

Most helpful comment

After migration from Schema Stitching to Federation, we tried several ways to keep the Gateway keeps its own schema. We couldn't separate the schema away the Gateway for some reasons.
Finally, we have to start 2 GraphQL server in same server with different port and path. The Federation GraphQL server must be run before Gateway GraphQL server started.

  • Federation GraphQL Server: /graphq-extended on port 3001.
  • Apollo Gateway Server: /graphql on port 3000.

We haven't found any better solution but currently it works ๐Ÿฅ‡ ๐Ÿ”ฅ

Here's the implementation:

export async function runFederationServer({ PORT: portFromEnv = DEFAULT_PORT }) {
  const path = '/graphql-extended'
  const app: express.Application = express()
  // ...
  const serviceSchema = buildFederatedSchema(gatewaySchema.schemas)
  const server = new ApolloServer({
    schema: serviceSchema,
    context: ({ req, res }) => { /* */ }
  })

  return app
}

export async function runServer({ PORT: portFromEnv = DEFAULT_PORT, GRAPHQL_ENDPOINTS: graphqlEndpoints = '' } = {}) {
  const path = '/graphql'
  const app: express.Application = express()
  // ...
  const serviceList = [
   {
     name: 'Extended Schema',
     uri: 'http://0.0.0.0:3001/graphql-extended',
   },
   {
     // Other Federation GraphQL servers
   },
  ]
  const gateway = new ApolloGateway({
    serviceList,
    buildService({ url }) { /* */ }
  })

  return app
}

runFederationServer(process.env).then(runServer(process.env))

All 7 comments

Hey @calebfaruki, thanks for reporting an issue!

One issue that I can see is that you're not awaiting your call to loadGateway(), which returns a Promise. This would explain your error (schema === undefined).

I might recommend a simpler approach by removing loadGateway() altogether:

const gateway = new ApolloGateway({
  debug: process.env.ENV !== 'prod',
  serviceList: [
    { name: 'gateway', url: `${GATEWAY_HOSTNAME}/graphql` },
    { name: 'remote-svc', url: `${REMOTE_SERVICE_HOSTNAME}/graphql` },
  ],
})

let gatewayConfig
try {
  gatewayConfig = await gateway.load()
} catch (err) {
  // error logic here (logging, etc.)
  // re-throw error
}

const server = new ApolloServer({
  ...gatewayConfig,
  cache,
  dataSources,
  engine: { apiKey: ENGINE_API_KEY },
  tracing: true,
  context,
})

Hope this helps, and let me know if you're still having any trouble ๐Ÿ˜„

Unfortunately, my script requires me to wrap the await gateway.load() in an async function. However, it is the gateway.load() function itself that is throwing the error.

In other words, when I execute the following:

    const gatewayConfig = await gateway.load()
    console.log(gatewayConfig)

The log statement is never executed. The try/catch block fires on the await statement with the error stack trace I listed in the first post of this thread. I can't even verify the value returned by the gateway.load() statement.

To validate my concern, I tried removing the loadGateway() function and invoking it using the following business logic:

const gateway = new ApolloGateway({
  debug: process.env.ENV !== 'prod',
  serviceList: [
    { name: 'gateway-app', url: `${GATEWAY_HOSTNAME}/graphql` },
    { name: 'remote-svc', url: `${REMOTE_SVC_HOSTNAME}/graphql` },
  ],
})

const apolloServer = async () => {
  try {
    const gatewayConfig = await gateway.load()
    console.log(gatewayConfig)
    const server = new ApolloServer({
      ...gatewayConfig,
      cache,
      dataSources,
      engine: { apiKey: ENGINE_API_KEY },
      tracing: true,
      context,
    })
    server.applyMiddleware({ app })
  } catch (err) {
    console.error(err)
  }
}

apolloServer()

module.exports = app

In doing this, I saw no meaningful change in error output.

Encountered error when loading gateway-app at http://localhost:8000/graphql: invalid json response body at http://localhost:8000/graphql reason: Unexpected token < in JSON at position 0
[DEBUG] Sun Jun 09 2019 00:24:10 GMT-0400 (Eastern Daylight Time) apollo-gateway: Configuration loaded for Gateway
[DEBUG] Sun Jun 09 2019 00:24:10 GMT-0400 (Eastern Daylight Time) apollo-gateway: Composing schema from service list:
  remote-svc
TypeError: schema.toConfig is not a function
    at Object.composeServices (/gateway-app/node_modules/@apollo/federation/dist/composition/compose.js:191:67)
    at Object.composeAndValidate (/gateway-app/node_modules/@apollo/federation/dist/composition/composeAndValidate.js:13:41)
    at ApolloGateway.createSchema (/gateway-app/node_modules/@apollo/gateway/dist/index.js:90:47)
    at ApolloGateway.<anonymous> (/gateway-app/node_modules/@apollo/gateway/dist/index.js:81:22)
    at Generator.next (<anonymous>)
    at fulfilled (/gateway-app/node_modules/@apollo/gateway/dist/index.js:4:58)
    at process._tickCallback (internal/process/next_tick.js:68:7)

Apologies, I misinterpreted the schema from the error as being the schema from the result of gateway.load().

This seems to be a problem with the service running on port 8000. Without the gateway running, can you make successful requests to the service? I'd be curious to know what the result of this query is if you can share it, or at least if it's successful:

query SDL { _service { sdl } }

I'll jump to a conclusion here based on the naming of your service: are you trying to run the gateway at port 8000 and treat it as a federated service at the same time? If so, that's not going to work; the gateway has to resolve all schemas before it can compose the final schema.

๐Ÿคฆโ€โ™‚ pardon my total oversight of the actual problem statement. There's no conclusion to jump to, you spelled it out for me in the first sentence of the issue.

If you're looking to share authentication context across services, I'd take a look here and see if this gets you on the right track.

I'll jump to a conclusion here based on the naming of your service: are you trying to run the gateway at port 8000 and treat it as a federated service at the same time? If so, that's not going to work; the gateway has to resolve all schemas before it can compose the final schema.

Exactly that.

Okay, so the solution here is to move the schema from the gateway server to another service.

Thanks for confirming that!

After migration from Schema Stitching to Federation, we tried several ways to keep the Gateway keeps its own schema. We couldn't separate the schema away the Gateway for some reasons.
Finally, we have to start 2 GraphQL server in same server with different port and path. The Federation GraphQL server must be run before Gateway GraphQL server started.

  • Federation GraphQL Server: /graphq-extended on port 3001.
  • Apollo Gateway Server: /graphql on port 3000.

We haven't found any better solution but currently it works ๐Ÿฅ‡ ๐Ÿ”ฅ

Here's the implementation:

export async function runFederationServer({ PORT: portFromEnv = DEFAULT_PORT }) {
  const path = '/graphql-extended'
  const app: express.Application = express()
  // ...
  const serviceSchema = buildFederatedSchema(gatewaySchema.schemas)
  const server = new ApolloServer({
    schema: serviceSchema,
    context: ({ req, res }) => { /* */ }
  })

  return app
}

export async function runServer({ PORT: portFromEnv = DEFAULT_PORT, GRAPHQL_ENDPOINTS: graphqlEndpoints = '' } = {}) {
  const path = '/graphql'
  const app: express.Application = express()
  // ...
  const serviceList = [
   {
     name: 'Extended Schema',
     uri: 'http://0.0.0.0:3001/graphql-extended',
   },
   {
     // Other Federation GraphQL servers
   },
  ]
  const gateway = new ApolloGateway({
    serviceList,
    buildService({ url }) { /* */ }
  })

  return app
}

runFederationServer(process.env).then(runServer(process.env))

You can define a local schema within your gateway code, and trick the gateway into thinking it is a remote service: https://stackoverflow.com/a/61637093/2748290

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jpcbarros picture jpcbarros  ยท  3Comments

dbrrt picture dbrrt  ยท  3Comments

hiucimon picture hiucimon  ยท  3Comments

manuelfink picture manuelfink  ยท  3Comments

veeramarni picture veeramarni  ยท  3Comments