Skip to content

Commit 321096d

Browse files
authored
Merge pull request #2640 from microsoft/fix/non-seekable-stream-to-v3
fix/non seekable stream to v3
2 parents 0e8245e + 6461bac commit 321096d

File tree

3 files changed

+467
-49
lines changed

3 files changed

+467
-49
lines changed

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
## [3.0.2](https://github.com/microsoft/OpenAPI.NET/compare/v3.0.1...v3.0.2) (2025-12-08)
44

5-
65
### Bug Fixes
76

87
* additional properties serialization should not emit a schema in v2 ([946cba9](https://github.com/microsoft/OpenAPI.NET/commit/946cba992a2733a60182453e38722b4ed789b729))
@@ -29,6 +28,14 @@
2928

3029
* adds support for OpenAPI 3.2.0 ([765a8dd](https://github.com/microsoft/OpenAPI.NET/commit/765a8dd4d6efd1a31b6a76d282ccffa5877a845a))
3130

31+
## [2.3.11](https://github.com/microsoft/OpenAPI.NET/compare/v2.3.10...v2.3.11) (2025-12-08)
32+
33+
### Bug Fixes
34+
35+
* additional properties serialization should not emit a schema in v2 ([946cba9](https://github.com/microsoft/OpenAPI.NET/commit/946cba992a2733a60182453e38722b4ed789b729))
36+
* additional properties serialization should not emit a schema in v2 fix: additional properties serialization should not emit booleans in v3.1+ ([275dd9d](https://github.com/microsoft/OpenAPI.NET/commit/275dd9d7525b1f490eccaf1e6e60829ae51bdf5d))
37+
* additional properties serialization should not emit booleans in v3.1+ ([946cba9](https://github.com/microsoft/OpenAPI.NET/commit/946cba992a2733a60182453e38722b4ed789b729))
38+
3239
## [2.3.10](https://github.com/microsoft/OpenAPI.NET/compare/v2.3.9...v2.3.10) (2025-11-17)
3340

3441
* empty strings should be quoted in yaml ([e919b33](https://github.com/microsoft/OpenAPI.NET/commit/e919b33e9d09159217066248483ef4c767865c82))

src/Microsoft.OpenApi/Reader/OpenApiModelFactory.cs

Lines changed: 71 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT license.
33

44
using System;
5+
using System.Diagnostics.CodeAnalysis;
56
using System.IO;
67
using System.Linq;
78
using System.Security;
@@ -359,76 +360,98 @@ SecurityException or
359360

360361
private static string InspectInputFormat(string input)
361362
{
362-
return input.StartsWith("{", StringComparison.OrdinalIgnoreCase) || input.StartsWith("[", StringComparison.OrdinalIgnoreCase) ? OpenApiConstants.Json : OpenApiConstants.Yaml;
363+
var trimmedInput = input.TrimStart();
364+
return trimmedInput.StartsWith("{", StringComparison.OrdinalIgnoreCase) || trimmedInput.StartsWith("[", StringComparison.OrdinalIgnoreCase) ?
365+
OpenApiConstants.Json : OpenApiConstants.Yaml;
363366
}
364367

365-
private static string InspectStreamFormat(Stream stream)
368+
/// <summary>
369+
/// Reads the initial bytes of the stream to determine if it is JSON or YAML.
370+
/// </summary>
371+
/// <remarks>
372+
/// It is important NOT TO change the stream type from MemoryStream.
373+
/// In Asp.Net core 3.0+ we could get passed a stream from a request or response body.
374+
/// In such case, we CAN'T use the ReadByte method as it throws NotSupportedException.
375+
/// Therefore, we need to ensure that the stream is a MemoryStream before calling this method.
376+
/// Maintaining this type ensures there won't be any unforeseen wrong usage of the method.
377+
/// </remarks>
378+
/// <param name="stream">The stream to inspect</param>
379+
/// <returns>The format of the stream.</returns>
380+
private static string InspectStreamFormat(MemoryStream stream)
381+
{
382+
return TryInspectStreamFormat(stream, out var format) ? format! : throw new InvalidOperationException("Could not determine the format of the stream.");
383+
}
384+
private static bool TryInspectStreamFormat(Stream stream, out string? format)
366385
{
367386
#if NET6_0_OR_GREATER
368387
ArgumentNullException.ThrowIfNull(stream);
369388
#else
370389
if (stream is null) throw new ArgumentNullException(nameof(stream));
371390
#endif
372391

373-
long initialPosition = stream.Position;
374-
int firstByte = stream.ReadByte();
375-
376-
// Skip whitespace if present and read the next non-whitespace byte
377-
if (char.IsWhiteSpace((char)firstByte))
392+
try
378393
{
379-
firstByte = stream.ReadByte();
380-
}
394+
var initialPosition = stream.Position;
395+
var firstByte = (char)stream.ReadByte();
396+
397+
// Skip whitespace if present and read the next non-whitespace byte
398+
while (char.IsWhiteSpace(firstByte))
399+
{
400+
firstByte = (char)stream.ReadByte();
401+
}
381402

382-
stream.Position = initialPosition; // Reset the stream position to the beginning
403+
stream.Position = initialPosition; // Reset the stream position to the beginning
383404

384-
char firstChar = (char)firstByte;
385-
return firstChar switch
405+
format = firstByte switch
406+
{
407+
'{' or '[' => OpenApiConstants.Json, // If the first character is '{' or '[', assume JSON
408+
_ => OpenApiConstants.Yaml // Otherwise assume YAML
409+
};
410+
return true;
411+
}
412+
catch (NotSupportedException)
413+
{
414+
// https://github.com/dotnet/aspnetcore/blob/c9d0750396e1d319301255ba61842721ab72ab10/src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseStream.cs#L40
415+
}
416+
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP || NET5_0_OR_GREATER
417+
catch (InvalidOperationException ex) when (ex.Message.Contains("AllowSynchronousIO", StringComparison.Ordinal))
418+
#else
419+
catch (InvalidOperationException ex) when (ex.Message.Contains("AllowSynchronousIO"))
420+
#endif
386421
{
387-
'{' or '[' => OpenApiConstants.Json, // If the first character is '{' or '[', assume JSON
388-
_ => OpenApiConstants.Yaml // Otherwise assume YAML
389-
};
422+
// https://github.com/dotnet/aspnetcore/blob/c9d0750396e1d319301255ba61842721ab72ab10/src/Servers/HttpSys/src/RequestProcessing/RequestStream.cs#L100-L108
423+
// https://github.com/dotnet/aspnetcore/blob/c9d0750396e1d319301255ba61842721ab72ab10/src/Servers/IIS/IIS/src/Core/HttpRequestStream.cs#L24-L30
424+
// https://github.com/dotnet/aspnetcore/blob/c9d0750396e1d319301255ba61842721ab72ab10/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestStream.cs#L54-L60
425+
}
426+
format = null;
427+
return false;
390428
}
391429

430+
private static async Task<MemoryStream> CopyToMemoryStreamAsync(Stream input, CancellationToken token)
431+
{
432+
var bufferStream = new MemoryStream();
433+
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP || NET5_0_OR_GREATER
434+
await input.CopyToAsync(bufferStream, token).ConfigureAwait(false);
435+
#else
436+
await input.CopyToAsync(bufferStream, 81920, token).ConfigureAwait(false);
437+
#endif
438+
bufferStream.Position = 0;
439+
return bufferStream;
440+
}
441+
392442
private static async Task<(Stream, string)> PrepareStreamForReadingAsync(Stream input, string? format, CancellationToken token = default)
393443
{
394444
Stream preparedStream = input;
395445

396-
if (!input.CanSeek)
446+
if (input is MemoryStream ms)
397447
{
398-
// Use a temporary buffer to read a small portion for format detection
399-
using var bufferStream = new MemoryStream();
400-
await input.CopyToAsync(bufferStream, 1024, token).ConfigureAwait(false);
401-
bufferStream.Position = 0;
402-
403-
// Inspect the format from the buffered portion
404-
format ??= InspectStreamFormat(bufferStream);
405-
406-
// If format is JSON, no need to buffer further — use the original stream.
407-
if (format.Equals(OpenApiConstants.Json, StringComparison.OrdinalIgnoreCase))
408-
{
409-
preparedStream = input;
410-
}
411-
else
412-
{
413-
// YAML or other non-JSON format; copy remaining input to a new stream.
414-
preparedStream = new MemoryStream();
415-
bufferStream.Position = 0;
416-
await bufferStream.CopyToAsync(preparedStream, 81920, token).ConfigureAwait(false); // Copy buffered portion
417-
await input.CopyToAsync(preparedStream, 81920, token).ConfigureAwait(false); // Copy remaining data
418-
preparedStream.Position = 0;
419-
}
448+
format ??= InspectStreamFormat(ms);
420449
}
421-
else
450+
else if (!input.CanSeek || !TryInspectStreamFormat(input, out format!))
422451
{
423-
format ??= InspectStreamFormat(input);
424-
425-
if (!format.Equals(OpenApiConstants.Json, StringComparison.OrdinalIgnoreCase))
426-
{
427-
// Buffer stream for non-JSON formats (e.g., YAML) since they require synchronous reading
428-
preparedStream = new MemoryStream();
429-
await input.CopyToAsync(preparedStream, 81920, token).ConfigureAwait(false);
430-
preparedStream.Position = 0;
431-
}
452+
// Copy to a MemoryStream to enable seeking and perform format inspection
453+
var bufferStream = await CopyToMemoryStreamAsync(input, token).ConfigureAwait(false);
454+
return await PrepareStreamForReadingAsync(bufferStream, format, token).ConfigureAwait(false);
432455
}
433456

434457
return (preparedStream, format);

0 commit comments

Comments
 (0)