Repository: cfug/dio

Issue:
Conditionally allows relative path for `baseUrl`
### Package

dio

### Version

5.4.0

### Operating-System

Web

### Output of `flutter doctor -v`

_No response_

### Dart Version

3.1.5

### Steps to Reproduce

Run the following snippet with `dart run`:
```dart
void main() {
  final dio = Dio(BaseOptions(baseUrl: 'api/'));
}
```

### Expected Result

A new Dio instance is created with the given `baseUrl` and can be used as usual.
It is a normal situation for web apps to separate their API under a separate endpoint group: `https://example.com/api/...`.

### Actual Result

The code above produces the following Assertion Error:
```
Unhandled exception:
'package:dio/src/options.dart': Failed assertion: line 145 pos 16: 'baseUrl.isEmpty || Uri.parse(baseUrl).host.isNotEmpty': is not true.
#0      _AssertionError._doThrowNew (dart:core-patch/errors_patch.dart:51:61)
#1      _AssertionError._throwNew (dart:core-patch/errors_patch.dart:40:5)
#2      new BaseOptions (package:dio/src/options.dart:145:16)
#3      main (package:anthill_front/src/shared/utils/http_client.dart:8:19)
#4      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:296:19)
#5      _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:189:12)

Process finished with exit code 255
```

The exception can be avoided if one changes the `baseUrl` in the BaseOptions after it was created:
```dart
final dio = Dio(BaseOptions()..baseUrl = 'api/');
```
Which is quite strange and not expected solution.


Discussion: You should write `https://example.com/api/` for `baseUrl`.
> You should write `https://example.com/api/` for `baseUrl`.

