diff --git a/README.md b/README.md
index 2719f747..03b97b25 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,7 @@
# API Dash ⚡️
-[](https://discord.com/invite/bBeSdtJ6Ue)
-
-
-
+
+
### Please support this initiative by giving this project a Star ⭐️
@@ -138,6 +136,13 @@ API Dash can be downloaded from the links below:
- Customize various options using a dedicated Settings screen.
- Window Configuration (Size & Position) is persisted and restored on app start. (Only macOS & Windows)
+
+## Supported by ❤️
+
+
+
+
+
## Code Generators
API Dash currently supports API integration code generation for the following languages/libraries.
diff --git a/doc/dev_guide/openapi.md b/doc/dev_guide/openapi.md
new file mode 100644
index 00000000..aa9b87f8
--- /dev/null
+++ b/doc/dev_guide/openapi.md
@@ -0,0 +1,111 @@
+# OpenAPI
+
+The OpenAPI import feature was failing when trying to import specifications from URLs like https://catfact.ninja/docs?api-docs.json. The error "The fetched content does not look like a valid OpenAPI spec (JSON or YAML)" was shown even though the content was a valid OpenAPI 3.0 specification. This was caused by a bug in the openapi_spec package (v0.15.0) that cannot parse OpenAPI specs containing "security": [[]] (empty security arrays), which is valid according to the OpenAPI 3.0 specification.
+
+> Fix
+
+Added a workaround in OpenApiImportService.tryParseSpec() that detects parsing failures and automatically removes problematic security fields containing empty arrays before retrying the parse operation. This is a temporary workaround until the upstream package is fixed.
+
+- [APIDash](https://drive.google.com/file/d/1CWocxCVW99-bEWkZlwInGq0JykHalv9a/view?usp=sharing) - Works without any fix
+- [Cat Fact API](https://drive.google.com/file/d/1ox71b3tT4Lv-9jw7zV1ronWQR_uW3K25/view?usp=drive_link) - Works with this fix
+- [DigitalOcean Droplet Metadata API](https://drive.google.com/file/d/1XKZXJvrwvAVm3OVBEZFhScOuCMjPJBZh/view?usp=drive_link) - Works without any fix
+- [GitHub v3 REST API](https://drive.google.com/file/d/1WcJXSosHPD0uiybJrqpJSknM5FA0De02/view?usp=drive_link) - Doesn't Work
+- [Swagger Petstore](https://drive.google.com/file/d/1LBqBrlcsXo7Clr7VKn7CYe75c_H4U8zQ/view?usp=drive_link) - Doesn't Work
+- [RailwayStations REST API](https://drive.google.com/file/d/1jVFk-hNf_gb_VeBuAomOgh6tWByU9Fyi/view?usp=drive_link) - Doesn't Work
+- [UniProt REST API Server](https://drive.google.com/file/d/1KTIqKC7SludxsyCYN6kXWQySve4GpbhD/view?usp=drive_link) - Doesn't Work
+- [VIT-AP VTOP API](https://drive.google.com/file/d/1B5Mh3IK2uUBoRSocEKQd2Dvf7SZWm03M/view?usp=drive_link) - Works without any fix
+
+It’s not our parser that causes the issue. The failures come from the documents themselves and how the openapi_spec package (correctly) enforces OpenAPI shapes. Valid security fields work fine as per the package docs; the broken cases are due to invalid spec content.
+
+### Findings per document
+
+- cat_facts.json (also the Cat Facts URL)
+
+ - Problem: Top-level security is malformed: security: [[]]
+ - Why it fails: In OpenAPI 3.0, top-level security must be an array of SecurityRequirement objects (maps). Examples:
+ - Valid: security: [] (no requirements) or security: [ { api_key: [] } ]
+ - Invalid: security: [[]] (array of arrays)
+ - openapi_spec error: type 'List' is not a subtype of type 'Map'
+ - Conclusion: The document is invalid. This is not a general “security field” issue, just this malformed shape.
+
+- railway-stations.yaml
+
+ - Problem: Component parameter reference points to a Parameter missing required fields (e.g., 'in').
+ - Error: CheckedFromJsonException: Could not create Parameter. There is a problem with "in". Invalid union type "null"!
+ - The stack/message points at $ref: #/components/parameters/Authorization.
+ - Conclusion: Not related to security. The referenced Parameter definition is incomplete (missing in: header|query|path|cookie) or otherwise invalid.
+
+- travel.yaml
+
+ - Problem: Same class of failure as railway-stations.yaml, with a parameter ref like $ref: #/components/parameters/page.
+ - Error: CheckedFromJsonException... problem with "in" (Invalid union type "null").
+ - Note: components.securitySchemes is present here and is not the cause.
+ - Conclusion: Also a spec issue with parameter component definitions/references.
+
+- digitalocean.yaml
+ - Result: Parses successfully with openapi_spec.
+ - Note: No top-level security; nothing problematic here.
+ - Conclusion: Confirms the parser handles valid documents correctly.
+
+Steps to reproduce failures from local files,
+
+```
+import 'dart:io';
+import 'package:openapi_spec/openapi_spec.dart';
+
+void main(List args) async {
+
+ // Pass file paths as args below.
+ final paths = args.isNotEmpty
+ ? args
+ : [
+ './cat_facts.json',
+ './railway-stations.yaml',
+ ];
+
+ for (final p in paths) {
+ stdout.writeln('\n=== Parsing: $p ===');
+ final f = File(p);
+ if (!await f.exists()) {
+ stdout.writeln('Skip: file not found');
+ continue;
+ }
+
+ final content = await f.readAsString();
+
+ try {
+ final spec = OpenApi.fromString(source: content, format: null);
+ stdout.writeln('SUCCESS: title="${spec.info.title}", version="${spec.info.version}"');
+ stdout.writeln('Paths: ${spec.paths?.length ?? 0}');
+ } catch (e, st) {
+ final err = e.toString();
+ stdout.writeln('FAIL: ${err.substring(0, err.length.clamp(0, 400))}...');
+ // Stack Trace
+ final stStr = st.toString();
+ if (stStr.isNotEmpty) {
+ stdout.writeln('Stack:\n$stStr');
+ }
+ }
+ }
+}
+```
+
+### How to run
+
+- Create a new dart project, put the openapi spec file and this script there.
+- Add the depndency, `dart pub add openapi_spec: ^0.15.0`
+- Run:
+ - `dart run path/to/this/file`
+
+### Expected outcomes
+
+- `cat_facts.json`
+
+ - FAIL with an error like:
+ - type 'List' is not a subtype of type 'Map'
+ - This is triggered by the invalid top-level security shape: security: [[]]
+
+- `railway-stations.yaml`
+ - FAIL with an error like:
+ - CheckedFromJsonException: Could not create `Parameter`. There is a problem with "in". Invalid union type "null"!
+ - This points to a components/parameters reference missing required “in”.
diff --git a/lib/dashbot/services/openapi_import_service.dart b/lib/dashbot/services/openapi_import_service.dart
index 954f649d..b5bcc90b 100644
--- a/lib/dashbot/services/openapi_import_service.dart
+++ b/lib/dashbot/services/openapi_import_service.dart
@@ -132,15 +132,53 @@ class OpenApiImportService {
/// Try to parse a JSON or YAML OpenAPI spec string.
/// Returns null if parsing fails.
+ ///
+ /// NOTE: There's a known issue with the openapi_spec package where
+ /// security fields containing empty arrays (e.g., "security": [[]])
+ /// cause parsing failures. This method includes a workaround.
static OpenApi? tryParseSpec(String source) {
try {
- // Let the library infer JSON/YAML
return OpenApi.fromString(source: source, format: null);
- } catch (_) {
+ } catch (e) {
+ // Try workaround for security field parsing issues
+ try {
+ final processedSource = _removeProblematicSecurityField(source);
+ if (processedSource != source) {
+ return OpenApi.fromString(source: processedSource, format: null);
+ }
+ } catch (_) {
+ // Workaround failed, fall through to return null
+ }
return null;
}
}
+ /// Removes problematic security fields that cause parsing issues.
+ /// TODO: Remove this workaround once openapi_spec package fixes
+ /// the issue with security fields containing empty arrays.
+ static String _removeProblematicSecurityField(String source) {
+ try {
+ final spec = jsonDecode(source) as Map;
+
+ if (spec.containsKey('security')) {
+ final security = spec['security'];
+ if (security is List && _hasEmptySecurityArrays(security)) {
+ spec.remove('security');
+ return jsonEncode(spec);
+ }
+ }
+
+ return source;
+ } catch (e) {
+ throw FormatException('Failed to preprocess OpenAPI spec: $e');
+ }
+ }
+
+ /// Checks if security list contains empty arrays that cause parsing issues.
+ static bool _hasEmptySecurityArrays(List security) {
+ return security.any((item) => item is List && item.isEmpty);
+ }
+
/// Build a single request payload from a path + method operation.
/// The payload mirrors CurlImportService payload shape for reuse.
static Map _payloadForOperation({
diff --git a/lib/main.dart b/lib/main.dart
index 510c28b0..2b80ff65 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -14,7 +14,6 @@ void main() async {
await Stac.initialize();
//Load all LLMs
- // await LLMManager.fetchAvailableLLMs();
await ModelManager.fetchAvailableModels();
var settingsModel = await getSettingsFromSharedPrefs();
@@ -30,9 +29,6 @@ void main() async {
settingsModel = settingsModel?.copyWithPath(workspaceFolderPath: null);
}
- // TODO: Load all models at init
- // await ModelManager.loadAvailableLLMs();
-
runApp(
ProviderScope(
overrides: [
diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart
index 51de409b..68c45cfd 100644
--- a/lib/providers/collection_providers.dart
+++ b/lib/providers/collection_providers.dart
@@ -250,12 +250,15 @@ class CollectionStateNotifier
newModel = switch (apiType) {
APIType.rest || APIType.graphql => currentModel.copyWith(
apiType: apiType,
+ requestTabIndex: 0,
name: name ?? currentModel.name,
description: description ?? currentModel.description,
httpRequestModel: const HttpRequestModel(),
- aiRequestModel: null),
+ aiRequestModel: null,
+ ),
APIType.ai => currentModel.copyWith(
apiType: apiType,
+ requestTabIndex: 0,
name: name ?? currentModel.name,
description: description ?? currentModel.description,
httpRequestModel: null,
@@ -350,8 +353,20 @@ class CollectionStateNotifier
executionRequestModel.httpRequestModel!);
}
- // Terminal: start network log
+ // Terminal
final terminal = ref.read(terminalStateProvider.notifier);
+
+ var valRes = getValidationResult(substitutedHttpRequestModel);
+ if (valRes != null) {
+ terminal.logSystem(
+ category: 'validation',
+ message: valRes,
+ level: TerminalLevel.error,
+ );
+ ref.read(showTerminalBadgeProvider.notifier).state = true;
+ }
+
+ // Terminal: start network log
final logId = terminal.startNetwork(
apiType: executionRequestModel.apiType,
method: substitutedHttpRequestModel.method,
diff --git a/lib/providers/ui_providers.dart b/lib/providers/ui_providers.dart
index 4e5d217f..638d8e0b 100644
--- a/lib/providers/ui_providers.dart
+++ b/lib/providers/ui_providers.dart
@@ -13,6 +13,7 @@ final historyCodePaneVisibleStateProvider = StateProvider((ref) => false);
final saveDataStateProvider = StateProvider((ref) => false);
final clearDataStateProvider = StateProvider((ref) => false);
final hasUnsavedChangesProvider = StateProvider((ref) => false);
+final showTerminalBadgeProvider = StateProvider((ref) => false);
// final nameTextFieldControllerProvider =
// StateProvider.autoDispose((ref) {
diff --git a/lib/screens/dashboard.dart b/lib/screens/dashboard.dart
index 5ff7987a..21457302 100644
--- a/lib/screens/dashboard.dart
+++ b/lib/screens/dashboard.dart
@@ -76,13 +76,21 @@ class Dashboard extends ConsumerWidget {
style: Theme.of(context).textTheme.labelSmall,
),
kVSpacer10,
- IconButton(
- isSelected: railIdx == 3,
- onPressed: () {
- ref.read(navRailIndexStateProvider.notifier).state = 3;
- },
- icon: const Icon(Icons.terminal_outlined),
- selectedIcon: const Icon(Icons.terminal),
+ Badge(
+ backgroundColor: Theme.of(context).colorScheme.error,
+ isLabelVisible:
+ ref.watch(showTerminalBadgeProvider) && railIdx != 3,
+ child: IconButton(
+ isSelected: railIdx == 3,
+ onPressed: () {
+ ref.read(navRailIndexStateProvider.notifier).state =
+ 3;
+ ref.read(showTerminalBadgeProvider.notifier).state =
+ false;
+ },
+ icon: const Icon(Icons.terminal_outlined),
+ selectedIcon: const Icon(Icons.terminal),
+ ),
),
Text(
'Logs',
diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane_graphql.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane_graphql.dart
index 87ed3104..65fd7956 100644
--- a/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane_graphql.dart
+++ b/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane_graphql.dart
@@ -15,7 +15,7 @@ class EditGraphQLRequestPane extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedId = ref.watch(selectedIdStateProvider);
- var tabIndex = ref.watch(
+ final tabIndex = ref.watch(
selectedRequestModelProvider.select((value) => value?.requestTabIndex));
final codePaneVisible = ref.watch(codePaneVisibleStateProvider);
final headerLength = ref.watch(selectedRequestModelProvider
@@ -34,9 +34,6 @@ class EditGraphQLRequestPane extends ConsumerWidget {
final hasAuth = ref.watch(selectedRequestModelProvider.select((value) =>
value?.httpRequestModel?.authModel?.type != APIAuthType.none));
- if (tabIndex >= 3) {
- tabIndex = 0;
- }
return RequestPane(
selectedId: selectedId,
codePaneVisible: codePaneVisible,
diff --git a/lib/services/hive_services.dart b/lib/services/hive_services.dart
index 7ae90149..ff3f1888 100644
--- a/lib/services/hive_services.dart
+++ b/lib/services/hive_services.dart
@@ -1,5 +1,5 @@
import 'package:flutter/foundation.dart';
-import 'package:hive_flutter/hive_flutter.dart';
+import 'package:hive_ce_flutter/hive_flutter.dart';
import 'secure_credential_storage.dart';
enum HiveBoxType { normal, lazy }
diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart
index 72b57468..bca76b79 100644
--- a/lib/utils/utils.dart
+++ b/lib/utils/utils.dart
@@ -8,4 +8,5 @@ export 'http_utils.dart';
export 'js_utils.dart';
export 'save_utils.dart';
export 'ui_utils.dart';
+export 'validation_utils.dart';
export 'window_utils.dart';
diff --git a/lib/utils/validation_utils.dart b/lib/utils/validation_utils.dart
new file mode 100644
index 00000000..261faa08
--- /dev/null
+++ b/lib/utils/validation_utils.dart
@@ -0,0 +1,18 @@
+import 'package:apidash_core/apidash_core.dart';
+
+String? getValidationResult(HttpRequestModel requestModel) {
+ if (requestModel.url.trim().isEmpty) {
+ return 'Request URL is empty. Please provide a valid URL.';
+ }
+ if (requestModel.method == HTTPVerb.get && requestModel.hasAnyBody) {
+ return 'GET request contains a body. This is not supported.';
+ }
+ if (requestModel.hasJsonData) {
+ try {
+ kJsonDecoder.convert(requestModel.body!);
+ } catch (e) {
+ return 'Invalid JSON in request body: ${e.toString()}';
+ }
+ }
+ return null;
+}
diff --git a/packages/better_networking/lib/models/http_request_model.dart b/packages/better_networking/lib/models/http_request_model.dart
index 3443e3d6..51caf361 100644
--- a/packages/better_networking/lib/models/http_request_model.dart
+++ b/packages/better_networking/lib/models/http_request_model.dart
@@ -47,6 +47,10 @@ class HttpRequestModel with _$HttpRequestModel {
bool get hasTextContentType => bodyContentType == ContentType.text;
int get contentLength => utf8.encode(body ?? "").length;
bool get hasBody => hasJsonData || hasTextData || hasFormData;
+ bool get hasAnyBody =>
+ (hasJsonContentType && contentLength > 0) ||
+ (hasTextContentType && contentLength > 0) ||
+ (hasFormDataContentType && formDataMapList.isNotEmpty);
bool get hasJsonData =>
kMethodsWithBody.contains(method) &&
hasJsonContentType &&
diff --git a/pubspec.lock b/pubspec.lock
index 311796c2..1817c5b3 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -844,14 +844,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.3"
- hive_flutter:
- dependency: "direct main"
+ hive_ce:
+ dependency: transitive
description:
- name: hive_flutter
- sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc
+ name: hive_ce
+ sha256: "81d39a03c4c0ba5938260a8c3547d2e71af59defecea21793d57fc3551f0d230"
url: "https://pub.dev"
source: hosted
- version: "1.1.0"
+ version: "2.15.1"
+ hive_ce_flutter:
+ dependency: "direct main"
+ description:
+ name: hive_ce_flutter
+ sha256: "26d656c9e8974f0732f1d09020e2d7b08ba841b8961a02dbfb6caf01474b0e9a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.3"
hooks_riverpod:
dependency: "direct main"
description:
@@ -944,6 +952,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.5"
+ isolate_channel:
+ dependency: transitive
+ description:
+ name: isolate_channel
+ sha256: f3d36f783b301e6b312c3450eeb2656b0e7d1db81331af2a151d9083a3f6b18d
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.2+1"
jaspr:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 8b44c46b..00428913 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -37,7 +37,7 @@ dependencies:
fvp: ^0.32.1
highlight: ^0.7.0
highlighter: ^0.1.1
- hive_flutter: ^1.1.0
+ hive_ce_flutter: ^2.3.3
hooks_riverpod: ^2.5.2
intl: ^0.19.0
jinja: ^0.6.1
diff --git a/test/dashbot/services/openapi_import_service_test.dart b/test/dashbot/services/openapi_import_service_test.dart
index 918f7fa3..6dfdb699 100644
--- a/test/dashbot/services/openapi_import_service_test.dart
+++ b/test/dashbot/services/openapi_import_service_test.dart
@@ -31,6 +31,86 @@ void main() {
expect(summary, contains('POST'));
});
+ test('tryParseSpec handles problematic security field with empty arrays',
+ () {
+ const specWithEmptySecurityArray = '''
+{
+ "openapi": "3.0.0",
+ "info": {
+ "title": "Cat Fact API",
+ "version": "1.0.0"
+ },
+ "paths": {
+ "/fact": {
+ "get": {
+ "responses": {
+ "200": {
+ "description": "Success"
+ }
+ }
+ }
+ }
+ },
+ "security": [[]]
+}''';
+
+ final result =
+ OpenApiImportService.tryParseSpec(specWithEmptySecurityArray);
+ expect(result, isNotNull);
+ expect(result!.info.title, equals('Cat Fact API'));
+ expect(result.info.version, equals('1.0.0'));
+ expect(result.paths, isNotNull);
+ expect(result.paths!.keys, contains('/fact'));
+ });
+
+ test('tryParseSpec handles valid security field with actual requirements',
+ () {
+ const specWithRealSecurity = '''
+{
+ "openapi": "3.0.0",
+ "info": {
+ "title": "Secured API",
+ "version": "1.0.0"
+ },
+ "paths": {
+ "/secured": {
+ "get": {
+ "responses": {
+ "200": {
+ "description": "Success"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "api_key": []
+ }
+ ]
+}''';
+
+ final result = OpenApiImportService.tryParseSpec(specWithRealSecurity);
+ expect(result, isNotNull);
+ expect(result!.info.title, equals('Secured API'));
+ });
+
+ test('tryParseSpec returns null for invalid JSON', () {
+ const invalidSpec = 'not valid json';
+ final result = OpenApiImportService.tryParseSpec(invalidSpec);
+ expect(result, isNull);
+ });
+
+ test('tryParseSpec returns null for non-OpenAPI JSON', () {
+ const nonOpenApiSpec = '''
+{
+ "notOpenApi": true,
+ "someField": "value"
+}''';
+ final result = OpenApiImportService.tryParseSpec(nonOpenApiSpec);
+ expect(result, isNull);
+ });
+
test('extractSpecMeta includes endpoints & baseUrl', () {
final spec = OpenApiImportService.tryParseSpec(_specJson)!;
final meta = OpenApiImportService.extractSpecMeta(spec);