Skip to content

Parse request headers as bytes so invalid UTF-8 cannot raise - #639

Open
koic wants to merge 1 commit into
modelcontextprotocol:mainfrom
koic:parse_request_headers_as_bytes
Open

koic wants to merge 1 commit into
modelcontextprotocol:mainfrom
koic:parse_request_headers_as_bytes

Conversation

@koic

@koic koic commented Oct 30, 2026

Copy link
Copy Markdown
Member

Motivation and Context

The streamable HTTP transport validates the Host, Origin, Accept, and Content-Type headers with downcase, split, and regexps. A Rack server hands header values over as ASCII-8BIT, where every byte is valid, but Rack::Test, Rack::MockRequest, and a framework that re-tags the env can pass them as UTF-8, and then a header carrying an invalid byte sequence makes those calls raise ArgumentError. For Host and Origin on any method and for Accept on a GET the exception escapes the Rack call, and on a POST the Accept and Content-Type checks turn it into a 500 with an exception report, instead of the rejection the check exists to produce.

The transport now reads those headers as bytes, so both environments behave like a Rack server does: a Host or Origin carrying an invalid byte in the compared part answers 403, an Accept whose required media type carries one answers 406, and a Content-Type whose media type carries one answers 415. Nothing changes for ASCII values, which host names and media types are on the wire (an internationalized host travels as punycode).

How Has This Been Tested?

A new transport test sends each of the four headers with a trailing invalid byte in a UTF-8-tagged string and asserts the status and message the check produces. It errors with ArgumentError: input string invalid without the change.
A second test sends such an Accept on a GET, which answers 406 now and raised before, since nothing rescues in handle_get.

Breaking Changes

An allowed_hosts: or allowed_origins: entry with a non-ASCII character no longer matches a header tagged UTF-8 in a test or framework environment; it never matched the bytes a Rack server hands over, so a deployment is unchanged.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

## Motivation and Context

The streamable HTTP transport validates the `Host`, `Origin`, `Accept`, and `Content-Type` headers with `downcase`,
`split`, and regexps. A Rack server hands header values over as ASCII-8BIT, where every byte is valid, but Rack::Test,
`Rack::MockRequest`, and a framework that re-tags the env can pass them as UTF-8, and then a header carrying
an invalid byte sequence makes those calls raise `ArgumentError`. For `Host` and `Origin` on any method and for
`Accept` on a GET the exception escapes the Rack call, and on a POST the `Accept` and `Content-Type` checks turn
it into a 500 with an exception report, instead of the rejection the check exists to produce.

The transport now reads those headers as bytes, so both environments behave like a Rack server does:
a `Host` or `Origin` carrying an invalid byte in the compared part answers 403, an `Accept` whose required
media type carries one answers 406, and a `Content-Type` whose media type carries one answers 415. Nothing
changes for ASCII values, which host names and media types are on the wire (an internationalized host travels
as punycode).

## How Has This Been Tested?

A new transport test sends each of the four headers with a trailing invalid byte in a UTF-8-tagged string
and asserts the status and message the check produces. It errors with `ArgumentError: input string invalid`
without the change.
A second test sends such an `Accept` on a GET, which answers 406 now and raised before, since nothing
rescues in `handle_get`.

## Breaking Changes

An `allowed_hosts:` or `allowed_origins:` entry with a non-ASCII character no longer matches a header tagged UTF-8
in a test or framework environment; it never matched the bytes a Rack server hands over, so a deployment is
unchanged.

This branch has not been deployed

No deployments
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

Successfully merging this pull request may close these issues.

2 participants