aio-libs/aiohttp · error · TypeError

Only io.IOBase, multidict and (name, file) pairs allowed, us

Error message

Only io.IOBase, multidict and (name, file) pairs allowed, use .add_field() for passing more complex parameters, got {rec!r}

What it means

Raised by FormData.add_fields when an element of the *fields sequence is not an io.IOBase, not a MultiDict/MultiDictProxy, and not a 2-element list/tuple. add_fields dispatches on the runtime type of each record; anything else falls into the else branch and raises TypeError, pointing the user at add_field() for complex parameters.

Source

Thrown at aiohttp/formdata.py:101

    def add_fields(self, *fields: Any) -> None:
        to_add: deque[Any] = deque(fields)

        while to_add:
            rec = to_add.popleft()

            if isinstance(rec, io.IOBase):
                k = guess_filename(rec, "unknown")
                self.add_field(k, rec)  # type: ignore[arg-type]

            elif isinstance(rec, (MultiDictProxy, MultiDict)):
                to_add.extend(rec.items())

            elif isinstance(rec, (list, tuple)) and len(rec) == 2:
                k, fp = rec
                self.add_field(k, fp)

            else:
                raise TypeError(
                    "Only io.IOBase, multidict and (name, file) "
                    "pairs allowed, use .add_field() for passing "
                    f"more complex parameters, got {rec!r}"
                )

    def _gen_form_urlencoded(self) -> payload.BytesPayload:
        # form data (x-www-form-urlencoded)
        data = []
        for type_options, _, value in self._fields:
            if not isinstance(value, str):
                raise TypeError(f"expected str, got {value!r}")
            data.append((type_options["name"], value))

        charset = self._charset if self._charset is not None else "utf-8"

        if charset == "utf-8":
            content_type = "application/x-www-form-urlencoded"
        else:

View on GitHub (pinned to c0ef574e29)

Solutions

  1. Use add_field() for each field with explicit name/value.
  2. Pass a list of (name, value) 2-tuples.
  3. Pass a MultiDict of name/value pairs for simple forms.

Example fix

// before
form = FormData([('field', val, 'text/plain')])  # 3-tuple not unpacked
// after
form = FormData()
form.add_field('field', val, content_type='text/plain')
Defensive patterns

Strategy: validation

Validate before calling

def normalize_fields(fields):
    if isinstance(fields, dict):
        return [(k, v) for k, v in fields.items()]
    if isinstance(fields, (list, tuple)):
        for rec in fields:
            assert isinstance(rec, (list, tuple)) and len(rec) == 2, f'bad record {rec!r}'
        return list(fields)
    raise TypeError('fields must be dict or list of 2-tuples')

Type guard

def is_valid_field_record(rec) -> bool:
    import io
    from multidict import MultiDict, MultiDictProxy
    return (
        isinstance(rec, io.IOBase)
        or isinstance(rec, (MultiDict, MultiDictProxy))
        or (isinstance(rec, (list, tuple)) and len(rec) == 2)
    )

Prevention

When it happens

Trigger: Passing FormData({'k': v}) is fine (dict converted), but FormData('plain string'), FormData(42), FormData((1,2,3)) (3-tuple), or FormData([{'nested': 'dict'}]) triggers it. Also FormData(SomeObj()).

Common situations: Passing a single string value directly to FormData instead of a list of pairs; passing 3-tuples (name, value, content_type) which add_fields does not unpack; passing a single non-file object.

Related errors


AI-assisted analysis of aio-libs/aiohttp@c0ef574e29 (2026-08-04). Data as JSON: /data/errors/1379bd6dd2294418.json. Report an issue: GitHub.