@@ -12,6 +12,31 @@ import { headersToFileMeta } from "../lib/util.ts";
1212
1313const defaultFetchTimeout = 30000 ; // 30 seconds
1414
15+ // WebKit (Safari, WKWebView) strips custom response headers (X-Last-Modified,
16+ // etc.) when it recognizes a file extension in the URL. Encoding the last dot
17+ // as %2E prevents this; the server decodes it transparently.
18+ const isWebKit = typeof navigator !== "undefined" &&
19+ / A p p l e W e b K i t / . test ( navigator . userAgent ) &&
20+ ! / C h r o m e / . test ( navigator . userAgent ) ;
21+
22+ function encodeExtensionDot ( url : string ) : string {
23+ // Only encode the file extension dot, not dots in the path prefix (like /.fs).
24+ // Manipulate the raw string — Safari's URL.pathname setter rejects %2E.
25+ const fsIdx = url . indexOf ( "/.fs/" ) ;
26+ if ( fsIdx < 0 ) return url ;
27+ const afterFs = fsIdx + 5 ; // position after "/.fs/"
28+ const filePart = url . substring ( afterFs ) ;
29+ const qIdx = filePart . indexOf ( "?" ) ;
30+ const path = qIdx >= 0 ? filePart . substring ( 0 , qIdx ) : filePart ;
31+ const rest = qIdx >= 0 ? filePart . substring ( qIdx ) : "" ;
32+ const lastDot = path . lastIndexOf ( "." ) ;
33+ if ( lastDot > path . lastIndexOf ( "/" ) ) {
34+ return url . substring ( 0 , afterFs ) +
35+ path . substring ( 0 , lastDot ) + "%2E" + path . substring ( lastDot + 1 ) + rest ;
36+ }
37+ return url ;
38+ }
39+
1540export class HttpSpacePrimitives implements SpacePrimitives {
1641 constructor (
1742 readonly url : string ,
@@ -40,6 +65,9 @@ export class HttpSpacePrimitives implements SpacePrimitives {
4065 }
4166
4267 try {
68+ if ( isWebKit ) {
69+ url = encodeExtensionDot ( url ) ;
70+ }
4371 options . signal = AbortSignal . timeout ( fetchTimeout ) ;
4472 options . redirect = "manual" ;
4573 const result = await fetch ( url , options ) ;
0 commit comments