I am using PowerShell and CSOM to mirror Sharepoint Online and OneDrive sites with all their files.
Consequently, after a few thousand files/a few hours of file download, an "The operation has timed out" exception is thrown, as expected. This is due to Microsoft's throttling.
To prevent the timeout, I am using the RequestTimeOut Paramter of the CSOM context, and also doing incremental retry, and also throttling the amount of ExecuteQuery() calls to 2 per second, and also decorating the CSOM call. That is all not enough, though.
The http response header of the failed call is supposed to include a "Retry-After" line, which I would like to use to time the retry.
The Exception happens either during Microsoft.SharePoint.Client.ClientContext's ExecuteQuery() or [Microsoft.SharePoint.Client.File]::OpenBinaryDirect().
Here is some simplified code extract:
$Context = New-Object Microsoft.SharePoint.Client.ClientContext($WebURL)
$Context.Credentials = $spoCredential
$Context.RequestTimeout = 60000; # 1 min
$Context.add_ExecutingWebRequest({
param($Source, $EventArgs)
$request = $EventArgs.WebRequestExecutor.WebRequest
$request.UserAgent = "XXX|CsomPs|MyScript/1.0"
})
$Web = $Context.Web
$Context.Load($Web)
$Context.ExecuteQuery()
Which all works perfectly well, until the "The operation has timed out" exception is thrown. Say, $Context.ExecuteQuery() of the sample throws the exception.
How do I access the http response and especially the http response headers and even more especially the Retry-After header within my CSOM powershell script?
Thanks!
First thing, you can't access and custom HTTP response. This is handled by SharePoint Online side.
Second, you should avoid getting throttled or blocked in SharePoint Online.
Microsoft don't have exact throttling limit, so it's better for you to reduce the number of operations per request or reduce frequency of calls
Here is the code of decorating traffic in CSOM
// Get access to source site
using (var ctx = new ClientContext("https://contoso.sharepoint.com/sites/team"))
{
//Provide account and pwd for connecting to SharePoint Online
var passWord = new SecureString();
foreach (char c in pwd.ToCharArray()) passWord.AppendChar(c);
ctx.Credentials = new SharePointOnlineCredentials("contoso#contoso.onmicrosoft.com",
passWord);
// Add our User Agent information
ctx.ExecutingWebRequest += delegate (object sender, WebRequestEventArgs e)
{
e.WebRequestExecutor.WebRequest.UserAgent = "NONISV|Contoso|GovernanceCheck/1.0";
};
// Normal CSOM Call with custom User-Agent information
Web site = ctx.Web;
ctx.Load(site);
ctx.ExecuteQuery();
}
If this is not helpful, you can follow up CoreThrottling this demonstration.
You can access the headers from the failed CSOM response this way. This is interchangeable to Powershell.
try {
context.ExecuteQuery();
}
catch(WebException wex) {
var response = wex.Response as HttpWebResponse;
if (response != null && (response.StatusCode == (HttpStatusCode)429 || response.StatusCode == (HttpStatusCode)503))
{
// Reference the headers in the throttled / failed response
response.Headers
}
}
Full MS code sample (search page for .RetryQuery): https://learn.microsoft.com/en-us/sharepoint/dev/general-development/how-to-avoid-getting-throttled-or-blocked-in-sharepoint-online
After some extensive throttling, i have never actually seen the Retry-After value come back as anything other than 120 (seconds).
Related
I have been trying to retrieve list of SendGrid transactional templates using API. I'm using correct API key and getting an empty array while there are about 5 transactional templates existing in my SendGrid account. Here is the response:
{
"templates": []
}
Any guesses what could be wrong?
Any guesses what could be wrong?
Yep, their documentation could be!
I also stuck with the problem and finally managed to solve it once I opened the devtools and saw how they request their own API from the UI. Long story short - one has to pass additional generations=dynamic query parameter. Here is the C# code I use:
var client = new SendGridClient("key");
var response = await client.RequestAsync(
SendGridClient.Method.GET,
urlPath: "/templates",
queryParams: "{\"generations\": \"dynamic\"}");
Using Api 7.3.0 PHP
require("../../sendgrid-php.php");
$apiKey = getenv('SENDGRID_API_KEY');
$sg = new \SendGrid($apiKey);
#Comma-delimited list specifying which generations of templates to return. Options are legacy, dynamic or legacy,dynamic
$query_params = json_decode('{"generations": "legacy,dynamic"}');
try {
#$response = $sg->client->templates()->get();
$response = $sg->client->templates()->get(null, $query_params);
echo $response->body();
exit;
} catch (Exception $e) {
echo '{"error":"Caught exception: '. $e->getMessage().'"}';
}
I had the same problem using the python wrapper provided by Sendgrid.
My code was similar to this:
response = SendGridAPIClient(<your api key>).client.templates.get({'generations': 'legacy,dynamic'})
This returned an empty array.
To fix you have to name the param or to pass None before the dict:
response = SendGridAPIClient(<your api key>).client.templates.get(None, {'generations': 'legacy,dynamic'})
or
response = SendGridAPIClient(<your api key>).client.templates.get(query_params={'generations': 'legacy,dynamic'})
When I try to call this https://learn.microsoft.com/en-us/rest/api/storageservices/get-blob-metadata api in microsoft flow I always get this error with 400 bad request.
I edited my authorization header regarding this answer https://stackoverflow.com/a/22029178/10389562 but couldn't get what am I doing wrong.
Method: GET
Uri: https://myaccount.blob.core.windows.net/containername/blobname?comp=metadata
Headers :
{
"Authorization": "SharedKey storageaccountname: primary key in the storage
account properties",
"x-ms-date": "Thu, 21 Sep 2018 23:45:00 GMT",
"x-ms-version": "2018-03-28"
}
After call this API I got this output
<?xml version="1.0" encoding="utf-8"?><Error>
<Code>InvalidAuthenticationInfo</Code><Message>Authentication information is
not given in the correct format. Check the value of Authorization header.
RequestId:f3b3051b-601e-00a4-4b3c-51c58d000000
Time:2018-09-20T23:46:40.6659210Z</Message></Error>
Thanks for any help
Update
In Microsoft Flow, calling Rest Api against Azure Storage seems not a valid way. Authorization needs x-ms-* headers sent by the flow(like x-ms-tracking-id,x-ms-workflow-id,etc.) adding to stringStr, which is not under our control. What's more, signature is only valid for 15m since generated.
There's a built-in Get Blob Metadata action. And for Storage, other common actions are available as well.
To set blob metadata, I suggest to host the logic in Azure function.
Follow this tutorial to create Function app and a httptrigger function, remember to choose the Storage Account where we need to set blob metadata.
Replace the httptrigger sample with code below, and modify metadataName to what you need.
#r "Microsoft.WindowsAzure.Storage"
using System.Net;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;
public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
dynamic data = await req.Content.ReadAsAsync<object>();
if (data == null)
{
return req.CreateResponse(HttpStatusCode.BadRequest, "No request body posted");
}
else
{
string metadata = data.metadata;
string blobName = data.blobName;
string containerName = data.containerName;
try
{
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(Environment.GetEnvironmentVariable("AzureWebJobsStorage"));
CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
CloudBlobContainer blobContainer = blobClient.GetContainerReference(containerName);
CloudBlob blob = blobContainer.GetBlobReference(blobName);
blob.Metadata.Add("metadataName", metadata);
blob.SetMetadata();
}
catch (Exception e)
{
log.Error(e.ToString());
return req.CreateResponse(HttpStatusCode.InternalServerError, "Fail to set metadata");
}
return (string.IsNullOrEmpty(metadata) || string.IsNullOrEmpty(blobName) || string.IsNullOrEmpty(containerName))
? req.CreateResponse(HttpStatusCode.BadRequest, "Please pass necessary parameters in the request body")
: req.CreateResponse(HttpStatusCode.OK, $"Metadata of {blobName} has been set");
}
}
In Microsoft Flow, create a Http action, post content below to the function url got in step 2.
{
"metadata": "test",
"blobName":"myblob",
"containerName":"mycontainer"
}
I'm making an async batch request with 50 report post request on it.
The first batch request returns me the Report Ids
1st Step
dynamic report_ids = await fb.PostTaskAsync(new
{
batch = batch,
access_token = token
});
Next I'm getting the reports info, to get the async status to see if they are ready to be downloaded.
2st Step
var tListBatchInfo = new List<DataTypes.Request.Batch>();
foreach (var report in report_ids)
{
if (report != null)
tListBatchInfo.Add(new DataTypes.Request.Batch
{
name = !ReferenceEquals(report.report_run_id, null) ? report.report_run_id.ToString() : report.id,
method = "GET",
relative_url = !ReferenceEquals(report.report_run_id, null) ? report.report_run_id.ToString() : report.id,
});
}
dynamic reports_info = await fb.PostTaskAsync(new
//dynamic results = fb.Post(new
{
batch = JsonConvert.SerializeObject(tListBatchInfo),
access_token = token
});
Some of the ids generated in the first step are returning this error, once I call them in the second step
Message: Unsupported get request. Object with ID '6057XXXXXX'
does not exist, cannot be loaded due to missing permissions, or does
not support this operation. Please read the Graph API documentation at
https://developers.facebook.com/docs/graph-api
I know the id is correct because I can see it in using facebook api explorer. What am I doing wrong?
This may be caused by Facebook's replication lag. That typically happens when your POST request is routed to server A, returning report ID, but query to that ID gets routed to server B, which doesn't know about the report existence yet.
If you try to query the ID later and it works, then it's the lag. Official FB advice for this is to simply wait a bit longer before querying the report.
https://developers.facebook.com/bugs/250454108686614/
I am having some trouble with PUT requests to the google sheets api.
I have this code
spreadsheet_inputer := WebClient(`$google_sheet_URI_cells/R3C6?access_token=$accesstoken`)
xml_test := XDoc{
XElem("entry")
{
addAttr("xmlns","http://www.w3.org/2005/Atom")
addAttr("xmlns:gs","http://schemas.google.com/spreadsheets/2006")
XElem("id") { XText("https://spreadsheets.google.com/feeds/cells/$spreadsheet_id/1/private/full/R3C6?access_token=$accesstoken"), },
XElem("link") { addAttr("rel","edit");addAttr("type","application/atom+xml");addAttr("href","https://spreadsheets.google.com/feeds/cells/$spreadsheet_id/1/private/full/R3C6?access_token=$accesstoken"); },
XElem("gs:cell") { addAttr("row","3");addAttr("col","6");addAttr("inputValue","testing 123"); },
},
}
spreadsheet_inputer.reqHeaders["If-match"] = "*"
spreadsheet_inputer.reqHeaders["Content-Type"] = "application/atom+xml"
spreadsheet_inputer.reqMethod = "PUT"
spreadsheet_inputer.writeReq
spreadsheet_inputer.reqOut.writeXml(xml_test.writeToStr).close
echo(spreadsheet_inputer.resStr)
Right now it returns
sys::IOErr: No input stream for response 0
at the echo statement.
I have all the necessary data (at least i'm pretty sure) and it works here https://developers.google.com/oauthplayground/
Just to note, it does not accurately update the calendars.
EDIT: I had it return the response code and it was a 0, any pointers on what this means from the google sheets api? Or the fantom webclient?
WebClient.resCode is a non-nullable Int so it is 0 by default hence the problem would be either the request not being sent or the response not being read.
As you are obviously writing the request, the problem should the latter. Try calling WebClient.readRes() before resStr.
This readRes()
Read the response status line and response headers. This method may be called after the request has been written via writeReq and reqOut. Once this method completes the response status and headers are available. If there is a response body, it is available for reading via resIn. Throw IOErr if there is a network or protocol error. Return this.
Try this:
echo(spreadsheet_inputer.readRes.resStr)
I suspect the following line will also cause you problems:
spreadsheet_inputer.reqOut.writeXml(xml_test.writeToStr).close
becasue writeXml() escapes the string to be XML safe, whereas you'll want to just print the string. Try this:
spreadsheet_inputer.reqOut.writeChars(xml_test.writeToStr).close
The Sharepoint Rest API uses a simple URL of the type http://mysite/_api/search/query?querytext='search_key' to return search results as an XML. When I run this directly in a browser, I see a valid XML response:
(1) Am I right in assuming the above response is generated using the current user's authorization?
(2) Can this URL be invoked from server side? I tried it in a web method (WCF web service), but received a 401 - Unauthorized:
public string GetSearchResults(string searchKey)
{
string webURL = SPContext.Current.Web.Url;
string searchURL = webURL + "/_api/search/query?querytext='" + searchKey + "'";
WebClient client = new WebClient();
string xmlResponse = client.DownloadString(searchURL); // throws 401
// parse xmlResponse and return appropriately
}
(3) What I really need is to be able to get the search results irrespective of the current user's access rights (the requirement is that users will see all search results, with an option to "request access" when needed).
I tried this in the above web method, but it still throws the same 401:
public string GetSearchResults(string searchKey)
{
string webURL = SPContext.Current.Web.Url;
string searchURL = webURL + "/_api/search/query?querytext='" + searchKey + "'";
string xmlResponse;
SPSecurity.RunWithElevatedPrivileges(delegate()
{
WebClient client = new WebClient();
xmlResponse = client.DownloadString(searchURL); // still 401
});
// parse xmlResponse and return appropriately
}
What is the right way to invoke the Rest URL from server side? Specifically, from a web method? And how can it be run as super user?
In order to perform REST request, authenticate the request via WebClient.Credentials Property
On Premise (your scenario)
WebClient client = new WebClient();
client.Credentials = new NetworkCredential(userName,password,domain);
SharePoint Online
WebClient client = new WebClient();
client.Credentials = new SharePointOnlineCredentials(username,securedPassword);
client.Headers.Add("X-FORMS_BASED_AUTH_ACCEPTED", "f");
Search results are always security trimmed by SharePoint so to make this work, you'd need to run your query after specifying new credentials as mentioned by Vadim. This is almost certainly not a good idea. If you're running code server side already, don't use the REST interface, just query directly using the search API.