The frontend does not necessarily know the full path. 
It's not clear to me, why using a relative path isn't allowed.
How do the instance be able to make requests if the full path is unavailable?
Web applications can request the same origin using relative paths.
Take a look at a [W3Schools reference](https://www.w3schools.com/html/html_filepaths.asp)
They also suggest to use relative paths when possible
Dio won't know your origin unless you define so. It's not a Web application.

Closing as intended and won't fix.
Not so sure, this may actually be a valid request for Flutter Web platform.
The problem is not that Dio doesn't know something, but it just stops users from using relative paths with an assert.

You cannot as that it is not a web app, because Dio can indeed be used as a client in a web application. 

As for me, it's quite strange to forbid the usage of something that is a part of the specification for no reason, especially when the actual request functionality works and the problem is in an unnecessary assert statement.

Or is there a reason why it is forbidden?
> Not so sure, this may actually be a valid request for Flutter Web platform.

It could be possible to resolve by `Uri.base.resolve(path)` specifically on Web.
https://api.flutter.dev/flutter/dart-core/Uri/base.html

However, it's also available when you're setting the `baseUrl`.
Yes, if you look at the initial issue, it is possible to bypass the assert using the setter for baseUrl after the BaseOptions is created.

But if it's so, why the asset is there in the first place?
> You cannot as that it is not a web app, because Dio can indeed be used as a client in a web application.

I have the opposite opinion here. Dio is not in any form, an application. As you declared, it can only be used in a Web application.

> As for me, it's quite strange to forbid the usage of something that is a part of the specification for no reason, especially when the actual request functionality works and the problem is in an unnecessary assert statement.

Could you provide a reproducible example that it can run without the assertion?

> But if it's so, why the asset is there in the first place?

You can use the blame view to determine when it's been introduced to find out the cause.
> Could you provide a reproducible example that it can run without the assertion?

I created a [repo with the minimal reproducible example](https://github.com/mitryp/dio_assert_demo) of both the exception and the supposed behaviour. Its README describes the steps to reproduce both situations.

> You can use the blame view to determine when it's been introduced to find out the cause.

[diox/#18](https://github.com/cfug/diox/pull/18) has an issue of #1373 connected, but it's still unclear why `baseUrl` must include the host in the first place.



Fix patch:
diff --git a/dio/CHANGELOG.md b/dio/CHANGELOG.md
index b4913ed83..3167d7386 100644
--- a/dio/CHANGELOG.md
+++ b/dio/CHANGELOG.md
@@ -9,6 +9,7 @@ See the [Migration Guide][] for the complete breaking changes list.**
 - Fix `receiveTimeout` for streamed responses.
 - Fix cancellation for streamed responses and downloads when using `IOHttpClientAdapter`.
 - Fix receive progress for streamed responses and downloads when using `IOHttpClientAdapter`.
+- Support relative `baseUrl` on the Web platform.
 
 ## 5.4.0
 
diff --git a/dio/lib/src/options.dart b/dio/lib/src/options.dart
index 8a816ae98..aa8d2f96d 100644
--- a/dio/lib/src/options.dart
+++ b/dio/lib/src/options.dart
@@ -92,8 +92,16 @@ typedef RequestEncoder = FutureOr<List<int>> Function(
 
 /// The mixin class for options that provides common attributes.
 mixin OptionsMixin {
-  /// Request base url, it can contain sub paths like: https://pub.dev/api/.
-  late String baseUrl;
+  /// Request base url, it can be multiple forms:
+  ///  - Contains sub paths, e.g. `https://pub.dev/api/`.
+  ///  - Relative path on Web, e.g. `api/`.
+  String get baseUrl => _baseUrl;
+  late String _baseUrl;
+
+  set baseUrl(String value) {
+    assert(value.isEmpty || kIsWeb || Uri.parse(value).host.isNotEmpty);
+    _baseUrl = value;
+  }
 
   /// Common query parameters.
   ///
@@ -141,9 +149,7 @@ class BaseOptions extends _RequestConfig with OptionsMixin {
     RequestEncoder? requestEncoder,
     ResponseDecoder? responseDecoder,
     ListFormat? listFormat,
-  })  : assert(connectTimeout == null || !connectTimeout.isNegative),
-        assert(baseUrl.isEmpty || Uri.parse(baseUrl).host.isNotEmpty),
-        super(
+  }) : super(
           method: method,
           receiveTimeout: receiveTimeout,
           sendTimeout: sendTimeout,
@@ -161,8 +167,8 @@ class BaseOptions extends _RequestConfig with OptionsMixin {
           responseDecoder: responseDecoder,
           listFormat: listFormat,
         ) {
-    this.queryParameters = queryParameters ?? {};
     this.baseUrl = baseUrl;
+    this.queryParameters = queryParameters ?? {};
     this.connectTimeout = connectTimeout;
   }
 
diff --git a/dio/lib/src/utils.dart b/dio/lib/src/utils.dart
index 0073bd7b0..04f7e44c1 100644
--- a/dio/lib/src/utils.dart
+++ b/dio/lib/src/utils.dart
@@ -6,6 +6,11 @@ import 'dart:developer' as dev;
 import 'options.dart';
 import 'parameter.dart';
 
+// See https://github.com/flutter/flutter/pull/112122.
+const kIsWeb = bool.hasEnvironment('dart.library.js_util')
+    ? bool.fromEnvironment('dart.library.js_util')
+    : identical(0, 0.0);
+
 // For the web platform, an inline `bool.fromEnvironment` translates to
 // `core.bool.fromEnvironment` instead of correctly being replaced by the
 // constant value found in the environment at build time.


Test patch:
diff --git a/dio/test/options_test.dart b/dio/test/options_test.dart
index f7d7a8464..159e597ed 100644
--- a/dio/test/options_test.dart
+++ b/dio/test/options_test.dart
@@ -470,18 +470,23 @@ void main() {
   });
 
   test('option invalid base url', () {
-    final opt1 = 'blob:http://localhost/xyz123';
-    final opt2 = 'https://pub.dev';
-    final opt3 = 'https://';
-    final opt4 = 'https://loremipsum/';
-    final opt5 = '';
-    final opt6 = 'pub.dev';
-    expect(Uri.parse(opt1).host.isNotEmpty, false);
-    expect(Uri.parse(opt2).host.isNotEmpty, true);
-    expect(Uri.parse(opt3).host.isNotEmpty, false);
-    expect(Uri.parse(opt4).host.isNotEmpty, true);
-    expect(Uri.parse(opt5).host.isNotEmpty, false);
-    expect(Uri.parse(opt6).host.isNotEmpty, false);
+    final invalidUrls = <String>[
+      'blob:http://localhost/xyz123',
+      'https://',
+      'pub.dev',
+    ];
+    final validUrls = <String>[
+      '',
+      'https://pub.dev',
+      'https://loremipsum/',
+      if (kIsWeb) 'api/',
+    ];
+    for (final url in invalidUrls) {
+      expect(() => BaseOptions(baseUrl: url), throwsA(isA<AssertionError>()));
+    }
+    for (final url in validUrls) {
+      expect(BaseOptions(baseUrl: url), isA<BaseOptions>());
+    }
   });
 
   test('Throws when using invalid methods', () async {