Code Monkey home page Code Monkey logo

httpclient-interception's Introduction

HttpClient Interception

A .NET Standard library for intercepting server-side HTTP dependencies.

NuGet version Build status

codecov OpenSSF Scorecard

Introduction

This library provides functionality for intercepting HTTP requests made using the HttpClient class in code targeting .NET Standard 2.0 (and later), and .NET Framework 4.7.2.

The primary use-case is for providing stub responses for use in tests for applications, such as an ASP.NET Core application, to drive your functional test scenarios.

The library is based around an implementation of DelegatingHandler, which can either be used directly as an implementation of HttpMessageHandler, or can be provided to instances of HttpClient. This also allows it to be registered via Dependency Injection to make it available for use in code under test without the application itself requiring any references to JustEat.HttpClientInterception or any custom abstractions of HttpClient.

This design means that no HTTP server needs to be hosted to proxy traffic to/from, so does not consume any additional system resources, such as needing to bind a port for HTTP traffic, making it lightweight to use.

Installation

To install the library from NuGet using the .NET SDK run:

dotnet add package JustEat.HttpClientInterception

Basic Examples

Request Interception

Fluent API

Below is a minimal example of intercepting an HTTP GET request to an API for a JSON resource to return a custom response using the fluent API:

// Arrange
var options = new HttpClientInterceptorOptions();
var builder = new HttpRequestInterceptionBuilder();

builder
    .Requests()
    .ForGet()
    .ForHttps()
    .ForHost("public.je-apis.com")
    .ForPath("terms")
    .Responds()
    .WithJsonContent(new { Id = 1, Link = "https://www.just-eat.co.uk/privacy-policy" })
    .RegisterWith(options);

using var client = options.CreateHttpClient();

// Act
// The value of json will be: {"Id":1, "Link":"https://www.just-eat.co.uk/privacy-policy"}
string json = await client.GetStringAsync("https://public.je-apis.com/terms");

snippet source | anchor

HttpRequestInterceptionBuilder objects are mutable, so properties can be freely changed once a particular setup has been registered with an instance of HttpClientInterceptorOptions as the state is captured at the point of registration. This allows multiple responses and paths to be configured from a single HttpRequestInterceptionBuilder instance where multiple registrations against a common hostname.

HTTP Bundle Files

HTTP requests to intercept can also be configured in an "HTTP bundle" file, which can be used to store the HTTP requests to intercept and their corresponding responses as JSON.

This functionality is analogous to our Shock pod for iOS.

JSON

Below is an example bundle file, which can return content in formats such as a string, JSON and base64-encoded data.

The full JSON schema for HTTP bundle files can be found here.

{
  "$schema": "https://raw.githubusercontent.com/justeattakeaway/httpclient-interception/main/src/HttpClientInterception/Bundles/http-request-bundle-schema.json",
  "id": "my-bundle",
  "comment": "A bundle of HTTP requests",
  "items": [
    {
      "id": "home",
      "comment": "Returns the home page",
      "uri": "https://www.just-eat.co.uk",
      "contentString": "<html><head><title>Just Eat</title></head></html>"
    },
    {
      "id": "terms",
      "comment": "Returns the Ts & Cs",
      "uri": "https://public.je-apis.com/terms",
      "contentFormat": "json",
      "contentJson": {
        "Id": 1,
        "Link": "https://www.just-eat.co.uk/privacy-policy"
      }
    }
  ]
}

snippet source | anchor

Code
// using JustEat.HttpClientInterception;

var options = new HttpClientInterceptorOptions().RegisterBundle("my-bundle.json");

var client = options.CreateHttpClient();

// The value of html will be "<html><head><title>Just Eat</title></head></html>"
var html = await client.GetStringAsync("https://www.just-eat.co.uk");

// The value of json will be "{\"Id\":1,\"Link\":\"https://www.just-eat.co.uk/privacy-policy\"}"
var json = await client.GetStringAsync("https://public.je-apis.com/terms");

