I am trying to call a REST service from a C# ASP.NET 4.0 application using RestSharp.
It's a fairly straightforward POST call to a https:// address; my code is something like this (CheckStatusRequest is a plain simple DTO with about four or five string and int properties - nothing fancy):
public CheckStatusResponse CheckStatus(CheckStatusRequest request) {
// set up RestClient
RestClient client = new RestClient();
string uri = "https://.......";
// create the request (see below)
IRestRequest restRequest = CreateRequestWithHeaders(url, Method.POST);
// add the body to the request
restRequest.AddBody(request);
// execute call
var restResponse = _restClient.Execute<CheckStatusResponse>(restRequest);
}
// set up request
private IRestRequest CreateRequestWithHeaders(string uri, Method method) {
// define request
RestRequest request = new RestRequest(uri, method);
// add two required HTTP headers
request.AddHeader("Accept", "application/json");
request.AddHeader("Content-Type", "application/json");
// define JSON as my format
request.RequestFormat = DataFormat.Json;
// attach the JSON.NET serializer for RestSharp
request.JsonSerializer = new RestSharpJsonNetSerializer();
return request;
}
The problem I'm having when I send these requests through Fiddler to see what's going on is that my request suddenly gets a third and unwanted HTTP header:
POST https://-some-url- HTTP/1.1
Accept: application/json
User-Agent: RestSharp/104.4.0.0
Content-Type: application/json
Host: **********.com
Content-Length: 226
Accept-Encoding: gzip, deflate <<<=== This one here is UNWANTED!
Connection: Keep-Alive
I suddenly have that Accept-Encoding HTTP header, which I never specified (and which I don't want to have in there). And now my response is no longer proper JSON (which I'm able to parse), but suddenly I get back gzipped binary data instead (which doesn't do real well when trying to JSON-deserialize)....
How can I get rid of that third unwanted HTTP header?
I tried to set it to something else - whatever I enter just gets appended to those settings
I tried to somehow "clear" that HTTP header - without any success
I tried finding a property on the RestClient or the RestRequest classes to specify "do not use GZip"
Looking at the sources (Http.Sync.cs and Http.Async.cs) of RestSharp you can see that these values are hardcoded:
webRequest.AutomaticDecompression =
DecompressionMethods.Deflate | DecompressionMethods.GZip | DecompressionMethods.None;
There is also an open issue that describes this problem. It was opened August 2014 but still not solved. I think you can leave a comment there and maybe they will pay attention.
Related
I'm trying to "repurpose" a third-party API used by a desktop application. I've found that the below code gets me very close to matching the packets sent by the app:
var formData = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>(JsonConvert.SerializeObject(myPayload), "")
});
var response = Client.PostAsync(myURL, formData).Result;
var json = response.Content.ReadAsStringAsync().Result;
This gets me almost exactly the same payload sent by the application, except it encodes the data (I know, "encoded" is right there in the name). I need to get the exact same request but without the data being encoded, but I can't quite find the right object(s) to pull it off. How do I keep this payload from being URL encoded?
Edit:
This is a login request I pulled from Wireshark emanating from the application:
POST /Login HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Host: 1.1.1.1
Content-Length: 161
Expect: 100-continue
Connection: Close
{"username":"myuser","auth-id":"0a0a140f81a2ce0c303386e93cec41bf04660c22a881be9a"}
This is what the above will generate:
POST /Login HTTP/1.1
Expect: 100-continue
Connection: Close
Content-Type: application/x-www-form-urlencoded
Content-Length: 221
Host: 1.1.1.1
%7B%22user-name%22%3A%22myuser%22%2C%22auth-id%22%3A%220a0a140f81a2ce0c303386e93cec41bf04660c22a881be9a%22%7D=
I've edited them for brevity so the Content-Length is wrong. I realize it might not be the best way to send this data, but I have no control over how it's consumed.
Since you're actually trying to send JSON, I think you need to wrap the JSON in a StringContent object rather than a FormUrlEncoded object. Form-encoded data and JSON data are two different ways of formatting a payload (another commonly used format would be XML, for example). Using them both together doesn't make any sense.
I think something like this should work:
var content = new StringContent(JsonConvert.SerializeObject(myPayload), Encoding.UTF8, "application/json");
var response = Client.PostAsync(myURL, content).Result;
var json = response.Content.ReadAsStringAsync().Result;
(P.S. the Content-Type: application/x-www-form-urlencoded header sent by the application appears to be misleading, since the request body clearly contains JSON. Presumably the receiving server is tolerant of this nonsense, or just ignores it because it's always expecting JSON.)
I looked at the MS Source code according to their interpretation the HttpClient itself does not have "Content-Type" only the content should have content-type. Seems logical except when you're dealing with MultipartFormDataContent.
MultipartFormDataContent completely ignores the following code:
string boundary = "--" + GenerateRandomString();
using (var content = new MultipartFormDataContent(boundary))
{
content.Headers.ContentType = new MediaTypeHeaderValue($"multipart/form-data");
content.Headers.ContentType.Parameters.Add(new NameValueHeaderValue("boundry", boundary));
...
}
No "Content-Type" is present in the request.
And also ignores:
string boundary = "--" + GenerateRandomString();
using (var content = new MultipartFormDataContent(boundary))
{
content.Headers.Remove("Content-Type");
content.Headers.TryAddWithoutValidation("Content-Type", "multipart/form-data; boundary=" + boundary);
...
}
Attemting to set it at on the HttpClient
client.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "multipart/form-data; boundary=" + boundary);
throws the following error:
System.InvalidOperationException: 'Misused header name. Make sure request headers are used with HttpRequestMessage, response headers with HttpResponseMessage, and content headers with HttpContent objects.'
I can find plenty of examples of how to do this using StringContent but none with MultipartFormDataContent. MultipartFormDataContent allows setting the Content-Type and Content-Disposition with each field, I need this more at the client level. I need a header that looks something like this:
accept: application/json
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
Authorization: Basic ZXlKbGRDSTZJakUxTWpZNU9UTXpOTnpkMjl5WkNJNklqa3haRGxtWkdKa1lUazRaVEJqWmpsalpUaGxNV1V3TXpOalxuWmpCbE1tVXhJaXdpZFhObGNpSTZJbUZrYldsdUluMD1cbjo=
Cache-Control: no-cache
Many non-Microsoft APIs require the "boundary" tag so it can distinguish the individual fields of data being sent. The validation here on the request seems a little over the top. Even TryAddWithoutValidation doesn't work (maybe a bug?). I realize that it may be possible to interpret RFC7578 in a way that says it shouldn't be required but flat out not allowing it doesn't seem right to me either. Anyone else ever run into this issue and solve it.
Initially I thought this was an HttpClient bug. I added logging to capture the request and the response. That logging was missing headers which lead me to believe that the missing "multi-part/form-data" content header was the issue and the reason the API I'm using kept telling me it couldn't find a required field. It turns out to be an issue with how the API handles the data it's sent when it's multi-part/form-data. After comparing both my HttpWebRequest and the HttpClient request in fiddler I discovered the following difference in the data being sent:
HttpWebRequest
----vekhftkcthxr
Content-Disposition: form-data; name="name";
d30-20180524
HttpClient
----bcgifxyjkmkw
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name=name
d30-20180524
I build the HttpWebRequest manually so I included the quotes and ending semi-colons. The HttpClient request is built for me and does not include the extra quotes and semi-colon. So the API I am using does not play well with request being generated by the HttpClient even though the request is technically correct.
Thanks to Panagiotis Kanavos for showing me my error.
I am attempting to use HttpClient in a .net core project to make a GET request to a REST service that accepts/returns JSON. I don't control the external service.
No matter how I try, I can't figure out to set the Content-Type header to application/json only.
When I use
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
it sends in the HTTP GET request:
Content-Type: application/json; charset=utf-8
However, this particular service does not work with this. It will only work if the header is:
Content-Type: application/json
I've tried setting headers without validation, and all the approaches I've found on the web/SO doesn't apply to .net core. All the other the approaches to sending HTTP requests aren't available in .net core, so I need to figure this out. How can I exclude the charset in content-type?
EDIT with workaround
As mentioned in the answers, the service should be using the Accept header. The workaround (as Shaun Luttin has in his answer) is to add an empty content to the GET (what? GETs don't have content! yeah...). It's not pretty, but it does work.
You're setting the Accept header. You need to set the ContentType header instead, which is only canonical for a POST.
var client = new HttpClient();
var content = new StringContent("myJson");
content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
var result = client.PostAsync("http://bigfont.ca", content).Result;
If you really want to set it for a GET, you can do this:
var client = new HttpClient();
var message = new HttpRequestMessage(HttpMethod.Get, "http://www.bigfont.ca");
message.Content = new StringContent(string.Empty);
message.Content.Headers.Clear();
message.Content.Headers.Add("Content-Type", "application/json");
var result = client.SendAsync(message).Result;
If you are the client and you perform a GET request how can you specify the Content-Type? Isn't it supposed to say what your are able to Accept ? According to this 7.2.1 Type you can only set Content-Type when there is Body.
I have a website that send an Ajax request to check validation of a number. I want send that request in my WPF application, using HttpClient.
It use post method, and here is the request url: http://sim.mci.ir/chk-number-availability
Here is the headers:
Accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.5
Cache-Control: no-cache
Connection: keep-alive
Content-Length: 18
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Cookie: _ga=GA1.2.589400539.1410935105; JSESSIONID=33719B6BDA36C7C23A96B5E73F602B08; _gat=1
Host: sim.mci.ir
Pragma: no-cache
Referer: http://sim.mci.ir/first-step
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0
X-Requested-With: XMLHttpRequest
And here is the only value:
number=09122175567
I used SendAsync() and PostAsync() methods in common ways, but it doesn't work. I think the problem is in request headers.
HttpClient client = new HttpClient();
var content = new FormUrlEncodedContent(new Dictionary<string, string>() {
{ "number", "09129457000" }
});
client.DefaultRequestHeaders.Referrer = new Uri("http://sim.mci.ir/first-step");
var resp = await client.PostAsync("http://sim.mci.ir/chk-number-availability", content);
var repsStr = await resp.Content.ReadAsStringAsync();
This will return false string
Well, Actually you can't send real AJAX request using HttpClient, but you can simulate it by adding XMLHttpRequest to request header like this:
client.DefaultRequestHeaders.Add("X-Requested-With", "XMLHttpRequest");
Here is a Helper method which requires Microsoft.AspNet.WebApi.Client nuget package:
private async TResponse CreateHttpRequestAsync<TRequest, TResponse>(Uri uri, TRequest request)
where TRequest : class
where TResponse : class, new()
{
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Add("X-Requested-With", "XMLHttpRequest");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
//client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _bearerToken);
var response = await client.PostAsJsonAsync(uri, request);
if (response.StatusCode == HttpStatusCode.OK)
{
var json = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<TResponse>(json);
}
else if(response.StatusCode == HttpStatusCode.Unauthorized)
{
throw new UnauthorizedException();
}
else
{
throw new InvalidOperationException();
}
}
}
You can't send an AJAX request using HttpClient. You can send an asynchronous HTTP request if you want.
AJAX is asynchronous JavaScript + XML (it has since become broader to mean Asynchronous JavaScript + other formats e.g. Json), but it's still fundamentally a browser technology involving JavaScript.
HttpClient doesn't know anything about JavaScript, you'd have to use a WebBrowser control for executing JavaScript.
Update:
Looking back at this question, I find it rather surprising that so many seemingly experienced devs don't understand the term AJAX and confuse it with a simple asynchronous HTTP request, so I think it'd be helpful to provide more info here.
Ajax is short for Asynchronous JAvaScript and XML, it allows web pages to send/receive asynchronous data serialized as XML via XMLHttpRequests, and then use that data to dynamically update the page without the need to reload the entire page which is all done using JavaScript.
Now over the last few years, XML has lost its luster as the de facto standard of formatting data and JSON is taking its place, and that has affected Ajax too, you typically see data formatted as JSON instead of XML when communicating with servers. So the acronym AJAX wasn't so precise anymore; AJAJ was proposed but its use did not become widespread, so people still use the term Ajax when in fact they are not sending/receiving XML, but that's not a very big deal because the two are essentially very similar other than the way the data is formatted:
it still involves a JavaScript.
a web page/application sending/receiving asynchronous requests.
a web browser that supports JavaScript and XMLHttpRequest.
With the above context, I hope its clear that what the OP is trying to do here, and the accepted answer for that matter, has very little to do with Ajax as it doesn't involve any of the above points. It is in fact, just a simple HTTP request.
I am using the ASP.NET Web API Client Libraries for .NET 4.0 (Microsoft.AspNet.WebApi.Client version 4.0.30506.0).
I need to send an HTTP DELETE with a request body. I have coded it as follows:
using (var client = new HttpClient())
{
client.BaseAddress = Uri;
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
// I would normally use httpClient.DeleteAsync but I can't because I need to set content on the request.
// For this reason I use httpClient.SendAsync where I can both specify the HTTP DELETE with a request body.
var request = new HttpRequestMessage(HttpMethod.Delete, string.Format("myresource/{0}", sessionId))
{
var data = new Dictionary<string, object> {{"some-key", "some-value"}};
Content = new ObjectContent<IDictionary<string, object>>(data, new JsonMediaTypeFormatter())
};
var response = await client.SendAsync(request);
// code elided
}
Per Fiddler, the request body is never serialized:
DELETE http://localhost:8888/myApp/sessions/blabla123 HTTP/1.1
Accept: application/json
Content-Type: application/json; charset=utf-8
Host: localhost:8888
Content-Length: 38
Expect: 100-continue
The response from the server:
HTTP/1.1 408 Request body incomplete
Date: Sun, 10 Aug 2014 17:55:17 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
Cache-Control: no-cache, must-revalidate
Timestamp: 13:55:17.256
The request body did not contain the specified number of bytes. Got 0, expected 38
I have tried a number of workarounds, including changing the type being serialized to something else, doing the serialization myself with JsonSerialize, changing the HTTP DELETE to PUT, etc...
Nothing worked. Any help would be much appreciated.
I resolved the issue, though it does not make sense. I noticed that if I changed my call to HTTP PUT or POST, it still failed to serialize the Content as a request body. That was rather strange as previous PUTs and POSTs were successful. After doing a ton of debugging into framework libraries (using Reflector), I finally got to the only thing left that was "different."
I am using NUnit 2.6.2. The structure of my test is:
[Test]
async public void Test()
{
// successful HTTP POST and PUT calls here
// successful HTTP DELETE with request body here (after
// moving it from the TearDown below)
}
[TearDown]
async public void TerminateSession()
{
// failed HTTP DELETE with request body here
}
Why does this fail in the TearDown but not in the Test itself? I have no idea. Is something going on with the TearDown attribute or with the use of the async keyword (since I await async calls)?
I am not sure what it is causing this behavior, but I do know now that I can submit an HTTP DELETE with a request body (as outlined in my code sample in the question).
Another solution that worked is as follows:
[Test]
async public void Test()
{
// create and use an HttpClient here, doing POSTs, PUTs, and GETs
}
// Notice the removal of the async keyword since now using Wait() in method body
[TearDown]
public void TerminateSession()
{
// create and use an HttpClient here and use Wait().
httpClient.SendAsync(httpRequestMessage).Wait();
}
I know it's never quite that helpful to say, "don't do it that way", but in this case I think it makes sense to split the calls into a DELETE followed or preceeded by a POST or PUT.
The HTTP RFC doesn't explicitly opine on the matter, so technically it means that we can. The other question, however, is should we do it.
In cases such as this I would look for other implementations to see what is the de facto standard. As you've found in the .net implementation, it appears that the designers did not expect to send a body with the DELETE call. So, let's look at another popular (and very different impl) Python Requests:
>>> r = requests.delete(url=url, auth=auth)
>>> r.status_code
204
>>> r.headers['status']
'204 No Content'
No body here other. So, if the spec authors didn't mention it, and popular implementations assume that there's no body, then the principle of least surprise means we shouldn't do it either.
So, if you can change the API, it will be easier on clients of the API to split into two calls. Otherwise, you'll likely have to resort to custom hackery to cram the body into a DELETE call.
The good news is that you've likely found a bug in the .net framework, which is an achievement in and of itself. Clients advertising a non-zero Content-Length without actually sending it are broken.
In case anybody else runs into this, one thing I've noticed that can cause this is if you set a header with a newline in it.
We had an encrypted OAuth token, which gets decrypted at runtime and set as the OAuth header on the app. The newline was encrypted into the token, so it was not obvious from looking at the configs or anything that it was there, but if you do:
var message = new HttpRequestMessage(HttpMethod.Post, "https://example.com");
message.Headers.ContentType = new MediaTypeHeaderValue("application/json");
message.Content = new StringContent("{ \"someKey\": \"someValue\" }", Encoding.UTF8);
// note the trailing newline
message.Headers.Authorization = new AuthenticationHeaderValue("OAuth", "my auth token\n");
var response = await httpClient.SendAsync(request);
The HTTP request will be sent, but the content will not be sent with it. There are no exceptions thrown when this happens and if you inspect the HttpRequestMessage, the content will appear to be there, but it does not actually get sent over the wire.
This happens in .NET 5 on Windows and Linux, I haven't tested it on other framework versions/platforms.