Add a poweredByHeader argument to server (#272)

Fixes #270

Allow passing `null` to omit the header.
diff --git a/pkgs/shelf/CHANGELOG.md b/pkgs/shelf/CHANGELOG.md
index 357fc6f..a69299d 100644
--- a/pkgs/shelf/CHANGELOG.md
+++ b/pkgs/shelf/CHANGELOG.md
@@ -1,6 +1,8 @@
 ## 1.4.0-dev
 
 * Add Response.unauthorized() constructor
+* Add `poweredByHeader` argument to `serve`, `serveRequests`, and
+  `handleRequest`.
 
 ## 1.3.2
 
diff --git a/pkgs/shelf/lib/shelf_io.dart b/pkgs/shelf/lib/shelf_io.dart
index 3387b74..062ade7 100644
--- a/pkgs/shelf/lib/shelf_io.dart
+++ b/pkgs/shelf/lib/shelf_io.dart
@@ -39,6 +39,14 @@
 ///
 /// See the documentation for [HttpServer.bind] and [HttpServer.bindSecure]
 /// for more details on [address], [port], [backlog], and [shared].
+///
+/// {@template shelf_io_header_defaults}
+/// Every response will get a "date" header and an "X-Powered-By" header.
+/// If the either header is present in the `Response`, it will not be
+/// overwritten.
+/// Pass [poweredByHeader] to set the default content for "X-Powered-By",
+/// pass `null` to omit this header.
+/// {@endtemplate}
 Future<HttpServer> serve(
   Handler handler,
   Object address,
@@ -46,6 +54,7 @@
   SecurityContext? securityContext,
   int? backlog,
   bool shared = false,
+  String? poweredByHeader = 'Dart with package:shelf',
 }) async {
   backlog ??= 0;
   var server = await (securityContext == null
@@ -57,7 +66,7 @@
           backlog: backlog,
           shared: shared,
         ));
-  serveRequests(server, handler);
+  serveRequests(server, handler, poweredByHeader: poweredByHeader);
   return server;
 }
 
@@ -70,9 +79,16 @@
 /// console and cause a 500 response with no body. Errors thrown asynchronously
 /// by [handler] will be printed to the console or, if there's an active error
 /// zone, passed to that zone.
-void serveRequests(Stream<HttpRequest> requests, Handler handler) {
+///
+/// {@macro shelf_io_header_defaults}
+void serveRequests(
+  Stream<HttpRequest> requests,
+  Handler handler, {
+  String? poweredByHeader = 'Dart with package:shelf',
+}) {
   catchTopLevelErrors(() {
-    requests.listen((request) => handleRequest(request, handler));
+    requests.listen((request) =>
+        handleRequest(request, handler, poweredByHeader: poweredByHeader));
   }, (error, stackTrace) {
     _logTopLevelError('Asynchronous error\n$error', stackTrace);
   });
@@ -81,7 +97,13 @@
 /// Uses [handler] to handle [request].
 ///
 /// Returns a [Future] which completes when the request has been handled.
-Future<void> handleRequest(HttpRequest request, Handler handler) async {
+///
+/// {@macro shelf_io_header_defaults}
+Future<void> handleRequest(
+  HttpRequest request,
+  Handler handler, {
+  String? poweredByHeader = 'Dart with package:shelf',
+}) async {
   Request shelfRequest;
   try {
     shelfRequest = _fromHttpRequest(request);
@@ -94,17 +116,17 @@
         body: 'Bad Request',
         headers: {HttpHeaders.contentTypeHeader: 'text/plain'},
       );
-      await _writeResponse(response, request.response);
+      await _writeResponse(response, request.response, poweredByHeader);
     } else {
       _logTopLevelError('Error parsing request.\n$error', stackTrace);
       final response = Response.internalServerError();
-      await _writeResponse(response, request.response);
+      await _writeResponse(response, request.response, poweredByHeader);
     }
     return;
   } catch (error, stackTrace) {
     _logTopLevelError('Error parsing request.\n$error', stackTrace);
     final response = Response.internalServerError();
-    await _writeResponse(response, request.response);
+    await _writeResponse(response, request.response, poweredByHeader);
     return;
   }
 
@@ -136,11 +158,12 @@
     await _writeResponse(
         _logError(
             shelfRequest, 'null response from handler.', StackTrace.current),
-        request.response);
+        request.response,
+        poweredByHeader);
     return;
   }
   if (shelfRequest.canHijack) {
-    await _writeResponse(response, request.response);
+    await _writeResponse(response, request.response, poweredByHeader);
     return;
   }
 
@@ -179,7 +202,8 @@
   );
 }
 
-Future<void> _writeResponse(Response response, HttpResponse httpResponse) {
+Future<void> _writeResponse(
+    Response response, HttpResponse httpResponse, String? poweredByHeader) {
   if (response.context.containsKey('shelf.io.buffer_output')) {
     httpResponse.bufferOutput =
         response.context['shelf.io.buffer_output'] as bool;
@@ -217,9 +241,9 @@
     httpResponse.headers.set(HttpHeaders.transferEncodingHeader, 'chunked');
   }
 
-  if (!response.headers.containsKey(_xPoweredByResponseHeader)) {
-    httpResponse.headers
-        .set(_xPoweredByResponseHeader, 'Dart with package:shelf');
+  if (poweredByHeader != null &&
+      !response.headers.containsKey(_xPoweredByResponseHeader)) {
+    httpResponse.headers.set(_xPoweredByResponseHeader, poweredByHeader);
   }
 
   if (!response.headers.containsKey(HttpHeaders.dateHeader)) {
diff --git a/pkgs/shelf/test/shelf_io_test.dart b/pkgs/shelf/test/shelf_io_test.dart
index 0887585..4175f54 100644
--- a/pkgs/shelf/test/shelf_io_test.dart
+++ b/pkgs/shelf/test/shelf_io_test.dart
@@ -365,7 +365,7 @@
       );
     });
 
-    test('defers to header in response', () async {
+    test('defers to header in response when default', () async {
       await _scheduleServer((request) {
         return Response.ok('test', headers: {poweredBy: 'myServer'});
       });
@@ -373,6 +373,49 @@
       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', () {