Skip to content

Parse the WWW-Authenticate challenge in linear time - #571

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

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

Conversation

@koic

@koic koic commented Sep 22, 2026

Copy link
Copy Markdown
Member

Motivation and Context

Discovery.parse_www_authenticate walked the Bearer challenge's parameters by slicing the header at the cursor and trimming the slice for every key=value pair, which copies the remainder of the header each time. The cost therefore grew with the square of the header's length, and the header is the server's to choose: parsing one with 200,000 parameters took over 20 seconds, on every request that drew such a 401 or 403, before the client could act on it.

The walk now uses a StringScanner, which matches each pair at the current position without copying, so the cost grows with the length of the header. The scanner locates the Bearer challenge as well, so a header whose earlier challenge holds multibyte text yields the same parameters as before.

The header is also the server's to fill. A value whose bytes are not valid in the string's encoding, such as a header tagged UTF-8 that carries a stray 0xFF, made the patterns raise ArgumentError from inside the transport's 401 handling, ahead of any AuthorizationError a caller is prepared for; the default adapter hands header values over as ASCII-8BIT, where every byte is valid, so it took a caller-supplied adapter or a header re-tagged along the way. The header is now scrubbed before it is parsed, so such bytes become the replacement character and the other parameters still come through, while a binary header is unchanged by the scrub.

How Has This Been Tested?

New tests in test/mcp/client/oauth/discovery_test.rb parse a header with 200,000 parameters within a bound the previous implementation exceeded many times over, and a header whose earlier challenge holds multibyte text. Two more parse a UTF-8 header with an invalid byte inside a quoted value, which raised against the previous library, and a binary header with a high byte.

Breaking Changes

None.

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

`Discovery.parse_www_authenticate` walked the Bearer challenge's parameters by slicing the header at the cursor and trimming
the slice for every `key=value` pair, which copies the remainder of the header each time. The cost therefore grew with
the square of the header's length, and the header is the server's to choose: parsing one with 200,000 parameters took over 20 seconds,
on every request that drew such a `401` or `403`, before the client could act on it.

The walk now uses a `StringScanner`, which matches each pair at the current position without copying, so the cost grows with
the length of the header. The scanner locates the Bearer challenge as well, so a header whose earlier challenge holds
multibyte text yields the same parameters as before.

The header is also the server's to fill. A value whose bytes are not valid in the string's encoding, such as a header tagged UTF-8
that carries a stray `0xFF`, made the patterns raise `ArgumentError` from inside the transport's `401` handling, ahead of
any `AuthorizationError` a caller is prepared for; the default adapter hands header values over as ASCII-8BIT, where every byte is valid,
so it took a caller-supplied adapter or a header re-tagged along the way. The header is now scrubbed before it is parsed, so such bytes
become the replacement character and the other parameters still come through, while a binary header is unchanged by the scrub.

## How Has This Been Tested?

New tests in `test/mcp/client/oauth/discovery_test.rb` parse a header with 200,000 parameters within a bound the previous implementation
exceeded many times over, and a header whose earlier challenge holds multibyte text. Two more parse a UTF-8 header with an invalid byte
inside a quoted value, which raised against the previous library, and a binary header with a high byte.

## Breaking Changes

None.

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