+44
-16
src/view/atproto.js
+44
-16
src/view/atproto.js
···
142
142
return avatars;
143
143
}
144
144
145
-
// Validate app URLs - only mark as invalid if URL is malformed
146
-
// Namespaces come from ATProto lexicons, so reversed domains are valid by definition
145
+
// Validate app URLs by checking if the domain is reachable
147
146
export async function validateAppUrls(appDivs) {
148
147
// Clear previous invalid apps
149
148
state.invalidApps.clear();
150
149
151
-
appDivs.forEach(({ div, namespace }) => {
150
+
const validationPromises = appDivs.map(async ({ div, namespace }) => {
152
151
const link = div.querySelector('.app-name');
153
152
const url = link?.dataset.url;
154
-
if (!url) return;
153
+
if (!url || !link) return;
155
154
156
155
try {
157
-
// Just check if it's a valid URL - don't try to fetch or validate the domain
158
-
new URL(url);
156
+
new URL(url); // Check syntax first
159
157
} catch (e) {
160
-
// Malformed URL - mark as invalid
161
-
if (link) {
162
-
const displayName = link.textContent.replace(' ↗', '').replace(' \u2197', '');
163
-
link.classList.add('invalid-link');
164
-
link.setAttribute('title', 'malformed URL');
165
-
link.style.pointerEvents = 'none';
166
-
link.textContent = displayName;
167
-
}
168
-
if (namespace) {
169
-
state.invalidApps.add(namespace);
158
+
markInvalid(link, namespace, 'malformed URL');
159
+
return;
160
+
}
161
+
162
+
// Try HEAD request with short timeout to check if domain is reachable
163
+
try {
164
+
const controller = new AbortController();
165
+
const timeout = setTimeout(() => controller.abort(), 3000);
166
+
167
+
await fetch(url, {
168
+
method: 'HEAD',
169
+
mode: 'no-cors',
170
+
signal: controller.signal,
171
+
});
172
+
173
+
clearTimeout(timeout);
174
+
// If we get here, domain is reachable (even if response is opaque due to CORS)
175
+
} catch (e) {
176
+
// Only mark as invalid for actual DNS/connection failures
177
+
// CORS blocks mean the server IS reachable, just not allowing our request
178
+
const errorMsg = e.message || '';
179
+
if (errorMsg.includes('ERR_NAME_NOT_RESOLVED') ||
180
+
errorMsg.includes('ERR_CONNECTION_REFUSED') ||
181
+
errorMsg.includes('ERR_CONNECTION_TIMED_OUT') ||
182
+
e.name === 'AbortError') {
183
+
markInvalid(link, namespace, 'domain not reachable');
170
184
}
185
+
// For CORS blocks (ERR_FAILED) and other errors, server exists so don't mark invalid
171
186
}
172
187
});
188
+
189
+
await Promise.all(validationPromises);
190
+
}
191
+
192
+
function markInvalid(link, namespace, reason) {
193
+
const displayName = link.textContent.replace(' ↗', '').replace(' \u2197', '');
194
+
link.classList.add('invalid-link');
195
+
link.setAttribute('title', reason);
196
+
link.style.pointerEvents = 'none';
197
+
link.textContent = displayName;
198
+
if (namespace) {
199
+
state.invalidApps.add(namespace);
200
+
}
173
201
}