blob: 2577b4c6b430948baacd16fb5885341be4695ee3 [file] [log] [blame]
// Copyright 2019 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:meta/meta.dart';
import '../service/config.dart';
import '../service/logging.dart';
import 'body.dart';
import 'exceptions.dart';
/// A class that services HTTP requests and returns HTTP responses.
///
/// `T` is the type of object that is returned as the body of the HTTP response
/// (before serialization). Subclasses whose HTTP responses don't include a
/// body should extend `RequestHandler<Body>` and return null in their service
/// handlers ([get] and [post]).
abstract class RequestHandler<T extends Body> {
/// Creates a new [RequestHandler].
const RequestHandler({
required this.config,
});
/// The global configuration of this AppEngine server.
final Config config;
/// Services an HTTP request.
///
/// Subclasses should generally not override this method. Instead, they
/// should override one of [get] or [post], depending on which methods
/// they support.
Future<void> service(
HttpRequest request, {
Future<void> Function(HttpStatusException)? onError,
}) {
return runZoned<Future<void>>(
() async {
final HttpResponse response = request.response;
try {
try {
T body;
switch (request.method) {
case 'GET':
body = await get();
break;
case 'POST':
body = await post();
break;
default:
throw MethodNotAllowed(request.method);
}
await _respond(body: body);
httpClient?.close();
return;
} on HttpStatusException {
rethrow;
} catch (error, stackTrace) {
log.severe('$error\n$stackTrace');
throw InternalServerError('$error\n$stackTrace');
}
} on HttpStatusException catch (error) {
if (onError != null) {
await onError(error);
}
response
..statusCode = error.statusCode
..write(error.message);
await response.flush();
await response.close();
return;
}
},
zoneValues: <RequestKey<dynamic>, Object>{
RequestKey.request: request,
RequestKey.response: request.response,
RequestKey.httpClient: httpClient ?? http.Client(),
},
);
}
/// Responds (using [response]) with the specified [status] and optional
/// [body].
///
/// Returns a future that completes when [response] has been closed.
Future<void> _respond({int status = HttpStatus.ok, Body body = Body.empty}) async {
response!.statusCode = status;
await response!.addStream(body.serialize().cast<List<int>>());
await response!.flush();
await response!.close();
}
/// Gets the value associated with the specified [key] in the request
/// context.
///
/// Concrete subclasses should not call this directly. Instead, they should
/// access the getters that are tied to specific keys, such as [request]
/// and [response].
///
/// If this is called outside the context of an HTTP request, this will
/// throw a [StateError].
@protected
U? getValue<U>(RequestKey<U> key, {bool allowNull = false}) {
final U? value = Zone.current[key] as U?;
if (!allowNull && value == null) {
throw StateError('Attempt to access ${key.name} while not in a request context');
}
return value;
}
/// Gets the current [HttpRequest].
///
/// If this is called outside the context of an HTTP request, this will
/// throw a [StateError].
@protected
HttpRequest? get request => getValue<HttpRequest>(RequestKey.request);
/// Gets the current [HttpResponse].
///
/// If this is called outside the context of an HTTP request, this will
/// throw a [StateError].
@protected
HttpResponse? get response => getValue<HttpResponse>(RequestKey.response);
/// Services an HTTP GET.
///
/// Subclasses should override this method if they support GET requests.
/// The default implementation will respond with HTTP 405 method not allowed.
@protected
Future<T> get() async {
throw const MethodNotAllowed('GET');
}
/// Services an HTTP POST.
///
/// Subclasses should override this method if they support POST requests.
/// The default implementation will respond with HTTP 405 method not allowed.
@protected
Future<T> post() async {
throw const MethodNotAllowed('POST');
}
/// The package:http Client to use for googleapis requests.
@protected
http.Client? get httpClient => getValue<http.Client>(
RequestKey.httpClient,
allowNull: true,
);
}
/// A key that can be used to index a value within the request context.
///
/// Subclasses will only need to deal directly with this class if they add
/// their own request context values.
@protected
class RequestKey<T> {
const RequestKey(this.name);
final String name;
static const RequestKey<HttpRequest> request = RequestKey<HttpRequest>('request');
static const RequestKey<HttpResponse> response = RequestKey<HttpResponse>('response');
static const RequestKey<http.Client> httpClient = RequestKey<http.Client>('httpClient');
@override
String toString() => '$runtimeType($name)';
}