Kuberwastaken/claurst · error

Remote settings: unexpected response shape

Error message

Remote settings: unexpected response shape

What it means

The remote-settings response body parsed neither as the expected RemoteSettingsResponse wrapper object nor as a bare settings object (the parser is deliberately permissive and tries both shapes). The server returned 200 but with a JSON body that fits neither contract.

Solutions

  1. Log or curl the raw response body to see what the endpoint actually returned
  2. Confirm the settings endpoint version matches what this client expects
  3. Fall back to cached/local settings until the server shape is fixed
Defensive patterns

Strategy: fallback

When it happens

Trigger: Thrown at src-rust/crates/core/src/remote_settings.rs:239 when the library encounters an invalid state.

Common situations: See trigger scenarios.


AI-assisted analysis of Kuberwastaken/claurst@b0637c97ec (2026-09-10). Data as JSON: /api/errors/ee884e3995a7d3a1. Report an issue: GitHub.

Appendix: source

Thrown at src-rust/crates/core/src/remote_settings.rs:239

                anyhow::bail!("Remote settings: not authorized ({})", status);
            }
            other => {
                anyhow::bail!("Remote settings: unexpected status {}", other);
            }
        }

        let body: Value = resp.json().await?;

        // Try to parse as the expected response shape, but be permissive —
        // accept raw settings object if the wrapper is missing.
        let (settings, _checksum) = if let Ok(parsed) =
            serde_json::from_value::<RemoteSettingsResponse>(body.clone())
        {
            (parsed.settings, parsed.checksum)
        } else if body.is_object() {
            (body, None)
        } else {
            anyhow::bail!("Remote settings: unexpected response shape");
        };

        Ok(Some(settings))
    }

    /// Fetch settings with exponential-backoff retry.
    ///
    /// On success returns the new settings value (or `None` for 304).
    /// On failure after all retries, returns the last error.
    pub async fn fetch_with_retry(&self, cached_checksum: Option<&str>) -> Result<Option<Value>> {
        let mut last_err = anyhow::anyhow!("No attempts made");
        for attempt in 1..=(DEFAULT_MAX_RETRIES + 1) {
            match self.fetch_once(cached_checksum).await {
                Ok(v) => return Ok(v),
                Err(e) => {
                    // Auth errors: don't retry
                    let msg = e.to_string();
                    if msg.contains("not authorized") || msg.contains("No authentication") {

View on GitHub (pinned to b0637c97ec)