blob: 95dc355562341cbab81f716af11186c0107c24e1 [file] [log] [blame]
// 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.
library pub.barback.base_server;
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:barback/barback.dart';
import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:stack_trace/stack_trace.dart';
import '../log.dart' as log;
import '../utils.dart';
import 'asset_environment.dart';
/// Base class for a pub-controlled server.
abstract class BaseServer<T> {
/// The [BuildEnvironment] being served.
final AssetEnvironment environment;
/// The underlying HTTP server.
final HttpServer _server;
/// The server's port.
int get port => _server.port;
/// The servers's address.
InternetAddress get address => _server.address;
/// The server's base URL.
Uri get url => baseUrlForAddress(_server.address, port);
/// The results of requests handled by the server.
///
/// These can be used to provide visual feedback for the server's processing.
/// This stream is also used to emit any programmatic errors that occur in the
/// server.
Stream<T> get results => _resultsController.stream;
final _resultsController = new StreamController<T>.broadcast();
BaseServer(this.environment, this._server) {
shelf_io.serveRequests(Chain.track(_server), const shelf.Pipeline()
.addMiddleware(shelf.createMiddleware(errorHandler: _handleError))
.addMiddleware(shelf.createMiddleware(responseHandler: _disableGzip))
.addHandler(handleRequest));
}
/// Closes this server.
Future close() {
return Future.wait([_server.close(), _resultsController.close()]);
}
/// Handles an HTTP request.
handleRequest(shelf.Request request);
/// Returns a 405 response to [request].
shelf.Response methodNotAllowed(shelf.Request request) {
logRequest(request, "405 Method Not Allowed");
return new shelf.Response(405,
body: "The ${request.method} method is not allowed for ${request.url}.",
headers: {'Allow': 'GET, HEAD'});
}
/// Returns a 404 response to [request].
///
/// If [asset] is given, it is the ID of the asset that couldn't be found.
shelf.Response notFound(shelf.Request request, {String error,
AssetId asset}) {
logRequest(request, "Not Found");
// TODO(rnystrom): Apply some styling to make it visually clear that this
// error is coming from pub serve itself.
var body = new StringBuffer();
body.writeln("""
<!DOCTYPE html>
<head>
<title>404 Not Found</title>
</head>
<body>
<h1>404 Not Found</h1>""");
if (asset != null) {
body.writeln("<p>Could not find asset "
"<code>${HTML_ESCAPE.convert(asset.path)}</code> in package "
"<code>${HTML_ESCAPE.convert(asset.package)}</code>.</p>");
}
if (error != null) {
body.writeln("<p>Error: ${HTML_ESCAPE.convert(error)}</p>");
}
body.writeln("""
</body>""");
// Force a UTF-8 encoding so that error messages in non-English locales are
// sent correctly.
return new shelf.Response.notFound(body.toString(),
headers: {'Content-Type': 'text/html; charset=utf-8'});
}
/// Log [message] at [log.Level.FINE] with metadata about [request].
void logRequest(shelf.Request request, String message) =>
log.fine("$this ${request.method} ${request.url}\n$message");
/// Adds [result] to the server's [results] stream.
void addResult(T result) {
_resultsController.add(result);
}
/// Adds [error] as an error to the server's [results] stream.
void addError(error, [stackTrace]) {
_resultsController.addError(error, stackTrace);
}
/// Handles an error thrown by [handleRequest].
_handleError(error, StackTrace stackTrace) {
_resultsController.addError(error, stackTrace);
close();
return new shelf.Response.internalServerError();
}
/// Disable GZIP responses.
///
/// This is primarily to optimize pub's startup. Since the transformer
/// plug-ins are loaded over HTTP, we pay the hit to GZIP encode and decode
/// them. Disabling this improves startup time by about 5% on my test.
///
// TODO(rnystrom): Remove this when #5187 is fixed and we don't have to use
// HTTP for isolates.
_disableGzip(shelf.Response response) {
if (!response.headers.containsKey('Content-Encoding')) {
return response.change(headers: {'Content-Encoding': ''});
}
return response;
}
}