I'm trying to use sync through an IIS 8 Webdav backend with Ensembles. The problem i encounter is that the first sync works fine, but when i try to sync a second time or on a second unit (iPad in this case) I get a server error 405 "method not allowed". Has anyone encountered this and got it working, to sync with IIS Webdav?
This is the allheaderfield property of the server response:
" UserInfo={NSLocalizedDescription=HTTP status code was {
Allow = "COPY, PROPFIND, DELETE, MOVE, PROPPATCH, LOCK, UNLOCK";
Connection = "Keep-Alive";
"Content-Length" = 1293;
"Content-Type" = "text/html";
Date = "Mon, 25 Jan 2016 12:02:07 GMT";
"Persistent-Auth" = true;
Server = "Microsoft-IIS/8.5";
"X-UA-Compatible" = "IE=8";
EDIT:
It might be possible that this isn't a configuration problem after all. I added a few logs and the createDirectoryAtPath method gives me HTTP error 405, this is the original code:
- (void)createDirectoryAtPath:(NSString *)path completion:(CDECompletionBlock)completion{
NSMutableURLRequest *request = [self mutableURLRequestForPath:path];
request.HTTPMethod = #"MKCOL";
[request setValue:#"application/xml" forHTTPHeaderField:#"Content-Type"];
[self sendURLRequest:request completion:^(NSError *error, NSInteger statusCode, NSData *responseData) {
if (completion) completion(error);
}];}
And this is the directoryExistsAtPath method:
- (void)directoryExistsAtPath:(NSString *)path completion:(CDEDirectoryExistenceCallback)completion{
[self sendPropertyFindRequestForPath:path depth:0 completion:^(NSError *error, NSInteger statusCode, NSData *responseData) {
if (error && statusCode != 404) {
if (completion) completion(NO, error);
}
else if (statusCode == 404) {
if (completion) completion(NO, nil);
}
else {
CDEWebDavResponseParser *parser = [[CDEWebDavResponseParser alloc] initWithData:responseData];
BOOL succeeded = [parser parse:&error];
if (!succeeded) {
if (completion) completion(NO, error);
return;
}
BOOL isDir = [parser.cloudItems.lastObject isKindOfClass:[CDECloudDirectory class]];
if (completion) completion(isDir, nil);
}
}];}
If i replace the first parameter (currently the isDir-variable) in the completion block at the end to YES, the 405 error does not appear.On logging the parser.clouditems.lastobject, I find that it is often (or always?) empty). So setting the parameter to YES, results in data being uploaded to my webdav, and the folders are in place. However, testing on a second unit (or reinstalling the app on the same unit), download never happens - the downloadFromPath never gets called, a "GET"-request is never sent.
Looking at the calling code in the underlying framework (CDECloudmanager mostly) hasn't led me anywhere so far.
As the directoryExistsAtPath is optional, i tried commenting it out, but i don't think it made a difference.
Another thing I noticed is that I get several baseline files in the baselines folder. According to the Ensembles documentation, there should only be one.
Any clues?
Ok seems I got this working at last. I had to make a change in the CDEWebDavCloudFileSystem class to avoid the 405 error. Doing that however, I encountered a 404 error. That one was solved by configuring the IIS webdav.
So step 1, I changed the request in the sendPropertyFindRequestForPath method:
new code:
static NSString *xml = #"<?xml version=\"1.0\" encoding=\"utf-8\" ?><D:propfind xmlns:D=\"DAV:\"><D:allprop/></D:propfind>";
original code:
static NSString *xml = #"<?xml version=\"1.0\" encoding=\"utf-8\" ?><D:propfind xmlns:D=\"DAV:\"><D:prop><D:href/><D:resourcetype/><D:creationdate/><D:getlastmodified/><D:getcontentlength/><D:response/></D:prop></D:propfind>";
To remove the 404 error appearing after that I had to add mime type application/xml to the .cdeevent extension.
This link goes into detail of how to configure IIS for that:
http://anandthearchitect.com/2013/08/01/webdav-404file-or-directory-not-found/
Related
I'm trying to create an iOS app that uses OAuth2 authentication using the native iOS NSURLSession URL loading classes. I gain an access token fine using the directions here:
http://www.freesound.org/docs/api/authentication.html
I subsequently launch the application and run a search query
https://www.freesound.org/apiv2/search/text/?query=snare
The request header fields looks like this (note my access token is not expired and I have confirmed it is the same as I received from performing the steps above)
{
"Authorization: Bearer" = MY_ACCESS_TOKEN;
}
This fails with:
{"detail": "Authentication credentials were not provided."}
The response headers look like this:
{
Allow = "GET, HEAD, OPTIONS";
Connection = "keep-alive";
"Content-Type" = "application/json";
Date = "Sat, 31 Jan 2015 13:56:32 GMT";
Server = "nginx/1.2.1";
"Transfer-Encoding" = Identity;
Vary = "Accept, Cookie";
"Www-Authenticate" = "Bearer realm=\"api\"";
}
The funny thing is that this does not always happen. If I repeat this entire process a number of times, deleting the app in between, it will eventually work. Once it works, it will continue to work while I'm developing. Sometimes then when I come back to it, say the next day, it stops working and I need to repeat this deleting and re-installing routine to get it back working again!
There's an authentication challenge delegate method on NSURLSession that will get called if implemented. It's a 'server trust' challenge. Could this be something to do with it? Would you even expect an authentication challenge of this nature? There's nothing mentioned about it in the docs alluded to above.
Any help would be much appreciated.
EDIT
This is how the search text ("snare") GET call is made.
I basically pass in an NSMutableURLRequest with the URL set to the above (https://www.freesound.org/apiv2/search/text/?query=snare). useAccessToken is set to YES.
- (void)makeRequest:(NSMutableURLRequest *)request useAccessToken:(BOOL)useAccessToken completion:(CompletionBlock)completion {
NSAssert(completion, #"No completion block.");
if (useAccessToken) {
NSString *accessToken = [[ODMFreesoundTokenCache sharedCache] accessToken];
NSAssert(accessToken.length, #"No access token.");
[request addValue:accessToken forHTTPHeaderField:#"Authorization: Bearer"];
}
NSLog(#"Making request: %# \n\nWith access token: %#", request, [[ODMFreesoundTokenCache sharedCache] accessToken]);
NSURLSessionDataTask *task = [self.session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSInteger code = [(NSHTTPURLResponse *)response statusCode];
if (code == 200) {
if (!error) {
id json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
NSLog(#"json: %#", json);
completion(json, error);
}
else {
completion(nil, error);
}
}
else {
NSString *reason = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSError *error = [NSError errorWithDomain:#"Request Error" code:code userInfo: reason ? #{NSLocalizedDescriptionKey : reason} : nil];
NSLog(#"error: %#", error);
completion(nil, error);
}
}];
[task resume];
}
The 2 flows for authentication described in the doc are not "safe" for a device. Using API keys would require the secret to be stored in the device.
The OAuth2 flow they support (authorization_code) requires a server to server call to exchange a code for the actual token (This step: http://www.freesound.org/docs/api/authentication.html#step-3). This call requires another credential (the client_secret that you probably should not store in the device either.
You need a server in between that negotiates this for you. Or a server that translates the code flow into token one. (Illustrated here: https://auth0.com/docs/protocols#5).
I have a URL that I'm trying to get XML from.
Now in my iOS app I have this, to get the data.
- (void)viewDidLoad {
[super viewDidLoad];
[self loadDataUsingNSURLConnection];
}
- (void) loadDataUsingNSURLConnection {
NSString *url = #"http://64.182.231.116/~spencerf/university_of_albany/u_albany_alumni_menu_test.xml";
[self getMenuItems:url];
}
And then finally this,
- (void)getMenuItems:(NSString*)url{
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
NSLog(#"response == %#", response);
NSLog(#"data: %#", data);
/*
self.mealdata=[[MealData alloc]init:data];
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
[self.loadingView removeFromSuperview];
});
*/
}];
}
Now sometimes when I run my app it works great and data is returned, but then sometimes, I would say about 25% of the time, with me changing nothing in-between runs. It returns no data, and the NSLog returns
2015-03-10 18:28:05.472 APP[6289:97905] response == <NSHTTPURLResponse:
0x7fee7628e000> { URL: http://64.182.231.116/~spencerf/university_of_albany/u_albany_alumni_menu_test.xml } { status code: 200, headers {
"Accept-Ranges" = bytes;
Connection = "Keep-Alive";
"Content-Length" = 0;
"Content-Type" = "application/xml";
Date = "Tue, 10 Mar 2015 22:28:04 GMT";
Etag = "W/\"218ceff-0-510f6aa0317dd\"";
"Keep-Alive" = "timeout=5, max=90";
"Last-Modified" = "Tue, 10 Mar 2015 22:28:03 GMT";
Server = Apache;
} }
2015-03-10 18:28:05.472 App[6289:97905] data: <>
Not sure why this is happening, and I can't tell the difference between when it works and when it doesn't what is changing? So Im not sure how to fix this?
What I want is it to get the data every time?
Thanks for the help in adavence.
Try to increase timeout interval for your request:
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]];
[request setTimeoutInterval:60.0];//seconds
Your code is fine. It is the server that is giving you trouble.
I reloaded the page that you mentioned 20 times. It successfully loaded 13 times. 7 times it returned no error but also an empty response body.
What you can do is simply check for this condition in your app and run the request again to try again.
Or talk to the server owner to find out if they can improve the reliability of this service.
Running your code on device, when there is no data received, error message is either null or Error Domain=NSURLErrorDomain Code=-1005 "The network connection was lost.".
As to the latter error, you can see AFNetworking/issues/2314, below is a comment extracted from the post.
when iOS client received HTTP response with a Keep-Alive header, it
keeps this connection to re-use later (as it should), but it keeps it
for more than the timeout parameter of the Keep-Alive header and then
when a second request comes it tries to re-use a connection that has
been dropped by the server.
You could also refer to the solution here, if you can't change the server, I'd suggest you retry the request when error code is -1005 or received data length is 0.
Perhaps Long-Polling would help you. The web server intermittently returns data when I tried it.
Long-Polling will allow you to continue sending requests until you get the data you specify back.
This code shows you an example of how implement long-polling
- (void) longPoll {
//create an autorelease pool for the thread
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
//compose the request
NSError* error = nil;
NSURLResponse* response = nil;
NSURL* requestUrl = [NSURL URLWithString:#"http://www.example.com/pollUrl"];
NSURLRequest* request = [NSURLRequest requestWithURL:requestUrl];
//send the request (will block until a response comes back)
NSData* responseData = [NSURLConnection sendSynchronousRequest:request
returningResponse:&response error:&error];
//pass the response on to the handler (can also check for errors here, if you want)
[self performSelectorOnMainThread:#selector(dataReceived:)
withObject:responseData waitUntilDone:YES];
//clear the pool
[pool drain];
//send the next poll request
[self performSelectorInBackground:#selector(longPoll) withObject: nil];
}
-(void) startPoll {
//not covered in this example: stopping the poll or ensuring that only 1 poll is active at any given time
[self performSelectorInBackground:#selector(longPoll) withObject: nil];
}
-(void) dataReceived: (NSData*) theData {
//process the response here
}
Your URL returns 404 Not Found, so you should fix server part
I'm dealing the authenticate issue with Tumblr account using [NSURLConnection sendAsynchronousRequest:queue:completionHandler:] to send the authenticate request, but here I meet a tough problem:
Whenever I send the request at the first time, everything goes perfectly, but when the first authentication is done and then resend the request second time, there comes "NSURLErrorDomain error -1012".
The authenticate page is loaded in a webview so that the authentication should be done in my app without a browser. But it is interesting that if the process runs in a browser there comes no error, errors only happen when using webview.
It was weird that the authentication goes with the same code, but only the first authentication can be done, only if I reinstall the app can I authenticate it again, and after this the problem comes again.
I did everything I can chase to solve the issue, I clean the cache and cookie in webview, step the authentication process to see parameters, set the cachePolicy of the request but nothing helps.
I also found that on ios6 the process goes without any error. But on ios7 I get the -1012.
code -1012 tells me that the user cancelled the authentication, but the process goes automatically and I do not cancel it.
I'm wondering if the problem comes from the NSURLConnection.
- (void)authenticate:(NSString *)URLScheme WithViewController:(UIViewController *)con callback:(TMAuthenticationCallback)callback {
self.threeLeggedOAuthTokenSecret = nil;
self.hostViewController = con;
self.callback = callback;
[self emptyCookieJar];
NSString *tokenRequestURLString = [NSString stringWithFormat:#"http://www.tumblr.com/oauth/request_token?oauth_callback=%#", TMURLEncode([NSString stringWithFormat:#"%#://tumblr-authorize", URLScheme])];
NSLog(#"%#", tokenRequestURLString);
NSMutableURLRequest *request = mutableRequestWithURLString(tokenRequestURLString);
NSLog(#"%#", request);
[[self class] signRequest:request withParameters:nil consumerKey:self.OAuthConsumerKey
consumerSecret:self.OAuthConsumerSecret token:nil tokenSecret:nil];
[self openOAuthViewController];
NSURLConnectionCompletionHandler handler = ^(NSURLResponse *response, NSData *data, NSError *error) {
NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode;
if (error) {
if (callback) {
callback(nil, nil, error);
}
return;
}
NSLog(#"%d", statusCode);
if (statusCode == 200) {
self.threeLeggedOAuthCallback = callback;
NSDictionary *responseParameters = formEncodedDataToDictionary(data);
self.threeLeggedOAuthTokenSecret = responseParameters[#"oauth_token_secret"];
NSURL *authURL = [NSURL URLWithString:
[NSString stringWithFormat:#"http://www.tumblr.com/oauth/authorize?oauth_token=%#",
responseParameters[#"oauth_token"]]];
[self initOAuthViewControllerWithURL:authURL];
} else {
if (callback) {
callback(nil, nil, errorWithStatusCode(statusCode));
}
}
};
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:handler];
}
Code above, everything goes normally before [NSURLConnection sendAsynchronousRequest:queue:completionHandler:],and after this method I got the error in completionHandler.
i want to get all articles from the shopware api(http://wiki.shopware.de/Shopware-API_cat_919.html)
but the i dont get the data into an NSDictionary
url i call: http://myshop.com/api/articles
here is the source i got
NSURL *url = [NSURL URLWithString:weburl];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response,
NSData *data, NSError *connectionError) {
if (data.length > 0 && connectionError == nil) {
NSDictionary *rest_data = [NSJSONSerialization JSONObjectWithData:data
options:0
error:NULL];
_newsDataForTable = [NSMutableArray array];
NSDictionary *news;
for (id key in rest_data[#"postalcodes"]) {
news = [rest_data[#"postalcodes"] objectForKey:key];
}
int iterator = 0;
for (id key in news) {
[_newsDataForTable insertObject:key[#"title"] atIndex:iterator];
iterator++;
}
[_newsTable reloadData];
[_newsTable numberOfRowsInSection:[_newsDataForTable count]];
[_newsTable reloadRowsAtIndexPaths:0 withRowAnimation:UITableViewRowAnimationLeft];
}
}];
}
There are a couple of things in your approach that could use improvement.
First, this is performing networking on the main queue. That is a no-no, wether the networking is synchronous or not. Creating a new NSOperationQueue for your connections and passing that instead of [NSOperationQueue mainQueue] is a huge improvement.
Second, the error handling is incorrect. In general the correct error handling pattern for Objective-C is to check wether a call resulted in the expected result before using the error. In this case, it's the NSURLResponse that should be checked, not the data. NSURLConnection may be able to connect to the remove service just fine but get no data back - and for many HTTP requests this is expected, correct behavior. If there is a problem connecting, the NSURLResponse will be nil. Check wether the response is nil, if it is then handle the error.
You're also not checking the HTTP response status code or MIME type. The server could respond with a 500, indicating a server error, or could mistakenly send you HTML (which would give the JSON parser fits).
A verbose example that does the above correctly is here. :
[NSURLConnection sendAsynchronousRequest:request queue:[self connectionQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if (response != nil){
if ([[self acceptableStatusCodes] containsIndex:[(NSHTTPURLResponse *)response statusCode] ]){
// The server responded with an HTTP status code that indicates success
if ([[self acceptableMIMETypes] containsObject:[[response MIMEType] lowerCaseString] ]){
// The server responded with a MIME type we can understand.
if ([data length] > 0){
NSError *jsonError = nil;
id jsonObject = nil;
// The server provided data in the response, which means we can attempt to parse it
// Note that we are not specifying NSJSONReadingMutableContainers or NSJSONReadingMutableLeaves, as this would result in
// an object that is not safe to use across threads.
jsonObject = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&jsonError];
if (jsonObject != nil){
// The JSON parser successfully parsed the data, and returned an object. There is nothing to tell us what kind of object was returned.
// We need to make sure it responds to the selectors we will be using - ideally, we'd pass this object to a method that takes an
// id parameter, not NSDictionary, and inside that method it would check wether the id object responds to the specific selectors
// it is going to use on it.
if ([jsonObject respondsToSelector:#selector(dictionaryWithDictionary:)]){
[self doStuffWithDictionary:jsonObject];
}
} else {
// The JSON parser was unable to understand the data we provided, and the error should indicate why.
[self presentError:jsonError];
}
} else {
// The server responded with data that was zero length. How you deal with this is up to your application's needs.
// You may create your own instance of NSError that describes the problem and pass it to your error handling, etc.
}
} else {
// The server response was a MIME type we could not understand. How you handle this is up to you.
}
} else {
// The server response indicates something went wrong: a 401 Not Found, etc.
// It's up to your application to decide what to do about HTTP statuses that indicate failure.
// You may create your own instance of NSError that describes the problem and pass it to your error handling, etc.
}
} else {
// Only inspect the error parameter if the response is nil.
// The error indicates why the URL loading system could not connect to the server.
// It is only valid to use this error if the server could not connect - which is indicated by a nil response
[self presentError:connectionError];
}
}];
// Returns the HTTP status codes we find acceptable.
- (NSIndexSet *) acceptableStatusCodes {
return [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(200, 99)];
}
// Returns the mime types we can accept and understand.
- (NSSet *) acceptableMimeTypes {
NSSet *result = nil;
result = [NSSet setWithObjects:#"application/json", #"application/json; charset=utf-8", nil];
return result;
}
// Generic error handling method.
- (void) presentError:(NSError *)error {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
}];
}
Yup, that's a lot of code, and it should be broken into smaller methods - but it illustrates the logic that should be implemented.
The NSError you are getting now
In your comments you indicate that you are getting an NSError with the domain NSURLErrorDomain and code -1002. If you look at NSURLErrors.h, you will see that NSURL errors map to CFURL errors. If you look at CFNetworkErrors.h, you can see that error code -1002 is kCFURLErrorUnsupportedURL. The URL loading system thinks the URL you are using is not a supported type. This is most likely because the scheme of your URL is incorrect, or how you are attempting to pass credentials as part of the URL is incorrect. Elsewhere in your comments you indicate you are passing credentials as follows:
username:apikey:someurl.com/foo/
Which should be more like:
https://username:apikey#someurl.com/foo/
But only if the service you are accessing is using a supported HTTP authentication type (i.e. Basic authentication). Either way, correctly composing the URL will fix the error you are currently seeing.
I'm downloading mp3 files from Rackspace cloud files, and for large files i'm encountering an issue where the download is completed successfully but the file is not yet downloaded completely. For example, a 40 MB mp3 file (01:00:00 duration) is download as as 4.5 MB mp3 file (00:10:30 duration). This doesn't happen all the time.
Any pointers as to what's going on?
Why is this happening, and how can i fix this issue?
How can i build a simple checksum logic to check if the file was downloaded completely?
Here's how i create and send an async request:
ASIHTTPRequest *request;
request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:urlString]];
[request setShouldAttemptPersistentConnection:NO];
[request setAllowResumeForFileDownloads:YES];
[request setDownloadProgressDelegate:self];
[request setShouldContinueWhenAppEntersBackground:YES];
[request setUserInfo:userInfo];
[request setDownloadDestinationPath:downloadPath];
[request setTemporaryFileDownloadPath:[NSString stringWithFormat:#"%#.download", downloadPath]];
[self.networkQueue addOperation:request];
[self.networkQueue go];
Note i'm using a network queue with 4 concurrent downloads.
Thanks.
Edit (Mon March 5, 2012, 03:25 PM)
So, further investigation shows that ASINetworkQueue is calling requestDidFinishSelector delegate method instead of requestDidFailSelector. The status code returned by the ASIHTTPRequest object is 206, HTTP/1.1 206 Partial Content in requestDidFinishSelector method. The status code should be 200, HTTP/1.1 200 OK.
I still don't know why! and i still don't know how to fix this. It seems that i'll have to delete the partially downloaded file and start the download process again. At this point the temporary file i.e. %#.download is removed, and this partially downloaded file is put at the destination path.
So, this is what i ended up doing, and hopefully this'll be enough (to solve the problem).
Here's how i'm creating the network queue:
- (ASINetworkQueue *)networkQueue {
if (!_networkQueue) {
_networkQueue = [[ASINetworkQueue alloc] init];
[_networkQueue setShowAccurateProgress:YES];
[_networkQueue setRequestDidFinishSelector:#selector(contentRequestDidSucceed:)];
[_networkQueue setRequestDidFailSelector:#selector(contentRequestDidFail:)];
[_networkQueue setShouldCancelAllRequestsOnFailure:NO];
[_networkQueue setDelegate:self];
}
return _networkQueue;
}
And here's what my contentRequestDidSucceed: method does:
- (void)contentRequestDidSucceed:(ASIHTTPRequest *)request {
// ASIHTTPRequest doesn't use HTTP status codes (except for redirection),
// so it's up to us to look out for problems (ex: 404) in the requestDidFinishSelector selector.
// requestDidFailSelector will be called only if there is the server can not be reached
// (time out, no connection, connection interrupted, ...)
// In certain cases ASIHTTPRequest/ASINetworkQueue calls the delegate method requestDidFinishSelector,
// instead it should call requestDidFailSelector. I've encountered this specific case with status code 206 (HTTP/1.1 206 Partial Content). In this case the file was not completely downloaded, so we'll have to re-process the request.
if ([request responseStatusCode] != 200) {
NSLog(#" ");
NSLog(#"======= BEEP =======");
NSLog(#" ");
// We're double checking that the file was indeed not downloaded completely!
// During internal testing, we encountered a case where download was successful
// but we received 206 as the response code (maybe we received the cached value).
unsigned long long progress = [request totalBytesRead] + [request partialDownloadSize];
unsigned long long total = [request contentLength] + [request partialDownloadSize];
if (progress != total) {
NSString *downloadPath = [request downloadDestinationPath];
NSString *temporaryDownloadPath = [self temporaryPathForFile:downloadPath];
// Move the file at destination path to the temporary destination path (back again)
NSError *moveError = nil;
[[[[NSFileManager alloc] init] autorelease] moveItemAtPath:downloadPath
toPath:temporaryDownloadPath
error:&moveError];
if (moveError) {
NSLog(#"Failed to move file from '%#' to '%#'", downloadPath, temporaryDownloadPath);
NSError *removeError = nil;
[ASIHTTPRequest removeFileAtPath:downloadPath error:&removeError];
if (removeError) {
NSLog(#"Failed to remove file from '%#'", downloadPath);
}
}
// Call the requestDidFailSelector method
[self contentRequestDidFail:request];
// Don't continue
return;
}
}
// TODO: Process successful request!
// . . .
}
If there's a better way to handle this, please let me know.