@@ -55,6 +55,51 @@ function isClerkGlobalProperlyLoaded(prop: 'Clerk' | '__internal_ClerkUiCtor'):
5555const isClerkProperlyLoaded = ( ) => isClerkGlobalProperlyLoaded ( 'Clerk' ) ;
5656const isClerkUiProperlyLoaded = ( ) => isClerkGlobalProperlyLoaded ( '__internal_ClerkUiCtor' ) ;
5757
58+ /**
59+ * Checks if an existing script has a request error using Performance API.
60+ *
61+ * @param scriptUrl - The URL of the script to check.
62+ * @returns True if the script has failed to load due to a network/HTTP error.
63+ */
64+ function hasScriptRequestError ( scriptUrl : string ) : boolean {
65+ if ( typeof window === 'undefined' || ! window . performance ) {
66+ return false ;
67+ }
68+
69+ const entries = performance . getEntriesByName ( scriptUrl , 'resource' ) as PerformanceResourceTiming [ ] ;
70+
71+ if ( entries . length === 0 ) {
72+ return false ;
73+ }
74+
75+ const scriptEntry = entries [ entries . length - 1 ] ;
76+
77+ // transferSize === 0 with responseEnd === 0 indicates network failure
78+ // transferSize === 0 with responseEnd > 0 might be a 4xx/5xx error or blocked request
79+ if ( scriptEntry . transferSize === 0 && scriptEntry . decodedBodySize === 0 ) {
80+ // If there was no response at all, it's definitely an error
81+ if ( scriptEntry . responseEnd === 0 ) {
82+ return true ;
83+ }
84+ // If we got a response but no content, likely an HTTP error (4xx/5xx)
85+ if ( scriptEntry . responseEnd > 0 && scriptEntry . responseStart > 0 ) {
86+ return true ;
87+ }
88+
89+ if ( 'responseStatus' in scriptEntry ) {
90+ const status = ( scriptEntry as any ) . responseStatus ;
91+ if ( status >= 400 ) {
92+ return true ;
93+ }
94+ if ( scriptEntry . responseStatus === 0 ) {
95+ return true ;
96+ }
97+ }
98+ }
99+
100+ return false ;
101+ }
102+
58103/**
59104 * Hotloads the Clerk JS script with robust failure detection.
60105 *
@@ -88,20 +133,30 @@ export const loadClerkJsScript = async (opts?: LoadClerkJsScriptOptions): Promis
88133 return null ;
89134 }
90135
91- const existingScript = document . querySelector < HTMLScriptElement > ( 'script[data-clerk-js-script]' ) ;
92-
93- if ( existingScript ) {
94- return waitForPredicateWithTimeout ( timeout , isClerkProperlyLoaded , rejectWith ( ) ) ;
95- }
96-
97136 if ( ! opts ?. publishableKey ) {
98137 errorThrower . throwMissingPublishableKeyError ( ) ;
99138 return null ;
100139 }
101140
141+ const scriptUrl = clerkJsScriptUrl ( opts ) ;
142+ const existingScript = document . querySelector < HTMLScriptElement > ( 'script[data-clerk-js-script]' ) ;
143+
144+ if ( existingScript ) {
145+ if ( hasScriptRequestError ( scriptUrl ) ) {
146+ existingScript . remove ( ) ;
147+ } else {
148+ try {
149+ await waitForPredicateWithTimeout ( timeout , isClerkProperlyLoaded , rejectWith ( ) , existingScript ) ;
150+ return null ;
151+ } catch {
152+ existingScript . remove ( ) ;
153+ }
154+ }
155+ }
156+
102157 const loadPromise = waitForPredicateWithTimeout ( timeout , isClerkProperlyLoaded , rejectWith ( ) ) ;
103158
104- loadScript ( clerkJsScriptUrl ( opts ) , {
159+ loadScript ( scriptUrl , {
105160 async : true ,
106161 crossOrigin : 'anonymous' ,
107162 nonce : opts . nonce ,
@@ -125,19 +180,30 @@ export const loadClerkUiScript = async (opts?: LoadClerkUiScriptOptions): Promis
125180 return null ;
126181 }
127182
128- const existingScript = document . querySelector < HTMLScriptElement > ( 'script[data-clerk-ui-script]' ) ;
129-
130- if ( existingScript ) {
131- return waitForPredicateWithTimeout ( timeout , isClerkUiProperlyLoaded , rejectWith ( ) ) ;
132- }
133-
134183 if ( ! opts ?. publishableKey ) {
135184 errorThrower . throwMissingPublishableKeyError ( ) ;
136185 return null ;
137186 }
138187
188+ const scriptUrl = clerkUiScriptUrl ( opts ) ;
189+ const existingScript = document . querySelector < HTMLScriptElement > ( 'script[data-clerk-ui-script]' ) ;
190+
191+ if ( existingScript ) {
192+ if ( hasScriptRequestError ( scriptUrl ) ) {
193+ existingScript . remove ( ) ;
194+ } else {
195+ try {
196+ await waitForPredicateWithTimeout ( timeout , isClerkUiProperlyLoaded , rejectWith ( ) , existingScript ) ;
197+ return null ;
198+ } catch {
199+ existingScript . remove ( ) ;
200+ }
201+ }
202+ }
203+
139204 const loadPromise = waitForPredicateWithTimeout ( timeout , isClerkUiProperlyLoaded , rejectWith ( ) ) ;
140- loadScript ( clerkUiScriptUrl ( opts ) , {
205+
206+ loadScript ( scriptUrl , {
141207 async : true ,
142208 crossOrigin : 'anonymous' ,
143209 nonce : opts . nonce ,
@@ -223,6 +289,7 @@ function waitForPredicateWithTimeout(
223289 timeoutMs : number ,
224290 predicate : ( ) => boolean ,
225291 rejectWith : Error ,
292+ existingScript ?: HTMLScriptElement ,
226293) : Promise < HTMLScriptElement | null > {
227294 return new Promise ( ( resolve , reject ) => {
228295 let resolved = false ;
@@ -232,6 +299,12 @@ function waitForPredicateWithTimeout(
232299 clearInterval ( pollInterval ) ;
233300 } ;
234301
302+ // Bail out early if the script fails to load, instead of waiting for the entire timeout
303+ existingScript ?. addEventListener ( 'error' , ( ) => {
304+ cleanup ( timeoutId , pollInterval ) ;
305+ reject ( rejectWith ) ;
306+ } ) ;
307+
235308 const checkAndResolve = ( ) => {
236309 if ( resolved ) {
237310 return ;
0 commit comments