Skip to content

Commit

Permalink
Capture headers from request content
Browse files Browse the repository at this point in the history
  • Loading branch information
sandermvanvliet committed Apr 30, 2024
1 parent 53d29b3 commit dcd2f64
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 1 deletion.
22 changes: 22 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
# Codenizer.HttpClient.Testable Changelog

## 2.10.0.0

Fixed an issue where the request headers for the content part of the request wouldn't be captured.

For example, sending a request like this:

```csharp
var stringContent = new StringContent("Hello");
stringContent.Headers.Expires = DateTimeOffset.UtcNow;

var request = new HttpRequestMessage(HttpMethod.Get, "/api/info")
{
Content = stringContent
}
```

would mean that on the captured request the `Expires` header wasn't present.

Now, all headers from the `request.Content.Headers` will be captured.

The headers from the request itself were already captured.

## 2.9.0.0

Add support to configure expected request content.
Expand Down
1 change: 1 addition & 0 deletions Codenizer.HttpClient.Testable.sln
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
ProjectSection(SolutionItems) = preProject
Changelog.md = Changelog.md
Directory.Build.props = Directory.Build.props
Directory.Packages.props = Directory.Packages.props
README.md = README.md
EndProjectSection
EndProject
Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

<Project>
<PropertyGroup>
<Version>2.9.0.0</Version>
<Version>2.10.0.0</Version>
<Authors>Sander van Vliet</Authors>
<Company>Codenizer BV</Company>
<Copyright>2024 Sander van Vliet</Copyright>
Expand Down
25 changes: 25 additions & 0 deletions src/Codenizer.HttpClient.Testable/TestableMessageHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,18 @@ private HttpRequestMessage CloneRequest(HttpRequestMessage request)
case StringContent stringContent:
clone.Content = new StringContent(stringContent.ReadAsStringAsync().GetAwaiter().GetResult());
break;
// FormUrlEncodedContent needs to be before ByteArrayContent
// because it inherits from it, and we need to handle it differently
case FormUrlEncodedContent formContent:
var serialized = formContent.ReadAsStringAsync().GetAwaiter().GetResult();
// serialized looks like field1=val1&field2=val2
var formValues = serialized
.Split('&')
.Select(kv => kv.Split('='))
.Select(parts => new KeyValuePair<string, string>(parts[0], parts[1]))
.ToArray();
clone.Content = new FormUrlEncodedContent(formValues);
break;
case ByteArrayContent byteArrayContent:
clone.Content = new ByteArrayContent(byteArrayContent.ReadAsByteArrayAsync().GetAwaiter().GetResult());
break;
Expand All @@ -212,6 +224,19 @@ private HttpRequestMessage CloneRequest(HttpRequestMessage request)
break;
}

if (clone.Content != null)
{
// Ensure we start with a clear slate
clone.Content.Headers.Clear();

// Copy all original content headers.
// The "other" request headers have already been copied above
foreach (var header in request.Content.Headers)
{
clone.Content.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
}

return clone;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using FluentAssertions;
using Xunit;

Expand Down Expand Up @@ -108,5 +110,78 @@ await action
.Should()
.NotThrowAsync("the request should be a copy and not the disposed original request");
}

[Fact]
public async void GivenRequestWithFormContent_CapturedRequestContainsContent()
{
var formValues = new[]
{
new KeyValuePair<string, string>("field1", "val1"),
new KeyValuePair<string, string>("field2", "val2")
};

await _client.PostAsync("/api/info", new FormUrlEncodedContent(formValues));

_handler
.Requests
.Single()
.Content
.Should()
.BeOfType<FormUrlEncodedContent>();
}

[Fact]
public async void GivenRequestWithFormContent_CapturedFormContentMatchesRequest()
{
var formUrlEncodedContent = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("field1", "val1"),
new KeyValuePair<string, string>("field2", "val2")
});

await _client.PostAsync("/api/info", formUrlEncodedContent);

var formValuesOnCapturedRequest = await _handler
.Requests
.Single()
.Content
.As<FormUrlEncodedContent>()
.ReadAsStringAsync();

formValuesOnCapturedRequest
.Should()
.Be((await formUrlEncodedContent.ReadAsStringAsync()));
}

[Fact]
public async void GivenRequestWithFormContent_AllOriginalHeadersExistOnTheCapturedRequest()
{
var formUrlEncodedContent = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("field1", "val1"),
new KeyValuePair<string, string>("field2", "val2")
});

// Set some additional content headers to verify
formUrlEncodedContent.Headers.LastModified = DateTimeOffset.UtcNow;
formUrlEncodedContent.Headers.Expires = DateTimeOffset.UtcNow;

var request = new HttpRequestMessage(HttpMethod.Get, "/api/info")
{
Content = formUrlEncodedContent
};
request.Headers.Accept.Clear();
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json", 0.8));

await _client.SendAsync(request);

var capturedRequest = _handler.Requests.Single();

// Using ToString() here because what's sent over the wire is the string representation
// of the DateTimeOffset and not a strictly serialized representation.
capturedRequest.Content.Headers.LastModified.ToString().Should().Be(formUrlEncodedContent.Headers.LastModified.ToString());
capturedRequest.Content.Headers.Expires.ToString().Should().Be(formUrlEncodedContent.Headers.Expires.ToString());
capturedRequest.Headers.Accept.Should().BeEquivalentTo(request.Headers.Accept);
}
}
}

0 comments on commit dcd2f64

Please sign in to comment.