Skip to content

Commit b48900f

Browse files
authored
Make HttpResponseMessage.Content non-nullable (#35910)
* Make HttpResponseMessage.Content non-nullable * Fix Microsoft.Extensions.Http test
1 parent 43b5d81 commit b48900f

File tree

11 files changed

+118
-34
lines changed

11 files changed

+118
-34
lines changed

src/libraries/Microsoft.Extensions.Http/tests/Logging/RedactedLogValueIntegrationTest.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public async Task RedactHeaderValueWithHeaderList_ValueIsRedactedBeforeLogging()
5151
m.EventId == LoggingScopeHttpMessageHandler.Log.EventIds.RequestHeader &&
5252
m.LoggerName == "System.Net.Http.HttpClient.test.LogicalHandler";
5353
}));
54-
Assert.Equal(
54+
Assert.StartsWith(
5555
@"Request Headers:
5656
Authorization: *
5757
Cache-Control: no-cache
@@ -63,7 +63,7 @@ public async Task RedactHeaderValueWithHeaderList_ValueIsRedactedBeforeLogging()
6363
m.EventId == LoggingHttpMessageHandler.Log.EventIds.RequestHeader &&
6464
m.LoggerName == "System.Net.Http.HttpClient.test.ClientHandler";
6565
}));
66-
Assert.Equal(
66+
Assert.StartsWith(
6767
@"Request Headers:
6868
Authorization: *
6969
Cache-Control: no-cache
@@ -75,7 +75,7 @@ public async Task RedactHeaderValueWithHeaderList_ValueIsRedactedBeforeLogging()
7575
m.EventId == LoggingHttpMessageHandler.Log.EventIds.ResponseHeader &&
7676
m.LoggerName == "System.Net.Http.HttpClient.test.ClientHandler";
7777
}));
78-
Assert.Equal(
78+
Assert.StartsWith(
7979
@"Response Headers:
8080
X-Sensitive: *
8181
Y-Non-Sensitive: innocuous value
@@ -87,7 +87,7 @@ public async Task RedactHeaderValueWithHeaderList_ValueIsRedactedBeforeLogging()
8787
m.EventId == LoggingScopeHttpMessageHandler.Log.EventIds.ResponseHeader &&
8888
m.LoggerName == "System.Net.Http.HttpClient.test.LogicalHandler";
8989
}));
90-
Assert.Equal(
90+
Assert.StartsWith(
9191
@"Response Headers:
9292
X-Sensitive: *
9393
Y-Non-Sensitive: innocuous value
@@ -132,7 +132,7 @@ public async Task RedactHeaderValueWithPredicate_ValueIsRedactedBeforeLogging()
132132
m.EventId == LoggingScopeHttpMessageHandler.Log.EventIds.RequestHeader &&
133133
m.LoggerName == "System.Net.Http.HttpClient.test.LogicalHandler";
134134
}));
135-
Assert.Equal(
135+
Assert.StartsWith(
136136
@"Request Headers:
137137
Authorization: *
138138
Cache-Control: no-cache
@@ -144,7 +144,7 @@ public async Task RedactHeaderValueWithPredicate_ValueIsRedactedBeforeLogging()
144144
m.EventId == LoggingHttpMessageHandler.Log.EventIds.RequestHeader &&
145145
m.LoggerName == "System.Net.Http.HttpClient.test.ClientHandler";
146146
}));
147-
Assert.Equal(
147+
Assert.StartsWith(
148148
@"Request Headers:
149149
Authorization: *
150150
Cache-Control: no-cache
@@ -156,7 +156,7 @@ public async Task RedactHeaderValueWithPredicate_ValueIsRedactedBeforeLogging()
156156
m.EventId == LoggingHttpMessageHandler.Log.EventIds.ResponseHeader &&
157157
m.LoggerName == "System.Net.Http.HttpClient.test.ClientHandler";
158158
}));
159-
Assert.Equal(
159+
Assert.StartsWith(
160160
@"Response Headers:
161161
X-Sensitive: *
162162
Y-Non-Sensitive: innocuous value
@@ -168,7 +168,7 @@ public async Task RedactHeaderValueWithPredicate_ValueIsRedactedBeforeLogging()
168168
m.EventId == LoggingScopeHttpMessageHandler.Log.EventIds.ResponseHeader &&
169169
m.LoggerName == "System.Net.Http.HttpClient.test.LogicalHandler";
170170
}));
171-
Assert.Equal(
171+
Assert.StartsWith(
172172
@"Response Headers:
173173
X-Sensitive: *
174174
Y-Non-Sensitive: innocuous value

src/libraries/System.Net.Http/ref/System.Net.Http.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,8 @@ public partial class HttpResponseMessage : System.IDisposable
209209
{
210210
public HttpResponseMessage() { }
211211
public HttpResponseMessage(System.Net.HttpStatusCode statusCode) { }
212-
public System.Net.Http.HttpContent? Content { get { throw null; } set { } }
212+
[System.Diagnostics.CodeAnalysis.AllowNull]
213+
public System.Net.Http.HttpContent Content { get { throw null; } set { } }
213214
public System.Net.Http.Headers.HttpResponseHeaders Headers { get { throw null; } }
214215
public bool IsSuccessStatusCode { get { throw null; } }
215216
public string? ReasonPhrase { get { throw null; } set { } }

src/libraries/System.Net.Http/src/System.Net.Http.csproj

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<OutputType>Library</OutputType>
44
<AssemblyName>System.Net.Http</AssemblyName>
@@ -18,12 +18,15 @@
1818
<Compile Include="System\Net\Http\ByteArrayHelpers.cs" />
1919
<Compile Include="System\Net\Http\ClientCertificateOption.cs" />
2020
<Compile Include="System\Net\Http\DelegatingHandler.cs" />
21+
<Compile Include="System\Net\Http\EmptyContent.cs" />
22+
<Compile Include="System\Net\Http\EmptyReadStream.cs" />
2123
<Compile Include="System\Net\Http\FormUrlEncodedContent.cs" />
2224
<Compile Include="System\Net\Http\Headers\AltSvcHeaderParser.cs" />
2325
<Compile Include="System\Net\Http\Headers\AltSvcHeaderValue.cs" />
2426
<Compile Include="System\Net\Http\Headers\KnownHeader.cs" />
2527
<Compile Include="System\Net\Http\Headers\HttpHeaderType.cs" />
2628
<Compile Include="System\Net\Http\Headers\KnownHeaders.cs" />
29+
<Compile Include="System\Net\Http\HttpBaseStream.cs" />
2730
<Compile Include="System\Net\Http\HttpClient.cs" />
2831
<Compile Include="System\Net\Http\HttpClientHandler.cs" />
2932
<Compile Include="System\Net\Http\HttpClientHandler.Core.cs" />
@@ -128,7 +131,6 @@
128131
<Compile Include="System\Net\Http\SocketsHttpHandler\CreditManager.cs" />
129132
<Compile Include="System\Net\Http\SocketsHttpHandler\CreditWaiter.cs" />
130133
<Compile Include="System\Net\Http\SocketsHttpHandler\DecompressionHandler.cs" />
131-
<Compile Include="System\Net\Http\SocketsHttpHandler\EmptyReadStream.cs" />
132134
<Compile Include="System\Net\Http\SocketsHttpHandler\FailedProxyCache.cs" />
133135
<Compile Include="System\Net\Http\SocketsHttpHandler\Http2Connection.cs" />
134136
<Compile Include="System\Net\Http\SocketsHttpHandler\Http2ConnectionException.cs" />
@@ -142,7 +144,6 @@
142144
<Compile Include="System\Net\Http\SocketsHttpHandler\Http3RequestStream.cs" />
143145
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpAuthenticatedConnectionHandler.cs" />
144146
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpAuthority.cs" />
145-
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpBaseStream.cs" />
146147
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpConnection.cs" />
147148
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpConnectionBase.cs" />
148149
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpConnectionHandler.cs" />
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.IO;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
9+
namespace System.Net.Http
10+
{
11+
/// <summary>Provides a zero-length HttpContent implementation.</summary>
12+
internal sealed class EmptyContent : HttpContent
13+
{
14+
protected internal override bool TryComputeLength(out long length)
15+
{
16+
length = 0;
17+
return true;
18+
}
19+
20+
protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) =>
21+
Task.CompletedTask;
22+
23+
protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) =>
24+
cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) :
25+
SerializeToStreamAsync(stream, context);
26+
27+
protected override Task<Stream> CreateContentReadStreamAsync() =>
28+
Task.FromResult<Stream>(EmptyReadStream.Instance);
29+
30+
protected override Task<Stream> CreateContentReadStreamAsync(CancellationToken cancellationToken) =>
31+
cancellationToken.IsCancellationRequested ? Task.FromCanceled<Stream>(cancellationToken) :
32+
CreateContentReadStreamAsync();
33+
34+
internal override Stream? TryCreateContentReadStream() => EmptyReadStream.Instance;
35+
36+
internal override bool AllowDuplex => false;
37+
}
38+
}

