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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions