{"record":{"id":"e680b05fa9517107","repo":"AlexxIT/go2rtc","slug":"ring-invalid-refresh-token-encoding-w","errorCode":null,"errorMessage":"ring: invalid refresh token encoding: %w","messagePattern":"ring: invalid refresh token encoding: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/ring/client.go","lineNumber":53,"sourceCode":"\tdeviceID := query.Get(\"device_id\")\n\t_, isSnapshot := query[\"snapshot\"]\n\n\tif encodedToken == \"\" || deviceID == \"\" || cameraID == \"\" {\n\t\treturn nil, errors.New(\"ring: wrong query\")\n\t}\n\n\tclient := &Client{\n\t\tdialogID: uuid.NewString(),\n\t}\n\n\tclient.cameraID, err = strconv.Atoi(cameraID)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"ring: invalid camera_id: %w\", err)\n\t}\n\n\trefreshToken, err := url.QueryUnescape(encodedToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"ring: invalid refresh token encoding: %w\", err)\n\t}\n\n\tclient.api, err = NewRestClient(RefreshTokenAuth{RefreshToken: refreshToken}, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Snapshot Flow\n\tif isSnapshot {\n\t\tclient.prod = NewSnapshotProducer(client.api, client.cameraID)\n\t\treturn client, nil\n\t}\n\n\tclient.wsClient, err = StartWebsocket(client.cameraID, client.api)\n\tif err != nil {\n\t\tclient.Stop()\n\t\treturn nil, err\n\t}","sourceCodeStart":35,"sourceCodeEnd":71,"githubUrl":"https://github.com/AlexxIT/go2rtc/blob/c245815e75e2a5fd60b4290f12bfc04e55a984d3/pkg/ring/client.go#L35-L71","documentation":"Dial() decodes the refresh token with url.QueryUnescape before authenticating with the Ring REST API. If encodedToken contains a malformed percent-escape (e.g. a lone '%' or invalid hex digits), QueryUnescape fails and Dial returns this wrapped error. The token must be URL-encoded when stored and is decoded once here.","triggerScenarios":"Calling ring.Dial with an encodedToken string containing invalid percent-encoding such as \"%ZZ\", a trailing \"%\", or a token that was double-encoded/corrupted in storage.","commonSituations":"Manually copying a refresh token and truncating it mid-escape; double-encoding a token that is already plain; storing the token in a config system that mangles '%' characters (e.g. templating engines that treat % specially).","solutions":["Inspect the token for '%' sequences and ensure each is followed by two valid hex digits.","If the token is already plain text (not URL-encoded), pass it without additional encoding rather than escaping again.","Re-obtain a fresh refresh token from the Ring auth flow and store it uncorrupted.","If your config layer expands '%', escape or disable interpolation for that value."],"exampleFix":"// before\nc, err := ring.Dial(ctx, camID, \"abc%ZZdef\")\n// after\ntoken := url.QueryEscape(refreshToken) // or use the raw, correctly-encoded token\nc, err := ring.Dial(ctx, camID, token)","handlingStrategy":"validation","validationCode":"if encodedToken == \"\" {\n    return errors.New(\"refresh token must not be empty\")\n}\nif _, err := url.QueryUnescape(encodedToken); err != nil {\n    return fmt.Errorf(\"refresh token has invalid percent-encoding: %v\", err)\n}","typeGuard":null,"tryCatchPattern":"client, err := ring.Dial(ctx, cameraID, encodedToken)\nif err != nil {\n    if strings.Contains(err.Error(), \"invalid refresh token encoding\") {\n        // re-fetch or re-store the token before retrying\n    }\n    return err\n}","preventionTips":["Store refresh tokens exactly as issued; do not double-encode.","Escape '%' characters if your config/templating layer interprets them.","Regenerate the token if it appears truncated or corrupted in storage."],"tags":["ring","authentication","url-encoding","token"],"backgroundTag":"invalid-url-format","analyzedSha":"c245815e75e2a5fd60b4290f12bfc04e55a984d3","analyzedAt":"2026-09-07T11:47:02.965Z","contentChangedAt":"2026-09-07T11:47:02.965Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}