Permalink
Browse files

Merge branch 'bmorton-timeouts'

  • Loading branch information...
2 parents e3264a7 + 035e773 commit 55c72a5f3553261adeedecda43eaaa129f2b8388 @blakewatters blakewatters committed Jan 20, 2012
View
@@ -199,6 +199,13 @@ NSString *RKPathAppendQueryParams(NSString *resourcePath, NSDictionary *queryPar
*/
@property (nonatomic, readonly) NSMutableDictionary *HTTPHeaders;
+/**
+ * An optional timeout interval within which the request should be cancelled.
+ * This is passed along to RKRequest if set. If it isn't set, it will default
+ * to RKRequest's default timeoutInterval.
+ */
+@property (nonatomic, assign) NSTimeInterval timeoutInterval;
+
/**
Accept all SSL certificates. This is a potential security exposure,
and should be used ONLY while debugging in a controlled environment.
View
@@ -90,6 +90,7 @@ @implementation RKClient
@synthesize requestCache = _requestCache;
@synthesize cachePolicy = _cachePolicy;
@synthesize requestQueue = _requestQueue;
+@synthesize timeoutInterval = _timeoutInterval;
+ (RKClient *)sharedClient {
return sharedClient;
@@ -222,6 +223,12 @@ - (void)setupRequest:(RKRequest *)request {
request.queue = self.requestQueue;
request.reachabilityObserver = self.reachabilityObserver;
+ // If a timeoutInterval was set on the client, we'll pass it on to the request.
+ // Otherwise, we'll let the request default to its own timeout interval.
+ if (self.timeoutInterval) {
+ request.timeoutInterval = self.timeoutInterval;
+ }
+
// OAuth 1 Parameters
request.OAuth1AccessToken = self.OAuth1AccessToken;
request.OAuth1AccessTokenSecret = self.OAuth1AccessTokenSecret;
View
@@ -121,6 +121,7 @@ typedef enum {
NSTimeInterval _cacheTimeoutInterval;
RKRequestQueue *_queue;
RKReachabilityObserver *_reachabilityObserver;
+ NSTimer *_timeoutTimer;
#if TARGET_OS_IPHONE
RKRequestBackgroundPolicy _backgroundPolicy;
@@ -183,6 +184,14 @@ typedef enum {
*/
@property (nonatomic, assign) RKRequestQueue *queue;
+/**
+ * The timeout interval within which the request should be cancelled
+ * if no data has been received
+ *
+ * @default 120.0
+ */
+@property (nonatomic, assign) NSTimeInterval timeoutInterval;
+
/**
* The policy to take on transition to the background (iOS 4.x and higher only)
*
@@ -380,6 +389,18 @@ typedef enum {
*/
- (void)cancel;
+/**
+ * Cancels request due to connection timeout exceeded.
+ * This will return an RKRequestConnectionTimeoutError via didFailLoadWithError:
+ */
+- (void)timeout;
+
+/**
+ * Invalidates the timeout timer.
+ * Called by RKResponse when the NSURLConnection begins receiving data.
+ */
+- (void)invalidateTimeoutTimer;
+
/**
* Returns YES when this is a GET request
*/
View
@@ -61,6 +61,7 @@ @implementation RKRequest
@synthesize OAuth2AccessToken = _OAuth2AccessToken;
@synthesize OAuth2RefreshToken = _OAuth2RefreshToken;
@synthesize queue = _queue;
+@synthesize timeoutInterval = _timeoutInterval;
@synthesize reachabilityObserver = _reachabilityObserver;
#if TARGET_OS_IPHONE
@@ -79,6 +80,7 @@ - (id)initWithURL:(NSURL*)URL {
_authenticationType = RKRequestAuthenticationTypeNone;
_cachePolicy = RKRequestCachePolicyDefault;
_cacheTimeoutInterval = 0;
+ _timeoutInterval = 120.0;
}
return self;
}
@@ -171,6 +173,9 @@ - (void)dealloc {
_OAuth2AccessToken = nil;
[_OAuth2RefreshToken release];
_OAuth2RefreshToken = nil;
+ [self invalidateTimeoutTimer];
+ [_timeoutTimer release];
+ _timeoutTimer = nil;
// Cleanup a background task if there is any
[self cleanupBackgroundTask];
@@ -320,6 +325,7 @@ - (void)cancelAndInformDelegate:(BOOL)informDelegate {
[_connection cancel];
[_connection release];
_connection = nil;
+ [self invalidateTimeoutTimer];
_isLoading = NO;
if (informDelegate && [_delegate respondsToSelector:@selector(requestDidCancelLoad:)]) {
@@ -377,6 +383,7 @@ - (void)fireAsynchronousRequest {
RKResponse* response = [[[RKResponse alloc] initWithRequest:self] autorelease];
_connection = [[NSURLConnection connectionWithRequest:_URLRequest delegate:response] retain];
+ _timeoutTimer = [NSTimer scheduledTimerWithTimeInterval:self.timeoutInterval target:self selector:@selector(timeout) userInfo:nil repeats:NO];
[[NSNotificationCenter defaultCenter] postNotificationName:RKRequestSentNotification object:self userInfo:nil];
}
@@ -416,6 +423,7 @@ - (void)sendAsynchronously {
_isLoading = YES;
[self didFinishLoad:response];
} else if ([self shouldDispatchRequest]) {
+ _timeoutTimer = [NSTimer scheduledTimerWithTimeInterval:self.timeoutInterval target:self selector:@selector(timeout) userInfo:nil repeats:NO];
#if TARGET_OS_IPHONE
// Background Request Policy support
UIApplication* app = [UIApplication sharedApplication];
@@ -486,6 +494,8 @@ - (RKResponse*)sendSynchronously {
[self didFinishLoad:response];
} else if ([self shouldDispatchRequest]) {
RKLogDebug(@"Sending synchronous %@ request to URL %@.", [self HTTPMethod], [[self URL] absoluteString]);
+ _timeoutTimer = [NSTimer scheduledTimerWithTimeInterval:self.timeoutInterval target:self selector:@selector(timeout) userInfo:nil repeats:NO];
+
if (![self prepareURLRequest]) {
// TODO: Logging
return nil;
@@ -533,6 +543,22 @@ - (void)cancel {
[self cancelAndInformDelegate:YES];
}
+- (void)timeout {
+ [self cancelAndInformDelegate:NO];
+ RKLogError(@"Failed to send request to %@ due to connection timeout. Timeout interval = %f", [[self URL] absoluteString], self.timeoutInterval);
+ NSString* errorMessage = [NSString stringWithFormat:@"The client timed out connecting to the resource at %@", [[self URL] absoluteString]];
+ NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
+ errorMessage, NSLocalizedDescriptionKey,
+ nil];
+ NSError* error = [NSError errorWithDomain:RKRestKitErrorDomain code:RKRequestConnectionTimeoutError userInfo:userInfo];
+ [self didFailLoadWithError:error];
+}
+
+- (void)invalidateTimeoutTimer {
+ [_timeoutTimer invalidate];
+ _timeoutTimer = nil;
+}
+
- (void)didFailLoadWithError:(NSError*)error {
if (_cachePolicy & RKRequestCachePolicyLoadOnError &&
[self.cache hasResponseForRequest:self]) {
@@ -180,6 +180,7 @@ - (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectio
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[_body appendData:data];
+ [_request invalidateTimeoutTimer];
if ([[_request delegate] respondsToSelector:@selector(request:didReceivedData:totalBytesReceived:totalBytesExectedToReceive:)]) {
[[_request delegate] request:_request didReceivedData:[data length] totalBytesReceived:[_body length] totalBytesExectedToReceive:_httpURLResponse.expectedContentLength];
}
@@ -189,6 +190,7 @@ - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSHTTPURLRe
RKLogDebug(@"NSHTTPURLResponse Status Code: %ld", (long) [response statusCode]);
RKLogDebug(@"Headers: %@", [response allHeaderFields]);
_httpURLResponse = [response retain];
+ [_request invalidateTimeoutTimer];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
@@ -199,6 +201,7 @@ - (void)connectionDidFinishLoading:(NSURLConnection *)connection {
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
_failureError = [error retain];
[_request didFailLoadWithError:_failureError];
+ [_request invalidateTimeoutTimer];
}
- (NSInputStream *)connection:(NSURLConnection *)connection needNewBodyStream:(NSURLRequest *)request {
View
@@ -27,5 +27,6 @@ typedef enum {
RKObjectLoaderRemoteSystemError = 1,
RKRequestBaseURLOfflineError = 2,
RKRequestUnexpectedResponseError = 3,
- RKObjectLoaderUnexpectedResponseError = 4
+ RKObjectLoaderUnexpectedResponseError = 4,
+ RKRequestConnectionTimeoutError = 5
} RKRestKitError;
@@ -106,6 +106,13 @@ - (void)testShouldNotSuspendTheMainQueueOnBaseURLChangeWhenReachabilityHasBeenEs
assertThatBool(client.requestQueue.suspended, is(equalToBool(NO)));
}
+- (void)testShouldAllowYouToChangeTheTimeoutInterval {
+ RKClient* client = [RKClient clientWithBaseURL:@"http://restkit.org"];
+ client.timeoutInterval = 20.0;
+ RKRequest* request = [client requestWithResourcePath:@"" delegate:nil];
+ assertThatFloat(request.timeoutInterval, is(equalToFloat(20.0)));
+}
+
- (void)testShouldPerformAPUTWithParams {
NSLog(@"PENDING ---> FIX ME!!!");
return;
@@ -86,6 +86,21 @@ - (void)testShouldSetURLRequestHTTPBodyByString {
assertThat(request.HTTPBodyString, equalTo(JSON));
}
+- (void)testShouldTimeoutAtInterval {
+ RKSpecResponseLoader* loader = [RKSpecResponseLoader responseLoader];
+ id loaderMock = [OCMockObject partialMockForObject:loader];
+ NSString* url = [NSString stringWithFormat:@"%@/timeout", RKSpecGetBaseURL()];
+ NSURL* URL = [NSURL URLWithString:url];
+ RKRequest* request = [[RKRequest alloc] initWithURL:URL];
+ request.delegate = loaderMock;
+ request.timeoutInterval = 3.0;
+ [[[loaderMock expect] andForwardToRealObject] request:request didFailLoadWithError:OCMOCK_ANY];
+ [request sendAsynchronously];
+ [loaderMock waitForResponse];
+ assertThatInt((int)loader.failureError.code, equalToInt(RKRequestConnectionTimeoutError));
+ [request release];
+}
+
#pragma mark - Background Policies
#if TARGET_OS_IPHONE
View
@@ -123,6 +123,15 @@ class RestKit::SpecServer < Sinatra::Base
content_type 'application/json'
params.to_json
end
+
+ get '/timeout' do
+ # We need to leave this around 4 seconds so we don't hold up the
+ # process too long and cause the tests launched after to fail.
+ sleep 4
+ status 200
+ content_type 'application/json'
+ params.to_json
+ end
get '/empty/array' do
status 200

0 comments on commit 55c72a5

Please sign in to comment.