How to properly handle errors on subscriptions with Apollo Server? - node.js

I have an Express + Apollo Server backend. I enabled subscriptions on it using ws and graphql-ws. Everything is working fine.
Now, I would like to handle resolvers errors properly: hide backend details in production, change message based on error type, add a unique ID, etc. On regular mutations, I'm able to do so using the formatResponse function.
On subscriptions, I can't find where I could do it. All I need is a function called before sending data to the client where I have access to data and errors.
How can I do that?
Here's how the WS Server is created:
// Create Web Socket Server
const wsServer = new WebSocketServer({
server: httpServer,
path: '/graphql'
});
const serverCleanup = graphqlWS.useServer(
{
schema: graphqlApp.schema,
context: async (ctx: any) => {
try {
// ...Some auth checking...
return context;
} catch (e) {
throw new ApolloAuthenticationError('you must be logged in');
}
}
},
wsServer
);
And an example of event sending:
import {PubSub} from 'graphql-subscriptions';
// ...
Subscription: {
tree: {
subscribe: withFilter(
() => pubsub.asyncIterator('some_id'),
(payload, variables) => {
const canReturn = true;
//...Some filtering logic...
return canReturn;
}
)
}
},

Related

Websockets with RTK Query configuration issues

I am trying to implement a Websocket connection from a React TypeScript app using RTK query. At the moment I am just trying to connect to a local socket.io server BUT ultimately it will be an AWS API Gateway with Cognito auth. In any case I having some problems getting this to work as a simple starting point. I have a few elements at play that may be causing the issue/s:-
MSW is being used to intercept http requests to mock a restful API locally. I wonder if this is one of the issues
I am adding the Websocket as a query to an RTK Query createApi object with other queries and mutations. In reality the Websocket query will need to hit a different API Gateway to the one that is being set as the baseQuery baseUrl currently. Do I need to create a new and separate RTK Query api using createApi() for the Websocket query?
Anyhow, here is the server code:-
// example CRA socket.io from https://github.com/socketio/socket.io/blob/main/examples/create-react-app-example/server.js
const getWebsocketServerMock = () => {
const io = require('socket.io')({
cors: {
origin: ['http://localhost:3000']
}
});
io.on('connection', (socket: any) => {
console.log(`connect: ${socket.id}`);
socket.on('hello!', () => {
console.log(`hello from ${socket.id}`);
});
socket.on('disconnect', () => {
console.log(`disconnect: ${socket.id}`);
});
});
io.listen(3001);
setInterval(() => {
io.emit('message', new Date().toISOString());
}, 1000);
console.log('Websocket server file initialised');
};
getWebsocketServerMock();
export {};
My RTK Query api file looks like this:-
reducerPath: 'someApi',
baseQuery: baseQueryWithReauth,
endpoints: (builder) => ({
getWebsocketResponse: builder.query<WebsocketResult, void>({
query: () => ``,
async onCacheEntryAdded(arg, { updateCachedData, cacheDataLoaded, cacheEntryRemoved }) {
try {
// wait for the initial query to resolve before proceeding
await cacheDataLoaded;
const socket = io('http://localhost:3001', {});
console.log(`socket.connected: ${socket.connected}`);
socket.on('connect', () => {
console.log('socket connected on rtk query');
});
socket.on('message', (message) => {
console.log(`received message: ${message}`);
// updateCachedData((draft) => {
// draft.push(message);
// });
});
await cacheEntryRemoved;
} catch {
// no-op in case `cacheEntryRemoved` resolves before `cacheDataLoaded`,
// in which case `cacheDataLoaded` will throw
}
}
}),
getSomeOtherQuery(.....),
getSomeOtherMutation(....),
Any advice or thoughts would be greatly appreciated! I guess my main question is should I be able to combine the websocket query in the same createApi function with other queries and mutations that need to use a different baseQuery url as they need to hit different API Gateways on AWS?
Much thanks,
Sam
You can circumvent the baseQuery from being used by specifying a queryFn instead of query on your endpoint.
In the most simple version, that just returns null as data so you can modify it later - but if you have an initial websocket request you can also do that in the queryFn.
queryFn: async () => { return { data: null } },

Client never receives messages from signalr hub SendAsync call from controller .NET Core in production

This works locally without any issues however when deployed to azure app service my client never receives any message when calling REST endpoint in controller.
My App Service has Web sockets enabled on the server via the configuration settings in azure.
Referencing docs for how to use the hubcontext outside of the hub: https://learn.microsoft.com/en-us/aspnet/core/signalr/hubcontext?view=aspnetcore-6.0
I have a successful opened a connection:
[2022-04-01T19:34:40.915Z] Information: WebSocket connected to wss://staging-env.com/hubs/notification?id=omz2rGgyuXw01QV6Vs6PbA.
I receive test broadcast we call from the client successfully:
Testing notification hub broadcasting 4/1/2022 7:34:40 PM
No error messages or information regarding a closed connection - everything pinging correctly in app logs.
I have a controller:
public class NotificationHubController : ControllerBase
{
private readonly IHubContext<NotificationHub> _hub;
private readonly ILogger<NotificationHubController> _logger;
public NotificationHubController(
ILogger<NotificationHubController> logger,
IHubContext<NotificationHub> hub
)
{
_logger = logger;
_hub = hub;
}
[HttpPost]
public async Task<IActionResult> SendNotification(NotificationDto dto)
{
_logger.LogInformation("Broadcasting notification update started");
await _hub.Clients.All.SendAsync("NotificationUpdate", dto); <-------- no errors reported
_logger.LogInformation("Broadcasting notification update resolved");
return Ok();
}
}
Hub:
using Microsoft.AspNetCore.SignalR;
namespace NMA.Api.Service.Hubs
{
public class NotificationHub: Hub
{
public Task NotifyConnection()
{
return Clients.All.SendAsync("TestBrodcasting", $"Testing notification hub
broadcasting {DateTime.Now.ToLocalTime()}");
}
}
}
Startup:
...
services.AddSignalR(hubOptions =>
{
hubOptions.EnableDetailedErrors = true;
});
...
app.UseEndpoints(endpoints =>
{
endpoints.MapHub<NotificationHub>("/hubs/notification");
endpoints.MapControllers();
});
Client:
(react js hook)
export const useNotificationHub = ({ onNotificationRecievedCallback }: NotificationHubProps) => {
const [connection, setConnection] = useState<HubConnection | null>(null);
useEffect(() => {
const establishConnection = async () => {
const newConnection = new HubConnectionBuilder()
.withUrl(`${process.env.REACT_APP_NMA_HUB_URL}/notification`)
.withAutomaticReconnect()
.build();
await newConnection.start();
newConnection.invoke("NotifyConnection").catch(function (err) {
console.error(err.toString());
});
newConnection.on("TestBrodcasting", function (time) {
console.log(time);
});
newConnection.on('NotificationUpdate', (response: NotificationAPIResponse) => {
console.log("Notification Recieved: ", response);
onNotificationRecievedCallback(response);
});
newConnection.onclose(() => {
console.log("Notification connection hub closed");
})
setConnection(newConnection);
}
establishConnection();
}, [onNotificationRecievedCallback]);
return {
}
}
I make the request via Insomnia to post a notification via the endpoint. I get a successful HTTP status return. My logs tell me that the that the call was made and it was successful. Yet not data received in the client.
I'm not sure what to do from here. I need to be able to invoke client calls from REST endpoints but this doesn't seem to work and I've exhausted my ability to use google to find alternatives.
I believe you are experiencing the issue because you are starting the connection and then setting up the on and onclose callbacks last. You should rather set up all of your callbacks first, and then starting the connection should be the last thing you do.
EG:
// BUILD THE CONNECTION FIRST
const newConnection = new HubConnectionBuilder()
.withUrl(`${process.env.REACT_APP_NMA_HUB_URL}/notification`)
.withAutomaticReconnect()
.build();
// SET UP CALLBACKS SECOND
newConnection.on("TestBrodcasting", function (time) {
console.log(time);
});
newConnection.on('NotificationUpdate', (response: NotificationAPIResponse) => {
console.log("Notification Recieved: ", response);
onNotificationRecievedCallback(response);
});
newConnection.onclose(() => {
console.log("Notification connection hub closed");
})
// START CONNECTION LAST
await newConnection.start();
// NOW YOU CAN INVOKE YOUR METHOD
newConnection.invoke("NotifyConnection").catch(function (err) {
console.error(err.toString());
});

Get the Message notification from Nodejs REST API using Angular8 in background

I am working on an application where I want to implement the message Inbox. I have created the message inbox using Angular8 and NodeJS REST API. Now I want to get the on inbox message in every 30 Second on the background when user login also it doesn't want to affecting the performance of the Angular app.
So I want to Implement the Web-worker with Angular8 to get the Data from NodeJS REST API but I am unable to create.
I have added following code in Angular 8 App
Add this code to app.component.ts
getWorker(token){
if (typeof Worker !== 'undefined') {
// Create a new
const worker = new Worker('../../web-worker/messenger.worker', { type: `module` });
worker.postMessage(token);
worker.onmessage = (e) => {
setTimeout(() => {
worker.postMessage(token)
}, 15000);
};
} else {
// Web Workers are not supported in this environment.
// You should add a fallback so that your program still executes correctly.
}
}
Created worker file with fetch
/// <reference lib="webworker" />
addEventListener('message', ({ data }) => {
const response = `worker response to ${data}`;
postMessage(response);
});
import { environment } from "src/environments/environment";
onmessage = (message:any) => {
fetch(environment.apiUrl +'messages/notification/1/1',
{ method:'GET',
headers:new Headers({
Authorization: `Bearer ${message.data}`
})
}
)
.then(response => {
return response.json()
})
.then(commits => {
// self.onmessage(message)
return commits
});
};
but it shows the type as fetch is should be show web worker Right?
Can anyone help me with this?

Swagger Client API key authentication

I have to authenticate to Bitmex API using my Api key but I got this error
Unable to connect: TypeError: Cannot read property 'add' of undefined
'use strict';
var SwaggerClient = require("swagger-client");
var _ = require('lodash');
var BitMEXAPIKeyAuthorization = require('./lib/BitMEXAPIKeyAuthorization');
require('dotenv').config();
new SwaggerClient({
// Switch this to `www.bitmex.com` when you're ready to try it out for real.
// Don't forget the `www`!
url: 'https://testnet.bitmex.com/api/explorer/swagger.json',
usePromise: true
})
.then(function(client) {
//console.log(client);
// Comment out if you're not requesting any user data.
client.clientAuthorizations.add("apiKey", new BitMEXAPIKeyAuthorization(process.env.BITMEX_API_KEY, process.env.BITMEX_API_SECRET));
// Print client capabilities
//
})
.catch(function(e) {
console.error("Unable to connect:", e);
})
Nodejs connector: https://github.com/BitMEX/api-connectors
You're using the latest version of the client, which doesn't do authorizations that way: https://github.com/swagger-api/swagger-js/blob/903569948d5a5c718d7b87d6832a672de4e76afc/docs/MIGRATION_2_X.md#authorizations
new SwaggerClient({
// Switch this to `www.bitmex.com` when you're ready to try it out for real.
// Don't forget the `www`!
url: 'https://testnet.bitmex.com/api/explorer/swagger.json',
usePromise: true,
authorizations: {
apiKey: new BitMEXAPIKeyAuthorization(process.env.BITMEX_API_KEY, process.env.BITMEX_API_SECRET)
}
})
.then(client => {
// Do whatever with client
})
.catch(function(e) {
console.error("Unable to connect:", e);
})

How to subscribe to stream in Angular2?

I'm having my streaming web-service running on localhost:8080/stream, which response when any new message added to subscribed mqtt stream. I want to consume this web-service in my Angular2 app. I'm using RxJS to consume NodeJS APIs in Angular2. I tried following code which calls localhost:8080/stream once and ends response. I want my observable to listen continuously to web-service.
var headers = new Headers();
headers.append('Access-Control-Allow-Origin', '*');
headers.append('Content-Type', 'application/json');
let options = new RequestOptions({ headers: headers }); // Create a request option
return this.http.get("http://localhost:8080/stream", options) // ...using post request
.map((res: Response) => res.json()) // ...and calling .json() on the response to return data
.catch((error: any) => Observable.throw(error.json().error));
If I understand your question right, you want to consume data from stream where new messages arrive at some period of time.
To achieve this You need add subscribe to the service.
return this.http.get("http://localhost:8080/stream", options) // ...using post request
.map((res: Response) => res.json()) // ...and calling .json() on the response to return data
.catch((error: any) => Observable.throw(error.json().error)
.subscribe(result => this.result =result));
Result will be updated as new data arrives, and you can use it the way want.
Note: It is best practice to make http calls separate in services and subscribe the service in your component.
For your reference I am adding an example I have worked on for demo purpose.
Create a service for http calls
#Injectable()
export class JsonPlaceHolderService{
constructor(private http:Http){}
getAllPosts():Observable<Post[]>{
return this.http.get("https://jsonplaceholder.typicode.com/posts")
.map(res=>res.json())
}
}
From your component call service and listen to changes continuously.
export class PostsComponent implements OnInit{
constructor(private _service:JsonPlaceHolderService){}
jphPosts:Post[];
title:string="JsonPlaceHolder's Post data";
ngOnInit():void{
this._service.getAllPosts().subscribe(
(data) =>this.jphPosts = data,
(err) => console.log(err),
()=>console.log("service call completed")
);
}
}
You should use websocket on angular and make it listen to your service URL after that you should listen to its events (open, close, message) and then create your own Subject stream using Rxjs to push the new data to the subscribers.
Please check the URL below:
https://medium.com/#lwojciechowski/websockets-with-angular2-and-rxjs-8b6c5be02fac
Streaming data from nodejs to angular with socket.io
This is something that would have been of great use when I was trying to do this. Following contains code from a socket.io package for angular credit goes to original author. This is taken from a working solution and may need some tweaking.
Server side
var app = express(),
http = require('http'),
ioServer = require('socket.io');
var httpServer = http.createServer(app);
var io = new ioServer();
httpServer.listen(1337, function(){
console.log('httpServer listening on port 1337');
});
io.attach(httpServer);
io.on('connection', function (socket){
console.log(Connected socket ' + socket.id);
});
//MQTT subscription
client.on('connect', function () {
client.subscribe(topic, function () {
console.log("subscribed to " + topic)
client.on('message', function (topic, msg, pkt) {
io.sockets.emit("message", {topic: topic, msg: msg, pkt: pkt});
});
});
});
Client Side
Create a customService in angular with following
import * as io from 'socket.io-client';
declare
private ioSocket: any;
private subscribersCounter = 0;
inside service class
constructor() {
this.ioSocket = io('socketUrl', {});
}
on(eventName: string, callback: Function) {
this.ioSocket.on(eventName, callback);
}
removeListener(eventName: string, callback?: Function) {
return this.ioSocket.removeListener.apply(this.ioSocket, arguments);
}
fromEvent<T>(eventName: string): Observable<T> {
this.subscribersCounter++;
return Observable.create((observer: any) => {
this.ioSocket.on(eventName, (data: T) => {
observer.next(data);
});
return () => {
if (this.subscribersCounter === 1) {
this.ioSocket.removeListener(eventName);
}
};
}).share();
}
In your component, Import customService as service
service.on("message", dat => {
console.log(dat);
});

Resources