{"record":{"id":"36afa9af441b6b0c","repo":"glanceapp/glance","slug":"unmarshalling-channel-shell-w","errorCode":null,"errorMessage":"unmarshalling channel shell: %w","messagePattern":"unmarshalling channel shell: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/glance/widget-twitch-channels.go","lineNumber":158,"sourceCode":"\trequest.Header.Add(\"Client-ID\", twitchGqlClientId)\n\n\tresponse, err := decodeJsonFromRequest[[]twitchOperationResponse](defaultHTTPClient, request)\n\tif err != nil {\n\t\treturn result, err\n\t}\n\n\tif len(response) != 2 {\n\t\treturn result, fmt.Errorf(\"expected 2 operation responses, got %d\", len(response))\n\t}\n\n\tvar channelShell twitchChannelShellOperationResponse\n\tvar streamMetadata twitchStreamMetadataOperationResponse\n\n\tfor i := range response {\n\t\tswitch response[i].Extensions.OperationName {\n\t\tcase \"ChannelShell\":\n\t\t\tif err = json.Unmarshal(response[i].Data, &channelShell); err != nil {\n\t\t\t\treturn result, fmt.Errorf(\"unmarshalling channel shell: %w\", err)\n\t\t\t}\n\t\tcase \"StreamMetadata\":\n\t\t\tif err = json.Unmarshal(response[i].Data, &streamMetadata); err != nil {\n\t\t\t\treturn result, fmt.Errorf(\"unmarshalling stream metadata: %w\", err)\n\t\t\t}\n\t\tdefault:\n\t\t\treturn result, fmt.Errorf(\"unknown operation name: %s\", response[i].Extensions.OperationName)\n\t\t}\n\t}\n\n\tif channelShell.UserOrError.Type != \"User\" {\n\t\tresult.Name = result.Login\n\t\treturn result, nil\n\t}\n\n\tresult.Exists = true\n\tresult.Name = channelShell.UserOrError.DisplayName\n\tresult.AvatarUrl = channelShell.UserOrError.ProfileImageUrl","sourceCodeStart":140,"sourceCodeEnd":176,"githubUrl":"https://github.com/glanceapp/glance/blob/91324e8de762702e97b0ac5c8e36271d644d8642/internal/glance/widget-twitch-channels.go#L140-L176","documentation":"Emitted while parsing the 'ChannelShell' part of a Twitch GQL (private GraphQL) batched response in the twitch-channels widget. The widget posts two persisted operations (ChannelShell, StreamMetadata) and json.Unmarshal of the ChannelShell payload into twitchChannelShellOperationResponse failed. It almost always means Twitch changed the shape of that operation's data without notice, since the GQL endpoint is undocumented.","triggerScenarios":"A POST to gql.twitch.tv/gql with the ChannelShell persisted query whose Data no longer matches twitchChannelShellOperationResponse (renamed/removed fields with incompatible types, or Data being null/absent). Triggers on any channel lookup once the schema change rolls out.","commonSituations":"Twitch A/B tests or ships a layout/API change; users on older Glance builds suddenly see all Twitch channels fail with 'Failed to fetch Twitch channel'; Data null when a channel is suspended/deleted.","solutions":["Capture the raw response body and diff it against twitchChannelShellOperationResponse to find the changed field","Update the struct in internal/glance/widget-twitch-channels.go to match the new payload (or relax field types, e.g. json.Number / pointers)","Upgrade Glance to the latest release, which usually pins a fixed persisted query hash and structs","If broken in your fork, verify the persistedQuery hash still resolves; a stale hash can return an error object instead of the expected data"],"exampleFix":"// before\nif err = json.Unmarshal(response[i].Data, &channelShell); err != nil {\n    return result, fmt.Errorf(\"unmarshalling channel shell: %w\", err)\n}\n\n// after (diagnose shape drift)\nif err = json.Unmarshal(response[i].Data, &channelShell); err != nil {\n    slog.Error(\"ChannelShell payload\", \"raw\", string(response[i].Data))\n    return result, fmt.Errorf(\"unmarshalling channel shell: %w\", err)\n}","handlingStrategy":"fallback","validationCode":null,"typeGuard":"func isTwitchUnmarshalErr(err error) bool {\n\tvar typeErr *json.UnmarshalTypeError\n\treturn errors.As(err, &typeErr)\n}","tryCatchPattern":"channels, err := widget.getChannels()\nif err != nil {\n    if strings.Contains(err.Error(), \"unmarshalling channel shell\") {\n        // Twitch GQL schema drift: render degraded UI, log for maintainers\n        slog.Error(\"twitch schema mismatch\", \"err\", err)\n    }\n    return err\n}","preventionTips":["Pin a Glance version and upgrade promptly when Twitch ships breaking GQL changes","Watch the repo's issues for 'Twitch widget broken' reports before debugging yourself","Keep the persisted query hash in sync with the struct definitions when forking"],"tags":["twitch","json","api-change","widget"],"backgroundTag":null,"analyzedSha":"91324e8de762702e97b0ac5c8e36271d644d8642","analyzedAt":"2026-08-15T14:12:54.279Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}