GraphQL subscription from multiple browsers/tabs - websocket

I have an react frontend and a python backend (using ariadne==0.13.0, uvicorn==0.15.0, uvicorn[standard]==0.15.0, fastapi==0.68.1) communicationg over graphql subscriptions. Everything works fine as long as I do not reload the page or load the page in a new browser window from same IP. Then the page crashes and takes some time to recover - Depending on the websocket timeout configured in uvicorn. I am experiencing the same issue with both my frontend and the graphql playgorund.
I understand that the different browsers or tabs are identified with the same IP, Port and protocol what possibly messes up the existing connection, but still it should be possible the use the page from different tabs as seen in:
https://fastapi.tiangolo.com/advanced/websockets/
My code:
SCHEMA = load_schema_from_path("schema.graphql")
query = QueryType()
subscription = SubscriptionType()
app = FastAPI()
schema = make_executable_schema(SCHEMA, [query, subscription])
graphql_server = GraphQL(schema, debug=True)
app.add_route("/graphql", graphql_server)
app.add_websocket_route("/graphql", graphql_server)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["POST", "GET"],
allow_headers=[
"accept",
"accept-language",
"content-language",
"content-type",
"x-apollo-tracing",
],
)
app.debug = True
uvicorn.run(app, host='0.0.0.0', port=7996)

The default setup for uvicorn is single threaded and the method implementing the subscription was synchronous and blocking. I had to reimplement it in an async manner.

Related

Secure websockets with Cro

Briefly: I created a service on an internet server using Cro and websocket. Very simple using Cro examples. No problem when sending and receiving data from an HTML page when the page is served as localhost. When the page is served using https, the websocket cannot be established.
How is the wss protocol be used with Cro?
Update: After installing cro and running cro stub :secure, the service.p6 has some more code not explicit in the documentation.
More detail:
I have a docker file running on the internet server, Cro is set to listen on 35145, so the docker command is docker --rm -t myApp -p 35145:35145
The service file contains
use Cro::HTTP::Log::File;
use Cro::HTTP::Server;
use Cro::HTTP::Router;
use Cro::HTTP::Router::WebSocket;
my $host = %*ENV<RAKU_WEB_REPL_HOST> // '0.0.0.0';
my $port = %*ENV<RAKU_WEB_REPL_PORT> // 35145;
my Cro::Service $http = Cro::HTTP::Server.new(
http => <1.1>,
:$host,
:$port,
application => routes(),
after => [
Cro::HTTP::Log::File.new(logs => $*OUT, errors => $*ERR)
]
);
$http.start;
react {
whenever signal(SIGINT) {
say "Shutting down...";
$http.stop;
done;
}
}
sub routes() {
route {
get -> 'raku' {
web-socket :json, -> $incoming {
supply whenever $incoming -> $message {
my $json = await $message.body;
if $json<code> {
my $stdout, $stderr;
# process code
emit({ :$stdout, :$stderr })
}
}
}
}
}
}
In the HTML I have a textarea container with an id raku-code. The js script has the following (I set websocketHost and websocketPort elsewhere in the script) in a handler that fires after the DOM is ready:
const connect = function() {
// Return a promise, which will wait for the socket to open
return new Promise((resolve, reject) => {
// This calculates the link to the websocket.
const socketProtocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:');
const socketUrl = `${socketProtocol}//${websocketHost}:${websocketPort}/raku`;
socket = new WebSocket(socketUrl);
// This will fire once the socket opens
socket.onopen = (e) => {
// Send a little test data, which we can use on the server if we want
socket.send(JSON.stringify({ "loaded" : true }));
// Resolve the promise - we are connected
resolve();
}
// This will fire when the server sends the user a message
socket.onmessage = (data) => {
let parsedData = JSON.parse(data.data);
const resOut = document.getElementById('raku-ws-stdout');
const resErr = document.getElementById('raku-ws-stderr');
resOut.textContent = parsedData.stdout;
resErr.textContent = parsedData.stderr;
}
When an HTML file with this JS script is set up, and served locally I can send data to the Cro app running on an internet server, and the Cro App (running in a docker image) processes and returns data which is placed in the right HTML container. Using Firefox and the developer tools, I can see that the ws connection is created.
But when I serve the same file via Apache which forces access via https, Firefox issues an error that the 'wss' connection cannot be created. In addition, if I force a 'ws' connection in the JS script, Firefox prevents the creation of a non-secure connection.
a) How do I change the Cro coding to allow for wss? From the Cro documentation it seems I need to add a Cro::TLS listener, but it isn't clear where to instantiate the listener.
b) If this is to be in a docker file, would I need to include the secret encryption keys in the image, which is not something I would like to do?
c) Is there a way to put the Cro app behind the Apache server so that the websocket is decrypted/encrypted by Apache?
How do I change the Cro coding to allow for wss? From the Cro documentation it seems I need to add a Cro::TLS listener, but it isn't clear where to instantiate the listener.
Just pass the needed arguments to Cro::HTTP::Server, it will set up the listener for you.
If this is to be in a docker file, would I need to include the secret encryption keys in the image, which is not something I would like to do?
No. You can keep them in a volume, or bind-mount them from the host machine.
Is there a way to put the Cro app behind the Apache server so that the websocket is decrypted/encrypted by Apache?
Yes, same as with any other app. Use mod_proxy, mod_proxy_wstunnel and a ProxyPass command. Other frontends such as nginx, haproxy, or envoy will also do the job.
Though is not a pure cro solution, but you can
run your cro app on (none ssl/https) http/web socket port - localhost
and then have an Nginx server (configured to serve https/ssl trafic) to handle incoming public https/ssl requests and bypass them
as a plain http traffic to your app using
nginx reverse proxy mechanism (this is also often referred as an ssl termination), that way you
remove a necessity to handle https/ssl on cro side.
The only hurdle here might be if a web sockets
protocol is handled well by Nginx proxy. I’ve never tried that but probably you should be fine according to the Nginx docs - https://www.nginx.com/blog/websocket-nginx/

