aio-libs/aiohttp · error · ValueError

bad content for quoted-string

Error message

bad content for quoted-string {content!r}

What it means

quoted_string raises ValueError when the content string contains characters outside the QCONTENT set (printable 7-bit ASCII 0x20-0x7E plus tab). This function formats values as RFC 5322 quoted-strings for Content-Disposition headers, which require 7-bit US-ASCII content.

Solutions

  1. Let content_disposition_header handle it — it catches quoted_string ValueError and falls back to RFC 5987 extended notation (filename* with charset encoding)
  2. If calling quoted_string directly, sanitize content to 7-bit ASCII first
  3. Use the filename* parameter approach: encode non-ASCII filenames with urllib.parse.quote

Example fix

# before (direct call fails on non-ASCII)
from aiohttp.helpers import quoted_string
quoted_string('café.pdf')  # ValueError

# after (let content_disposition_header handle encoding)
from aiohttp.helpers import content_disposition_header
header = content_disposition_header('attachment', params={'filename': 'café.pdf'})
# produces: attachment; filename*=utf-8''caf%C3%A9.pdf
Defensive patterns

Strategy: validation

Validate before calling

QCONTENT = {chr(i) for i in range(0x20, 0x7F)} | {'\t'}

def is_quoted_string_safe(content: str) -> bool:
    return bool(content) and QCONTENT > set(content)

if not is_quoted_string_safe(my_content):
    # Encode for RFC 5987 extended notation instead
    from urllib.parse import quote
    content = f"utf-8''{quote(my_content, encoding='utf-8')}"

Type guard

def is_ascii_printable(s: str) -> bool:
    return all(0x20 <= ord(c) <= 0x7E or c == '\t' for c in s)

Try / catch

from aiohttp.helpers import quoted_string
try:
    result = quoted_string(content)
except ValueError:
    from urllib.parse import quote
    result = f"utf-8''{quote(content, encoding='utf-8')}"

Prevention

When it happens

Trigger: Calling helpers.quoted_string(content) or indirectly via content_disposition_header when a disposition parameter value contains non-ASCII characters (e.g. Unicode filenames with accented characters, CJK text, emoji) and quote_fields is True.

Common situations: Uploading a file with a Unicode name (e.g. 'café.pdf', 'データ.xlsx') as a multipart form field; Content-Disposition parameters with emoji or other non-ASCII content; content_disposition_header called with internationalized filenames.

Related errors


AI-assisted analysis of aio-libs/aiohttp@d041d4d0fd (2026-08-11). Data as JSON: /api/errors/0ee2b986c89bea7a. Report an issue: GitHub.

Appendix: source

Thrown at aiohttp/helpers.py:412

    if name and isinstance(name, str) and name[0] != "<" and name[-1] != ">":
        return Path(name).name
    return default


not_qtext_re = re.compile(r"[^\041\043-\133\135-\176]")
QCONTENT = {chr(i) for i in range(0x20, 0x7F)} | {"\t"}


def quoted_string(content: str) -> str:
    """Return 7-bit content as quoted-string.

    Format content into a quoted-string as defined in RFC5322 for
    Internet Message Format. Notice that this is not the 8-bit HTTP
    format, but the 7-bit email format. Content must be in usascii or
    a ValueError is raised.
    """
    if not (QCONTENT > set(content)):
        raise ValueError(f"bad content for quoted-string {content!r}")
    return not_qtext_re.sub(lambda x: "\\" + x.group(0), content)


def content_disposition_header(
    disptype: str,
    quote_fields: bool = True,
    _charset: str = "utf-8",
    params: dict[str, str] | None = None,
) -> str:
    """Sets ``Content-Disposition`` header for MIME.

    This is the MIME payload Content-Disposition header from RFC 2183
    and RFC 7579 section 4.2, not the HTTP Content-Disposition from
    RFC 6266.

    disptype is a disposition type: inline, attachment, form-data.
    Should be valid extension token (see RFC 2183)

View on GitHub (pinned to d041d4d0fd)