Migrating to dart2 & angel2
This commit is contained in:
parent
103a4340af
commit
7c702012c1
6 changed files with 106 additions and 80 deletions
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:angel_framework/http.dart';
|
||||||
import 'package:angel_proxy/angel_proxy.dart';
|
import 'package:angel_proxy/angel_proxy.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
@ -19,7 +20,7 @@ main() async {
|
||||||
publicPath: '/pub',
|
publicPath: '/pub',
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
);
|
);
|
||||||
app.use(pubProxy.handleRequest);
|
app.all("/pub/*", pubProxy.handleRequest);
|
||||||
|
|
||||||
// Pub's HTML assumes that the site's styles, etc. are on the absolute path `/static`.
|
// Pub's HTML assumes that the site's styles, etc. are on the absolute path `/static`.
|
||||||
// This is not the case here. Let's patch that up:
|
// This is not the case here. Let's patch that up:
|
||||||
|
@ -35,10 +36,10 @@ main() async {
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
recoverFrom404: false,
|
recoverFrom404: false,
|
||||||
);
|
);
|
||||||
app.use(dartlangProxy.handleRequest);
|
app.all('*', dartlangProxy.handleRequest);
|
||||||
|
|
||||||
// In case we can't connect to dartlang.org, show an error.
|
// In case we can't connect to dartlang.org, show an error.
|
||||||
app.use('Couldn\'t connect to Pub or dartlang.');
|
app.fallback((req, res) => res.write('Couldn\'t connect to Pub or dartlang.'));
|
||||||
|
|
||||||
app.logger = new Logger('angel')
|
app.logger = new Logger('angel')
|
||||||
..onRecord.listen(
|
..onRecord.listen(
|
||||||
|
@ -49,7 +50,7 @@ main() async {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
var server = await app.startServer(InternetAddress.LOOPBACK_IP_V4, 8080);
|
var server = await AngelHttp(app).startServer(InternetAddress.loopbackIPv4, 8080);
|
||||||
print('Listening at http://${server.address.address}:${server.port}');
|
print('Listening at http://${server.address.address}:${server.port}');
|
||||||
print('Check this out! http://${server.address.address}:${server.port}/pub/packages/angel_framework');
|
print('Check this out! http://${server.address.address}:${server.port}/pub/packages/angel_framework');
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:convert';
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
import 'package:http/src/base_client.dart' as http;
|
import 'package:http_parser/http_parser.dart';
|
||||||
import 'package:http/src/request.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:http/src/response.dart' as http;
|
|
||||||
import 'package:http/src/streamed_response.dart' as http;
|
|
||||||
|
|
||||||
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
|
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
|
||||||
|
final MediaType _fallbackMediaType = MediaType('application', 'octet-stream');
|
||||||
|
|
||||||
class Proxy {
|
class Proxy {
|
||||||
String _prefix;
|
String _prefix;
|
||||||
|
|
||||||
final Angel app;
|
final Angel app;
|
||||||
final http.BaseClient httpClient;
|
final http.Client httpClient;
|
||||||
|
|
||||||
/// If `true` (default), then the plug-in will ignore failures to connect to the proxy, and allow other handlers to run.
|
/// If `true` (default), then the plug-in will ignore failures to connect to the proxy, and allow other handlers to run.
|
||||||
final bool recoverFromDead;
|
final bool recoverFromDead;
|
||||||
|
@ -20,6 +20,8 @@ class Proxy {
|
||||||
final String host, mapTo, publicPath;
|
final String host, mapTo, publicPath;
|
||||||
final int port;
|
final int port;
|
||||||
final String protocol;
|
final String protocol;
|
||||||
|
|
||||||
|
/// If `null` then no timout is added for requests
|
||||||
final Duration timeout;
|
final Duration timeout;
|
||||||
|
|
||||||
Proxy(
|
Proxy(
|
||||||
|
@ -34,7 +36,10 @@ class Proxy {
|
||||||
this.recoverFrom404: true,
|
this.recoverFrom404: true,
|
||||||
this.timeout,
|
this.timeout,
|
||||||
}) {
|
}) {
|
||||||
_prefix = publicPath.replaceAll(_straySlashes, '');
|
if (this.recoverFromDead == null) throw ArgumentError.notNull("recoverFromDead");
|
||||||
|
if (this.recoverFrom404 == null) throw ArgumentError.notNull("recoverFrom404");
|
||||||
|
|
||||||
|
_prefix = publicPath?.replaceAll(_straySlashes, '') ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
void close() => httpClient.close();
|
void close() => httpClient.close();
|
||||||
|
@ -43,20 +48,17 @@ class Proxy {
|
||||||
Future<bool> handleRequest(RequestContext req, ResponseContext res) {
|
Future<bool> handleRequest(RequestContext req, ResponseContext res) {
|
||||||
var path = req.path.replaceAll(_straySlashes, '');
|
var path = req.path.replaceAll(_straySlashes, '');
|
||||||
|
|
||||||
if (_prefix?.isNotEmpty == true) {
|
if (_prefix.isNotEmpty) {
|
||||||
if (!path.startsWith(_prefix))
|
if (!path.startsWith(_prefix)) return new Future<bool>.value(true);
|
||||||
return new Future<bool>.value(true);
|
|
||||||
else {
|
|
||||||
path = path.replaceFirst(_prefix, '').replaceAll(_straySlashes, '');
|
path = path.replaceFirst(_prefix, '').replaceAll(_straySlashes, '');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return servePath(path, req, res);
|
return servePath(path, req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Proxies a request to the given path on the remote server.
|
/// Proxies a request to the given path on the remote server.
|
||||||
Future<bool> servePath(
|
Future<bool> servePath(String path, RequestContext req, ResponseContext res) async {
|
||||||
String path, RequestContext req, ResponseContext res) async {
|
|
||||||
http.StreamedResponse rs;
|
http.StreamedResponse rs;
|
||||||
|
|
||||||
final mapping = '$mapTo/$path'.replaceAll(_straySlashes, '');
|
final mapping = '$mapTo/$path'.replaceAll(_straySlashes, '');
|
||||||
|
@ -74,8 +76,7 @@ class Proxy {
|
||||||
'host': port == null ? host : '$host:$port',
|
'host': port == null ? host : '$host:$port',
|
||||||
'x-forwarded-for': req.remoteAddress.address,
|
'x-forwarded-for': req.remoteAddress.address,
|
||||||
'x-forwarded-port': req.uri.port.toString(),
|
'x-forwarded-port': req.uri.port.toString(),
|
||||||
'x-forwarded-host':
|
'x-forwarded-host': req.headers.host ?? req.headers.value('host') ?? 'none',
|
||||||
req.headers.host ?? req.headers.value('host') ?? 'none',
|
|
||||||
'x-forwarded-proto': protocol,
|
'x-forwarded-proto': protocol,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -83,19 +84,18 @@ class Proxy {
|
||||||
headers[name] = values.join(',');
|
headers[name] = values.join(',');
|
||||||
});
|
});
|
||||||
|
|
||||||
headers['cookie'] =
|
headers[HttpHeaders.cookieHeader] = req.cookies.map<String>((c) => '${c.name}=${c.value}').join('; ');
|
||||||
req.cookies.map<String>((c) => '${c.name}=${c.value}').join('; ');
|
|
||||||
|
|
||||||
var body;
|
var body;
|
||||||
|
|
||||||
if (req.method != 'GET' && app.storeOriginalBuffer == true) {
|
if (req.method != 'GET' && app.keepRawRequestBuffers == true) {
|
||||||
await req.parse();
|
body = (await req.parse()).originalBuffer;
|
||||||
if (req.originalBuffer?.isNotEmpty == true) body = req.originalBuffer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var rq = new http.Request(req.method, Uri.parse(url));
|
var rq = new http.Request(req.method, Uri.parse(url));
|
||||||
rq.headers.addAll(headers);
|
rq.headers.addAll(headers);
|
||||||
rq.headers['host'] = rq.url.host;
|
rq.headers['host'] = rq.url.host;
|
||||||
|
rq.encoding = Utf8Codec(allowMalformed: true);
|
||||||
|
|
||||||
if (body != null) rq.bodyBytes = body;
|
if (body != null) rq.bodyBytes = body;
|
||||||
|
|
||||||
|
@ -106,37 +106,51 @@ class Proxy {
|
||||||
if (timeout != null) future = future.timeout(timeout);
|
if (timeout != null) future = future.timeout(timeout);
|
||||||
rs = await future;
|
rs = await future;
|
||||||
} on TimeoutException catch (e, st) {
|
} on TimeoutException catch (e, st) {
|
||||||
if (recoverFromDead != false)
|
if (recoverFromDead) return true;
|
||||||
return true;
|
|
||||||
else
|
|
||||||
throw new AngelHttpException(
|
throw new AngelHttpException(
|
||||||
e,
|
e,
|
||||||
stackTrace: st,
|
stackTrace: st,
|
||||||
statusCode: 504,
|
statusCode: 504,
|
||||||
message:
|
message: 'Connection to remote host "$host" timed out after ${timeout.inMilliseconds}ms.',
|
||||||
'Connection to remote host "$host" timed out after ${timeout.inMilliseconds}ms.',
|
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (recoverFromDead != false)
|
if (recoverFromDead) return true;
|
||||||
return true;
|
|
||||||
else
|
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rs.statusCode == 404 && recoverFrom404 != false) return true;
|
if (rs.statusCode == 404 && recoverFrom404) return true;
|
||||||
|
if (rs.contentLength == 0 && recoverFromDead) return true;
|
||||||
|
|
||||||
|
MediaType mediaType;
|
||||||
|
if (rs.headers.containsKey(HttpHeaders.contentTypeHeader)) {
|
||||||
|
try {
|
||||||
|
mediaType = MediaType.parse(rs.headers[HttpHeaders.contentTypeHeader]);
|
||||||
|
} on FormatException catch (e, st) {
|
||||||
|
if (recoverFromDead) return true;
|
||||||
|
|
||||||
|
throw new AngelHttpException(
|
||||||
|
e,
|
||||||
|
stackTrace: st,
|
||||||
|
statusCode: 504,
|
||||||
|
message: 'Host "$host" returned a malformed content-type',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mediaType = _fallbackMediaType;
|
||||||
|
}
|
||||||
|
|
||||||
|
var proxiedHeaders = new Map<String, String>.from(rs.headers);
|
||||||
|
|
||||||
// http/2 client implementations usually get confused by transfer-encoding
|
|
||||||
res
|
res
|
||||||
|
..contentType = mediaType
|
||||||
..statusCode = rs.statusCode
|
..statusCode = rs.statusCode
|
||||||
..headers.addAll(new Map<String, String>.from(rs.headers)
|
..headers.addAll(proxiedHeaders);
|
||||||
..remove(HttpHeaders.TRANSFER_ENCODING));
|
|
||||||
|
|
||||||
if (rs.contentLength == 0 && recoverFromDead != false) return true;
|
|
||||||
|
|
||||||
var stream = rs.stream;
|
var stream = rs.stream;
|
||||||
|
|
||||||
if (rs.headers['content-encoding'] == 'gzip')
|
// [upgrading to dart2] Keeping this workaround as a reference. It's not properly typed for dart2
|
||||||
stream = stream.transform(GZIP.encoder);
|
//if (rs.headers[HttpHeaders.contentEncodingHeader] == 'gzip') stream = stream.transform(gzip.encoder);
|
||||||
|
|
||||||
await stream.pipe(res);
|
await stream.pipe(res);
|
||||||
|
|
||||||
|
|
11
pubspec.yaml
11
pubspec.yaml
|
@ -4,11 +4,12 @@ version: 1.1.1
|
||||||
author: Tobe O <thosakwe@gmail.com>
|
author: Tobe O <thosakwe@gmail.com>
|
||||||
homepage: https://github.com/angel-dart/proxy
|
homepage: https://github.com/angel-dart/proxy
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=1.19.0 <3.0.0"
|
sdk: ">=2.0.0 <3.0.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
angel_framework: ^1.1.0-alpha
|
angel_framework: ^2.0.0-alpha # The core server library.
|
||||||
http: ^0.11.3
|
http: ^0.11.3
|
||||||
dev_dependencies:
|
|
||||||
angel_test: ^1.1.0-alpha
|
dev_dependencies:
|
||||||
test: ^0.12.15
|
angel_test: 2.0.0-alpha
|
||||||
|
test: ^1.0.0
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:angel_framework/http.dart';
|
||||||
import 'package:angel_proxy/angel_proxy.dart';
|
import 'package:angel_proxy/angel_proxy.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
@ -14,10 +15,11 @@ main() {
|
||||||
String url;
|
String url;
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
app = new Angel()..storeOriginalBuffer = true;
|
app = new Angel()..keepRawRequestBuffers = true;
|
||||||
|
var appHttp = AngelHttp(app);
|
||||||
var httpClient = new http.Client();
|
var httpClient = new http.Client();
|
||||||
|
|
||||||
testServer = await testApp().startServer();
|
testServer = await startTestServer();
|
||||||
|
|
||||||
var proxy1 = new Proxy(
|
var proxy1 = new Proxy(
|
||||||
app,
|
app,
|
||||||
|
@ -34,10 +36,10 @@ main() {
|
||||||
mapTo: '/foo',
|
mapTo: '/foo',
|
||||||
);
|
);
|
||||||
|
|
||||||
app.use(proxy1.handleRequest);
|
app.all("/proxy/*", proxy1.handleRequest);
|
||||||
app.use(proxy2.handleRequest);
|
app.all("*", proxy2.handleRequest);
|
||||||
|
|
||||||
app.use((req, res) {
|
app.fallback((req, res) {
|
||||||
print('Intercepting empty from ${req.uri}');
|
print('Intercepting empty from ${req.uri}');
|
||||||
res.write('intercept empty');
|
res.write('intercept empty');
|
||||||
});
|
});
|
||||||
|
@ -54,7 +56,9 @@ main() {
|
||||||
if (rec.stackTrace != null) print(rec.stackTrace);
|
if (rec.stackTrace != null) print(rec.stackTrace);
|
||||||
});
|
});
|
||||||
|
|
||||||
server = await app.startServer();
|
await appHttp.startServer();
|
||||||
|
|
||||||
|
server = appHttp.httpServer;
|
||||||
url = 'http://${server.address.address}:${server.port}';
|
url = 'http://${server.address.address}:${server.port}';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -68,7 +72,7 @@ main() {
|
||||||
test('publicPath', () async {
|
test('publicPath', () async {
|
||||||
final response = await client.get('$url/proxy/hello');
|
final response = await client.get('$url/proxy/hello');
|
||||||
print('Response: ${response.body}');
|
print('Response: ${response.body}');
|
||||||
expect(response.body, equals('"world"'));
|
expect(response.body, equals('world'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('empty', () async {
|
test('empty', () async {
|
||||||
|
@ -80,13 +84,12 @@ main() {
|
||||||
test('mapTo', () async {
|
test('mapTo', () async {
|
||||||
final response = await client.get('$url/bar');
|
final response = await client.get('$url/bar');
|
||||||
print('Response: ${response.body}');
|
print('Response: ${response.body}');
|
||||||
expect(response.body, equals('"baz"'));
|
expect(response.body, equals('baz'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('original buffer', () async {
|
test('original buffer', () async {
|
||||||
var response = await client.post('$url/proxy/body',
|
var response = await client
|
||||||
body: JSON.encode({'foo': 'bar'}),
|
.post('$url/proxy/body', body: json.encode({'foo': 'bar'}), headers: {'content-type': 'application/json'});
|
||||||
headers: {'content-type': 'application/json'});
|
|
||||||
print('Response: ${response.body}');
|
print('Response: ${response.body}');
|
||||||
expect(response.body, isNotEmpty);
|
expect(response.body, isNotEmpty);
|
||||||
expect(response.body, isNot('intercept empty'));
|
expect(response.body, isNot('intercept empty'));
|
||||||
|
|
|
@ -1,18 +1,26 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:angel_framework/http.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
Angel testApp() {
|
Future<HttpServer> startTestServer() {
|
||||||
final app = new Angel()..lazyParseBodies = true;
|
final app = new Angel()
|
||||||
|
..eagerParseRequestBodies = false
|
||||||
|
..keepRawRequestBuffers = true;
|
||||||
|
|
||||||
app.get('/hello', 'world');
|
app.get('/hello', (req, res) => res.write('world'));
|
||||||
app.get('/foo/bar', 'baz');
|
app.get('/foo/bar', (req, res) => res.write('baz'));
|
||||||
app.post('/body', (RequestContext req, res) async {
|
app.post('/body', (RequestContext req, res) async {
|
||||||
var body = await req.lazyBody();
|
var body = await req.parseBody();
|
||||||
print('Body: $body');
|
app.logger.info('Body: $body');
|
||||||
return body;
|
return body;
|
||||||
});
|
});
|
||||||
|
|
||||||
app.logger = new Logger('testApp');
|
app.logger = new Logger('testApp');
|
||||||
|
var server = AngelHttp(app);
|
||||||
|
app.dumpTree();
|
||||||
|
|
||||||
return app..dumpTree();
|
return server.startServer();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:angel_framework/http.dart';
|
||||||
import 'package:angel_proxy/angel_proxy.dart';
|
import 'package:angel_proxy/angel_proxy.dart';
|
||||||
import 'package:angel_test/angel_test.dart';
|
import 'package:angel_test/angel_test.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
@ -10,43 +11,42 @@ import 'package:test/test.dart';
|
||||||
main() {
|
main() {
|
||||||
Angel app, testApp;
|
Angel app, testApp;
|
||||||
TestClient client;
|
TestClient client;
|
||||||
|
Proxy layer;
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
testApp = new Angel();
|
testApp = new Angel();
|
||||||
testApp.get('/foo', (req, res) async {
|
testApp.get('/foo', (req, res) async {
|
||||||
res.write('pub serve');
|
res.write('pub serve');
|
||||||
});
|
});
|
||||||
testApp.get('/empty', (req, res) => res.end());
|
testApp.get('/empty', (req, res) => res.close());
|
||||||
|
|
||||||
testApp.responseFinalizers.add((req, ResponseContext res) async {
|
testApp.responseFinalizers.add((req, res) async {
|
||||||
print('OUTGOING: ' + new String.fromCharCodes(res.buffer.toBytes()));
|
print('OUTGOING: ' + new String.fromCharCodes(res.buffer.toBytes()));
|
||||||
});
|
});
|
||||||
|
|
||||||
testApp.injectEncoders({'gzip': GZIP.encoder});
|
testApp.encoders.addAll({'gzip': gzip.encoder});
|
||||||
|
|
||||||
var server = await testApp.startServer();
|
var server = await AngelHttp(testApp).startServer();
|
||||||
|
|
||||||
app = new Angel();
|
app = new Angel();
|
||||||
app.get('/bar', (req, res) => res.write('normal'));
|
app.get('/bar', (req, res) => res.write('normal'));
|
||||||
|
|
||||||
var httpClient = new http.Client();
|
var httpClient = new http.Client();
|
||||||
|
|
||||||
var layer = new Proxy(
|
layer = new Proxy(
|
||||||
app,
|
app,
|
||||||
httpClient,
|
httpClient,
|
||||||
server.address.address,
|
server.address.address,
|
||||||
port: server.port,
|
port: server.port,
|
||||||
publicPath: '/proxy',
|
publicPath: '/proxy',
|
||||||
);
|
);
|
||||||
app.use(layer.handleRequest);
|
app.all("*", layer.handleRequest);
|
||||||
|
|
||||||
app.responseFinalizers.add((req, ResponseContext res) async {
|
app.responseFinalizers.add((req, ResponseContext res) async {
|
||||||
print('Normal. Buf: ' +
|
print('Normal. Buf: ' + new String.fromCharCodes(res.buffer.toBytes()) + ', headers: ${res.headers}');
|
||||||
new String.fromCharCodes(res.buffer.toBytes()) +
|
|
||||||
', headers: ${res.headers}');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.injectEncoders({'gzip': GZIP.encoder});
|
app.encoders.addAll({'gzip': gzip.encoder});
|
||||||
|
|
||||||
client = await connectTo(app);
|
client = await connectTo(app);
|
||||||
});
|
});
|
||||||
|
@ -61,11 +61,10 @@ main() {
|
||||||
|
|
||||||
test('proxied', () async {
|
test('proxied', () async {
|
||||||
var rq = new MockHttpRequest('GET', Uri.parse('/proxy/foo'))..close();
|
var rq = new MockHttpRequest('GET', Uri.parse('/proxy/foo'))..close();
|
||||||
await app.handleRequest(rq);
|
var rqc = await HttpRequestContext.from(rq, app, '/proxy/foo');
|
||||||
var response = await rq.response
|
var rsc = HttpResponseContext(rq.response, app);
|
||||||
.transform(GZIP.decoder)
|
await app.executeHandler(layer, rqc, rsc);
|
||||||
.transform(UTF8.decoder)
|
var response = await rq.response.transform(gzip.decoder).transform(utf8.decoder).join();
|
||||||
.join();
|
|
||||||
expect(response, 'pub serve');
|
expect(response, 'pub serve');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue