mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-15 08:30:57 +01:00
Page loads of errors show an icon page
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
91a9ee5295
commit
8068205d0c
34
app/lib/desktop/error_page.dart
Normal file
34
app/lib/desktop/error_page.dart
Normal file
@ -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 =
|
||||
'<!doctype html><html><head><meta charset="utf-8"> '
|
||||
'<title>Diet Guard</title><style>html,body{margin:0;height:100%;'
|
||||
'background:#211D1B;display:flex;align-items:center;'
|
||||
'justify-content:center}</style></head><body> '
|
||||
'<svg width="96" height="96" viewBox="0 0 24 24" fill="none" '
|
||||
' stroke="#A39892" stroke-width="2" stroke-linecap="round" '
|
||||
' aria-hidden="true"><circle cx="12" cy="12" r="9"/>'
|
||||
'<line x1="5.6" y1="18.4" x2="18.4" y2="5.6"/></svg></body></html>';
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -13,6 +13,16 @@ import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
/// Removes <title> and <style> tags, strips all remaining HTML tags, and
|
||||
/// returns every [A-Za-z]{2,} match — i.e. all visible words the browser would
|
||||
/// paint for a top-level navigation.
|
||||
List<String> paintedWords(String html) {
|
||||
var noTitle = html.replaceAll(RegExp(r'<title>.*?</title>', dotAll: true), '');
|
||||
var noStyle = noTitle.replaceAll(RegExp(r'<style>.*?</style>', 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('<svg'));
|
||||
expect(paintedWords(response.body), isEmpty);
|
||||
});
|
||||
}
|
||||
|
||||
group('proxy error responses', () {
|
||||
@ -105,14 +127,16 @@ void main() {
|
||||
|
||||
Uri url(String path) => Uri.parse('http://localhost:${server.port}$path');
|
||||
|
||||
test('a proxy error loaded as a page has an empty body', () async {
|
||||
test('a proxy error loaded as a page shows only an icon', () async {
|
||||
final request = http.Request('GET', url('/github/api/repos/o/r/contents/x'))
|
||||
..headers['sec-fetch-mode'] = 'navigate';
|
||||
final response = await http.Response.fromStream(
|
||||
await http.Client().send(request),
|
||||
);
|
||||
expect(response.statusCode, HttpStatus.unauthorized);
|
||||
expect(response.body, isEmpty, reason: 'nothing painted in the window');
|
||||
expect(response.headers['content-type'], contains('text/html'));
|
||||
expect(response.body, contains('<svg'));
|
||||
expect(paintedWords(response.body), isEmpty);
|
||||
});
|
||||
|
||||
test('the app fetch still gets the proxy error body', () async {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user