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

example: with-apollo-subscription #10902

Closed
tobkle opened this issue Mar 9, 2020 · 14 comments
Closed

example: with-apollo-subscription #10902

tobkle opened this issue Mar 9, 2020 · 14 comments

Comments

@tobkle
Copy link

tobkle commented Mar 9, 2020

Feature request

A working example with a simple Apollo Client Subscription.

Is your feature request related to a problem? Please describe.

Trying to get an Apollo Client setup to work with Next.JS version 9 using the with-apollo example and switching between apollo-link-ws and apollo-link-http by using the http-link split option: There is a hint for such a solution in this example: subscriptions-transport-ws. However, I'm trying now for days to figure out, how this works. Having overcome all errors meanwhile, and get the useQuery to work, but the useSubscription never leaves the loading state. There was another hint here: addressing such an infinite loading state. But I don't get it working.

A clear and concise description of what you want and what your use case is.
Want to use Apollo Subscriptions with Next.JS Version 9.

Describe the solution you'd like

A clear and concise description of what you want to happen.

Describe alternatives you've considered

Based on the example with-apollo, tried to do that:

./ApolloClient.js:

import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { split } from 'apollo-link';
import { HttpLink } from 'apollo-link-http';
import { WebSocketLink } from 'apollo-link-ws';
import { setContext } from 'apollo-link-context';
import { getMainDefinition } from 'apollo-utilities';
import { SubscriptionClient } from 'subscriptions-transport-ws';
import { parseCookies } from 'nookies';
import * as ws from 'ws';
import fetch from 'isomorphic-unfetch';

export default function createApolloClient(initialState, ctx) {
  const HASURA_HTTP_URI = `https://${SERVER}/v1/graphql`
  const HASURA_WS_URI = `wss://${SERVER}/v1/graphql`
  const COOKIE_JWT_TOKEN = process.env.COOKIE_JWT_TOKEN;
  const token = process.browser ? parseCookies()[COOKIE_JWT_TOKEN] : '';

  const authLink = setContext((_, { headers }) => {
	return {
		headers: {
			...headers,
			authorization: token ? `Bearer ${token}` : ''
		}
	};
  });

 let wsLink = new WebSocketLink({
	uri: HASURA_WS_URI,
	options: {
		lazy: true,
		reconnect: true,
		connectionParams: {
			headers: {
				Authorization: token ? `Bearer ${token}` : ''
			}
		}
	},
	webSocketImpl: ws
  });

  let httpLink = authLink.concat(
	new HttpLink({
		uri: HASURA_HTTP_URI,
		credentials: 'include',
		fetch: fetch
	})
  );

  let myLink = process.browser
	? split(
		({ query }) => {
			const { kind, operation } = getMainDefinition(query);
			return kind === 'OperationDefinition' && operation === 'subscription';
		},
		wsLink,
		httpLink
	  )
	: httpLink;

  return new ApolloClient({
	ssrMode: typeof window === 'undefined',
	link: myLink,
	cache: new InMemoryCache().restore(initialState)
  });
}

./pages/apollo_subscription.js:

import Layout from '../components/layouts/Default';
import { withApollo } from '../lib/apollo';
import { useSubscription /* as __useSubscription */ } from '@apollo/react-hooks';
import gql from 'graphql-tag';

// const useSubscription = (query, options) => (variables) => {
//   const [values, setValues] = useState({ loading: true });
//   __useSubscription(
//     query,
//     {
//       ...options,
//       variables,
//       onSubscriptionData: (options) => {
//         console.log(options)
//         setValues(() => options.subscriptionData);
//       }
//     }
//   );
//   return values;
// };

const MEMBERSHIPS = gql`
	subscription MySubscription {
		my_clients {
			client_id
		}
	}
`;

const ApolloSubscription = () => {
	const { loading, error, data } = useSubscription(MEMBERSHIPS, { variables: {} });

	React.useEffect(
    () => { console.log('loading:', loading, 'data:', data, 'error:', error); 
  },[ loading, data, error ]);

	if (loading) return <Layout pageTitle="Apollo">Loading ...</Layout>;
	if (error)
	       return (
		<Layout pageTitle="Apollo">
				<pre>Error ... {JSON.stringify(error, null, 2)}</pre>
			</Layout>
	      );

	return (
		<React.Fragment>
			<Layout pageTitle="Apollo">
				<h3>Subscriptions</h3>
				<pre>
					DATA:
					{data && JSON.stringify(data, null, 2)}
				</pre>
			</Layout>
		</React.Fragment>
	);
};

