{"record":{"id":"b8f9e4291257aef9","repo":"grafana/k6","slug":"unmarshalling-options-for-sdk-w","errorCode":null,"errorMessage":"unmarshalling options for SDK: %w","messagePattern":"unmarshalling options for SDK: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/cloudapi/provisioning/api.go","lineNumber":201,"sourceCode":"\t}\n}\n\n// StartLocalExecution starts a local-execution test run via the\n// provisioning API. It generates a K6-Idempotency-Key header for\n// safe retries. The caller provides options as pre-marshalled JSON.\nfunc (c *Client) StartLocalExecution(\n\tctx context.Context, loadTestID int64, req StartLocalExecutionRequest,\n) (*StartLocalExecutionResponse, error) {\n\t// Generate idempotency key: 8 random bytes hex-encoded (16 chars).\n\tvar key [8]byte\n\tif _, err := rand.Read(key[:]); err != nil {\n\t\treturn nil, fmt.Errorf(\"generating idempotency key: %w\", err)\n\t}\n\n\t// SDK adapter: unmarshal json.RawMessage → map[string]any.\n\tvar opts map[string]any\n\tif err := json.Unmarshal(req.Options, &opts); err != nil {\n\t\treturn nil, fmt.Errorf(\"unmarshalling options for SDK: %w\", err)\n\t}\n\n\tmaxVUs, err := toInt32(req.MaxVUs)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"max_vus: %w\", err)\n\t}\n\ttotalDuration, err := toInt32(req.TotalDuration)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"total_duration: %w\", err)\n\t}\n\n\tsdkReq := k6cloud.NewStartLocalExecutionTestRequest(opts, maxVUs, totalDuration)\n\n\tif req.ArchiveSize > 0 {\n\t\tv, err := toInt32(req.ArchiveSize)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"archive_size: %w\", err)\n\t\t}","sourceCodeStart":183,"sourceCodeEnd":219,"githubUrl":"https://github.com/grafana/k6/blob/01ffac6f245854c1b8adc6a69857c76a15f90022/internal/cloudapi/provisioning/api.go#L183-L219","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nresp, err := c.StartLocalExecution(ctx, loadTestID, provisioning.StartLocalExecutionRequest{\n\tOptions: rawOpts, // rawOpts assumed valid, may be nil\n})\n\n// after — guarantee the contract at construction\nmarshalled, err := json.Marshal(opts) // opts is lib.Options\nif err != nil {\n\treturn fmt.Errorf(\"marshalling options: %w\", err)\n}\nif !json.Valid(marshalled) {\n\treturn errors.New(\"internal error: marshalled options are not valid JSON\")\n}\nresp, err := c.StartLocalExecution(ctx, loadTestID, provisioning.StartLocalExecutionRequest{Options: marshalled})","handlingStrategy":"validation","validationCode":"// Validate Options is a JSON object before calling StartLocalExecution.\nfunc validJSONObject(raw json.RawMessage) bool {\n\tif !json.Valid(raw) {\n\t\treturn false\n\t}\n\tvar probe map[string]any\n\treturn json.Unmarshal(raw, &probe) == nil\n}\n\nif !validJSONObject(req.Options) {\n\treturn errors.New(\"StartLocalExecutionRequest.Options must be marshalled lib.Options JSON (an object)\")\n}","typeGuard":"func isJSONObject(raw json.RawMessage) bool {\n\ttrimmed := bytes.TrimSpace(raw)\n\treturn len(trimmed) > 0 && trimmed[0] == '{' && json.Valid(trimmed)\n}","tryCatchPattern":"if _, err := c.StartLocalExecution(ctx, id, req); err != nil {\n\tvar synErr *json.SyntaxError\n\tif errors.As(err, &synErr) || strings.Contains(err.Error(), \"cannot unmarshal\") {\n\t\t// caller passed bad Options — fix construction, do not retry\n\t\tlog.Errorf(\"Options payload invalid; first bytes: %.80s\", req.Options)\n\t}\n}","preventionTips":["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."],"tags":["k6","cloud","provisioning","json","options","deserialization"],"backgroundTag":"json-parse-error","analyzedSha":"01ffac6f245854c1b8adc6a69857c76a15f90022","analyzedAt":"2026-08-18T03:05:52.393Z","contentChangedAt":"2026-08-18T03:05:52.393Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}