Further examples of using HTTP bundles can be found in the tests, such as for changing the response code, the HTTP method, and matching to HTTP requests based on the request headers.

Fault Injection

Below is a minimal example of intercepting a request to inject an HTTP fault:

var options = new HttpClientInterceptorOptions();

var builder = new HttpRequestInterceptionBuilder()
    .Requests()
    .ForHost("public.je-apis.com")
    .WithStatus(HttpStatusCode.InternalServerError)
    .RegisterWith(options);

var client = options.CreateHttpClient();

// Throws an HttpRequestException
await Assert.ThrowsAsync<HttpRequestException>(
    () => client.GetStringAsync("http://public.je-apis.com"));

snippet source | anchor

Registering Request Interception When Using IHttpClientFactory

If you are using IHttpClientFactory to register HttpClient for Dependency Injection in a .NET application (or later), you can implement a custom IHttpMessageHandlerBuilderFilter to register during test setup, which makes an instance of HttpClientInterceptorOptions available to inject an HTTP handler.

A working example of this approach can be found in the sample application.

/// <summary>
/// A class that registers an intercepting HTTP message handler at the end of
/// the message handler pipeline when an <see cref="HttpClient"/> is created.
/// </summary>
public sealed class HttpClientInterceptionFilter(HttpClientInterceptorOptions options) : IHttpMessageHandlerBuilderFilter
{
    /// <inheritdoc/>
    public Action<HttpMessageHandlerBuilder> Configure(Action<HttpMessageHandlerBuilder> next)
    {
        return (builder) =>
        {
            // Run any actions the application has configured for itself
            next(builder);

            // Add the interceptor as the last message handler
            builder.AdditionalHandlers.Add(options.CreateHttpMessageHandler());
        };
    }
}

snippet source | anchor

Setting Up HttpClient for Dependency Injection Manually

Below is an example of setting up IServiceCollection to register HttpClient for Dependency Injection in a manner that allows tests to use HttpClientInterceptorOptions to intercept HTTP requests. Similar approaches can be used with other IoC containers.

You may wish to consider registering HttpClient as a singleton, rather than as transient, if you do not use properties such as BaseAddress on instances of HttpClient. This allows the same instance to be used throughout the application, which improves performance and resource utilisation under heavy server load. If using a singleton instance, ensure that you manage the lifetime of your message handlers appropriately so they are not disposed of incorrectly and update the registration for your HttpClient instance appropriately.

services.AddTransient(
    (serviceProvider) =>
    {
        // Create a handler that makes actual HTTP calls
        HttpMessageHandler handler = new HttpClientHandler();

        // Have any delegating handlers been registered?
        var handlers = serviceProvider
            .GetServices<DelegatingHandler>()
            .ToList();

        if (handlers.Count > 0)
        {
            // Attach the initial handler to the first delegating handler
            DelegatingHandler previous = handlers.First();
            previous.InnerHandler = handler;

            // Chain any remaining handlers to each other
            foreach (DelegatingHandler next in handlers.Skip(1))
            {
                next.InnerHandler = previous;
                previous = next;
            }

            // Replace the initial handler with the last delegating handler
            handler = previous;
        }

        // Create the HttpClient using the inner HttpMessageHandler
        return new HttpClient(handler);
    });

Then in the test project register HttpClientInterceptorOptions to provide an implementation of DelegatingHandler. If using a singleton for HttpClient as described above, update the registration for the tests appropriately so that the message handler is shared.

var options = new HttpClientInterceptorOptions();

var server = new WebHostBuilder()
    .UseStartup<Startup>()
    .ConfigureServices(
        (services) => services.AddTransient((_) => options.CreateHttpMessageHandler()))
    .Build();

server.Start();

Further Examples

Further examples of using the library can be found by following the links below:

  1. Example tests
  2. Sample application with tests
  3. This library's own tests

