1

I am attempting to upload a file from a React frontend to a Scala/Play Framework backend and it is not working.

So I have a button that uploads a file and calls handleUpload:

handleUpload=()=>{
    if(this.state.file!=null){
      console.log('value of this.state.file: ', this.state.file)
      var dataSend = new FormData();
      dataSend.append('file_upload', this.state.file); 
      console.log('value of dataSend before send: ', dataSend);
      this.setState({lastCommand: 'file_upload'}, ()=>{
        this.props.sendRequest({
          url: "http://localhost:9000/upload",
          method: "post",
          multipart: true,
          data:{
            payload:{
              data: dataSend
            }
          }
        });
      })
    }
  }

this.props.sendRequest is in my actions (Redux) and is the following:

export const request = payload => {
  return function(dispatch) {
    console.log("value of payload in actions request: ", payload)
    var config = {}
    if (payload.method=="post"||payload.method=="patch"){
      config = {
        url:    payload.url,
        method: payload.method,
        withCredentials: true,
        data:   payload.data,
      }
      if('multipart' in payload && payload.multipart){
        config.headers = { 'Content-Type': 'multipart/form-data' }
      }
    }else if (payload.method=='get'){
      config = {
        url:    payload.url,
        method: payload.method,
        withCredentials: true,
      }
    }

    console.log('value of config before send in actions/request: ', config);

    axios(config)
    .then(response=>{
      return dispatch({
        val: response,
        type: (payload.method=="post"||payload.method=="patch")?"POST_RETURN_SUCCESSFUL":"GET_RETURN_SUCCESSFUL", 
        sentID: ('sentID' in payload)?payload.sentID:null
      })
    })
    .catch(error=>{
      console.log('there was an error in request return in actions', error);
      return dispatch({
        val: error,
        type: (payload.method=="post"||payload.method=="patch")?"POST_RETURN_ERROR":"GET_RETURN_ERROR",
        sentID: ('sentID' in payload)?payload.sentID:null
      })
    });
  };
}

Here is the value of the config as console logged above:

value of config before send in actions/request:  
{…}
​
data: {…}
​​
payload: {…}
​​​
data: FormData {  }
​​​
<prototype>: Object { … }
​​
<prototype>: Object { … }
​
headers: {…}
​​
"Content-Type": "multipart/form-data"
​​
<prototype>: Object { … }
​
method: "post"
​
url: "http://localhost:9000/upload"
​
withCredentials: true

Note that while it appears that the form data is empty this is an artifact of the inability of the web console to display it (How to inspect FormData?)

This should be hitting the following route in my play framework (as defined in my config/routes file):

+nocsrf
POST    /upload        controllers.PostController.admin_upload

where +nocsrf is simply a handler to prevent cross site request forgery.

NOTE: I am sending to the url "http://localhost:9000/upload" on the frontend.

My upload function controllers.PostController.admin_upload is a simple hello_there

def admin_upload[T]:Action[AnyContent] = Action.async {request: Request
  [AnyContent] => Future { 
      println("****************************")
      println("inside admin_upload")
      println("****************************")
      var responseVal = new Response().standard_response("/admin/patch", "DUMMY - STILL WORKING ON ROUTE")
      Ok(responseVal)
    }(ec)
  }

I get no console logging on the backend, and I get a 400 NOT FOUND error on the frontend.

Here is my network request headers:

Host: localhost:9000
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:70.0) Gecko/20100101 Firefox/70.0
Accept: application/json, text/plain, */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data
Content-Length: 23
Origin: http://localhost:3000
Connection: keep-alive
Referer: http://localhost:3000/admin
Cookie: PLAY_SESSION=SUPERDOOPERSECRETDAWG

and response headers:

HTTP/1.1 400 Bad Request
Vary: Origin
Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Credentials: true
X-Permitted-Cross-Domain-Policies: master-only
Date: Wed, 13 Nov 2019 17:08:34 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 1170

I am totally lost, this should work.

What is broken?