export default withApollo({ ssr: false })(ApolloSubscription);

A clear and concise description of any alternative solutions or features you've considered.

Additional context

Add any other context or screenshots about the feature request here.

@tobkle
Copy link
Author

tobkle commented Mar 10, 2020

Found a solution without apollo using npm swr instead...

@johnniehard
Copy link

Found this issue because I also had some initial trouble with using useSubscription. These changes seem to be working for me so far. I can use useSubscription just fine.

apolloClient.js from with-apollo example, with changes to use ws if client-side:

import { ApolloClient } from 'apollo-client'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { HttpLink } from 'apollo-link-http'
import fetch from 'isomorphic-unfetch'

import { WebSocketLink } from "apollo-link-ws";
import { SubscriptionClient } from "subscriptions-transport-ws";

export default function createApolloClient(initialState, ctx) {

    const ssrMode = Boolean(ctx)
    // The `ctx` (NextPageContext) will only be present on the server.
    // use it to extract auth headers (ctx.req) or similar.

    let link
    if (ssrMode) {
        link = new HttpLink({
            uri: "http://localhost:8080/v1/graphql", // Server URL (must be absolute)
            credentials: 'same-origin', // Additional fetch() options like `credentials` or `headers`
            fetch,
        })
    } else {
        const client = new SubscriptionClient("ws://localhost:8080/v1/graphql", {
            reconnect: true
        });

        link = new WebSocketLink(client);
    }

    return new ApolloClient({
        ssrMode,
        link,
        cache: new InMemoryCache().restore(initialState),
    })
}

Is there any problem with this solution that I haven't bumped into yet? Or should something like this perhaps be included in the with-apollo example?

@tobkle
Copy link
Author

tobkle commented Mar 16, 2020

Thanks a lot! @johnniehard With your hints I got it working.
Added the authorization logic additionally.
Would be usefull having it in a with-apollo-subscription example

import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
import fetch from 'isomorphic-unfetch';
import { WebSocketLink } from 'apollo-link-ws';
import { SubscriptionClient } from 'subscriptions-transport-ws';
import { parseCookies } from 'nookies';

const SERVER = process.env.GRAPHQL_SERVER;
const HTTP_URI = `https://${SERVER}/v1/graphql`;
const WS_URI = `wss://${SERVER}/v1/graphql`;
const COOKIE_JWT_TOKEN = process.env.COOKIE_JWT_TOKEN;

export default function createApolloClient(initialState, ctx) {
  const ssrMode = (typeof window === 'undefined');

  let link, token;
  if (ssrMode) {
    // on Server...
    token = parseCookies(ctx)[COOKIE_JWT_TOKEN]
    link = new HttpLink({
	uri: HTTP_URI,
	credentials: 'same-origin',
	headers: {
	  authorization: token ? `Bearer ${token}` : ''
	},
	fetch
    });
  } else {
    // on Client...
    token = parseCookies()[COOKIE_JWT_TOKEN]
    const client = new SubscriptionClient(
      WS_URI, {
        reconnect: true,
        connectionParams: {
          headers: {
            authorization: token ? `Bearer ${token}` : ''
          }
        }
      }
    );
   link = new WebSocketLink(client);
}

return new ApolloClient({
  ssrMode,
  link,
  cache: new InMemoryCache().restore(initialState)
  });
}

@tobkle tobkle closed this as completed Mar 16, 2020
@johnniehard
Copy link

@tobkle what's COOKIE_JWT_TOKEN in your example? The name of the cookie?

@tobkle
Copy link
Author

tobkle commented Mar 21, 2020

yes

@schwamic
Copy link

schwamic commented Apr 22, 2020

@tobkle I use auth0 as auth-provider and therefore the session-cookie is httpOnly, which means parseCookie() can't find the session-cookie on client side. I think it is a security issue if you use session-cookies which are accessable via document.cookie... more here: owasp-http-only

This is my auth-solution without cookies:

import { ApolloClient } from 'apollo-client'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { HttpLink } from 'apollo-link-http'
import { setContext } from 'apollo-link-context'
import { onError } from 'apollo-link-error'
import fetch from 'isomorphic-unfetch'
import { WebSocketLink } from 'apollo-link-ws'
import { SubscriptionClient } from 'subscriptions-transport-ws'
import { split } from 'apollo-link'
import { getMainDefinition } from 'apollo-utilities'