Benchmarks

Generated with the Benchmarks project using BenchmarkDotNet using commit ef93cb5 on 04/02/2024.


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22621.3007/22H2/2022Update/SunValley2)
12th Gen Intel Core i7-1270P, 1 CPU, 16 logical and 12 physical cores
.NET SDK 8.0.101
  [Host]     : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2
  Job-IMNGZO : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2

Arguments=/p:UseArtifactsOutput=false

Method Mean Error StdDev Median Gen0 Allocated
GetBytes 1.708 μs 0.0338 μs 0.0898 μs 1.698 μs 0.3223 2.98 KB
GetHtml 1.618 μs 0.0324 μs 0.0703 μs 1.614 μs 0.3319 3.06 KB
GetJsonDocument 1.942 μs 0.0381 μs 0.0604 μs 1.921 μs 0.3071 2.84 KB
GetJsonObject 2.460 μs 0.0778 μs 0.2195 μs 2.413 μs 0.3281 3.05 KB
GetJsonObjectSourceGenerator 2.345 μs 0.0421 μs 0.1102 μs 2.311 μs 0.3281 3.05 KB
GetJsonObjectWithRefit 5.169 μs 0.1025 μs 0.2117 μs 5.120 μs 0.6714 6.35 KB
GetStream 209.607 μs 4.5593 μs 13.3717 μs 210.077 μs 0.2441 3.16 KB

Feedback

Any feedback or issues can be added to the issues for this project in GitHub.

Repository

The repository is hosted in GitHub: https://github.com/justeattakeaway/httpclient-interception.git

Building and Testing

Compiling the library yourself requires Git and the .NET SDK to be installed (version 7.0.100 or later).

To build and test the library locally from a terminal/command-line, run one of the following set of commands:

git clone https://github.com/justeattakeaway/httpclient-interception.git
cd httpclient-interception
./build.ps1

License

This project is licensed under the Apache 2.0 license.

httpclient-interception's People

Contributors

alikhalili avatar azure-pipelines[bot] avatar demetrios-loutsios avatar dependabot-preview[bot] avatar dependabot[bot] avatar gareththackeray-je avatar github-actions[bot] avatar hwoodiwiss avatar jet-codeflow-maintainer[bot] avatar josephwoodward avatar martincostello avatar nukeeperbot avatar simoncropp avatar slang25 avatar westdiscgolf avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

httpclient-interception's Issues

Question: How to simulate a timeout/no response from downstream service

I want to write a test for validating behaviour of a class once it has made a call to downstream service that is currently down/unavailable (ie, it's never going to process the incoming request)

Am I right in thinking that you would do this as shown in this sample test:

https://github.com/justeat/httpclient-interception/blob/3632d2f3d62ff51e2e3e3119d254d0f6d35ab10f/tests/HttpClientInterception.Tests/Examples.cs#L284

or is there another way?

Remove Newtonsoft.Json

Consider removing Newtonsoft.Json as a dependency and just using System.Text.Json instead.

ILogger<T> Integration?

When IHttpClientFactory is used with this library, the other built-in message handlers log things. InterceptingHttpMessageHandler on the other hand has no logging at all, though it does have some hooks to allow the integrator to do some logging themselves.

Maybe it would be beneficial to provider a way to wire in ILogger<T>/ILoggerFactory so the message handler had some built-in logging of it's own? The downside to this is creating a dependency on the logging abstractions.

MissingMethodException when running on mono with newer Newtonsoft.Json

This is quite an exotic setup I know, but this has my tests failing in CI, so I'll be looking into this too.

I have an example here:
a776327#diff-ad2944e2db1bf6a75b8602f406da7846R13

When running under mono, if I call .WithJsonContent(... then it pops with a runtime exception:

Error Message:
   System.MissingMethodException : Method not found: JustEat.HttpClientInterception.HttpRequestInterceptionBuilder JustEat.HttpClientInterception.HttpRequestInterceptionBuilderExtensions.WithJsonContent(JustEat.HttpClientInterception.HttpRequestInterceptionBuilder,object,Newtonsoft.Json.JsonSerializerSettings)

In recreating this, I discovered that it's not an issue if you are referencing Newtonsoft.Json 9.0.1, but if you reference a higher version such as 12.x then this issue pops up.

after upgrade, build error

Visual Studio 2022

byte[] content = await response.ContentFactory!().ConfigureAwait(false) ?? []; line 479

internal sealed class StringSyntaxAttribute(string syntax, params object?[] arguments) : Attribute line10

WithResponseHeaders and WithContentHeaders overloads

Is your feature request related to a problem? Please describe.

I have an extension method which allows me to register responses for intercepted requests such that each subsequent request that matches, returns a different response content. I do this by using a closure of a Counter object, which gets incremented using WithInterceptionCallback, as follows:

[EditorBrowsable(EditorBrowsableState.Never)]
static class HttpRequestInterceptionBuilderExtensions
{
    public static HttpRequestInterceptionBuilder WithJsonResponses<T>(
        this HttpRequestInterceptionBuilder builder,
        Func<int, T, CancellationToken, Task> callback,
        T[] content,
        JsonSerializerOptions? options = null)
    {
        var counter = new Counter(content.Length);
        T item = default!;

        return builder
            .WithInterceptionCallback(async (_, cancellationToken) =>
            {
                counter.Increment();
                item = content[counter.Value - 1];
                await callback(counter.Value, item, cancellationToken);
            })
            .WithMediaType("application/json")
            .WithContent(() =>
            {
                if (item is Exception exception)
                    throw exception;
                    
                return JsonSerializer.SerializeToUtf8Bytes(item, options);
            });
    }

    class Counter
    {
        readonly int _maxValue;

        public Counter(int maxValue) => _maxValue = maxValue;

        public int Value { get; private set; }

        public void Increment()
        {
            if (Value == _maxValue) return;

            Value++;
        }
    }
}

I would like to do the same for the response headers and content headers as well. This requires new overloads for WithResponseHeaders and WithContentHeaders, and some associated code changes.

Describe the solution you'd like

New overloads for WithResponseHeaders and WithContentHeaders, which allow passing a Func<IDictionary<string, ICollection<string>>>.

Describe alternatives you've considered

No alternatives exist.

Additional context

The reason for requiring different responses to the same request is that the API being "mocked" is one that returns data which changes over time. My test suite is set up so that I can verify this behaviour.

Add ability to add multiple hosts

The Problem

I have some tests that run across multiple environments and I would like to codify that into the test. Initially I've left of the ForHost declaration but that introduces potential issues with it running against any service that matches the other builder routes configured.

Proposed Solution
I'm just wondering if there's been thought/discussions around adding the following overload to enable you to configure multiple hosts:

ForHost(params string[] host)

Alternatives

One alternative I was considering (which I feel could be a better option) is a regular expression overload as specifying multiple hosts feels a bit inconsistent with the idea of mocking out an HTTP endpoint (as opposed to many, which host implies).

Would be happy to contribute the PR if this is something you feel is valuable outside of my single use case.

Add [StringSyntax] attributes

Is your feature request related to a problem? Please describe.

A new attribute, [StringSyntax], is being added to .NET 7 that allows tools such as Visual Studio to provide syntax highlighting etc. for string variables that expect certain formats (JSON, URI, XML, Regex).

Describe the solution you'd like

Add [StringSyntax] to all methods where it would be appropriate. Examples include:

URI

https://github.com/justeat/httpclient-interception/blob/961bfc07a358f58ed2d1af6dace413ef826470e7/src/HttpClientInterception/HttpRequestInterceptionBuilderExtensions.cs#L224

Describe alternatives you've considered

None.

Additional context

Unless Visual Studio supports consuming the attribute by name rather than the concrete type, this would only work for .NET 7+.

Consider registering HttpClient as a Singleton, not Transient

Hi guys,

Thank you for sharing this really good idea, testing HttpClient is always a pain :)

One comment though: in the Sample and documentation the HttpClient is being registered as transient. That could cause a lot of problems with the server (performance, TCP/IP Port Exhaustion, etc.), as HttpClient is intended to be instantiated once and reused throughout the lifetime of an application or context.

Take a look here for more information:
https://docs.microsoft.com/en-us/azure/architecture/antipatterns/improper-instantiation/

Cheers!

Revise documentation covering usage together with IHttpClientFactory

I was reading through the documentation that describes how to use HttpClientInterception together with IHttpClientFactory, and I found the recommended solution to be a bit misleading and quite over-engineered, especially having to handroll your own implementation of IHttpMessageHandlerBuilderFilter.

I was able to make it work just by doing this:

var builder = new HttpRequestInterceptionBuilder();

builder.Requests()
       .ForGet()
       .ForAnyHost()
       .Responds()
       .WithStatus(HttpStatusCode.OK)
       .RegisterWith(options);

var services = new ServiceCollection();
services.AddHttpClient("myclient")
        .AddHttpMessageHandler(options.CreateHttpMessageHandler));    // 👈🏻 This line is all you need

var serviceProvider = services.BuildServiceProvider();

var httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>();
var client = httpClientFactory.CreateClient("myclient");

Perhaps the documentation could be updated to reflect this?

NullReferenceException when Deregister builder with a custom matcher

Hi Martin,

Deregister a builder from HttpClientInterceptorOptions object has not work correctly. This edge case has occurred when a custom matcher was added to a builder and then tried to deregister it from options.

This exception is raised because it should be configured matcher before calling buildKey in the Deregister method.

Here is a sample scenario for this bug:

var builder = new HttpRequestInterceptionBuilder()
    .Requests().For(async (request) => await Task.FromResult(true));

var options = new HttpClientInterceptorOptions()
    .Register(builder);

options.Deregister(builder); // NullReferenceException is raised

Improve custom matching

Issues #247 and #248 have highlighted that making conditional behaviour can be too difficult and unintuitive to achieve.

Consider implementing additional functionality to make it easy to add extra matching conditions to requests so that registrations for the same URL can be matched depending on arbitrary conditions. For example this could be used to cause fuzzing by having requests randomly fail.

Something like this where 50% of requests on average get rate-limited:

var builder200 = new HttpRequestInterceptionBuilder()
    .Requests()
    .ForHttps()
    .ForHost("api.github.com")
    .ForPath("orgs/justeat")
    .AndFor(() => DateTime.UtcNow.Millisecond % 2 != 0)
    .Responds()
    .WithJsonContent(new { id = 1516790, login = "justeat", url = "https://api.github.com/orgs/justeat" });

var builder429 = new HttpRequestInterceptionBuilder()
    .Requests()
    .ForHttps()
    .ForHost("api.github.com")
    .ForPath("orgs/justeat")
    .AndFor(() => DateTime.UtcNow.Millisecond % 2 == 0)
    .Responds()
    .WithStatus(HttpStatusCode.TooManyRequests)
    .WithJsonContent(new { error = "Too many requests" });

var options = new HttpClientInterceptorOptions()
    .Register(builder200)
    .Register(builder429);

Response sequence

I would like to have an option to setup a sequence of responses for certain request. It would certainly help with testing retry policies.

Describe the solution you'd like

Moq allows for user to setup a sequence of responses like:

consumerService.SetupSequence(c => c.Consume(It.IsAny<CancellationToken>()))
    .Returns(payload)
    .Throws<OperationCanceledException>()
    .Returns(payload);

It would be great if we would be able to do the same in this library:

