HttpAbstractions 1.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. commit 0f4f1950f15323e7cda7665b7e277efdd6034531
  2. Author: David Fowler <[email protected]>
  3. Date: Tue Jun 26 11:49:12 2018 -0700
  4. Attempt to make it easier to detect when the request is done (#1021)
  5. - Today the async local reference to the HttpContext flows when the execution context is captured. When the http request has ended, the HttpContext property will return the reference to an invalid HttpContext instead of returning null. This change stores both the request id and the HttpContext and makes sure both match before returning anything valid.
  6. - This is still racy but should catch more cases of people doing bad things.
  7. - There will still be issue if people store the context in a local and use that reference instead of accessing it through the property getter but we can live with that.
  8. - Set the TraceIdentifier to null in the default HttpContextFactory
  9. - Added tests
  10. diff --git a/src/Microsoft.AspNetCore.Http/HttpContextAccessor.cs b/src/Microsoft.AspNetCore.Http/HttpContextAccessor.cs
  11. index 5a4676234cd..897c27f7345 100644
  12. --- a/src/Microsoft.AspNetCore.Http/HttpContextAccessor.cs
  13. +++ b/src/Microsoft.AspNetCore.Http/HttpContextAccessor.cs
  14. @@ -7,17 +7,20 @@ namespace Microsoft.AspNetCore.Http
  15. {
  16. public class HttpContextAccessor : IHttpContextAccessor
  17. {
  18. - private static AsyncLocal<HttpContext> _httpContextCurrent = new AsyncLocal<HttpContext>();
  19. + private static AsyncLocal<(string traceIdentifier, HttpContext context)> _httpContextCurrent = new AsyncLocal<(string traceIdentifier, HttpContext context)>();
  20. public HttpContext HttpContext
  21. {
  22. get
  23. {
  24. - return _httpContextCurrent.Value;
  25. + var value = _httpContextCurrent.Value;
  26. + // Only return the context if the stored request id matches the stored trace identifier
  27. + // context.TraceIdentifier is cleared by HttpContextFactory.Dispose.
  28. + return value.traceIdentifier == value.context?.TraceIdentifier ? value.context : null;
  29. }
  30. set
  31. {
  32. - _httpContextCurrent.Value = value;
  33. + _httpContextCurrent.Value = (value?.TraceIdentifier, value);
  34. }
  35. }
  36. }
  37. diff --git a/src/Microsoft.AspNetCore.Http/HttpContextFactory.cs b/src/Microsoft.AspNetCore.Http/HttpContextFactory.cs
  38. index 8236a388a56..c793ba402e8 100644
  39. --- a/src/Microsoft.AspNetCore.Http/HttpContextFactory.cs
  40. +++ b/src/Microsoft.AspNetCore.Http/HttpContextFactory.cs
  41. @@ -53,6 +53,10 @@ namespace Microsoft.AspNetCore.Http
  42. {
  43. _httpContextAccessor.HttpContext = null;
  44. }
  45. +
  46. + // Null out the TraceIdentifier here as a sign that this request is done,
  47. + // the HttpContextAcessor implementation relies on this to detect that the request is over
  48. + httpContext.TraceIdentifier = null;
  49. }
  50. }
  51. }
  52. \ No newline at end of file
  53. diff --git a/test/Microsoft.AspNetCore.Http.Tests/HttpContextAccessorTests.cs b/test/Microsoft.AspNetCore.Http.Tests/HttpContextAccessorTests.cs
  54. new file mode 100644
  55. index 00000000000..c1521b1bc34
  56. --- /dev/null
  57. +++ b/test/Microsoft.AspNetCore.Http.Tests/HttpContextAccessorTests.cs
  58. @@ -0,0 +1,197 @@
  59. +// Copyright (c) .NET Foundation. All rights reserved.
  60. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  61. +
  62. +using System;
  63. +using System.Collections.Generic;
  64. +using System.Linq;
  65. +using System.Net.WebSockets;
  66. +using System.Reflection;
  67. +using System.Security.Claims;
  68. +using System.Threading;
  69. +using System.Threading.Tasks;
  70. +using Microsoft.AspNetCore.Http.Features;
  71. +using Xunit;
  72. +
  73. +namespace Microsoft.AspNetCore.Http
  74. +{
  75. + public class HttpContextAccessorTests
  76. + {
  77. + [Fact]
  78. + public async Task HttpContextAccessor_GettingHttpContextReturnsHttpContext()
  79. + {
  80. + var accessor = new HttpContextAccessor();
  81. +
  82. + var context = new DefaultHttpContext();
  83. + context.TraceIdentifier = "1";
  84. + accessor.HttpContext = context;
  85. +
  86. + await Task.Delay(100);
  87. +
  88. + Assert.Same(context, accessor.HttpContext);
  89. + }
  90. +
  91. + [Fact]
  92. + public void HttpContextAccessor_GettingHttpContextWithOutSettingReturnsNull()
  93. + {
  94. + var accessor = new HttpContextAccessor();
  95. +
  96. + Assert.Null(accessor.HttpContext);
  97. + }
  98. +
  99. + [Fact]
  100. + public async Task HttpContextAccessor_GettingHttpContextReturnsNullHttpContextIfSetToNull()
  101. + {
  102. + var accessor = new HttpContextAccessor();
  103. +
  104. + var context = new DefaultHttpContext();
  105. + context.TraceIdentifier = "1";
  106. + accessor.HttpContext = context;
  107. +
  108. + var checkAsyncFlowTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
  109. + var waitForNullTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
  110. + var afterNullCheckTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
  111. +
  112. + ThreadPool.QueueUserWorkItem(async _ =>
  113. + {
  114. + // The HttpContext flows with the execution context
  115. + Assert.Same(context, accessor.HttpContext);
  116. +
  117. + checkAsyncFlowTcs.SetResult(null);
  118. +
  119. + await waitForNullTcs.Task;
  120. +
  121. + try
  122. + {
  123. + Assert.Null(accessor.HttpContext);
  124. +
  125. + afterNullCheckTcs.SetResult(null);
  126. + }
  127. + catch (Exception ex)
  128. + {
  129. + afterNullCheckTcs.SetException(ex);
  130. + }
  131. + });
  132. +
  133. + await checkAsyncFlowTcs.Task;
  134. +
  135. + // Null out the accessor
  136. + accessor.HttpContext = null;
  137. + context.TraceIdentifier = null;
  138. +
  139. + waitForNullTcs.SetResult(null);
  140. +
  141. + Assert.Null(accessor.HttpContext);
  142. +
  143. + await afterNullCheckTcs.Task;
  144. + }
  145. +
  146. + [Fact]
  147. + public async Task HttpContextAccessor_GettingHttpContextReturnsNullHttpContextIfDifferentTraceIdentifier()
  148. + {
  149. + var accessor = new HttpContextAccessor();
  150. +
  151. + var context = new DefaultHttpContext();
  152. + context.TraceIdentifier = "1";
  153. + accessor.HttpContext = context;
  154. +
  155. + var checkAsyncFlowTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
  156. + var waitForNullTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
  157. + var afterNullCheckTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
  158. +
  159. + ThreadPool.QueueUserWorkItem(async _ =>
  160. + {
  161. + // The HttpContext flows with the execution context
  162. + Assert.Same(context, accessor.HttpContext);
  163. +
  164. + checkAsyncFlowTcs.SetResult(null);
  165. +
  166. + await waitForNullTcs.Task;
  167. +
  168. + try
  169. + {
  170. + Assert.Null(accessor.HttpContext);
  171. +
  172. + afterNullCheckTcs.SetResult(null);
  173. + }
  174. + catch (Exception ex)
  175. + {
  176. + afterNullCheckTcs.SetException(ex);
  177. + }
  178. + });
  179. +
  180. + await checkAsyncFlowTcs.Task;
  181. +
  182. + // Reset the trace identifier on the first request
  183. + context.TraceIdentifier = null;
  184. +
  185. + // Set a new http context
  186. + var context2 = new DefaultHttpContext();
  187. + context2.TraceIdentifier = "2";
  188. + accessor.HttpContext = context2;
  189. +
  190. + waitForNullTcs.SetResult(null);
  191. +
  192. + Assert.Same(context2, accessor.HttpContext);
  193. +
  194. + await afterNullCheckTcs.Task;
  195. + }
  196. +
  197. + [Fact]
  198. + public async Task HttpContextAccessor_GettingHttpContextDoesNotFlowIfAccessorSetToNull()
  199. + {
  200. + var accessor = new HttpContextAccessor();
  201. +
  202. + var context = new DefaultHttpContext();
  203. + context.TraceIdentifier = "1";
  204. + accessor.HttpContext = context;
  205. +
  206. + var checkAsyncFlowTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
  207. +
  208. + accessor.HttpContext = null;
  209. +
  210. + ThreadPool.QueueUserWorkItem(_ =>
  211. + {
  212. + try
  213. + {
  214. + // The HttpContext flows with the execution context
  215. + Assert.Null(accessor.HttpContext);
  216. + checkAsyncFlowTcs.SetResult(null);
  217. + }
  218. + catch (Exception ex)
  219. + {
  220. + checkAsyncFlowTcs.SetException(ex);
  221. + }
  222. + });
  223. +
  224. + await checkAsyncFlowTcs.Task;
  225. + }
  226. +
  227. + [Fact]
  228. + public async Task HttpContextAccessor_GettingHttpContextDoesNotFlowIfExecutionContextDoesNotFlow()
  229. + {
  230. + var accessor = new HttpContextAccessor();
  231. +
  232. + var context = new DefaultHttpContext();
  233. + context.TraceIdentifier = "1";
  234. + accessor.HttpContext = context;
  235. +
  236. + var checkAsyncFlowTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
  237. +
  238. + ThreadPool.UnsafeQueueUserWorkItem(_ =>
  239. + {
  240. + try
  241. + {
  242. + // The HttpContext flows with the execution context
  243. + Assert.Null(accessor.HttpContext);
  244. + checkAsyncFlowTcs.SetResult(null);
  245. + }
  246. + catch (Exception ex)
  247. + {
  248. + checkAsyncFlowTcs.SetException(ex);
  249. + }
  250. + }, null);
  251. +
  252. + await checkAsyncFlowTcs.Task;
  253. + }
  254. + }
  255. +}
  256. \ No newline at end of file
  257. diff --git a/test/Microsoft.AspNetCore.Http.Tests/HttpContextFactoryTests.cs b/test/Microsoft.AspNetCore.Http.Tests/HttpContextFactoryTests.cs
  258. index ba983198e75..80e421273ab 100644
  259. --- a/test/Microsoft.AspNetCore.Http.Tests/HttpContextFactoryTests.cs
  260. +++ b/test/Microsoft.AspNetCore.Http.Tests/HttpContextFactoryTests.cs
  261. @@ -22,7 +22,27 @@ namespace Microsoft.AspNetCore.Http
  262. var context = contextFactory.Create(new FeatureCollection());
  263. // Assert
  264. - Assert.True(ReferenceEquals(context, accessor.HttpContext));
  265. + Assert.Same(context, accessor.HttpContext);
  266. + }
  267. +
  268. + [Fact]
  269. + public void DisposeHttpContextSetsHttpContextAccessorToNull()
  270. + {
  271. + // Arrange
  272. + var accessor = new HttpContextAccessor();
  273. + var contextFactory = new HttpContextFactory(Options.Create(new FormOptions()), accessor);
  274. +
  275. + // Act
  276. + var context = contextFactory.Create(new FeatureCollection());
  277. + var traceIdentifier = context.TraceIdentifier;
  278. +
  279. + // Assert
  280. + Assert.Same(context, accessor.HttpContext);
  281. +
  282. + contextFactory.Dispose(context);
  283. +
  284. + Assert.Null(accessor.HttpContext);
  285. + Assert.NotEqual(traceIdentifier, context.TraceIdentifier);
  286. }
  287. [Fact]