fix(client/oauth): preserve token endpoint error diagnostics - #549
fix(client/oauth): preserve token endpoint error diagnostics#549junyuanz1 wants to merge 1 commit into
Conversation
| nil | ||
| # RFC 6749 permits printable ASCII except double quotes and backslashes. | ||
| # Replace other characters to keep provider text on one log line. | ||
| value = value.gsub(/[^\x20-\x21\x23-\x5B\x5D-\x7E]/, " ").strip |
There was a problem hiding this comment.
JSON.parse keeps invalid UTF-8 inside a JSON string (the result is a UTF-8 String with valid_encoding? false), and this gsub then raises ArgumentError: invalid byte sequence in UTF-8 while the exception is still being built. The rescue JSON::ParserError above does not cover it, nor do the InvalidGrantError / AuthorizationError rescues in MCP::Client::HTTP's refresh path, so one invalid byte in error_description turns a refresh failure into an ArgumentError out of the client call and skips the refresh-recovery decision. main never touches the description, so this is new here; it reproduces on Ruby 3.4.5 / json 2.19.7 and Ruby 4.0.6 / json 3.0.2:
value = JSON.parse("{\"error\":\"invalid_grant\",\"error_description\":\"bad \xFF byte\"}".b)["error_description"]
value.gsub(/[^\x20-\x21\x23-\x5B\x5D-\x7E]/, " ") # => ArgumentError: invalid byte sequence in UTF-8Scrub first, as the server-side Challenge#quote does for the same reason:
| value = value.gsub(/[^\x20-\x21\x23-\x5B\x5D-\x7E]/, " ").strip | |
| value = value.scrub(" ").gsub(/[^\x20-\x21\x23-\x5B\x5D-\x7E]/, " ").strip |
Also wrap the diagnostic extraction in token_endpoint_error with a broader rescue that falls back to the status-only message, so building the exception can never raise, and add tests: error exactly "invalid_grant" with an invalid byte in error_description still raises InvalidGrantError with the sanitized description, and invalid bytes in both fields still yield an AuthorizationError.
| ) | ||
| ``` | ||
|
|
||
| ### Token endpoint errors |
There was a problem hiding this comment.
| ### Token endpoint errors | |
| ### Token Endpoint Errors |
| error = token_endpoint_diagnostic(parsed["error"], limit: 128) | ||
| description = token_endpoint_diagnostic(parsed["error_description"], limit: 512) |
There was a problem hiding this comment.
These limits define behavior the docs state; please name them next to the class's other constants so the code and the docs point at one place.
| error = token_endpoint_diagnostic(parsed["error"], limit: 128) | |
| description = token_endpoint_diagnostic(parsed["error_description"], limit: 512) | |
| error = token_endpoint_diagnostic(parsed["error"], limit: TOKEN_ENDPOINT_ERROR_MAX_LENGTH) | |
| description = token_endpoint_diagnostic(parsed["error_description"], limit: TOKEN_ENDPOINT_ERROR_DESCRIPTION_MAX_LENGTH) |
Token endpoint failures currently discard the OAuth error description, leaving callers with only an HTTP status. For example, Notion's
invalid_requestresponse explaining that a client used multiple authentication methods becomes onlyToken endpoint returned status 400., forcing callers to replay the request to diagnose it.Preserve RFC 6749 §5.2
erroranderror_descriptionin the exception message and exposehttp_status,error, anderror_descriptionreaders. KeepInvalidGrantErrorclassification and refresh recovery behavior unchanged. Malformed responses retain the status-only fallback.Only the two diagnostic fields are included; other response fields and the raw body are excluded. Normalize text to the RFC's printable ASCII set and cap error codes at 128 characters and descriptions at 512. Document that provider-controlled descriptions may still contain sensitive information and require application logging policies.
Validation:
git diff --checkpassed.