Billionmail/BillionMail · error

marshal lipsync request: %w

Error message

marshal lipsync request: %w

What it means

BuildLipSyncRequest JSON-encodes the lipsync API request body before constructing the http.Request. If json.Marshal fails on the request struct, the error is wrapped as 'marshal lipsync request: %w'. This is rare for this struct (plain strings/fields), but can happen if unsupported types (channels, funcs, NaN floats) sneak into the payload.

Source

Thrown at core/internal/service/video_gen/lipsync.go:77

// DefaultLipSyncConfig returns config with API key from env.
func DefaultLipSyncConfig(outputDir string) LipSyncConfig {
	return LipSyncConfig{
		APIKey:    os.Getenv("SYNCLABS_API_KEY"),
		OutputDir: outputDir,
	}
}

// BuildLipSyncRequest constructs the HTTP request for lip sync generation.
// Exported for testing without making API calls.
func BuildLipSyncRequest(cfg LipSyncConfig, req LipSyncRequest) (*http.Request, error) {
	if req.Model == "" {
		req.Model = "sync-1.7.1-beta"
	}

	body, err := json.Marshal(req)
	if err != nil {
		return nil, fmt.Errorf("marshal lipsync request: %w", err)
	}

	httpReq, err := http.NewRequest("POST", cfg.lipSyncBase()+lipSyncPath, bytes.NewReader(body))
	if err != nil {
		return nil, fmt.Errorf("create lipsync request: %w", err)
	}

	httpReq.Header.Set("x-api-key", cfg.APIKey)
	httpReq.Header.Set("Content-Type", "application/json")
	return httpReq, nil
}

// BuildLipSyncStatusRequest constructs the HTTP request to check job status.
// Exported for testing.
func BuildLipSyncStatusRequest(cfg LipSyncConfig, jobID string) (*http.Request, error) {
	url := fmt.Sprintf("%s%s/%s", cfg.lipSyncBase(), lipSyncPath, jobID)
	httpReq, err := http.NewRequest("GET", url, nil)
	if err != nil {

View on GitHub (pinned to fc36c76c05)

Solutions

  1. Read the wrapped cause to identify the offending field/type in the request struct
  2. Remove or convert non-serializable fields (chan/func/cyclic structures) to JSON-safe types
  3. Add encoding:"json" tags or change unsupported field types to strings/numbers
  4. Test with a representative request struct to catch regressions (existing tests already cover headers/body)

Example fix

// before
type LipSyncRequest struct {
    Callback func() `json:"callback"` // unsupported type
}
// after
type LipSyncRequest struct {
    CallbackURL string `json:"callback_url"`
Defensive patterns

Strategy: try-catch

Validate before calling

// JSON round-trip check before submitting
if b, err := json.Marshal(req); err != nil {
    return fmt.Errorf("request not serializable: %w", err)
} else { _ = b }

Try / catch

httpReq, err := video_gen.BuildLipSyncRequest(cfg)
if err != nil {
    if strings.Contains(err.Error(), "marshal lipsync request") {
        // unsupported field type in the request struct — fix struct or drop the field
        return fmt.Errorf("request struct contains non-JSON types: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: json.Marshal(req) returns an error — req (or a nested field added to it) contains a type JSON cannot encode (e.g. chan, func, or a type with a broken MarshalJSON) or a custom MarshalJSON implementation returns an error.

Common situations: A new field with a non-serializable type was added to the lipsync request struct; a custom json.Marshaler on a nested type errors; NaN/Inf float values in dynamically-set fields.

Understand the failure class

Background: json.Marshal / "failed to marshal" errors in Go: why "unsupported type" happens and how to fix it — this error's family across 22 libraries.

Related errors


AI-assisted analysis of Billionmail/BillionMail@fc36c76c05 (2026-09-05). Data as JSON: /api/errors/2d43ccac2edd6b81. Report an issue: GitHub.