builder
    .Requests()
    .ForPost()
    .ForHttps()
    .ForHost(host)
    .ForPath(endpoint)
    .RespondsInSequence()
        .WithStatus(HttpStatusCode.InternalServerError)
    .Then()
        .WithStatus(HttpStatusCode.InternalServerError)
    .Then()
        .WithStatus(HttpStatusCode.OK)
    .RegisterWith(options);

Fix remote certificate is invalid for https

Is your feature request related to a problem? Please describe.

How-to fix System.Security.Authentication.AuthenticationException: The remote certificate is invalid according to the validation procedure: RemoteCertificateNameMismatch ?

Describe the solution you'd like

https://stackoverflow.com/questions/72211623/how-to-fix-remote-certificate-is-invalid-according-to-the-validation-procedure
https://stackoverflow.com/questions/9983265/the-remote-certificate-is-invalid-according-to-the-validation-procedure
ServicePointManager.ServerCertificateValidationCallback += (o, c, ch, er) => true;

Describe alternatives you've considered

ForHttpsIgnoreCertificate method

Additional context

Verify calls to the registered end points have been called [discussion]

There are times where we are setting up a number of requests to match in a test and as part of using the tooling to write the functionality we want to know when the setup calls have not been called when they should have been. So it can be used in a "test first" type of a way to make sure the setup urls are called when the fix has been developed. The ability to throw exceptions when calls are made which aren't configured is great, it's the opposite side of that functionality as I want it to fail or at least check when they're configured and not called.

Describe the solution you'd like
Some sort of public API which allows for verifying the registered calls have been called as part of the request and/or a way to verify all the calls which have been registered have been called.

Initial thoughts would be similar to the Verify behaviour functionality on Moq - https://github.com/moq/moq4/blob/7848788b5968eb29056599db91f9e9b9d16f9ba9/src/Moq/Mock.cs#L255

Describe alternatives you've considered
I was thinking you could create custom content predicates which when matched could keep track of the url and request. Then using the lookup functionality at the end of the processing you could manually check that all the entries had been called as expected. The downside of this it would be custom for each test and would have to be manually setup each time.

I was thinking maybe it could be based off the key creation routine and how the registrations are added to the mapping dictionary and then when they are called through the GetResponseAsync method it could be set as matched. Then could either pass in the builder instance for the request (so it could generate the key again) to check, or some other mechanism to verify that the call had been requested.

The other check that it would need to do would be the ability to use some sort of predicate to make sure the correct call had been requested. For example when the same url needs to be called multiple times with different payloads. So I was thinking it could interrogate the payload as well so you could match it in the same way that you can register predicates in ForContent to match on the content as well as the url etc.

Maybe add an override to the Register method to something like ...

public HttpClientInterceptorOptions Register(HttpRequestInterceptionBuilder builder, out string registrationKey)

So on each registration you can get access to the key value which has been used so you can then request back in later but not overly a fan of this API change.

Thoughts

Just wanted to add this here in case there was an appetite to do something like this. I would be happy to contribute a PR if I could get some guidance on naming and approach to make sure it fitted with the plan for the library.

Test Failures Due to Authorization Issues After Upgrading to .NET 8 and HttpClient-Interception

We recently upgraded our project to .NET 8 and updated the HttpClient-Interception package to its latest version. Following this upgrade, we are encountering an issue where multiple tests are failing due to authorization failures.

Environment
.NET Version: 8.0
HttpClient-Interception Version: 4.1.0
OS: Windows

Issue Description
Prior to the upgrade, all our tests were passing consistently. The tests that are failing are specifically those that rely on HttpClient-Interception for mocking HTTP requests and responses. These tests are now failing with an "unauthorized" error, although no changes were made to the test setup or the codebase, apart from the version upgrades.

Steps to Reproduce
Upgrade the project to .NET 8.
Upgrade HttpClient-Interception to the latest version.
Run the existing tests which involve HTTP request mocking.

Expected Behavior
Tests that were passing previously should continue to pass after the upgrade, assuming no changes to the test logic or setup.

