Similar to this issue.
Using AFNetworking 2.0.3 and trying to upload an image using AFHTTPSessionManager's POST + constructingBodyWithBlock. For reasons unknown, it seems as though the HTTP post body is always blank when the request is made to the server.
I subclass AFHTTPSessionManager below (hence the usage of [self POST ...].
I've tried constructing the request two ways.
Method 1: I just tried to pass params and then add only the image data should it exist.
- (void) createNewAccount:(NSString *)nickname accountType:(NSInteger)accountType primaryPhoto:(UIImage *)primaryPhoto
{
NSString *accessToken = self.accessToken;
// Ensure none of the params are nil, otherwise it'll mess up our dictionary
if (!nickname) nickname = #"";
if (!accessToken) accessToken = #"";
NSDictionary *params = #{#"nickname": nickname,
#"type": [[NSNumber alloc] initWithInteger:accountType],
#"access_token": accessToken};
NSLog(#"Creating new account %#", params);
[self POST:#"accounts" parameters:params constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
if (primaryPhoto) {
[formData appendPartWithFileData:UIImageJPEGRepresentation(primaryPhoto, 1.0)
name:#"primary_photo"
fileName:#"image.jpg"
mimeType:#"image/jpeg"];
}
} success:^(NSURLSessionDataTask *task, id responseObject) {
NSLog(#"Created new account successfully");
} failure:^(NSURLSessionDataTask *task, NSError *error) {
NSLog(#"Error: couldn't create new account: %#", error);
}];
}
Method 2: tried to build the form data in the block itself:
- (void) createNewAccount:(NSString *)nickname accountType:(NSInteger)accountType primaryPhoto:(UIImage *)primaryPhoto
{
// Ensure none of the params are nil, otherwise it'll mess up our dictionary
if (!nickname) nickname = #"";
NSLog(#"Creating new account %#", params);
[self POST:#"accounts" parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
[formData appendPartWithFormData:[nickname dataUsingEncoding:NSUTF8StringEncoding] name:#"nickname"];
[formData appendPartWithFormData:[NSData dataWithBytes:&accountType length:sizeof(accountType)] name:#"type"];
if (self.accessToken)
[formData appendPartWithFormData:[self.accessToken dataUsingEncoding:NSUTF8StringEncoding] name:#"access_token"];
if (primaryPhoto) {
[formData appendPartWithFileData:UIImageJPEGRepresentation(primaryPhoto, 1.0)
name:#"primary_photo"
fileName:#"image.jpg"
mimeType:#"image/jpeg"];
}
} success:^(NSURLSessionDataTask *task, id responseObject) {
NSLog(#"Created new account successfully");
} failure:^(NSURLSessionDataTask *task, NSError *error) {
NSLog(#"Error: couldn't create new account: %#", error);
}];
}
Using either method, when the HTTP request hits the server, there is no POST data or query string params, only HTTP headers.
Transfer-Encoding: Chunked
Content-Length:
User-Agent: MyApp/1.0 (iPhone Simulator; iOS 7.0.3; Scale/2.00)
Connection: keep-alive
Host: 127.0.0.1:5000
Accept: */*
Accept-Language: en;q=1, fr;q=0.9, de;q=0.8, zh-Hans;q=0.7, zh-Hant;q=0.6, ja;q=0.5
Content-Type: multipart/form-data; boundary=Boundary+0xAbCdEfGbOuNdArY
Accept-Encoding: gzip, deflate
Any thoughts? Also posted a bug in AFNetworking's github repo.
Rob is absolutely right, the problem you're seeing is related to the (now closed) issue 1398. However, I wanted to provide a quick tl;dr in case anyone else was looking.
First, here's a code snippet provided by gberginc on github that you can model your file uploads after:
NSString* apiUrl = #"http://example.com/upload";
// Prepare a temporary file to store the multipart request prior to sending it to the server due to an alleged
// bug in NSURLSessionTask.
NSString* tmpFilename = [NSString stringWithFormat:#"%f", [NSDate timeIntervalSinceReferenceDate]];
NSURL* tmpFileUrl = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:tmpFilename]];
// Create a multipart form request.
NSMutableURLRequest *multipartRequest = [[AFHTTPRequestSerializer serializer] multipartFormRequestWithMethod:#"POST"
URLString:apiUrl
parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData> formData)
{
[formData appendPartWithFileURL:[NSURL fileURLWithPath:filePath]
name:#"file"
fileName:fileName
mimeType:#"image/jpeg" error:nil];
} error:nil];
// Dump multipart request into the temporary file.
[[AFHTTPRequestSerializer serializer] requestWithMultipartFormRequest:multipartRequest
writingStreamContentsToFile:tmpFileUrl
completionHandler:^(NSError *error) {
// Once the multipart form is serialized into a temporary file, we can initialize
// the actual HTTP request using session manager.
// Create default session manager.
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
// Show progress.
NSProgress *progress = nil;
// Here note that we are submitting the initial multipart request. We are, however,
// forcing the body stream to be read from the temporary file.
NSURLSessionUploadTask *uploadTask = [manager uploadTaskWithRequest:multipartRequest
fromFile:tmpFileUrl
progress:&progress
completionHandler:^(NSURLResponse *response, id responseObject, NSError *error)
{
// Cleanup: remove temporary file.
[[NSFileManager defaultManager] removeItemAtURL:tmpFileUrl error:nil];
// Do something with the result.
if (error) {
NSLog(#"Error: %#", error);
} else {
NSLog(#"Success: %#", responseObject);
}
}];
// Add the observer monitoring the upload progress.
[progress addObserver:self
forKeyPath:#"fractionCompleted"
options:NSKeyValueObservingOptionNew
context:NULL];
// Start the file upload.
[uploadTask resume];
}];
And secondly, to summarize the problem (and why you have to use a temporary file as a work around), it really is two fold.
Apple considers the content-length header to be under its control, and when a HTTP body stream is set for a NSURLRequest Apple's libraries will set the encoding to Chunked and then abandon that header (and thereby clearing any content-length value AFNetworking sets)
The server the upload is hitting doesn't support Transfer-Encoding: Chunked (eg. S3)
But it turns out, if you're uploading a request from a file (because the total request size is known ahead of time), Apple's libraries will properly set the content-length header. Crazy right?
Digging into this further, it appears that when you use NSURLSession in conjunction with setHTTPBodyStream, even if the request sets Content-Length (which AFURLRequestSerialization does in requestByFinalizingMultipartFormData), that header is not getting sent. You can confirm this by comparing the allHTTPHeaderFields of the task's originalRequest and currentRequest. I also confirmed this with Charles.
What's interesting is that Transfer-Encoding is getting set as chunked (which is correct in general when the length is unknown).
Bottom line, this seems to be a manifestation of AFNetworking's choice to use setHTTPBodyStream rather than setHTTPBody (which doesn't suffer from this behavior), which, when combined with NSURLSession results in this behavior of malformed requests.
I think this is related to AFNetworking issue 1398.
I was running into this problem myself, and was trying both methods and the suggested method here...
Turns out, it was as simple as changing the appended data "name" key to "file" instead of the filename variable.
Be sure your data key matches, or you will see the same symptom of an empty data set.
Related
I'm using AFNetworking in my project as network layer.
For images I had some problems related to the caching and I updated the framework to 3.0 version.
During the update process I have some problems with requests which fetch the media content (images and videos).
This is my class :
+ (void)requestWithMethod:(NSString*)methodTypeValue path:(NSString*)pathValue parameters:(NSDictionary *)params token:(NSString*)token debug:(BOOL)debug completionHandler:(void (^)(id result, NSError *error))block {
if(debug){
NSLog(#"Method Type = %# \n",methodTypeValue);
NSLog(#"Link = %# \n",pathValue);
NSLog(#"Token = %#", token);
}
NSString *linkRequest = [NSString stringWithFormat:#"%#%#",GatewayRoot,pathValue];
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
NSMutableURLRequest *req = [[AFJSONRequestSerializer serializer] requestWithMethod:methodTypeValue URLString:linkRequest parameters:nil error:nil];
[req setValue:#"application/json" forHTTPHeaderField:#"Content-Type"];
if (token) {
[req setValue:token forHTTPHeaderField:#"AccessToken"];
}
[[manager dataTaskWithRequest:req completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) {
if (!error) {
if (debug) {
NSLog(#"response:\n %# \n",responseObject);
}
block(responseObject,nil);
} else {
NSLog(#"Error: %#, %#, %#", error, response, responseObject);
block(nil, error);
}
}] resume];
}
I made test and everything is fine related to the normal requests.
I have to fetch some images and videos from server and I receive this error :
NSLocalizedDescription=Request failed: unacceptable content-type: image/png
NSLocalizedDescription=Request failed: unacceptable content-type: video/mp4
I tried to set the content types in this ways :
manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:#"image/jpeg",#"video/mpeg",...nil];
manager.responseSerializer.acceptableContentTypes =[manager.responseSerializer.acceptableContentTypes setByAddingObject:#"image/jpeg",#"video/mpeg",...nil];
Please, can you help my how to set the acceptable content types ?
I have other problem related to image caching: with old version I loged in and I fetch the user profile image and after I loged out and relogin with other credentials I received the same image for user profile (I found that it was an issue realted to AFNetworking caching images).
Can you help me with the clear cache request flow ?
Thank a lot for your help !
Rather than setting the acceptable content types yourself, you probably just want to specify the appropriate responseSerializer. For example, if you want the UIImage, you can do:
manager.responseSerializer = [AFImageResponseSerializer serializer];
If you want the NSData, you can do:
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
If you don't want your request to be cached, you should set the req.cachePolicy to NSURLRequestReloadIgnoringCacheData or implement AFURLSessionManager cache response block, setDataTaskWillCacheResponseBlock. Or, of course, you could change your server so that those resources that shouldn't be cached have their header fields set accordingly.
After trying nearly every response on the subject, I've come up without a working answer to my problem.
The problem: So I've implemented the uploading portion of my app using AFNetworking 2.0.3 after porting from AFNetworking 1.3:
-(void)commandWithParams:(NSMutableDictionary*)params onCompletion:(JSONResponseBlock)completionBlock {
NSData* uploadFile = nil;
if ([params objectForKey:#"file"]) {
uploadFile = (NSData*)[params objectForKey:#"file"];
[params removeObjectForKey:#"file"];
}
AFHTTPRequestOperationManager *manager = [[AFHTTPRequestOperationManager alloc] initWithBaseURL:[NSURL URLWithString:#"http://54.204.17.38"]];
manager.responseSerializer = [AFJSONResponseSerializer serilizer];
manager.responseSerializer.acceptableContentTypes = [NSSet setWithObject:#"application/json"];
AFHTTPRequestOperation *apiRequest = [manager POST:#"/API" parameters:params constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
if (uploadFile) {
[formData appendPartWithFileData:uploadFile name:#"file" fileName:#"photo.jpg" mimeType:#"image/jpeg"];
}
} success:^(AFHTTPRequestOperation *operation, id responseObject) {
completionBlock(responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
completionBlock([NSDictionary dictionaryWithObject:[error localizedDescription] forKey:#"error"]);
}];
[apiRequest start];
}
The error I get when using this code is "Request failed: unacceptable content-type: text/html" I know you might be wondering if the server is responding with proper JSON, and I have every reason to think it is after inspecting the response headers in my browser that say 'MIME type: application/json'. Also, I am using 'header('Content-type: application/json')' at the top of my API as well (PHP API). Now, if I change the serialization type to 'AFHTTPResponseSerializer' instead of 'AFJSONResponseSerializer', it will not spit out the JSON error, but it will give me a different error (a random unrecognized selector error).
Any thoughts on why I cannot seem to get a JSON response out of this method?
You can set the AFHTTPSessionManager to accept any MIME Type:
manager.responseSerializer.acceptableContentTypes = [NSSet setWithObject:#"text/html"];
Got it! So, turns out, unknowingly, although my API was returning valid JSON, matter examining the header response logged on the Xcode side of things (thru NSLog(#"Error: %#", error);), it was actually returning text/HTML because it wasn't actually hitting the correct file, it was getting re-routed by a header somewhere. After explicitly stating the API path to be /API/index.php and not just /API, it started returning the valid JSON! Next, after making sure the response was properly JSON serialized (using requestManager.responseSerializer = [AFJSONResponseSerializer serializer];), the app worked!
Hopefully this helps someone who was having the same issue :)
So I am trying to upload a file (picture) to the a server using their web service called UploadFile which takes 2 variables.
FileInfo info and a int requestId (which in my case will always be 0)
The FileInfo object contains several variables name (String), description(String), content(binary data of the file, in this case it would be the image), id (String), and name (String)
How do I interface with this server to make the request go through? Normally when I have been pulling/posting information I have just been doing JSON calls, but I am guessing uploading is different. I am not sure how to do this when the service requires a custom object to be passed.
Do I need to create the object in my App?
I am trying to use AFNetworking's AFHTTPRequestOperations for this.
I am trying to use their example as a springboard, but I still need to make sure on what to change in their upload example since the example uses direct uploading of an image and I need to upload a FileInfo object instead of a jpeg.
NSURL *url = [NSURL URLWithString:#"https://SomeDomain.com/Services/FileService.svc/UploadFile"];
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:url];
NSData *imageData = UIImageJPEGRepresentation([self.photoImageView image], 0.5);
NSMutableURLRequest *request = [httpClient multipartFormRequestWithMethod:#"POST" path:#"/upload" parameters:nil constructingBodyWithBlock: ^(id <AFMultipartFormData>formData) {
[formData appendPartWithFileData:imageData name:#"avatar" fileName:#"Upload.jpg" mimeType:#"image/jpg"];
}];
NSLog(#"Request %#", [request description]);
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
NSLog(#"Operation: %#", [operation description]);
[operation setUploadProgressBlock:^(NSUInteger bytesWritten, long long totalBytesWritten, long long totalBytesExpectedToWrite) {
NSLog(#"Sent %lld of %lld bytes", totalBytesWritten, totalBytesExpectedToWrite);
}];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"RESPONSE: %#", [responseObject description]);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Failed: %#", [error description]);
}];
[httpClient enqueueHTTPRequestOperation:operation];
Thanks,
Alan
if you see the AFNetworking documentation of the methode multipartFormRequestWithMethod...you have a Dictionnary parameter where you can putt all the infos needed by the Web service.
/** Creates an NSMutableURLRequest object with the specified HTTP
method and path, and constructs a multipart/form-data HTTP body,
using the specified parameters and multipart form data block. See
http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.2
Multipart form requests are automatically streamed, reading files
directly from disk along with in-memory data in a single HTTP body.
The resulting NSMutableURLRequest object has an HTTPBodyStream
property, so refrain from setting HTTPBodyStream or HTTPBody on
this request object, as it will clear out the multipart form body
stream. #param method The HTTP method for the request. This
parameter must not be GET or HEAD, or nil. #param path The path
to be appended to the HTTP client's base URL and used as the request
URL. #param parameters The parameters to be encoded and set in the
request HTTP body. #param block A block that takes a single argument
and appends data to the HTTP body. The block argument is an object
adopting the AFMultipartFormData protocol. This can be used to
upload files, encode HTTP body as JSON or XML, or specify multiple
values for the same parameter, as one might for array values.
#return An NSMutableURLRequest object */
Refer this. They have given different methods:
https://github.com/AFNetworking/AFNetworking
I am getting the NSURLErrorDomain Code=-1021 "request body stream exhausted"
NSLocalizedDescription=request body stream exhausted, NSUnderlyingError=0x2088c080 "request body stream exhausted"}
This error is generated when uploading multiple big size images
I am using AFNetworking and tried to search for a fix online, but didn't succeed
NSDictionary *clientUniqueId = [NSDictionary dictionaryWithObject:NSLocalizedString(uniqueDrId, nil) forKey:#"clientUniqueId"];
NSMutableURLRequest *request = [client multipartFormRequestWithMethod:#"POST"
path:pendingUpload.urlPath
parameters:clientUniqueId
constructingBodyWithBlock:^(id<AFMultipartFormData> formData)
{
[formData appendPartWithFormData:[pendingUpload dataRecordData] name:#"dr"];
NSArray *attachments = pendingUpload.attachments;
if (attachments != nil) {
for (Attachment *attachment in attachments) {
[formData appendPartWithFileData:attachment.data
name:attachment.key
fileName:attachment.filename
mimeType:attachment.contentType];
}
}
}];
As described in the AFNetworking FAQ:
Why are some upload requests failing with the error "request body stream exhausted"? What does that mean, and how do I fix this?
When uploading over a 3G or EDGE connection, requests may fail with "request body stream exhausted". Using -throttleBandwidthWithPacketSize:delay: your multipart form construction block, you can set a maximum packet size and delay according to the recommended values (kAFUploadStream3GSuggestedPacketSize and kAFUploadStream3GSuggestedDelay). This lowers the risk of the input stream exceeding its allocated bandwidth. Unfortunately, as of iOS 6, there is no definite way to distinguish between a 3G, EDGE, or LTE connection. As such, it is not recommended that you throttle bandwidth based solely on network reachability. Instead, you should consider checking for the "request body stream exhausted" in a failure block, and then retrying the request with throttled bandwidth.
I was experiencing this issue also and didn't have any luck with the throttleBandwithWithPacketSize method. I believe in my case it was an authentication challenge issue.
What I finally did was switch to the URLSession connection method in AFNetworking 2.0 and that seemed to solve it for me. Here is the code I ended up using:
NSString *uploadAttachmentURL = #"https://mydomain.zendesk.com/api/v2/uploads.json?filename=screenshot.jpeg";
NSData *imageData = UIImageJPEGRepresentation(image, 1.0);
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
_afHTTPSessionManager = [[AFHTTPSessionManager alloc] initWithSessionConfiguration:configuration];
// hack to allow 'text/plain' content-type to work
NSMutableSet *contentTypes = [NSMutableSet setWithSet:_AFOpManager.responseSerializer.acceptableContentTypes];
[contentTypes addObject:#"text/plain"];
_afHTTPSessionManager.responseSerializer.acceptableContentTypes = contentTypes;
[_afHTTPSessionManager.requestSerializer setAuthorizationHeaderFieldWithUsername:#"[USERNAME]" password:#"[PASSWORD]"];
[_afHTTPSessionManager POST:uploadAttachmentURL parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
[formData appendPartWithFileData:imageData name:#"screenshot" fileName:#"photo.jpg" mimeType:#"image/jpeg"];
} success:^(NSURLSessionDataTask *task, id responseObject) {
DDLogError(#"screenshot operation success! %#", responseObject);
} failure:^(NSURLSessionDataTask *task, NSError *error) {
DDLogError(#"Operation Error: %#", error);
}];
I am trying to post a sound to echo nest for file analysis. The POST method is no different than any other method, and I believe it's not echonest dependent.
Here is the documentation. I need the "upload" part.
http://developer.echonest.com/docs/v4/track.html
NSURLRequest *request = [self multipartFormRequestWithMethod:#"POST" path:[NSString stringWithFormat:#"track/upload", self.apiKey] parameters:dictionary constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
[formData appendPartWithFileData:data name:#"track" fileName:[path lastPathComponent] mimeType:#"multipart/form-data"];
}];
AFURLConnectionOperation *operation = [self HTTPRequestOperationWithRequest:request success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"done");
NSLog(#"response: %#", operation.responseString);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"error: %#", error.description);
NSLog(#"response: %#", operation.responseString);
NSLog(#"headers: %#", operation.request.allHTTPHeaderFields.description);
NSLog(#"operation url: %#", operation.request.URL.absoluteString);
}];
However, I have problems. Here is my response:
{"response": {"status": {"version": "4.2", "code": 4, "message": "track - Missing Parameter: track or url is required with a POST content-type of \"application/x-www-form-urlencoded\" or \"multipart/form-data\""}}}
I thought I have already given the "track" parameter to be the data I initialized. Can anyone help me?
I experienced same problem few weeks ago. And here is what i found.
AFNetworking makes slightly wrong multipart/form-data request.
In AFHTTPClient.m file,
static inline NSString * AFMultipartFormFinalBoundary() {
return [NSString stringWithFormat:#"%#--%#--%#%#", kAFMultipartFormCRLF, kAFMultipartFormBoundary, kAFMultipartFormCRLF, kAFMultipartFormCRLF];
}
They put kAFMultipartFormCRLF twice at the end of final boundary of a request.
But Echonest only except request with one CRLF at the end of multipart/form-data post request.
I don't know which one is exactly the righteous way in terms of HTTP protocol standard, but if you modify that code line (remove a kAFMultipartFormCRLF), i think your code would work well.
FYI, while writing this answer, I explored current AFNetworking Git repo, and found out that this defect has been fixed just 5 days ago. So i guess you can just use latest source code to fix the problem.