Upload video problem: error 131

ios
oauth
video
objective-c
media-upload

#1

I’m having some issue with second request(command:APPEND) of uploading video, which is made on POST /1.1/media/upload.json.
I read documentation about how to build Authentication header for video upload, and I did everything like explained here. With example data I see that Authentication header generates correctly, I’m getting exact same result. But when I’m changing consumer_key, authentication_token and other parameters to my application’s values - getting error Twitter API error : Internal error (code 131).

Here is step by step how I’m doing this after receiving positive response for command:INIT:

  1. Generating nonce from NSUUID, base64 encoded
    NSString *nonce = [[[NSUUID UUID].UUIDString dataUsingEncoding:NSUTF8StringEncoding] base64EncodedStringWithOptions:0];
  2. getting timestamp
    [NSString stringWithFormat:@"%lu", (unsigned long)[[NSDate date] timeIntervalSince1970]];
  3. putting all together:
NSString *baseURL = @"https://upload.twitter.com/1.1/media/upload.json";

parameters[@"oauth_consumer_key"] = @"<consumer key here>";
parameters[@"oauth_nonce"] = nonce;
parameters[@"oauth_signature_method"] = @"HMAC-SHA1";
parameters[@"oauth_timestamp"] = [NSString stringWithFormat:@"%lu", (unsigned long)[[NSDate date] timeIntervalSince1970]];
parameters[@"oauth_token"] = [Twitter sharedInstance].session.authToken;
parameters[@"oauth_version"] = @"1.0";

Based on the documentation I should not include any other than auth_* parameters when generating Authentication header.
4. Now we sorting parameters, percent encode them and generating signature base string

NSMutableCharacterSet *set = [NSMutableCharacterSet alphanumericCharacterSet];
[set addCharactersInString:@"._-"];
                
NSArray *sorted = [[parameters allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
NSMutableString *temp = [NSMutableString new];
for (NSString *key in sorted) {
    [temp appendFormat:@"%@=%@&", key, [parameters[key] stringByAddingPercentEncodingWithAllowedCharacters:set]];
}
NSString *parameterString = [temp stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"&"]];

This produces something like followed:

oauth_consumer_key=<consumer key here>&oauth_nonce=kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1318622958&oauth_token=<oauth token here>&oauth_version=1.0

Signature base string:

NSMutableString *signatureBaseString = [NSMutableString new];
[signatureBaseString appendString:@"POST&"];
[signatureBaseString appendString:[baseURL stringByAddingPercentEncodingWithAllowedCharacters:set]];
[signatureBaseString appendString:@"&"];
[signatureBaseString appendString:[parameterString stringByAddingPercentEncodingWithAllowedCharacters:set]];

And it looks like

POST&https%3A%2F%2Fapi.twitter.com%2F1%2Fstatuses%2Fupdate.json&oauth_consumer_key%3D<consumer key here>%26oauth_nonce%3DkYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1318622958%26oauth_token%3D<oauth token here>%26oauth_version%3D1.0

After that concatenating consumer secret + & + token secret:

NSString *consumerSecret = @"<consumer secret here>";
NSString *tokenSecret = [Twitter sharedInstance].session.authTokenSecret;
NSString *signingKey = [NSString stringWithFormat:@"%@&%@", [consumerSecret stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding], [tokenSecret stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];

Next step is sha1, key is signingKey and data is signatureBaseString. It will produce some 20-digit string. This will be our oauth_signature:
parameters[@"oauth_signature"] = signature;

  1. Lastly, generating Authentication header:
NSMutableString *DST = [NSMutableString new];
[DST appendString:@"OAuth "];
for (NSString *key in parameters.allKeys) {
    [DST appendFormat:@"%@=\"%@\", ", key, [parameters[key] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
}                
NSString *authorization = [DST stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@", "]];
  1. Building multipart/form-data request now:
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:baseURL] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:60];
request.HTTPMethod = @"POST";
NSString *boundary = [NSUUID UUID].UUIDString;
[request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary] forHTTPHeaderField:@"Content-Type"];
            
[request setValue:authorization forHTTPHeaderField:@"Authorization"];

boundary = [NSString stringWithFormat:@"--%@", boundary];
            
/// will acummulate HTTP body bytes into this variable
NSMutableData *HTTPBody = [NSMutableData new];
            
NSString *temp = [NSString stringWithFormat:@"%@\r\ncontent-disposition: form-data; name=\"%@\"\r\n\r\n%@\r\n", boundary, @"command", @"APPEND"];
[HTTPBody appendData:[temp dataUsingEncoding:NSUTF8StringEncoding]];
temp = [NSString stringWithFormat:@"%@\r\ncontent-disposition: form-data; name=\"%@\"\r\n\r\n%@\r\n", boundary, @"media_id", mediaID];
[HTTPBody appendData:[temp dataUsingEncoding:NSUTF8StringEncoding]];
            
