feat(tonic-web): implement grpc <-> grpc-web protocol translation (#455)
tonic-web enables tonic servers to handle requests from grpc-web clients directly, without the need of an external proxy. Co-authored-by: John Hernandez <[email protected]> Co-authored-by: zancas <[email protected]>
This commit is contained in:
co-authored by
John Hernandez
zancas
parent
352b0f584b
commit
c309063254
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
*
|
||||
* Copyright 2018 Google LLC
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.
|
||||
*
|
||||
*/
|
||||
|
||||
// Adapted from https://github.com/grpc/grpc-web/tree/master/test/interop
|
||||
|
||||
global.XMLHttpRequest = require("xhr2");
|
||||
|
||||
const parseArgs = require('minimist');
|
||||
const argv = parseArgs(process.argv, {
|
||||
string: ['mode', 'host']
|
||||
});
|
||||
|
||||
const SERVER_HOST = `http://${argv.host || "localhost"}:9999`;
|
||||
|
||||
if (argv.mode === 'binary') {
|
||||
console.log('Testing tonic-web mode (binary)...');
|
||||
} else {
|
||||
console.log('Testing tonic-web mode (text)...');
|
||||
}
|
||||
console.log('Tonic server:', SERVER_HOST);
|
||||
|
||||
const PROTO_PATH = argv.mode === 'binary' ? './binary' : './text';
|
||||
|
||||
const {
|
||||
Empty,
|
||||
SimpleRequest,
|
||||
StreamingOutputCallRequest,
|
||||
EchoStatus,
|
||||
Payload,
|
||||
ResponseParameters
|
||||
} = require(`${PROTO_PATH}/test_pb.js`);
|
||||
|
||||
const {TestServiceClient} = require(`${PROTO_PATH}/test_grpc_web_pb.js`);
|
||||
|
||||
const assert = require('assert');
|
||||
const grpc = {};
|
||||
grpc.web = require('grpc-web');
|
||||
|
||||
function multiDone(done, count) {
|
||||
return function () {
|
||||
count -= 1;
|
||||
if (count <= 0) {
|
||||
done();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function doEmptyUnary(done) {
|
||||
const testService = new TestServiceClient(SERVER_HOST, null, null);
|
||||
testService.emptyCall(new Empty(), null, (err, response) => {
|
||||
assert.ifError(err);
|
||||
assert(response instanceof Empty);
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
function doLargeUnary(done) {
|
||||
const testService = new TestServiceClient(SERVER_HOST, null, null);
|
||||
const req = new SimpleRequest();
|
||||
const size = 314159;
|
||||
|
||||
const payload = new Payload();
|
||||
payload.setBody('0'.repeat(271828));
|
||||
|
||||
req.setPayload(payload);
|
||||
req.setResponseSize(size);
|
||||
|
||||
testService.unaryCall(req, null, (err, response) => {
|
||||
assert.ifError(err);
|
||||
assert.equal(response.getPayload().getBody().length, size);
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
function doServerStreaming(done) {
|
||||
const testService = new TestServiceClient(SERVER_HOST, null, null);
|
||||
const sizes = [31415, 9, 2653, 58979];
|
||||
|
||||
const responseParams = sizes.map((size, idx) => {
|
||||
const param = new ResponseParameters();
|
||||
param.setSize(size);
|
||||
param.setIntervalUs(idx * 10);
|
||||
return param;
|
||||
});
|
||||
|
||||
const req = new StreamingOutputCallRequest();
|
||||
req.setResponseParametersList(responseParams);
|
||||
|
||||
const stream = testService.streamingOutputCall(req);
|
||||
|
||||
done = multiDone(done, sizes.length);
|
||||
let numCallbacks = 0;
|
||||
stream.on('data', (response) => {
|
||||
assert.equal(response.getPayload().getBody().length, sizes[numCallbacks]);
|
||||
numCallbacks++;
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
function doCustomMetadata(done) {
|
||||
const testService = new TestServiceClient(SERVER_HOST, null, null);
|
||||
done = multiDone(done, 3);
|
||||
|
||||
const req = new SimpleRequest();
|
||||
const size = 314159;
|
||||
const ECHO_INITIAL_KEY = 'x-grpc-test-echo-initial';
|
||||
const ECHO_INITIAL_VALUE = 'test_initial_metadata_value';
|
||||
const ECHO_TRAILING_KEY = 'x-grpc-test-echo-trailing-bin';
|
||||
const ECHO_TRAILING_VALUE = 0xababab;
|
||||
|
||||
const payload = new Payload();
|
||||
payload.setBody('0'.repeat(271828));
|
||||
|
||||
req.setPayload(payload);
|
||||
req.setResponseSize(size);
|
||||
|
||||
const call = testService.unaryCall(req, {
|
||||
[ECHO_INITIAL_KEY]: ECHO_INITIAL_VALUE,
|
||||
[ECHO_TRAILING_KEY]: ECHO_TRAILING_VALUE
|
||||
}, (err, response) => {
|
||||
assert.ifError(err);
|
||||
assert.equal(response.getPayload().getBody().length, size);
|
||||
done();
|
||||
});
|
||||
|
||||
call.on('metadata', (metadata) => {
|
||||
assert(ECHO_INITIAL_KEY in metadata);
|
||||
assert.equal(metadata[ECHO_INITIAL_KEY], ECHO_INITIAL_VALUE);
|
||||
done();
|
||||
});
|
||||
|
||||
call.on('status', (status) => {
|
||||
assert('metadata' in status);
|
||||
assert(ECHO_TRAILING_KEY in status.metadata);
|
||||
assert.equal(status.metadata[ECHO_TRAILING_KEY], ECHO_TRAILING_VALUE);
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
function doStatusCodeAndMessage(done) {
|
||||
const testService = new TestServiceClient(SERVER_HOST, null, null);
|
||||
const req = new SimpleRequest();
|
||||
|
||||
const TEST_STATUS_MESSAGE = 'test status message';
|
||||
const echoStatus = new EchoStatus();
|
||||
echoStatus.setCode(2);
|
||||
echoStatus.setMessage(TEST_STATUS_MESSAGE);
|
||||
|
||||
req.setResponseStatus(echoStatus);
|
||||
|
||||
testService.unaryCall(req, {}, (err, response) => {
|
||||
assert(err);
|
||||
assert('code' in err);
|
||||
assert('message' in err);
|
||||
assert.equal(err.code, 2);
|
||||
assert.equal(err.message, TEST_STATUS_MESSAGE);
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
function doUnimplementedMethod(done) {
|
||||
const testService = new TestServiceClient(SERVER_HOST, null, null);
|
||||
testService.unimplementedCall(new Empty(), {}, (err, response) => {
|
||||
assert(err);
|
||||
assert('code' in err);
|
||||
assert.equal(err.code, 12);
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
const testCases = {
|
||||
'empty_unary': {testFunc: doEmptyUnary},
|
||||
'large_unary': {testFunc: doLargeUnary},
|
||||
'server_streaming': {
|
||||
testFunc: doServerStreaming,
|
||||
skipBinaryMode: true
|
||||
},
|
||||
'custom_metadata': {testFunc: doCustomMetadata},
|
||||
'status_code_and_message': {testFunc: doStatusCodeAndMessage},
|
||||
'unimplemented_method': {testFunc: doUnimplementedMethod}
|
||||
};
|
||||
|
||||
|
||||
describe('tonic-web interop tests', function () {
|
||||
Object.keys(testCases).forEach((testCase) => {
|
||||
if (argv.mode === 'binary' && testCases[testCase].skipBinaryMode) return;
|
||||
it('should pass ' + testCase, testCases[testCase].testFunc);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "grpc-web-interop-test",
|
||||
"version": "0.1.0",
|
||||
"description": "gRPC-Web Interop Test Client",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "mocha -b --timeout 500 ./interop_client.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"google-protobuf": "~3.14.0",
|
||||
"grpc-web": "~1.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"minimist": "~1.2.5",
|
||||
"mocha": "~7.1.1",
|
||||
"xhr2": "~0.2.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
|
||||
// Copyright 2015-2016 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.
|
||||
|
||||
// Adapted from https://github.com/grpc/grpc-web/tree/master/src/proto/grpc/testing
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package grpc.testing;
|
||||
|
||||
service TestService {
|
||||
rpc EmptyCall(grpc.testing.Empty) returns (grpc.testing.Empty);
|
||||
rpc UnaryCall(SimpleRequest) returns (SimpleResponse);
|
||||
rpc StreamingOutputCall(StreamingOutputCallRequest)
|
||||
returns (stream StreamingOutputCallResponse);
|
||||
rpc UnimplementedCall(grpc.testing.Empty) returns (grpc.testing.Empty);
|
||||
}
|
||||
|
||||
message Empty {}
|
||||
|
||||
message BoolValue {
|
||||
bool value = 1;
|
||||
}
|
||||
|
||||
message Payload {
|
||||
bytes body = 2;
|
||||
}
|
||||
|
||||
message EchoStatus {
|
||||
int32 code = 1;
|
||||
string message = 2;
|
||||
}
|
||||
|
||||
message SimpleRequest {
|
||||
int32 response_size = 2;
|
||||
Payload payload = 3;
|
||||
EchoStatus response_status = 7;
|
||||
}
|
||||
|
||||
message SimpleResponse {
|
||||
Payload payload = 1;
|
||||
}
|
||||
|
||||
message ResponseParameters {
|
||||
int32 size = 1;
|
||||
int32 interval_us = 2;
|
||||
}
|
||||
|
||||
message StreamingOutputCallRequest {
|
||||
repeated ResponseParameters response_parameters = 2;
|
||||
Payload payload = 3;
|
||||
EchoStatus response_status = 7;
|
||||
}
|
||||
|
||||
message StreamingOutputCallResponse {
|
||||
Payload payload = 1;
|
||||
}
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
HOST=${1:-"localhost"}
|
||||
|
||||
npm test -- --host="$HOST"
|
||||
npm test -- --mode=binary --host="$HOST"
|
||||
Reference in New Issue
Block a user