Actual Behavior
Tests involving mocked HTTP requests are failing with an "unauthorized" error.

Additional Information
Error logs/output:
Shouldly.ShouldAssertException : response.response.StatusCode
should be
HttpStatusCode.Created
but was
HttpStatusCode.Unauthorized

Could you please provide any insights into why this might be happening? Are there any breaking changes in the latest version of HttpClient-Interception that could affect authorization? Also, are there any additional configurations or steps that we need to consider after upgrading to .NET 8 and the latest version of HttpClient-Interception?

Any guidance or suggestions would be greatly appreciated.

Thank you for your help!

Support for intercepting all requests sent to HttpClient

Would it be possible to easily implement intercepting all requests (i.e. not specifying a host) for the purposes of then doing some sort of assertions on the request.

For example if I am unit testing a class which uses HttpClient and I don't care about the host or the verb that is being sent to (this is covered by other tests) I simply want to test that the correct headers are being applied.

Question: Should HttpClient created from HttpClientInterceptorOptions auto-follow redirects?

Expected behaviour

I have created a HttpClientInterceptorOptions instance, defined 2 interceptions (one to capture requests to http://foo.com and return a 302 pointing to https://google.com and another to capture requests for https://google.com and response with some text content), and registered both of those interceptions with the HttpClientInterceptorOptions instance. My expectation is that if I create an HttpClient via HttpClientInterceptorOptions.CreateHttpClient() and then call client.GetAsync("http://foo.com"), the client should get the 302 from the initial request, redirect to google.com, and the final response should contain my text content from the google.com interception.

Actual behaviour

But...the resulting response is the 301 to google.com. The client does not auto-follow the redirect.

Steps to reproduce

using System.Net;
using JustEat.HttpClientInterception;
using Microsoft.Net.Http.Headers;

var clientOptions = new HttpClientInterceptorOptions();

new HttpRequestInterceptionBuilder()
    .Requests()
    .ForHttp()
    .ForHost("foo.com")
    .ForPath("dosomething")
    .Responds()
    .WithInterceptionCallback(msg =>
    {
        Console.WriteLine("Request for foo.com intercepted!!!");
    })
    .WithStatus(HttpStatusCode.Redirect)
    .WithResponseHeader(HeaderNames.Location, "https://google.com")
    .RegisterWith(clientOptions); 

new HttpRequestInterceptionBuilder()
    .Requests()
    .ForHttps()
    .ForHost("google.com")
    .Responds()
    .WithInterceptionCallback(msg =>
    {
        Console.WriteLine("Request for google.com intercepted!!!");
    })
    .WithContent("Hello!")
    .RegisterWith(clientOptions);

var client = clientOptions.CreateHttpClient();
var response = client.GetAsync("http://foo.com/dosomething").Result;
var content = response.Content.ReadAsStringAsync().Result;

Support conditional interception based on the body

Expected behaviour

Enough infrastructure should be available in the library so that a user may elect to read the body before it is sent (at their own risk, for example if it is in a write-only stream) and inform the interception message handler to intercept (or not) a request.

Actual behaviour

While it is possible to intercept the request before it is sent and inspect the body, it is not possible to change whether or not the request is intercepted.

Same request but with different responses

When reading the documentation, I fail to find a solution to how I can register different responses from the same combination of http method and uri.

I want to mimic the pattern:
GET /items -> empty list
POST /items -> 201
GET /items -> 200 list with the new item

Pardon, if I have overlooked something.

request: more examples for response changing based on how many times called so far

My current situation is that I want to test a Polly policy for handling 429 (too many requests) responses. I'm hoping to be able to have the interceptor return 429 on the first (or maybe the first two) calls, then 200 on the ones after.

Describe the solution you'd like
This is probably just a sample to be included

Describe alternatives you've considered
I tried keeping a count local var and creating 2 options builders, using WithInterceptionCallback overload passing a Predicate, with one that returned 429 with ++count < 3 then another with count >= 3 that returned 200, but that didn't work (very possible bug on my side and it would have worked fine if I'd done it correctly)