src/libraries/System.Net.Http/src/System/Net/Http/HttpResponseMessage.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5+
using System.Diagnostics.CodeAnalysis;
56
using System.Net.Http.Headers;
67
using System.Text;
78

@@ -39,9 +40,10 @@ public Version Version
3940

4041
internal void SetVersionWithoutValidation(Version value) => _version = value;
4142

42-
public HttpContent? Content
43+
[AllowNull]
44+
public HttpContent Content
4345
{
44-
get { return _content; }
46+
get { return _content ??= new EmptyContent(); }
4547
set
4648
{
4749
CheckDisposed();

src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,10 @@ public async Task GetContentAsync_NullResponseContent_ReturnsDefaultValue()
221221
{
222222
Assert.Same(string.Empty, await client.GetStringAsync(CreateFakeUri()));
223223
Assert.Same(Array.Empty<byte>(), await client.GetByteArrayAsync(CreateFakeUri()));
224-
Assert.Same(Stream.Null, await client.GetStreamAsync(CreateFakeUri()));
224+
225+
Stream s = await client.GetStreamAsync(CreateFakeUri());
226+
Assert.NotNull(s);
227+
Assert.Equal(-1, s.ReadByte());
225228
}
226229
}
227230

src/libraries/System.Net.Http/tests/FunctionalTests/HttpResponseMessageTest.cs

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5-
using System;
65
using System.IO;
7-
using System.Net.Http.Headers;
8-
using System.Threading;
96
using System.Threading.Tasks;
107

118
using Xunit;
@@ -22,7 +19,7 @@ public void Ctor_Default_CorrectDefaults()
2219
Assert.Equal(HttpStatusCode.OK, rm.StatusCode);
2320
Assert.Equal("OK", rm.ReasonPhrase);
2421
Assert.Equal(new Version(1, 1), rm.Version);
25-
Assert.Null(rm.Content);
22+
Assert.NotNull(rm.Content);
2623
Assert.Null(rm.RequestMessage);
2724
}
2825
}
@@ -35,7 +32,7 @@ public void Ctor_SpecifiedValues_CorrectValues()
3532
Assert.Equal(HttpStatusCode.Accepted, rm.StatusCode);
3633
Assert.Equal("Accepted", rm.ReasonPhrase);
3734
Assert.Equal(new Version(1, 1), rm.Version);
38-
Assert.Null(rm.Content);
35+
Assert.NotNull(rm.Content);
3936
Assert.Null(rm.RequestMessage);
4037
}
4138
}
@@ -232,8 +229,15 @@ public void Content_SetToNull_Accepted()
232229
{
233230
using (var rm = new HttpResponseMessage())
234231
{
232+
HttpContent c1 = rm.Content;
233+
Assert.Same(c1, rm.Content);
234+
235235
rm.Content = null;
236-
Assert.Null(rm.Content);
236+
237+
HttpContent c2 = rm.Content;
238+
Assert.Same(c2, rm.Content);
239+
240+
Assert.NotSame(c1, c2);
237241
}
238242
}
239243

