How to handle message sent from server to client with RSocket? - spring-webflux

I try to use RSocketRequester to send a message from the server to the specific client, but I don't know how to handle it on the frontend. The server is Spring Webflux with the controller like this:
data class Message(val message: String)
#Controller
class RSocketController {
private val log = LoggerFactory.getLogger(RSocketController::class.java)
#MessageMapping("say.hello")
fun sayHello(message: String): Flux<Message> {
log.info("say hello {}", message)
return Flux.just(Message("server says hello"))
}
#MessageMapping("say.hi")
fun sayHi(message: String, rSocketRequester: RSocketRequester): Flux<Message> {
log.info("say hi {}", message)
rSocketRequester
.route("say.hello")
.data(Message("server says hi hello ;)"))
.send()
.subscribe()
return Flux.just(Message("server says hi!!"))
}
}
On the frontend I use rsocket-js. The sayHello method works just fine (request-stream), but when I call the sayHi method I want to send two messages from the server. The first one to say.hello endpoint, and the second to say.hi endpoint. I've got rsocket-js implementation like this:
sayHello() {
console.log("say hello");
this.requestStream("say.hello");
},
sayHi() {
console.log("say hi");
this.requestStream("say.hi");
},
connect() {
const transport = new RSocketWebSocketClient({
url: "ws://localhost:8080/rsocket"
});
const client = new RSocketClient({
serializers: {
data: JsonSerializer,
metadata: IdentitySerializer
},
setup: {
keepAlive: 60000,
lifetime: 180000,
dataMimeType: "application/json",
metadataMimeType: "message/x.rsocket.routing.v0"
},
transport
});
client.connect().subscribe({
onComplete: socket => {
this.socket = socket;
console.log("complete connection");
},
onError: error => {
console.log("got connection error");
console.error(error);
},
onSubscribe: cancel => {
console.log("subscribe connection");
console.log(cancel);
}
});
},
requestStream(url) {
if (this.socket) {
this.socket
.requestStream({
data: url + " from client",
metadata: String.fromCharCode(url.length) + url
})
.subscribe({
onComplete: () => console.log("requestStream done"),
onError: error => {
console.log("got error with requestStream");
console.error(error);
},
onNext: value => {
// console.log("got next value in requestStream..");
console.log("got data from sever");
console.log(value.data);
},
// Nothing happens until `request(n)` is called
onSubscribe: sub => {
console.log("subscribe request Stream!");
sub.request(2147483647);
// sub.request(3);
}
});
} else {
console.log("not connected...");
}
}
I can see both messages in Google Chrome DevTools -> Network -> rsocket. So the client receives them but I can't catch in the code the one sent by RSocketRequester.
It seems that the server uses fireAndForget method. How to handle it on the client side?

