How do you shut down a stale Apollo-Client websocket connection? - apollo-client

I have a page that is loading connected apollo-client pages as widgets inside of a main page. My GraphQL server is self-hosted. They are served up through iFrame (self-hosted), connecting back to my own server. Communications are done through a 3rd party's iFrame communications SDK.
For some reason the widgets are not cleared out from the window when they are stale (I have no control over this). However I do have access to know when they are "stale". When they turn stale I want to disconnect / shutdown the websocket connection. The trouble is the still-connected clients are eating up my back-end's CPU. I am watching the websocket connection through chrome dev-tools. I notice every 5 seconds it sends a keep-alive request to the websocket sever. Every so often I see a stop request, and I want to figure out how to replicate that.
Im my react apollo-connected component I tried calling these two commands, but after they are called with no errors, the keep-alive flags are still being sent to the websocket server.
this.props.client.stop()
this.props.client.clearStore();
How do I tell the apollo-client to shut itself down?

For Apollo V3, the WebSocketLink has an internal SubscriptionClient instance, but the problem is that WebSocketLink doesn't expose methods that give you access to the SubscriptionClient instance, so there's no accessing SubscriptionClient.close(). Fortunately, WebSocketLink accepts a client as an argument:
const subscriptionClient = new SubscriptionClient(`wss://example.com/subscriptions`, {
// example options:
reconnect: true,
lazy: true,
connectionParams: () => ({ accessToken: 'secret' }),
});
const wsLink = new WebSocketLink(subscriptionClient);
Now you just need to move subscriptionClient into a context in order to gain access to the client in various places:
export const SubscriptionClientContext = createContext<
SubscriptionClient | undefined
>(undefined);
export const useSubscriptionClient = (): SubscriptionClient => {
const subscriptionClient = useContext(SubscriptionClientContext);
if (subscriptionClient === undefined) {
throw Error(
'SubscriptionClient not initiated, can only be called inside SubscriptionClientContext.Provider',
);
}
return subscriptionClient;
};
<SubscriptionClientContext.Provider value={subscriptionClient}>
<App />
</SubscriptionClientContext.Provider>
This will let you access methods on the client for logout behavior in various parts of the app:
const subscriptionClient = useSubscriptionClient();
subscriptionClient.close();
There are also two arguments for .close, that have various behaviors. E.g. close and reconnect, close and do not reconnect.

Related

Nextjs with ApolloClient with basic routing to separate graphql server

I am building a Nextjs App that has a separate GraphQL server endpoint. I wanted to be able to use ApolloClient (React) for this project, just to gain familiarity with the technology.
I used the Nextjs with-apollo example to get started. My understanding is that it creates a separate ApolloClient for Server side and Client side GraphQL requests. My current problem is that the GraphQL endpoint I want to access requires Authorization (meaning I need to pass it a Bearer API token) I don't want to leave that API token in the NEXT_PUBLIC environment variables for fear that someone might be able to find it.
So my question is: What is the best approach here? Do i:
Send the requests to my Nextjs server before sending them to the separate GraphQL endpoint to conceal my environment variable? Can I do that with #apollo/client HTTPLink? Can I still use useQuery or do I need to use something like axios?
Only create 1 ApolloClient (on the server, with the credentials) and pass that to the browser as well? How would I do that?
Create a REST endpoint that my client-side Next Application can query to get the credentials?
Is there a canonical way of getting secrets to the client without exposing them?
Some other method...
Reference:
// lib/apolloClient.js
// ... imports ignored ...
let apolloClient;
function createApolloClient() {
// this line is the line in question...
// potentially exposing my API_TOKEN because NEXT_PUBLIC_ env variables
// are exposed on both the server and the client
let apiToken = process.env.NEXT_PUBLIC_API_TOKEN
return new ApolloClient({
ssrMode: typeof window === "undefined", // set to true for SSR
uri: "https://my-separate-graphql-server/endpoint",
headers: {
Authorization: 'Bearer ' + apiToken,
},
cache: new InMemoryCache(),
});
}
export function initializeApollo(initialState = null) {
const _apolloClient = apolloClient ?? createApolloClient();
// If your page has Next.js data fetching methods that use Apollo Client,
// the initial state gets hydrated here
if (initialState) {
// Get existing cache, loaded during client side data fetching
const existingCache = _apolloClient.extract();
// Restore the cache using the data passed from
// getStaticProps/getServerSideProps combined with the existing cached data
_apolloClient.cache.restore({ ...existingCache, ...initialState });
}
// For SSG and SSR always create a new Apollo Client
if (typeof window === "undefined") return _apolloClient;
// Create the Apollo Client once in the client
if (!apolloClient) apolloClient = _apolloClient;
return _apolloClient;
}
export function useApollo(initialState) {
const store = useMemo(() => initializeApollo(initialState), [initialState]);
return store;
}

