mirror of https://github.com/grpc/grpc-dart.git
Fix for duplicate headers (#397)
This commit is contained in:
parent
b8197a5897
commit
e634f9749f
|
@ -65,11 +65,16 @@ jobs:
|
||||||
release-channel: ${{ matrix.sdk }}
|
release-channel: ${{ matrix.sdk }}
|
||||||
- name: Report version
|
- name: Report version
|
||||||
run: dart --version
|
run: dart --version
|
||||||
- name: Install grpcwebproxy
|
- name: Install envoy
|
||||||
if: ${{ matrix.platform == 'chrome' }}
|
if: ${{ matrix.platform == 'chrome' }}
|
||||||
run: |
|
run: |
|
||||||
./tool/install-grpcwebproxy.sh
|
sudo apt update
|
||||||
echo "/tmp/grpcwebproxy" >> $GITHUB_PATH
|
sudo apt install apt-transport-https ca-certificates curl gnupg-agent software-properties-common
|
||||||
|
curl -sL 'https://getenvoy.io/gpg' | sudo apt-key add -
|
||||||
|
apt-key fingerprint 6FF974DB | grep "5270 CEAC 57F6 3EBD 9EA9 005D 0253 D0B2 6FF9 74DB"
|
||||||
|
sudo add-apt-repository "deb [arch=amd64] https://dl.bintray.com/tetrate/getenvoy-deb $(lsb_release -cs) stable"
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y getenvoy-envoy
|
||||||
env:
|
env:
|
||||||
MATRIX_OS: ${{ matrix.os }}
|
MATRIX_OS: ${{ matrix.os }}
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
newer of protobuf compiler plugin.
|
newer of protobuf compiler plugin.
|
||||||
* `Client.$createCall` is deprecated because it does not invoke client
|
* `Client.$createCall` is deprecated because it does not invoke client
|
||||||
interceptors.
|
interceptors.
|
||||||
|
* Fix an issue [#380](https://github.com/grpc/grpc-dart/issues/380) causing
|
||||||
|
incorrect duplicated headers in gRPC-Web requests.
|
||||||
* Change minimum required Dart SDK to 2.8 to enable access to Unix domain sockets.
|
* Change minimum required Dart SDK to 2.8 to enable access to Unix domain sockets.
|
||||||
* Add support for Unix domain sockets in `Socket.serve` and `ClientChannel`.
|
* Add support for Unix domain sockets in `Socket.serve` and `ClientChannel`.
|
||||||
|
|
||||||
|
|
|
@ -37,9 +37,9 @@ pub get
|
||||||
pub run test
|
pub run test
|
||||||
```
|
```
|
||||||
|
|
||||||
gRPC-web tests require [`grpcwebproxy`](
|
gRPC-web tests require [`envoy`](
|
||||||
https://github.com/improbable-eng/grpc-web/tree/master/go/grpcwebproxy) by
|
https://www.envoyproxy.io/docs/envoy/latest/start/start.html) binary to be
|
||||||
Improbable Engineering to be available in the PATH. Pre-built binaries are [available](https://github.com/improbable-eng/grpc-web/releases).
|
available in the PATH.
|
||||||
|
|
||||||
## Guidelines for Pull Requests
|
## Guidelines for Pull Requests
|
||||||
|
|
||||||
|
|
|
@ -169,9 +169,6 @@ class XhrClientConnection extends ClientConnection {
|
||||||
for (final header in metadata.keys) {
|
for (final header in metadata.keys) {
|
||||||
request.setRequestHeader(header, metadata[header]);
|
request.setRequestHeader(header, metadata[header]);
|
||||||
}
|
}
|
||||||
request.setRequestHeader('Content-Type', 'application/grpc-web+proto');
|
|
||||||
request.setRequestHeader('X-User-Agent', 'grpc-web-dart/0.1');
|
|
||||||
request.setRequestHeader('X-Grpc-Web', '1');
|
|
||||||
// Overriding the mimetype allows us to stream and parse the data
|
// Overriding the mimetype allows us to stream and parse the data
|
||||||
request.overrideMimeType('text/plain; charset=x-user-defined');
|
request.overrideMimeType('text/plain; charset=x-user-defined');
|
||||||
request.responseType = 'text';
|
request.responseType = 'text';
|
||||||
|
|
|
@ -4,6 +4,7 @@ import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:grpc/grpc.dart';
|
import 'package:grpc/grpc.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:stream_channel/stream_channel.dart';
|
import 'package:stream_channel/stream_channel.dart';
|
||||||
|
|
||||||
import 'src/generated/echo.pbgrpc.dart';
|
import 'src/generated/echo.pbgrpc.dart';
|
||||||
|
@ -30,31 +31,71 @@ class EchoService extends EchoServiceBase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final envoyPort = 9999;
|
||||||
|
|
||||||
|
final envoyConfig = '''
|
||||||
|
static_resources:
|
||||||
|
listeners:
|
||||||
|
- name: listener_0
|
||||||
|
address:
|
||||||
|
socket_address: { address: 0.0.0.0, port_value: 0 }
|
||||||
|
filter_chains:
|
||||||
|
- filters:
|
||||||
|
- name: envoy.http_connection_manager
|
||||||
|
config:
|
||||||
|
codec_type: auto
|
||||||
|
stat_prefix: ingress_http
|
||||||
|
route_config:
|
||||||
|
name: local_route
|
||||||
|
virtual_hosts:
|
||||||
|
- name: local_service
|
||||||
|
domains: ["*"]
|
||||||
|
routes:
|
||||||
|
- match: { prefix: "/" }
|
||||||
|
route: { cluster: echo_service }
|
||||||
|
cors:
|
||||||
|
allow_origin_string_match:
|
||||||
|
- prefix: "*"
|
||||||
|
allow_methods: GET, PUT, DELETE, POST, OPTIONS
|
||||||
|
allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout
|
||||||
|
max_age: "1728000"
|
||||||
|
expose_headers: custom-header-1,grpc-status,grpc-message
|
||||||
|
http_filters:
|
||||||
|
- name: envoy.filters.http.grpc_web
|
||||||
|
- name: envoy.filters.http.cors
|
||||||
|
- name: envoy.filters.http.router
|
||||||
|
clusters:
|
||||||
|
- name: echo_service
|
||||||
|
connect_timeout: 0.25s
|
||||||
|
type: static
|
||||||
|
http2_protocol_options: {}
|
||||||
|
lb_policy: round_robin
|
||||||
|
hosts:
|
||||||
|
- socket_address: { address: 127.0.0.1, port_value: %TARGET_PORT% }
|
||||||
|
''';
|
||||||
|
|
||||||
hybridMain(StreamChannel channel) async {
|
hybridMain(StreamChannel channel) async {
|
||||||
// Spawn a gRPC server.
|
// Spawn a gRPC server.
|
||||||
final server = Server([EchoService()]);
|
final server = Server([EchoService()]);
|
||||||
await server.serve(port: 0);
|
await server.serve(port: 0);
|
||||||
_info('grpc server listening on ${server.port}');
|
_info('grpc server listening on ${server.port}');
|
||||||
|
|
||||||
|
// Create Envoy configuration.
|
||||||
|
final tempDir = await Directory.systemTemp.createTemp();
|
||||||
|
final config = p.join(tempDir.path, 'config.yaml');
|
||||||
|
await File(config).writeAsString(
|
||||||
|
envoyConfig.replaceAll('%TARGET_PORT%', server.port.toString()));
|
||||||
|
|
||||||
// Spawn a proxy that would translate gRPC-web protocol into gRPC protocol
|
// Spawn a proxy that would translate gRPC-web protocol into gRPC protocol
|
||||||
// for us. We use grpcwebproxy by Improbable Engineering. See CONTRIBUTING.md
|
// for us. We use envoy proxy. See CONTRIBUTING.md for setup.
|
||||||
// for setup.
|
|
||||||
Process proxy;
|
Process proxy;
|
||||||
try {
|
try {
|
||||||
proxy =
|
proxy = await Process.start('envoy', ['-c', config, '-l', 'debug']);
|
||||||
await Process.start('grpcwebproxy${Platform.isWindows ? '.exe' : ''}', [
|
|
||||||
'--backend_addr',
|
|
||||||
'localhost:${server.port}',
|
|
||||||
'--run_tls_server=false',
|
|
||||||
'--server_http_debug_port',
|
|
||||||
'0',
|
|
||||||
'--allow_all_origins',
|
|
||||||
]);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('''
|
print('''
|
||||||
Failed to start grpcwebproxy: $e.
|
Failed to start envoy: $e.
|
||||||
|
|
||||||
Make sure that grpcwebproxy is available in the PATH see CONTRIBUTING.md
|
Make sure that envoy is available in the PATH see CONTRIBUTING.md
|
||||||
if you are running tests locally.
|
if you are running tests locally.
|
||||||
''');
|
''');
|
||||||
channel.sink.add(0);
|
channel.sink.add(0);
|
||||||
|
@ -62,28 +103,41 @@ if you are running tests locally.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse output of the proxy process looking for a port it selected.
|
// Parse output of the proxy process looking for a port it selected.
|
||||||
final portRe = RegExp(r'listening for http on: .*:(\d+)');
|
final portRe = RegExp(
|
||||||
|
r'Set listener listener_0 socket factory local address to 0.0.0.0:(\d+)');
|
||||||
|
|
||||||
proxy.stderr
|
proxy.stderr
|
||||||
.transform(utf8.decoder)
|
.transform(utf8.decoder)
|
||||||
.transform(const LineSplitter())
|
.transform(const LineSplitter())
|
||||||
.listen((line) {
|
.listen((line) {
|
||||||
_info('grpcwebproxy|stderr] $line');
|
_info('envoy|stderr] $line');
|
||||||
|
final m = portRe.firstMatch(line);
|
||||||
|
if (m != null) {
|
||||||
|
channel.sink.add(int.parse(m[1]));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
proxy.stdout
|
proxy.stdout
|
||||||
.transform(utf8.decoder)
|
.transform(utf8.decoder)
|
||||||
.transform(const LineSplitter())
|
.transform(const LineSplitter())
|
||||||
.listen((line) {
|
.listen((line) {
|
||||||
_info('grpcwebproxy|stdout] $line');
|
_info('envoy|stdout] $line');
|
||||||
final m = portRe.firstMatch(line);
|
});
|
||||||
if (m != null) {
|
|
||||||
final port = int.parse(m[1]);
|
proxy.exitCode.then((value) {
|
||||||
channel.sink.add(port);
|
_info('proxy quit with ${value}');
|
||||||
|
if (value != 0) {
|
||||||
|
channel.sink.addError('proxy exited with ${value}');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
proxy.exitCode.then((value) => _info('proxy quit with ${value}'));
|
// Wait for the harness to tell us to shutdown.
|
||||||
|
await channel.stream.first;
|
||||||
|
proxy.kill();
|
||||||
|
if (tempDir.existsSync()) {
|
||||||
|
tempDir.deleteSync(recursive: true);
|
||||||
|
}
|
||||||
|
channel.sink.add('EXITED');
|
||||||
}
|
}
|
||||||
|
|
||||||
void _info(String line) {
|
void _info(String line) {
|
||||||
|
|
|
@ -1,64 +1,100 @@
|
||||||
@TestOn('browser')
|
@TestOn('browser')
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:stream_channel/stream_channel.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
import 'package:grpc/grpc_web.dart';
|
import 'package:grpc/grpc_web.dart';
|
||||||
|
|
||||||
import 'src/generated/echo.pbgrpc.dart';
|
import 'src/generated/echo.pbgrpc.dart';
|
||||||
|
|
||||||
/// Starts gRPC server and a gRPC-web proxy (see grpc_web_server.dart for
|
|
||||||
/// implementation.
|
|
||||||
///
|
|
||||||
/// Returns uri which can be used to talk to using gRPC-web channel.
|
|
||||||
///
|
|
||||||
/// Note: server will be shut down when the test which spawned it finishes
|
|
||||||
/// running.
|
|
||||||
Future<Uri> startServer() async {
|
|
||||||
// Spawn the server code on the server side, it will send us back port
|
|
||||||
// number we should be talking to.
|
|
||||||
final serverChannel = spawnHybridUri('grpc_web_server.dart');
|
|
||||||
final port = await serverChannel.stream.first;
|
|
||||||
|
|
||||||
// Note: we would like to test https as well, but we can't easily do it
|
|
||||||
// because browsers like chrome don't trust self-signed certificates by
|
|
||||||
// default.
|
|
||||||
return Uri.parse('http://localhost:$port');
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
// Test verifies that gRPC-web echo example works by talking to a gRPC
|
// Test verifies that gRPC-web echo example works by talking to a gRPC
|
||||||
// server (written in Dart) via gRPC-web protocol through a third party
|
// server (written in Dart) via gRPC-web protocol through a third party
|
||||||
// gRPC-web proxy.
|
// gRPC-web proxy.
|
||||||
test('gRPC-web echo test', () async {
|
test('gRPC-web echo test', () async {
|
||||||
final serverUri = await startServer();
|
final server = await GrpcWebServer.start();
|
||||||
final channel = GrpcWebClientChannel.xhr(serverUri);
|
try {
|
||||||
final service = EchoServiceClient(channel);
|
final channel = GrpcWebClientChannel.xhr(server.uri);
|
||||||
|
final service = EchoServiceClient(channel);
|
||||||
|
|
||||||
const testMessage = 'hello from gRPC-web';
|
const testMessage = 'hello from gRPC-web';
|
||||||
|
|
||||||
// First test a simple echo request.
|
// First test a simple echo request.
|
||||||
final response = await service.echo(EchoRequest()..message = testMessage);
|
final response = await service.echo(EchoRequest()..message = testMessage);
|
||||||
expect(response.message, equals(testMessage));
|
|
||||||
|
|
||||||
// Now test that streaming requests also works by asking echo server
|
|
||||||
// to send us a number of messages every 100 ms. Check that we receive
|
|
||||||
// them fast enough (if streaming is broken we will receive all of them
|
|
||||||
// in one go).
|
|
||||||
final sw = Stopwatch()..start();
|
|
||||||
final timings = await service
|
|
||||||
.serverStreamingEcho(ServerStreamingEchoRequest()
|
|
||||||
..message = testMessage
|
|
||||||
..messageCount = 20
|
|
||||||
..messageInterval = 100)
|
|
||||||
.map((response) {
|
|
||||||
expect(response.message, equals(testMessage));
|
expect(response.message, equals(testMessage));
|
||||||
final timing = sw.elapsedMilliseconds;
|
|
||||||
sw.reset();
|
// Now test that streaming requests also works by asking echo server
|
||||||
return timing;
|
// to send us a number of messages every 100 ms. Check that we receive
|
||||||
}).toList();
|
// them fast enough (if streaming is broken we will receive all of them
|
||||||
final maxDelay = timings.reduce(math.max);
|
// in one go).
|
||||||
expect(maxDelay, lessThan(500));
|
final sw = Stopwatch()..start();
|
||||||
|
final timings = await service
|
||||||
|
.serverStreamingEcho(ServerStreamingEchoRequest()
|
||||||
|
..message = testMessage
|
||||||
|
..messageCount = 20
|
||||||
|
..messageInterval = 100)
|
||||||
|
.map((response) {
|
||||||
|
expect(response.message, equals(testMessage));
|
||||||
|
final timing = sw.elapsedMilliseconds;
|
||||||
|
sw.reset();
|
||||||
|
return timing;
|
||||||
|
}).toList();
|
||||||
|
final maxDelay = timings.reduce(math.max);
|
||||||
|
expect(maxDelay, lessThan(500));
|
||||||
|
} finally {
|
||||||
|
await server.shutdown();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class GrpcWebServer {
|
||||||
|
final StreamChannel channel;
|
||||||
|
final Future<void> whenExited;
|
||||||
|
final Uri uri;
|
||||||
|
|
||||||
|
GrpcWebServer(this.channel, this.whenExited, this.uri);
|
||||||
|
|
||||||
|
Future<void> shutdown() async {
|
||||||
|
channel.sink.add('shutdown');
|
||||||
|
await whenExited;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Starts gRPC server and a gRPC-web proxy (see grpc_web_server.dart for
|
||||||
|
/// implementation.
|
||||||
|
///
|
||||||
|
/// Returns uri which can be used to talk to using gRPC-web channel.
|
||||||
|
///
|
||||||
|
/// Note: you need to explicitly call shutdown on the returned object
|
||||||
|
/// otherwise envoy proxy process leaks.
|
||||||
|
static Future<GrpcWebServer> start() async {
|
||||||
|
// Spawn the server code on the server side, it will send us back port
|
||||||
|
// number we should be talking to.
|
||||||
|
final serverChannel = spawnHybridUri('grpc_web_server.dart');
|
||||||
|
final portCompleter = Completer<int>();
|
||||||
|
final exitCompleter = Completer<void>();
|
||||||
|
serverChannel.stream.listen((event) {
|
||||||
|
if (!portCompleter.isCompleted) {
|
||||||
|
portCompleter.complete(event);
|
||||||
|
} else if (event == 'EXITED') {
|
||||||
|
exitCompleter.complete();
|
||||||
|
}
|
||||||
|
}, onError: (e) {
|
||||||
|
if (!portCompleter.isCompleted) {
|
||||||
|
portCompleter.completeError(e);
|
||||||
|
} else if (!exitCompleter.isCompleted) {
|
||||||
|
exitCompleter.completeError(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
final port = await portCompleter.future;
|
||||||
|
|
||||||
|
// Note: we would like to test https as well, but we can't easily do it
|
||||||
|
// because browsers like chrome don't trust self-signed certificates by
|
||||||
|
// default.
|
||||||
|
return GrpcWebServer(serverChannel, exitCompleter.future,
|
||||||
|
Uri.parse('http://localhost:$port'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,44 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
set -ex
|
|
||||||
|
|
||||||
VERSION=v0.13.0
|
|
||||||
SUFFIX=
|
|
||||||
WGET=wget
|
|
||||||
|
|
||||||
case $TRAVIS_OS_NAME in
|
|
||||||
linux)
|
|
||||||
VARIANT=linux-x86_64
|
|
||||||
;;
|
|
||||||
osx)
|
|
||||||
VARIANT=osx-x86_64
|
|
||||||
;;
|
|
||||||
windows)
|
|
||||||
VARIANT=win64.exe
|
|
||||||
SUFFIX=.exe
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
case $MATRIX_OS in
|
|
||||||
ubuntu-latest)
|
|
||||||
VARIANT=linux-x86_64
|
|
||||||
;;
|
|
||||||
macos-latest)
|
|
||||||
VARIANT=osx-x86_64
|
|
||||||
;;
|
|
||||||
windows-latest)
|
|
||||||
VARIANT=win64.exe
|
|
||||||
SUFFIX=.exe
|
|
||||||
WGET=C:/msys64/usr/bin/wget.exe
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
BINARY=grpcwebproxy-${VERSION}-${VARIANT}
|
|
||||||
|
|
||||||
${WGET} https://github.com/improbable-eng/grpc-web/releases/download/${VERSION}/${BINARY}.zip -O /tmp/grpcwebproxy.zip
|
|
||||||
rm -rf /tmp/grpcwebproxy
|
|
||||||
mkdir /tmp/grpcwebproxy
|
|
||||||
cd /tmp/grpcwebproxy
|
|
||||||
unzip /tmp/grpcwebproxy.zip
|
|
||||||
mv dist/${BINARY} ./grpcwebproxy${SUFFIX}
|
|
||||||
|
|
Loading…
Reference in New Issue