@@ -249,6 +253,35 @@ public void StatusCode_InvalidStatusCodeRange_ThrowsArgumentOutOfRangeException(
249253
}
250254
}
251255

256+
[Fact]
257+
public async Task DefaultContent_ReadableNotWritable_Success()
258+
{
259+
var resp = new HttpResponseMessage();
260+
261+
HttpContent c = resp.Content;
262+
Assert.NotNull(c);
263+
Assert.Same(c, resp.Content);
264+
Assert.NotSame(resp.Content, new HttpResponseMessage().Content);
265+
266+
Assert.Equal(0, c.Headers.ContentLength);
267+
268+
Task<Stream> t = c.ReadAsStreamAsync();
269+
Assert.Equal(TaskStatus.RanToCompletion, t.Status);
270+
271+
Stream s = await t;
272+
Assert.NotNull(s);
273+
274+
Assert.Equal(-1, s.ReadByte());
275+
Assert.Equal(0, s.Read(new byte[1], 0, 1));
276+
Assert.Equal(0, await s.ReadAsync(new byte[1], 0, 1));
277+
Assert.Equal(0, await s.ReadAsync(new Memory<byte>(new byte[1])));
278+
279+
Assert.Throws<NotSupportedException>(() => s.WriteByte(0));
280+
Assert.Throws<NotSupportedException>(() => s.Write(new byte[1], 0, 1));
281+
await Assert.ThrowsAsync<NotSupportedException>(() => s.WriteAsync(new byte[1], 0, 1));
282+
await Assert.ThrowsAsync<NotSupportedException>(async () => await s.WriteAsync(new ReadOnlyMemory<byte>(new byte[1])));
283+
}
284+
252285
[Fact]
253286
public void ToString_DefaultAndNonDefaultInstance_DumpAllFields()
254287
{

src/libraries/System.Net.Http/tests/StressTests/HttpStress/ClientOperations.cs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ public static (string name, Func<RequestContext, Task> operation)[] Operations =
192192
using HttpResponseMessage m = await ctx.SendAsync(req);
193193

194194
ValidateStatusCode(m);
195-
ValidateServerContent(await m.Content!.ReadAsStringAsync(), expectedLength);
195+
ValidateServerContent(await m.Content.ReadAsStringAsync(), expectedLength);
196196
}),
197197

