Skip to content

Night Shift: fix YouTube API hang on script failure#17

Open
EtanHey wants to merge 1 commit intomasterfrom
nightshift/2026-02-21-6195
Open

Night Shift: fix YouTube API hang on script failure#17
EtanHey wants to merge 1 commit intomasterfrom
nightshift/2026-02-21-6195

Conversation

@EtanHey
Copy link
Owner

@EtanHey EtanHey commented Feb 21, 2026

User description

Automated improvement by GolemsZikaron Night Shift.

fix YouTube API hang on script failure


PR Type

Bug fix, Tests


Description

  • Fix YouTube API promise hang when script fails to load

  • Add error handler and timeout to reject cached promise

  • Reset apiLoadPromise on failure to enable retries

  • Add comprehensive tests for error handling and retry scenarios


Diagram Walkthrough

flowchart LR
  A["loadYouTubeAPI()"] --> B["Promise with timeout"]
  B --> C["Script error handler"]
  C --> D["Reset apiLoadPromise"]
  D --> E["Allow retry on next mount"]
  B --> F["10s timeout fallback"]
  F --> D
Loading

File Walkthrough

Relevant files
Bug fix
YouTubePlayer.tsx
Add error handling and timeout to YouTube API loader         

src/components/YouTubePlayer.tsx

  • Convert promise to reject on script load errors instead of hanging
  • Add 10-second timeout fallback for hung API loads
  • Reset apiLoadPromise to null on failure to enable retries
  • Add error event listeners to script element for both new and existing
    scripts
+24/-3   
Tests
YouTubePlayer.test.tsx
Add tests for API load failure and retry behavior               

src/components/YouTubePlayer.test.tsx

  • Add test for error display when YouTube API script fails to load
  • Add test verifying retry capability after initial script load failure
  • Tests simulate script error events and verify error message display
  • Tests confirm component can successfully retry with YT API available
+61/-0   

Note

Low Risk
Localized changes to client-side script loading with added test coverage; low risk aside from potential edge cases around script event timing/timeouts.

Overview
Prevents YouTubePlayer from hanging indefinitely when the YouTube IFrame API script fails to load by adding timeout-based rejection and error event handling to loadYouTubeAPI, and resetting the cached apiLoadPromise on failure to allow retries.

Adds tests covering script load failure UI (Failed to load YouTube player) and verifying a subsequent mount can succeed after an initial load error.

Written by Cursor Bugbot for commit 27ba3fc. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

  • Bug Fixes

    • Improved robustness of YouTube API initialization with timeout protection and error handling to prevent indefinite loading states.
  • New Features

    • Added retry capability after YouTube player load failures.
  • Tests

    • Added test coverage for script load error scenarios and recovery flows.

The loadYouTubeAPI() function cached a promise that would never resolve
if the script failed to load (network error, ad blocker, etc). This
permanently broke the YouTube player until page reload.

- Add script error handler that rejects promise and resets cache
- Add 10s timeout as fallback for hung loads
- Reset apiLoadPromise on failure so subsequent mounts can retry
- Add tests for error handling and retry behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vercel
Copy link

vercel bot commented Feb 21, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
songscript Ready Ready Preview, Comment Feb 21, 2026 2:06am

@coderabbitai
Copy link

coderabbitai bot commented Feb 21, 2026

📝 Walkthrough

Walkthrough

This PR adds timeout and error handling mechanisms to the YouTube IFrame API loading process in YouTubePlayer. The implementation introduces a configurable load timeout, script error detection, and promise rejection logic, with corresponding test coverage validating error scenarios and retry capability.

Changes

Cohort / File(s) Summary
YouTube API Loading Enhancement
src/components/YouTubePlayer.tsx
Introduces API_LOAD_TIMEOUT_MS constant and expands loadYouTubeAPI Promise with timeout rejection, script error event listeners, onLoad/onError helpers, timeout cleanup on success, and promise reset on rejection.
Test Coverage for Error Handling
src/components/YouTubePlayer.test.tsx
Adds two tests: one validating error message display when YouTube API script fails to load, and another verifying retry functionality after initial script load failure.

Sequence Diagram

sequenceDiagram
    participant Component as YouTubePlayer<br/>Component
    participant Script as Script<br/>Element
    participant API as YouTube<br/>IFrame API
    participant Timeout as Timeout<br/>Mechanism

    Component->>Script: Create & append script tag
    Component->>Timeout: Start API_LOAD_TIMEOUT_MS
    Script->>Timeout: Poll for completion
    
    alt API Load Success
        API->>Script: Trigger onYouTubeIframeAPIReady
        Script->>Component: onLoad() - Promise resolves
        Timeout->>Component: Clear timeout
    else API Load Error
        Script->>Component: onError() - Script error event
        Component->>Component: Promise rejects
        Timeout->>Component: Clear timeout
    else Timeout Expires
        Timeout->>Component: Promise rejects (timeout)
        Component->>Component: Reset apiLoadPromise
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A script that loads with grace and care,
With timeouts set and errors spare,
Retry paths when things go wrong,
Your YouTube player, strong and long! 📺✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Night Shift: fix YouTube API hang on script failure' accurately describes the main change: fixing a YouTube API hang when the script fails to load by adding error handling and timeout mechanisms.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch nightshift/2026-02-21-6195

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@qodo-code-review
Copy link

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Listener leak risk: The new error event listener is added to an existing youtube-iframe-api script on each
call without being removed/registered once, which can accumulate handlers across mounts
and cause unexpected multiple rejections or memory leaks.

