Skip to content

Commit 2436d73

Browse files
committed
fix: non-seekable json streams would fail to load as a document
1 parent 1f9bff4 commit 2436d73

File tree

2 files changed

+172
-14
lines changed

2 files changed

+172
-14
lines changed

src/Microsoft.OpenApi/Reader/OpenApiModelFactory.cs

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -403,20 +403,12 @@ private static string InspectStreamFormat(Stream stream)
403403
// Inspect the format from the buffered portion
404404
format ??= InspectStreamFormat(bufferStream);
405405

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-
}
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;
420412
}
421413
else
422414
{

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

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Threading.Tasks;
44
using System.IO;
55
using System;
6+
using System.Threading;
67

78
namespace Microsoft.OpenApi.Tests.Reader;
89

@@ -119,4 +120,169 @@ await File.WriteAllTextAsync(tempFilePathReferrer,
119120
Assert.NotNull(readResult.Document.Components);
120121
Assert.Equal(baseUri, readResult.Document.BaseUri);
121122
}
123+
[Fact]
124+
public async Task CanLoadANonSeekableStream()
125+
{
126+
// Given
127+
var documentJson =
128+
"""
129+
{
130+
"openapi": "3.1.0",
131+
"info": {
132+
"title": "Sample API",
133+
"version": "1.0.0"
134+
},
135+
"paths": {}
136+
}
137+
""";
138+
using var memoryStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(documentJson));
139+
using var nonSeekableStream = new NonSeekableStream(memoryStream);
140+
141+
// When
142+
var (document, _) = await OpenApiDocument.LoadAsync(nonSeekableStream);
143+
144+
// Then
145+
Assert.NotNull(document);
146+
Assert.Equal("Sample API", document.Info.Title);
147+
}
148+
149+
public sealed class NonSeekableStream : Stream
150+
{
151+
private readonly Stream _innerStream;
152+
public NonSeekableStream(Stream stream) : base()
153+
{
154+
_innerStream = stream;
155+
}
156+
public override bool CanSeek => false;
157+
158+
public override long Position { get => _innerStream.Position; set => throw new InvalidOperationException("Seeking is not supported."); }
159+
160+
public override bool CanRead => _innerStream.CanRead;
161+
162+
public override bool CanWrite => _innerStream.CanWrite;
163+
164+
public override long Length => _innerStream.Length;
165+
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
166+
{
167+
return _innerStream.BeginRead(buffer, offset, count, callback, state);
168+
}
169+
170+
public override void Flush()
171+
{
172+
_innerStream.Flush();
173+
}
174+
175+
public override int Read(byte[] buffer, int offset, int count)
176+
{
177+
return _innerStream.Read(buffer, offset, count);
178+
}
179+
180+
public override long Seek(long offset, SeekOrigin origin)
181+
{
182+
throw new NotSupportedException("Seeking is not supported.");
183+
}
184+
185+
public override void SetLength(long value)
186+
{
187+
_innerStream.SetLength(value);
188+
}
189+
190+
public override void Write(byte[] buffer, int offset, int count)
191+
{
192+
_innerStream.Write(buffer, offset, count);
193+
}
194+
protected override void Dispose(bool disposing)
195+
{
196+
_innerStream.Dispose();
197+
base.Dispose(disposing);
198+
}
199+
200+
public override async ValueTask DisposeAsync()
201+
{
202+
await _innerStream.DisposeAsync();
203+
await base.DisposeAsync();
204+
}
205+
206+
public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
207+
{
208+
return _innerStream.CopyToAsync(destination, bufferSize, cancellationToken);
209+
}
210+
211+
public override bool CanTimeout => _innerStream.CanTimeout;
212+
213+
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
214+
{
215+
return _innerStream.BeginWrite(buffer, offset, count, callback, state);
216+
}
217+
218+
public override void CopyTo(Stream destination, int bufferSize)
219+
{
220+
_innerStream.CopyTo(destination, bufferSize);
221+
}
222+
223+
public override void Close()
224+
{
225+
_innerStream.Close();
226+
}
227+
228+
public override int EndRead(IAsyncResult asyncResult)
229+
{
230+
return _innerStream.EndRead(asyncResult);
231+
}
232+
233+
public override void EndWrite(IAsyncResult asyncResult)
234+
{
235+
_innerStream.EndWrite(asyncResult);
236+
}
237+
238+
public override int ReadByte()
239+
{
240+
return _innerStream.ReadByte();
241+
}
242+
243+
public override void WriteByte(byte value)
244+
{
245+
_innerStream.WriteByte(value);
246+
}
247+
248+
public override Task FlushAsync(CancellationToken cancellationToken)
249+
{
250+
return _innerStream.FlushAsync(cancellationToken);
251+
}
252+
253+
public override int Read(Span<byte> buffer)
254+
{
255+
return _innerStream.Read(buffer);
256+
}
257+
258+
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
259+
{
260+
return _innerStream.ReadAsync(buffer, offset, count, cancellationToken);
261+
}
262+
263+
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
264+
{
265+
return _innerStream.ReadAsync(buffer, cancellationToken);
266+
}
267+
268+
public override int ReadTimeout { get => _innerStream.ReadTimeout; set => _innerStream.ReadTimeout = value; }
269+
270+
public override void Write(ReadOnlySpan<byte> buffer)
271+
{
272+
_innerStream.Write(buffer);
273+
}
274+
275+
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
276+
{
277+
return _innerStream.WriteAsync(buffer, offset, count, cancellationToken);
278+
}
279+
280+
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
281+
{
282+
return _innerStream.WriteAsync(buffer, cancellationToken);
283+
}
284+
285+
public override int WriteTimeout { get => _innerStream.WriteTimeout; set => _innerStream.WriteTimeout = value; }
286+
287+
}
122288
}

0 commit comments

Comments
 (0)