Skip to content

Commit 4bcbd51

Browse files
authoredApr 9, 2025··
fix: relative references in subdirectory documents are not loading #1674 (#2243)
* Fix: relative references in subdirectory documents are not loading #1674 Use OpenApiDocuments BaseUri as location of the document. This allows to have during loading further documents a base Url for retrieval, which can be combined with a relative Uri to get an absolute. * PR feedback: remove unnecessary variable * Fix loading on linux
1 parent 3604382 commit 4bcbd51

24 files changed

+241
-73
lines changed
 

‎src/Microsoft.OpenApi.YamlReader/OpenApiYamlReader.cs

+7-5
Original file line numberDiff line numberDiff line change
@@ -26,25 +26,27 @@ public class OpenApiYamlReader : IOpenApiReader
2626

2727
/// <inheritdoc/>
2828
public async Task<ReadResult> ReadAsync(Stream input,
29+
Uri location,
2930
OpenApiReaderSettings settings,
3031
CancellationToken cancellationToken = default)
3132
{
3233
if (input is null) throw new ArgumentNullException(nameof(input));
3334
if (input is MemoryStream memoryStream)
3435
{
35-
return Read(memoryStream, settings);
36+
return Read(memoryStream, location, settings);
3637
}
3738
else
3839
{
3940
using var preparedStream = new MemoryStream();
4041
await input.CopyToAsync(preparedStream, copyBufferSize, cancellationToken).ConfigureAwait(false);
4142
preparedStream.Position = 0;
42-
return Read(preparedStream, settings);
43+
return Read(preparedStream, location, settings);
4344
}
4445
}
4546

4647
/// <inheritdoc/>
4748
public ReadResult Read(MemoryStream input,
49+
Uri location,
4850
OpenApiReaderSettings settings)
4951
{
5052
if (input is null) throw new ArgumentNullException(nameof(input));
@@ -74,13 +76,13 @@ public ReadResult Read(MemoryStream input,
7476
};
7577
}
7678

77-
return Read(jsonNode, settings);
79+
return Read(jsonNode, location, settings);
7880
}
7981

8082
/// <inheritdoc/>
81-
public static ReadResult Read(JsonNode jsonNode, OpenApiReaderSettings settings)
83+
public static ReadResult Read(JsonNode jsonNode, Uri location, OpenApiReaderSettings settings)
8284
{
83-
return _jsonReader.Read(jsonNode, settings);
85+
return _jsonReader.Read(jsonNode, location, settings);
8486
}
8587

8688
/// <inheritdoc/>

‎src/Microsoft.OpenApi/Interfaces/IOpenApiReader.cs

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
33

4+
using System;
45
using System.IO;
56
using System.Threading;
67
using System.Threading.Tasks;
@@ -18,18 +19,20 @@ public interface IOpenApiReader
1819
/// Async method to reads the stream and parse it into an Open API document.
1920
/// </summary>
2021
/// <param name="input">The stream input.</param>
22+
/// <param name="location">Location of where the document that is getting loaded is saved</param>
2123
/// <param name="settings"> The OpenApi reader settings.</param>
2224
/// <param name="cancellationToken">Propagates notification that an operation should be cancelled.</param>
2325
/// <returns></returns>
24-
Task<ReadResult> ReadAsync(Stream input, OpenApiReaderSettings settings, CancellationToken cancellationToken = default);
26+
Task<ReadResult> ReadAsync(Stream input, Uri location, OpenApiReaderSettings settings, CancellationToken cancellationToken = default);
2527

2628
/// <summary>
2729
/// Provides a synchronous method to read the input memory stream and parse it into an Open API document.
2830
/// </summary>
2931
/// <param name="input"></param>
32+
/// <param name="location">Location of where the document that is getting loaded is saved</param>
3033
/// <param name="settings"></param>
3134
/// <returns></returns>
32-
ReadResult Read(MemoryStream input, OpenApiReaderSettings settings);
35+
ReadResult Read(MemoryStream input, Uri location, OpenApiReaderSettings settings);
3336

3437
/// <summary>
3538
/// Reads the MemoryStream and parses the fragment of an OpenAPI description into an Open API Element.

‎src/Microsoft.OpenApi/Interfaces/IOpenApiVersionService.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
33

4+
using System;
45
using Microsoft.OpenApi.Models;
56
using Microsoft.OpenApi.Reader.ParseNodes;
67

