envoy / Ambassador

Super lightweight web framework in Swift based on SWSGI
MIT License
185 stars 46 forks source link

Multi-part form upload returns empty data using DataReader #39

Open pranavs18 opened 6 years ago

pranavs18 commented 6 years ago

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 'file=@temp.jpg' http://localhost:8080/image , the d variable declared above is nil.

fangpenlin commented 6 years ago

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 :)

pranavs18 commented 6 years ago
        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 'file=@temp.jpg' 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)
fangpenlin commented 6 years ago

@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"])
         }
     }
pranavs18 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 "file=@temp.jpg, 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.

sushant-here commented 5 years ago

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

divyadilip91 commented 4 years ago

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