@@ -255,18 +255,26 @@ public function is_file_readable_externally_by_storedfile(stored_file $file) {
255255 }
256256
257257 $ path = $ this ->get_external_path_from_storedfile ($ file );
258- if (is_readable ($ path )) {
259- return true ;
258+ error_clear_last ();
259+ $ readable = @is_readable ($ path );
260+ if (!$ readable ) {
261+ $ error = error_get_last ();
262+ if ($ error !== null ) {
263+ throw new \Exception (
264+ 'Transient external storage error: ' . ($ error ['message ' ] ?? 'Unknown error ' )
265+ );
266+ }
260267 }
261-
262- return false ;
268+ return $ readable ;
263269 }
264270
265271 /**
266272 * is_file_readable_externally_by_hash
267273 * @param mixed $contenthash
268274 *
269275 * @return bool
276+ * @throws \Exception On transient external storage errors (e.g. S3 connectivity failure).
277+ * Returns false only when the object is confirmed absent.
270278 */
271279 public function is_file_readable_externally_by_hash ($ contenthash ) {
272280 if ($ contenthash === sha1 ('' )) {
@@ -277,8 +285,22 @@ public function is_file_readable_externally_by_hash($contenthash) {
277285
278286 $ path = $ this ->get_external_path_from_hash ($ contenthash , false );
279287
280- // Note - it is not possible to perform a content recovery safely from a hash alone.
281- return is_readable ($ path );
288+ // Suppress the PHP Warning from the S3 StreamWrapper so we can inspect it ourselves.
289+ // If is_readable returns false AND a PHP error was raised, the failure is transient
290+ // (e.g. a connectivity blip) rather than the object genuinely being absent. In that
291+ // case we throw so callers can retry rather than silently recording OBJECT_LOCATION_ERROR.
292+ error_clear_last ();
293+ $ readable = @is_readable ($ path );
294+ if (!$ readable ) {
295+ $ error = error_get_last ();
296+ if ($ error !== null ) {
297+ throw new \Exception (
298+ 'Transient external storage error for ' . $ contenthash . ': ' .
299+ ($ error ['message ' ] ?? 'Unknown error ' )
300+ );
301+ }
302+ }
303+ return $ readable ;
282304 }
283305
284306 /**
@@ -520,20 +542,24 @@ public function xsendfile_file(stored_file $file): bool {
520542 }
521543
522544 $ contenthash = $ file ->get_contenthash ();
523- if (
524- $ this ->presigned_url_configured () &&
525- $ this ->presigned_url_should_redirect_file ($ file ) &&
526- $ this ->is_file_stored_externally_by_hash ($ contenthash )
527- ) {
528- return $ this ->redirect_to_presigned_url ($ contenthash , headers_list ());
529- }
545+ try {
546+ if (
547+ $ this ->presigned_url_configured () &&
548+ $ this ->presigned_url_should_redirect_file ($ file ) &&
549+ $ this ->is_file_readable_externally_by_hash ($ contenthash )
550+ ) {
551+ return $ this ->redirect_to_presigned_url ($ contenthash , headers_list ());
552+ }
530553
531- $ ranges = $ this ->get_valid_http_ranges ($ file ->get_filesize ());
532- if (
533- $ this ->externalclient ->support_presigned_urls () && !empty ($ ranges ) &&
534- $ this ->is_file_stored_externally_by_hash ($ contenthash )
535- ) {
536- return $ this ->externalclient ->proxy_range_request ($ file , $ ranges );
554+ $ ranges = $ this ->get_valid_http_ranges ($ file ->get_filesize ());
555+ if (
556+ $ this ->externalclient ->support_presigned_urls () && !empty ($ ranges ) &&
557+ $ this ->is_file_readable_externally_by_hash ($ contenthash )
558+ ) {
559+ return $ this ->externalclient ->proxy_range_request ($ file , $ ranges );
560+ }
561+ } catch (\Exception $ e ) {
562+ // Transient external storage error - fall back to standard file serving.
537563 }
538564
539565 return false ;
@@ -555,12 +581,16 @@ public function xsendfile($contenthash) {
555581 return parent ::xsendfile ($ contenthash );
556582 }
557583 $ headers = headers_list ();
558- if (
559- $ this ->presigned_url_configured () &&
560- $ this ->is_file_stored_externally_by_hash ($ contenthash ) &&
561- $ this ->presigned_url_should_redirect ($ contenthash , $ headers )
562- ) {
563- return $ this ->redirect_to_presigned_url ($ contenthash , $ headers );
584+ try {
585+ if (
586+ $ this ->presigned_url_configured () &&
587+ $ this ->is_file_readable_externally_by_hash ($ contenthash ) &&
588+ $ this ->presigned_url_should_redirect ($ contenthash , $ headers )
589+ ) {
590+ return $ this ->redirect_to_presigned_url ($ contenthash , $ headers );
591+ }
592+ } catch (\Exception $ e ) {
593+ // Transient external storage error - fall back to standard file serving.
564594 }
565595 return false ;
566596 }
@@ -609,7 +639,13 @@ protected function get_object_handle_for_path($path, $type = \stored_file::FILE_
609639 switch ($ type ) {
610640 case \stored_file::FILE_HANDLE_FOPEN :
611641 $ context = $ this ->externalclient ->get_seekable_stream_context ();
612- return fopen ($ path , 'rb ' , false , $ context );
642+ $ handle = @fopen ($ path , 'rb ' , false , $ context );
643+ if ($ handle === false ) {
644+ $ error = error_get_last ();
645+ $ message = $ error ['message ' ] ?? 'Unknown filesystem error occurred. ' ;
646+ throw new \Exception ('Failed to open object file handle: ' . $ message );
647+ }
648+ return $ handle ;
613649 case \stored_file::FILE_HANDLE_GZOPEN :
614650 // Binary reading of file in gz format.
615651 return gzopen ($ path , 'rb ' );
0 commit comments