How to connect nextjs app to graphql studio

I'm trying to figure out how to open the apollo studio for my next js app.
When I run the local host, I get a message that says:
Server started at http://localhost:5555/graphql
When I click that link, I get a page that says:
GET query missing.
I'm trying to find a way to get to the apollo studio explorer.
For others looking (or maybe for myself the next time I forget), the address: http://localhost:5555/graphql gets inserted in the sandbox address bar, that you find at the url: https://studio.apollographql.com/sandbox/explorer. It won't work if you put the local host address in the url bar
I faced the same issue and have managed to solve it by connecting to apollo studio as a deployed graph (not using the sandbox) but running locally.
Firstly I followed this tutorial https://master--apollo-docs-index.netlify.app/docs/tutorial/production/ which does not use NextJS but it does connect a react app to the apollo studio sandbox then by section 5 it connects the deployed graph to apollo studio. Unfortunately section 5 is quite outdated so i will try to fill in the blanks and get you up and running.
After you have set up an account in apollo studio add a new graph (+ New Graph button). Use whatever architecture you like but I tried this using 'supergraph'.
On the next page ('Publish your schema using Federation') I used the 'schema document' tab and pipeline: 'Federation 2 Supergraph'.This generates the 2 of the 3 env keys you need to add to your local env file and feed it into your app. keys as follows:
APOLLO_KEY - this starts 'service:' and ends before the space, it is a single line about 50 characters long.
APOLLO_GRAPH_REF - this can be found at the end of the line below the APOLLO_KEY. it is a single word with a '#' symbol in the middle.
APOLLO_SCHEMA_REPORTING=true - written as shown here.
Do not close the 'Publish your schema using Federation' page/ re-open it if you have closed it as it will indicate that you have successful connected the graph when you run the app locally after the next step.
Start the app locally using the CLI and in the browser request a page that queries the apollo server.
Watch the CLI as the page is served and you should see the comment 'Apollo usage reporting starting!', also the 'Publish your schema using Federation' page should confirm the graph has been connected. Now you can use all the features of the sandbox as well as monitoring etc.
Hope this helps.
The reason why Next.js doesn't allow you to connect to Apollo Studio is because Next.js does not allow CORS by default in api handlers.
Apollo Studio tries to send a request from its own domain and it's blocked by Next.js default setup.
Let's assume you have your graphql/Apollo server in your NextJs app at /api/graphql path. When you navigate to that path (from your local) by using http://localhost:3000/api/graphql it will show you the welcome page and allow you to access Apollo Sandbox.
Once you enter the Apollo Sandbox in the bottom right corner it will display this message:
When you run the diagnose problem on your local you'll see the following message:
$ npx diagnose-endpoint#1.1.0 --endpoint=http://localhost:3000/api/graphql
Diagnosing http://localhost:3000/api/graphql
⚠️ OPTIONS response is missing header 'access-control-allow-methods: POST'
⚠️ POST response missing 'access-control-allow-origin' header.
If using cookie-based authentication, the following headers are required from your endpoint:
access-control-allow-origin: https://studio.apollographql.com
access-control-allow-credentials: true
Otherwise, a wildcard value would work:
access-control-allow-origin: *
(📫 Interested in previewing a local tunnel to bypass CORS requirements? Please let us know at https://docs.google.com/forms/d/e/1FAIpQLScUCi3PdMerraiy6GpD-QiC_9KEKVHr4oDL5Vef5fIvzqqQWg/viewform )
The solution for the problem looks like this:
import type { NextApiRequest, NextApiResponse } from "next";
import Cors from "cors";
import { server } from "../../apollo";
// Initializing the cors middleware
// You can read here: https://github.com/expressjs/cors#configuration-options
const cors = Cors({
methods: ["POST"],
});
// Helper method to wait for a middleware to execute before continuing
// And to throw an error when an error happens in a middleware
function runMiddleware(
req: NextApiRequest,
res: NextApiResponse,
fn: Function
) {
return new Promise((resolve, reject) => {
fn(req, res, (result: any) => {
if (result instanceof Error) {
return reject(result);
}
return resolve(result);
});
});
}
export const config = {
api: {
bodyParser: false,
},
};
const startServer = server.start();
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
// Run cors middleware (to allow Apollo Studio access)
await runMiddleware(req, res, cors);
// run apollo server
await startServer;
await server.createHandler({ path: "/api/graphql" })(req, res);
}
It combines using the Apollo server and this CORS example
the import:
import { server } from "../../apollo"
from the example above is the apollo server that looks like this:
import { ApolloServer } from "apollo-server-micro";
import { typeDefs } from "./schema";
import { resolvers } from "./resolvers";
export const server = new ApolloServer({
typeDefs,
resolvers,
});
You can also use alternative options like embedding sandbox into your app but I'm finding the above solution a bit easier for my current needs so hope it helps you as well.

