diff --git a/frontend/lib/navigation.dart b/frontend/lib/navigation.dart index 3ce61de..0333178 100644 --- a/frontend/lib/navigation.dart +++ b/frontend/lib/navigation.dart @@ -374,8 +374,15 @@ void _onDestinationSelected(_NavDest destination, BuildContext context) { // Opens an external link in a new tab. Origin-relative paths (e.g. the API docs) // resolve against the current host, since the backend serves both the front-end // and the API docs from the same origin. +// Resolves an external link against [base] (defaulting to the current page). +// Origin-relative paths (e.g. "/api/docs") gain the current scheme and host so +// the resulting URI is launchable; absolute URLs are returned unchanged. +// Without this, launching a scheme-less URI fails when the app is served from +// the backend (e.g. Docker). +Uri resolveExternalUri(String url, {Uri? base}) => (base ?? Uri.base).resolve(url); + Future _openExternal(String url) async { - final uri = Uri.parse(url); + final uri = resolveExternalUri(url); if (await canLaunchUrl(uri)) { await launchUrl(uri, mode: LaunchMode.externalApplication); } diff --git a/frontend/test/unit/resolve_external_uri_test.dart b/frontend/test/unit/resolve_external_uri_test.dart new file mode 100644 index 0000000..26e4863 --- /dev/null +++ b/frontend/test/unit/resolve_external_uri_test.dart @@ -0,0 +1,28 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:frontend/navigation.dart'; + +void main() { + final origin = Uri.parse('http://192.168.1.10:8080/web/devices'); + + test('resolves an origin-relative path against the current host', () { + expect( + resolveExternalUri('/api/docs', base: origin), + Uri.parse('http://192.168.1.10:8080/api/docs'), + ); + }); + + test('preserves the scheme and host of an https origin', () { + final secure = Uri.parse('https://example.com/web/'); + expect( + resolveExternalUri('/api/docs', base: secure), + Uri.parse('https://example.com/api/docs'), + ); + }); + + test('returns an absolute URL unchanged', () { + expect( + resolveExternalUri('https://docs.example.com/spec', base: origin), + Uri.parse('https://docs.example.com/spec'), + ); + }); +}