As #VladMamaev said, we can provide a responder to the client like in this example https://github.com/rsocket/rsocket-js/blob/master/packages/rsocket-examples/src/LeaseClientExample.js#L104
For me, fireAndForget method is enough.
export class EchoResponder {
constructor(callback) {
this.callback = callback;
}
fireAndForget(payload) {
this.callback(payload);
}
}
import { EchoResponder } from "~/assets/EchoResponder";
...
const messageReceiver = payload => {
//do what you want to do with received message
console.log(payload)
};
const responder = new EchoResponder(messageReceiver);
connect() {
const transport = new RSocketWebSocketClient({
url: "ws://localhost:8080/rsocket"
});
const client = new RSocketClient({
serializers: {
data: JsonSerializer,
metadata: IdentitySerializer
},
setup: {
keepAlive: 60000,
lifetime: 180000,
dataMimeType: "application/json",
metadataMimeType: "message/x.rsocket.routing.v0"
},
responder: responder,
transport
});

Related

Change rabbitmq exchange with nestjs

I am using rabbitmq with nestjs. I need to replicate a message from one queue to another. I set up an exchange on rabbitmq to make it work. But how can I change the exchange of rabbitmq inside nestjs?
my api gateway
my current rabbitmq configuration inside nestjs:
constructor( ) {
this.rabbitmq = ClientProxyFactory.create({
transport: Transport.RMQ,
options: {
urls: [`amqp://${this.configService.get<string>('RABBITMQ_USER')}:${this.configService.get<string>('RABBITMQ_PASSWORD')}#${this.configService.get<string>('RABBITMQ_URL')}`],
queue: 'students'
}
})
}
createStudent(#Body() body: CreateStudentDto): Observable<any> {
return this.rabbitmq.send('createStudent', body)
}
my client
#MessagePattern('createStudent')
async createStudent(#Payload() student: Student, #Ctx() context: RmqContext) {
const channel = context.getChannelRef()
const originalMsg = context.getMessage()
try {
let response = await this.studentService.createStudent(student)
await channel.ack(originalMsg)
return response;
} catch(error) {
this.logger.log(`error: ${JSON.stringify(error.message)}`)
const filterAckError = ackErrors.filter(ackError => error.message.includes(ackError))
if (filterAckError.length > 0) {
await channel.ack(originalMsg)
}
}
}
I need the message to be sent to two queues.

Apollo Server & 4xx status codes

Currently, my Apollo Server(running on HapiJS) returns HTTP 200 for every request, including failed ones.
I would like the GraphQL server to return HTTP 4xx for unsuccessful requests. The primary reason for it is that I want to set up monitoring for my ELB.
I know that Apollo Server has an engine platform, but I want to implement it using my current infrastructure.
Any ideas of how I could accomplish that? I tried to capture 'onPreResponse' event for my HapiJS server but I couldn't modify status code there.
After reading this answer. Here is a solution by modifying the hapijs plugin graphqlHapi of hapiApollo.ts file.
server.ts:
import { makeExecutableSchema } from 'apollo-server';
import { ApolloServer, gql } from 'apollo-server-hapi';
import Hapi from 'hapi';
import { graphqlHapi } from './hapiApollo';
const typeDefs = gql`
type Query {
_: String
}
`;
const resolvers = {
Query: {
_: () => {
throw new Error('some error');
},
},
};
const schema = makeExecutableSchema({ typeDefs, resolvers });
const port = 3000;
async function StartServer() {
const app = new Hapi.Server({ port });
graphqlHapi.register(app, { path: '/graphql', graphqlOptions: { schema } });
app.ext('onPreResponse', (request: any, h: any) => {
const response = request.response;
if (!response.isBoom) {
return h.continue;
}
return h.response({ message: response.message }).code(400);
});
await app.start();
}
StartServer()
.then(() => {
console.log(`apollo server is listening on http://localhost:${port}/graphql`);
})
.catch((error) => console.log(error));
hapiApollo.ts:
import Boom from 'boom';
import { Server, Request, RouteOptions } from 'hapi';
import { GraphQLOptions, runHttpQuery, convertNodeHttpToRequest } from 'apollo-server-core';
import { ValueOrPromise } from 'apollo-server-types';
export interface IRegister {
(server: Server, options: any, next?: Function): void;
}
export interface IPlugin {
name: string;
version?: string;
register: IRegister;
}
export interface HapiOptionsFunction {
(request?: Request): ValueOrPromise<GraphQLOptions>;
}
export interface HapiPluginOptions {
path: string;
vhost?: string;
route?: RouteOptions;
graphqlOptions: GraphQLOptions | HapiOptionsFunction;
}
const graphqlHapi: IPlugin = {
name: 'graphql',
register: (server: Server, options: HapiPluginOptions, next?: Function) => {
if (!options || !options.graphqlOptions) {
throw new Error('Apollo Server requires options.');
}
server.route({
method: ['GET', 'POST'],
path: options.path || '/graphql',
vhost: options.vhost || undefined,
options: options.route || {},
handler: async (request, h) => {
try {
const { graphqlResponse, responseInit } = await runHttpQuery([request, h], {
method: request.method.toUpperCase(),
options: options.graphqlOptions,
query:
request.method === 'post'
? // TODO type payload as string or Record
(request.payload as any)
: request.query,
request: convertNodeHttpToRequest(request.raw.req),
});
// add our custom error handle logic
const graphqlResponseObj = JSON.parse(graphqlResponse);
if (graphqlResponseObj.errors && graphqlResponseObj.errors.length) {
throw new Error(graphqlResponseObj.errors[0].message);
}
const response = h.response(graphqlResponse);
Object.keys(responseInit.headers as any).forEach((key) =>
response.header(key, (responseInit.headers as any)[key]),
);
return response;
} catch (error) {
// handle our custom error
if (!error.name) {
throw Boom.badRequest(error.message);
}
if ('HttpQueryError' !== error.name) {
throw Boom.boomify(error);
}
if (true === error.isGraphQLError) {
const response = h.response(error.message);
response.code(error.statusCode);
response.type('application/json');
return response;
}
const err = new Boom(error.message, { statusCode: error.statusCode });
if (error.headers) {
Object.keys(error.headers).forEach((header) => {
err.output.headers[header] = error.headers[header];
});
}
// Boom hides the error when status code is 500
err.output.payload.message = error.message;
throw err;
}
},
});
if (next) {
next();
}
},
};
export { graphqlHapi };
Now, when the GraphQL resolver throws an error, the client-side will receive our custom response with Http status code 400 instead of 200 status code with GraphQL errors response.
General from the browser:
Request URL: http://localhost:3000/graphql
Request Method: POST
Status Code: 400 Bad Request
Remote Address: 127.0.0.1:3000
Referrer Policy: no-referrer-when-downgrade
The response body is: {"message":"some error"}

Ionic 2 clearing App cache on logout

My login function consists of a http request (an irrelevant one just for the check) with the entered credentials. This way I can resolve the request or get a rejection which I handle by not pushing the next component with the NavController onto the stack.
On logout, the credentials, which are saved in the Ionic Storage, are deleted. Now starts the problem: Any credentials that are saved now in the storage seems not to be used by the login request as the request doesn't throw an Auth exception. Only after clearing the Browser cache it works again.
This all happened by serving the app in a web browser and on the phone.
How is it possible to clear the App cache (not only the View/Component cache) with Ionic 2 to prevent this behaviour? There is no documentation or question concerning this problem existent at the moment.
Auth Service:
#Injectable()
export class AuthService {
HAS_LOGGED_IN = 'hasLoggedIn';
constructor(private storage: Storage) {
// this.rest = rest;
console.log('auth');
}
setCredentials(credentials) {
this.storage.set('username', credentials.username);
this.storage.set('password', credentials.password);
}
logout(): void {
this.storage.remove('username');
this.storage.remove('password');
this.storage.remove(this.HAS_LOGGED_IN);
}
hasLoggedIn() {
return this.storage.get(this.HAS_LOGGED_IN).then( value => {
console.log('hasLoggedIN value: ' + value);
return value === true;
});
}
}
Login Component:
#Component({
selector: 'page-login',
templateUrl: 'login.html',
})
export class LoginPage {
model: any;
HAS_LOGGED_IN: string = 'hasLoggedIn';
constructor(private navCtrl: NavController,
private viewCtrl: ViewController,
private auth: AuthService,
private toastCtrl: ToastController,
private rest: RestService,
private storage: Storage) {
}
ionViewDidLoad() {
this.model = {};
}
ionViewWillEnter() {
this.viewCtrl.showBackButton(false);
this.displayTab(false);
}
login() {
console.log(this.model);
console.log('login() claled');
this.displayTab(true);
this.auth.setCredentials(this.model);
this.rest.getEntryPoint().then(data => {
console.log(data);
this.storage.set(this.HAS_LOGGED_IN, true);
this.navCtrl.push(OverviewPage);
}).catch(err => {
this.storage.set(this.HAS_LOGGED_IN, false);
console.log('Error:');
console.log(err);
this.navCtrl.push(LoginPage).then(response => {
console.log(response);
console.log(this.navCtrl);
console.log('pushed login 1');
});
});
}
validate(items: boolean) {
if (items) {
let toast = this.toastCtrl.create({
message: 'Passwort und Benutzername sind zwingend',
duration: 3000,
position: 'bottom',
});
toast.onDidDismiss(() => {
console.log('Dismissed toast');
});
toast.present();
}
}
private displayTab(display: boolean) {
let elements = document.querySelectorAll('.tabbar');
if (elements != null) {
Object.keys(elements).map((key) => {
elements[key].style.transform = display ? 'translateY(0)' : 'translateY(70px)';
});
}
}
}
Intercepting Http methods (this service is used in another service that makes the actual Rest calls):
#Injectable()
export class HttpInterceptorService {
constructor(#Inject(Http) private http: Http, private storage: Storage) {
this.http = http;
console.log('interceptor');
}
get(url) {
return new Promise((resolve, reject) => {
let headers = new Headers();
this.createAuthorizationHeader(headers).then(() => {
return this.http.get(url, {
headers: headers,
}).subscribe(data => {
resolve(data.json());
}, err => {
reject(err);
});
});
});
}
put(url: string, attributes?) {
return new Promise((resolve, reject) => {
let headers = new Headers();
this.createAuthorizationHeader(headers).then(() => {
return this.http.put(url, (attributes) ? attributes : {}, {
headers: headers,
}).subscribe(data => {
resolve(data.json());
}, err => {
reject(err);
});
});
});
}
post(url: string, data) {
return new Promise((resolve, reject) => {
let headers = new Headers();
this.createAuthorizationHeader(headers).then(() => {
return this.http.post(url, data, {
headers: headers,
}).subscribe(output => {
resolve(output.json());
}, err => {
reject(err);
});
});
});
}
private createAuthorizationHeader(headers: Headers): Promise<void> {
console.log('creating auth header');
return new Promise(resolve => {
this.storage.get('username')
.then( username => {
this.storage.get('password')
.then( password => {
headers.append('Authorization', 'Basic ' +
btoa(username + ':' + password));
resolve();
});
});
});
}
}
The other components are using hasLoggedIn() function to check if we are logged in or not when switching back to the App. If not logged in anymore (storage cleaned in any way) we get back to the LoginPage component.
The problem was a session header in the response that I somehow didn't see beforehand.
My solution is a simple Cookies clearing:
window.cookies.clear(function() {});

