Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit b34c439

Browse files
author
Liudmila Molkova
committed
Send notification about HTTP response without content (netfx)
1 parent 7c03c54 commit b34c439

File tree

2 files changed

+141
-33
lines changed

2 files changed

+141
-33
lines changed

src/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/HttpHandlerDiagnosticListener.cs

Lines changed: 102 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,31 @@ public override void RemoveAt(int index)
539539
{
540540
s_instance.RaiseResponseEvent(request, response);
541541
}
542+
else
543+
{
544+
// In case reponse content length is 0 and request is async,
545+
// we won't have a HttpWebResponse set on request object when this method is called
546+
// http://referencesource.microsoft.com/#System/net/System/Net/HttpWebResponse.cs,525
547+
548+
// But we there will be CoreResponseData object that is either exception
549+
// or the internal HTTP reponse representation having status, content and headers
550+
551+
var coreResponse = s_coreResponseAccessor(request);
552+
if (coreResponse != null && s_coreResponseDataType.IsInstanceOfType(coreResponse))
553+
{
554+
HttpStatusCode status = s_coreStatusCodeAccessor(coreResponse);
555+
WebHeaderCollection headers = s_coreHeadersAccessor(coreResponse);
556+
557+
// Manual creation of HttpWebResponse here is not possible as this method is eventually called from the
558+
// HttpWebResponse ctor. So we will send Stop event with the Status and Headers payload
559+
// to notify listeners about response;
560+
// We use two different names for Stop events since one event with payload type that varies creates
561+
// complications for efficient payload parsing and is not supported by DiagnosicSource helper
562+
// libraries (e.g. Microsoft.Extensions.DiagnosticAdapter)
563+
564+
s_instance.RaiseResponseEvent(request, status, headers);
565+
}
566+
}
542567
}
543568

544569
base.RemoveAt(index);
@@ -606,22 +631,33 @@ private void RaiseResponseEvent(HttpWebRequest request, HttpWebResponse response
606631
// Response event could be received several times for the same request in case it was redirected
607632
// IsLastResponse checks if response is the last one (no more redirects will happen)
608633
// based on response StatusCode and number or redirects done so far
609-
if (request.Headers[RequestIdHeaderName] != null && IsLastResponse(request, response))
634+
if (request.Headers[RequestIdHeaderName] != null && IsLastResponse(request, response.StatusCode))
610635
{
611636
// only send Stop if request was instrumented
612637
this.Write(RequestStopName, new { Request = request, Response = response });
613638
}
614639
}
615640

