0

I deployed a PHP API that receives a file, a text parameter and an authorization header.

When I curl my API from my terminal works fine:

curl -v --request POST --header "Content-Type:multipart/form-data" \
  --form image=@examples/sample.jpeg \
  --form projectID="xxxx" \
  -H "key:mykey" https://api.example.com/v1/api.php/image

Now, using vanilla JS and a HTML form as this, works fine as well:

formElem.onsubmit = async (e) => {
    e.preventDefault();

    let body = new FormData(formElem);
    body.append("projectID", "xxxx");

    let response = await fetch('https://api.example.com/v1/api.php/image', {
      method: 'POST',
      body: body,
      headers: {    
        "key": "mykey"
      }
    });

    let result = await response.json();

I have also tried from other languages and works fine.

However, when I send the request using NodeJS I get this error message:

406 Not Acceptable! An appropriate representation of the requested resource could not be found on this server. This error was generated by Mod_Security

I understand 406 errors are related to these headers:

  • Accept
  • Accept-Encoding
  • Accept-Language

I've tried everything I could think of with those headers, even forcing them to be exactly as the ones sent with the code that works but I still get the 406 error.

How else can I debug this? It's crazy that it works with other languages but not with NodeJS. Is NodeJS adding something extra to the requests that triggers this error?

This is the code that runs the fetch using Axios:

async function getURLImage(image,projectID) {  
  const requestBody = new FormData()
  requestBody.append('image', fs.createReadStream(image))
  requestBody.append('projectID', projectID)
      
  let response = await axios.post(baseURL,requestBody, {
  
    headers: {
      ...requestBody.getHeaders(),
      "key": key
    },
  })
  .then(data => {
    console.log(requestBody)
    console.log('then:', data.data)
    console.log('then:', data)
  }) 
  .catch(err => {
    console.log(requestBody)
    console.log('catch',err.data)
    console.log('catch',err)
  })
}

Same thing without using Axios:

async function getURLImage(image,projectID) { 
  const requestBody = new FormData()
  requestBody.append('image', fs.createReadStream(image))
  requestBody.append('projectID', projectID)
      
  let response = await fetch(baseURL,{
    method: 'POST',
    body: requestBody, 
    headers: {
      "key": key
    },
  })

  .then(data => {
    console.log(requestBody)
    console.log('then:', data.headers)
    console.log('then:', data)
    return data.json()
  }) 
  .then(data => {
  
    console.log('then2:', data)
  }) 
  .catch(err => {
    console.log(requestBody)
    console.log('catch',err.data)
    console.log('catch',err)
  })
}

Any ideas would be extremely helpful. Thanks!

xtian777x
  • 113
  • 6
  • 1
    Have you tried setting the [multipart form-data `content-type` header](https://stackoverflow.com/a/59177066/1913729)? – blex Aug 22 '22 at 22:17
  • @ChrisG yes, this is the way I found to send files. Also, if I run that code from a JS script, works fine. The code also works if I deploy the API to a server without any security, but I don't want to do that in production of course – xtian777x Aug 23 '22 at 00:01
  • 1
    @blex yes, although FormData() adds that header automatically – xtian777x Aug 23 '22 at 00:01
  • @xtian777x not when using Axios with Node it doesn't. See https://github.com/form-data/form-data#axios. You need to include `requestBody.getHeaders()` into the `headers` object – Phil Aug 23 '22 at 03:27
  • @Phil Indeed. I edited my question to add that. I tried with that line but still getting the same 406 error. One thing I noticed in my response is _events: [Object: null prototype] { error: [Function: handleStreamError] } . I don't know what that is and if that's affecting the request. I event get that when commenting the file stream – xtian777x Aug 23 '22 at 03:40

1 Answers1

0

I found the solution. What my axios request was lacking was the getHeaders() function and a correct content length.

So first I added getHeaders() to get the correct multipart/form-data and boundary and then used getLengthSync() to get the content length. This will fail with the original code I posted because the file is being treat as a stream, so getLengthSync() can't computer the length. I need to send the file as a blob (fs.readFile) for getLengthSync() to work.

This is the final code that worked for me:

async function getURLImage(image,projectID) {
  
  fs.readFile(image, (err, file)=>{
    if(err){
      console.log(err)
      return;
    }
    const form = new FormData()
    form.append('image', file,'nft.jpeg')
    form.append('projectID', projectID)

      
  axios.post(baseURL,requestBody, {
  
    headers: {
      "key": key,
      ...form.getHeaders(),
      "Content-Length": form.getLengthSync()
    },
    })
    .then(data => {
      console.log(form)
      console.log('then:', data.data)
      console.log('then:', data)
    }) 
    .catch(err => {
      console.log(form)
      console.log('catch',err.data)
      console.log('catch',err)
    })
  })
  
}
xtian777x
  • 113
  • 6