GraphQL requesting fields that don't exist without error

I'm trying to handle backwards compatibility with my GraphQL API.
We have on-premise servers that get periodically updated based off of when they connect to the internet. We have a Mobile app that talks to the on-premise server.
Problem
We get into an issue where the Mobile app is up to date and the on-premise server isn't. When a change in the Schema occurs, it causes issues.
Example
Product version 1
type Product {
name: String
}
Product version 2
type Product {
name: String
account: String
}
New version of mobile app asks for:
product(id: "12345") {
name
account
}
Because account is not valid in version 1, I get the error:
"Cannot query field \"account\" on type \"Product\"."
Does anyone know how I can avoid this issue so I don't recieve this particular error. I'm totally fine with account coming back with Null or just some other plan of attack for updating Schema's. But having it completely blow up with no response is not good
Your question did not specify what you're actually using on the backend. But it should be possible to customize the validation rules a GraphQL service uses in any implementation based on the JavaScript reference implementation. Here's how you do it in GraphQL.js:
const { execute, parse, specifiedRules, validate } = require('graphql')
const validationRules = specifiedRules.filter(rule => rule.name !== 'FieldsOnCorrectType')
const document = parse(someQuery)
const errors = validate(schema, document, validationRules)
const data = await execute({ schema, document })
By omitting the FieldsOnCorrectType rule, you won't get any errors and unrecognized fields will simply be left off the response.
But you really shouldn't do that.
Modifying the validation rules will result in spec-breaking changes to your server, which can cause issues with client libraries and other tools you use.
This issue really boils down to your deployment process. You should not push new versions of the client that depend on a newer version of the server API until that version is deployed to the server. Period. That would hold true regardless of whether you're using GraphQL, REST, SOAP, etc.

Getting proxy settings from electron

When i set proxy settings in the system my Electron application gets it automatically for common requests (axios package), but not for websockets (socket.io-client package). If i provide proxy settings manually then websockets starts to work too, but i want to try to avoid manual configuration, because it may be tricky to the real app's users. So i'm looking any way to get proxy settings from electron and transfer them to the websockets.
I have tried to use login event for my browserWindow in the both main and renderer process, but it doesn't trigger at all. Is there any way to get proxy settings that electron uses?
I've found the solution. It's possible to take proxy-settings inside the main process.
mainWindow = new BrowserWindow({})
const ses = mainWindow.webContents.session;
ses.resolveProxy('url_you_need_to_achieve', (proxy) => {
// do whatever you want with proxy string, that contains proxy-setting
});

why is my express middleware session undefined?

I'm trying to use the express middleware for sessions (I'm not understanding but I feel that I am very close).
the reason I have ended up asking is that the express docs (http://expressjs.com/api.html#middleware) are calling it express.cookieSession where as everyone else (mostly on here) Ive seen discussing it have been calling it express.session. I'm really not sure now, I just have a big lump of possibly useful code ??? but every example I see is different ... how does it work?
var express = require('express')
, http = require('http');
, app = express();
store = new express.session.MemoryStore;
app.use(express.cookieParser());
//app.use(express.cookieSession());
app.use(express.session({secret:'whateverman',key:'express.sid',store:store}));
app.use(app.router);
app.all('/*',function(req,res,next){res.header("Access-Control-Allow-Origin","*");res.header("Access-Control-Allow-Headers","X-Requested-With");next();});
var server = http.createServer(app);
server.listen(8000);
var io = require('socket.io').listen(server);
io.sockets.on('connection',function(socket){
socket.on('reglogin',function(_){_.session.e='some#email.com';});
});
socket.on('reglogin' produces:
TypeError: Cannot set property 'e' of undefined
//-------------------------------update on question
if express is dependant on connect but express can access connects middleware as if it were its own, eg:
app.use(express.cookieSession()); //app is express
app.use(connect.cookieSession()); //does exactly the same
then surely the same logic would apply to socket.io which is dependant on express:
io.use(connect.CookieSession()); //io is socket.io
Am I wrong here? does Socket.io have the same .use method? update:(answer is no to io.use)
io.interoperate(app.use(express.cookieSession())); LOL
----------------------------UPDATE---------------------------------
I've followed the following npm modules guide lines in a despirate attemp to get sessions working and filed each and every time on handshake with no cookie:
express.io
session.socket.io
session.io + sessionstore
currently using the latter with console log:
warn - handshake error Could not find cookie with key: connect.sid

Resources