Skip to content

Commit b40e593

Browse files
authored
Merge pull request #907 from Udhay-Adithya/url-bug-fix
fix openapi spec import for specific urls
2 parents 716fb40 + b51222e commit b40e593

File tree

3 files changed

+231
-2
lines changed

3 files changed

+231
-2
lines changed

doc/dev_guide/openapi.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# OpenAPI
2+
3+
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.
4+
5+
> Fix
6+
7+
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.
8+
9+
- [APIDash](https://drive.google.com/file/d/1CWocxCVW99-bEWkZlwInGq0JykHalv9a/view?usp=sharing) - Works without any fix
10+
- [Cat Fact API](https://drive.google.com/file/d/1ox71b3tT4Lv-9jw7zV1ronWQR_uW3K25/view?usp=drive_link) - Works with this fix
11+
- [DigitalOcean Droplet Metadata API](https://drive.google.com/file/d/1XKZXJvrwvAVm3OVBEZFhScOuCMjPJBZh/view?usp=drive_link) - Works without any fix
12+
- [GitHub v3 REST API](https://drive.google.com/file/d/1WcJXSosHPD0uiybJrqpJSknM5FA0De02/view?usp=drive_link) - Doesn't Work
13+
- [Swagger Petstore](https://drive.google.com/file/d/1LBqBrlcsXo7Clr7VKn7CYe75c_H4U8zQ/view?usp=drive_link) - Doesn't Work
14+
- [RailwayStations REST API](https://drive.google.com/file/d/1jVFk-hNf_gb_VeBuAomOgh6tWByU9Fyi/view?usp=drive_link) - Doesn't Work
15+
- [UniProt REST API Server](https://drive.google.com/file/d/1KTIqKC7SludxsyCYN6kXWQySve4GpbhD/view?usp=drive_link) - Doesn't Work
16+
- [VIT-AP VTOP API](https://drive.google.com/file/d/1B5Mh3IK2uUBoRSocEKQd2Dvf7SZWm03M/view?usp=drive_link) - Works without any fix
17+
18+
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.
19+
20+
### Findings per document
21+
22+
- cat_facts.json (also the Cat Facts URL)
23+
24+
- Problem: Top-level security is malformed: security: [[]]
25+
- Why it fails: In OpenAPI 3.0, top-level security must be an array of SecurityRequirement objects (maps). Examples:
26+
- Valid: security: [] (no requirements) or security: [ { api_key: [] } ]
27+
- Invalid: security: [[]] (array of arrays)
28+
- openapi_spec error: type 'List<dynamic>' is not a subtype of type 'Map<String, dynamic>'
29+
- Conclusion: The document is invalid. This is not a general “security field” issue, just this malformed shape.
30+
31+
- railway-stations.yaml
32+
33+
- Problem: Component parameter reference points to a Parameter missing required fields (e.g., 'in').
34+
- Error: CheckedFromJsonException: Could not create Parameter. There is a problem with "in". Invalid union type "null"!
35+
- The stack/message points at $ref: #/components/parameters/Authorization.
36+
- Conclusion: Not related to security. The referenced Parameter definition is incomplete (missing in: header|query|path|cookie) or otherwise invalid.
37+
38+
- travel.yaml
39+
40+
- Problem: Same class of failure as railway-stations.yaml, with a parameter ref like $ref: #/components/parameters/page.
41+
- Error: CheckedFromJsonException... problem with "in" (Invalid union type "null").
42+
- Note: components.securitySchemes is present here and is not the cause.
43+
- Conclusion: Also a spec issue with parameter component definitions/references.
44+
45+
- digitalocean.yaml
46+
- Result: Parses successfully with openapi_spec.
47+
- Note: No top-level security; nothing problematic here.
48+
- Conclusion: Confirms the parser handles valid documents correctly.
49+
50+
Steps to reproduce failures from local files,
51+
52+
```
53+
import 'dart:io';
54+
import 'package:openapi_spec/openapi_spec.dart';
55+
56+
void main(List<String> args) async {
57+
58+
// Pass file paths as args below.
59+
final paths = args.isNotEmpty
60+
? args
61+
: <String>[
62+
'./cat_facts.json',
63+
'./railway-stations.yaml',
64+
];
65+
66+
for (final p in paths) {
67+
stdout.writeln('\n=== Parsing: $p ===');
68+
final f = File(p);
69+
if (!await f.exists()) {
70+
stdout.writeln('Skip: file not found');
71+
continue;
72+
}
73+
74+
final content = await f.readAsString();
75+
76+
try {
77+
final spec = OpenApi.fromString(source: content, format: null);
78+
stdout.writeln('SUCCESS: title="${spec.info.title}", version="${spec.info.version}"');
79+
stdout.writeln('Paths: ${spec.paths?.length ?? 0}');
80+
} catch (e, st) {
81+
final err = e.toString();
82+
stdout.writeln('FAIL: ${err.substring(0, err.length.clamp(0, 400))}...');
83+
// Stack Trace
84+
final stStr = st.toString();
85+
if (stStr.isNotEmpty) {
86+
stdout.writeln('Stack:\n$stStr');
87+
}
88+
}
89+
}
90+
}
91+
```
92+
93+
### How to run
94+
95+
- Create a new dart project, put the openapi spec file and this script there.
96+
- Add the depndency, `dart pub add openapi_spec: ^0.15.0`
97+
- Run:
98+
- `dart run path/to/this/file`
99+
100+
### Expected outcomes
101+
102+
- `cat_facts.json`
103+
104+
- FAIL with an error like:
105+
- type 'List<dynamic>' is not a subtype of type 'Map<String, dynamic>'
106+
- This is triggered by the invalid top-level security shape: security: [[]]
107+
108+
- `railway-stations.yaml`
109+
- FAIL with an error like:
110+
- CheckedFromJsonException: Could not create `Parameter`. There is a problem with "in". Invalid union type "null"!
111+
- This points to a components/parameters reference missing required “in”.

lib/dashbot/services/openapi_import_service.dart

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,15 +132,53 @@ class OpenApiImportService {
132132

133133
/// Try to parse a JSON or YAML OpenAPI spec string.
134134
/// Returns null if parsing fails.
135+
///
136+
/// NOTE: There's a known issue with the openapi_spec package where
137+
/// security fields containing empty arrays (e.g., "security": [[]])
138+
/// cause parsing failures. This method includes a workaround.
135139
static OpenApi? tryParseSpec(String source) {
136140
try {
137-
// Let the library infer JSON/YAML
138141
return OpenApi.fromString(source: source, format: null);
139-
} catch (_) {
142+
} catch (e) {
143+
// Try workaround for security field parsing issues
144+
try {
145+
final processedSource = _removeProblematicSecurityField(source);
146+
if (processedSource != source) {
147+
return OpenApi.fromString(source: processedSource, format: null);
148+
}
149+
} catch (_) {
150+
// Workaround failed, fall through to return null
151+
}
140152
return null;
141153
}
142154
}
143155

156+
/// Removes problematic security fields that cause parsing issues.
157+
/// TODO: Remove this workaround once openapi_spec package fixes
158+
/// the issue with security fields containing empty arrays.
159+
static String _removeProblematicSecurityField(String source) {
160+
try {
161+
final spec = jsonDecode(source) as Map<String, dynamic>;
162+
163+
if (spec.containsKey('security')) {
164+
final security = spec['security'];
165+
if (security is List && _hasEmptySecurityArrays(security)) {
166+
spec.remove('security');
167+
return jsonEncode(spec);
168+
}
169+
}
170+
171+
return source;
172+
} catch (e) {
173+
throw FormatException('Failed to preprocess OpenAPI spec: $e');
174+
}
175+
}
176+
177+
/// Checks if security list contains empty arrays that cause parsing issues.
178+
static bool _hasEmptySecurityArrays(List<dynamic> security) {
179+
return security.any((item) => item is List && item.isEmpty);
180+
}
181+
144182
/// Build a single request payload from a path + method operation.
145183
/// The payload mirrors CurlImportService payload shape for reuse.
146184
static Map<String, dynamic> _payloadForOperation({

test/dashbot/services/openapi_import_service_test.dart

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,86 @@ void main() {
3131
expect(summary, contains('POST'));
3232
});
3333

34+
test('tryParseSpec handles problematic security field with empty arrays',
35+
() {
36+
const specWithEmptySecurityArray = '''
37+
{
38+
"openapi": "3.0.0",
39+
"info": {
40+
"title": "Cat Fact API",
41+
"version": "1.0.0"
42+
},
43+
"paths": {
44+
"/fact": {
45+
"get": {
46+
"responses": {
47+
"200": {
48+
"description": "Success"
49+
}
50+
}
51+
}
52+
}
53+
},
54+
"security": [[]]
55+
}''';
56+
57+
final result =
58+
OpenApiImportService.tryParseSpec(specWithEmptySecurityArray);
59+
expect(result, isNotNull);
60+
expect(result!.info.title, equals('Cat Fact API'));
61+
expect(result.info.version, equals('1.0.0'));
62+
expect(result.paths, isNotNull);
63+
expect(result.paths!.keys, contains('/fact'));
64+
});
65+
66+
test('tryParseSpec handles valid security field with actual requirements',
67+
() {
68+
const specWithRealSecurity = '''
69+
{
70+
"openapi": "3.0.0",
71+
"info": {
72+
"title": "Secured API",
73+
"version": "1.0.0"
74+
},
75+
"paths": {
76+
"/secured": {
77+
"get": {
78+
"responses": {
79+
"200": {
80+
"description": "Success"
81+
}
82+
}
83+
}
84+
}
85+
},
86+
"security": [
87+
{
88+
"api_key": []
89+
}
90+
]
91+
}''';
92+
93+
final result = OpenApiImportService.tryParseSpec(specWithRealSecurity);
94+
expect(result, isNotNull);
95+
expect(result!.info.title, equals('Secured API'));
96+
});
97+
98+
test('tryParseSpec returns null for invalid JSON', () {
99+
const invalidSpec = 'not valid json';
100+
final result = OpenApiImportService.tryParseSpec(invalidSpec);
101+
expect(result, isNull);
102+
});
103+
104+
test('tryParseSpec returns null for non-OpenAPI JSON', () {
105+
const nonOpenApiSpec = '''
106+
{
107+
"notOpenApi": true,
108+
"someField": "value"
109+
}''';
110+
final result = OpenApiImportService.tryParseSpec(nonOpenApiSpec);
111+
expect(result, isNull);
112+
});
113+
34114
test('extractSpecMeta includes endpoints & baseUrl', () {
35115
final spec = OpenApiImportService.tryParseSpec(_specJson)!;
36116
final meta = OpenApiImportService.extractSpecMeta(spec);

0 commit comments

Comments
 (0)