616-
private bool IsLastResponse(HttpWebRequest request, HttpWebResponse response)
641+
private void RaiseResponseEvent(HttpWebRequest request, HttpStatusCode statusCode, WebHeaderCollection headers)
642+
{
643+
// Response event could be received several times for the same request in case it was redirected
644+
// IsLastResponse checks if response is the last one (no more redirects will happen)
645+
// based on response StatusCode and number or redirects done so far
646+
if (request.Headers[RequestIdHeaderName] != null && IsLastResponse(request, statusCode))
647+
{
648+
this.Write(RequestStopExName, new { Request = request, StatusCode = statusCode, Headers = headers });
649+
}
650+
}
651+
652+
private bool IsLastResponse(HttpWebRequest request, HttpStatusCode statusCode)
617653
{
618654
if (request.AllowAutoRedirect)
619655
{
620-
if (response.StatusCode == HttpStatusCode.Ambiguous || // 300
621-
response.StatusCode == HttpStatusCode.Moved || // 301
622-
response.StatusCode == HttpStatusCode.Redirect || // 302
623-
response.StatusCode == HttpStatusCode.RedirectMethod || // 303
624-
response.StatusCode == HttpStatusCode.RedirectKeepVerb) // 307
656+
if (statusCode == HttpStatusCode.Ambiguous || // 300
657+
statusCode == HttpStatusCode.Moved || // 301
658+
statusCode == HttpStatusCode.Redirect || // 302
659+
statusCode == HttpStatusCode.RedirectMethod || // 303
660+
statusCode == HttpStatusCode.RedirectKeepVerb) // 307
625661
{
626662
return s_autoRedirectsAccessor(request) >= request.MaximumAutomaticRedirections;
627663
}
@@ -642,40 +678,27 @@ private static void PrepareReflectionObjects()
642678
s_connectionType = systemNetHttpAssembly?.GetType("System.Net.Connection");
643679
s_writeListField = s_connectionType?.GetField("m_WriteList", BindingFlags.Instance | BindingFlags.NonPublic);
644680

645-
// Second step: Generate an accessor for HttpWebRequest._HttpResponse
646-
FieldInfo responseField = typeof(HttpWebRequest).GetField("_HttpResponse", BindingFlags.NonPublic | BindingFlags.Instance);
647-
if (responseField != null)
648-
{
649-
string methodName = responseField.ReflectedType.FullName + ".get_" + responseField.Name;
650-
DynamicMethod getterMethod = new DynamicMethod(methodName, typeof(HttpWebResponse), new Type[] { typeof(HttpWebRequest) }, true);
651-
ILGenerator generator = getterMethod.GetILGenerator();
652-
generator.Emit(OpCodes.Ldarg_0);
653-
generator.Emit(OpCodes.Ldfld, responseField);
654-
generator.Emit(OpCodes.Ret);
655-
s_httpResponseAccessor = (Func<HttpWebRequest, HttpWebResponse>)getterMethod.CreateDelegate(typeof(Func<HttpWebRequest, HttpWebResponse>));
656-
}
681+
s_httpResponseAccessor = CreateFieldGetter<HttpWebRequest, HttpWebResponse>("_HttpResponse", BindingFlags.NonPublic | BindingFlags.Instance);
682+
s_autoRedirectsAccessor = CreateFieldGetter<HttpWebRequest, int>("_AutoRedirects", BindingFlags.NonPublic | BindingFlags.Instance);
683+
s_coreResponseAccessor = CreateFieldGetter<HttpWebRequest, object>("_CoreResponse", BindingFlags.NonPublic | BindingFlags.Instance);
657684

658-
// Third step: Generate an accessor for HttpWebRequest._AutoRedirects
659-
FieldInfo redirectsField = typeof(HttpWebRequest).GetField("_AutoRedirects", BindingFlags.NonPublic | BindingFlags.Instance);
660-
if (redirectsField != null)
685+
s_coreResponseDataType = systemNetHttpAssembly?.GetType("System.Net.CoreResponseData");
686+
if (s_coreResponseDataType != null)
661687
{
662-
string methodName = redirectsField.ReflectedType.FullName + ".get_" + redirectsField.Name;
663-
DynamicMethod getterMethod = new DynamicMethod(methodName, typeof(int), new Type[] { typeof(HttpWebRequest) }, true);
664-
ILGenerator generator = getterMethod.GetILGenerator();
665-
generator.Emit(OpCodes.Ldarg_0);
666-
generator.Emit(OpCodes.Ldfld, redirectsField);
667-
generator.Emit(OpCodes.Ret);
668-
s_autoRedirectsAccessor = (Func<HttpWebRequest, int>)getterMethod.CreateDelegate(typeof(Func<HttpWebRequest, int>));
688+
s_coreStatusCodeAccessor = CreateFieldGetter<HttpStatusCode>(s_coreResponseDataType, "m_StatusCode", BindingFlags.Public | BindingFlags.Instance);
689+
s_coreHeadersAccessor = CreateFieldGetter<WebHeaderCollection>(s_coreResponseDataType, "m_ResponseHeaders", BindingFlags.Public | BindingFlags.Instance);
669690
}
670-
671691
// Double checking to make sure we have all the pieces initialized
672692
if (s_connectionGroupListField == null ||
673693
s_connectionGroupType == null ||
674694
s_connectionListField == null ||
675695
s_connectionType == null ||
676696
s_writeListField == null ||
677697
s_httpResponseAccessor == null ||
678-
s_autoRedirectsAccessor == null)
698+
s_autoRedirectsAccessor == null ||
699+
s_coreResponseDataType == null ||
700+
s_coreStatusCodeAccessor == null ||
701+
s_coreHeadersAccessor == null)
679702
{
680703
// If anything went wrong here, just return false. There is nothing we can do.
681704
throw new InvalidOperationException("Unable to initialize all required reflection objects");
@@ -697,7 +720,48 @@ private static void PerformInjection()
697720
servicePointTableField.SetValue(null, newTable);
698721
}
699722

700-
#endregion
723+
private static Func<TClass, TField> CreateFieldGetter<TClass, TField>(string fieldName, BindingFlags flags) where TClass : class
724+
{
725+
FieldInfo field = typeof(TClass).GetField(fieldName, flags);
726+
if (field != null)
727+
{
728+
string methodName = field.ReflectedType.FullName + ".get_" + field.Name;
729+
DynamicMethod getterMethod = new DynamicMethod(methodName, typeof(TField), new [] { typeof(TClass) }, true);
730+
ILGenerator generator = getterMethod.GetILGenerator();
731+
generator.Emit(OpCodes.Ldarg_0);
732+
generator.Emit(OpCodes.Ldfld, field);
733+
generator.Emit(OpCodes.Ret);
734+
return (Func<TClass, TField>)getterMethod.CreateDelegate(typeof(Func<TClass, TField>));
735+
}
736+
737+
return null;
738+
}
739+
740+
741+
/// <summary>
742+
/// Creates getter for a field defined in private or internal type
743+
/// repesented with classType variable
744+
/// </summary>
745+
private static Func<object, TField> CreateFieldGetter<TField>(Type classType, string fieldName, BindingFlags flags)
746+
{
747+
FieldInfo field = classType.GetField(fieldName, flags);
748+
if (field != null)
749+
{
750+
string methodName = classType.FullName + ".get_" + field.Name;
751+
DynamicMethod getterMethod = new DynamicMethod(methodName, typeof(TField), new [] { typeof(object) }, true);
752+
ILGenerator generator = getterMethod.GetILGenerator();
753+
generator.Emit(OpCodes.Ldarg_0);
754+
generator.Emit(OpCodes.Castclass, classType);
755+
generator.Emit(OpCodes.Ldfld, field);
756+
generator.Emit(OpCodes.Ret);
757+
758+
return (Func<object, TField>)getterMethod.CreateDelegate(typeof(Func<object, TField>));
759+
}
760+
761+
return null;
762+
}
763+
764+
#endregion
701765

702766
internal static HttpHandlerDiagnosticListener s_instance = new HttpHandlerDiagnosticListener();
703767

@@ -706,6 +770,7 @@ private static void PerformInjection()
706770
private const string ActivityName = "System.Net.Http.Desktop.HttpRequestOut";
707771
private const string RequestStartName = "System.Net.Http.Desktop.HttpRequestOut.Start";
708772
private const string RequestStopName = "System.Net.Http.Desktop.HttpRequestOut.Stop";
773+
private const string RequestStopExName = "System.Net.Http.Desktop.HttpRequestOut.Ex.Stop";
709774
private const string InitializationFailed = "System.Net.Http.InitializationFailed";
710775
private const string RequestIdHeaderName = "Request-Id";
711776
private const string CorrelationContextHeaderName = "Correlation-Context";
@@ -721,7 +786,11 @@ private static void PerformInjection()
721786
private static FieldInfo s_writeListField;
722787
private static Func<HttpWebRequest, HttpWebResponse> s_httpResponseAccessor;
723788
private static Func<HttpWebRequest, int> s_autoRedirectsAccessor;
789+
private static Func<HttpWebRequest, object> s_coreResponseAccessor;
790+
private static Func<object, HttpStatusCode> s_coreStatusCodeAccessor;
791+
private static Func<object, WebHeaderCollection> s_coreHeadersAccessor;
792+
private static Type s_coreResponseDataType;
724793

725-
#endregion
794+
#endregion
726795
}
727796
}

