I'm trying to send a push notification though Pushover API with attached image using C# .NET. The following code returns a json formatted error "message cannot be blank". But the message variable is not empty. Since SSL is outdated I tried using TLS 1.2 explicitly. The same error appears without the image parameter.
public async Task PushImage(string title, string message, Stream image, string userKey, string appKey)
{
// This does not work - error "message cannot be blank"
using (HttpClient httpClient = new HttpClient())
{
//specify to use TLS 1.2 as default connection
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
MultipartFormDataContent form = new MultipartFormDataContent();
form.Add(new StringContent(appKey), "token");
form.Add(new StringContent(userKey), "user");
form.Add(new StringContent(message), "message");
var imageParameter = new StreamContent(image);
imageParameter.Headers.ContentType = MediaTypeHeaderValue.Parse("image/png");
form.Add(imageParameter, "attachment", "image.png");
// Remove content type that is not in the docs
foreach (var param in form)
param.Headers.ContentType = null;
HttpResponseMessage responseMessage = await httpClient.PostAsync(BaseApiUrl, form);
if (responseMessage.IsSuccessStatusCode)
return;
string contentText = responseMessage.Content.ReadAsStringAsync().Result;
var response = JsonConvert.DeserializeObject<PushResponse>(contentText);
throw new ApplicationException(
$"Push image request failed with status {(int)responseMessage.StatusCode} {responseMessage.StatusCode}: {response.Errors.JoinStrings(". ") ?? ""}");
}
}
Result:
{"message":"cannot be blank","errors":["message cannot be blank"],"status":0,"request":"94152901-3b8f-45d6-ae6b-f7fc10b3439c"}
I've looked at the raw request through Charles and it appears more or less as the docs suggest. However there is a small difference.
Curl - which works - produces parameters that look like this:
--------------------------30e0433d33c92cae
Content-Disposition: form-data; name="message"
my message
--------------------------30e0433d33c92cae--
HttpClient - which does not yet work - produces this for each parameter:
--70ae375f-ef30-4885-8a8a-d38363080024
Content-Disposition: form-data; name=message
my message
--70ae375f-ef30-4885-8a8a-d38363080024--
Note the difference in quotes. If I intercept the message in Charles and enclose the parameter names in double quotes as well as increase Content-Length by the same amount, it works!
It turns out that you need to enclose the parameter names in double quotes, like so:
form.Add(new StringContent(appKey), "\"token\"");
form.Add(new StringContent(userKey), "\"user\"");
form.Add(new StringContent(message), "\"message\"");
...
form.Add(imageParameter, "\"attachment\"", "image.png");
Don't ask me why. I just want to get on with my life and forget this entire day I spent debugging this issue...
Related
I'm fairly new to .NET's HTTPClient class, hence kindly excuse if I sounded noob. I'm tryin to replicate Postman's POST request in C# .Net and written following code. However I'm not getting any response but StatusCode: 404. Could someone assist understanding where I'm going wrong?
Also I'd like to understand, how do set Body in following code.
var httpClient = new HttpClient
{
BaseAddress = new Uri("https://testURL.com"),
Timeout = TimeSpan.FromMinutes(10)
};
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("audio/wav"));
httpClient.DefaultRequestHeaders.Add("Authorization", "Basic ldjfdljfdlfjdsjfdsl");
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("model", "Test"),
});
var result = httpClient.PostAsync("api/v1/recognize", content).Result;
Here is what I'm doing in Postman and it works:
"Params" in Postman refers to query parameters which are appended to the URL. You'll see that the URL in Postman contains the parameters you added in the "Params" tab:
However, it seems those are just dummy values you've entered so perhaps you don't need them? In any case, the way you add query parameters to the request for HttpClient is a little different as it needs to be added to the URL.
After that you also need to add the audio file as content to your request. At the moment you're setting the "Accept" header to "audio/wav" but you probably want to set the "Content-Type" header instead (or are you expecting a WAV file to be returned in the response too?).
As far as I can see this is what you're missing:
using (var httpClient = new HttpClient())
{
httpClient.Timeout = TimeSpan.FromMinutes(10);
// Set request headers
httpClient.DefaultRequestHeaders.Add("Authorization", "Basic ldjfdljfdlfjdsjfdsl");
// Set query parameters
var uriBuilder = new UriBuilder("https://testURL.com/api/v1/recognize");
uriBuilder.Query = "model=Test";
// Build request body
// Read bytes from the file being uploaded
var fileBytes = File.ReadAllBytes(wavFilePath);
// Create request content with metadata/headers to tell the
// backend which type of data (media type) is being uploaded
var byteArrayContent = new ByteArrayContent(fileBytes);
byteArrayContent.Headers.ContentType = MediaTypeHeaderValue.Parse("audio/wav");
// Wrap/encode the content as "multipart/form-data"
// See example of how the output/request looks here:
// https://dotnetfiddle.net/qDMwFh
var requestContent = new MultipartFormDataContent
{
{byteArrayContent, "audio", "filename.wav"}
};
var response = await httpClient.PostAsync(uriBuilder.Uri, requestContent);
}
I haven't tested this of course against your application, but it should be something along the lines of this. It might be that the backend doesn't expect "multipart/form-data" and just needs the "audio/wav". I can't see the output headers in your Postman screenshots, but if so, you can use byteArrayContent directly instead of wrapping it in MultipartFormDataContent.
Note: Don't use httpClient.PostAsync(...).Result. If you want to use the asynchronous method, you should await it. Depending on your code, using Result might give you problems if you're not careful. And remember to dispose the HttpClient after use (easiest solution is to use a using statement). If you plan on reusing the HttpClient for more requests, you can avoid disposing it until you're done.
I call the microsoft recognize text api by passing the image that I had taken from my phone, there no error occur but every time the api will return me empty string as result doesn't matter what image I post . I try those image with the microsoft ocr api and it return me result, can anyone help ?
I call the microsoft recognize text api by passing the image that I had taken from my phone, there no error occur but every time the api will return me empty string as result doesn't matter what image I post.
In documentation of Recognize Text API, we can find:
The service has accepted the request and will start processing later.
It will return Accepted immediately and include an “Operation-Location” header. Client side should further query the operation status using the URL specified in this header.
I suspect that you directly get/extract content from the response after you made a request to recognize text, so the content would be string.Empty.
To get recognize text operation result, you need to make a further request using the URL specified in response header "Operation-Location".
In following test code, we can find the content is indeed empty.
Test Code:
var client = new HttpClient();
var queryString = HttpUtility.ParseQueryString(string.Empty);
client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", "{Subscription_Key_here}");
queryString["mode"] = "Printed";
var uri = "https://{region}.api.cognitive.microsoft.com/vision/v2.0/recognizeText?" + queryString;
HttpResponseMessage response;
var imagePath = #"D:\xxx\xxx\xxx\testcontent.PNG";
Stream imageStream = File.OpenRead(imagePath);
BinaryReader binaryReader = new BinaryReader(imageStream);
byte[] byteData = binaryReader.ReadBytes((int)imageStream.Length);
using (var content = new ByteArrayContent(byteData))
{
content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
response = await client.PostAsync(uri, content);
if (response.IsSuccessStatusCode)
{
var contentString = await response.Content.ReadAsStringAsync();
var operation_location = response.Headers.GetValues("Operation-Location").FirstOrDefault();
Console.WriteLine($"Response content is empty({contentString == string.Empty}).\n\rYou should further query the operation status using the URL ({operation_location}) specified in response header.");
}
}
Test Result:
Note: Recognize Text API is currently in preview and is only available for English text. As you mentioned, OCR technology in Computer Vision can also help detect text content in an image, and OCR currently supports 25 languages, sometimes we can use OCR as an alternative solution.
I am struggling with Rest call. Here is my code and it is working for basic authentication.
public async Task RunAsync(string name, string value)
{
using (var handler = new HttpClientHandler { UseDefaultCredentials = true })
using (var client = new HttpClient(handler))
{
var byteArray = Encoding.ASCII.GetBytes("username:password");
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray));
var urlRestGet = HomeController.url;
client.BaseAddress = new Uri(urlRestGet + "?name=" + name + "&value=" + value + "");
client.DefaultRequestHeaders.Accept.Clear();
**1. if(HomeController.contentType.ToLower()=="xml"){
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));
}**
else if (HomeController.contentType.ToLower() == "json")
{
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}
HttpResponseMessage response = await client.GetAsync(urlRestGet + "?name=" + name + "&value=" + value + "");
if (response.IsSuccessStatusCode)
{
//Get the response
loginJsonString = await response.Content.ReadAsStringAsync();
//Converting to xml
using (var stream = new MemoryStream(Encoding.ASCII.GetBytes(loginJsonString)))
{
var output = new XmlDictionaryReaderQuotas();
xmlResult = XDocument.Load(JsonReaderWriterFactory.CreateJsonReader(stream, output)).ToString();
}
}
}
}
1) If the content type is application/xml am I correct to use line 1 part in the code.
2) How can I make this code more generic. (when the authentication type is different eg: tokenized or cookiebased how can I change this.)
There are a couple of things about your code I do not understand.
What is HomeController.contentType all about? The name "HomeController" suggests you're writing an ASP.NET MVC Controller (serverside). Though you seem to be writing something intended to be used as a HTTP client. But I could be mistaken or mislead here.
You are reading a Json response, then loading it as a Xml document?
I'll try to answer anyway.
1) If the content type is application/xml am I correct to use line 1 part in the code.
The Accept header sent by the client (you) tells the server that you accept the given content type. If you send Accept application/xml you tell the server you prefer if the response is Xml.
Your code seem to assume the response's content type is always Json.
You could include both application/xml and application/json as Accept headers in your request. The server should honor that request and pick the first supported content type for it's response.
When processing the response you should check the actual content type and handle the response content appropriately.
Remember that Accept only tells the server that you prefer those content types. The server may decide not to honor your whishes and can return any content type it desires.
2) How can I make this code more generic. (when the authentication type is different eg: tokenized or cookiebased how can I change this.)
If you mean tokenized as in a query parameter you should probably handle your query parameters as a collection rather than a hardcoded formatted string.
Check out the NameValueCollection class and this SO question on NameValueCollection to query string.
To handle cookies, you basically need to copy/re-use the cookie collection returned in a response in the next request.
See this SO question on how to inject cookies when you create a new HttpClient.
... but it's much easier to use a library
As you already discovered, making a robust REST/HTTP client is not a easy task.
And as #Mafii and #Fildor already pointed out in comments there are already numerous libraries available. RestSharp (https://restsharp.org) being one very popular.
I am POST-ing an image with HttpClient and it works well for files with Latin names, but as soon as a name contains any non-ASCII characters it gets transformed to a sequence of question marks. If I create an html form and use a browser to post the file, the file name is sent in UTF8 and the target server perfectly accepts it.
using (var client = new HttpClient())
{
var streamContent = new StreamContent(someImageFileStream);
streamContent.Headers.Add(
"Content-Disposition",
"form-data; name=\"image\"; filename=\"Тест.jpg\"");
var content = new MultipartFormDataContent();
content.Add(streamContent);
await client.PostAsync("http://localhost.fiddler/", content);
}
This produces the following request:
POST http://localhost/ HTTP/1.1
Content-Type: multipart/form-data; boundary="e6fe89be-e652-4fe3-8859-8c7a339c5550"
Host: localhost
Content-Length: 10556
--e6fe89be-e652-4fe3-8859-8c7a339c5550
Content-Disposition: form-data; name="image"; filename="????.jpg"
...here goes the contents of the file...
I understand that HttpClient might work according to some standard, but anyway, is there any workaround?
UPDATE: The external API doesn't want to accept the format filename*=utf-8''Тест.jpg, it expects filename="Тест.jpg".
This is another way to workaround the limitation of HttpClient without tampering with internal fields. Inspired by this answer.
using (var client = new HttpClient())
{
var streamContent = new StreamContent(someImageFileStream);
streamContent.Headers.Add("Content-Disposition",
new string(Encoding.UTF8.GetBytes("form-data; name=\"image\"; filename=\"Тест.jpg\"").
Select(b => (char)b).ToArray()));
var content = new MultipartFormDataContent();
content.Add(streamContent);
await client.PostAsync("http://localhost.fiddler/", content);
}
I confirm that even .net core 2.2 doesn't have proper support for uploading files whose names contain non-ASCII characters. HttpClient does work according to some standard but Java servers don't care about that standard and expect UTF-8 formatted headers.
OK, I've found a way to force MultipartFormDataContent to forget the ancient RFCs and use UTF8 instead. The trick is to use reflection to overwrite the DefaultHttpEncoding defined in the internal static class HttpRuleParser.
typeof(HttpClient)
.Assembly
.GetType("System.Net.Http.HttpRuleParser")
.GetField("DefaultHttpEncoding", BindingFlags.Static | BindingFlags.NonPublic)
.SetValue(null, System.Text.Encoding.UTF8);
Not sure which bad consequences that might cause, but I suppose there are none.
Instead of adding a header that you built yourself, use the .NET library:
streamContent.Headers.ContentDisposition =
new System.Net.Http.Headers.ContentDispositionHeaderValue("form-data") {
Name = "image",
FileName = "Тест.jpg" };
That creates the header per the web docs and RFC5987.
Content-Disposition: form-data; name=image; filename="=?utf-8?B?0KLQtdGB0YIuanBn?="
If it helps, you can also remove the "filename*"
//It deletes filename* parametr
foreach (var content in multipartContent) {
var headerContent = content.Headers.ContentDisposition.Parameters.Where(x => x.Name == "filename*").SingleOrDefault();
if(headerContent != null)
content.Headers.ContentDisposition.Parameters.Remove(headerContent);
}
I'm a bit stuck, I am trying to create a UWP App that will post XML content to a web service. I can get this to work in a regular .net console app without an issue. Trying to re-create this using UWP is proving to be tricky. Using fiddler I've narrowed down that the web service end point isn't receiving my content. It looks like the headers are setup properly the content length is sent correctly but the actual content isn't sent. Here is the heart of the code, it crashes/throws an exception after:
HttpResponseMessage ResponseMessage = await request.PostAsync(requestUri, httpContent2).ContinueWith(
(postTask) => postTask.Result.EnsureSuccessStatusCode());
When I try to execute the PostASync, looking at fiddler, I'm getting:
HTTP/1.1 408 Request body incomplete
Date: Mon, 14 Nov 2016 15:38:53 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
Cache-Control: no-cache, must-revalidate
Timestamp: 10:38:53.430
The request body did not contain the specified number of bytes. Got 0, expected 617
I'm positive that I am getting content to post correct (I read it from a file, I send it to debug window to verify and it's correct). I think it might have to do with HttpContent httpContent2 - In regular .NET I've never needed to use this but with PostAsync I need to use it.
Any thoughts would be appreciated, thank you!
public async void PostWebService()
{
string filePath = "Data\\postbody.txt";
string url = "https://outlook.office365.com/EWS/Exchange.asmx";
Uri requestUri = new Uri(url); //replace your Url
var myClientHandler = new HttpClientHandler();
myClientHandler.Credentials = new NetworkCredential("user#acme.com", "password");
HttpClient request = new HttpClient(myClientHandler);
string contents = await ReadFileContentsAsync(filePath);
Debug.WriteLine(contents);
HttpContent httpContent2 = new StringContent(contents, Encoding.UTF8, "text/xml");
string s = await httpContent2.ReadAsStringAsync();
Debug.WriteLine(s); //just checking to see if httpContent has the correct data
//HttpResponseMessage ResponseMessage = await request.PostAsync(requestUri, httpContent);
request.MaxResponseContentBufferSize = 65000;
HttpResponseMessage ResponseMessage = await request.PostAsync(requestUri, httpContent2).ContinueWith(
(postTask) => postTask.Result.EnsureSuccessStatusCode());
Debug.WriteLine(ResponseMessage.ToString());
}
Well it seems like I found the root cause to my problem. This is appears to be a known bug with System.Net.Http.HttpClient when using network authentication. See this article here
My initial mistake was that I wasn't catching an exceptions thrown by PostAsync. once I wrapped that inside a try/catch block I got the following exception thrown:
“This IRandomAccessStream does not support the GetInputStreamAt method because it requires cloning and this stream does not support cloning.”
The first paragraph of the article I linked to above states:
When you use the System.Net.Http.HttpClient class from a .NET
framework based Universal Windows Platform (UWP) app and send a
HTTP(s) PUT or POST request to a URI which requires Integrated Windows
Authentication – such as Negotiate/NTLM, an exception will be thrown.
The thrown exception will have an InnerException property set to the
message:
“This IRandomAccessStream does not support the GetInputStreamAt method
because it requires cloning and this stream does not support cloning.”
The problem happens because the request as well as the entity body of
the POST/PUT request needs to be resubmitted during the authentication
challenge. The above problem does not happen for HTTP verbs such as
GET which do not require an entity body.
This is a known issue in the RTM release of the Windows 10 SDK and we
are tracking a fix for this issue for a subsequent release.
The recommendation and work around that worked for me was to use the Windows.Web.Http.HttpClient instead of System.Net.Http.HttpClient
Using that recommendation, the following code worked for me:
string filePath = "Data\\postbody.txt";
string url = "https://outlook.office365.com/EWS/Exchange.asmx";
Uri requestUri = new Uri(url); //replace your Url
string contents = await ReadFileContentsAsync(filePath);
string search_str = txtSearch.Text;
Debug.WriteLine("Search query:" + search_str);
contents = contents.Replace("%SEARCH%", search_str);
Windows.Web.Http.Filters.HttpBaseProtocolFilter hbpf = new Windows.Web.Http.Filters.HttpBaseProtocolFilter();
Windows.Security.Credentials.PasswordCredential pcred = new Windows.Security.Credentials.PasswordCredential(url, "username#acme.com", "password");
hbpf.ServerCredential = pcred;
HttpClient request = new HttpClient(hbpf);
Windows.Web.Http.HttpRequestMessage hreqm = new Windows.Web.Http.HttpRequestMessage(Windows.Web.Http.HttpMethod.Post, new Uri(url));
Windows.Web.Http.HttpStringContent hstr = new Windows.Web.Http.HttpStringContent(contents, Windows.Storage.Streams.UnicodeEncoding.Utf8, "text/xml");
hreqm.Content = hstr;
// consume the HttpResponseMessage and the remainder of your code logic from here.
try
{
Windows.Web.Http.HttpResponseMessage hrespm = await request.SendRequestAsync(hreqm);
Debug.WriteLine(hrespm.Content);
String respcontent = await hrespm.Content.ReadAsStringAsync();
}
catch (Exception ex)
{
string e = ex.Message;
Debug.WriteLine(e);
}
Hopefully this is helpful to someone else hitting this issue.