Translating RestSharp request to HttpClient request - c#

I'm programmatically uploading files to a remote server. The files are moderately large and I'd like to present a progress report to my users so they can see something happening. I was able to implement the upload using Postman which helpfully translated the whole thing to RestSharp.
But RestSharp does not provide any kind of progress tracking. I tried to implement the same functionality using HttpClient but it goes wrong somewhere the and server just throws a "400 - Bad Request" without telling exactly what is bad about it (its API documentation is also not for the faint of heart).
So, here's what Postman / RestSharp provide and which is working:
var client = new RestClient("https://opencast/ingest/addMediaPackage");
client.Timeout = -1;
var request = new RestRequest(Method.POST);
request.AddHeader("Authorization", "Basic FooBarBaz=");
request.AddParameter("creator", file.Creator);
request.AddParameter("title", file.Title);
request.AddParameter("flavor", "presentation/source");
request.AddParameter("description", file.Description);
try
{
request.AddFile("BODY", path);
IRestResponse response = await client.ExecuteAsync(request);
_logger.LogInformation($"Response after file upload: {response.StatusCode}");
File.Delete(path);
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception when uploading files: {Message}", ex.Message);
}
and here's what I tried to do with HttpClient (without try-catch):
var request = new HttpRequestMessage(HttpMethod.Post, $"https://opencast/ingest/addMediaPackage");
request.Headers.Add("Authorization", "Basic FooBarBaz=");
using var form = new MultipartFormDataContent();
using var fileContent = new ByteArrayContent(await File.ReadAllBytesAsync(path));
fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/form-data");
form.Add(fileContent, "BODY", Path.GetFileName(path));
form.Add(new StringContent(file.Creator), "creator");
form.Add(new StringContent(file.Title), "title");
form.Add(new StringContent("presentation/source"), "flavor");
form.Add(new StringContent(file.Description), "description");
request.Content = form;
var client = clientFactory.CreateClient(); //which is a IHttpClientFactory
var response = await client.SendAsync(request);
This code sends the file to the server which, after completing the upload, throws a 400.
Currently not seeing the difference. I could intercept the requests to see where they differ but maybe someone here can see the problem right away?
Update: It gets weirder. If I just use clientFactory.PostAsync(form) and add the Auth headers through form.Add then I get a 200 (i.e. Success) but the server simply swallows the file.

Okay, I found the solution. I'm not sure whether the WTF is me or the guys behind the server but...
... you need to add the fileContent last.
Yes, the order of the parameters matters for this.

Related

API PUT /marketing/contacts/imports with restsharp