EDIT:

If I drop the Content-Type header on the axios request and console out the result I do get inside the admin_upload function but the request body is of course empty because I haven't sent a form. So I've narrowed the problem to "how do I send the correct content-type flag to play framework?"

****************************
inside admin_upload
****************************
value of request.body: 
AnyContentAsJson({"payload":{"data":{}}})
request.body.asMultipartFormData
None

EDIT EDIT:

Even the most simple example as shown from here: https://www.playframework.com/documentation/2.7.x/ScalaFileUpload

Doe not appear to work.

  def admin_upload = Action(parse.multipartFormData) { request =>
    println("****************************")
    println("inside admin_upload")
    println("****************************")
    println(request.body)
    var responseVal = new Response().standard_response("/admin/patch", "DUMMY - STILL WORKING ON ROUTE")
    Ok(responseVal)
  }

EDIT EDIT EDIT:

OK - this does work in Insomnia, I had it configured incorrectly. This means that the issue is that axios is not working correctly, I'll send a bug report to them.

axios is borked

Peter Weyand
  • 2,159
  • 9
  • 40
  • 72
  • Have you tried to test upload manually? using cURL? Where is the request URI/path in the traces ? – cchantep Nov 13 '19 at 17:20
  • So, I am testing manually now - I'm using insomnia (like postman). Ditching the Content-Type header works but I don't know yet if that will allow me to send the form data. Something is weird. (I get `For request 'POST /upload' [Missing boundary header]` if I use Content-Type). – Peter Weyand Nov 13 '19 at 17:24
  • when you print the request headers could you show the route you are hitting. are you hitting the correct route? – jstuartmilne Nov 13 '19 at 17:30
  • Sorry - see above edit. I think play framework is not seeing `Content-Type` header correctly. – Peter Weyand Nov 13 '19 at 17:31
  • Play is using content type properly – cchantep Nov 13 '19 at 18:08

1 Answers1

0

The solution was to make a cut out in my request handler and use fetch instead of axios for file uploads. Like this:

export const request = payload => {
  return function(dispatch) {
    console.log("value of payload in actions request: ", payload)
    var config = {}
    if('multipart' in payload && payload.multipart){
      config = {
        method: "POST",
        body: payload.data
      }
      fetch("http://localhost:9000/upload", config)
      .then(response => response.json())
      .then(response => {
        console.log("inside success in fetch and value of response: ", response)
        return dispatch({
          val: response,
          type: "POST_RETURN_SUCCESSFUL"
        })
      })
      .catch(error =>{
        console.log('there was an error in request return in actions', error);
        return dispatch({
          val: error,
          type: "POST_RETURN_ERROR"
        })
      });
    }else{
      if (payload.method=="post"||payload.method=="patch"){
        config = {
          url:    payload.url,
          method: payload.method,
          withCredentials: true,
          data:   payload.data,
        }
      }else if (payload.method=='get'){
        config = {
          url:    payload.url,
          method: payload.method,
          withCredentials: true,
        }
      }
      console.log('value of config before send in actions/request: ', config);

      axios(config)
      .then(response=>{
        return dispatch({
          val: response,
          type: (payload.method=="post"||payload.method=="patch")?"POST_RETURN_SUCCESSFUL":"GET_RETURN_SUCCESSFUL", 
          sentID: ('sentID' in payload)?payload.sentID:null
        })
      })
      .catch(error=>{
        console.log('there was an error in request return in actions', error);
        return dispatch({
          val: error,
          type: (payload.method=="post"||payload.method=="patch")?"POST_RETURN_ERROR":"GET_RETURN_ERROR",
          sentID: ('sentID' in payload)?payload.sentID:null
        })
      });
    }
  };
}

Axios has problems an issue with file uploads. See here: https://github.com/axios/axios/issues/318

The workaround then is to use fetch.

marcospereira
  • 12,045
  • 3
  • 46
  • 52
Peter Weyand
  • 2,159
  • 9
  • 40
  • 72