src/System.Diagnostics.DiagnosticSource/tests/HttpHandlerDiagnosticListenerTests.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,45 @@ public async Task TestBasicReceiveAndResponseEvents()
134134
}
135135
}
136136

137+
/// <summary>
138+
/// Test to make sure we get both request and response events.
139+
/// </summary>
140+
[Fact]
141+
public async Task TestResponseWithoutContentEvents()
142+
{
143+
using (var eventRecords = new EventObserverAndRecorder())
144+
{
145+
// Send a random Http request to generate some events
146+
using (var client = new HttpClient())
147+
{
148+
(await client.GetAsync(Configuration.Http.RemoteEmptyContentServer)).Dispose();
149+
}
150+
151+
// We should have exactly one Start and one Stop event
152+
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key.EndsWith("Start")));
153+
Assert.Equal(1, eventRecords.Records.Count(rec => rec.Key.EndsWith("Stop")));
154+
Assert.Equal(2, eventRecords.Records.Count);
155+
156+
// Check to make sure: The first record must be a request, the next record must be a response.
157+
KeyValuePair<string, object> startEvent;
158+
Assert.True(eventRecords.Records.TryDequeue(out startEvent));
159+
Assert.Equal("System.Net.Http.Desktop.HttpRequestOut.Start", startEvent.Key);
160+
HttpWebRequest startRequest = ReadPublicProperty<HttpWebRequest>(startEvent.Value, "Request");
161+
Assert.NotNull(startRequest);
162+
Assert.NotNull(startRequest.Headers["Request-Id"]);
163+
164+
KeyValuePair<string, object> stopEvent;
165+
Assert.True(eventRecords.Records.TryDequeue(out stopEvent));
166+
Assert.Equal("System.Net.Http.Desktop.HttpRequestOut.Ex.Stop", stopEvent.Key);
167+
HttpWebRequest stopRequest = ReadPublicProperty<HttpWebRequest>(stopEvent.Value, "Request");
168+
Assert.Equal(startRequest, stopRequest);
169+
HttpStatusCode status = ReadPublicProperty<HttpStatusCode>(stopEvent.Value, "StatusCode");
170+
Assert.NotNull(status);
171+
172+
WebHeaderCollection headers = ReadPublicProperty<WebHeaderCollection>(stopEvent.Value, "Headers");
173+
Assert.NotNull(headers);
174+
}
175+
}
137176
/// <summary>
138177
/// Test that if request is redirected, it gets only one Start and one Stop event
139178
/// </summary>

0 commit comments

Comments
 (0)