Skip to content

Commit bb87da1

Browse files
author
maro114510
committed
fix: Add error handling and use token cache
1 parent 41a1040 commit bb87da1

1 file changed

Lines changed: 202 additions & 29 deletions

File tree

content.js

Lines changed: 202 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,55 @@ const api = typeof browser !== "undefined" ? browser : chrome;
55
// =============================================================================
66

77
const FEEDLY_API_BASE = "https://api.feedly.com";
8+
const TOKEN_CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes
9+
10+
// =============================================================================
11+
// Error Handling
12+
// =============================================================================
13+
14+
const ErrorCode = {
15+
NO_TOKEN: 'NO_TOKEN',
16+
AUTH_FAILED: 'AUTH_FAILED',
17+
RATE_LIMITED: 'RATE_LIMITED',
18+
NETWORK_ERROR: 'NETWORK_ERROR',
19+
SERVER_ERROR: 'SERVER_ERROR',
20+
WRONG_PAGE: 'WRONG_PAGE',
21+
UNKNOWN: 'UNKNOWN'
22+
};
23+
24+
const UserMessages = {
25+
NO_TOKEN: "Please sign in to Feedly first.",
26+
AUTH_FAILED: "Session expired. Please refresh the page.",
27+
RATE_LIMITED: "Too many requests. Please wait a moment.",
28+
NETWORK_ERROR: "Network error. Please check your connection.",
29+
SERVER_ERROR: "Feedly service is temporarily unavailable.",
30+
WRONG_PAGE: "Please open a Feedly Read Later page.",
31+
UNKNOWN: "Something went wrong. Please try again."
32+
};
33+
34+
class FeedlyError extends Error {
35+
constructor(code, technicalDetail) {
36+
super(technicalDetail);
37+
this.name = 'FeedlyError';
38+
this.code = code;
39+
this.userMessage = UserMessages[code] || UserMessages.UNKNOWN;
40+
}
41+
42+
getUserMessage() {
43+
return this.userMessage;
44+
}
45+
46+
getDebugInfo() {
47+
return `[${this.code}] ${this.message}`;
48+
}
49+
}
50+
51+
function classifyHttpError(status) {
52+
if (status === 401 || status === 403) return ErrorCode.AUTH_FAILED;
53+
if (status === 429) return ErrorCode.RATE_LIMITED;
54+
if (status >= 500) return ErrorCode.SERVER_ERROR;
55+
return ErrorCode.UNKNOWN;
56+
}
857

958
// =============================================================================
1059
// Constants
@@ -55,15 +104,67 @@ function normalizeCount(count) {
55104
// Feedly API Functions
56105
// =============================================================================
57106

58-
// Token storage for communication between page and content script
59-
let cachedAccessToken = null;
107+
// Token storage with metadata for TTL and change detection
108+
let tokenCache = {
109+
token: null,
110+
cachedAt: 0,
111+
sourceHash: null
112+
};
113+
114+
/**
115+
* Generate a simple hash for change detection (djb2 algorithm).
116+
* @param {string} str - String to hash
117+
* @returns {number} Simple numeric hash
118+
*/
119+
function simpleHash(str) {
120+
if (!str) return 0;
121+
let hash = 0;
122+
for (let i = 0; i < str.length; i++) {
123+
const char = str.charCodeAt(i);
124+
hash = ((hash << 5) - hash) + char;
125+
hash = hash & hash;
126+
}
127+
return hash;
128+
}
129+
130+
/**
131+
* Check if cached token is still valid.
132+
* Validates TTL expiration and localStorage data integrity.
133+
* @returns {boolean} True if cache is valid
134+
*/
135+
function isTokenCacheValid() {
136+
if (!tokenCache.token) {
137+
return false;
138+
}
139+
140+
// Check TTL expiration
141+
if (Date.now() - tokenCache.cachedAt > TOKEN_CACHE_TTL_MS) {
142+
return false;
143+
}
144+
145+
// Validate against current localStorage
146+
try {
147+
const currentSessionData = localStorage.getItem("feedly.session");
148+
if (simpleHash(currentSessionData) !== tokenCache.sourceHash) {
149+
return false;
150+
}
151+
} catch (e) {
152+
return false;
153+
}
154+
155+
return true;
156+
}
60157

61158
/**
62159
* Clear the cached access token.
63160
* Called when authentication fails to allow re-fetching from localStorage.
64161
*/
65162
function clearAccessTokenCache() {
66-
cachedAccessToken = null;
163+
tokenCache = {
164+
token: null,
165+
cachedAt: 0,
166+
sourceHash: null
167+
};
67168
}
68169

69170
/**
@@ -77,18 +178,25 @@ function clearAccessTokenCache() {
77178
* @returns {Promise<string|null>} Access token or null if not available
78179
*/
79180
async function getAccessToken() {
80-
// Return cached token if available
81-
if (cachedAccessToken) {
82-
return cachedAccessToken;
181+
// Return cached token if still valid
182+
if (isTokenCacheValid()) {
183+
return tokenCache.token;
83184
}
84185

186+
// Clear expired/invalid cache
187+
clearAccessTokenCache();
188+
85189
try {
86190
const sessionData = localStorage.getItem("feedly.session");
87191
if (sessionData) {
88192
const session = JSON.parse(sessionData);
89193
if (session.feedlyToken) {
90-
cachedAccessToken = session.feedlyToken;
91-
return cachedAccessToken;
194+
tokenCache = {
195+
token: session.feedlyToken,
196+
cachedAt: Date.now(),
197+
sourceHash: simpleHash(sessionData)
198+
};
199+
return tokenCache.token;
92200
}
93201
}
94202
} catch (e) {
@@ -107,26 +215,38 @@ async function getAccessToken() {
107215
async function feedlyApiRequest(endpoint, options = {}) {
108216
const token = await getAccessToken();
109217
if (!token) {
110-
throw new Error("No Feedly access token available");
218+
throw new FeedlyError(ErrorCode.NO_TOKEN, 'No access token in localStorage');
111219
}
112220

113221
const url = `${FEEDLY_API_BASE}${endpoint}`;
114-
const response = await fetch(url, {
115-
...options,
116-
headers: {
117-
"Authorization": `Bearer ${token}`,
118-
"Content-Type": "application/json",
119-
...options.headers
120-
}
121-
});
222+
223+
let response;
224+
try {
225+
response = await fetch(url, {
226+
...options,
227+
headers: {
228+
"Authorization": `Bearer ${token}`,
229+
"Content-Type": "application/json",
230+
...options.headers
231+
}
232+
});
233+
} catch (networkError) {
234+
throw new FeedlyError(
235+
ErrorCode.NETWORK_ERROR,
236+
`Fetch failed: ${networkError.message}`
237+
);
238+
}
122239

123240
if (!response.ok) {
124241
// Clear token cache on authentication errors to allow retry with fresh token
125242
if (response.status === 401 || response.status === 403) {
126243
clearAccessTokenCache();
127244
}
128-
const errorText = await response.text().catch(() => "Unknown error");
129-
throw new Error(`Feedly API error ${response.status}: ${errorText}`);
245+
const errorBody = await response.text().catch(() => "");
246+
throw new FeedlyError(
247+
classifyHttpError(response.status),
248+
`API ${response.status}: ${errorBody.substring(0, 200)}`
249+
);
130250
}
131251

132252
// DELETE requests may return empty body
@@ -244,7 +364,7 @@ async function unsaveEntriesViaAPI(userId, entryIds) {
244364
async function handleOpenViaAPI(settings) {
245365
const token = await getAccessToken();
246366
if (!token) {
247-
throw new Error("No access token available. Please ensure you are logged into Feedly.");
367+
throw new FeedlyError(ErrorCode.NO_TOKEN, 'No access token available');
248368
}
249369

250370
const userId = await getUserId();
@@ -605,7 +725,7 @@ function findScrollContainer(startNode) {
605725
*/
606726
async function handleOpenViaDOM(settings) {
607727
if (!(await waitForReadLaterPage(2000))) {
608-
return { ok: false, error: "This tab is not a Feedly Read Later page." };
728+
throw new FeedlyError(ErrorCode.WRONG_PAGE, `Not on Read Later page: ${location.href}`);
609729
}
610730

611731
if (settings.mode === "all") {
@@ -639,22 +759,33 @@ async function handleOpen(settings) {
639759
result = await handleOpenViaAPI(settings);
640760
} catch (e) {
641761
apiError = e;
642-
console.warn("[Feedly Opener] API operation failed, falling back to DOM:", e.message);
762+
if (e instanceof FeedlyError) {
763+
console.warn("[Feedly Opener] API failed:", e.getDebugInfo());
764+
} else {
765+
console.warn("[Feedly Opener] API failed:", e.message);
766+
}
643767
}
644768

645769
// Fallback to DOM-based approach if API failed
646770
if (!result || !result.ok) {
647771
try {
648772
result = await handleOpenViaDOM(settings);
649-
if (apiError) {
650-
result.apiError = apiError.message;
651-
}
652773
} catch (domError) {
653-
console.error("[Feedly Opener] DOM operation also failed:", domError);
774+
if (domError instanceof FeedlyError) {
775+
console.error("[Feedly Opener] DOM failed:", domError.getDebugInfo());
776+
} else {
777+
console.error("[Feedly Opener] DOM failed:", domError.message);
778+
}
779+
780+
const userMessage = apiError instanceof FeedlyError
781+
? apiError.getUserMessage()
782+
: UserMessages.UNKNOWN;
783+
654784
return {
655785
ok: false,
656-
error: `API error: ${apiError?.message || "unknown"}. DOM error: ${domError.message}`,
657-
method: "failed"
786+
error: userMessage,
787+
method: "failed",
788+
errorCode: apiError instanceof FeedlyError ? apiError.code : ErrorCode.UNKNOWN
658789
};
659790
}
660791
}
@@ -706,6 +837,33 @@ function isRecentlyReadLater() {
706837
return location.origin === "https://feedly.com" && lastReadLaterUrl.length > 0;
707838
}
708839

840+
// =============================================================================
841+
// Message Security
842+
// =============================================================================
843+
844+
/**
845+
* Validate message sender is from our own extension.
846+
* @param {Object} sender - Message sender object
847+
* @returns {boolean} True if sender is valid
848+
*/
849+
function validateSender(sender) {
850+
return sender && sender.id === api.runtime.id;
851+
}
852+
853+
/**
854+
* Validate and sanitize settings from message.
855+
* @param {Object} raw - Raw settings object from message
856+
* @returns {Object} Validated settings with safe defaults
857+
*/
858+
function validateSettings(raw) {
859+
const validModes = ['all', 'count'];
860+
return {
861+
mode: validModes.includes(raw?.mode) ? raw.mode : 'all',
862+
count: Math.max(1, Math.min(999, Math.floor(Number(raw?.count)) || 10)),
863+
reload: typeof raw?.reload === 'boolean' ? raw.reload : true
864+
};
865+
}
866+
709867
// =============================================================================
710868
// Message Listener
711869
// =============================================================================
@@ -717,7 +875,22 @@ if (!window.__feedlyReadLaterOpenerListenerAdded) {
717875
return false;
718876
}
719877

720-
handleOpen(message.settings || {}).then(sendResponse);
878+
// Validate sender is from our own extension
879+
if (!validateSender(sender)) {
880+
sendResponse({ ok: false, error: "Invalid message sender" });
881+
return true;
882+
}
883+
884+
// Validate and sanitize settings
885+
const settings = validateSettings(message.settings);
886+
887+
handleOpen(settings)
888+
.then(sendResponse)
889+
.catch((e) => {
890+
console.error("[Feedly Opener]", e);
891+
const userMessage = e instanceof FeedlyError ? e.getUserMessage() : UserMessages.UNKNOWN;
892+
sendResponse({ ok: false, error: userMessage });
893+
});
721894
return true;
722895
});
723896
}

0 commit comments

Comments
 (0)