diff --git a/app/lib/desktop/error_page.dart b/app/lib/desktop/error_page.dart
new file mode 100644
index 0000000..8c0ab3b
--- /dev/null
+++ b/app/lib/desktop/error_page.dart
@@ -0,0 +1,34 @@
+/// A wordless page for an error the window itself loaded: Chrome would
+/// otherwise paint its own "This page isn't working" sentences over an
+/// empty error body. Only the title carries the app's name (OS-level label).
+library error_page;
+
+import 'dart:io';
+
+/// HTML page with only an icon — no words painted by the browser.
+const String errorIconPage =
+ '
'
+ 'Diet Guard '
+ '';
+
+/// Whether [request] is the window loading a page, not the app's fetch().
+bool isNavigation(HttpRequest request) =>
+ request.headers.value('sec-fetch-mode') == 'navigate';
+
+/// Replaces an error answer to a page load with [errorIconPage]. Does
+/// nothing for fetch() calls, successes, or a response already written.
+void paintErrorIcon(HttpRequest request) {
+ final response = request.response;
+ if (!isNavigation(request) || response.statusCode < 400) return;
+ try {
+ response.headers.contentType = ContentType.html;
+ response.write(errorIconPage);
+ } on Exception {
+ return; // body already sent: leave it alone
+ }
+}
diff --git a/app/lib/desktop/github_proxy.dart b/app/lib/desktop/github_proxy.dart
index 7ddec8e..54c8477 100644
--- a/app/lib/desktop/github_proxy.dart
+++ b/app/lib/desktop/github_proxy.dart
@@ -19,6 +19,7 @@ library;
import 'dart:convert';
import 'dart:io';
+import 'package:diet_guard_app/desktop/error_page.dart' show isNavigation;
import 'package:http/http.dart' as http;
/// GitHub's REST API base, which [GitHubProxy] fronts.
@@ -177,9 +178,8 @@ class GitHubProxy {
}
void _passThrough(HttpRequest request, http.Response response) {
- final isNavigate = request.headers.value('sec-fetch-mode') == 'navigate';
request.response.statusCode = response.statusCode;
- if (isNavigate) {
+ if (isNavigation(request)) {
// Top-level navigation (window load) must not paint error JSON; the app's
// own fetch() calls never send 'navigate', so they still get the body.
return;
diff --git a/app/lib/desktop/wrapper_server.dart b/app/lib/desktop/wrapper_server.dart
index 3fa9696..b4729c0 100644
--- a/app/lib/desktop/wrapper_server.dart
+++ b/app/lib/desktop/wrapper_server.dart
@@ -24,6 +24,7 @@ import 'dart:io';
import 'package:crdt_sync/crdt_sync.dart';
import 'package:diet_guard_app/desktop/content_types.dart' as content_types;
+import 'package:diet_guard_app/desktop/error_page.dart';
import 'package:diet_guard_app/desktop/github_proxy.dart';
import 'package:diet_guard_app/services/desktop_wrapper.dart';
import 'package:path/path.dart' as p;
@@ -91,6 +92,7 @@ class WrapperServer {
} on Exception {
request.response.statusCode = HttpStatus.internalServerError;
}
+ paintErrorIcon(request);
await request.response.close();
}
}
diff --git a/app/pubspec.yaml b/app/pubspec.yaml
index 10f8dfa..154d3b1 100644
--- a/app/pubspec.yaml
+++ b/app/pubspec.yaml
@@ -2,7 +2,7 @@ name: diet_guard_app
description: "Companion phone app for diet_guard: log meals on the go."
publish_to: 'none'
-version: 1.0.152+1
+version: 1.0.153+1
environment:
sdk: ^3.12.2
diff --git a/app/test/desktop/desktop_wrapper_no_text_test.dart b/app/test/desktop/desktop_wrapper_no_text_test.dart
index 75d2d4b..a33b4f9 100644
--- a/app/test/desktop/desktop_wrapper_no_text_test.dart
+++ b/app/test/desktop/desktop_wrapper_no_text_test.dart
@@ -13,6 +13,16 @@ import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:path/path.dart' as p;
+/// Removes and ', dotAll: true), '');
+ var noTags = noStyle.replaceAll(RegExp(r'<[^>]*>'), '');
+ return RegExp(r'[A-Za-z]{2,}').allMatches(noTags).map((m) => m.group(0)!).toList();
+}
+
void main() {
late Directory tempDir;
late WrapperServer server;
@@ -69,6 +79,18 @@ void main() {
expect(response.statusCode, status);
expect(response.body, isEmpty, reason: 'nothing painted in the window');
});
+
+ test('$method $path loaded as a page shows only an icon', () async {
+ final request = http.Request(method, url(path))
+ ..headers['sec-fetch-mode'] = 'navigate';
+ final response = await http.Response.fromStream(
+ await http.Client().send(request),
+ );
+ expect(response.statusCode, status);
+ expect(response.headers['content-type'], contains('text/html'));
+ expect(response.body, contains('