No error shown in console when thrown from inside hapi plugin

For some reason no error shows up in the server console when I start my hapi server with nodemon and navigate to http://localhost:3000/hapi-ext-fetch and this makes debugging very difficult. Here is my code:
var Hapi = require('hapi');
var Joi = require('joi');
var fetch = require('isomorphic-fetch');
var debugMode = { debug: { request: [ 'error', 'request-internal' ] }};
var server = new Hapi.Server(debugMode);
server.connection({ port: 3000 });
var myPlugin = {
register: function (server, options, next) {
server.route([
{
method: 'GET',
path: '/{name}',
handler: function ( request, reply ) {
throw new Error('this error isnt shown!');
},
config: {
validate: {
params: {
name: Joi.string().min(3).max(10)
}
}
}
}
]);
next();
}
};
myPlugin.register.attributes = {
name: 'myPlugin',
version: '1.0.0'
};
server.register([
{
register: myPlugin,
routes: {
prefix: '/test'
}
}
], function() {
server.ext( 'onPreResponse', ( request, reply ) => {
if ( typeof request.response.statusCode !== 'undefined' ) {
return reply.continue();
}
fetch('http://localhost:3000/test/whatever')
.then(function(result) {
reply(result);
})
.catch(function(err) {
reply('error on server side: ' + err.stack);
});
});
server.start((err) => {
if (err) {
throw err;
}
console.log('Server running at:', server.info.uri);
});
});
I'm using hapi 13.0.0
Can't say I totally understand your use case here and if this question will be helpful to other people. But what you're trying to do it seems is:
Send a request to /hapi-fetch-ext
Have that request 404
And then in an onPreResponse go fetch another route /test/whatever
Hope to see the "this error isn't shown error"
Not sure if you're aware but this is going to cause an infinite cycle of requests (your fetch will cause another onPreResponse and so on and so on). So you should probably only go fetch on a 404:
server.ext( 'onPreResponse', ( request, reply ) => {
if (request.response.isBoom && request.response.output.statusCode === 404) {
return fetch('http://localhost:3000/test/whatever')
.then(function(result) {
reply(result);
})
.catch(function(err) {
reply('error on server side: ' + err.stack);
});
}
return reply.continue();
});

