0

I want to post an image to a server-side Flask endpoint. I run the following GET and POST using the following code, and then I describe what the server receives. How can I make sure the server receives the image and post parameters in the POST request?

    [_client POST:@"post_image"
             parameters:parameters
             constructingBodyWithBlock:nil
             success:^(NSURLSessionDataTask *task, id responseObject) {
                 NSLog([responseObject description]);
             }
             failure:^(NSURLSessionDataTask *task, NSError *error) {
                 NSLog([error description]);
             }
     ];

   [_client GET:@"test_get"
     parameters:parameters
     success:^(NSURLSessionDataTask *task, id responseObject) {
         NSLog([responseObject description]);
     }
     failure:^(NSURLSessionDataTask *task, NSError *error) {
         NSLog([error description]);
     }
   ];

The logs print:

2014-07-02 10:29:06.883 lens-app[2808:60b] {
    code = "";
    data = "good stuff";
    error = "";
    success = 1;
}
2014-07-02 10:29:06.889 lens-app[2808:60b] {
    code = "";
    data = "good stuff";
    error = "";
    success = 1;
}

I use this Flask code to describe the requests the server receives:

def print_request():
  print 'request'
  print str(request)
  print 'request.headers'
  print str(request.headers)
  print 'request.args'
  print str(request.args)
  print 'request.form'
  print str(request.form)
  print 'request.files'
  print str(request.files)
  print 'request.data'
  print str(request.data)
  print 'request.stream'
  print str(request.stream.read())
  print 'request.environ'
  print str(request.environ)
  print 'request.get_json()'
  print str(request.get_json())
  print 'row post body'
  print str(request.environ['body_copy'])

@app.route('/post_image', methods=['POST'])
def post_image():
  print_request()
  return json_response('good stuff')

@app.route('/test_get', methods=['GET'])
def test_get():
  print_request()
  return json_response('good stuff')

Here's the result of the post request:

request
<Request 'http://ec2-xyz.us-west-2.compute.amazonaws.com/post_image' [POST]>
request.headers
Transfer-Encoding: Chunked
Content-Length:
User-Agent: lens-app/1.0 (iPhone; iOS 7.1.1; Scale/2.00)
Connection: keep-alive
Host: ec2-xyz.us-west-2.compute.amazonaws.com
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+F841C4764BD7CE95
Accept-Encoding: gzip, deflate


request.args
ImmutableMultiDict([])
request.form
ImmutableMultiDict([])
request.files
ImmutableMultiDict([])
request.data

request.stream

request.environ
{'wsgi.multiprocess': False, 'SERVER_SOFTWARE': 'Werkzeug/0.9.6', 'SCRIPT_NAME': '', 'HTTP_TRANSFER_ENCODING': 'Chunked', 'REQUEST_METHOD': 'POST', 'PATH_INFO': '/post_image', 'SERVER_PROTOCOL': 'HTTP/1.1', 'QUERY_STRING': '', 'werkzeug.server.shutdown': <function shutdown_server at 0x2c45c80>, 'CONTENT_LENGTH': '', 'HTTP_USER_AGENT': 'lens-app/1.0 (iPhone; iOS 7.1.1; Scale/2.00)', 'HTTP_CONNECTION': 'keep-alive', 'SERVER_NAME': '0.0.0.0', 'REMOTE_PORT': 44640, 'wsgi.url_scheme': 'http', 'SERVER_PORT': '80', 'werkzeug.request': <Request 'http://ec2-54-214-166-2.us-west-2.compute.amazonaws.com/post_image' [POST]>, 'body_copy': '', 'wsgi.input': <cStringIO.StringI object at 0x2be5ad0>, 'HTTP_HOST': 'ec2-xyz.us-west-2.compute.amazonaws.com', 'wsgi.multithread': False, 'HTTP_ACCEPT': '*/*', 'wsgi.version': (1, 0), 'wsgi.run_once': False, 'wsgi.errors': <open file '<stderr>', mode 'w' at 0x7fccfbd3f1e0>, 'REMOTE_ADDR': '204.28.119.158', 'HTTP_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+F841C4764BD7CE95', 'HTTP_ACCEPT_ENCODING': 'gzip, deflate'}
request.get_json()
None
row post body

Here's the result of the get request:

request
<Request 'http://ec2-xyz.us-west-2.compute.amazonaws.com/test_get?up=what's&you=Hey' [GET]>
request.headers
Content-Length:
User-Agent: lens-app/1.0 (iPhone; iOS 7.1.1; Scale/2.00)
Connection: keep-alive
Host: ec2-xyz.us-west-2.compute.amazonaws.com
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:
Accept-Encoding: gzip, deflate


request.args
ImmutableMultiDict([('you', u'Hey'), ('up', u"what's")])
request.form
ImmutableMultiDict([])
request.files
ImmutableMultiDict([])
request.data

request.stream

request.environ
{'wsgi.multiprocess': False, 'SERVER_SOFTWARE': 'Werkzeug/0.9.6', 'SCRIPT_NAME': '', 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/test_get', 'SERVER_PROTOCOL': 'HTTP/1.1', 'QUERY_STRING': 'up=what%27s&you=Hey', 'werkzeug.server.shutdown': <function shutdown_server at 0x2c45c08>, 'CONTENT_LENGTH': '', 'HTTP_USER_AGENT': 'lens-app/1.0 (iPhone; iOS 7.1.1; Scale/2.00)', 'HTTP_CONNECTION': 'keep-alive', 'SERVER_NAME': '0.0.0.0', 'REMOTE_PORT': 33553, 'wsgi.url_scheme': 'http', 'SERVER_PORT': '80', 'werkzeug.request': <Request 'http://ec2-xyz.us-west-2.compute.amazonaws.com/test_get?up=what's&you=Hey' [GET]>, 'body_copy': '', 'wsgi.input': <cStringIO.StringI object at 0x2be5ad0>, 'HTTP_HOST': 'ec2-xyz.us-west-2.compute.amazonaws.com', 'wsgi.multithread': False, 'HTTP_ACCEPT': '*/*', 'wsgi.version': (1, 0), 'wsgi.run_once': False, 'wsgi.errors': <open file '<stderr>', mode 'w' at 0x7fccfbd3f1e0>, 'REMOTE_ADDR': '204.28.119.158', 'HTTP_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': '', 'HTTP_ACCEPT_ENCODING': 'gzip, deflate'}
request.get_json()
None
row post body
David Snabel-Caunt
  • 57,804
  • 13
  • 114
  • 132
Rose Perrone
  • 61,572
  • 58
  • 208
  • 243

1 Answers1

0

It turns out that AFNetworking only supports streaming multipart POST requests. I'm using mod_wsgi server-side, and only mod_wsgi versions 3.0+ started to support chunked HTTP transfers. Here's the comment on the change in the mod_wsgi list of Changes in Version 3.0:

  1. Now allow chunked request content. Such content will be dechunked and available for reading by WSGI application. See:

http://code.google.com/p/modwsgi/issues/detail?id=1 To enable this feature, you must use:

WSGIChunkedRequest On for appropriate context in Apache configuration.

Do note however that WSGI is technically incapable of supporting chunked request content without all chunked request content having to be first read in and buffered. This is because WSGI requires CONTENT_LENGTH be set when there is any request content.

In mod_wsgi no buffering is done. Thus, to be able to read the request content in the case of a chunked transfer encoding, you need to step outside of the WSGI specification and do things it says you aren't meant to.

You have two choices for how you can do this. The first choice you have is to call read() on wsgi.input but not supply any argument at all. This will cause all request content to be read in and returned.

The second is to loop on calling read() on wsgi.input with a set block size passed as argument and do this until read() returns an empty string.

Because both calling methods are not allowed under WSGI specification, in using these your code will not be portable to other WSGI hosting mechanisms.

Here's the workaround I use. I don't use AFNetworking, so that I upload an image without streaming. I found this code here.

- (NSString *)uploadImageData:(NSData *)imageData toURL:(NSURL *)url withTitle:(NSString *)title {
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
    [request setURL:url];

    // create a boundary to delineate the file
    NSString *boundary = @"14737809831466499882746641449";
    // tell the server what to expect
    NSString *contentType =
    [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary];
    [request addValue:contentType forHTTPHeaderField: @"Content-Type"];

    // make a buffer for the post body
    NSMutableData *body = [NSMutableData data];

    // add a boundary to show where the title starts
    [body appendData:[[NSString stringWithFormat:@"\r\n--%@\r\n", boundary]
                      dataUsingEncoding:NSASCIIStringEncoding]];

    // add the title
    [body appendData:[
                      @"Content-Disposition: form-data; name=\"title\"\r\n\r\n"
                      dataUsingEncoding:NSASCIIStringEncoding]];
    [body appendData:[title
                      dataUsingEncoding:NSASCIIStringEncoding]];

    // add a boundary to show where the file starts
    [body appendData:[[NSString stringWithFormat:@"\r\n--%@\r\n", boundary]
                      dataUsingEncoding:NSASCIIStringEncoding]];

    // add a form field
    [body appendData:[
                      @"Content-Disposition: form-data; name=\"image\"; filename=\"image.jpeg\"\r\n"
                      dataUsingEncoding:NSASCIIStringEncoding]];

    // tell the server to expect some binary
    [body appendData:[
                      @"Content-Type: application/octet-stream\r\n"
                      dataUsingEncoding:NSASCIIStringEncoding]];
    [body appendData:[
                      @"Content-Transfer-Encoding: binary\r\n"
                      dataUsingEncoding:NSASCIIStringEncoding]];
    [body appendData:[[NSString stringWithFormat:
                       @"Content-Length: %i\r\n\r\n", imageData.length]
                      dataUsingEncoding:NSASCIIStringEncoding]];

    // add the payload
    [body appendData:[NSData dataWithData:imageData]];

    // tell the server the payload has ended
    [body appendData:
     [[NSString stringWithFormat:@"\r\n--%@--\r\n", boundary]
      dataUsingEncoding:NSASCIIStringEncoding]];

    // add the POST data as the request body
    [request setHTTPMethod:@"POST"];
    [request setHTTPBody:body];

    // now let's make the connection to the web
    NSData *returnData = [NSURLConnection sendSynchronousRequest:request returningResponse:nil error:nil];
    return [[NSString alloc] initWithData:returnData encoding:NSUTF8StringEncoding];
}
Community
  • 1
  • 1
Rose Perrone
  • 61,572
  • 58
  • 208
  • 243