Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit f9e5248

Browse filesBrowse files
committed
fix: reading streams in an asp.net context would cause async exceptions
1 parent 2436d73 commit f9e5248
Copy full SHA for f9e5248

2 files changed

+275-45Lines changed: 275 additions & 45 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

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

Copy file name to clipboardExpand all lines: src/Microsoft.OpenApi/Reader/OpenApiModelFactory.cs
+73-39Lines changed: 73 additions & 39 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;
@@ -362,65 +363,98 @@ private static string InspectInputFormat(string input)
362363
return input.StartsWith("{", StringComparison.OrdinalIgnoreCase) || input.StartsWith("[", StringComparison.OrdinalIgnoreCase) ? OpenApiConstants.Json : OpenApiConstants.Yaml;
363364
}
364365

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

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))
390+
try
378391
{
379-
firstByte = stream.ReadByte();
380-
}
392+
var initialPosition = stream.Position;
393+
var firstByte = (char)stream.ReadByte();
394+
395+
// Skip whitespace if present and read the next non-whitespace byte
396+
if (char.IsWhiteSpace(firstByte))
397+
{
398+
firstByte = (char)stream.ReadByte();
399+
}
381400

382-
stream.Position = initialPosition; // Reset the stream position to the beginning
401+
stream.Position = initialPosition; // Reset the stream position to the beginning
383402

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

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

396-
if (!input.CanSeek)
444+
if (input is MemoryStream ms)
397445
{
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-
// we need to copy the stream to memory string we've already started reading it and can't reposition it
407-
preparedStream = new MemoryStream();
408-
bufferStream.Position = 0;
409-
await bufferStream.CopyToAsync(preparedStream, 81920, token).ConfigureAwait(false); // Copy buffered portion
410-
await input.CopyToAsync(preparedStream, 81920, token).ConfigureAwait(false); // Copy remaining data
411-
preparedStream.Position = 0;
446+
format ??= InspectStreamFormat(ms);
412447
}
413-
else
448+
else if (!input.CanSeek)
414449
{
415-
format ??= InspectStreamFormat(input);
416-
417-
if (!format.Equals(OpenApiConstants.Json, StringComparison.OrdinalIgnoreCase))
418-
{
419-
// Buffer stream for non-JSON formats (e.g., YAML) since they require synchronous reading
420-
preparedStream = new MemoryStream();
421-
await input.CopyToAsync(preparedStream, 81920, token).ConfigureAwait(false);
422-
preparedStream.Position = 0;
423-
}
450+
// Copy to a MemoryStream to enable seeking and perform format inspection
451+
var bufferStream = await CopyToMemoryStreamAsync(input, token).ConfigureAwait(false);
452+
return await PrepareStreamForReadingAsync(bufferStream, format, token).ConfigureAwait(false);
453+
}
454+
else if (!TryInspectStreamFormat(input, out format!))
455+
{
456+
var bufferStream = await CopyToMemoryStreamAsync(input, token).ConfigureAwait(false);
457+
return await PrepareStreamForReadingAsync(bufferStream, format, token).ConfigureAwait(false);
424458
}
425459

426460
return (preparedStream, format);
Collapse file

‎test/Microsoft.OpenApi.Tests/Reader/OpenApiModelFactoryTests.cs‎

Copy file name to clipboardExpand all lines: test/Microsoft.OpenApi.Tests/Reader/OpenApiModelFactoryTests.cs
+202-6Lines changed: 202 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,7 @@ await File.WriteAllTextAsync(tempFilePathReferrer,
120120
Assert.NotNull(readResult.Document.Components);
121121
Assert.Equal(baseUri, readResult.Document.BaseUri);
122122
}
123-
[Fact]
124-
public async Task CanLoadANonSeekableStream()
125-
{
126-
// Given
127-
var documentJson =
123+
private readonly string documentJson =
128124
"""
129125
{
130126
"openapi": "3.1.0",
@@ -135,6 +131,18 @@ public async Task CanLoadANonSeekableStream()
135131
"paths": {}
136132
}
137133
""";
134+
private readonly string documentYaml =
135+
"""
136+
openapi: 3.1.0
137+
info:
138+
title: Sample API
139+
version: 1.0.0
140+
paths: {}
141+
""";
142+
[Fact]
143+
public async Task CanLoadANonSeekableStreamInJsonAndDetectFormat()
144+
{
145+
// Given
138146
using var memoryStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(documentJson));
139147
using var nonSeekableStream = new NonSeekableStream(memoryStream);
140148

@@ -146,6 +154,194 @@ public async Task CanLoadANonSeekableStream()
146154
Assert.Equal("Sample API", document.Info.Title);
147155
}
148156