198198
("GET Partial",
@@ -204,7 +204,7 @@ public static (string name, Func<RequestContext, Task> operation)[] Operations =
204204

205205
ValidateStatusCode(m);
206206

207-
using (Stream s = await m.Content!.ReadAsStreamAsync())
207+
using (Stream s = await m.Content.ReadAsStreamAsync())
208208
{
209209
s.ReadByte(); // read single byte from response and throw the rest away
210210
}
@@ -221,7 +221,7 @@ public static (string name, Func<RequestContext, Task> operation)[] Operations =
221221

222222
ValidateStatusCode(res);
223223

224-
await res.Content!.ReadAsStringAsync();
224+
await res.Content.ReadAsStringAsync();
225225

226226
bool isValidChecksum = ValidateServerChecksum(res.Headers, expectedChecksum);
227227
string failureDetails = isValidChecksum ? "server checksum matches client checksum" : "server checksum mismatch";
@@ -273,7 +273,7 @@ public static (string name, Func<RequestContext, Task> operation)[] Operations =
273273
using HttpResponseMessage m = await ctx.SendAsync(req);
274274

275275
ValidateStatusCode(m);
276-
ValidateContent(expectedResponse, await m.Content!.ReadAsStringAsync(), $"Uri: {uri}");
276+
ValidateContent(expectedResponse, await m.Content.ReadAsStringAsync(), $"Uri: {uri}");
277277
}),
278278

279279
("GET Aborted",
@@ -332,7 +332,7 @@ public static (string name, Func<RequestContext, Task> operation)[] Operations =
332332

333333
ValidateStatusCode(m);
334334
string checksumMessage = ValidateServerChecksum(m.Headers, checksum) ? "server checksum matches client checksum" : "server checksum mismatch";
335-
ValidateContent(content, await m.Content!.ReadAsStringAsync(), checksumMessage);
335+
ValidateContent(content, await m.Content.ReadAsStringAsync(), checksumMessage);
336336
}),
337337

