0

I am attempting to utilize protobuf.js and provide it a transport layer (rpcimpl) since it is transport agnostic.

I can successfully convert all the proto files and to a direct grpc Client and Sever via protobuf (loadSync, lookup) to grpc's (loadObject). This allows me to get a concrete grpc implementation of server and client up with tests. The next step was to test a protobuf client (instable) to grpc server stable. This is more out of curiosity to see if we can be independent of grpc's library itself and just use protobuf.js .

My tests which are failing is always sending a null buffer over to the grpc server. Resulting in an "Illegal Buffer Error".

Client.js

const http2 = require('http2');

const {
  // HTTP2_HEADER_AUTHORITY,
  HTTP2_HEADER_CONTENT_TYPE,
  HTTP2_HEADER_CONTENT_LENGTH,
  HTTP2_HEADER_METHOD,
  HTTP2_HEADER_PATH,
  // HTTP2_HEADER_SCHEME,
  HTTP2_HEADER_TE
  // HTTP2_HEADER_USER_AGENT
} = http2.constants;

const MIN_CONNECT_TIMEOUT_MS = 20000;

const { Duplex } = require('stream');

function bufferToStream(buffer) {
  const stream = new Duplex();
  stream.push(buffer);
  stream.push(null);
  return stream;
} 

// taken from googles grpc-js channel
// https://github.com/grpc/grpc-node/blob/master/packages/grpc-js-core/src/channel.ts#L181-L190
function clientTimeout({ client, deadline }, cb) {
  const now = new Date();

  const connectionTimeout = Math.max(
    deadline.getTime() - now.getTime(),
    MIN_CONNECT_TIMEOUT_MS
  );

  const id = setTimeout(() => {
    cb(new Error('connection timed out!'));
    client.close();
  }, connectionTimeout);
  return id;
}

function makeClient(connString, cb) {
  const deadline = new Date();
  const client = http2.connect(connString, {});
  client.on('socketError', cb);
  client.on('error', cb);

  const connectionTimerId = clientTimeout({ deadline, client }, cb);

  client.on('connect', () => {
    clearTimeout(connectionTimerId);
    cb(null, client);
  });
}

function fullNameToPath(fullName) {
  return fullName.replace(/(\.)(.*)(\.)/, /$2/);
}

module.exports = class Client {
  constructor(connString) {
    // this.options = url.parse(connString); needed for http2.js
    this.connString = connString;
    this.rpcImpl = this.rpcImpl.bind(this);
  }


  rpcImpl(pbMethod, payload, cb) {
    makeClient(this.connString, (err, client) => {
      try {
        const path = fullNameToPath(pbMethod.fullName);
        const req = client.request({
          [HTTP2_HEADER_METHOD]: 'POST',
          [HTTP2_HEADER_PATH]: path,
          [HTTP2_HEADER_CONTENT_TYPE]: 'application/grpc',
          // [HTTP2_HEADER_CONTENT_LENGTH]: payload.length,
          [HTTP2_HEADER_TE]: 'trailers'
        });

        // req.once('response', (headers) => {
          // const grpcStatus = parseInt(headers['grpc-status'], 10);
          // 0 IS OK
          // if (grpcStatus) {
            // cb(new Error(headers['grpc-message']));
          // }
        // });

        const data = [];
        // req.once('error', cb);
        req.on('data', (chunk) => {
          data.push(chunk);
        });
        req.once('end', () => {
          if (data.length) {
            cb(null, Buffer.from(data.join()));
          }
          client.destroy();
        });
        // bufferToStream(payload).pipe(req);
        req.write(payload, null, cb);
        req.end();
      } catch (e) {
        cb(e);
      }
    });
  }
};

getProtoPath.js

module.exports = (_paths = [__dirname, 'src']) => (...args) => {
  let paths = _paths;
  if (!Array.isArray(paths)) {
    paths = [paths];
  }
  return path.join.apply(null, paths.concat(args));
};

helloworld.proto

// Copyright 2015 gRPC authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

syntax = "proto3";

option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";
option objc_class_prefix = "HLW";