Invalidate session with custom authenticator

Using ember-cli 0.1.2 and ember-cli-simple-auth 0.7.0, I need to invalidate the session both on client and server. As explained here I need to do something similar to the authenticate method making an ajax request to the server and ensuring its success before emptying the session:
import Ember from 'ember';
import Base from "simple-auth/authenticators/base";
var CustomAuthenticator = Base.extend({
tokenEndpoint: 'http://127.0.0.1:3000/api/v1/auth/login',
restore: function(data) {
},
authenticate: function(credentials) {
var _this = this;
return new Ember.RSVP.Promise(function(resolve, reject) {
Ember.$.ajax({
url: _this.tokenEndpoint,
type: 'POST',
data: JSON.stringify({ email: credentials.identification, password: credentials.password }),
contentType: 'application/json'
}).then(function(response) {
Ember.run(function() {
resolve({ token: response.token });
});
}, function(xhr, status, error) {
var response = JSON.parse(xhr.responseText);
Ember.run(function() {
reject(response.error);
});
});
});
},
invalidate: function() {
var _this = this;
return new Ember.RSVP.Promise(function(resolve, reject) {
Ember.$.ajax({
url: _this.tokenEndpoint,
type: 'DELETE'
}).then(function(response) {
resolve();
}, function(xhr, status, error) {
var response = JSON.parse(xhr.responseText);
Ember.run(function() {
reject(response.error);
});
});
});
}
// invalidate: function() {
// var _this = this;
// return new Ember.RSVP.Promise(function(resolve) {
// Ember.$.ajax({ url: _this.tokenEndpoint, type: 'DELETE' }).always(function() {
// resolve();
// });
// });
// }
});
export default {
name : 'authentication',
before : 'simple-auth',
initialize : function(container) {
container.register('authenticator:custom', CustomAuthenticator);
}
};
My logout API endpoint need the token (in the headers). How do I pass it? I read this but my authorizer seems ignoring it and I got a 401:
import Ember from 'ember';
import Base from 'simple-auth/authorizers/base';
var CustomAuthorizer = Base.extend({
authorize: function(jqXHR, requestOptions){
Ember.debug("AUTHORIZING!");
}
});
export default {
name : 'authorization',
before : 'simple-auth',
initialize : function(container) {
container.register('authorizer:custom', CustomAuthorizer);
}
};
My environment.js:
/* jshint node: true */
module.exports = function(environment) {
var ENV = {
modulePrefix: 'wishhhh',
environment: environment,
baseURL: '/',
locationType: 'auto',
EmberENV: {
FEATURES: {
// Here you can enable experimental features on an ember canary build
// e.g. 'with-controller': true
}
},
APP: {
// Here you can pass flags/options to your application instance
// when it is created
}
};
// TODO: disabled because of https://github.com/stefanpenner/ember-cli/issues/2174
ENV.contentSecurityPolicyHeader = 'Disabled-Content-Security-Policy'
ENV['simple-auth'] = {
authorizer: 'authorizer:custom',
// crossOriginWhitelist: ['http://localhost:3000']
crossOriginWhitelist: ['*']
}
if (environment === 'development') {
// ENV.APP.LOG_RESOLVER = true;
ENV.APP.LOG_ACTIVE_GENERATION = true;
// ENV.APP.LOG_TRANSITIONS = true;
// ENV.APP.LOG_TRANSITIONS_INTERNAL = true;
ENV.APP.LOG_VIEW_LOOKUPS = true;
}
if (environment === 'test') {
// Testem prefers this...
ENV.baseURL = '/';
ENV.locationType = 'auto';
// keep test console output quieter
ENV.APP.LOG_ACTIVE_GENERATION = false;
ENV.APP.LOG_VIEW_LOOKUPS = false;
ENV.APP.rootElement = '#ember-testing';
}
if (environment === 'production') {
}
return ENV;
};
The following is the Ember inspector output when, eventually, I try to logout:
Did you actually configure Ember Simple Auth to use your custom authorizer? In that case it should authorize the session invalidation request automatically.
Alternatively you could add the token in the authenticator's invalidate method which gets passed the session's contents.
Thanks to marcoow, I found out that it was actually a problem with every request not only the logout one. My authorizer never got called. Problem was environment setup of crossOriginWhitelist which, in order to work with my dev API, I had to set to ['http://127.0.0.1:3000']. Neither ['http://localhost:3000'] nor [*] worked.