Referred Code
const existingScript = document.getElementById('youtube-iframe-api')
if (existingScript) {
  if (window.YT && window.YT.Player) {
    clearTimeout(timeout)
    resolve()
  } else {
    const originalCallback = window.onYouTubeIframeAPIReady
    window.onYouTubeIframeAPIReady = () => {
      originalCallback?.()
      onLoad()
    }
    existingScript.addEventListener('error', onError)
  }

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status:
Error surfaced to UI: The promise now rejects with specific error strings (e.g., Failed to load YouTube API
script, YouTube API load timed out) and it is not visible from the diff whether these
messages are shown to end users or only handled internally.

Referred Code
const timeout = setTimeout(() => {
  apiLoadPromise = null
  reject(new Error('YouTube API load timed out'))
}, API_LOAD_TIMEOUT_MS)

const onLoad = () => {
  clearTimeout(timeout)
  resolve()
}

const onError = () => {
  clearTimeout(timeout)
  apiLoadPromise = null
  reject(new Error('Failed to load YouTube API script'))
}

Learn more about managing compliance generic rules or creating your own custom rules

Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review
Copy link

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Remove failed script for faster retries

In the onError and timeout handlers for the YouTube API script, remove the
failed script tag from the DOM to prevent subsequent retry attempts from
hanging.

src/components/YouTubePlayer.tsx [101-115]

 const timeout = setTimeout(() => {
+  document.getElementById('youtube-iframe-api')?.remove()
   apiLoadPromise = null
   reject(new Error('YouTube API load timed out'))
 }, API_LOAD_TIMEOUT_MS)
 
 const onLoad = () => {
   clearTimeout(timeout)
   resolve()
 }
 
 const onError = () => {
   clearTimeout(timeout)
+  document.getElementById('youtube-iframe-api')?.remove()
   apiLoadPromise = null
   reject(new Error('Failed to load YouTube API script'))
 }
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a flaw in the new retry logic where a failed script tag is not removed, causing subsequent retries to hang for 10 seconds until a timeout. Implementing this change will make the retry mechanism more robust and efficient.

Medium
  • More

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

clearTimeout(timeout)
apiLoadPromise = null
reject(new Error('Failed to load YouTube API script'))
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Failed script tag not removed, breaking retry mechanism

Medium Severity

The onError handler resets apiLoadPromise to null to allow retries, but doesn't remove the failed <script id="youtube-iframe-api"> element from the DOM. On retry, loadYouTubeAPI() finds the dead script via existingScript, attaches new listeners, but the already-errored script won't fire events again — so the retry silently hangs until the 10-second timeout. The test masks this by manually calling script.remove() before retrying.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9a964b0 and 27ba3fc.

📒 Files selected for processing (2)
  • src/components/YouTubePlayer.test.tsx
  • src/components/YouTubePlayer.tsx
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TanStack Start framework with Bun runtime for the application

Tests must pass locally via bun run test before committing code, as Husky pre-commit hooks will block commits with failing tests

Files:

  • src/components/YouTubePlayer.tsx
  • src/components/YouTubePlayer.test.tsx
**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Components with logic MUST have corresponding test files following the naming convention ComponentName.test.tsx

Files:

  • src/components/YouTubePlayer.tsx
  • src/components/YouTubePlayer.test.tsx
**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Vitest for unit testing and Playwright for integration testing

Files:

  • src/components/YouTubePlayer.test.tsx
**/*.test.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Component tests should be located in the same directory as the component file with naming convention ComponentName.test.tsx

Files:

  • src/components/YouTubePlayer.test.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (5)
src/components/YouTubePlayer.tsx (4)

86-87: LGTM on the timeout constant.

Clean separation of the magic number into a named constant.

One consideration: the 10-second timeout is reasonable for most networks but may be tight on very slow connections. If you ever want to make this testable or configurable, exporting it (or accepting it as a parameter) would help, but it's fine as-is for now.


95-115: Solid promise guarding with onLoad/onError helpers.

The timeout → reject → cache-reset pattern is correct: once a promise is settled, subsequent resolve()/reject() calls are no-ops, so there's no double-settlement risk if both timeout and error (or timeout and success) fire. Cache reset on failure enables the retry path correctly.


133-142: LGTM on new script creation path.

Error listener is attached before appending to DOM, so no race between insertion and listener registration. The onYouTubeIframeAPIReady callback is set before the script is added, ensuring no missed callback.


249-254: LGTM on the catch block.

The isMounted guard correctly prevents state updates after unmount. The generic error message 'Failed to load YouTube player' is user-friendly and matches what the tests assert.

src/components/YouTubePlayer.test.tsx (1)

176-199: Good coverage of the script error path.

The test correctly simulates the failure scenario: YT is undefined, the component creates a script tag, the error event fires, and the error message is displayed. Script cleanup at line 198 prevents DOM pollution across tests.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/YouTubePlayer.test.tsx`:
- Around line 175-235: Add a test that covers the timeout rejection path for the
API loader: in YouTubePlayer.test.tsx create a new test (e.g., "rejects and
allows retry when API load times out") that uses vi.useFakeTimers(), ensures
window.YT is undefined, renders <YouTubePlayer videoId="..."/>, advances timers
past API_LOAD_TIMEOUT_MS to trigger the timeout, waits for the "Failed to load
YouTube player" UI, unmounts and removes the 'youtube-iframe-api' script, then
assert that apiLoadPromise has been cleared so a subsequent render (after
setting window.YT to include MockYTPlayer/PlayerState) will call MockYTPlayer;
finally restore real timers and clean up the script tag. Ensure you reference
API_LOAD_TIMEOUT_MS, apiLoadPromise, YouTubePlayer and MockYTPlayer when
locating where to add assertions and cleanup.
- Around line 201-235: The test "allows retry after script load failure"
currently only asserts MockYTPlayer was called; update the assertion to verify
it was invoked for the retry render with videoId "retry123" by checking the
mock's call arguments (e.g., inspect MockYTPlayer.mock.calls or use a matcher)
to confirm the player was created with the expected videoId for the retry
render.

In `@src/components/YouTubePlayer.tsx`:
- Around line 117-131: When an existingScript is present but window.YT.Player
isn't ready the code currently attaches listeners which can miss a
previously-fired error; instead, if existingScript exists and window.YT?.Player
is falsy, remove the stale element (call existingScript.remove()) and clear any
stale window.onYouTubeIframeAPIReady if it points to a leftover callback, then
fall through to the normal script-creation path so a fresh <script> is inserted
and proper load/error handlers are attached; keep the original branch that
resolves immediately when window.YT.Player exists and only use the
remove-and-recreate behavior when the player is not ready.
- Line 84: The module-scoped variable apiLoadPromise in YouTubePlayer.tsx is
cached between tests and should be reset to ensure test isolation; update your
test suite's beforeEach hook to set apiLoadPromise = null so each test starts
with a clean module state and the YouTube API loader logic (apiLoadPromise) is
re-evaluated per test run.

Comment on lines +175 to +235

it('shows error when YouTube API script fails to load', async () => {
// Remove YT so the component tries to load the script
;(window as unknown as { YT: unknown }).YT = undefined

render(<YouTubePlayer videoId="test123" />)

// The component should have created a script tag - fire error on it
await waitFor(() => {
const script = document.getElementById('youtube-iframe-api')
expect(script).not.toBeNull()
})

const script = document.getElementById('youtube-iframe-api')!
act(() => {
script.dispatchEvent(new Event('error'))
})

await waitFor(() => {
expect(screen.getByText('Failed to load YouTube player')).toBeInTheDocument()
})

// Clean up the script tag for other tests
script.remove()
})

it('allows retry after script load failure', async () => {
// First: fail the load
;(window as unknown as { YT: unknown }).YT = undefined

const { unmount } = render(<YouTubePlayer videoId="test123" />)

await waitFor(() => {
const script = document.getElementById('youtube-iframe-api')
expect(script).not.toBeNull()
})

const script = document.getElementById('youtube-iframe-api')!
act(() => {
script.dispatchEvent(new Event('error'))
})

await waitFor(() => {
expect(screen.getByText('Failed to load YouTube player')).toBeInTheDocument()
})

unmount()
script.remove()

// Second: retry with YT available should succeed
;(window as unknown as { YT: unknown }).YT = {
Player: MockYTPlayer,
PlayerState: { UNSTARTED: -1, ENDED: 0, PLAYING: 1, PAUSED: 2, BUFFERING: 3, CUED: 5 },
}

render(<YouTubePlayer videoId="retry123" />)

await waitFor(() => {
expect(MockYTPlayer).toHaveBeenCalled()
})
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Missing test: timeout rejection path.

The API_LOAD_TIMEOUT_MS (10 s) timeout is a core part of this fix — it's the fallback when neither load nor error fires. There's no test that verifies this behavior. Consider adding a test using vi.useFakeTimers() to advance time past the timeout and assert that the promise rejects and apiLoadPromise is reset for retry.

Sketch:

it('rejects and allows retry when API load times out', async () => {
  vi.useFakeTimers()
  ;(window as unknown as { YT: unknown }).YT = undefined

  const { unmount } = render(<YouTubePlayer videoId="test123" />)

  // Advance past the 10s timeout
  await act(async () => { vi.advanceTimersByTime(10_000) })

  await waitFor(() => {
    expect(screen.getByText('Failed to load YouTube player')).toBeInTheDocument()
  })

  unmount()
  document.getElementById('youtube-iframe-api')?.remove()
  vi.useRealTimers()
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/YouTubePlayer.test.tsx` around lines 175 - 235, Add a test
that covers the timeout rejection path for the API loader: in
YouTubePlayer.test.tsx create a new test (e.g., "rejects and allows retry when
API load times out") that uses vi.useFakeTimers(), ensures window.YT is
undefined, renders <YouTubePlayer videoId="..."/>, advances timers past
API_LOAD_TIMEOUT_MS to trigger the timeout, waits for the "Failed to load
YouTube player" UI, unmounts and removes the 'youtube-iframe-api' script, then
assert that apiLoadPromise has been cleared so a subsequent render (after
setting window.YT to include MockYTPlayer/PlayerState) will call MockYTPlayer;
finally restore real timers and clean up the script tag. Ensure you reference
API_LOAD_TIMEOUT_MS, apiLoadPromise, YouTubePlayer and MockYTPlayer when
locating where to add assertions and cleanup.

Comment on lines +201 to +235
it('allows retry after script load failure', async () => {
// First: fail the load
;(window as unknown as { YT: unknown }).YT = undefined

const { unmount } = render(<YouTubePlayer videoId="test123" />)

await waitFor(() => {
const script = document.getElementById('youtube-iframe-api')
expect(script).not.toBeNull()
})

const script = document.getElementById('youtube-iframe-api')!
act(() => {
script.dispatchEvent(new Event('error'))
})

await waitFor(() => {
expect(screen.getByText('Failed to load YouTube player')).toBeInTheDocument()
})

unmount()
script.remove()

// Second: retry with YT available should succeed
;(window as unknown as { YT: unknown }).YT = {
Player: MockYTPlayer,
PlayerState: { UNSTARTED: -1, ENDED: 0, PLAYING: 1, PAUSED: 2, BUFFERING: 3, CUED: 5 },
}

render(<YouTubePlayer videoId="retry123" />)

await waitFor(() => {
expect(MockYTPlayer).toHaveBeenCalled()
})
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Strengthen the retry assertion to verify the correct videoId.

The assertion at line 233 only checks that MockYTPlayer was called, but doesn't verify it was called with the retry videoId ("retry123"). A more precise assertion prevents false positives if the mock was somehow called during the first (failing) render path.

♻️ Proposed fix
     await waitFor(() => {
-      expect(MockYTPlayer).toHaveBeenCalled()
+      const lastCall = MockYTPlayer.mock.calls[MockYTPlayer.mock.calls.length - 1]
+      expect((lastCall[1] as { videoId?: string }).videoId).toBe('retry123')
     })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/YouTubePlayer.test.tsx` around lines 201 - 235, The test
"allows retry after script load failure" currently only asserts MockYTPlayer was
called; update the assertion to verify it was invoked for the retry render with
videoId "retry123" by checking the mock's call arguments (e.g., inspect
MockYTPlayer.mock.calls or use a matcher) to confirm the player was created with
the expected videoId for the retry render.

@@ -83,41 +83,62 @@ interface YouTubePlayerProps {

let apiLoadPromise: Promise<void> | null = null
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check references to apiLoadPromise in both files
rg -n 'apiLoadPromise' src/components/YouTubePlayer.tsx
echo "---"
rg -n 'apiLoadPromise' src/components/YouTubePlayer.test.tsx

Repository: EtanHey/songscript

Length of output: 312


🏁 Script executed:

#!/bin/bash
# Get the full test file to understand current test setup
wc -l src/components/YouTubePlayer.test.tsx

Repository: EtanHey/songscript

Length of output: 103


🏁 Script executed:

#!/bin/bash
# Read the test file to understand test setup and state management
cat -n src/components/YouTubePlayer.test.tsx

Repository: EtanHey/songscript

Length of output: 8124


🏁 Script executed:

#!/bin/bash
# Verify the loadYouTubeAPI function implementation to confirm the caching behavior
sed -n '84,145p' src/components/YouTubePlayer.tsx

Repository: EtanHey/songscript

Length of output: 1662


Reset apiLoadPromise in beforeEach to ensure test isolation and prevent module state pollution.

The module-scoped apiLoadPromise is not reset between tests, creating a potential fragility. While current tests pass because most rely on beforeEach setting window.YT (which triggers the early return on line 91) and error tests explicitly reset the state, this masks an underlying issue: if a test completes the promise successfully, it remains cached for subsequent tests. Adding apiLoadPromise = null to the beforeEach hook would eliminate this fragility and make tests explicitly independent of module state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/YouTubePlayer.tsx` at line 84, The module-scoped variable
apiLoadPromise in YouTubePlayer.tsx is cached between tests and should be reset
to ensure test isolation; update your test suite's beforeEach hook to set
apiLoadPromise = null so each test starts with a clean module state and the
YouTube API loader logic (apiLoadPromise) is re-evaluated per test run.

Comment on lines 117 to 131
const existingScript = document.getElementById('youtube-iframe-api')
if (existingScript) {
if (window.YT && window.YT.Player) {
clearTimeout(timeout)
resolve()
} else {
const originalCallback = window.onYouTubeIframeAPIReady
window.onYouTubeIframeAPIReady = () => {
originalCallback?.()
resolve()
onLoad()
}
existingScript.addEventListener('error', onError)
}
return
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Edge case: existing script that already errored before this code runs.

If a previous attempt inserted the <script> tag and it fired its error event before this new promise executor attaches the listener at line 128, the error event won't re-fire, and the promise will hang until the 10 s timeout. The timeout does provide a safety net, but the caller experiences a needless 10-second delay in that scenario.

A simple mitigation: when an existing script is found and window.YT.Player isn't ready, also check whether the script's network state indicates a failure. For example:

🛡️ Suggested improvement
       } else {
+        // If the existing script already failed, reject immediately
+        if ((existingScript as HTMLScriptElement).error || 
+            ((existingScript as HTMLScriptElement).readyState === undefined && 
+             !(existingScript as HTMLScriptElement).src)) {
+          onError()
+          return
+        }
         const originalCallback = window.onYouTubeIframeAPIReady

Alternatively, a simpler approach: remove the stale script tag and fall through to create a fresh one:

♻️ Alternative: remove stale script and re-create
     const existingScript = document.getElementById('youtube-iframe-api')
     if (existingScript) {
       if (window.YT && window.YT.Player) {
         clearTimeout(timeout)
         resolve()
-      } else {
-        const originalCallback = window.onYouTubeIframeAPIReady
-        window.onYouTubeIframeAPIReady = () => {
-          originalCallback?.()
-          onLoad()
-        }
-        existingScript.addEventListener('error', onError)
+      } else {
+        // Script exists but YT isn't ready — remove it and create a fresh one
+        existingScript.remove()
       }
-      return
+      if (window.YT && window.YT.Player) return
     }

This is a narrow edge case and the timeout does cover it, so this is not blocking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/YouTubePlayer.tsx` around lines 117 - 131, When an
existingScript is present but window.YT.Player isn't ready the code currently
attaches listeners which can miss a previously-fired error; instead, if
existingScript exists and window.YT?.Player is falsy, remove the stale element
(call existingScript.remove()) and clear any stale
window.onYouTubeIframeAPIReady if it points to a leftover callback, then fall
through to the normal script-creation path so a fresh <script> is inserted and
proper load/error handlers are attached; keep the original branch that resolves
immediately when window.YT.Player exists and only use the remove-and-recreate
behavior when the player is not ready.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant