Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Axios adds array indexes for objects in arrays where it shouldn't #5094

Closed
doits opened this issue Oct 11, 2022 · 12 comments
Closed

Axios adds array indexes for objects in arrays where it shouldn't #5094

doits opened this issue Oct 11, 2022 · 12 comments

Comments

@doits
Copy link

doits commented Oct 11, 2022

Describe the bug

config.paramsSerializer.indexes = false (default) adds indexes if an object is inside an array but it shouldn't.

To Reproduce

https://runkit.com/doits/634530f2d3a446000813ef73

var axios = require("axios")

const req = await axios.get('https://www.example.com', { params: { a: [{ name: 'abc' }], b: ['x', 'y'] } })

console.log(req.request.path)

// => "/?a[0][name]=abc&b[]=x&b[]=y"

Expected behavior

No a[0] but a[]:

// => "/?a[][name]=abc&b[]=x&b[]=y"

Environment

  • Axios Version 1.1.2

Additional context/Screenshots

None

@DigitalBrainJS
Copy link
Collaborator

DigitalBrainJS commented Oct 11, 2022

This is expected behavior. The indexes option only controls brackets mode for flat arrays. Axios will add explicit indexes for nested object elements, as this is required to keep data consistency by getting an unambiguous representation of an object as a URL string. The goal is not to follow qs, but to have maximum compatibility with the server framework (express.js) out of the box.
Obviously, in the case of only one element in the array, we can omit its index, but this optimization was not implemented because its importance was in doubt, while the lib size is matter. But this optimization can be added if the community sees fit.

Let's say we have the following object we want ta pass to the backend:

const params = {
  users: [
    {name: 'abc', age: 27, orders: [1, 2, 3]},
    {name: 'qwerty', age: 32, orders: [4, 5, 6]}]
  ,
  data: ['x', 'y']
};

It will be encoded by Axios as users[0][name]=abc&users[0][age]=27&users[0][orders][0]=1&users[0][orders][1]=2&users[0][orders][2]=3&users[1][name]=qwerty&users[1][age]=32&users[1][orders][0]=4&users[1][orders][1]=5&users[1][orders][2]=6&data[]=x&data[]=y

So server framework like express.js will handle it as the totally same object because of explicit indexes for nested objects:

{
  users: [
    { name: 'abc', age: '27', orders: [1, 2, 3] },
    { name: 'qwerty', age: '32', orders: [4, 5, 6] }
  ],
  data: [ 'x', 'y' ]
}

But if you encode the params with qs in brackets mode you will get:
users[][name]=abc&users[][age]=27&users[][orders][]=1&users[][orders][]=2&users[][orders][]=3&users[][name]=qwerty&users[][age]=32&users[][orders][]=4&users[][orders][]=5&users[][orders][]=6&data[]=x&data[]=y

{"users":[{"name":["abc","qwerty"],"age":["27","32"],"orders":["1","2","3","4","5","6"]}],"data":["x","y"]} 

Here you can't determine how many orders the user actually has. That is why Axios adds indexes for nested object elements despite the indexes option being set to false (empty brackets).

@doits
Copy link
Author

doits commented Oct 11, 2022

Allright, thanks for the detailed information, I understand what you mean.

I reported it because my Rails server cannot handle it and it worked before (with qs as a custom paramsSerializer), so I couldn't get axios 1.1.2 to work with my Rails API.

I'll check out some more complex query and report back how it works there. Maybe the order of params is important, eg

?users[][name]=X
&users[][orders][]=O1
&users[][orders][]=O2
&users[][name]=Y
&users[][orders][]=O3
&users[][orders][]=O4

gets parsed as

{ "users": [{ "name": "X", orders: ["O1", "O2"] }, { "name": "Y", "orders": ["O3", "O4"] }] }

because of the query params order. (just a wild guess though, I have not verified it yet)

@DigitalBrainJS
Copy link
Collaborator

because of the query params order.

Hmm... If Rails can't handle explicit indexes, then perhaps we should consider adding another encoding mode, since the library size won't increase in this particular case. It was assumed that all frameworks at least can handle encoding using explicit indexes.

@doits
Copy link
Author

doits commented Oct 11, 2022

Here are some examples how Rails parses params (tested it in my App):

1. ?a=b

=> {"a":"b"}


2. ?a[]=b

 => {"a":["b"]}


3. ?a[0]=b

=> {"a":{"0":"b"}}

4. ?a[][name]=x

=> {"a":[{"name":"x"}]}


5. ?a[0][name]=x

