Ben Copsey

Important changes to persistent connections behaviour:

1) Fix a problem where a temporary request body would be discarded when retrying on a new connection
2) By default, all requests using POST, PUT or having a body at the time requestMethod is set will NOT use a persistent connection.
You can override this by setting requestMethod manually, then setting shouldAttemptPersistentConnection to YES

closes gh-94
... ... @@ -2,7 +2,7 @@
// ASIHTTPRequest.h
//
// Created by Ben Copsey on 04/10/2007.
// Copyright 2007-2010 All-Seeing Interactive. All rights reserved.
// Copyright 2007-2011 All-Seeing Interactive. All rights reserved.
//
// A guide to the main features is available at:
// http://allseeing-i.com/ASIHTTPRequest
... ... @@ -92,7 +92,7 @@ typedef void (^ASIDataBlock)(NSData *data);
// Temporarily stores the url we are about to redirect to. Will be nil again when we do redirect
NSURL *redirectURL;
// The delegate, you need to manage setting and talking to your delegate in your subclasses
// The delegate - will be notified of various changes in state via the ASIHTTPRequestDelegate protocol
id <ASIHTTPRequestDelegate> delegate;
// Another delegate that is also notified of request status changes and progress updates
... ... @@ -100,7 +100,7 @@ typedef void (^ASIDataBlock)(NSData *data);
// NOTE: WILL BE RETAINED BY THE REQUEST
id <ASIHTTPRequestDelegate, ASIProgressDelegate> queue;
// HTTP method to use (GET / POST / PUT / DELETE / HEAD). Defaults to GET
// HTTP method to use (eg: GET / POST / PUT / DELETE / HEAD etc). Defaults to GET
NSString *requestMethod;
// Request body - only used when the whole body is stored in memory (shouldStreamPostDataFromDisk is false)
... ... @@ -403,7 +403,10 @@ typedef void (^ASIDataBlock)(NSData *data);
// The number of times this request has retried (when numberOfTimesToRetryOnTimeout > 0)
int retryCount;
// Temporarily set to YES when a closed connection forces a retry (internally, this stops ASIHTTPRequest cleaning up a temporary post body)
BOOL willRetryRequest;
// When YES, requests will keep the connection to the server alive for a while to allow subsequent requests to re-use it for a substantial speed-boost
// Persistent connections will not be used if the server explicitly closes the connection
// Default is YES
... ... @@ -443,7 +446,6 @@ typedef void (^ASIDataBlock)(NSData *data);
// This timer checks up on the request every 0.25 seconds, and updates progress
NSTimer *statusTimer;
// The download cache that will be used for this request (use [ASIHTTPRequest setDefaultCache:cache] to configure a default cache
id <ASICacheDelegate> downloadCache;
... ... @@ -464,7 +466,6 @@ typedef void (^ASIDataBlock)(NSData *data);
BOOL shouldContinueWhenAppEntersBackground;
UIBackgroundTaskIdentifier backgroundTask;
#endif
// When downloading a gzipped response, the request will use this helper object to inflate the response
ASIDataDecompressor *dataDecompressor;
... ... @@ -938,7 +939,7 @@ typedef void (^ASIDataBlock)(NSData *data);
@property (retain,readonly) NSString *responseStatusMessage;
@property (retain) NSMutableData *rawResponseData;
@property (assign) NSTimeInterval timeOutSeconds;
@property (retain) NSString *requestMethod;
@property (retain, nonatomic) NSString *requestMethod;
@property (retain) NSMutableData *postBody;
@property (assign) unsigned long long contentLength;
@property (assign) unsigned long long postLength;
... ...
... ... @@ -2,7 +2,7 @@
// ASIHTTPRequest.m
//
// Created by Ben Copsey on 04/10/2007.
// Copyright 2007-2010 All-Seeing Interactive. All rights reserved.
// Copyright 2007-2011 All-Seeing Interactive. All rights reserved.
//
// A guide to the main features is available at:
// http://allseeing-i.com/ASIHTTPRequest
... ... @@ -24,7 +24,7 @@
#import "ASIDataCompressor.h"
// Automatically set on build
NSString *ASIHTTPRequestVersion = @"v1.8-56 2011-02-06";
NSString *ASIHTTPRequestVersion = @"v1.8-76 2011-05-08";
static NSString *defaultUserAgent = nil;
... ... @@ -225,6 +225,7 @@ static NSOperationQueue *sharedQueue = nil;
@property (retain) NSString *responseStatusMessage;
@property (assign) BOOL inProgress;
@property (assign) int retryCount;
@property (assign) BOOL willRetryRequest;
@property (assign) BOOL connectionCanBeReused;
@property (retain, nonatomic) NSMutableDictionary *connectionInfo;
@property (retain, nonatomic) NSInputStream *readStream;
... ... @@ -598,6 +599,27 @@ static NSOperationQueue *sharedQueue = nil;
[stream close];
}
- (NSString *)requestMethod
{
[[self cancelledLock] lock];
NSString *m = requestMethod;
[[self cancelledLock] unlock];
return m;
}
- (void)setRequestMethod:(NSString *)newRequestMethod
{
[[self cancelledLock] lock];
if (requestMethod != newRequestMethod) {
[requestMethod release];
requestMethod = [newRequestMethod retain];
if ([requestMethod isEqualToString:@"POST"] || [requestMethod isEqualToString:@"PUT"] || [postBody length] || postBodyFilePath) {
[self setShouldAttemptPersistentConnection:NO];
}
}
[[self cancelledLock] unlock];
}
- (NSURL *)url
{
[[self cancelledLock] lock];
... ... @@ -1296,6 +1318,10 @@ static NSOperationQueue *sharedQueue = nil;
CFReadStreamSetProperty((CFReadStreamRef)[self readStream], CFSTR("ASIStreamID"), [[self connectionInfo] objectForKey:@"id"]);
} else {
#if DEBUG_PERSISTENT_CONNECTIONS
NSLog(@"Request %@ will not use a persistent connection",self);
#endif
}
[connectionsLock unlock];
... ... @@ -1524,7 +1550,7 @@ static NSOperationQueue *sharedQueue = nil;
}
// Clean up any temporary file used to store request body for streaming
if (![self authenticationNeeded] && [self didCreateTemporaryPostDataFile]) {
if (![self authenticationNeeded] && ![self willRetryRequest] && [self didCreateTemporaryPostDataFile]) {
[self removeTemporaryUploadFile];
[self removeTemporaryCompressedUploadFile];
[self setDidCreateTemporaryPostDataFile:NO];
... ... @@ -3257,7 +3283,9 @@ static NSOperationQueue *sharedQueue = nil;
[self unscheduleReadStream];
}
#if DEBUG_PERSISTENT_CONNECTIONS
NSLog(@"Request #%@ finished using connection #%@",[self requestID], [[self connectionInfo] objectForKey:@"id"]);
if ([self requestID]) {
NSLog(@"Request #%@ finished using connection #%@",[self requestID], [[self connectionInfo] objectForKey:@"id"]);
}
#endif
[[self connectionInfo] removeObjectForKey:@"request"];
[[self connectionInfo] setObject:[NSDate dateWithTimeIntervalSinceNow:[self persistentConnectionTimeoutSeconds]] forKey:@"expires"];
... ... @@ -3382,6 +3410,11 @@ static NSOperationQueue *sharedQueue = nil;
- (BOOL)retryUsingNewConnection
{
if ([self retryCount] == 0) {
[self setWillRetryRequest:YES];
[self cancelLoad];
[self setWillRetryRequest:NO];
#if DEBUG_PERSISTENT_CONNECTIONS
NSLog(@"Request attempted to use connection #%@, but it has been closed - will retry with a new connection", [[self connectionInfo] objectForKey:@"id"]);
#endif
... ... @@ -3405,8 +3438,6 @@ static NSOperationQueue *sharedQueue = nil;
{
NSError *underlyingError = NSMakeCollectable([(NSError *)CFReadStreamCopyError((CFReadStreamRef)[self readStream]) autorelease]);
[self cancelLoad];
if (![self error]) { // We may already have handled this error
// First, check for a 'socket not connected', 'broken pipe' or 'connection lost' error
... ... @@ -3431,8 +3462,10 @@ static NSOperationQueue *sharedQueue = nil;
reason = [NSString stringWithFormat:@"%@: SSL problem (Possible causes may include a bad/expired/self-signed certificate, clock set to wrong date)",reason];
}
}
[self cancelLoad];
[self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:ASIConnectionFailureErrorType userInfo:[NSDictionary dictionaryWithObjectsAndKeys:reason,NSLocalizedDescriptionKey,underlyingError,NSUnderlyingErrorKey,nil]]];
} else {
[self cancelLoad];
}
[self checkRequestStatus];
}
... ... @@ -4767,6 +4800,7 @@ static NSOperationQueue *sharedQueue = nil;
@synthesize inProgress;
@synthesize numberOfTimesToRetryOnTimeout;
@synthesize retryCount;
@synthesize willRetryRequest;
@synthesize shouldAttemptPersistentConnection;
@synthesize persistentConnectionTimeoutSeconds;
@synthesize connectionCanBeReused;
... ...
... ... @@ -57,7 +57,7 @@
#endif
- (void)testAutomaticRetry;
- (void)testCloseConnection;
- (void)testPersistentConnectionTimeout;
- (void)testPersistentConnections;
- (void)testNilPortCredentialsMatching;
@property (retain, nonatomic) NSMutableData *responseData;
... ...
... ... @@ -1690,29 +1690,62 @@
}
- (void)testPersistentConnectionTimeout
- (void)testPersistentConnections
{
// allseeing-i.com is configured to keep persistent connections alive for 2 seconds
// Ensure we parse a keep-alive header
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:@"http://allseeing-i.com"]];
BOOL success = ([request persistentConnectionTimeoutSeconds] == 60);
GHAssertTrue(success,@"Request failed to default to 60 seconds for connection timeout");
[request startSynchronous];
NSNumber *connectionId = [request connectionID];
success = ([request persistentConnectionTimeoutSeconds] == 2);
GHAssertTrue(success,@"Request failed to use time out set by server");
// Wait 3 seconds - connection should have timed out
sleep(3);
request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:@"http://allseeing-i.com"]];
[request startSynchronous];
success = ([[request connectionID] intValue] != [connectionId intValue]);
GHAssertTrue(success,@"Reused a connection that should have timed out");
// Ensure persistent connections are turned off by default with POST/PUT and/or a request body
request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:@"http://allseeing-i.com"]];
[request appendPostData:[@"Foo" dataUsingEncoding:NSUTF8StringEncoding]];
[request startSynchronous];
success = ![request shouldAttemptPersistentConnection];
GHAssertTrue(success,@"Used a persistent connection with a body");
request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:@"http://allseeing-i.com"]];
[request setRequestMethod:@"PUT"];
[request startSynchronous];
success = ![request shouldAttemptPersistentConnection];
GHAssertTrue(success,@"Used a persistent connection with PUT");
request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:@"http://allseeing-i.com"]];
[request setRequestMethod:@"POST"];
[request startSynchronous];
success = ![request shouldAttemptPersistentConnection];
GHAssertTrue(success,@"Used a persistent connection with POST");
// Ensure we can force a persistent connection
request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:@"http://allseeing-i.com"]];
[request setRequestMethod:@"POST"];
[request setShouldAttemptPersistentConnection:YES];
[request startSynchronous];
success = [request shouldAttemptPersistentConnection];
GHAssertTrue(success,@"Failed to use a persistent connection");
}
- (void)testRemoveUploadProgress
... ...