Due to internal reason, I need to recode my servlet from Java to c#.
I am trying to upload a CSV file using the API PUT /marketing/contacts/imports with restsharp.
I cannot manage to send the file properly.
Code Snippet
Please fine below my java piece of code working:
File file = new File(CSV);
byte[] data;
try {
data = Files.readAllBytes(file.toPath());
HttpResponse<String> response2 = Unirest.put(URLSengrid)
.header(processSendgridHeader(headerFromSengrid).get(0), processSendgridHeader(headerFromSengrid).get(1))
//("x-amz-server-side-encryption", "aws:kms")
.body(data)
.asString();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
And here the non working c# code:
byte[] file = System.IO.File.ReadAllBytes(testPath);
var clientSecondCall = new RestClient(URLSendgrid);
var requestSecondCall = new RestRequest(Method.PUT);
requestSecondCall.AddHeader("content -type", "application/json");
requestSecondCall.AddHeader("x-amz-server-side-encryption", "aws:kms");
requestSecondCall.AddParameter("application/json", "{"file_type":"csv","field_mappings":["e1_T","e2_T","_rf2_T","e4_T","e5_T","e12_T","e13_T","e14_T","e15_T","e16_T"]}", ParameterType.RequestBody);
requestSecondCall.AddFile("file", file, testPath);
I spent a long time looking for an answer without success. Any help would be appreciated
Technical details:
sendgrid-csharp version: 9.*
csharp version: v4.0.303190
I believe the problem is the way you send the file in your c# code.
The Java code is clearly using the Body of the request, while the c# code is using RestSharp.
Restsharp is sending files in as a Multipart form, which your server is probably not qualified to handle.
I would recommend using HttpClient object:
HttpClient client = new HttpClient();
HttpRequestMessage request = new HttpRequestMessage();
request.Method = HttpMethod.Put;
request.RequestUri = new Uri( "Your Url");
request.Content = new StringContent(File.ReadAllText(yourFilePath));
request.Headers.Add("your header name", "your header value");
var response = client.SendAsync(request).Result;

PostAsync in UWP App not working (no content sent)

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.

Unable to create a shared link using the Box API V2

UPDATE: I figured it out and posted the answer below.
All I'm trying to do is update any file attribute. Description, name, anything, but no matter how I format it I get a 403.
I need to be able to modify a file so it can be shared via the Box API from a cloud app. I'm updating someone else's code from V1, but they are no longer available... I've tried many things but mostly just get 403 Forbidden errors.
There are no issues with OAuth2, that works fine and I can list files and folders, but can not modify them. This question is about sharing, but I can't change a description either. The box account is mine and I authenticate with my admin credentials. Any suggestions would be appreciated.
Here is the method I am using. I pass in the fileId and token and I've left out try/catch etc. for brevity.
string uri = string.Format("https://api.box.com/2.0/files/{0}", fileId);
string body = "{\"shared_link\": {\"access\": \"open\"}}";
byte[] postArray = Encoding.ASCII.GetBytes(body);
using (var client = new WebClient())
{
client.Headers.Add("Content-Type", "application/x-www-form-urlencoded");
client.Headers.Add("Authorization: Bearer " + token);
var response = client.UploadData(uri, postArray);
var responseString = Encoding.Default.GetString(response);
}
Thanks.
Okay, My Homer Simpson moment...
UploadData is a POST, I needed to do a PUT. Here is the solution.
string uri = String.Format(UriFiles, fileId);
string response = string.Empty;
string body = "{\"shared_link\": {\"access\": \"open\"}}";
byte[] postArray = Encoding.ASCII.GetBytes(body);
try
{
using (var client = new WebClient())
{
client.Headers.Add("Authorization: Bearer " + token);
client.Headers.Add("Content-Type", "application/json");
response = client.UploadString(uri, "PUT", body);
}
}
catch (Exception ex)
{
return null;
}
return response;
try changing your content type to 'multipart/form-data'?
I just looked up the api at: https://developers.box.com/docs/#files-upload-a-file
and it looks like the server is expecting a multipart post
here is stack overflow post on posting multipart data:
ASP.NET WebApi: how to perform a multipart post with file upload using WebApi HttpClient

Upload file via POST call with C#, RestSharp, Redmine API

I'm developing a C# app that uses Redmine REST API, it uses RestSharp Client. All other REST calls I make work fine but I cannot find a way to upload attachments. I've widely searched the web and tried many solutions but nothing worked.
Redmine documentiation on attachments: http://www.redmine.org/projects/redmine/wiki/Rest_api#Attaching-files
The code actually looks like:
RestClient client = new RestClient("http://myclient/redmine/");
client.Authenticator = new HttpBasicAuthenticator("myuser", "mypsw");
var request2 = new RestRequest("uploads.json", Method.POST);
request2.AddHeader("Content-Type", "application/octet-stream");
request2.RequestFormat = RestSharp.DataFormat.Json;
byte[] dataToSend = File.ReadAllBytes(AddIssue.attach.Text);
request2.AddBody(dataToSend);
IRestResponse response2 = client.Execute(request2);
resultbox.Text = response2.Content;
If I execute it above nothing happens and the response is empty. If I remove line 7 (the AddBody), it actually works but of course nothing is uploaded, JSON response:
{
"upload": {
"token": "11."
}
}
So actually, the real question is what to punt in AddBody() to send the file as application/octet-stream. Since RestSharp also has a request.AddFile() method, I tried it too in different ways but nothing...
Any help much appreciated!
As I mentioned in my comment, it sounds like Redmine might have requirements similar to Dropbox. Here is the solution that worked for me (based on the question Upload to dropbox using Restsharp PCL):
public static void UploadFileToDropbox(string filePath)
{
RestClient client = new RestClient("https://api-content.dropbox.com/1/");
IRestRequest request = new RestRequest("files_put/auto/{path}", Method.PUT);
FileInfo fileInfo = new FileInfo(filePath);
long fileLength = fileInfo.Length;
request.AddHeader("Authorization", "Bearer INSERT_DEVELOPER_TOKEN_HERE");
request.AddHeader("Content-Length", fileLength.ToString());
request.AddUrlSegment("path", string.Format("Public/{0}", fileInfo.Name));
byte[] data = File.ReadAllBytes(filePath);
var body = new Parameter
{
Name = "file",
Value = data,
Type = ParameterType.RequestBody,
};
request.Parameters.Add(body);
IRestResponse response = client.Execute(request);
}
Also published as a Gist.
I know this isn't your exact situation, but hopefully it gives you some ideas.

Why my Http client making 2 requests when I specify credentials?

I created RESTful webservice (WCF) where I check credentials on each request. One of my clients is Android app and everything seems to be great on server side. I get request and if it's got proper header - I process it, etc..
Now I created client app that uses this service. This is how I do GET:
// Create the web request
var request = WebRequest.Create(Context.ServiceURL + uri) as HttpWebRequest;
if (request != null)
{
request.ContentType = "application/json";
// Add authentication to request
request.Credentials = new NetworkCredential(Context.UserName, Context.Password);
// Get response
using (var response = request.GetResponse() as HttpWebResponse)
{
// Get the response stream
if (response != null)
{
var reader = new StreamReader(response.GetResponseStream());
// Console application output
var s = reader.ReadToEnd();
var serializer = new JavaScriptSerializer();
var returnValue = (T)serializer.Deserialize(s, typeof(T));
return returnValue;
}
}
}
So, this code get's my resource and deserializes it. As you see - I'm passing credentials in my call.
Then when debugging on server-side I noticed that I get 2 requests every time - one without authentication header and then server sends back response and second request comes bach with credentials. I think it's bad for my server - I'd rather don't make any roundtrips. How should I change client so it doesn't happen? See screenshot of Fiddler
EDIT:
This is JAVA code I use from Android - it doesn't do double-call:
MyHttpResponse response = new MyHttpResponse();
HttpClient client = mMyApplication.getHttpClient();
try
{
HttpGet request = new HttpGet(serviceURL + url);
request.setHeader(new BasicHeader(HTTP.CONTENT_TYPE, "application/json"));
request.addHeader("Authorization", "Basic " + Preferences.getAuthorizationTicket(mContext));
ResponseHandler<String> handler = new BasicResponseHandler();
response.Body = client.execute(request, handler);
response.Code = HttpURLConnection.HTTP_OK;
response.Message = "OK";
}
catch (HttpResponseException e)
{
response.Code = e.getStatusCode();
response.Message = e.getMessage();
LogData.InsertError(mContext, e);
}
The initial request doesn't ever specify the basic header for authentication. Additionally, since a realm is specified, you have to get that from the server. So you have to ask once: "hey, I need this stuff" and the server goes "who are you? the realm of answering is 'secure area'." (because realm means something here) Just because you added it here:
request.Credentials = new NetworkCredential(Context.UserName, Context.Password);
doesn't mean that it's going to be for sure attached everytime to the request.
Then you respond with the username/password (in this case you're doing BASIC so it's base64 encoded as name:password) and the server decodes it and says "ok, you're all clear, here's your data".
This is going to happen on a regular basis, and there's not a lot you can do about it. I would suggest that you also turn on HTTPS since the authentication is happening in plain text over the internet. (actually what you show seems to be over the intranet, but if you do go over the internet make it https).
Here's a link to Wikipedia that might help you further: http://en.wikipedia.org/wiki/Basic_access_authentication
Ok, I got it. I manually set HttpHeader instead of using request.Credentials
request.Headers.Add(HttpRequestHeader.Authorization, "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(Context.UserName + ":" + Context.Password)));
Now I see only single requests as expected..
As an option you can use PreAuthenticate property of HttpClientHandler. This would require a couple of lines more
var client = new HttpClient(new HttpClientHandler
{
Credentials = yourCredentials,
PreAuthenticate = true
});
With using this approach, only the first request is sent without credentials, but all the rest requests are OK.

Categories