Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Including existing registrations in HttpRequestNotInterceptedException #855

Open
drewburlingame opened this issue Jul 15, 2024 · 2 comments
Labels
feature-request A request for new functionality

Comments

@drewburlingame
Copy link

When first using this library, we found it difficult to debug issues when a registration wasn't found that we expected. We use Refit and found that sometimes the mismatch was due to order of url params or encoding issues. We also have test infra to configure common calls like auth sequences for various integrations.

We ended up extending IntereceptingHttpMessageHandler to enrich the HttpRequestNotInterceptedException with a list of registered interceptions. Below is the handler we created. It's not perfect since it can't easily report custom matching, but it significantly improved our experience of troubleshooting missing registrations. I've been intending to offer this as a PR but I don't think I'll have time soon so wanted to offer this for anyone it might benefit or would like to use for a PR.

internal class FriendlierErrorInterceptingHttpMessageHandler(HttpClientInterceptorOptions options)
    : InterceptingHttpMessageHandler(options)
{
    internal readonly HttpClientInterceptorOptions Options = options;

    internal Task<HttpResponseMessage> SendAsyncInternal(HttpRequestMessage request,
        CancellationToken cancellationToken) =>
        SendAsync(request, cancellationToken);

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        try
        {
            return await base.SendAsync(request, cancellationToken);
        }
        catch (HttpRequestNotInterceptedException e)
        {
            var registeredMappings = GetRegisteredMappings().ToOrderedDelimitedString(Environment.NewLine);
            var unescapedDataString = Uri.UnescapeDataString(request.RequestUri!.AbsoluteUri);
            var recordableOptions = Options as RecordableHttpClientInterceptorOptions;
            var msg = $"No HTTP response is configured for {recordableOptions?.TestFilePath}" +
                      $"\n\n{request.Method.Method} {request.RequestUri!.AbsoluteUri}" +
                      $"\n({unescapedDataString})";
            throw e.Request is null
                ? new HttpRequestNotInterceptedException($"{msg}\n\nRegistered Mappings:\n\n{registeredMappings}")
                : new HttpRequestNotInterceptedException($"{msg}\n\nRegistered Mappings:\n\n{registeredMappings}", e.Request);
        }
    }

    private IEnumerable<string> GetRegisteredMappings()
    {
        var mappings = (IDictionary)typeof(HttpClientInterceptorOptions)
            .GetField("_mappings", BindingFlags.Instance | BindingFlags.NonPublic)!
            .GetValue(Options)!;

        if(mappings.Count == 0)
            return Array.Empty<string>();

        // Keys prefixed with CUSTOM: will not show the query params of the URI.
        // We will append them at the end of the method.
        var registrations = mappings.Values.Cast<object>().ToCollection();

        // sealed internal class: HttpInterceptionResponse
        var type = registrations.First().GetType();
        Dictionary<string, PropertyInfo> propertyInfos = type
            .GetProperties(BindingFlags.Instance|BindingFlags.NonPublic)
            .ToDictionary(d => d.Name);

        string? GetValue(string propertyName, object o) => propertyInfos[propertyName].GetValue(o)?.ToString();
        string? IfExists(string propertyName, object o, string text) =>
            propertyInfos[propertyName].GetValue(o) != null ? $" {text}" : null;
        string? IfTrue(string propertyName, object o, string text) =>
            ((bool?)propertyInfos[propertyName].GetValue(o)).GetValueOrDefault() ? $" {text}" : null;

        return registrations
            .Select(o => $"{GetValue("Method", o)} {GetValue("RequestUri", o)}" +
                         $"{IfExists("ContentMatcher", o, "+content-matching")}" +
                         $"{IfExists("UserMatcher", o, "+user-matching")}" +
                         $"{IfTrue("IgnoreHost", o, "ignore-host")}" +
                         $"{IfTrue("IgnorePath", o, "ignore-path")}" +
                         $"{IfTrue("IgnoreQuery", o, "ignore-query")}")
            .Concat(HttpRequestInterceptionBuilderExtensions.GetCurrentTestUnescapedQueries());
    }
}
@drewburlingame drewburlingame added the feature-request A request for new functionality label Jul 15, 2024
@martincostello
Copy link
Member

Thanks for this.

I wonder if a better way to build this in at some point would be to add some APIs related to introspection (which would maybe fit with some one-day aim to improve logging/debugging), and then something like this could build on top of that.

@drewburlingame
Copy link
Author

This is definitely a hack and there are better ways to provide this if done from within the framework. Providing this level of detail is also probably not necessary for all consumers of the package. I could see making it an option and/or lazily resolved property on the exception.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature-request A request for new functionality
Projects
None yet
Development

No branches or pull requests

2 participants