let accessToken = null

const requestAccessToken = async () => {
  if (accessToken) return

  const res = await fetch(`${process.env.DOMAIN}/api/session`)
  if (res.ok) {
    const json = await res.json()
    accessToken = json.accessToken
  } else {
    accessToken = 'public'
  }
}

// return the headers to the context so httpLink can read them
const authLink = setContext(async (req, { headers }) => {
  await requestAccessToken()
  if (!accessToken || accessToken === 'public') {
    return {
      headers,
    }
  } else {
    return {
      headers: {
        ...headers,
        Authorization: `Bearer ${accessToken}`,
      },
    }
  }
})

// remove cached token on 401 from the server
const resetTokenLink = onError(({ networkError }) => {
  if (networkError && networkError.name === 'ServerError' && networkError.statusCode === 401) {
    accessToken = null
  }
})

const httpLink = new HttpLink({
  uri: process.env.API_URL,
  credentials: 'include',
  fetch,
})

const createWSLink = () => {
  return new WebSocketLink(
    new SubscriptionClient(process.env.API_WS, {
      lazy: true,
      reconnect: true,
      connectionParams: async () => {
        await requestAccessToken()
        return {
          headers: {
            authorization: accessToken ? `Bearer ${accessToken}` : '',
          },
        }
      },
    })
  )
}

export default function createApolloClient(initialState, ctx) {
  accessToken = null
  const ssrMode = typeof window === 'undefined'
  let link
  if (ssrMode) {
    link = authLink.concat(resetTokenLink).concat(httpLink)
  } else {
    const wsLink = createWSLink()
    link = split(
      ({ query }) => {
        const definition = getMainDefinition(query)
        return definition.kind === 'OperationDefinition' && definition.operation === 'subscription'
      },
      resetTokenLink.concat(wsLink),
      authLink.concat(resetTokenLink).concat(httpLink)
    )
  }
  return new ApolloClient({
    ssrMode,
    link,
    cache: new InMemoryCache().restore(initialState),
  })
}

@themmyloluwaa
Copy link

Hi @johnniehard and @tobkle, thank you for the solution, works like a charm. I noticed that for every mutation, this current set up uses the websocket setup instead of the HTTP setup. I'm wondering if this doesn't have any performance implication on the server or the app itself.

@themmyloluwaa
Copy link

#Just Incase anyone needs a solution to the problem stated above.

import { ApolloClient } from "apollo-client";
import { InMemoryCache } from "apollo-cache-inmemory";
import { HttpLink } from "apollo-link-http";
import fetch from "isomorphic-unfetch";
import { WebSocketLink } from "apollo-link-ws";
import { SubscriptionClient } from "subscriptions-transport-ws";
import cookie from "js-cookie";
import { getMainDefinition } from "apollo-utilities";
import { split } from "apollo-link";

const URI = "http://localhost:4000";
const WS_URI = "ws://localhost:4000";

export default function createApolloClient(initialState, ctx) {
  // console.log("in apolloCLient", ctx);
  // let token;
  let link, token, httpLink, wsLink;
  const ssrMode = typeof window === "undefined";
  token = cookie.get("token");
  // console.log("in apolloCLient", token);

  httpLink = new HttpLink({
    uri: URI,
    credentials: "same-origin",
    headers: {
      Authorization: token ? `Bearer ${token}` : ""
    },

    fetch
  });

  if (ssrMode) {
    return new ApolloClient({
      ssrMode,
      link: httpLink,
      cache: new InMemoryCache().restore(initialState)
    });
  } else {
    // on Client...

    const client = new SubscriptionClient(WS_URI, {
      reconnect: true,
      connectionParams: {
        // headers: {
        Authorization: token ? `Bearer ${token}` : ""
        // }
      }
    });
    wsLink = new WebSocketLink(client);

    link = process.browser
      ? split(
          //only create the split in the browser
          // split based on operation type
          ({ query }) => {
            const { kind, operation } = getMainDefinition(query);
            return (
              kind === "OperationDefinition" && operation === "subscription"
            );
          },
          wsLink,
          httpLink
        )
      : httpLink;

    return new ApolloClient({
      ssrMode,
      link,
      cache: new InMemoryCache().restore(initialState)
    });
  }
}