157+
[Fact]
158+
public async Task CanLoadANonSeekableStreamInYamlAndDetectFormat()
159+
{
160+
// Given
161+
using var memoryStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(documentYaml));
162+
using var nonSeekableStream = new NonSeekableStream(memoryStream);
163+
var settings = new OpenApiReaderSettings();
164+
settings.AddYamlReader();
165+
166+
// When
167+
var (document, _) = await OpenApiDocument.LoadAsync(nonSeekableStream, settings: settings);
168+
169+
// Then
170+
Assert.NotNull(document);
171+
Assert.Equal("Sample API", document.Info.Title);
172+
}
173+
174+
[Fact]
175+
public async Task CanLoadAnAsyncOnlyStreamInJsonAndDetectFormat()
176+
{
177+
// Given
178+
await using var memoryStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(documentJson));
179+
await using var nonSeekableStream = new AsyncOnlyStream(memoryStream);
180+
181+
// When
182+
var (document, _) = await OpenApiDocument.LoadAsync(nonSeekableStream);
183+
184+
// Then
185+
Assert.NotNull(document);
186+
Assert.Equal("Sample API", document.Info.Title);
187+
}
188+
189+
[Fact]
190+
public async Task CanLoadAnAsyncOnlyStreamInYamlAndDetectFormat()
191+
{
192+
// Given
193+
await using var memoryStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(documentYaml));
194+
await using var nonSeekableStream = new AsyncOnlyStream(memoryStream);
195+
var settings = new OpenApiReaderSettings();
196+
settings.AddYamlReader();
197+
198+
// When
199+
var (document, _) = await OpenApiDocument.LoadAsync(nonSeekableStream, settings: settings);
200+
201+
// Then
202+
Assert.NotNull(document);
203+
Assert.Equal("Sample API", document.Info.Title);
204+
}
205+
206+
public sealed class AsyncOnlyStream : Stream
207+
{
208+
private readonly Stream _innerStream;
209+
public AsyncOnlyStream(Stream stream) : base()
210+
{
211+
_innerStream = stream;
212+
}
213+
public override bool CanSeek => _innerStream.CanSeek;
214+
215+
public override long Position { get => _innerStream.Position; set => throw new NotSupportedException("Blocking operations are not supported"); }
216+
217+
public override bool CanRead => _innerStream.CanRead;
218+
219+
public override bool CanWrite => _innerStream.CanWrite;
220+
221+
public override long Length => _innerStream.Length;
222+
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
223+
{
224+
return _innerStream.BeginRead(buffer, offset, count, callback, state);
225+
}
226+
227+
public override void Flush()
228+
{
229+
throw new NotSupportedException("Blocking operations are not supported.");
230+
}
231+
232+
public override int Read(byte[] buffer, int offset, int count)
233+
{
234+
throw new NotSupportedException("Blocking operations are not supported.");
235+
}
236+
237+
public override long Seek(long offset, SeekOrigin origin)
238+
{
239+
throw new NotSupportedException("Blocking operations are not supported.");
240+
}
241+
242+
public override void SetLength(long value)
243+
{
244+
_innerStream.SetLength(value);
245+
}
246+
247+
public override void Write(byte[] buffer, int offset, int count)
248+
{
249+
throw new NotSupportedException("Blocking operations are not supported.");
250+
}
251+
protected override void Dispose(bool disposing)
252+
{
253+
throw new NotSupportedException("Blocking operations are not supported.");
254+
}
255+
256+
public override async ValueTask DisposeAsync()
257+
{
258+
await _innerStream.DisposeAsync();
259+
await base.DisposeAsync();
260+
}
261+
262+
public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
263+
{
264+
return _innerStream.CopyToAsync(destination, bufferSize, cancellationToken);
265+
}
266+
267+
public override bool CanTimeout => _innerStream.CanTimeout;
268+
269+
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
270+
{
271+
return _innerStream.BeginWrite(buffer, offset, count, callback, state);
272+
}
273+
274+
public override void CopyTo(Stream destination, int bufferSize)
275+
{
276+
throw new NotSupportedException("Blocking operations are not supported.");
277+
}
278+
279+
public override void Close()
280+
{
281+
_innerStream.Close();
282+
}
283+
284+
public override int EndRead(IAsyncResult asyncResult)
285+
{
286+
return _innerStream.EndRead(asyncResult);
287+
}
288+
289+
public override void EndWrite(IAsyncResult asyncResult)
290+
{
291+
_innerStream.EndWrite(asyncResult);
292+
}
293+
294+
public override int ReadByte()
295+
{
296+
throw new NotSupportedException("Blocking operations are not supported.");
297+
}
298+
299+
public override void WriteByte(byte value)
300+
{
301+
throw new NotSupportedException("Blocking operations are not supported.");
302+
}
303+
304+
public override Task FlushAsync(CancellationToken cancellationToken)
305+
{
306+
return _innerStream.FlushAsync(cancellationToken);
307+
}
308+
309+
public override int Read(Span<byte> buffer)
310+
{
311+
throw new NotSupportedException("Blocking operations are not supported.");
312+
}
313+
314+
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
315+
{
316+
return _innerStream.ReadAsync(buffer, offset, count, cancellationToken);
317+
}
318+
319+
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
320+
{
321+
return _innerStream.ReadAsync(buffer, cancellationToken);
322+
}
323+
324+
public override int ReadTimeout { get => _innerStream.ReadTimeout; set => _innerStream.ReadTimeout = value; }
325+
326+
public override void Write(ReadOnlySpan<byte> buffer)
327+
{
328+
throw new NotSupportedException("Blocking operations are not supported.");
329+
}
330+
331+
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
332+
{
333+
return _innerStream.WriteAsync(buffer, offset, count, cancellationToken);
334+
}
335+
336+
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
337+
{
338+
return _innerStream.WriteAsync(buffer, cancellationToken);
339+
}
340+
341+
public override int WriteTimeout { get => _innerStream.WriteTimeout; set => _innerStream.WriteTimeout = value; }
342+
343+
}
344+
149345
public sealed class NonSeekableStream : Stream
150346
{
151347
private readonly Stream _innerStream;
@@ -155,7 +351,7 @@ public NonSeekableStream(Stream stream) : base()
155351
}
156352
public override bool CanSeek => false;
157353

158-
public override long Position { get => _innerStream.Position; set => throw new InvalidOperationException("Seeking is not supported."); }
354+
public override long Position { get => _innerStream.Position; set => throw new NotSupportedException("Seeking is not supported."); }
159355

160356
public override bool CanRead => _innerStream.CanRead;
161357

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.