temp = [NSString stringWithFormat:@"%@\r\ncontent-disposition: form-data; name=\"%@\"\r\n\r\n%@\r\n", boundary, @"segment_index", @"0"];
[HTTPBody appendData:[temp dataUsingEncoding:NSUTF8StringEncoding]];
            
temp = [NSString stringWithFormat:@"%@\r\ncontent-disposition: form-data; name=\"%@\"\r\n\r\n", boundary, @"media"];
[HTTPBody appendData:[temp dataUsingEncoding:NSUTF8StringEncoding]];
[HTTPBody appendData:video];
[HTTPBody appendData:[[NSString stringWithFormat:@"%@--", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
            
[request setHTTPBody:HTTPBody];

Request in raw will look like following:

--RDE4NDY1OUYtQzUwMS00MzM3LUI1MjMtOTQyM0I1ODY0RUI5
Content-Disposition: form-data; name="command"

APPEND
--RDE4NDY1OUYtQzUwMS00MzM3LUI1MjMtOTQyM0I1ODY0RUI5
Content-Disposition: form-data; name="media_id"

707908369483493377
--RDE4NDY1OUYtQzUwMS00MzM3LUI1MjMtOTQyM0I1ODY0RUI5
Content-Disposition: form-data; name="segment_index"

0
--RDE4NDY1OUYtQzUwMS00MzM3LUI1MjMtOTQyM0I1ODY0RUI5
Content-Disposition: form-data; name="media"
Content-Type: application/octet-stream
Content-Transfer-Encoding: binary

<VIDEO BYTES HERE>
--RDE4NDY1OUYtQzUwMS00MzM3LUI1MjMtOTQyM0I1ODY0RUI5--

Finally, executing request:

TWTRAPIClient *client = [Twitter sharedInstance].APIClient;
[client sendTwitterRequest:request completion:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
}];

After a while(depending on video size and internet connection) request fails with error

Error Domain=TwitterAPIErrorDomain Code=131 "Request failed: internal server error (500)" UserInfo={NSErrorFailingURLKey=https://upload.twitter.com/1.1/media/upload.json, NSLocalizedDescription=Request failed: internal server error (500), NSLocalizedFailureReason=Twitter API error : Internal error (code 131)}

In some other threads I saw somebody suggest to put auth_* parameters into POST body, but it doesn’t looks right.

Please help me with this


#2

I made it work by generating OAuth header using TWTRAPIClient. Seems that something wrong in my realization.
Here is the code which works:

NSString *baseURL = @"https://upload.twitter.com/1.1/media/upload.json";    
TWTRAPIClient *client = [Twitter sharedInstance].APIClient;
NSMutableURLRequest *request = [[client URLRequestWithMethod:@"POST" URL:baseURL parameters:nil error:NULL] mutableCopy];
    
NSString *boundary = [NSUUID UUID].UUIDString;
boundary = [[Base64 encode:[boundary dataUsingEncoding:NSUTF8StringEncoding]] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
[request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary] forHTTPHeaderField:@"Content-Type"];
    
boundary = [NSString stringWithFormat:@"--%@", boundary];

NSMutableData *HTTPBody = [NSMutableData new];
    
NSString *temp = [NSString stringWithFormat:@"%@\r\nContent-Disposition: form-data; name=\"%@\"\r\n\r\n%@\r\n", boundary, @"command", @"APPEND"];
[HTTPBody appendData:[temp dataUsingEncoding:NSUTF8StringEncoding]];
    
temp = [NSString stringWithFormat:@"%@\r\nContent-Disposition: form-data; name=\"%@\"\r\n\r\n%@\r\n", boundary, @"media_id", mediaID];
[HTTPBody appendData:[temp dataUsingEncoding:NSUTF8StringEncoding]];
    
temp = [NSString stringWithFormat:@"%@\r\nContent-Disposition: form-data; name=\"%@\"\r\n\r\n%@\r\n", boundary, @"segment_index", @"0"];
[HTTPBody appendData:[temp dataUsingEncoding:NSUTF8StringEncoding]];
    
temp = [NSString stringWithFormat:@"%@\r\nContent-Disposition: form-data; name=\"%@\"\r\n%@\r\n%@\r\n\r\n", boundary, @"media", @"Content-Type: application/octet-stream", @"Content-Transfer-Encoding: binary"];
[HTTPBody appendData:[temp dataUsingEncoding:NSUTF8StringEncoding]];
    
[HTTPBody appendData:video];
[HTTPBody appendData:[[NSString stringWithFormat:@"\r\n%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
    
[request setHTTPBody:HTTPBody];

[[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        
        NSDictionary *resp = nil;
        if (data) {
            resp = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL];
        }
        
        if (completion) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completion(mediaID, ((NSHTTPURLResponse *)response).statusCode, error);
            });
        }
        
}] resume];

Must notice that response may have statusCode 204, which is content not found, but that is fine. When you call command:FINALIZE for this media_id - you will be able to use this media in your status update.

I hope it will help somebody.