router-for-me/CLIProxyAPI · error

Codex live multipart body requires an sdp field

Error message

Codex live multipart body requires an sdp field

What it means

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.

Source

Thrown at internal/client/codex/live/live.go:659

		}
		if errClose != nil {
			return nil, "", "", fmt.Errorf("failed to close Codex live multipart field: %w", errClose)
		}

		switch part.FormName() {
		case "sdp":
			value := string(partBody)
			sdp = &value
		case "session":
			if !json.Valid(partBody) {
				return nil, "", "", errors.New("Codex live session field must contain valid JSON")
			}
			session = append(json.RawMessage(nil), partBody...)
			model = modelFromJSON(partBody)
		}
	}
	if sdp == nil {
		return nil, "", "", errors.New("Codex live multipart body requires an sdp field")
	}
	if model == "" {
		model = defaultLiveModel
	}

	encoded, errEncode := encodeCallRequest(*sdp, session)
	if errEncode != nil {
		return nil, "", "", errEncode
	}
	return encoded, "application/json", model, nil
}

func encodeCallRequest(sdp string, session json.RawMessage) ([]byte, error) {
	payload := struct {
		SDP     string          `json:"sdp"`
		Session json.RawMessage `json:"session,omitempty"`
	}{
		SDP:     sdp,

View on GitHub (pinned to 78f0c4079e)

Solutions

  1. Always check the error from the call that returns the StreamID before issuing reads.
  2. Fail fast in the plugin if the returned StreamID is empty.
  3. Keep the stream lifecycle (open → read* → close) in one function so the ID cannot be lost.

Example fix

// before
resp, _ := host.Call(ctx, streamOpenMethod, raw) // error ignored
readReq, _ := json.Marshal(pluginapi.HTTPStreamReadRequest{StreamID: resp.StreamID}) // "" if resp failed

// after
respRaw, err := host.Call(ctx, streamOpenMethod, raw)
if err != nil {
    return err
}
var resp pluginapi.HTTPStreamResponse
if err := json.Unmarshal(respRaw, &resp); err != nil || resp.StreamID == "" {
    return fmt.Errorf("no stream id from open: %v", err)
}
Defensive patterns

Strategy: validation

Validate before calling

if streamID == "" {
    return errors.New("http stream id is required")
}

Type guard

func hasStreamID(id string) bool { return strings.TrimSpace(id) != "" }

Try / catch

_, _, err := bridge.Read(ctx, id)
if err != nil && strings.Contains(err.Error(), "stream id is required") {
    return errors.New("programming error: read called without an open stream")
}

Prevention

When it happens

Trigger: 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.

Common situations: Error handling that continues after a failed stream open; copying example code that stubs the ID; schema drift where the response field name changed.

Related errors


AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15). Data as JSON: /api/errors/7afc8efb4848489c. Report an issue: GitHub.