Additional context
While there are certainly scopes to change behavior, in this case I need the behavior to change in the context of a single HttpClient.GetAsync call, so AFAICT I can't use scopes to change the behavior in the "middle of the call". Ideally the sample would maybe include something like an array of objects/tuples of status code and content and as each call came in, it used the next item in the array to determine how to respond.

Add source link support

As per a number of other Just Eat libraries, should add support for Source Link embedding to improve "debuggability".

Support multiple builder registration

To make it easier to register multiple requests at once, should add an registration overload that supports, for example, IEnumerable<T>.

There's maybe also mileage in some sort of clone/copy functionality so you can use one builder to seed others without needing to worry about conflicting result object registrations etc.

Add exception list for urls to ignore when ThrowOnMissingRegistration is true

Is your feature request related to a problem? Please describe.

I'm using httpclient-interception and WireMock.Net together.

The scenario:

My application talks to an external (central) API (SaltBae), and from SaltBae I receive a list of different servers (SteakServers). In my integration tests, I intercept all requests going to SaltBae with httpclient-interception and all SteakServers are mocked and created by using WireMock.Net. The HttpClient used for the testing is created from _clientOptions.CreateHttpClient(); and is injected with Autofac in my test setup. This all works nicely so far.

The problem:

However, to make the above work I had to set ThrowOnMissingRegistration to false because httpclient-interception would throw on requests that are meant to be handled by WireMock.Net. But I would still like to have ThrowOnMissingRegistration = true while it ignores any requests that go to any of the Steak servers. ThrowOnMissingRegistration is very useful while writing tests as it shows exactly which requests still need to be mocked.

Describe the solution you'd like

Basically use the fluent API for creating requests that are then added to a special property IgnoredRequests, which are then taken into account before throwing an MissingRegistrationException

_clientOptions = new HttpClientInterceptorOptions
{
    ThrowOnMissingRegistration = true,
};

        new HttpRequestInterceptionBuilder()
            .Requests()
            .ForHttp()
            .ForHost("localhost")
            .ForPort(-1)
            .IgnoringPath()
            .IgnoringQuery()
            .RegisterWith(_clientOptions.IgnoredRequests);

or a simpler solution might be a property that set all requests to localhost to be ignored by the MissingRegistrationException

Describe alternatives you've considered

I tried the following to whitelist all localhost requests but that still intercepts the requests and doesn't return any response

new HttpRequestInterceptionBuilder()
    .Requests()
    .ForGet()
    .ForHttp()
    .ForHost("localhost")
    .ForPort(-1)
    .IgnoringPath()
    .IgnoringQuery()
    .RegisterWith(_clientOptions);

I also tried moving away from httpclient-interception and replace that with WireMock.Net but that caused more problems as the urls werent intercepted anymore and httpclient-interception is just exactly what I need.

Additional context

Nope

Ignore order of query params

Is your feature request related to a problem? Please describe.

We have urls generated by a 3rd party framework and the order some query params are added is not always deterministic.

It would be helpful if the matcher was able to determine if query params were the same when not added in the same order.

Describe the solution you'd like

Ideally this would just work. If there's concern of introducing a breaking change, consider adding a property like HttpClientInterceptorOptions.IgnoreQueryParamOrder.

The matcher would split the query string into parts before comparing.

Describe alternatives you've considered

I created a custom matcher to do this, but I don't want to copy that code into every solution that uses this package and it seems silly to create a custom nuget just for that.

Allow access to CancellationToken

The CancellationToken that is passed into the delegating handler is flowed into the inner delegating handler which is cool, however it isn't passed into the interception DSL, so there's no way to say in our tests "did the request get aborted".

It would be handy if the WithInterceptionCallback gave us that CancellationToken, what are your thoughts on this?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.