sipeed/picoclaw · error

invalid JSON: %v

Error message

invalid JSON: %v

What it means

Returned by POST /api/oauth/login when json.Unmarshal of the body into {provider, method, token} fails; %v names the exact JSON defect (syntax, wrong types, trailing data). All three fields are strings; extra fields are ignored, so the failure is always malformed JSON rather than schema mismatch.

Source

Thrown at web/backend/api/oauth.go:187

		"providers": providersResp,
	})
}

func (h *Handler) handleOAuthLogin(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
	if err != nil {
		http.Error(w, "failed to read request body", http.StatusBadRequest)
		return
	}
	defer r.Body.Close()

	var req struct {
		Provider string `json:"provider"`
		Method   string `json:"method"`
		Token    string `json:"token"`
	}
	if err = json.Unmarshal(body, &req); err != nil {
		http.Error(w, fmt.Sprintf("invalid JSON: %v", err), http.StatusBadRequest)
		return
	}

	provider, err := normalizeOAuthProvider(req.Provider)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	method := strings.ToLower(strings.TrimSpace(req.Method))
	if !isOAuthMethodSupported(provider, method) {
		http.Error(
			w,
			fmt.Sprintf("unsupported login method %q for provider %q", method, provider),
			http.StatusBadRequest,
		)
		return
	}

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Send JSON.stringify({provider: 'anthropic', method: 'token', token: tsk}) with Content-Type: application/json
  2. Use the %v offset — it points at the offending byte in the body
  3. If migrating from a form client, convert FormData to a plain object before stringify

Example fix

// before
body: new FormData(loginForm) // sends multipart, not JSON

// after
body: JSON.stringify({
  provider: 'anthropic',
  method: 'token',
  token: tokenInput.value,
})
Defensive patterns

Strategy: validation

Validate before calling

function buildLoginPayload(provider: string, method: string, token: string): string {
  for (const [k, v] of Object.entries({ provider, method, token })) {
    if (typeof v !== 'string') throw new Error(`${k} must be a string`);
  }
  return JSON.stringify({ provider, method, token });
}

Type guard

function isOAuthLoginBody(v: unknown): v is { provider: string; method: string; token: string } {
  return typeof v === 'object' && v !== null
    && typeof (v as any).provider === 'string'
    && typeof (v as any).method === 'string'
    && typeof (v as any).token === 'string';
}

Try / catch

try {
  const res = await fetch('/api/oauth/login', {...});
  if (res.status === 400 && (await res.text()).startsWith('invalid JSON')) {
    /* %v points at the bad byte — JSON.parse locally to find and fix it */
  }
} catch (e) { /* network */ }

Prevention

When it happens

Trigger: POST /api/oauth/login with unquoted keys, {"method": 5} (number vs string), double-encoded JSON, or a body like 'provider=openai' (form-encoded without JSON conversion).

Common situations: Sending FormData/x-www-form-urlencoded directly; JSON.stringify applied twice; hand-built curl with shell-mangled quotes; token containing an unescaped quote breaking a template-built string.

Understand the failure class

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/75fecb5ea120b49d. Report an issue: GitHub.