grafana/k6 · error
unmarshalling options for SDK
Error message
unmarshalling options for SDK: %w
What it means
Returned by Client.StartLocalExecution (internal/cloudapi/provisioning/api.go:201) when json.Unmarshal cannot decode req.Options (a json.RawMessage of pre-marshalled lib.Options) into map[string]any before handing it to the cloud SDK. It fails when Options is empty/nil ('unexpected end of JSON input'), is valid JSON but not an object (array/string/number → type error), or contains a syntax error. This is a caller-side contract violation: the field must be the output of marshalling lib.Options.
Solutions
- Ensure req.Options is produced by json.Marshal of a lib.Options value (or at minimum is a valid JSON object) before calling StartLocalExecution.
- If Options can legitimately be absent, pass explicit 'null' or '{}' consistently with what the backend expects rather than an empty slice.
- Add a json.Valid(req.Options) plus object-shape check at the construction site to fail with a clearer message (see defense code).
- Log the first ~200 bytes of Options on failure to spot double-encoding or truncation.
Example fix
// before
resp, err := c.StartLocalExecution(ctx, loadTestID, provisioning.StartLocalExecutionRequest{
Options: rawOpts, // rawOpts assumed valid, may be nil
})
// after — guarantee the contract at construction
marshalled, err := json.Marshal(opts) // opts is lib.Options
if err != nil {
return fmt.Errorf("marshalling options: %w", err)
}
if !json.Valid(marshalled) {
return errors.New("internal error: marshalled options are not valid JSON")
}
resp, err := c.StartLocalExecution(ctx, loadTestID, provisioning.StartLocalExecutionRequest{Options: marshalled}) Defensive patterns
Strategy: validation
Validate before calling
// Validate Options is a JSON object before calling StartLocalExecution.
func validJSONObject(raw json.RawMessage) bool {
if !json.Valid(raw) {
return false
}
var probe map[string]any
return json.Unmarshal(raw, &probe) == nil
}
if !validJSONObject(req.Options) {
return errors.New("StartLocalExecutionRequest.Options must be marshalled lib.Options JSON (an object)")
} Type guard
func isJSONObject(raw json.RawMessage) bool {
trimmed := bytes.TrimSpace(raw)
return len(trimmed) > 0 && trimmed[0] == '{' && json.Valid(trimmed)
} Try / catch
if _, err := c.StartLocalExecution(ctx, id, req); err != nil {
var synErr *json.SyntaxError
if errors.As(err, &synErr) || strings.Contains(err.Error(), "cannot unmarshal") {
// caller passed bad Options — fix construction, do not retry
log.Errorf("Options payload invalid; first bytes: %.80s", req.Options)
}
} Prevention
- Always build Options via json.Marshal of a lib.Options value; never hand-assemble strings.
- Centralize request construction in one helper so all call sites share the validated path.
- Round-trip check in tests: Unmarshal(Marshal(opts), &map[string]any{}) must succeed.
When it happens
Trigger: Calling StartLocalExecution with Options nil or empty; passing JSON that is an array or scalar rather than an object; passing already-double-encoded JSON (a quoted string containing JSON); passing truncated marshal output.
Common situations: New call sites in cmd/ or xk6 extensions building StartLocalExecutionRequest by hand instead of via json.Marshal(lib.Options); refactors that pass an intermediate struct; tests with hand-written option strings.
Understand the failure class
Background: JSON parse error: "Unexpected token" / "not valid JSON" / "failed to parse" — what JSON parsers are really complaining about — this error's family across 45 libraries.
Related errors
AI-assisted analysis of grafana/k6@01ffac6f24 (2026-08-18).
Data as JSON: /api/errors/b8f9e4291257aef9.
Report an issue: GitHub.
Appendix: source
Thrown at internal/cloudapi/provisioning/api.go:201
}
}
// StartLocalExecution starts a local-execution test run via the
// provisioning API. It generates a K6-Idempotency-Key header for
// safe retries. The caller provides options as pre-marshalled JSON.
func (c *Client) StartLocalExecution(
ctx context.Context, loadTestID int64, req StartLocalExecutionRequest,
) (*StartLocalExecutionResponse, error) {
// Generate idempotency key: 8 random bytes hex-encoded (16 chars).
var key [8]byte
if _, err := rand.Read(key[:]); err != nil {
return nil, fmt.Errorf("generating idempotency key: %w", err)
}
// SDK adapter: unmarshal json.RawMessage → map[string]any.
var opts map[string]any
if err := json.Unmarshal(req.Options, &opts); err != nil {
return nil, fmt.Errorf("unmarshalling options for SDK: %w", err)
}
maxVUs, err := toInt32(req.MaxVUs)
if err != nil {
return nil, fmt.Errorf("max_vus: %w", err)
}
totalDuration, err := toInt32(req.TotalDuration)
if err != nil {
return nil, fmt.Errorf("total_duration: %w", err)
}
sdkReq := k6cloud.NewStartLocalExecutionTestRequest(opts, maxVUs, totalDuration)
if req.ArchiveSize > 0 {
v, err := toInt32(req.ArchiveSize)
if err != nil {
return nil, fmt.Errorf("archive_size: %w", err)
}View on GitHub (pinned to 01ffac6f24)