// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. @TestOn('vm') library; import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:async/async.dart'; import 'package:http/http.dart' as http; import 'package:http_parser/http_parser.dart' as parser; import 'package:shelf/shelf.dart'; import 'package:shelf/shelf_io.dart' as shelf_io; import 'package:shelf/src/util.dart'; import 'package:test/test.dart'; import 'ssl_certs.dart'; import 'test_util.dart'; void main() { tearDown(() async { if (_server != null) { await _server!.close(force: true); _server = null; } }); test('sync handler returns a value to the client', () async { await _scheduleServer(syncHandler); var response = await _get(); expect(response.statusCode, HttpStatus.ok); expect(response.body, 'Hello from /'); }); test('async handler returns a value to the client', () async { await _scheduleServer(asyncHandler); var response = await _get(); expect(response.statusCode, HttpStatus.ok); expect(response.body, 'Hello from /'); }); test('thrown error leads to a 500', () async { await _scheduleServer((request) { throw UnsupportedError('test'); }); var response = await _get(); expect(response.statusCode, HttpStatus.internalServerError); expect(response.body, 'Internal Server Error'); }); test('async error leads to a 500', () async { await _scheduleServer((request) { return Future.error('test'); }); var response = await _get(); expect(response.statusCode, HttpStatus.internalServerError); expect(response.body, 'Internal Server Error'); }); test('supports HEAD requests', () async { await _scheduleServer((request) { return Response(200, headers: {'content-length': '1'}); }); var response = await _head(); expect(response.headers['content-length'], '1'); }); test('Request is populated correctly', () async { late Uri uri; await _scheduleServer((request) { expect(request.method, 'GET'); expect(request.requestedUri, uri); expect(request.url.path, 'foo/bar'); expect(request.url.pathSegments, ['foo', 'bar']); expect(request.protocolVersion, '1.1'); expect(request.url.query, 'qs=value'); expect(request.handlerPath, '/'); return syncHandler(request); }); uri = Uri.http('localhost:$_serverPort', '/foo/bar', {'qs': 'value'}); var response = await http.get(uri); expect(response.statusCode, HttpStatus.ok); expect(response.body, 'Hello from /foo/bar'); }); test('Request can handle colon in first path segment', () async { await _scheduleServer(syncHandler); var response = await _get(path: 'user:42'); expect(response.statusCode, HttpStatus.ok); expect(response.body, 'Hello from /user:42'); }); test('chunked requests are un-chunked', () async { await _scheduleServer(expectAsync1((request) { expect(request.contentLength, isNull); expect(request.method, 'POST'); expect( request.headers, isNot(contains(HttpHeaders.transferEncodingHeader))); expect( request.read().toList(), completion(equals([ [1, 2, 3, 4] ]))); return Response.ok(null); })); var request = http.StreamedRequest('POST', Uri.http('localhost:$_serverPort', '')); request.sink.add([1, 2, 3, 4]); // ignore: unawaited_futures request.sink.close(); var response = await request.send(); expect(response.statusCode, HttpStatus.ok); }); test('custom response headers are received by the client', () async { await _scheduleServer((request) { return Response.ok('Hello from /', headers: {'test-header': 'test-value', 'test-list': 'a, b, c'}); }); var response = await _get(); expect(response.statusCode, HttpStatus.ok); expect(response.headers['test-header'], 'test-value'); expect(response.body, 'Hello from /'); }); test('multiple headers are received from the client', () async { await _scheduleServer((request) { return Response.ok('Hello from /', headers: { 'requested-values': request.headersAll['request-values']!, 'requested-values-length': request.headersAll['request-values']!.length.toString(), 'set-cookie-values': request.headersAll['set-cookie']!, 'set-cookie-values-length': request.headersAll['set-cookie']!.length.toString(), }); }); final response = await _get(headers: { 'request-values': ['a', 'b'], 'set-cookie': ['c', 'd'], }); expect(response.statusCode, HttpStatus.ok); expect(response.headers['requested-values'], 'a, b'); expect(response.headers['requested-values-length'], '1'); expect(response.headers['set-cookie-values'], 'c, d'); expect(response.headers['set-cookie-values-length'], '2'); }); test('custom status code is received by the client', () async { await _scheduleServer((request) { return Response(299, body: 'Hello from /'); }); var response = await _get(); expect(response.statusCode, 299); expect(response.body, 'Hello from /'); }); test('custom request headers are received by the handler', () async { await _scheduleServer((request) { expect(request.headers, containsPair('custom-header', 'client value')); // dart:io HttpServer splits multi-value headers into an array // validate that they are combined correctly expect(request.headers, containsPair('multi-header', 'foo,bar,baz')); return syncHandler(request); }); var headers = { 'custom-header': 'client value', 'multi-header': 'foo,bar,baz' }; var response = await _get(headers: headers); expect(response.statusCode, HttpStatus.ok); expect(response.body, 'Hello from /'); }); test('post with empty content', () async { await _scheduleServer((request) async { expect(request.mimeType, isNull); expect(request.encoding, isNull); expect(request.method, 'POST'); expect(request.contentLength, 0); var body = await request.readAsString(); expect(body, ''); return syncHandler(request); }); var response = await _post(); expect(response.statusCode, HttpStatus.ok); expect(response.stream.bytesToString(), completion('Hello from /')); }); test('post with request content', () async { await _scheduleServer((request) async { expect(request.mimeType, 'text/plain'); expect(request.encoding, utf8); expect(request.method, 'POST'); expect(request.contentLength, 9); var body = await request.readAsString(); expect(body, 'test body'); return syncHandler(request); }); var response = await _post(body: 'test body'); expect(response.statusCode, HttpStatus.ok); expect(response.stream.bytesToString(), completion('Hello from /')); }); test('supports request hijacking', () async { await _scheduleServer((request) { expect(request.method, 'POST'); request.hijack(expectAsync1((channel) { expect(channel.stream.first, completion(equals('Hello'.codeUnits))); channel.sink.add('HTTP/1.1 404 Not Found\r\n' 'date: Mon, 23 May 2005 22:38:34 GMT\r\n' 'Content-Length: 13\r\n' '\r\n' 'Hello, world!' .codeUnits); channel.sink.close(); })); }); var response = await _post(body: 'Hello'); expect(response.statusCode, HttpStatus.notFound); expect(response.headers['date'], 'Mon, 23 May 2005 22:38:34 GMT'); expect( response.stream.bytesToString(), completion(equals('Hello, world!'))); }); test('reports an error if a HijackException is thrown without hijacking', () async { await _scheduleServer((request) => throw const HijackException()); var response = await _get(); expect(response.statusCode, HttpStatus.internalServerError); }); test('passes asynchronous exceptions to the parent error zone', () async { await runZonedGuarded(() async { var server = await shelf_io.serve((request) { Future(() => throw StateError('oh no')); return syncHandler(request); }, 'localhost', 0); var response = await http.get(Uri.http('localhost:${server.port}', '/')); expect(response.statusCode, HttpStatus.ok); expect(response.body, 'Hello from /'); await server.close(); }, expectAsync2((error, stack) { expect(error, isOhNoStateError); })); }); test("doesn't pass asynchronous exceptions to the root error zone", () async { var response = await Zone.root.run(() async { var server = await shelf_io.serve((request) { Future(() => throw StateError('oh no')); return syncHandler(request); }, 'localhost', 0); try { return await http.get(Uri.http('localhost:${server.port}', '/')); } finally { await server.close(); } }); expect(response.statusCode, HttpStatus.ok); expect(response.body, 'Hello from /'); }); test('a bad HTTP host request results in a 500 response', () async { await _scheduleServer(syncHandler); var socket = await Socket.connect('localhost', _serverPort); try { socket.write('GET / HTTP/1.1\r\n'); socket.write('Host: ^^super bad !@#host\r\n'); socket.write('\r\n'); } finally { await socket.close(); } expect( await utf8.decodeStream(socket), contains('500 Internal Server Error')); }); test('a bad HTTP URL request results in a 400 response', () async { await _scheduleServer(syncHandler); final socket = await Socket.connect('localhost', _serverPort); try { socket.write('GET /#/ HTTP/1.1\r\n'); socket.write('Host: localhost\r\n'); socket.write('\r\n'); } finally { await socket.close(); } expect(await utf8.decodeStream(socket), contains('400 Bad Request')); }); group('date header', () { test('is sent by default', () async { await _scheduleServer(syncHandler); // Update beforeRequest to be one second earlier. HTTP dates only have // second-level granularity and the request will likely take less than a // second. var beforeRequest = DateTime.now().subtract(const Duration(seconds: 1)); var response = await _get(); expect(response.headers, contains('date')); var responseDate = parser.parseHttpDate(response.headers['date']!); expect(responseDate.isAfter(beforeRequest), isTrue); expect(responseDate.isBefore(DateTime.now()), isTrue); }); test('defers to header in response', () async { var date = DateTime.utc(1981, 6, 5); await _scheduleServer((request) { return Response.ok('test', headers: {HttpHeaders.dateHeader: parser.formatHttpDate(date)}); }); var response = await _get(); expect(response.headers, contains('date')); var responseDate = parser.parseHttpDate(response.headers['date']!); expect(responseDate, date); }); }); group('X-Powered-By header', () { const poweredBy = 'x-powered-by'; test('defaults to "Dart with package:shelf"', () async { await _scheduleServer(syncHandler); var response = await _get(); expect( response.headers, containsPair(poweredBy, 'Dart with package:shelf'), ); }); test('defers to header in response when default', () async { await _scheduleServer((request) { return Response.ok('test', headers: {poweredBy: 'myServer'}); }); var response = await _get(); expect(response.headers, containsPair(poweredBy, 'myServer')); }); test('can be set at the server level', () async { _server = await shelf_io.serve( syncHandler, 'localhost', 0, poweredByHeader: 'ourServer', ); var response = await _get(); expect( response.headers, containsPair(poweredBy, 'ourServer'), ); }); test('defers to header in response when set at the server level', () async { _server = await shelf_io.serve( (request) { return Response.ok('test', headers: {poweredBy: 'myServer'}); }, 'localhost', 0, poweredByHeader: 'ourServer', ); var response = await _get(); expect(response.headers, containsPair(poweredBy, 'myServer')); }); test('is omitted when set to null', () async { _server = await shelf_io.serve( syncHandler, 'localhost', 0, poweredByHeader: null, ); var response = await _get(); expect( response.headers, isNot(contains(poweredBy)), ); }); }); group('chunked coding', () { group('is added when the transfer-encoding header is', () { test('unset', () async { await _scheduleServer((request) { return Response.ok(Stream.fromIterable([ [1, 2, 3, 4] ])); }); var response = await _get(); expect(response.headers, containsPair(HttpHeaders.transferEncodingHeader, 'chunked')); expect(response.bodyBytes, equals([1, 2, 3, 4])); }); test('"identity"', () async { await _scheduleServer((request) { return Response.ok( Stream.fromIterable([ [1, 2, 3, 4] ]), headers: {HttpHeaders.transferEncodingHeader: 'identity'}); }); var response = await _get(); expect(response.headers, containsPair(HttpHeaders.transferEncodingHeader, 'chunked')); expect(response.bodyBytes, equals([1, 2, 3, 4])); }); }); test('is preserved when the transfer-encoding header is "chunked"', () async { await _scheduleServer((request) { return Response.ok( Stream.fromIterable(['2\r\nhi\r\n0\r\n\r\n'.codeUnits]), headers: {HttpHeaders.transferEncodingHeader: 'chunked'}); }); var response = await _get(); expect(response.headers, containsPair(HttpHeaders.transferEncodingHeader, 'chunked')); expect(response.body, equals('hi')); }); group('is not added when', () { test('content-length is set', () async { await _scheduleServer((request) { return Response.ok( Stream.fromIterable([ [1, 2, 3, 4] ]), headers: {HttpHeaders.contentLengthHeader: '4'}); }); var response = await _get(); expect(response.headers, isNot(contains(HttpHeaders.transferEncodingHeader))); expect(response.bodyBytes, equals([1, 2, 3, 4])); }); test('status code is 1xx', () async { await _scheduleServer((request) { return Response(123, body: const Stream>.empty()); }); var response = await _get(); expect(response.headers, isNot(contains(HttpHeaders.transferEncodingHeader))); expect(response.body, isEmpty); }); test('status code is 204', () async { await _scheduleServer((request) { return Response(204, body: const Stream>.empty()); }); var response = await _get(); expect(response.headers, isNot(contains(HttpHeaders.transferEncodingHeader))); expect(response.body, isEmpty); }); test('status code is 304', () async { await _scheduleServer((request) { return Response(304, body: const Stream>.empty()); }); var response = await _get(); expect(response.headers, isNot(contains(HttpHeaders.transferEncodingHeader))); expect(response.body, isEmpty); }); }); }); test('respects the "shelf.io.buffer_output" context parameter', () async { var controller = StreamController(); await _scheduleServer((request) { controller.add('Hello, '); return Response.ok(utf8.encoder.bind(controller.stream), context: {'shelf.io.buffer_output': false}); }); var request = http.Request('GET', Uri.http('localhost:$_serverPort', '')); var response = await request.send(); var stream = StreamQueue(utf8.decoder.bind(response.stream)); var data = await stream.next; expect(data, equals('Hello, ')); controller.add('world!'); data = await stream.next; expect(data, equals('world!')); await controller.close(); expect(stream.hasNext, completion(isFalse)); }); test('includes the dart:io HttpConnectionInfo in request context', () async { await _scheduleServer((request) { expect(request.context, containsPair('shelf.io.connection_info', isA())); var connectionInfo = request.context['shelf.io.connection_info'] as HttpConnectionInfo; expect(connectionInfo.remoteAddress, equals(_server!.address)); expect(connectionInfo.localPort, equals(_server!.port)); return syncHandler(request); }); var response = await _get(); expect(response.statusCode, HttpStatus.ok); }); group('ssl tests', () { var securityContext = SecurityContext() ..setTrustedCertificatesBytes(certChainBytes) ..useCertificateChainBytes(certChainBytes) ..usePrivateKeyBytes(certKeyBytes, password: 'dartdart'); var sslClient = HttpClient(context: securityContext); Future scheduleSecureGet() => sslClient.getUrl(Uri.https('localhost:${_server!.port}', '')); test('secure sync handler returns a value to the client', () async { await _scheduleServer(syncHandler, securityContext: securityContext); var req = await scheduleSecureGet(); var response = await req.close(); expect(response.statusCode, HttpStatus.ok); expect(await response.cast>().transform(utf8.decoder).single, 'Hello from /'); }); test('secure async handler returns a value to the client', () async { await _scheduleServer(asyncHandler, securityContext: securityContext); var req = await scheduleSecureGet(); var response = await req.close(); expect(response.statusCode, HttpStatus.ok); expect(await response.cast>().transform(utf8.decoder).single, 'Hello from /'); }); }); } int get _serverPort => _server!.port; HttpServer? _server; Future _scheduleServer( Handler handler, { SecurityContext? securityContext, }) async { assert(_server == null); _server = await shelf_io.serve( handler, 'localhost', 0, securityContext: securityContext, ); } Future _get( {Map */ Object>? headers, String path = ''}) => _request((client, url) => client.getUrl(url), headers: headers, path: path); Future _head( {Map */ Object>? headers, String path = ''}) => _request((client, url) => client.headUrl(url), headers: headers, path: path); Future _request( Future Function(HttpClient, Uri) request, { Map */ Object>? headers, String path = '', }) async { // TODO: use http.Client once it supports sending/receiving multiple headers. final client = HttpClient(); try { final rq = await request(client, Uri.http('localhost:$_serverPort', path)); headers?.forEach((key, value) { rq.headers.add(key, value); }); final rs = await rq.close(); final rsHeaders = {}; rs.headers.forEach((name, values) { rsHeaders[name] = joinHeaderValues(values)!; }); return http.Response.fromStream(http.StreamedResponse( rs, rs.statusCode, headers: rsHeaders, )); } finally { client.close(force: true); } } Future _post( {Map? headers, String? body}) { var request = http.Request('POST', Uri.http('localhost:$_serverPort', '')); if (headers != null) request.headers.addAll(headers); if (body != null) request.body = body; return request.send(); }