I have the following code to set content-md5 in my GET method request using HttpClient
httpClient.DefaultRequestHeaders.TryAddWithoutValidation("content-md5", "value");
I cannot use HttpRequestMessage content to set it because it's not a POST method. When using Postman it works like a charm but fails when using HttpClient.GetAsync.
Client request a hmac to the server as follows
{
"content_to_hash": "my content"
}
The server will give response like this
{
"content_md5": "88af7ceab9fdafb76xxxxx",
"date": "Sat, 02 May 2020 00:13:16 +0700",
"hmac_value": "WfHgFyT792IENmK8Mqz9LysmP8ftOP00qA="
}
Now I have to access a GET request using that hmac where it's the problem because I cannot set in httpClient GET request header.
Here's the image
From reading the HttpClient and related source code, there's no way you can get around this and add the header to the actual request object headers. There is an internal list of invalid headers, which includes any Content-* headers. It has to be on a content object.
Therefore, my suggest solution is to create your own content object:
public class NoContentMd5 : HttpContent
{
protected override Task SerializeToStreamAsync(Stream stream, TransportContext context)
{
return Task.CompletedTask;
}
protected override bool TryComputeLength(out long length)
{
length = 0;
return false;
}
public NoContentMd5(byte[] contentMd5)
{
this.Headers.ContentMD5 = contentMd5;
}
public NoContentMd5(string contentMd5)
{
this.Headers.TryAddWithoutValidation("Content-MD5", contentMd5);
}
}
This will add the Content-MD5 header with a value of your choosing, but the request won't contain a body.
The next problem you'll encounter is that you're trying to make a GET request with content, which isn't supported by the helper client.GetAsync(...) method. You'll have to make your own request object and use client.SendAsync(...) instead:
HttpClient client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost/my/test/uri");
request.Content = new NoContentMd5("d41d8cd98f00b204e9800998ecf8427e ");
var result = await client.SendAsync(request);
Note that if you have your Content-MD5 hash as bytes, I've also added a constructor to NoContentMd5 for byte[] too.
The only potential issue with this is that it includes a Content-Length: 0 header. Hopefully that's OK with the API you're working with.
There's an alternative solution described in this answer to question with a similar issue. I'd argue against using it since is vulnerable to changes in the implementation details of HttpRequestHeaders (because it uses reflection, so if MS change the code, it might break) .
Aside from the fact that it's not considered good practice to send a body with GET request (see HTTP GET with request body), you can try this:
using (var content = new StringContent(string.Empty))
using (var request = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri("http://localhost"),
Content = content
})
{
request.Headers.TryAddWithoutValidation("content-md5", "value");;
using (var response = await httpClient.SendAsync(request))
{
response.EnsureSuccessStatusCode();
}
}
UPDATE:
The proper way would be to set the ContentMD5 property on the HttpContentHeaders, for example:
content.Headers.ContentMD5 = Convert.FromBase64String(hashString);
But as you pointed out in the comments, trying to send content in a GET request causes an error.
Related
I am getting response in text/html format.How can I set response header so that I get response in application/json format.
Code:
var httpRequestMessage = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = builder.Uri,
Headers = {
{ HttpResponseHeader.ContentType.ToString(), "application/json" },
{ "Auth", "###################################################" }
}
};
var httpResponse = await _httpClient.SendAsync(httpRequestMessage);
You don't. Content-Type header
In responses, a Content-Type header tells the client what the content type of the returned content actually is.
The server decides the content type of the response and the client uses it to handle accordingly. There is nothing you can set in your request to do that out of the box.
If you are handling the response creation, you could create a custom header to indicate the type of the response and have the server side code handle it, but that's not usually how it should be done.
As stated by #Athanasios, you cannot specify the Content-Type returned by the server. But, there is one thing you can try : you can tell the server that you'd like to get a JSON format response.
Set the HTTP Header Accept to application/json
But, it's still up to the server in the end to decide what content will be returned
It seems that you are looking for Accept header. Try next:
var httpRequestMessage = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = builder.Uri,
Headers = {
{ "Accept", "application/json"},
{ "Auth", "###################################################" }
}
};
I'm trying to POST a MultipartFormDataContent in a real case scenario, a data content object could contain anything from a simple string to a video file I'm using a serialized object down there, just a proof of concept.
Also I would like to note that using JSON objects wont serve my real life scenarios
public class GzipMultipartContent : MultipartFormDataContent
{
public GzipMultipartContent()
{
Headers.ContentEncoding.Add("gzip");
}
protected override Task SerializeToStreamAsync(Stream stream, TransportContext context)
{
return Task.Factory.StartNew(() =>
{
using (var gzip = new GZipStream(stream, CompressionMode.Compress, true))
base.SerializeToStreamAsync(gzip, context);
});
}
}
and here's how I call it
var gzipped = new GzipMultipartContent();
var test = new TestClass();
gzipped.Add(new StringContent(JsonConvert.SerializeObject(test)), "value");
var client = new HttpClient();
var result = client.PostAsync("http://localhost:60001/api/Home/", gzipped).Result;
and here's the post action in the controller
// POST: api/Home
[HttpPost]
public void Post([FromForm] object value)
{
}
I have added a break point at the server side and made sure it doesn't even reach the Post method, also I have tried with a normal POST request to make sure that it's not a server configuration problem or a URL mistyping
Client side
If the code in question is your real code, then there are at least two issues:
Did not wait on base.SerializeToStreamAsync
You created a new task, but you did not wait until the base class completed writing to the compressed stream in the task. So you could send unpredictable content to server.
Did not override Content-Length
MultipartFormDataContent calculates length of content based on data not compressed, since you have compressed data, you must re-compute length for the compressed data.
Frankly, I don't think you need to inherit from MultipartFormDataContent to make it compressed. Instead, you could compress the entire MultipartFormDataContent in a wrapper HttpContent:
public class GzipCompressedContent : HttpContent
{
private readonly HttpContent _content;
public GzipCompressedContent(HttpContent content)
{
// Copy original headers
foreach (KeyValuePair<string, IEnumerable<string>> header in content.Headers)
{
Headers.TryAddWithoutValidation(header.Key, header.Value);
}
Headers.ContentEncoding.Add("gzip");
_content = content;
}
protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context)
{
using (var gzip = new GZipStream(stream, CompressionMode.Compress, true))
{
// Compress the entire original content
await _content.CopyToAsync(gzip);
}
}
protected override bool TryComputeLength(out long length)
{
// Content-Lenght is optional, so set to -1
length = -1;
return false;
}
}
And use it:
var test = new TestClass();
using (var client = new HttpClient())
{
var form = new MultipartFormDataContent();
form.Add(new StringContent(JsonConvert.SerializeObject(test)), "value");
var compressed = new GzipCompressedContent(form);
var result = await client.PostAsync(..., compressed);
}
Server side
Your server needs to support compressed stream.
For example, by default, ASP.NET Core does not support compressed request, if you send GZip compressed request to an ASP.NET Core application, you will see exception:
System.IO.IOException: Unexpected end of Stream, the content may have already been read by another component.
at Microsoft.AspNetCore.WebUtilities.MultipartReaderStream.ReadAsync(Byte[] buffer, Int32 offset, Int32 count, CancellationToken cancellationToken)
The above exception happens in action invocation pipeline before any controller action takes place. So controller actions in this case could not be reached.
To fix such, you will need to enable server side request decompression support.
If you are using ASP.NET Core, check out this nuget package.
I am not sure if I understood the issue, but if it is that your request isn't getting to the server, while your "normal" POST requests are, then I think that I found your problem.
I think that the issue is that your server doesn't know what Content-Type is coming to it. I literally copy-pasted your code, but added
Headers.ContentType = new MediaTypeHeaderValue("application/x-gzip");
to GzipMultipartContent.cs ctor.
After I added the type, I was hitting my breakpoint in the localhost server.
Source: Content-Type
In requests, (such as POST or PUT), the client tells the server what type of data is actually sent.
HttpClient automatically handles 303 (SeeOther) as described in https://msdn.microsoft.com/en-us/library/system.net.httpstatuscode(v=vs.118).aspx
SeeOther automatically redirects the client to the URI specified in
the Location header as the result of a POST. The request to the
resource specified by the Location header will be made with a GET.
The same behavior seems to happen on GET requests, but the call fails as if my Authorization header was missing.
I set the header using
public void SetOauthToken(String key, String token)
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(key, token);
}
I am able to fix the error by intercepting the 303 and calling the URI from Location header by hand.
For example I change
public async Task<T> GetXML<T>(String url)
{
using (HttpResponseMessage response = await client.GetAsync(url))
{
string result = await response.Content.ReadAsStringAsync();
var serializer = new XmlSerializer(typeof(T));
return (T)serializer.Deserialize(new StringReader(result));
}
}
to
public async Task<T> GetXML<T>(String url)
{
using (HttpResponseMessage response = await client.GetAsync(url))
{
if (response.StatusCode == HttpStatusCode.SeeOther)
{
return await GetXML<T>(response.Headers.Location.ToString());
}
else
{
string result = await response.Content.ReadAsStringAsync();
var serializer = new XmlSerializer(typeof(T));
return (T)serializer.Deserialize(new StringReader(result));
}
}
}
and it fixes the bug.
When examining the redirect request everything seems fine. The redirect call works from Postman.
Any idea why is this happening?
First, don't always believe the http status responses. Websites often change the values to make it harder for hackers to get access to the server. When a Http connection is made a negotiation takes place between client and server using the http headers. The negotiation attempts to get a common mode for the transfer. For example a server may be designed to work in English, French, and German. The client includes English in the header so the client will then connect to the English webpages. So you leaving out the AuthenticationHeaderValue is not a bug, but a line of code that is required. A IE webpage is more robust than the Net Http Client and sends more default headers. If you used a sniffer like wireshark or fiddler and compared the headers using an IE and you application you would see the differences.
This occurred within the context of coding against Google Cloud Messaging, but applies elsewhere.
Consider the following:
var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("key=XXX");
and
var http = new HttpClient();
http.DefaultRequestHeaders.Add("Authorization", "key=XXX");
both of which generate a FormatException:
System.FormatException : The format of value key=XXX' is invalid.
The solution is to remove the equals sign.
Digging into reflector shows there is oodles of validation and parsing code that runs when adding a a new header value. Why is all this necessary? Shouldn't this client just be getting out of our way?
How do you escape the equals sign so that adding this value succeeds?
Not sure if still relevant, but I recently ran into this same issue and was able to solve it by calling a different method to add the header information:
var http = new HttpClient();
http.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", "key=XXX");
To your "why is all this (parsing and validation) necessary" question, the answer is: it is defined in the HTTP standard.
In HTTP/1.1 and RFC2617, the value an authentication header (such as WWW-Authenticate and Authorization) has two parts: a scheme part, and a parameter part.
For HTTP Basic Authentication, the scheme is "Basic", and the parameter may be something like "QWxhZGRpbjpvcGVuIHNlc2FtZQ==", so the whole header becomes:
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
That's why your "key=XXX" doesn't pass validation, because it lacks a scheme part.
I ran into this error and stumbled on to this post when I added a space to the end of an Authorization header.
this.bearerAuthHttpClient.DefaultRequestHeaders.Add("Authorization ", $"Bearer {token}");
You can see the offending " " after Authorization.
It took me about 15 min before I saw my typo...
I got around this exception (my FormatException caused by commas in the value) by setting the Authorization header in the following way:
var authenticationHeaderValue = new AuthenticationHeaderValue("some scheme", "some value");
client.DefaultRequestHeaders.Authorization = authenticationHeaderValue;
I've been going through a few questions this morning while dealing with an external API that doesn't follow the HTTP spec to the letter.
As part of my posting, they want the Content-Type and Content-Disposition, which cannot be added to the HttpClient object. To add those headers, you need to create an HttpRequestMessage. On there, you need to add the headers to the Content property.
private HttpRequestMessage GetPostMessage(string uri, string contentType,
string fileName, Stream content)
{
var request = new HttpRequestMessage
{
Content = new StreamContent(content),
RequestUri = new Uri(uri),
Method = HttpMethod.Post
};
// contentType = "video/mp4"
request.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType);
//Need TryAddWithoutValidation because of the equals sign in the value.
request.Content
.Headers
.TryAddWithoutValidation("Content-Disposition",
$"attachment; filename=\"{Path.GetFileName(fileName)}\"");
// If there is no equals sign in your content disposition, this will work:
// request.Content.Headers.ContentDisposition =
// new ContentDispositionHeaderValue($"attachment; \"{Path.GetFileName(fileName)}\"");
return request;
}
In my case I am generating ETags string values from a byte[] RowVersion SQL field.
So I need to add wrap the generated. i.e. AAAAAAAAF5s= string inside " as follows...
var eTag = department.RowVersion.ToETagString();
httpClient.DefaultRequestHeaders.Add(Microsoft.Net.Http.Headers.HeaderNames.IfMatch, $"\"{eTag}\"")
public class DepartmentForHandleDto
{
public string Name { get; set; }
public string GroupName { get; set; }
public byte[] RowVersion { get; set; }
}
public static class ByteArrayExtensions
{
public static string ToETagString(this byte[] byteArray)
{
return Convert.ToBase64String(byteArray != null && byteArray.Length > 0 ? byteArray : new byte[8]);
}
}
I want to get the size of an http:/.../file before I download it. The file can be a webpage, image, or a media file. Can this be done with HTTP headers? How do I download just the file HTTP header?
Yes, assuming the HTTP server you're talking to supports/allows this:
public long GetFileSize(string url)
{
long result = -1;
System.Net.WebRequest req = System.Net.WebRequest.Create(url);
req.Method = "HEAD";
using (System.Net.WebResponse resp = req.GetResponse())
{
if (long.TryParse(resp.Headers.Get("Content-Length"), out long ContentLength))
{
result = ContentLength;
}
}
return result;
}
If using the HEAD method is not allowed, or the Content-Length header is not present in the server reply, the only way to determine the size of the content on the server is to download it. Since this is not particularly reliable, most servers will include this information.
Can this be done with HTTP headers?
Yes, this is the way to go. If the information is provided, it's in the header as the Content-Length. Note, however, that this is not necessarily the case.
Downloading only the header can be done using a HEAD request instead of GET. Maybe the following code helps:
HttpWebRequest req = (HttpWebRequest)WebRequest.Create("http://example.com/");
req.Method = "HEAD";
long len;
using(HttpWebResponse resp = (HttpWebResponse)(req.GetResponse()))
{
len = resp.ContentLength;
}
Notice the property for the content length on the HttpWebResponse object – no need to parse the Content-Length header manually.
Note that not every server accepts HTTP HEAD requests. One alternative approach to get the file size is to make an HTTP GET call to the server requesting only a portion of the file to keep the response small and retrieve the file size from the metadata that is returned as part of the response content header.
The standard System.Net.Http.HttpClient can be used to accomplish this. The partial content is requested by setting a byte range on the request message header as:
request.Headers.Range = new RangeHeaderValue(startByte, endByte)
The server responds with a message containing the requested range as well as the entire file size. This information is returned in the response content header (response.Content.Header) with the key "Content-Range".
Here's an example of the content range in the response message content header:
{
"Key": "Content-Range",
"Value": [
"bytes 0-15/2328372"
]
}
In this example the header value implies the response contains bytes 0 to 15 (i.e., 16 bytes total) and the file is 2,328,372 bytes in its entirety.
Here's a sample implementation of this method:
public static class HttpClientExtensions
{
public static async Task<long> GetContentSizeAsync(this System.Net.Http.HttpClient client, string url)
{
using (var request = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Get, url))
{
// In order to keep the response as small as possible, set the requested byte range to [0,0] (i.e., only the first byte)
request.Headers.Range = new System.Net.Http.Headers.RangeHeaderValue(from: 0, to: 0);
using (var response = await client.SendAsync(request))
{
response.EnsureSuccessStatusCode();
if (response.StatusCode != System.Net.HttpStatusCode.PartialContent)
throw new System.Net.WebException($"expected partial content response ({System.Net.HttpStatusCode.PartialContent}), instead received: {response.StatusCode}");
var contentRange = response.Content.Headers.GetValues(#"Content-Range").Single();
var lengthString = System.Text.RegularExpressions.Regex.Match(contentRange, #"(?<=^bytes\s[0-9]+\-[0-9]+/)[0-9]+$").Value;
return long.Parse(lengthString);
}
}
}
}
WebClient webClient = new WebClient();
webClient.OpenRead("http://stackoverflow.com/robots.txt");
long totalSizeBytes= Convert.ToInt64(webClient.ResponseHeaders["Content-Length"]);
Console.WriteLine((totalSizeBytes));
HttpClient client = new HttpClient(
new HttpClientHandler() {
Proxy = null, UseProxy = false
} // removes the delay getting a response from the server, if you not use Proxy
);
public async Task<long?> GetContentSizeAsync(string url) {
using (HttpResponseMessage responce = await client.GetAsync(url))
return responce.Content.Headers.ContentLength;
}