Skip to content

Server returns 415 for Content-Types with parameters (e.g., application/json; charset=utf-8) #234

@ElanHR

Description

@ElanHR

Summary

The server-side codec lookup in _protocol_connect.codec_name_from_content_type strips only the application/ (or application/connect+) prefix and uses the rest verbatim as the codec name. When a client sends a Content-Type with parameters — e.g. application/json; charset=utf-8 — the lookup key becomes json; charset=utf-8, no codec matches, and _server_async.py:213 (and the _server_sync.py counterparts at :306/:437) raises HTTPException(HTTPStatus.UNSUPPORTED_MEDIA_TYPE).

Per RFC 9110 §8.3.2 the charset parameter is part of the standard media-type grammar and the value is case-insensitive so servers are expected to tolerate it.

Reproduction

# Works:
curl -X POST -H 'Content-Type: application/json' -d '{}' \
  https://my-server/<service>/<method>
# → 401/200 — codec resolves

# Fails:
curl -X POST -H 'Content-Type: application/json; charset=utf-8' -d '{}' \
  https://my-server/<service>/<method>
# → HTTP 415 Unsupported Media Type

Hits in production when a browser or proxy adds ; charset=utf-8 to the Connect-ES request (we observed this on a Connect-ES --> Connect-Python path even though @connectrpc/connect's request-header.js builds the Content-Type without parameters).

Expected behavior

Something matching the reference Go implementation, ie. canonicalizeContentType calls mime.ParseMediaType, discards/canonicalizes parameters, and matches on the bare media type.

Suggested fix

In _protocol_connect.codec_name_from_content_type, parse the input via Python's media-type parser before stripping the prefix:

from email.message import Message

def codec_name_from_content_type(content_type: str, *, stream: bool) -> str:
    msg = Message()
    msg["content-type"] = content_type
    base = msg.get_content_type()  # e.g. "application/json"
    prefix = (
        CONNECT_STREAMING_CONTENT_TYPE_PREFIX
        if stream
        else CONNECT_UNARY_CONTENT_TYPE_PREFIX
    )
    if base.startswith(prefix):
        return base[len(prefix):]
    return base

Thanks for all the work on the project. Happy to send a PR if it's useful. :)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions