glanceapp/glance · error

unmarshalling channel shell: %w

Error message

unmarshalling channel shell: %w

What it means

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.

Source

Thrown at internal/glance/widget-twitch-channels.go:158

	request.Header.Add("Client-ID", twitchGqlClientId)

	response, err := decodeJsonFromRequest[[]twitchOperationResponse](defaultHTTPClient, request)
	if err != nil {
		return result, err
	}

	if len(response) != 2 {
		return result, fmt.Errorf("expected 2 operation responses, got %d", len(response))
	}

	var channelShell twitchChannelShellOperationResponse
	var streamMetadata twitchStreamMetadataOperationResponse

	for i := range response {
		switch response[i].Extensions.OperationName {
		case "ChannelShell":
			if err = json.Unmarshal(response[i].Data, &channelShell); err != nil {
				return result, fmt.Errorf("unmarshalling channel shell: %w", err)
			}
		case "StreamMetadata":
			if err = json.Unmarshal(response[i].Data, &streamMetadata); err != nil {
				return result, fmt.Errorf("unmarshalling stream metadata: %w", err)
			}
		default:
			return result, fmt.Errorf("unknown operation name: %s", response[i].Extensions.OperationName)
		}
	}

	if channelShell.UserOrError.Type != "User" {
		result.Name = result.Login
		return result, nil
	}

	result.Exists = true
	result.Name = channelShell.UserOrError.DisplayName
	result.AvatarUrl = channelShell.UserOrError.ProfileImageUrl

View on GitHub (pinned to 91324e8de7)

Solutions

  1. Capture the raw response body and diff it against twitchChannelShellOperationResponse to find the changed field
  2. Update the struct in internal/glance/widget-twitch-channels.go to match the new payload (or relax field types, e.g. json.Number / pointers)
  3. Upgrade Glance to the latest release, which usually pins a fixed persisted query hash and structs
  4. If broken in your fork, verify the persistedQuery hash still resolves; a stale hash can return an error object instead of the expected data

Example fix

// before
if err = json.Unmarshal(response[i].Data, &channelShell); err != nil {
    return result, fmt.Errorf("unmarshalling channel shell: %w", err)
}

// after (diagnose shape drift)
if err = json.Unmarshal(response[i].Data, &channelShell); err != nil {
    slog.Error("ChannelShell payload", "raw", string(response[i].Data))
    return result, fmt.Errorf("unmarshalling channel shell: %w", err)
}
Defensive patterns

Strategy: fallback

Type guard

func isTwitchUnmarshalErr(err error) bool {
	var typeErr *json.UnmarshalTypeError
	return errors.As(err, &typeErr)
}

Try / catch

channels, err := widget.getChannels()
if err != nil {
    if strings.Contains(err.Error(), "unmarshalling channel shell") {
        // Twitch GQL schema drift: render degraded UI, log for maintainers
        slog.Error("twitch schema mismatch", "err", err)
    }
    return err
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of glanceapp/glance@91324e8de7 (2026-08-15). Data as JSON: /api/errors/36afa9af441b6b0c. Report an issue: GitHub.