Skip to content

Fix TypeError on 403 OAuth error body with a string#146

Open
orthodoX wants to merge 1 commit into
patvice:mainfrom
orthodoX:fix-oauth-403-string-error
Open

Fix TypeError on 403 OAuth error body with a string#146
orthodoX wants to merge 1 commit into
patvice:mainfrom
orthodoX:fix-oauth-403-string-error

Conversation

@orthodoX

Copy link
Copy Markdown

Summary

A 403 from an OAuth-protected MCP server crashes the StreamableHTTP transport with
TypeError: String does not have #dig method instead of producing a usable error. This
turns token rejected signal into a crash that surfaces far from its
cause (the transport runs the request on a background thread, so the original backtrace is
lost and the re-raised error is a bare TransportError).

Root cause

handle_oauth_authorization_error assumes the 403 body is a JSON-RPC error and digs two
levels in:

error_message = error_body.dig("error", "message") || "Authorization failed"

But when a server's OAuth/resource layer rejects the token, the body is an RFC 6749 OAuth
error response
(§5.2), where
error is a string, not a nested object:

{ "error": "invalid_token", "error_description": "The access token expired" }

Hash#dig("error", "message") then calls #dig on the String value of error and raises
TypeError: String does not have #dig method. The method's only rescue is
rescue JSON::ParserError, so the TypeError escapes uncaught.

This path is reached specifically for status_code == 403 && @oauth_provider when there is no
www-authenticate challenge header (handle_authorization_challenge
handle_client_errorhandle_oauth_authorization_error), which is exactly how many OAuth
resource servers respond to an expired/invalid token.

Reproduction

transport = RubyLLM::MCP::Native::Transports::StreamableHTTP.allocate
resp = Struct.new(:body).new('{"error":"invalid_token","error_description":"expired"}')
transport.send(:handle_oauth_authorization_error, resp, 403)
# Before: TypeError: String does not have #dig method
# After:  RubyLLM::MCP::Errors::TransportError:
#         Authorization failed (403 Forbidden). expired. Check token scope and permissions.

Backwards compatibility

None broken. The exception class and message format are preserved; the JSON-RPC branch
behaves exactly as before. The only change is that the previously-crashing OAuth-shaped body
now yields a normal TransportError.

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.

1 participant