Skip to content

Commit 8e535e9

Browse files
committed
Detect Safari limitation for external links and bookmarks
Safari does not fire webRequest events for main document requests when navigating from external apps (e.g., clicking a link in a text file) or bookmarks. This is a known Safari API limitation. Changes: - Track navigations via webNavigation.onBeforeNavigate - Detect when webRequest.onHeadersReceived never fires - Show "Reload Required" message explaining the limitation - Display "RLD" badge when headers unavailable - Badge now shows full "MISS" instead of truncated "MIS" - Add KNOWN_ISSUES.md documenting Safari webRequest limitations The detection works by setting a headersReceived flag when headers arrive. If navigation completes without headers, we know Safari skipped the webRequest events and prompt the user to reload. Note: Initial implementation had a bug where the headersReceived check blocked legitimate header updates on page reload. Fixed by removing the early-return - we now always process headers when received, using the flag only for Safari limitation detection.
1 parent 1127310 commit 8e535e9

File tree

5 files changed

+198
-79
lines changed

5 files changed

+198
-79
lines changed

CF Cache Status/CF Cache Status Extension/Resources/background.js

Lines changed: 137 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -5,153 +5,214 @@
55
* the toolbar badge with the cache status (HIT/MISS/etc).
66
*/
77

8-
// Import shared constants (loaded via manifest.json)
9-
// Uses: TRACKED_HEADERS, STATUS_COLORS, detectCDN, parseCacheStatus
10-
118
// =============================================================================
129
// State Management
1310
// =============================================================================
1411

1512
/** Stores cache header data per tab ID */
1613
const tabData = new Map();
1714

18-
/** Tracks tabs with pending navigations to capture only the first main request */
19-
const pendingNavigations = new Set();
20-
21-
/** Connected popup ports, keyed by tab ID they're watching */
15+
/** Connected popup ports */
2216
const popupPorts = new Map();
2317

24-
/**
25-
* Notifies the popup if one is connected and watching this tab.
26-
* @param {number} tabId - Tab ID that was updated
27-
*/
28-
function notifyPopup(tabId) {
29-
const port = popupPorts.get(tabId);
30-
if (port) {
31-
port.postMessage({ type: 'update', data: tabData.get(tabId) || null });
32-
}
33-
}
18+
/** Track pending navigations to detect missing headers (Safari limitation workaround) */
19+
const pendingNavigations = new Map();
3420

3521
// =============================================================================
3622
// Badge Management
3723
// =============================================================================
3824

39-
/**
40-
* Updates the toolbar badge text and color for a tab.
41-
* @param {number} tabId - Browser tab ID
42-
* @param {string|null} status - Cache status (HIT, MISS, etc.)
43-
* @param {string|null} cdn - CDN identifier
44-
*/
45-
function updateBadge(tabId, status, cdn) {
46-
const displayStatus = status || 'NONE';
47-
const colors = STATUS_COLORS[displayStatus] || STATUS_COLORS['NONE'];
48-
25+
function updateBadge(tabId, status) {
4926
const badgeTextMap = {
50-
'HIT': 'HIT', 'MISS': 'MISS', 'EXPIRED': 'EXP', 'STALE': 'STL',
51-
'REVALIDATED': 'REV', 'BYPASS': 'BYP', 'DYNAMIC': 'DYN',
52-
'REFRESH': 'REF', 'ERROR': 'ERR'
27+
'HIT': 'HIT',
28+
'MISS': 'MISS',
29+
'EXPIRED': 'EXP',
30+
'STALE': 'STALE',
31+
'REVALIDATED': 'REV',
32+
'BYPASS': 'BYP',
33+
'DYNAMIC': 'DYN',
34+
'REFRESH': 'REF',
35+
'ERROR': 'ERR'
5336
};
54-
const badgeText = status ? (badgeTextMap[status] || status.substring(0, 3)) : '';
37+
const badgeText = status ? (badgeTextMap[status.toUpperCase()] || status) : '';
38+
const color = status === 'HIT' ? '#34C759' : status === 'MISS' ? '#FF3B30' : '#8E8E93';
5539

5640
browser.action.setBadgeText({ text: badgeText });
57-
browser.action.setBadgeBackgroundColor({ color: colors.badge });
41+
browser.action.setBadgeBackgroundColor({ color });
5842

5943
try {
6044
browser.action.setBadgeText({ text: badgeText, tabId });
61-
browser.action.setBadgeBackgroundColor({ color: colors.badge, tabId });
45+
browser.action.setBadgeBackgroundColor({ color, tabId });
6246
} catch (e) {
6347
// Per-tab badges not supported
6448
}
6549
}
6650

67-
/**
68-
* Clears the badge for a tab.
69-
* @param {number} tabId - Browser tab ID
70-
*/
7151
function clearBadge(tabId) {
7252
browser.action.setBadgeText({ text: '' });
7353
try {
7454
browser.action.setBadgeText({ text: '', tabId });
75-
} catch (e) {
76-
// Per-tab badges not supported
55+
} catch (e) {}
56+
}
57+
58+
// =============================================================================
59+
// Popup Communication
60+
// =============================================================================
61+
62+
function notifyPopup(tabId) {
63+
const port = popupPorts.get(tabId);
64+
if (port) {
65+
port.postMessage({ type: 'update', data: tabData.get(tabId) || null });
7766
}
7867
}
7968

8069
// =============================================================================
8170
// Event Listeners
8271
// =============================================================================
8372

84-
// Track navigation start to capture only the first main document request
73+
// --- Web Navigation Events ---
74+
8575
browser.webNavigation.onBeforeNavigate.addListener((details) => {
8676
if (details.frameId === 0) {
87-
pendingNavigations.add(details.tabId);
8877
tabData.delete(details.tabId);
78+
clearBadge(details.tabId);
79+
notifyPopup(details.tabId);
80+
81+
// Track navigation to detect Safari limitation where webRequest events don't fire
82+
pendingNavigations.set(details.tabId, {
83+
url: details.url,
84+
timestamp: Date.now(),
85+
headersReceived: false
86+
});
8987
}
9088
});
9189

92-
// Update URL after navigation completes (handles redirects)
9390
browser.webNavigation.onCompleted.addListener((details) => {
9491
if (details.frameId === 0) {
9592
const data = tabData.get(details.tabId);
93+
const pending = pendingNavigations.get(details.tabId);
94+
95+
// Check if we never received headers (Safari limitation with external links/bookmarks)
96+
if (pending && !pending.headersReceived) {
97+
const existingData = tabData.get(details.tabId);
98+
if (existingData) {
99+
existingData.noHeaders = true;
100+
existingData.url = details.url;
101+
} else {
102+
tabData.set(details.tabId, {
103+
url: details.url,
104+
headers: {},
105+
status: null,
106+
cdn: null,
107+
timestamp: Date.now(),
108+
noHeaders: true
109+
});
110+
}
111+
112+
// Show reload badge
113+
browser.action.setBadgeText({ text: 'RLD' });
114+
browser.action.setBadgeBackgroundColor({ color: '#8E8E93' });
115+
try {
116+
browser.action.setBadgeText({ text: 'RLD', tabId: details.tabId });
117+
browser.action.setBadgeBackgroundColor({ color: '#8E8E93', tabId: details.tabId });
118+
} catch (e) {}
119+
120+
notifyPopup(details.tabId);
121+
}
122+
123+
// Update URL if we have data (handles redirects)
96124
if (data && data.url !== details.url) {
97125
data.url = details.url;
98126
}
127+
128+
pendingNavigations.delete(details.tabId);
99129
}
100130
});
101131

102-
// Capture response headers for main document requests
103-
browser.webRequest.onHeadersReceived.addListener(
104-
(details) => {
105-
if (details.type !== 'main_frame' || details.frameId !== 0) return;
106-
if (!pendingNavigations.has(details.tabId)) return;
132+
// --- Web Request Events ---
107133

108-
pendingNavigations.delete(details.tabId);
134+
/**
135+
* Process response headers from a main frame request.
136+
* Called by both onHeadersReceived and onResponseStarted (Safari fallback).
137+
*/
138+
function processMainFrameHeaders(details) {
139+
// Mark navigation as having received headers (for Safari limitation detection)
140+
const pendingNav = pendingNavigations.get(details.tabId);
141+
if (pendingNav) {
142+
pendingNav.headersReceived = true;
143+
}
109144

110-
// Extract tracked headers
111-
const headers = {};
112-
for (const header of details.responseHeaders || []) {
113-
const name = header.name.toLowerCase();
114-
if (TRACKED_HEADERS.includes(name)) {
115-
headers[name] = header.value;
116-
}
145+
// Extract tracked headers
146+
const headers = {};
147+
for (const header of details.responseHeaders || []) {
148+
const name = header.name.toLowerCase();
149+
if (TRACKED_HEADERS.includes(name)) {
150+
headers[name] = header.value;
117151
}
152+
}
118153

119-
// Detect CDN and parse status using shared functions
120-
const cdn = detectCDN(headers);
121-
const status = parseCacheStatus(headers, cdn);
154+
// Detect CDN and parse status
155+
const cdn = detectCDN(headers);
156+
const status = parseCacheStatus(headers, cdn);
122157

123-
tabData.set(details.tabId, {
124-
url: details.url,
125-
headers,
126-
status,
127-
cdn,
128-
timestamp: Date.now()
129-
});
158+
// Store data
159+
tabData.set(details.tabId, {
160+
url: details.url,
161+
headers,
162+
status,
163+
cdn,
164+
timestamp: Date.now()
165+
});
130166

131-
updateBadge(details.tabId, status, cdn);
132-
notifyPopup(details.tabId);
167+
updateBadge(details.tabId, status);
168+
notifyPopup(details.tabId);
169+
}
170+
171+
browser.webRequest.onHeadersReceived.addListener(
172+
(details) => {
173+
if (details.type === 'main_frame' && details.frameId === 0) {
174+
processMainFrameHeaders(details);
175+
}
133176
},
134177
{ urls: ['<all_urls>'] },
135178
['responseHeaders']
136179
);
137180

138-
// Clean up data when tab closes
181+
// Safari fallback: onResponseStarted sometimes fires when onHeadersReceived doesn't
182+
browser.webRequest.onResponseStarted.addListener(
183+
(details) => {
184+
if (details.type !== 'main_frame' || details.frameId !== 0) {
185+
return;
186+
}
187+
188+
const pendingNav = pendingNavigations.get(details.tabId);
189+
if (pendingNav && !pendingNav.headersReceived) {
190+
processMainFrameHeaders(details);
191+
}
192+
},
193+
{ urls: ['<all_urls>'] },
194+
['responseHeaders']
195+
);
196+
197+
// --- Tab Events ---
198+
139199
browser.tabs.onRemoved.addListener((tabId) => {
140200
tabData.delete(tabId);
201+
popupPorts.delete(tabId);
141202
pendingNavigations.delete(tabId);
142203
});
143204

144-
// Update badge when switching tabs
145205
browser.tabs.onActivated.addListener((activeInfo) => {
146206
const data = tabData.get(activeInfo.tabId);
147207
if (data) {
148-
updateBadge(activeInfo.tabId, data.status, data.cdn);
208+
updateBadge(activeInfo.tabId, data.status);
149209
} else {
150210
clearBadge(activeInfo.tabId);
151211
}
152212
});
153213

154-
// Update badge when window focus changes
214+
// --- Window Events ---
215+
155216
browser.windows.onFocusChanged.addListener(async (windowId) => {
156217
if (windowId === browser.windows.WINDOW_ID_NONE) return;
157218

@@ -160,7 +221,7 @@ browser.windows.onFocusChanged.addListener(async (windowId) => {
160221
if (tabs?.[0]) {
161222
const data = tabData.get(tabs[0].id);
162223
if (data) {
163-
updateBadge(tabs[0].id, data.status, data.cdn);
224+
updateBadge(tabs[0].id, data.status);
164225
} else {
165226
clearBadge(tabs[0].id);
166227
}
@@ -170,7 +231,8 @@ browser.windows.onFocusChanged.addListener(async (windowId) => {
170231
}
171232
});
172233

173-
// Handle messages from popup and content scripts
234+
// --- Message Handling ---
235+
174236
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
175237
if (message.type === 'getTabData') {
176238
sendResponse(tabData.get(message.tabId) || null);
@@ -196,20 +258,19 @@ browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
196258
return true;
197259
});
198260

199-
// Handle popup connections for reactive updates
261+
// --- Popup Port Connection ---
262+
200263
browser.runtime.onConnect.addListener((port) => {
201264
if (port.name !== 'popup') return;
202265

203266
port.onMessage.addListener((msg) => {
204267
if (msg.type === 'subscribe' && msg.tabId) {
205268
popupPorts.set(msg.tabId, port);
206-
// Send initial data immediately
207269
port.postMessage({ type: 'update', data: tabData.get(msg.tabId) || null });
208270
}
209271
});
210272

211273
port.onDisconnect.addListener(() => {
212-
// Remove port from map when popup closes
213274
for (const [tabId, p] of popupPorts) {
214275
if (p === port) {
215276
popupPorts.delete(tabId);

CF Cache Status/CF Cache Status Extension/Resources/popup.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* Cache Status - Popup Script
3-
*
3+
*
44
* macOS Native Inspector Design
55
* Features: Dark Mode support, Dynamic Grid, Full Stats.
66
*/
@@ -26,10 +26,15 @@ function updateUI(data) {
2626
const statusType = getStatusType(data.status);
2727
const subtitle = (STATUS_DESCRIPTIONS[data.status.toUpperCase()] || `${cdnName} detected`)
2828
.replace('{cdn}', cdnName);
29-
29+
3030
renderHero(data.status, subtitle, statusType);
3131
} else if (hasHeaders) {
3232
renderHero('N/A', 'No cache status detected', 'neutral');
33+
} else if (data?.noHeaders) {
34+
// Safari doesn't provide headers for external links/bookmarks
35+
renderHero('Reload Required', 'Safari does not expose headers for pages opened from bookmarks or external apps. Reload the page to capture cache status.', 'neutral');
36+
hideAllSections();
37+
return;
3338
} else {
3439
renderHero('No Data', 'Navigate or reload to capture headers', 'neutral');
3540
hideAllSections();
@@ -301,7 +306,6 @@ document.addEventListener('DOMContentLoaded', async () => {
301306
}
302307
});
303308

304-
// Subscribe to updates for this tab (also triggers initial data send)
305309
port.postMessage({ type: 'subscribe', tabId: tabs[0].id });
306310
} catch (error) {
307311
console.error('Error connecting:', error);

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
### Added
8+
- Detection of Safari limitation for bookmarks and external links
9+
- "Reload Required" message when headers unavailable (with explanation)
10+
- "RLD" badge indicator when reload is needed
11+
- KNOWN_ISSUES.md documenting Safari webRequest API limitations
12+
713
### Changed
14+
- Badge now shows full "MISS" instead of truncated "MIS"
815
- Release scripts now use shared `config.sh` for app name and paths
916
- `just release` no longer waits for notarization (async workflow)
1017
- Version and build number now auto-derived from git tag and commit count

0 commit comments

Comments
 (0)