HttpClient Interception

repository·main·Indexed 18 days ago

https://github.com/justeattakeaway/httpclient-interception

A .NET Standard library for intercepting server-side HTTP dependencies to facilitate testing and control over outgoing HTTP requests. It allows developers to define request patterns and responses via a Fluent API or JSON bundle files, simulate server errors by injecting HTTP faults, and integrate with IHttpClientFactory and ASP.NET Core using DelegatingHandler for non-intrusive black-box testing.

Tokens
3.1K
Snippets
8
Records
12
Agent score
64%

What's inside JustEat.HttpClientInterception

  1. What is HttpClient Interception and when to use it

    main

    HttpClient Interception is a .NET Standard library designed to intercept HTTP requests made via the HttpClient class.

    Primary Use Case: Providing stub responses for functional testing scenarios in applications (such as ASP.NET Core). This allows you to simulate server-side HTTP dependencies without hosting an actual HTTP server.

    Key Benefits:

    • Lightweight: Since it does not host a proxy server, it does not consume system resources like port bindings.
    • Non-intrusive: It uses DelegatingHandler, meaning it can be registered via Dependency Injection. This allows the code under test to remain unaware of the interception logic, requiring no direct references to the library or custom abstractions.
  2. Use HTTP Bundle files for request interception

    main

    HTTP bundles allow you to store intercepted requests and their responses in a JSON file. This is useful for managing large sets of mock data. You can load a bundle using HttpClientInterceptorOptions.RegisterBundle(string fileName).

    {
      "$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"
          }
        }
      ]
    }
  3. Register interception with IHttpClientFactory

    main

    When using IHttpClientFactory in a .NET application, you can register the interceptor by implementing IHttpMessageHandlerBuilderFilter. This allows you to inject the intercepting handler into the message handler pipeline during test setup without modifying the application code.

    /// <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());
            };
        }
    }
  4. Build and test the library locally

    main

    To compile and test the library from source, you must have Git and the .NET SDK (version 7.0.100 or later) installed. Use the provided PowerShell build script to automate the process.

    git clone https://github.com/justeattakeaway/httpclient-interception.git
    cd httpclient-interception
    ./build.ps1
  5. Perform black-box testing with HttpClientInterception

    main

    You can perform black-box testing by self-hosting your ASP.NET Core application using WebApplicationFactory<T> and intercepting outgoing HTTP calls.

    To set this up in your test project:

    1. Self-host the server: Use an xUnit collection fixture to host the application using Kestrel via WebApplicationFactory<T>.
    2. Configure Interception: Create and register a shared HttpClientInterceptorOptions instance. This provides the DelegatingHandler implementation required for Dependency Injection.
    3. Implement IHttpMessageHandlerBuilderFilter: Use a custom implementation of IHttpMessageHandlerBuilderFilter to wire the interception handler into the HttpClient pipeline during the test execution.
    4. Intercept Calls: Use JustEat.HttpClientInterception to intercept specific outgoing requests (e.g., to the GitHub API) and return mocked responses, allowing you to test your API resources without making real network calls.
  6. Integrate HttpClientInterception into an ASP.NET Core application

    main

    To use JustEat.HttpClientInterception in an ASP.NET Core application for testing external API dependencies, you need to perform three main integration steps:

    1. Register the HttpClient: Use an extension method to register your HttpClient (e.g., when using Refit) so it is compatible with the interception mechanism.
    2. Register Dependencies: In your Startup class, register the dependencies required for the external API calls.
    3. Inject the Interface: Inject the service interface (e.g., IGitHub) into your controllers or services as you normally would. The interception happens transparently at the HttpClient level.
  7. Manually configure HttpClient for Dependency Injection

    main

    If you are manually configuring IServiceCollection for Dependency Injection, you can chain DelegatingHandler instances to ensure the interceptor is part of the pipeline. In your test project, register HttpClientInterceptorOptions and use options.CreateHttpMessageHandler() to provide the intercepting handler.

    // Application Setup
    services.AddTransient(
        (serviceProvider) =>
        {
            HttpMessageHandler handler = new HttpClientHandler();
            var handlers = serviceProvider.GetServices<DelegatingHandler>().ToList();
    
            if (handlers.Count > 0)
            {
                DelegatingHandler previous = handlers.First();
                previous.InnerHandler = handler;
    
                foreach (DelegatingHandler next in handlers.Skip(1))
                {
                    next.InnerHandler = previous;
                    previous = next;
                }
                handler = previous;
            }
    
            return new HttpClient(handler);
        });
    
    // Test Setup
    var options = new HttpClientInterceptorOptions();
    var server = new WebHostBuilder()
        .UseStartup<Startup>()
        .ConfigureServices(
            (services) => services.AddTransient((_) => options.CreateHttpMessageHandler()))
        .Build();
    
    server.Start();
  8. Load an HTTP bundle in code

    main

    To use a JSON bundle file, register it with HttpClientInterceptorOptions and then create your HttpClient.

    // 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");
  9. Inject HTTP faults for testing

    main

    You can simulate server errors by intercepting a request and specifying a status code using .WithStatus(HttpStatusCode) in the builder.

    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", TestContext.Current.CancellationToken)));
  10. Intercept HTTP requests using the Fluent API

    main

    You can use HttpRequestInterceptionBuilder and HttpClientInterceptorOptions to define specific request patterns and their corresponding responses. The builder is mutable, allowing you to register multiple different responses using the same builder instance by calling RegisterWith(options) after each configuration block.

    // 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", TestContext.Current.CancellationToken);