@@ -24,8 +25,9 @@ internal interface IOpenApiVersionService
2425
/// Converts a generic RootNode instance into a strongly typed OpenApiDocument
2526
/// </summary>
2627
/// <param name="rootNode">RootNode containing the information to be converted into an OpenAPI Document</param>
28+
/// <param name="location">Location of where the document that is getting loaded is saved</param>
2729
/// <returns>Instance of OpenApiDocument populated with data from rootNode</returns>
28-
OpenApiDocument LoadDocument(RootNode rootNode);
30+
OpenApiDocument LoadDocument(RootNode rootNode, Uri location);
2931

3032
/// <summary>
3133
/// Gets the description and summary scalar values in a reference object for V3.1 support

‎src/Microsoft.OpenApi/Interfaces/IStreamLoader.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ public interface IStreamLoader
1717
/// <summary>
1818
/// Use Uri to locate data and convert into an input object.
1919
/// </summary>
20+
/// <param name="baseUrl">Base URL of parent to which a relative reference could be loaded.
21+
/// If the <paramref name="uri"/> is an absolute parameter the value of this parameter will be ignored</param>
2022
/// <param name="uri">Identifier of some source of an OpenAPI Description</param>
2123
/// <param name="cancellationToken">The cancellation token.</param>
2224
/// <returns>A data object that can be processed by a reader to generate an <see cref="OpenApiDocument"/></returns>
23-
Task<Stream> LoadAsync(Uri uri, CancellationToken cancellationToken = default);
25+
Task<Stream> LoadAsync(Uri baseUrl, Uri uri, CancellationToken cancellationToken = default);
2426
}
2527
}

‎src/Microsoft.OpenApi/Models/OpenApiDocument.cs

+7-6
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,9 @@ public HashSet<OpenApiTag>? Tags
109109
public Dictionary<string, object>? Metadata { get; set; }
110110

111111
/// <summary>
112-
/// Implements IBaseDocument
112+
/// Absolute location of the document or a generated placeholder if location is not given
113113
/// </summary>
114-
public Uri BaseUri { get; }
114+
public Uri BaseUri { get; internal set; }
115115

116116
/// <summary>
117117
/// Parameter-less constructor
@@ -571,14 +571,15 @@ private static string ConvertByteArrayToString(byte[] hash)
571571
}
572572
else
573573
{
574-
string relativePath = OpenApiConstants.ComponentsSegment + reference.Type.GetDisplayName() + "/" + id;
574+
string relativePath = $"#{OpenApiConstants.ComponentsSegment}{reference.Type.GetDisplayName()}/{id}";
575+
Uri? externalResourceUri = useExternal ? Workspace?.GetDocumentId(reference.ExternalResource) : null;
575576

576-
uriLocation = useExternal
577-
? Workspace?.GetDocumentId(reference.ExternalResource)?.OriginalString + relativePath
577+
uriLocation = useExternal && externalResourceUri is not null
578+
? externalResourceUri.AbsoluteUri + relativePath
578579
: BaseUri + relativePath;
579580
}
580581

581-
return Workspace?.ResolveReference<IOpenApiReferenceable>(uriLocation);
582+
return Workspace?.ResolveReference<IOpenApiReferenceable>(new Uri(uriLocation).AbsoluteUri);
582583
}
583584

584585
/// <summary>

‎src/Microsoft.OpenApi/Reader/OpenApiJsonReader.cs

+9-3
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@ public class OpenApiJsonReader : IOpenApiReader
2525
/// Reads the memory stream input and parses it into an Open API document.
2626
/// </summary>
2727
/// <param name="input">Memory stream containing OpenAPI description to parse.</param>
28+
/// <param name="location">Location of where the document that is getting loaded is saved</param>
2829
/// <param name="settings">The Reader settings to be used during parsing.</param>
2930
/// <returns></returns>
3031
public ReadResult Read(MemoryStream input,
32+
Uri location,
3133
OpenApiReaderSettings settings)
3234
{
3335
if (input is null) throw new ArgumentNullException(nameof(input));
@@ -52,16 +54,18 @@ public ReadResult Read(MemoryStream input,
5254
};
5355
}
5456

55-
return Read(jsonNode, settings);
57+
return Read(jsonNode, location, settings);
5658
}
5759