package helloworld;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

helloworld.spec.js

const sinon = require('sinon');
const { expect } = require('chai');
const { Server, ServerCredentials } = require('grpc');
const Client = require('../../../src/protobufjs/transports/http2/Client');

const getProtoPath = require('./getProtoPath');
const { loadSync } = require('google-proto-files');

const protPath = getProtoPath(__dirname)('../../protos/helloworld.proto');
const URI = '127.0.0.1:50061';

const client = new Client(`http://${URI}`);

const makeServiceImpl = () => ({
  sayHello: sinon.stub().yields(null, {
    message: 'Hello James'
  })
});

// const debug = require('../../debug').spawn('test:protobufjs');
describe('Client protobufjs (rpcimpl)', () => {
  let protobufSvc, api, server, grpcServerSvcImpl;

  // using GRPC server as a baseline of a real grpc server
  function initServer() {
    server = new Server();

    server.bind(URI, ServerCredentials.createInsecure());
    server.addService(
      api.toGrpc().Greeter.service,
      (grpcServerSvcImpl = makeServiceImpl())
    );
    server.start();
  }

  describe('client', () => {
    beforeEach(() => {
      api = loadSync(protPath).lookup('helloworld');
      protobufSvc = api.Greeter.create(client.rpcImpl);

      initServer();
    });

    afterEach(() => {
      if (server) server.forceShutdown();
      // eslint-disable-next-line no-multi-assign
      protobufSvc = undefined;
      api = undefined;
      server = undefined;
      grpcServerSvcImpl = undefined;
    });

    it('created', () => {
      expect(protobufSvc).to.be.ok;
    });

    describe('SayHello', () => {
      it('callback', (done) => {
        // eslint-disable-next-line
        protobufSvc.sayHello({ name: 'Bond' }, (err, resp) => {
          if (err) {
            return done(err); // YAY ILLEGAL BUFFER, HELP!!
          }
          expect(grpcServerSvcImpl.sayHello.called).to.be.ok;
          expect(resp.message).to.equal('Hello James');
          done();
        });
      });
    });
  });
});
Nick
  • 1,174
  • 11
  • 20
  • Have you tried console.log dumping payload before the write() to see what you're actually sending? I use util.inspect() a lot for stuff like this. It sounds like https://github.com/dcodeIO/protobuf.js/issues/972. – John Smith Mar 31 '18 at 04:53
  • Yeah, I just walk through the debugger and it's an Empty buffer, not null or undefined. This is correct but I'll double check for differences. – Nick Mar 31 '18 at 12:35
  • Also I am seeing this in both node 8.X and 9.X. – Nick Mar 31 '18 at 12:38
  • I honestly don't know exactly what you are doing here (my lack of knowledge) but, I kinda hacked your files and pointed everything to the local dir and ran node helloworld.spec.js and if I put a console.log right under rpcImpl(pbMethod, payload, cb) it never runs. But if I put it last under module.exports I get a log. It's like rpcImpl never happens. Again. I'm not sure I really get the modules your using. It does run without returning any errors. I only loaded describe and required it. Other then that the code is yours. what is const { getProtoPath } = require('../../../') pointing to? – John Smith Mar 31 '18 at 22:20
  • getProtoPath basically does the same thing as this , https://github.com/googleapis/nodejs-proto-files/blob/master/index.js#L6-L9 – Nick Mar 31 '18 at 22:24
  • Added getProtoPath – Nick Mar 31 '18 at 22:28
  • 1
    Your client isn't actually implementing the gRPC protocol, which is documented [here](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md). If you're just trying to do a simple protobuf over HTTP/2 protocol, it's not going to be compatible. And if you are actually trying to implement gRPC, you have to add the length-delimited message framing. You would probably also want to do the deframing on the receiving side and handle response trailers to get the status code and details. – murgatroid99 Apr 02 '18 at 16:43
  • That's what I figured and I was trying to reverse engineer some of the details on the grpc-node-native code, but it gets tougher with the C code ;) .Thanks for the link. – Nick Apr 02 '18 at 18:01

0 Answers0