{"record":{"id":"240b21d249954de3","repo":"basecamp/fizzy","slug":"client-data-is-not-valid-json","errorCode":null,"errorMessage":"Client data is not valid JSON","messagePattern":"Client data is not valid JSON","errorType":"exception","errorClass":"ActionPack::WebAuthn::InvalidResponseError","httpStatus":null,"severity":"error","filePath":"lib/action_pack/web_authn/authenticator/response.rb","lineNumber":69,"sourceCode":"  end\n\n  def validate!\n    super\n  rescue ActiveModel::ValidationError\n    raise ActionPack::WebAuthn::InvalidResponseError, errors.full_messages.join(\", \")\n  end\n\n  # Returns the RelyingParty used for RP ID validation.\n  def relying_party\n    ActionPack::WebAuthn.relying_party\n  end\n\n  # Parses the client data JSON string into a Hash. Raises\n  # +InvalidResponseError+ if the JSON is malformed.\n  def client_data\n    @client_data ||= JSON.parse(client_data_json)\n  rescue JSON::ParserError\n    raise ActionPack::WebAuthn::InvalidResponseError, \"Client data is not valid JSON\"\n  end\n\n  def authenticator_data\n    nil\n  end\n\n  private\n    def challenge_must_be_present\n      if client_data[\"challenge\"].blank?\n        errors.add(:base, \"Challenge missing\")\n      end\n    end\n\n    def challenge_must_not_be_expired\n      return if errors.any?\n\n      signed_message = Base64.urlsafe_decode64(client_data[\"challenge\"])\n","sourceCodeStart":51,"sourceCodeEnd":87,"githubUrl":"https://github.com/basecamp/fizzy/blob/7aabe7458060d8a1759a53b7ede39e74e6c0b20d/lib/action_pack/web_authn/authenticator/response.rb#L51-L87","documentation":"Authenticator::Response#client_data runs JSON.parse on the clientDataJSON string handed to AttestationResponse/AssertionResponse. JSON::ParserError is re-raised as InvalidResponseError. Note that the browser's clientDataJSON is plain UTF-8 JSON (not base64), so any wrapping or partial decoding of it on the server will produce this error.","triggerScenarios":"Constructing a Response with client_data_json that is base64-encoded JSON, an empty string, an HTML error page captured by a proxy, a truncated body, or a Ruby-inspect artifact like \\\"{\\\"type\\\":...}\\\" from string interpolation.","commonSituations":"Teams that uniformly base64-encode all WebAuthn fields client-side and forget to decode clientDataJSON server-side; fetch wrappers that .text() a failed request and pass the error page along; truncation by middleware; test fixtures storing clientDataJSON already-decoded once.","solutions":["Pass the browser's response.clientDataJSON string through verbatim — do not base64-decode it server-side.","If your transport base64-encodes everything, decode that one field first: JSON.parse(Base64.urlsafe_decode64(value)).","Pre-validate at the boundary with JSON.parse(value) and return 400 early so the failure names the field.","Log the first 80 chars of the received value — HTML tags or base64 patterns identify the mangling source fast."],"exampleFix":"# before\nresponse = ActionPack::WebAuthn::Authenticator::AssertionResponse.new(\n  client_data_json: params[:client_data_json] # arrives base64-encoded\n)\n\n# after — decode only if your client encoded it, then hand over clean JSON\njson = params[:client_data_json]\njson = Base64.urlsafe_decode64(json) if json.match?(/^[A-Za-z0-9_-]+={0,2}$/) && !json.strip.start_with?('{')\nresponse = ActionPack::WebAuthn::Authenticator::AssertionResponse.new(client_data_json: json)","handlingStrategy":"validation","validationCode":"json = params[:client_data_json].to_s\nbegin\n  JSON.parse(json)\nrescue JSON::ParserError\n  return render(json: { error: 'client_data_json must be raw JSON' }, status: :bad_request)\nend","typeGuard":"def valid_client_data_json?(value)\n  JSON.parse(value.to_s)\n  true\nrescue JSON::ParserError\n  false\nend","tryCatchPattern":"begin\n  response = ActionPack::WebAuthn::Authenticator::AssertionResponse.new(client_data_json: json, **rest)\n  response.validate!\nrescue ActionPack::WebAuthn::InvalidResponseError => e\n  render json: { error: e.message }, status: :bad_request\nend","preventionTips":["Send clientDataJSON as plain UTF-8 JSON; base64-encode only binary fields (authenticatorData, signature).","If the transport base64-encodes everything, decode that one field before constructing the Response.","Add a JSON.parse smoke check in request specs for every WebAuthn endpoint."],"tags":["webauthn","json","client-data","passkeys"],"backgroundTag":"webauthn-invalid-client-data","analyzedSha":"7aabe7458060d8a1759a53b7ede39e74e6c0b20d","analyzedAt":"2026-08-21T18:33:25.349Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}