I'm working on an iPhone application that involves uploading full photos from the camera (generally between 1.5 to 2.0 MB each) as well as their thumbnails (much smaller) to Amazon S3.
The thumbnails always successfully upload, but sometimes the full images don't, and when they fail, they fail with POSIX error code 12, aka ENOMEM. However, I've added debug code to print the amount of free memory when the error happens, and there's always quite a bit free, usually more than 100 MB.
Furthermore, the error crops up more often when the upload is happening over 3G and less when it's over wifi -- which seems strange, since the request isn't downloading much and the file being uploaded is already in memory (I've also tried streaming it from disk with no improvement).
I've tried uploading the file using NSURLConnection, the Foundation CFHTTP* functions, and the ASIHTTPRequest library, but regardless, the error happens with the same frequency. Even stranger, all my Googling has revealed is that end users sometimes get error code 12 from Safari -- I haven't seen any iOS developers mentioning it. I'm working with an inherited code base, so it's possible there's something wrong with it, but I'm not even sure what to look for. Any insight would be greatly appreciated!
The only way I was able to work around this issue, is using sockets directly and forming HTTP header manually. So my uploading code currently looks like this:
- (void)socketClose
{
[_inputStream setDelegate:nil];
[_inputStream close];
[_inputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
SCR_RELEASE_SAFELY(_inputStream);
[_outputStream setDelegate:nil];
[_outputStream close];
[_outputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
SCR_RELEASE_SAFELY(_outputStream);
SCR_RELEASE_SAFELY(_headerBuffer);
}
- (void)sendRequest
{
[self socketClose];
SCR_RELEASE_SAFELY(_headerBuffer);
if (!_shouldCancel)
{
NSString *httpMessage = [NSString stringWithFormat:@"POST upload.php HTTP/1.1\r\n"
"Host:"
#ifndef TESTBED
" %@"
#endif
"\r\n"
"User-Agent: MyApp/3.0.0 CFNetwork/534 Darwin/10.7.0\r\n"
"Content-Length: %d\r\n"
"Accept: */*\r\n"
"Accept-Language: en-us\r\n"
"Accept-Encoding: gzip, deflate\r\n"
"Content-Type: application/x-www-form-urlencoded\r\n"
"Connection: keep-alive\r\n\r\n"
"data="
#ifndef TESTBED
, [self.serverUrl host]
#endif
, _bytesToUpload];
NSString *key = @"data=";
NSData *keyData = [key dataUsingEncoding:NSASCIIStringEncoding];
_bytesToUpload -= [keyData length];
_bytesToUpload = MAX(0, _bytesToUpload);
_headerBuffer = [[NSMutableData alloc] initWithData:[httpMessage dataUsingEncoding:NSUTF8StringEncoding]];
_writtenDataBytes = 0;
CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault
, (CFStringRef)[self.serverUrl host]
#ifdef TESTBED
, 8888
#else
, 80
#endif
, (CFReadStreamRef *)(&_inputStream)
, (CFWriteStreamRef *)(&_outputStream));
[_inputStream setDelegate:self];
[_outputStream setDelegate:self];
[_inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[_outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[_inputStream open];
[_outputStream open];
}
}
- (void)stream:(NSStream *)theStream handleEvent:(NSStreamEvent)streamEvent
{
if (_outputStream == theStream)
{
switch (streamEvent)
{
case NSStreamEventOpenCompleted:
{
[self regenerateTimeoutTimer];
break;
}
case NSStreamEventHasSpaceAvailable:
{
SCR_RELEASE_TIMER(_timeoutTimer);
NSInteger length = _headerBuffer.length;
if (length > 0)
{
NSInteger written = [_outputStream write:(const uint8_t *)[_headerBuffer bytes] maxLength:length];
NSInteger rest = length - written;
if (rest > 0)
{
memmove([_headerBuffer mutableBytes], (const uint8_t *)[_headerBuffer mutableBytes] + written, rest);
}
[_headerBuffer setLength:rest];
}
else
{
const uint8_t *dataBytes = [_data bytes];
while ([_outputStream hasSpaceAvailable] && (_writtenDataBytes < _bytesToUpload))
{
NSInteger written = [_outputStream write:dataBytes
maxLength:MIN(_dataLength, _bytesToUpload - _writtenDataBytes)];
if (written > 0)
{
_writtenDataBytes += written;
}
}
}
[self regenerateTimeoutTimer];
break;
}
case NSStreamEventErrorOccurred:
{
SCR_RELEASE_TIMER(_timeoutTimer);
[self reportError:[theStream streamError]];
break;
}
case NSStreamEventEndEncountered:
{
SCR_RELEASE_TIMER(_timeoutTimer);
[self socketClose];
break;
}
}
}
else if (_inputStream == theStream)
{
switch (streamEvent)
{
case NSStreamEventHasBytesAvailable:
{
SCR_RELEASE_TIMER(_timeoutTimer);
/* Read server response here if you wish */
[self socketClose];
break;
}
case NSStreamEventErrorOccurred:
{
SCR_RELEASE_TIMER(_timeoutTimer);
[self reportError:[theStream streamError]];
break;
}
case NSStreamEventEndEncountered:
{
SCR_RELEASE_TIMER(_timeoutTimer);
[self socketClose];
break;
}
}
}
}
Although ASIHTTPRequest could work here, we decided to walk away from such dependencies both in order to get performance and to keep everything under our own control accurately. You can use Wireshark tool in order to debug this kind of things.
The key to getting around this issue is to upload the file using a stream. When using NSMutableURLRequest, this can be accomplished using something similar to the following:
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPBodyStream:[NSInputStream inputStreamWithFileAtPath:filePath]];
When using ASIHTTPRequest, streaming a file is accomplished with this:
ASIHTTPRequest* request = [ASIHTTPRequest requestWithURL:url];
[request setPostBodyFilePath:filePath];
rRequest.shouldStreamPostDataFromDisk = YES;
Have resolved this error with using operation for request (NSMutableUrlConnection
) with @autorelease{}
for main function.
NSPOXIS appears only sometimes.
- (void)main
NSURLConnection* connection;
@autoreleasepool //urgently needed for 3G upload
{
self.currentRequest = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"test.php"]];
[self.currentRequest setHTTPMethod:@"PUT"];
[self.currentRequest setHTTPBody:self.data];//inpustStream doesn't work
connection = [NSURLConnection connectionWithRequest:self.currentRequest delegate:self];
[connection start];
}//end autorelease pool
do
{
[[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate: [NSDate distantFuture]];
if ([self isCancelled])
{
connection = nil;
isFailed = YES;
break;
}
self.status(statusUpdateMessage);
}
while (!isFailed && !isCompleted);
[timer invalidate];//test
timer = nil;
//corresponding of status via blocks
self.completed(!isFailed);
self.status(isFailed ? errorMessage : @"Completed");
if (isFailed)
{
self.failed(errorMessage != nil ? errorMessage : @"Undefined error");
}
self.data = nil;
self.currentRequest = nil;
connection = nil;
}