{"record":{"id":"ea2e241b06cedffe","repo":"gastownhall/beads","slug":"failed-to-marshal-request-body-w","errorCode":null,"errorMessage":"failed to marshal request body: %w","messagePattern":"failed to marshal request body: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/gitlab/client.go","lineNumber":106,"sourceCode":"\n\tif len(params) > 0 {\n\t\tvalues := url.Values{}\n\t\tfor k, v := range params {\n\t\t\tvalues.Set(k, v)\n\t\t}\n\t\tu += \"?\" + values.Encode()\n\t}\n\n\treturn u\n}\n\n// doRequest performs an HTTP request with authentication and retry logic.\nfunc (c *Client) doRequest(ctx context.Context, method, urlStr string, body interface{}) ([]byte, http.Header, error) {\n\tvar reqBody io.Reader\n\tif body != nil {\n\t\tjsonBody, err := json.Marshal(body)\n\t\tif err != nil {\n\t\t\treturn nil, nil, fmt.Errorf(\"failed to marshal request body: %w\", err)\n\t\t}\n\t\treqBody = bytes.NewReader(jsonBody)\n\t}\n\n\tvar lastErr error\n\tfor attempt := 0; attempt <= MaxRetries; attempt++ {\n\t\t// Reset body reader at top of loop so retries after network errors\n\t\t// don't send empty bodies (the reader may be at EOF).\n\t\tif body != nil {\n\t\t\tjsonBody, err := json.Marshal(body)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, nil, fmt.Errorf(\"failed to marshal request body: %w\", err)\n\t\t\t}\n\t\t\treqBody = bytes.NewReader(jsonBody)\n\t\t}\n\n\t\treq, err := http.NewRequestWithContext(ctx, method, urlStr, reqBody)\n\t\tif err != nil {","sourceCodeStart":88,"sourceCodeEnd":124,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/gitlab/client.go#L88-L124","documentation":"doRequest serializes the request body to JSON before sending. If the supplied body struct cannot be marshaled (unsupported types like channels, funcs, or cyclic references), the request is aborted locally and this wrapped error is returned. In practice it is nearly impossible to trigger because bodies are plain structs.","triggerScenarios":"Calling any Client method (CreateIssue, UpdateIssue, etc.) with a body containing fields that encoding/json cannot marshal: channels, funcs, cycles, or invalid types in custom struct fields passed via extension points.","commonSituations":"Forking/monkey-patching the client with a body struct containing unexported-but-problematic types; embedding a sync.Mutex by value is fine, but embedding a channel or a func-typed field without json tags pointing at it; hand-building request bodies in tests.","solutions":["Inspect the body struct for channel/func/cyclic fields and remove or tag them `json:\"-\"`","Ensure all body fields are JSON-serializable types (string, int, slices, maps, pointers)","If passing a custom body, validate with json.Marshal in a unit test before the real call","Update to the latest version in case a field type was changed upstream"],"exampleFix":"// before\ntype update struct{ Hook func() `json:\"hook\"` }\nclient.UpdateIssue(ctx, 42, update{Hook: fn}) // marshal fails\n// after\ntype update struct{ Hook func() `json:\"-\"` }\nclient.UpdateIssue(ctx, 42, update{Hook: fn})","handlingStrategy":"validation","validationCode":"if _, err := json.Marshal(body); err != nil {\n\treturn fmt.Errorf(\"body not JSON-marshalable: %w\", err)\n}","typeGuard":"func jsonSafe(v interface{}) bool {\n\tswitch v.(type) { case chan struct{}, func(): return false }\n\treturn json.NewMarshalableCheck(v) == nil // or: json.Marshal(v) == nil error\n}","tryCatchPattern":"if err != nil && strings.Contains(err.Error(), \"failed to marshal request body\") {\n\tlog.Fatalf(\"programmer error: request body is not JSON-serializable: %v\", err)\n}","preventionTips":["Keep request body structs to plain JSON types; tag non-serializable fields with `json:\"-\"`","Add a test that marshals every client request struct","Avoid embedding channels, funcs, or self-referencing pointers in payload types"],"tags":["json","serialization","gitlab"],"backgroundTag":"json-marshal-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}