| // 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)'; |
| } |