{"record":{"id":"c20966fe72a94089","repo":"Billionmail/BillionMail","slug":"lip-sync-completed-but-no-video-url","errorCode":null,"errorMessage":"lip sync completed but no video URL","messagePattern":"lip sync completed but no video URL","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/internal/service/video_gen/orchestrator.go","lineNumber":415,"sourceCode":"}\n\n// pollLipSync polls lip sync status until completed or timeout.\nfunc pollLipSync(ctx context.Context, cfg LipSyncConfig, jobID string) (string, error) {\n\tdeadline := time.Now().Add(lipSyncTimeout)\n\tfor {\n\t\tif time.Now().After(deadline) {\n\t\t\treturn \"\", fmt.Errorf(\"lip sync timed out after %v\", lipSyncTimeout)\n\t\t}\n\n\t\tresp, err := CheckLipSyncStatus(ctx, cfg, jobID)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"check lip sync status: %w\", err)\n\t\t}\n\n\t\tswitch resp.Status {\n\t\tcase \"completed\":\n\t\t\tif resp.VideoURL == \"\" {\n\t\t\t\treturn \"\", fmt.Errorf(\"lip sync completed but no video URL\")\n\t\t\t}\n\t\t\treturn resp.VideoURL, nil\n\t\tcase \"failed\":\n\t\t\treturn \"\", fmt.Errorf(\"lip sync failed: %s\", resp.Error)\n\t\t}\n\n\t\ttime.Sleep(lipSyncPollDelay)\n\t}\n}\n\n// withRetry retries a function up to maxRetries times with exponential backoff.\nfunc withRetry[T any](fn func() (T, error)) (T, error) {\n\tvar result T\n\tvar err error\n\tdelay := retryBaseDelay\n\n\tfor attempt := 0; attempt <= maxRetries; attempt++ {\n\t\tresult, err = fn()","sourceCodeStart":397,"sourceCodeEnd":433,"githubUrl":"https://github.com/Billionmail/BillionMail/blob/fc36c76c050c3775c5e899faf7403cf0262d2744/core/internal/service/video_gen/orchestrator.go#L397-L433","documentation":"The provider reported the lip-sync job status as 'completed', but the response payload's VideoURL field was empty. This is a contract violation from the provider — a completed job must include the output video URL — so pollLipSync treats it as a failure.","triggerScenarios":"CheckLipSyncStatus returns Status==\"completed\" with resp.VideoURL == \"\"; can happen with provider schema changes or partial job output.","commonSituations":"Provider API version drift where the URL moved to a different field (e.g. result.video.url); job produced output in an alternate format/location; provider bug on certain inputs.","solutions":["Log the full provider response to see where the URL actually lives","Update CheckLipSyncStatus response parsing to match the current provider schema","Re-run the lip-sync job; if reproducible, report to the provider","Add a defensive fallback that checks alternate URL fields"],"exampleFix":"// before\nif resp.VideoURL == \"\" {\n\treturn \"\", fmt.Errorf(\"lip sync completed but no video URL\")\n}\n// after\nif resp.VideoURL == \"\" && resp.Result != nil && resp.Result.VideoURL != \"\" {\n\tresp.VideoURL = resp.Result.VideoURL // handle v2 nested schema\n}\nif resp.VideoURL == \"\" {\n\treturn \"\", fmt.Errorf(\"lip sync completed but no video URL\")\n}","handlingStrategy":"type-guard","validationCode":"if resp.Status == \"completed\" && resp.VideoURL == \"\" {\n\tlog.Printf(\"provider returned completed without URL: %+v\", resp)\n}","typeGuard":"func hasValidVideoURL(resp *LipSyncStatusResponse) bool {\n\treturn resp != nil && resp.Status == \"completed\" && strings.HasPrefix(resp.VideoURL, \"http\")\n}","tryCatchPattern":"url, err := pollLipSync(ctx, cfg, jobID)\nif err != nil && strings.Contains(err.Error(), \"no video URL\") {\n\t// schema drift: inspect raw payload, re-run or fall back to legacy field\n}","preventionTips":["Pin the provider API version in requests to avoid schema drift","Add an integration test asserting completed responses contain a URL","Log the full raw response when this fires to capture schema changes early","Subscribe to provider changelogs/release notes"],"tags":["api-contract","empty-response","external-service","schema-change"],"backgroundTag":"unexpected-empty-response","analyzedSha":"fc36c76c050c3775c5e899faf7403cf0262d2744","analyzedAt":"2026-09-05T21:28:54.019Z","contentChangedAt":"2026-09-05T21:28:54.019Z","schemaVersion":2},"datasetVersion":"2026-09-12T22:17:10.623Z"}