Is there analog of express's app.set( 'something', something ) in koa?

I need socket.io instance in several places in my app. To achieve this in express i can do this:
app.set('io', io);
In koa right now i have this:
app.use( async ( ctx, next ) => {
ctx.io = io;
await next();
});
This works, but this middleware executes every time my server recieves request. Is there a better way to do this?
I don't know how you are fully implementing but there are a couple things that you can do is you can either pass an addition argument and upgrade the connection to a websocket that will bypass the rest of the middlewares. Or, what I do personally is just have any websocket connection go to a different end point. This will help with any future scalability issues. for example, if you need to create clusters of your server then you will have more control as well will help you testing your backend easier. That's what I would do atleast. My socket.io back end looks like this:
server.ts
oh yea I'm using typescript in the back end
require('dotenv').config({ path: __dirname + '/.env' });
import Koa from 'koa';
const koa = new Koa();
import cors from '#koa/cors';
const PORT = process.env.CHAT_PORT || 3000;
const ENV = process.env.NODE_ENV || 'development';
const server = require('http').createServer(app, { origins: 'http://server.ip' });
const io = (module.exports.io = require('socket.io')(server));
import SocketManager from './lib/SocketManager';
app.use(
cors({
origin: '*',
optionsSuccessStatus: 200,
}),
);
// server setup
server.listen(PORT, (err: ErrorEvent): void => {
if (err) console.error('❌ Unable to connect the server: ', err);
console.log(`💻 Chat server listening on port ${PORT} - ${ENV} environment`);
});
io.on('connection', SocketManager);
then just create a socket manager that imports the io instance and you can then go ahead and handle all the connections.
I hope this is the answer you were looking for/gave you some better insight.

Can't connect to web socket from Electron when using self signed cert

I have an Electron app which tries to connect to a device over a web socket. The connection is encrypted (i.e. wss) but the SSL certificate is self signed and thus, untrusted.
Connecting inside Chrome is ok and it works. However inside Electron I run into problems. Without putting any certificate-error handlers on the BrowserWindow or on the app I receive the following error in the console output:
WebSocket connection to 'wss://some_ip:50443/' failed: Error in connection establishment: net::ERR_CERT_AUTHORITY_INVALID
Then shortly after:
User is closing WAMP connection.... unreachable
In my code, to make the connection I run the following.
const connection = new autobahn.Connection({
realm: 'some_realm',
url: 'wss://some_ip:50443'
});
connection.onopen = (session, details) => {
console.log('* User is opening WAMP connection....', session, details);
};
connection.onclose = (reason, details) => {
console.log('* User is closing WAMP connection....', reason, details);
return true;
};
connection.open();
// alternatively, this also displays the same error
const socket = new WebSocket(`wss://some_ip:50443`);
socket.onopen = function (event) {
console.log(event);
};
socket.onclose = function (event) {
console.log(event);
};
NOTE: Autobahn is a Websocket library for connecting using the WAMP protocol to a socket server of some sort. (in my case, the device) The underlying protocol is wss. Underneath the code above, a native JS new WebSocket() is being called. In other words:
As I mentioned, I've tested this code in the browser window and it works. I've also built a smaller application to try and isolate the issue. Still no luck.
I have tried adding the following code to my main.js process script:
app.commandLine.appendSwitch('ignore-certificate-errors');
and
win.webContents.on('certificate-error', (event, url, error, certificate, callback) => {
// On certificate error we disable default behaviour (stop loading the page)
// and we then say "it is all fine - true" to the callback
event.preventDefault();
callback(true);
});
and
app.on('certificate-error', (event, webContents, link, error, certificate, callback) => {
// On certificate error we disable default behaviour (stop loading the page)
// and we then say "it is all fine - true" to the callback
event.preventDefault();
callback(true);
});
This changed the error to:
WebSocket connection to 'wss://some_ip:50443/' failed: WebSocket opening handshake was canceled
My understanding is that the 'certificate-error' handlers above should escape any SSL certificate errors and allow the application to proceed. However, they're not.
I've also tried adding the following to main.js:
win = new BrowserWindow({
webPreferences: {
nodeIntegration: true,
webSecurity: false
}
});
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = '1';
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
With Election, how do I properly deal with a certificate from an untrusted authority? i.e. a self signed cert.
Any help would be much appreciated.
I had the same problem , all i added was your line:
app.commandLine.appendSwitch('ignore-certificate-errors');
I use socket.io, but i think its the same principal.
I do however connect to the https protocol and not wss directly.
This is what my connection looks like on the page:
socket = io.connect(
'https://yoursocketserver:yourport', {
'socketpath',
secure: false,
transports: ['websocket']
});
That seems to have done the trick.
Thank you for the help :) i hope this answer helps you too.

Nuxt window is not defined on server-side rendering

I am trying to get the authorization headers from localStorage inside my middleware. Unfortunately this doesn't work on the first page load, because it is server-rendered.
How could I fix this?
const cookieName = 'feathers-jwt';
import { ApolloClient, createNetworkInterface } from 'apollo-client';
import 'isomorphic-fetch';
const API_ENDPOINT = 'http://localhost:3000/graphql';
const networkInterface = createNetworkInterface({ uri: API_ENDPOINT });
networkInterface.use([{
applyMiddleware(req, next) {
if (!req.options.headers) {
req.options.headers = {}; // Create the header object if needed.
}
req.options.headers['authorization'] = window.localStorage.getItem(cookieName);
next();
}
}]);
const apolloClient = new ApolloClient({
networkInterface,
transportBatching: true
});
export default apolloClient;
source: http://dev.apollodata.com/core/network.html
As I understand it, when you're rendering on the server you don't have access to window and document. In apps that render on both the server and in the client, you need to build in a check to see where you are, and handle that accordingly.
You can use this snippet for the detection of where you are:
var canUseDOM = !!(
typeof window !== 'undefined' &&
window.document &&
window.document.createElement
)
Use it to check if you are running server-side or client-side. In your case I would do the following:
If you're server-side you can check the cookies in the HTTP request itself;
If you're client-side you can check your localStorage store instead.
Of course, you can always opt to server-side render your website as an anonymous not authorised user by default. But that would cause the front-end to blink in and out of authorised state and would be annoying for the user.
In your case, I'd try to find authorisation cookies from the actual cookies that are present in your HTTP request.

How do I handle DB connections on HTTP requests?

Do I open and close a connection to a RethinkDB server on every HTTP request? I was reading a tutorial of the official examples. It describes an Express app and it basically looks like:
var app = express();
app.use(openConnection);
app.use(/* do stuff */);
app.use(closeConnection);
Is this considered best practice or is this the only practice since there is no native connection pooling or other approaches?
When it comes to how to deal with connections, there are a couple of options:
Single Connection
The simplest option is to just open a single connection and use it through out your app. This is extremely simple but probably doesn't work for larger applications where you might be executing a lot of requests.
In JavaScript, the connection can be appended to the r object and used throughout the application.
import r from 'rethinkdb';
import express from 'express';
let app = express();
r.connect().then((conn) => {
r.conn = conn;
});
app.use('/table-list', (req, res) => {
r.db('test').tableList().run(conn)
.then((result) => {
return res.json(result);
});
});
Open & Close
You can also just open and close a connection every single time you do a request. This approach is also simple, but it's a bit more verbose.
r.connect().then((conn) =>
return r.db('test').tableList().run(conn)
.then((result) => {
console.log(result);
return result;
})
.then(() => {
conn.close();
});
});
Per Request Connection
As you noted earlier, you can also open a connection per-request, use that connection throughout the request, and then close it when the request is done.
Connection Pooling
Finally, you can also use connection pooling if you use rethinkdbdash, which abstracts away connections for you.

Resources