Hi,
I am trying to implement subscriptions from a GraphiQL server. Schemas, resolvers, and the server is done and appears to be working. (I got no issues with the subscriptions from the graphiql surface).
But implementing to the client I have the issue:
ws://localhost:3000/subscriptions
turns back to
http://localhost:3000/subscriptions
in the browser request. Other graphQL queries run through well, only subscriptions get stacked. I am using NextJS with Apollo client 2.00
const client = new SubscriptionClient(
wsUri,
{
reconnect: true
},
)
If I remove the 'ws' from the SubscriptionClient, and look at the browser the request works well. It doesnt force the 'ws:' to turn into 'http'. But than my site doesnt load because this error appears:
Unable to find native implementation, or alternative implementation for Websocket!
so my apollo code goes with 'ws' added as third argument . Any idea how to solve the problem? Subscriptions work as expected from graphiQL surface.
Client Apollo code :
```import ApolloClient from 'apollo-client'
import { ApolloLink } from 'apollo-link'
import { split } from 'apollo-link'
import { HttpLink } from 'apollo-link-http'
import WebSocketLink from 'apollo-link-ws'
import { SubscriptionClient } from 'subscriptions-transport-ws'
import { getOperationAST } from 'graphql'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { getMainDefinition } from 'apollo-utilities'
import fetch from 'isomorphic-fetch'
import ws from 'ws'
let apolloClient = null
const httpUri = 'http://localhost:3000/graphql'
const wsUri = 'ws://localhost:3000/subscriptions'
// Polyfill fetch() on the server (used by apollo-client)
if (!process.browser) {
global.fetch = fetch
}
const client = new SubscriptionClient(
wsUri,
{
reconnect: true
},
ws
)
const hasSubscriptionOperation = ({ query: { definitions } }) =>
definitions.some(
({ kind, operation }) =>
kind === 'OperationDefinition' && operation === 'subscription'
)
const link = ApolloLink.split(
hasSubscriptionOperation,
new WebSocketLink(client),
new HttpLink({ uri: httpUri })
)
function create(initialState) {
return new ApolloClient({
connectToDevTools: process.browser,
ssrMode: !process.browser, // Disables forceFetch on the server (so queries are only run once)
link,
cache: new InMemoryCache().restore(initialState || {})
})
}
export default function initApollo(initialState) {
// Make sure to create a new client for every server-side request so that data
// isn't shared between connections (which would be bad)
if (!process.browser) {
return create(initialState)
}
// Reuse client on the client-side
if (!apolloClient) {
apolloClient = create(initialState)
}
return apolloClient}
Had the same issue, realized it was related to ssr.
You must use the wslink only in the browser, so I did this:
const wsLink = process.browser ? new WebSocketLink({ // if you instantiate in the server, the error will be thrown
uri: `ws://localhost:4000/subscriptions`,
options: {
reconnect: true
}
}) : null;
const httplink = new HttpLink({
uri: 'http://localhost:3000/graphql',
credentials: 'same-origin'
});
const 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;
It will only create the split in the browser, otherwise it will only use the http link.
^ should be added to the README wherever websocket stuff is mentioned. Just wasted hours because of outdated README.
Apollo needs a housekeeping epic before better OSS comes along and retires it.
@corysimmons feel free to make a PR that updates the readme, so you can be the OSS effort that improves Apollo instead of makes a new OSS...
@wmertens No. But cool copypasta and thanks for the @ =)
@DevNvll and @corysimmons -> First off, this was super-helpful. Quick question : How does the authentication work in this case because NextJS is SSR and wslink is client-side. I am not sure how to grab the access_token for wslink running on browser in this case. Code sample:
const wsLink = isBrowser ? new WebSocketLink({
uri: 'ws://127.0.0.1:8080/v1/graphql',
options: {
lazy: true,
reconnect: true,
connectionParams: () => {
return {headers: {Authorization: 'Bearer TOKEN'}}
}
}
}) : null;
@kanil4 Haven't had time to sit down and watch this one but this guy usually has really good videos. I think this might help you: https://www.youtube.com/watch?v=25GS0MLT8JU
This solution helped me fix my wslink using nextjs but it also prevents me from seeing any httplink requests in the browser network tab. Anyone have a fix for this?
Thanks a lot DevNvll still VERY usefull in late 2019 !!!
Thanks @DevNvll . Solution still works in early 2020 on a Gatsby site hosted by Netlify.
Thanks @DevNvll! It works with my Gatsby Apollo setup. However, one question, how do we implement authLink. I tried to concat authLink with httpLink and I get an error cannot read property concat of null. Anyone has successfully implemented authLink too in SSR to enable authentication? Thanks
For an example using Apollo client v3.0 and TypeScript, here is my version:
```import { ApolloClient, InMemoryCache, ApolloLink, HttpLink, split, OperationVariables } from '@apollo/client'
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
}
```
Hey @dan-lynch thanks for the updated example. Exactly what I'm trying to get working at the moment. One off-topic question about something that confuse me to this day. Hope you don't mind.
Should errorLink always come first? And before authLink? If auth gets an error (token expired) link chain then falls back to the errorLink? ErrorLink can then handle refresh token logic if your example would be exanded?
And by the way getMainDefinition seem to be available in @apollo/client/utilities so no need to install it separately 馃憤
Had the same issue, realized it was related to ssr.
You must use the wslink only in the browser, so I did this:const wsLink = process.browser ? new WebSocketLink({ // if you instantiate in the server, the error will be thrown uri: `ws://localhost:4000/subscriptions`, options: { reconnect: true } }) : null; const httplink = new HttpLink({ uri: 'http://localhost:3000/graphql', credentials: 'same-origin' }); const 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;It will only create the split in the browser, otherwise it will only use the http link.
THANK YOU! I HAVE NO IDEA WHY ITS WORKING AFTER I COPY AND PASTED YOUR CODE. BUT IT IS!!!!
const httpLink = new HttpLink({
uri: GRAPHQL_HTTP_ENDPINT,
credentials: 'same-origin',
});
const wssLink = new WebSocketLink({
uri: GRAPHQL_WSS_ENDPOINT,
options: {
reconnect: true,
},
webSocketImpl: require('websocket').w3cwebsocket
});
function createApolloClient() {
return new ApolloClient({
ssrMode: typeof window === 'undefined',
link: split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wssLink,
httpLink,
),
cache: new InMemoryCache(),
});
}
In Next, GraphQl project
Here is an Angular implementation for anyone looking. This happens to use firebase for the token, but anything should work here...
import * as ws from 'ws';
import { Inject, NgModule, PLATFORM_ID } from "@angular/core";
import { Apollo } from "apollo-angular";
import { ApolloLink, split } from "apollo-link";
import { getMainDefinition } from "apollo-utilities";
import { InMemoryCache } from "apollo-cache-inmemory";
import { HttpLink, HttpLinkModule } from "apollo-angular-link-http";
import { WebSocketLink } from "apollo-link-ws";
import { setContext } from "apollo-link-context";
import { FirebaseService } from "./firebase.service";
import { onError } from "apollo-link-error";
import { isPlatformBrowser } from '@angular/common';
const uri = "YOUR ENDPOINT";
@NgModule({
imports: [HttpLinkModule],
})
export class ApolloModule {
constructor(apollo: Apollo, httpLink: HttpLink, fs: FirebaseService, @Inject(PLATFORM_ID) platformId: Object) {
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
graphQLErrors.map(({ message, locations, path }) =>
console.log(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
)
);
if (networkError) {
console.log(`[Network error]: ${networkError}`);
}
}
});
const http = ApolloLink.from([
setContext(async () => {
return {
headers: await fs.getToken()
};
}),
httpLink.create({
uri: `https://${uri}`
})
]);
let link: ApolloLink;
if (!isPlatformBrowser(platformId)) {
// Create a WebSocket link:
const wsl = new WebSocketLink({
uri: `wss://${uri}`,
options: {
reconnect: true,
connectionParams: async () => {
return await fs.getToken()
}
},
webSocketImpl: ws
});
link = split(
// split based on operation type
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === "OperationDefinition" &&
definition.operation === "subscription"
);
},
wsl,
http
);
} else {
link = http;
}
apollo.create({
link: errorLink.concat(link),
cache: new InMemoryCache()
} as any);
}
}
Most helpful comment
Had the same issue, realized it was related to ssr.
You must use the wslink only in the browser, so I did this:
It will only create the split in the browser, otherwise it will only use the http link.