Skip to content

Commit 0b723d4

Browse files
Merge pull request #454 from martincostello/fix-361
Fix NullReferenceException when deregistering a builder with a custom matcher
2 parents f82597c + 2c232a1 commit 0b723d4

File tree

3 files changed

+156
-12
lines changed

3 files changed

+156
-12
lines changed

src/HttpClientInterception/HttpClientInterceptorOptions.cs

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,10 @@ public HttpClientInterceptorOptions Clone()
105105
OnMissingRegistration = OnMissingRegistration,
106106
OnSend = OnSend,
107107
ThrowOnMissingRegistration = ThrowOnMissingRegistration,
108+
_comparer = _comparer,
109+
_mappings = new ConcurrentDictionary<string, HttpInterceptionResponse>(_mappings, _comparer),
108110
};
109111

110-
clone._comparer = _comparer;
111-
clone._mappings = new ConcurrentDictionary<string, HttpInterceptionResponse>(_mappings, _comparer);
112-
113112
return clone;
114113
}
115114

@@ -158,6 +157,9 @@ public HttpClientInterceptorOptions Deregister(HttpMethod method, Uri uri)
158157
/// <exception cref="ArgumentNullException">
159158
/// <paramref name="builder"/> is <see langword="null"/>.
160159
/// </exception>
160+
/// <exception cref="InvalidOperationException">
161+
/// The HTTP registration associated with <paramref name="builder"/> could not be deregistered.
162+
/// </exception>
161163
/// <remarks>
162164
/// If <paramref name="builder"/> has been reconfigured since it was used
163165
/// to register a previous HTTP request interception it will not remove that
@@ -170,10 +172,22 @@ public HttpClientInterceptorOptions Deregister(HttpRequestInterceptionBuilder bu
170172
throw new ArgumentNullException(nameof(builder));
171173
}
172174

173-
HttpInterceptionResponse interceptor = builder.Build();
175+
// Use any key stored in the builder to deregister the interception,
176+
// if available, otherwise rebuild from the builder itself.
177+
string? key = builder.Key;
174178

175-
string key = BuildKey(interceptor);
176-
_mappings.Remove(key);
179+
if (key is null)
180+
{
181+
HttpInterceptionResponse interceptor = builder.Build();
182+
key = BuildKey(interceptor);
183+
}
184+
185+
bool removed = _mappings.Remove(key);
186+
187+
if (!removed)
188+
{
189+
throw new InvalidOperationException("Failed to deregister HTTP request interception. The builder has not been used to register an HTTP request or has been mutated since it was registered.");
190+
}
177191

178192
return this;
179193
}
@@ -232,7 +246,7 @@ public HttpClientInterceptorOptions RegisterByteArray(
232246
StatusCode = statusCode,
233247
};
234248

235-
ConfigureMatcherAndRegister(interceptor);
249+
_ = ConfigureMatcherAndRegister(interceptor);
236250

237251
return this;
238252
}
@@ -291,7 +305,7 @@ public HttpClientInterceptorOptions RegisterStream(
291305
StatusCode = statusCode,
292306
};
293307

294-
ConfigureMatcherAndRegister(interceptor);
308+
_ = ConfigureMatcherAndRegister(interceptor);
295309

296310
return this;
297311
}
@@ -315,7 +329,11 @@ public HttpClientInterceptorOptions Register(HttpRequestInterceptionBuilder buil
315329

316330
HttpInterceptionResponse interceptor = builder.Build();
317331

318-
ConfigureMatcherAndRegister(interceptor);
332+
string key = ConfigureMatcherAndRegister(interceptor);
333+
334+
// Store the key so deregistration for the builder works if
335+
// it is not mutated after the registration has occurred.
336+
builder.SetMatchKey(key);
319337

320338
return this;
321339
}
@@ -390,8 +408,8 @@ private static string BuildKey(HttpInterceptionResponse interceptor)
390408
if (interceptor.UserMatcher is not null || interceptor.ContentMatcher is not null)
391409
{
392410
// Use the internal matcher's hash code as UserMatcher (a delegate)
393-
// will always return the hash code. See https://stackoverflow.com/q/6624151/1064169
394-
return $"CUSTOM:{interceptor.InternalMatcher!.GetHashCode().ToString(CultureInfo.InvariantCulture)}";
411+
// will always return the same hash code. See https://stackoverflow.com/q/6624151/1064169
412+
return $"CUSTOM:{interceptor.InternalMatcher?.GetHashCode().ToString(CultureInfo.InvariantCulture)}";
395413
}
396414

397415
var builderForKey = new UriBuilder(interceptor.RequestUri!);
@@ -498,7 +516,7 @@ private static async Task<HttpResponseMessage> BuildResponseAsync(HttpRequestMes
498516
return new(false, null);
499517
}
500518

501-
private void ConfigureMatcherAndRegister(HttpInterceptionResponse registration)
519+
private string ConfigureMatcherAndRegister(HttpInterceptionResponse registration)
502520
{
503521
RequestMatcher matcher;
504522

@@ -514,7 +532,10 @@ private void ConfigureMatcherAndRegister(HttpInterceptionResponse registration)
514532
registration.InternalMatcher = matcher;
515533

516534
string key = BuildKey(registration);
535+
517536
_mappings[key] = registration;
537+
538+
return key;
518539
}
519540

520541
private sealed class OptionsScope : IDisposable

0 commit comments

Comments
 (0)