{"record":{"id":"7afc8efb4848489c","repo":"router-for-me/CLIProxyAPI","slug":"codex-live-multipart-body-requires-an-sdp-field","errorCode":null,"errorMessage":"Codex live multipart body requires an sdp field","messagePattern":"Codex live multipart body requires an sdp field","errorType":"validation","errorClass":null,"httpStatus":400,"severity":"error","filePath":"internal/client/codex/live/live.go","lineNumber":659,"sourceCode":"\t\t}\n\t\tif errClose != nil {\n\t\t\treturn nil, \"\", \"\", fmt.Errorf(\"failed to close Codex live multipart field: %w\", errClose)\n\t\t}\n\n\t\tswitch part.FormName() {\n\t\tcase \"sdp\":\n\t\t\tvalue := string(partBody)\n\t\t\tsdp = &value\n\t\tcase \"session\":\n\t\t\tif !json.Valid(partBody) {\n\t\t\t\treturn nil, \"\", \"\", errors.New(\"Codex live session field must contain valid JSON\")\n\t\t\t}\n\t\t\tsession = append(json.RawMessage(nil), partBody...)\n\t\t\tmodel = modelFromJSON(partBody)\n\t\t}\n\t}\n\tif sdp == nil {\n\t\treturn nil, \"\", \"\", errors.New(\"Codex live multipart body requires an sdp field\")\n\t}\n\tif model == \"\" {\n\t\tmodel = defaultLiveModel\n\t}\n\n\tencoded, errEncode := encodeCallRequest(*sdp, session)\n\tif errEncode != nil {\n\t\treturn nil, \"\", \"\", errEncode\n\t}\n\treturn encoded, \"application/json\", model, nil\n}\n\nfunc encodeCallRequest(sdp string, session json.RawMessage) ([]byte, error) {\n\tpayload := struct {\n\t\tSDP     string          `json:\"sdp\"`\n\t\tSession json.RawMessage `json:\"session,omitempty\"`\n\t}{\n\t\tSDP:     sdp,","sourceCodeStart":641,"sourceCodeEnd":677,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/client/codex/live/live.go#L641-L677","documentation":"The HTTP stream bridge's read rejects a call whose stream id is empty (or the bridge receiver is nil). IDs are minted by open() as incrementing decimal counters, so an empty id means the caller never obtained one — it is a programming error in the RPC sequence, not a runtime race.","triggerScenarios":"Plugin calls host http stream read with an empty/zero StreamID, typically because it ignored an error from the open/execute step and used the zero-value field, or lost the ID variable.","commonSituations":"Error handling that continues after a failed stream open; copying example code that stubs the ID; schema drift where the response field name changed.","solutions":["Always check the error from the call that returns the StreamID before issuing reads.","Fail fast in the plugin if the returned StreamID is empty.","Keep the stream lifecycle (open → read* → close) in one function so the ID cannot be lost."],"exampleFix":"// before\nresp, _ := host.Call(ctx, streamOpenMethod, raw) // error ignored\nreadReq, _ := json.Marshal(pluginapi.HTTPStreamReadRequest{StreamID: resp.StreamID}) // \"\" if resp failed\n\n// after\nrespRaw, err := host.Call(ctx, streamOpenMethod, raw)\nif err != nil {\n    return err\n}\nvar resp pluginapi.HTTPStreamResponse\nif err := json.Unmarshal(respRaw, &resp); err != nil || resp.StreamID == \"\" {\n    return fmt.Errorf(\"no stream id from open: %v\", err)\n}","handlingStrategy":"validation","validationCode":"if streamID == \"\" {\n    return errors.New(\"http stream id is required\")\n}","typeGuard":"func hasStreamID(id string) bool { return strings.TrimSpace(id) != \"\" }","tryCatchPattern":"_, _, err := bridge.Read(ctx, id)\nif err != nil && strings.Contains(err.Error(), \"stream id is required\") {\n    return errors.New(\"programming error: read called without an open stream\")\n}","preventionTips":["Check the open-call error and non-empty StreamID before any read.","Scope the stream ID to the function that opened the stream.","Fail fast on empty IDs instead of sending them across the RPC boundary."],"tags":["pluginhost","streaming","http","validation"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}