Skip to content

Commit 2cf9dd1

Browse files
authored
Implement SendFormUrlEncoded method for application/x-www-form-urlencoded form data (#27)
In #9, the `SendForm` extension method was changed from sending a `application/x-www-form-urlencoded` request to sending one as `multipart/form-data` instead. This is a breaking change in some cases if sending to an endpoint that doesn't expect form data to come in with `multipart/form-data` encoding (OIDC access token requests for example, see opserver/Opserver#426). This PR re-adds the old behaviour as a new extension method so consuming libraries don't need to add it back themselves. I expect `application/x-www-form-urlencoded` is the more common media type that consumers will want (when size limitations or file uploads aren't a concern), but I didn't want to revert `SendForm` back to its original behaviour to avoid breaking dependent projects again. I could also obsolete `SendForm` and provide `SendMultipartForm` / `SendFormUrlEncoded` but I don't think it's important enough to inconvenience consumers with changing their code (and getting build errors using `TreatWarningsAsErrors`).
1 parent fb393ce commit 2cf9dd1

File tree

2 files changed

+65
-7
lines changed

2 files changed

+65
-7
lines changed

src/StackExchange.Utils.Http/Extensions.Send.cs

+13-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ public static IRequestBuilder SendContent(this IRequestBuilder builder, HttpCont
2424
}
2525

2626
/// <summary>
27-
/// Adds a <see cref="NameValueCollection"/> as the body for this request.
27+
/// Adds a <see cref="NameValueCollection"/> as the body for this request, with a content type of
28+
/// <c>multipart/form-data</c>.
2829
/// </summary>
2930
/// <param name="builder">The builder we're working on.</param>
3031
/// <param name="form">The <see cref="NameValueCollection"/> (e.g. FormCollection) to use.</param>
@@ -38,7 +39,17 @@ public static IRequestBuilder SendForm(this IRequestBuilder builder, NameValueCo
3839
}
3940
return SendContent(builder, content);
4041
}
41-
42+
43+
/// <summary>
44+
/// Adds a <see cref="NameValueCollection"/> as the body for this request, with a content type of
45+
/// <c>application/x-www-form-urlencoded</c>.
46+
/// </summary>
47+
/// <param name="builder">The builder we're working on.</param>
48+
/// <param name="form">The <see cref="NameValueCollection"/> (e.g. FormCollection) to use.</param>
49+
/// <returns>The request builder for chaining.</returns>
50+
public static IRequestBuilder SendFormUrlEncoded(this IRequestBuilder builder, NameValueCollection form) =>
51+
SendContent(builder, new FormUrlEncodedContent(form.AllKeys.ToDictionary(k => k, v => form[v])));
52+
4253
/// <summary>
4354
/// Adds raw HTML content as the body for this request.
4455
/// </summary>

tests/StackExchange.Utils.Tests/HttpTests.cs

+52-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Collections.Specialized;
34
using System.Net;
45
using System.Net.Http;
56
using System.Threading.Tasks;
@@ -142,7 +143,7 @@ public async Task Timeouts()
142143
public async Task AddHeaderWithoutValidation()
143144
{
144145
var result = await Http.Request("https://httpbin.org/bearer")
145-
.AddHeaderWithoutValidation("Authorization","abcd")
146+
.AddHeaderWithoutValidation("Authorization", "abcd")
146147
.ExpectJson<HttpBinResponse>()
147148
.GetAsync();
148149
Assert.True(result.RawRequest.Headers.Contains("Authorization"));
@@ -162,7 +163,7 @@ public async Task AddHeaders()
162163
.ExpectJson<HttpBinResponse>()
163164
.GetAsync();
164165

165-
Assert.Equal(HttpStatusCode.OK,result.StatusCode);
166+
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
166167
Assert.Equal("Test", result.Data.Headers["Custom"]);
167168
// Content-Type should be present because we're sending a body up
168169
Assert.StartsWith("application/json", result.Data.Headers["Content-Type"]);
@@ -181,7 +182,7 @@ public async Task TestAddHeadersWhereClientAddsHeaderBeforeContent()
181182
.ExpectJson<HttpBinResponse>()
182183
.GetAsync();
183184

184-
Assert.Equal(HttpStatusCode.OK,result.StatusCode);
185+
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
185186
Assert.Equal("Test", result.Data.Headers["Custom"]);
186187
Assert.Equal("application/json; charset=utf-8", result.Data.Headers["Content-Type"]);
187188
}
@@ -198,7 +199,7 @@ public async Task TestAddHeadersWhereClientAddsHeaderAndNoContent()
198199
.ExpectJson<HttpBinResponse>()
199200
.GetAsync();
200201

201-
Assert.Equal(HttpStatusCode.OK,result.StatusCode);
202+
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
202203
Assert.Equal("Test", result.Data.Headers["Custom"]);
203204
// Content-Type is NOT sent when there's no body - this is correct behavior
204205
Assert.DoesNotContain("Content-Type", result.Data.Headers.Keys);
@@ -221,14 +222,60 @@ public async Task PatchRequest()
221222
Assert.Equal(Http.DefaultSettings.UserAgent, result.Data.Headers["User-Agent"]);
222223
}
223224

225+
[Fact]
226+
public async Task SendFormUsesMultipartFormData()
227+
{
228+
string expectedFormKey = "key";
229+
string expectedFormValue = "value";
230+
var form = new NameValueCollection
231+
{
232+
[expectedFormKey] = expectedFormValue
233+
};
234+
var result = await Http.Request("https://httpbin.org/post")
235+
.SendForm(form)
236+
.ExpectJson<HttpBinResponse>()
237+
.PostAsync();
238+
239+
Assert.NotNull(result);
240+
Assert.True(result.Success);
241+
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
242+
var actualContentType = Assert.Contains("Content-Type", (IDictionary<string, string>)result.Data.Headers);
243+
Assert.StartsWith("multipart/form-data;", actualContentType);
244+
var actualFormValue = Assert.Contains(expectedFormKey, (IDictionary<string, string>)result.Data.Form);
245+
Assert.Equal(expectedFormValue, actualFormValue);
246+
}
247+
248+
[Fact]
249+
public async Task SendFormUrlEncodedUsesFormDataUrlEncoded()
250+
{
251+
string expectedFormKey = "key";
252+
string expectedFormValue = "value";
253+
var form = new NameValueCollection
254+
{
255+
[expectedFormKey] = expectedFormValue
256+
};
257+
var result = await Http.Request("https://httpbin.org/post")
258+
.SendFormUrlEncoded(form)
259+
.ExpectJson<HttpBinResponse>()
260+
.PostAsync();
261+
262+
Assert.NotNull(result);
263+
Assert.True(result.Success);
264+
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
265+
var actualContentType = Assert.Contains("Content-Type", (IDictionary<string, string>)result.Data.Headers);
266+
Assert.Equal("application/x-www-form-urlencoded", actualContentType);
267+
var actualFormValue = Assert.Contains(expectedFormKey, (IDictionary<string, string>)result.Data.Form);
268+
Assert.Equal(expectedFormValue, actualFormValue);
269+
}
270+
224271
[Fact]
225272
public async Task LargePost()
226273
{
227274
// 5MB string
228275
var myString = new string('*', 1048576 * 5);
229276

230277
var form = new System.Collections.Specialized.NameValueCollection
231-
{
278+
{
232279
["requestsJson"] = myString
233280
};
234281

0 commit comments

Comments
 (0)