From a10ee7fa37a33cf3a874939cb611e479cae9aeba Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Sun, 13 Oct 2024 12:54:18 -0500 Subject: [PATCH] [web_app] Minor clean up to API and HTTP related files --- pkg/web_app/lib/src/api_client/_rpc.dart | 79 ++++++++----------- .../lib/src/api_client/api_client.dart | 13 +-- pkg/web_app/lib/src/deferred/http.dart | 50 ++++-------- pkg/web_app/lib/src/deferred/markdown.dart | 2 +- 4 files changed, 57 insertions(+), 87 deletions(-) diff --git a/pkg/web_app/lib/src/api_client/_rpc.dart b/pkg/web_app/lib/src/api_client/_rpc.dart index 083d904768..cc461534e0 100644 --- a/pkg/web_app/lib/src/api_client/_rpc.dart +++ b/pkg/web_app/lib/src/api_client/_rpc.dart @@ -4,6 +4,7 @@ import 'dart:async'; import 'dart:html'; + import 'package:_pub_shared/pubapi.dart'; import '../_dom_helper.dart'; @@ -17,7 +18,7 @@ Future rpc({ /// The async RPC call. If this throws, the error will be displayed as a modal /// popup, and then it will be re-thrown (or `onError` will be called). - Future Function()? fn, + required Future Function() fn, /// Message to show when the RPC returns without exceptions. required Element? successMessage, @@ -37,12 +38,12 @@ Future rpc({ return null; } - // capture keys + // Capture key down events. final keyDownSubscription = window.onKeyDown.listen((event) { event.preventDefault(); event.stopPropagation(); }); - // disable inputs and buttons that are not already disabled + // Disable inputs and buttons that are not already disabled. final inputs = document .querySelectorAll('input') .cast() @@ -67,22 +68,21 @@ Future rpc({ } R? result; - Exception? error; - String? errorMessage; + ({Exception exception, String message})? error; try { - result = await fn!(); + result = await fn(); } on RequestException catch (e) { final asJson = e.bodyAsJson(); if (e.status == 401 && asJson.containsKey('go')) { - final location = e.bodyAsJson()['go'] as String; + final location = asJson['go'] as String; final locationUri = Uri.tryParse(location); if (locationUri != null && locationUri.toString().isNotEmpty) { await cancelSpinner(); - final errorObject = e.bodyAsJson()['error'] as Map?; + final errorObject = asJson['error'] as Map?; final message = errorObject?['message']; await modalMessage( 'Further consent needed.', - Element.p() + ParagraphElement() ..text = [ if (message != null) message, 'You will be redirected, please authorize the action.', @@ -101,21 +101,25 @@ Future rpc({ return null; } } - error = e; - errorMessage = _requestExceptionMessage(e) ?? 'Unexpected error: $e'; + error = ( + exception: e, + message: _requestExceptionMessage(asJson) ?? 'Unexpected error: $e' + ); } catch (e) { - error = Exception('Unexpected error: $e'); - errorMessage = 'Unexpected error: $e'; + error = ( + exception: Exception('Unexpected error: $e'), + message: 'Unexpected error: $e' + ); } finally { await cancelSpinner(); } if (error != null) { - await modalMessage('Error', await markdown(errorMessage!)); + await modalMessage('Error', await markdown(error.message)); if (onError != null) { - return await onError(error); + return await onError(error.exception); } else { - throw error; + throw error.exception; } } @@ -128,37 +132,18 @@ Future rpc({ return result; } -String? _requestExceptionMessage(RequestException e) { - try { - final map = e.bodyAsJson(); - String? message; - - if (map['error'] is Map) { - final errorMap = map['error'] as Map; - if (errorMap['message'] is String) { - message = errorMap['message'] as String; - } - } - - // TODO: remove after the server is migrated to returns only `{'error': {'message': 'XX'}}` - if (message == null && map['message'] is String) { - message = map['message'] as String; - } - - // TODO: check if we ever send responses like this and remove if not - if (message == null && map['error'] is String) { - message = map['error'] as String; - } - - return message; - } on FormatException catch (_) { - // ignore bad body - } - return null; -} - -Element _createSpinner() => Element.div() +String? _requestExceptionMessage(Map jsonBody) => + switch (jsonBody) { + {'error': {'message': final String errorMessage}} => errorMessage, + // TODO: Remove after the server is migrated to return only `{'error': {'message': 'XX'}}`. + {'message': final String errorMessage} => errorMessage, + // TODO: Check if we ever send responses like this and remove if not. + {'error': final String errorMessage} => errorMessage, + _ => null, + }; + +Element _createSpinner() => DivElement() ..className = 'spinner-frame' ..children = [ - Element.div()..className = 'spinner', + DivElement()..className = 'spinner', ]; diff --git a/pkg/web_app/lib/src/api_client/api_client.dart b/pkg/web_app/lib/src/api_client/api_client.dart index 25bba236a2..7fb01272a9 100644 --- a/pkg/web_app/lib/src/api_client/api_client.dart +++ b/pkg/web_app/lib/src/api_client/api_client.dart @@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:html'; + import 'package:_pub_shared/pubapi.dart'; import 'package:api_builder/_client_utils.dart'; @@ -24,18 +25,18 @@ PubApiClient get unauthenticatedClient => PubApiClient(_baseUrl, client: http.Client()); /// The pub API client to use with account credentials. -PubApiClient get client { - return PubApiClient(_baseUrl, client: http.createClientWithCsrf()); -} +PubApiClient get client => + PubApiClient(_baseUrl, client: http.createClientWithCsrf()); -/// Sends a JSON request to the [path] endpoint using [verb] method with [body] content. +/// Sends a JSON request to the [path] endpoint using +/// [verb] method with [body] content. /// /// Sets the `Content-Type` header to `application/json; charset="utf-8` and /// expects a valid JSON response body. -Future> sendJson({ +Future> sendJson({ required String verb, required String path, - required Map? body, + required Map? body, }) async { final client = http.createClientWithCsrf(); try { diff --git a/pkg/web_app/lib/src/deferred/http.dart b/pkg/web_app/lib/src/deferred/http.dart index 9b928e765b..7ca251c760 100644 --- a/pkg/web_app/lib/src/deferred/http.dart +++ b/pkg/web_app/lib/src/deferred/http.dart @@ -4,60 +4,42 @@ import 'dart:html'; +import 'package:collection/collection.dart' show IterableExtension; import 'package:http/browser_client.dart'; import 'package:http/http.dart'; export 'package:http/http.dart'; /// Creates an authenticated [Client] that extends request with the CSRF header. -Client createClientWithCsrf() { - return _AuthenticatedClient(); -} +Client createClientWithCsrf() => _AuthenticatedClient(); -String? _getCsrfMetaContent() { - final values = document.head - ?.querySelectorAll('meta[name="csrf-token"]') - .map((e) => e.attributes['content']) - .nonNulls - .toList(); - if (values == null || values.isEmpty) { - return null; - } - return values.first.trim(); -} +String? get _csrfMetaContent => document.head + ?.querySelectorAll('meta[name="csrf-token"]') + .map((e) => e.getAttribute('content')) + .firstWhereOrNull((tokenContent) => tokenContent != null) + ?.trim(); /// An HTTP [Client] which sends custom headers alongside each request: /// /// - `x-pub-csrf-token` header when the HTML document's `` contains the /// `` element. class _AuthenticatedClient extends _BrowserClient { - _AuthenticatedClient() - : super( - getHeadersFn: () async { - final csrfToken = _getCsrfMetaContent(); - return { - if (csrfToken != null && csrfToken.isNotEmpty) - 'x-pub-csrf-token': csrfToken, - }; - }, - ); + @override + Future> get _sendHeaders async => { + if (_csrfMetaContent case final csrfToken? when csrfToken.isNotEmpty) + 'x-pub-csrf-token': csrfToken, + }; } /// An [Client] which updates the headers for each request. -class _BrowserClient extends BrowserClient { - final Future> Function() getHeadersFn; - final _client = BrowserClient(); - _BrowserClient({ - required this.getHeadersFn, - }); +abstract class _BrowserClient extends BrowserClient { + final BrowserClient _client = BrowserClient(); @override Future send(BaseRequest request) async { - final headers = await getHeadersFn(); - final modifiedRequest = _RequestImpl.fromRequest( request, - updateHeaders: headers, + updateHeaders: await _sendHeaders, ); return await _client.send(modifiedRequest); @@ -67,6 +49,8 @@ class _BrowserClient extends BrowserClient { void close() { _client.close(); } + + Future> get _sendHeaders; } class _RequestImpl extends BaseRequest { diff --git a/pkg/web_app/lib/src/deferred/markdown.dart b/pkg/web_app/lib/src/deferred/markdown.dart index 6bc8d603c7..0b24e5e0d8 100644 --- a/pkg/web_app/lib/src/deferred/markdown.dart +++ b/pkg/web_app/lib/src/deferred/markdown.dart @@ -8,7 +8,7 @@ import 'package:markdown/markdown.dart' as md; /// Creates an [Element] with Markdown-formatted content. Future markdown(String text) async { - return Element.div() + return DivElement() ..setInnerHtml( md.markdownToHtml(text), validator: NodeValidator(uriPolicy: _UnsafeUriPolicy()),