338338
("POST Multipart Data",
@@ -346,7 +346,7 @@ public static (string name, Func<RequestContext, Task> operation)[] Operations =
346346

347347
ValidateStatusCode(m);
348348
string checksumMessage = ValidateServerChecksum(m.Headers, checksum) ? "server checksum matches client checksum" : "server checksum mismatch";
349-
ValidateContent(formData.expected, await m.Content!.ReadAsStringAsync(), checksumMessage);
349+
ValidateContent(formData.expected, await m.Content.ReadAsStringAsync(), checksumMessage);
350350
}),
351351

352352
("POST Duplex",
@@ -359,7 +359,7 @@ public static (string name, Func<RequestContext, Task> operation)[] Operations =
359359
using HttpResponseMessage m = await ctx.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
360360

361361
ValidateStatusCode(m);
362-
string response = await m.Content!.ReadAsStringAsync();
362+
string response = await m.Content.ReadAsStringAsync();
363363

364364
string checksumMessage = ValidateServerChecksum(m.TrailingHeaders, checksum, required: false) ? "server checksum matches client checksum" : "server checksum mismatch";
365365
ValidateContent(content, await m.Content.ReadAsStringAsync(), checksumMessage);
@@ -376,7 +376,7 @@ public static (string name, Func<RequestContext, Task> operation)[] Operations =
376376
using HttpResponseMessage m = await ctx.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
377377

378378
ValidateStatusCode(m);
379-
string response = await m.Content!.ReadAsStringAsync();
379+
string response = await m.Content.ReadAsStringAsync();
380380

381381
// trailing headers not supported for all servers, so do not require checksums
382382
bool isValidChecksum = ValidateServerChecksum(m.TrailingHeaders, checksum, required: false);
@@ -416,7 +416,7 @@ public static (string name, Func<RequestContext, Task> operation)[] Operations =
416416

417417
ValidateStatusCode(m);
418418
string checksumMessage = ValidateServerChecksum(m.Headers, checksum) ? "server checksum matches client checksum" : "server checksum mismatch";
419-
ValidateContent(content, await m.Content!.ReadAsStringAsync(), checksumMessage);
419+
ValidateContent(content, await m.Content.ReadAsStringAsync(), checksumMessage);
420420
}),
421421

422422
("HEAD",
@@ -428,7 +428,7 @@ public static (string name, Func<RequestContext, Task> operation)[] Operations =
428428

429429
ValidateStatusCode(m);
430430

431-
if (m.Content!.Headers.ContentLength != expectedLength)
431+
if (m.Content.Headers.ContentLength != expectedLength)
432432
{
433433
throw new Exception($"Expected {expectedLength}, got {m.Content.Headers.ContentLength}");
434434
}
@@ -446,7 +446,7 @@ public static (string name, Func<RequestContext, Task> operation)[] Operations =
446446

447447
ValidateStatusCode(m);
448448

449-
string r = await m.Content!.ReadAsStringAsync();
449+
string r = await m.Content.ReadAsStringAsync();
450450
if (r != "") throw new Exception($"Got unexpected response: {r}");
451451
}),
452452

@@ -460,7 +460,7 @@ public static (string name, Func<RequestContext, Task> operation)[] Operations =
460460

461461
ValidateStatusCode(m);
462462

463-
string r = await m.Content!.ReadAsStringAsync();
463+
string r = await m.Content.ReadAsStringAsync();
464464
if (r != "") throw new Exception($"Got unexpected response: {r}");
465465
}),
466466

@@ -472,7 +472,7 @@ public static (string name, Func<RequestContext, Task> operation)[] Operations =
472472
using HttpResponseMessage m = await ctx.SendAsync(req);
473473

474474
ValidateStatusCode(m);
475-
ValidateServerContent(await m.Content!.ReadAsStringAsync(), expectedLength);
475+
ValidateServerContent(await m.Content.ReadAsStringAsync(), expectedLength);
476476
}),
477477
};
478478

0 commit comments

Comments
 (0)