@dan-lynch
Copy link

For an example using Apollo client v3.0 and TypeScript, here is my version:

import { WebSocketLink } from '@apollo/link-ws'
import { onError } from '@apollo/link-error'
import { setContext } from '@apollo/link-context'
import { getMainDefinition } from 'apollo-utilities'
import { API_URL, WS_URL } from 'helpers/constants'
import { userService } from 'services/userService'

global.fetch = require('node-fetch')

let globalApolloClient: any = null

const wsLinkwithoutAuth = () =>
  new WebSocketLink({
    uri: WS_URL,
    options: {
      reconnect: true,
    },
  })

const wsLinkwithAuth = (token: string) =>
  new WebSocketLink({
    uri: WS_URL,
    options: {
      reconnect: true,
      connectionParams: {
        authToken: `Bearer ${token}`,
      },
    },
  })

function createIsomorphLink() {
  return new HttpLink({
    uri: API_URL,
  })
}

function createWebSocketLink() {
  return userService.token ? wsLinkwithAuth(userService.token) : wsLinkwithoutAuth()
}

const errorLink = onError(({ networkError, graphQLErrors }) => {
  if (graphQLErrors) {
    graphQLErrors.map((err) => {
      console.warn(err.message)
    })
  }
  if (networkError) {
    console.warn(networkError)
  }
})

const authLink = setContext((_, { headers }) => {
  const token = userService.token
  const authorization = token ? `Bearer ${token}` : null
  return token
    ? {
        headers: {
          ...headers,
          authorization,
        },
      }
    : {
        headers: {
          ...headers,
        },
      }
})

const httpLink = ApolloLink.from([errorLink, authLink, createIsomorphLink()])

export function createApolloClient(initialState = {}) {
  const ssrMode = typeof window === 'undefined'
  const cache = new InMemoryCache().restore(initialState)

  const link = ssrMode
    ? httpLink
    : process.browser
    ? split(
        ({ query }: any) => {
          const { kind, operation }: OperationVariables = getMainDefinition(query)
          return kind === 'OperationDefinition' && operation === 'subscription'
        },
        createWebSocketLink(),
        httpLink
      )
    : httpLink

  return new ApolloClient({
    ssrMode,
    link,
    cache,
  })
}

export function initApolloClient(initialState = {}) {
  if (typeof window === 'undefined') {
    return createApolloClient(initialState)
  }

  if (!globalApolloClient) {
    globalApolloClient = createApolloClient(initialState)
  }

  return globalApolloClient
}

@david718
Copy link

how to make apollo server for subscription in client??

@dan-lynch
Copy link

dan-lynch commented Jul 18, 2020

@david718

If you're using Apollo client, you don't need an Apollo server specifically - any GraphQL server will do!

There's several ways of going about this - for example, I'm using PostGraphile, which creates a GraphQL server based on a PostgreSQL DB)

For more information around Apollo Server specifically, here's a good place to start:
https://www.apollographql.com/docs/apollo-server/

@tettoffensive
Copy link

@david718 I'm trying to figure out the same thing.

So far I've got in my apollo-micro-server

/pages/api/graphql.ts:

const apolloServer = new ApolloServer({
  typeDefs,
  resolvers,
  subscriptions: {
    path: '/api/graphql',
    keepAlive: 9000,
    onConnect: () => console.log('connected'),
    onDisconnect: () => console.log('disconnected'),
  },

But it seems somehow, you've got to do something with apolloServer.installSubscriptionHandlers() to handle websocket requests.

I can't really figure that out.

@tobkle How did you do this? You said you used swr (which I am using for my other graphql requests), but that example doesn't set anything up with the server.

@acomito
Copy link

acomito commented Jan 18, 2021

@dan-lynch

What is going on here?

export function initApolloClient(initialState = {}) {
  if (typeof window === 'undefined') {
    return createApolloClient(initialState)
  }

  if (!globalApolloClient) {
    globalApolloClient = createApolloClient(initialState)
  }

  return globalApolloClient
}

doesn't this always return createApolloClient(initialState) as globalApolloClient is null?

@balazsorban44
Copy link
Member

This issue has been automatically locked due to no recent activity. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.

@vercel vercel locked as resolved and limited conversation to collaborators Jan 28, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants