Ambassador icon indicating copy to clipboard operation
Ambassador copied to clipboard

Multi-part form upload returns empty data using DataReader

Open pranavs18 opened this issue 7 years ago • 8 comments

Can someone give an example of how to handle POST requests where we are uploading files/photos (multipart/form-data).

I tried to use DataReader(input) snippet but that always gives me an empty data back. Does environ need to have another key set ? Looking at python's uwsgi definitions, they advise to use cgi.FieldStorage.

d = cgi.FieldStorage(environ=environ, fp=environ['wsgi.input'], keep_blank_values=True)

Could you provide an example of how do we handle this case ?

DataReader.read(input) { d in print(d) }

In this case, when using curl -v -X POST -F '[email protected]' http://localhost:8080/image , the d variable declared above is nil.

pranavs18 avatar Aug 06 '18 20:08 pranavs18

Hey @pranavs18, can you give me a workable code example that allows me to reproduce the problem you describe here? It would be helpful for me to figure what's going on there for you :)

fangpenlin avatar Aug 07 '18 04:08 fangpenlin

     router["/image"] = JSONResponse(handler: {environ -> Any in

         let input = environ["swsgi.input"] as! SWSGIInput
         DataReader.read(input) { data in
             // get the image bytes from the request object
             funcABC(data)
             startResponse("200 OK", [])
             sendBody(Data("".utf8))
             sendBody(Data())
         }
         return ["Image processed":"1"]
         }

I am trying to use curl -v -X POST -F '[email protected]' http://localhost:8080/image . This would do the multi-part form-data image upload. The issue with the above code snippet is that when we run the above curl command, the data in the closure is empty. I believe, since it's a multi part form-data upload on the server side, I would need to extract the file field from the http request body object to get the bytes. How can I do this using embassy?

If I were to do this in swift in another http framework, I can perhaps do this like:

let content = request.formData?["file"]?.part.body 
// get bytes  

let d = Data(bytes: content)

pranavs18 avatar Aug 07 '18 06:08 pranavs18

@pranavs18 JSONResponse will respond immediately after you return via return ["Image processed":"1"]. Then it will close the HTTP connection. DataReader.read(input) is an async operation, which means it will wait until new HTTP body data arrive. So when JSONResponse handler returns, DataReader.read won't get a chance to really read the data.

As if you want to read the complete HTTP body payload, you will need to wait until DataReader.read called the callback then send the response to peer. For JSONResponse, there's another async constructor you can use

https://github.com/envoy/Ambassador/blob/7992f4a2379066a8590496650ef595d4a5442a59/Ambassador/Responses/JSONResponse.swift#L24

Like this, you can modify your code with the sendJSON for finishing up the response

     router["/image"] = JSONResponse(handler: { (environ, sendJSON) -> Any in

         let input = environ["swsgi.input"] as! SWSGIInput
         DataReader.read(input) { data in
             // get the image bytes from the request object
             funcABC(data)
             sendJSON(["Image processed":"1"])
         }
     }

fangpenlin avatar Aug 08 '18 03:08 fangpenlin

I did try this before and it gives me a compilation error at the handler:{ (: Unable to infer closure type in the current context. I am using swift 4.1

            router["/image"] = JSONResponse(handler: { (environ, sendJSON) -> Any in
            let input = environ["swsgi.input"] as! SWSGIInput
            
            DataReader.read(input) { data in
                funcABC(data)
                sendJSON(["Image processed":"1"])
            }
        })

pranavs18 avatar Aug 08 '18 05:08 pranavs18

It works if I change Any to void and I do get the bytes of the image uploaded. But I wonder how do I extract the image content from there ?

This snippet compiles but the data passed in funcABC() expects an image but fails to find that. When I print data, it gives me the total bytes. How would I extract the file content from there ?

           router["/image"] = JSONResponse(handler: { (environ, sendJSON) -> Void in
           let input = environ["swsgi.input"] as! SWSGIInput
           
           DataReader.read(input) { data in
               funcABC(data)
               sendJSON(["Image processed":"1"])
           }
       })

pranavs18 avatar Aug 08 '18 06:08 pranavs18

I actually figured out the problem.

When I used curl with --data-binary @temp.jpg , that worked like a charm. All the bytes were correctly received and processed.

When I used curl with -F "[email protected], that fails. And it is probably because, the extra string file= and the multipart form-data boundary bytes need to be extracted away I believe from the total bytes uploaded. In this case, the total bytes uploaded are much more than in the case of --data-binary.

May be, you could add support for processing multi-part form-data as well.

pranavs18 avatar Aug 08 '18 08:08 pranavs18

@fangpenlin Are there any plans or a workaround for supporting multi-part form-data?

squeaky-nose avatar Dec 11 '18 23:12 squeaky-nose

Is the ambassador authorizer now allowing multipart form data parameters to be passed through?

divyadilip91 avatar Apr 14 '20 15:04 divyadilip91