5860
/// <summary>
5961
/// Parses the JsonNode input into an Open API document.
6062
/// </summary>
6163
/// <param name="jsonNode">The JsonNode input.</param>
64+
/// <param name="location">Location of where the document that is getting loaded is saved</param>
6265
/// <param name="settings">The Reader settings to be used during parsing.</param>
6366
/// <returns></returns>
6467
public ReadResult Read(JsonNode jsonNode,
68+
Uri location,
6569
OpenApiReaderSettings settings)
6670
{
6771
if (jsonNode is null) throw new ArgumentNullException(nameof(jsonNode));
@@ -79,7 +83,7 @@ public ReadResult Read(JsonNode jsonNode,
7983
try
8084
{
8185
// Parse the OpenAPI Document
82-
document = context.Parse(jsonNode);
86+
document = context.Parse(jsonNode, location);
8387
document.SetReferenceHostDocument();
8488
}
8589
catch (OpenApiException ex)
@@ -115,10 +119,12 @@ public ReadResult Read(JsonNode jsonNode,
115119
/// Reads the stream input asynchronously and parses it into an Open API document.
116120
/// </summary>
117121
/// <param name="input">Memory stream containing OpenAPI description to parse.</param>
122+
/// <param name="location">Location of where the document that is getting loaded is saved</param>
118123
/// <param name="settings">The Reader settings to be used during parsing.</param>
119124
/// <param name="cancellationToken">Propagates notifications that operations should be cancelled.</param>
120125
/// <returns></returns>
121126
public async Task<ReadResult> ReadAsync(Stream input,
127+
Uri location,
122128
OpenApiReaderSettings settings,
123129
CancellationToken cancellationToken = default)
124130
{
@@ -144,7 +150,7 @@ public async Task<ReadResult> ReadAsync(Stream input,
144150
};
145151
}
146152

147-
return Read(jsonNode, settings);
153+
return Read(jsonNode, location, settings);
148154
}
149155

150156
/// <inheritdoc/>

‎src/Microsoft.OpenApi/Reader/OpenApiModelFactory.cs

+13-9
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,13 @@ private static async Task<ReadResult> InternalLoadAsync(Stream input, string for
240240
{
241241
settings ??= DefaultReaderSettings.Value;
242242
var reader = settings.GetReader(format);
243-
var readResult = await reader.ReadAsync(input, settings, cancellationToken).ConfigureAwait(false);
243+
var location = new Uri(OpenApiConstants.BaseRegistryUri);
244+
if (input is FileStream fileStream)
245+
{
246+
location = new Uri(fileStream.Name);
247+
}
248+
249+
var readResult = await reader.ReadAsync(input, location, settings, cancellationToken).ConfigureAwait(false);
244250

245251
if (settings.LoadExternalRefs)
246252
{
@@ -258,13 +264,10 @@ private static async Task<ReadResult> InternalLoadAsync(Stream input, string for
258264

259265
private static async Task<OpenApiDiagnostic> LoadExternalRefsAsync(OpenApiDocument? document, OpenApiReaderSettings settings, string? format = null, CancellationToken token = default)
260266
{
261-
// Create workspace for all documents to live in.
262-
var baseUrl = settings.BaseUrl ?? new Uri(OpenApiConstants.BaseRegistryUri);
263-
var openApiWorkSpace = new OpenApiWorkspace(baseUrl);
264-
265-
// Load this root document into the workspace
266-
var streamLoader = new DefaultStreamLoader(baseUrl, settings.HttpClient);
267-
var workspaceLoader = new OpenApiWorkspaceLoader(openApiWorkSpace, settings.CustomExternalLoader ?? streamLoader, settings);
267+
// Load this document into the workspace
268+
var streamLoader = new DefaultStreamLoader(settings.HttpClient);
269+
var workspace = document?.Workspace ?? new OpenApiWorkspace();
270+
var workspaceLoader = new OpenApiWorkspaceLoader(workspace, settings.CustomExternalLoader ?? streamLoader, settings);
268271
return await workspaceLoader.LoadAsync(new OpenApiReference() { ExternalResource = "/" }, document, format ?? OpenApiConstants.Json, null, token).ConfigureAwait(false);
269272
}
270273

@@ -280,8 +283,9 @@ private static ReadResult InternalLoad(MemoryStream input, string format, OpenAp
280283
throw new ArgumentException($"Cannot parse the stream: {nameof(input)} is empty or contains no elements.");
281284
}
282285

286+
var location = new Uri(OpenApiConstants.BaseRegistryUri);
283287
var reader = settings.GetReader(format);
284-
var readResult = reader.Read(input, settings);
288+
var readResult = reader.Read(input, location, settings);
285289
return readResult;
286290
}
287291

‎src/Microsoft.OpenApi/Reader/ParsingContext.cs

+5-4
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,9 @@ public ParsingContext(OpenApiDiagnostic diagnostic)
6262
/// Initiates the parsing process. Not thread safe and should only be called once on a parsing context
6363
/// </summary>
6464
/// <param name="jsonNode">Set of Json nodes to parse.</param>
65+
/// <param name="location">Location of where the document that is getting loaded is saved</param>
6566
/// <returns>An OpenApiDocument populated based on the passed yamlDocument </returns>
66-
public OpenApiDocument Parse(JsonNode jsonNode)
67+
public OpenApiDocument Parse(JsonNode jsonNode, Uri location)
6768
{
6869
RootNode = new RootNode(this, jsonNode);
6970

@@ -75,20 +76,20 @@ public OpenApiDocument Parse(JsonNode jsonNode)
7576
{
7677
case string version when version.is2_0():
7778
VersionService = new OpenApiV2VersionService(Diagnostic);
78-
doc = VersionService.LoadDocument(RootNode);
79+
doc = VersionService.LoadDocument(RootNode, location);
7980
this.Diagnostic.SpecificationVersion = OpenApiSpecVersion.OpenApi2_0;
8081
ValidateRequiredFields(doc, version);
8182
break;
8283

8384
case string version when version.is3_0():
8485
VersionService = new OpenApiV3VersionService(Diagnostic);
85-
doc = VersionService.LoadDocument(RootNode);
86+
doc = VersionService.LoadDocument(RootNode, location);
8687
this.Diagnostic.SpecificationVersion = version.is3_1() ? OpenApiSpecVersion.OpenApi3_1 : OpenApiSpecVersion.OpenApi3_0;
8788
ValidateRequiredFields(doc, version);
8889
break;
8990
case string version when version.is3_1():
9091
VersionService = new OpenApiV31VersionService(Diagnostic);
91-
doc = VersionService.LoadDocument(RootNode);
92+
doc = VersionService.LoadDocument(RootNode, location);
9293
this.Diagnostic.SpecificationVersion = OpenApiSpecVersion.OpenApi3_1;
9394
ValidateRequiredFields(doc, version);
9495
break;

‎src/Microsoft.OpenApi/Reader/Services/DefaultStreamLoader.cs

+5-11
Original file line numberDiff line numberDiff line change
@@ -17,30 +17,24 @@ namespace Microsoft.OpenApi.Reader.Services
1717
/// </summary>
1818
public class DefaultStreamLoader : IStreamLoader
1919
{
20-
private readonly Uri baseUrl;
2120
private readonly HttpClient _httpClient;
2221

2322
/// <summary>
2423
/// The default stream loader
2524
/// </summary>
26-
/// <param name="baseUrl"></param>
2725
/// <param name="httpClient">The HttpClient to use to retrieve documents when needed</param>
28-
public DefaultStreamLoader(Uri baseUrl, HttpClient httpClient)
26+
public DefaultStreamLoader(HttpClient httpClient)
2927
{
30-
this.baseUrl = baseUrl;
3128
_httpClient = Utils.CheckArgumentNull(httpClient);
3229
}
3330

3431
/// <inheritdoc/>
35-
public async Task<Stream> LoadAsync(Uri uri, CancellationToken cancellationToken = default)
32+
public async Task<Stream> LoadAsync(Uri baseUrl, Uri uri, CancellationToken cancellationToken = default)
3633
{
37-
var absoluteUri = (baseUrl.AbsoluteUri.Equals(OpenApiConstants.BaseRegistryUri), baseUrl.IsAbsoluteUri, uri.IsAbsoluteUri) switch
34+
var absoluteUri = baseUrl.AbsoluteUri.Equals(OpenApiConstants.BaseRegistryUri) switch
3835
{
39-
(true, _, _) => new Uri(Path.Combine(Directory.GetCurrentDirectory(), uri.ToString())),
40-
// this overcomes a URI concatenation issue for local paths on linux OSes
41-
(_, true, false) when baseUrl.Scheme.Equals("file", StringComparison.OrdinalIgnoreCase) && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows) =>
42-
new Uri(Path.Combine(baseUrl.AbsoluteUri, uri.ToString())),
43-
(_, _, _) => new Uri(baseUrl, uri),
36+
true => new Uri(Path.Combine(Directory.GetCurrentDirectory(), uri.ToString())),
37+
_ => new Uri(baseUrl, uri),
4438
};
4539

4640
return absoluteUri.Scheme switch

‎src/Microsoft.OpenApi/Reader/Services/OpenApiWorkspaceLoader.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ internal async Task<OpenApiDiagnostic> LoadAsync(OpenApiReference reference,
4848
// If not already in workspace, load it and process references
4949
if (item.ExternalResource is not null && !_workspace.Contains(item.ExternalResource))
5050
{
51-
var input = await _loader.LoadAsync(new(item.ExternalResource, UriKind.RelativeOrAbsolute), cancellationToken).ConfigureAwait(false);
51+
var uri = new Uri(item.ExternalResource, UriKind.RelativeOrAbsolute);
52+
var input = await _loader.LoadAsync(item.HostDocument!.BaseUri, uri, cancellationToken).ConfigureAwait(false);
5253
var result = await OpenApiDocument.LoadAsync(input, format, _readerSettings, cancellationToken).ConfigureAwait(false);
5354
// Merge diagnostics
5455
if (result.Diagnostic != null)

‎src/Microsoft.OpenApi/Reader/V2/OpenApiDocumentDeserializer.cs

+5-2
Original file line numberDiff line numberDiff line change
@@ -227,9 +227,12 @@ private static string BuildUrl(string? scheme, string? host, string? basePath)
227227
return uriBuilder.ToString();
228228
}
229229

230-
public static OpenApiDocument LoadOpenApi(RootNode rootNode)
230+
public static OpenApiDocument LoadOpenApi(RootNode rootNode, Uri location)
231231
{
232-
var openApiDoc = new OpenApiDocument();
232+
var openApiDoc = new OpenApiDocument
233+
{
234+
BaseUri = location
235+
};
233236

234237
var openApiNode = rootNode.GetMap();
235238

‎src/Microsoft.OpenApi/Reader/V2/OpenApiV2VersionService.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,9 @@ public OpenApiV2VersionService(OpenApiDiagnostic diagnostic)
5050
[typeof(OpenApiXml)] = OpenApiV2Deserializer.LoadXml
5151
};
5252

53-
public OpenApiDocument LoadDocument(RootNode rootNode)
53+
public OpenApiDocument LoadDocument(RootNode rootNode, Uri location)
5454
{
55-
return OpenApiV2Deserializer.LoadOpenApi(rootNode);
55+
return OpenApiV2Deserializer.LoadOpenApi(rootNode, location);
5656
}
5757

5858
public T? LoadElement<T>(ParseNode node, OpenApiDocument doc) where T : IOpenApiElement

‎src/Microsoft.OpenApi/Reader/V3/OpenApiDocumentDeserializer.cs

+5-2
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,12 @@ internal static partial class OpenApiV3Deserializer
3838
{s => s.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, p, n, _) => o.AddExtension(p, LoadExtension(p, n))}
3939
};
4040

41-
public static OpenApiDocument LoadOpenApi(RootNode rootNode)
41+
public static OpenApiDocument LoadOpenApi(RootNode rootNode, Uri location)
4242
{
43-
var openApiDoc = new OpenApiDocument();
43+
var openApiDoc = new OpenApiDocument
44+
{
45+
BaseUri = location
46+
};
4447
var openApiNode = rootNode.GetMap();
4548

4649
ParseMap(openApiNode, openApiDoc, _openApiFixedFields, _openApiPatternFields, openApiDoc);

‎src/Microsoft.OpenApi/Reader/V3/OpenApiV3VersionService.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@ public OpenApiV3VersionService(OpenApiDiagnostic diagnostic)
6464
[typeof(OpenApiSchemaReference)] = OpenApiV3Deserializer.LoadMapping
6565
};
6666

67-
public OpenApiDocument LoadDocument(RootNode rootNode)
67+
public OpenApiDocument LoadDocument(RootNode rootNode, Uri location)
6868
{
69-
return OpenApiV3Deserializer.LoadOpenApi(rootNode);
69+
return OpenApiV3Deserializer.LoadOpenApi(rootNode, location);
7070
}
7171

7272
public T LoadElement<T>(ParseNode node, OpenApiDocument doc) where T : IOpenApiElement

0 commit comments

Comments
 (0)
Please sign in to comment.