=> {"a":{"0":{"name":"x"}}


6. ?a[][name]=x&a[][name]=y

=> {"a":[{"name":"x"},{"name":"y"}]}


7. ?a[][name]=x&a[][name]=y&a[][orders]=O1

=> {"a":[{"name":"x"},{"name":"y","orders":"O1"}]}


8. ?a[][name]=x&a[][name]=y&a[][orders][]=O1

=> {"a":[{"name":"x"},{"name":"y","orders":["O1"]}]}


9. ?a[][name]=x&a[][name]=y&a[][orders][]=O1&a[][orders][]=O2

=> {"a":[{"name":"x"},{"name":"y","orders":["O1","O2"]}]}


10. ?a[][name]=x&a[][orders][]=O1&a[][orders][]=O2&a[][name]=y&a[][orders][]=O3&a[][orders][]=O4

=> {"a":[{"name":"x","orders":["O1","O2"]},{"name":"y","orders":["O3","O4"]}]}


11. ?a[][name]=x&a[][orders][]=O1&a[][orders][]=O2&a[][other]=y&a[][orders][]=O3&a[][orders][]=O4

=> {"a":[{"name":"x","orders":["O1","O2","O3","O4"],"other":"y"}]


12. ?a[][name]=x&a[][orders][]=O1&a[][orders][]=O2&a[][other]=y&a[][orders][]=O3&a[][other]=z&a[][orders][]=O4

=> {"a":[{"name":"x","orders":["O1","O2","O3"],"other":"y"},{"other":"z","orders":["O4"]}]}

My takeaway is:

  • Rails doesn't like explicit indexes at all (example 3 and 5)
  • When Rails encounters a duplicate key that is no array, it creates a new object (example 7)
  • It really depends on the order of params which object gets the items (example 10)
  • There must be an existing duplicated key before a new object is created, if there is a new one it does not create another object (example 11)
  • It can by any duplicated key, it is not required be the first one (example 12, the duplicated key is other instead of name)

But it looks like this method of parsing does not work for every input. I just tested the other way around how Rails encodes these params without a duplicated key:

{"a":[{"name":"x"},{"other":"y"}]}

It generates an url with this query ...

?a[][name]=x&a[][other]=y

... but if you feed this back to Rails it parses it as ...

{"a":[{"name":"x","other":"y"}]}

Looks like Rails cannot handle every object in url params 🤷

@doits
Copy link
Author

doits commented Oct 11, 2022

I started a discussion at rack/rack#1974 (this is what Rails uses when parsing params), maybe we get some more info there.

@doits
Copy link
Author

doits commented Oct 12, 2022

rack/rack#1974 just confirmed that there is this ambiguity when building queries without indexes.

Maybe axios shouldn't care about it then either and generate queries without indexes even if there is a possible ambiguity, because it was always like this?

On the other hand something like #5108 would allow to pass an own params serializer (like qs) again. So if you don't want to support a serialization format with ambiguities, having the option to pass an own serializer solves this too (so everybody can do what they want).

Though at least people using Rails need this functionality, maybe it is a good idea to support this out of the box with config.paramsSerializer.indexes = false?

@DigitalBrainJS
Copy link
Collaborator

Probably, the current paramsSerializer.indexes = false behavior should be refactored as paramsSerializer.indexes = undefined (default) to serialize in "mixed mode", and paramsSerializer.indexes = false should be totally without indexes.
But in any case, it seems that adding custom serialization feature support is a must, even if we add RoR support, as there will always be some kind of incompatible framework.

@doits
Copy link
Author

doits commented Oct 13, 2022

Yeah, seems good for me. Maybe think about using paramsSerializer.indexes as a string option that can be one of these: mixed | noBrackets | emptyBrackets | always | custom. When set to custom, it requires a paramsSerializer.customSerializer: (params) => string | null function to handle the serialization.

IMO this is easier to understand from an outside perspective (and you even have the option to add more ways to serialize, who knows).

@jasonsaayman
Copy link
Member

Closing cause the custom serializer has been merged

@basicallydan
Copy link

Hiya! Just popping in here to show my support for a solution like what @DigitalBrainJS suggested. As a RoR developer, a simple way to tell Axios not to put indexes in without having to use a custom serializer would be helpful. This thread was helpful for learning that I needed to do that, though.

Keep up the good work, thanks!

@aripollak
Copy link

For sending FormData, another option is to pass the data first through https://github.com/therealparmesh/object-to-formdata, since by default that does not add array indexes, even with nested objects.

@jaymoh
Copy link

jaymoh commented Mar 7, 2024

I was here a year before today and couldn't find a solution, so I just downgraded my Axios version to 0.2, which was working before upgrading. I realize Ruby on Rails and Laravel have the same issue when receiving a request from Axios. So I'm answering for anyone who will encounter this issue, and for my future self. Here is a solution that seems to work. It rides on paramSerializer which I researched more about after reading it in this issue.

const axiosInstance = axios.create({
  baseURL: 'your_base_url',
  headers: {
    Accept: 'application/json',
  },
  paramsSerializer: params => {
    // Iterate over all properties of the params object
    for (const key in params) {
      // Check if the property is an array
      if (Array.isArray(params[key])) {
        // Check if the first element of the array is an object
        if (params[key].length > 0 && typeof params[key][0] === 'object') {
          // Map over the array and convert each object to a JSON string
          params[key] = params[key].map(relation => JSON.stringify(relation))
        }
      }
    }
    return Qs.stringify(params, { arrayFormat: 'brackets' })
  },
})

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants