fix: call OpenSky API directly from browser to bypass Vercel IP blocks (#1)
* fix: switch flights API to edge runtime, lower timeouts
- Switch to Edge Runtime (30s timeout on ALL Vercel plans, near-zero cold starts)
- Lower FETCH_TIMEOUT_MS from 20s to 8s (prevents Vercel killing the function)
- Lower TOKEN_TIMEOUT_MS from 5s to 3s
- Replace Buffer.from() with btoa() for edge compatibility
- Remove maxDuration (not needed for edge functions)
Fixes 502 Bad Gateway on Vercel deployments caused by Hobby plan's
10s function timeout being exceeded by the 20s fetch timeout.
* fix: revert edge runtime, keep reduced timeouts
Edge runtime caused 500 on Vercel. Reverted to Node.js serverless
with reduced timeouts only:
- FETCH_TIMEOUT_MS: 20s -> 8s
- TOKEN_TIMEOUT_MS: 5s -> 3s
- Restored Buffer.from() for basic auth
* fix: handle DOMException unavailability in Vercel runtime
- Fix TypeError: 'Right-hand side of instanceof is not an object'
caused by DOMException not existing in Vercel's server runtime
- Use safe abort detection: check err.name === 'AbortError' on Error
first, then conditionally check DOMException if available
- Restore TOKEN_TIMEOUT_MS to 5s (3s was too aggressive from Vercel)
- Keep FETCH_TIMEOUT_MS at 8s (down from original 20s)
* fix: deploy to Frankfurt, increase timeouts, add retry logic
- Set preferredRegion to 'fra1' (Frankfurt) — closest to OpenSky EU servers
- Increase FETCH_TIMEOUT_MS to 15s and TOKEN_TIMEOUT_MS to 8s
(OpenSky is slow from cloud IPs, needs more time)
- Add retry logic: 1 retry with 500ms delay before giving up
- Keep safe DOMException handling for Vercel runtime compat
* feat: add Railway proxy for OpenSky API, proxy mode for Vercel route
OpenSky blocks/throttles requests from Vercel's IP ranges. This adds:
1. **Railway proxy** (`proxy/`) — standalone Node.js server that handles
OpenSky API calls with auth, caching, and CORS. Zero dependencies.
2. **Proxy mode in Vercel route** — when OPENSKY_PROXY_URL env var is set,
the Vercel /api/flights route forwards requests to the Railway proxy
instead of calling OpenSky directly.
Setup:
- Deploy proxy/ to Railway with OPENSKY credentials
- Set OPENSKY_PROXY_URL in Vercel to the Railway URL
- Remove OPENSKY credentials from Vercel (only needed on Railway)
* refactor: call OpenSky directly from browser, remove server-side proxy
OpenSky supports CORS (Access-Control-Allow-Origin: *), so we can
call the API directly from the user's browser. This bypasses
Vercel's cloud-provider IPs that OpenSky blocks.
Changes:
- opensky.ts: fetch from opensky-network.org/api directly (was /api/flights)
- use-flights.ts: fix DOMException abort detection
- Remove src/app/api/flights/route.ts (server-side proxy)
- Remove proxy/ directory (Railway proxy)
The app is now fully static — no server-side API routes needed.
* chore: remove server-side API route (now browser-side)
* chore: remove Railway proxy (no longer needed)
* chore: remove proxy package.json
* chore: remove proxy README
* polish: production-grade cleanup, security hardening, remove redundant comments
- Remove redundant JSDoc blocks in opensky.ts, keep only @see link
- Add bounds-clamping for lat/lng parameters (defense-in-depth)
- Fix memory leak: map.on("movestart") listener now cleaned up on unmount
- Validate GA_ID format before interpolating into script tag (XSS defense)
- Remove duplicate canonical <link> tag (already set via metadata.alternates)
- Sanitize JSON-LD output with \u003c escaping to prevent </script> injection
- Use useRef instead of useState for mutable TrailStore class instance
- Fix unused useState import in use-trail-history
- Add Map.displayName for React DevTools
- Fix Tailwind lint: px-[2px] → px-0.5
- Remove unused OPENSKY credentials from .env.example
* chore: push remaining polished files (flight-tracker, use-flights, map)
- flight-tracker: movestart listener cleanup on unmount
- use-flights: clean up redundant comments, fix abort detection
- map: add displayName, remove redundant comment prefix
* chore: polish control-panel (clean comments, Tailwind lint fix)
This commit is contained in:
@ -236,7 +236,7 @@ function PanelDialog({
|
||||
</a>
|
||||
<div className="border-t border-white/3 pt-2 px-2.5">
|
||||
<p className="text-[10px] font-medium text-white/10 tracking-wide">
|
||||
v0.1 · OpenSky Network
|
||||
v0.1 \u00b7 OpenSky Network
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -298,7 +298,7 @@ function PanelDialog({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile tab bar — at bottom for thumb reach */}
|
||||
{/* Mobile tab bar */}
|
||||
<div className="flex sm:hidden items-center gap-1 border-t border-white/6 px-3 pt-2 pb-3">
|
||||
<nav className="flex flex-1 gap-1">
|
||||
{TABS.map(({ id, icon: Icon, label }) => {
|
||||
@ -423,7 +423,7 @@ function SearchContent({
|
||||
{city.name}
|
||||
</p>
|
||||
<p className="text-[11px] font-medium text-white/25">
|
||||
{city.iata} · {city.country}
|
||||
{city.iata} \u00b7 {city.country}
|
||||
</p>
|
||||
</div>
|
||||
<ChevronRight className="h-3.5 w-3.5 shrink-0 text-white/12 transition-colors group-hover:text-white/25" />
|
||||
@ -457,7 +457,7 @@ function StyleContent({
|
||||
</div>
|
||||
<div className="border-t border-white/4 px-5 py-3">
|
||||
<p className="text-[11px] font-medium text-white/12">
|
||||
Satellite © Esri · Terrain © OpenTopoMap · Base maps ©
|
||||
Satellite \u00a9 Esri \u00b7 Terrain \u00a9 OpenTopoMap \u00b7 Base maps \u00a9
|
||||
CARTO
|
||||
</p>
|
||||
</div>
|
||||
@ -632,7 +632,7 @@ function OrbitSpeedSlider({
|
||||
const activeLabel =
|
||||
ORBIT_SPEED_PRESETS.find(
|
||||
(p) => Math.abs(p.value - value) < ORBIT_SNAP_THRESHOLD,
|
||||
)?.label ?? `${value.toFixed(2)}×`;
|
||||
)?.label ?? `${value.toFixed(2)}\u00d7`;
|
||||
|
||||
function handleChange(vals: number[]) {
|
||||
let raw = vals[0];
|
||||
@ -666,7 +666,7 @@ function OrbitSpeedSlider({
|
||||
onValueChange={handleChange}
|
||||
aria-label="Orbit speed"
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-x-0 top-1/2 -translate-y-1/2 flex justify-between px-[2px]">
|
||||
<div className="pointer-events-none absolute inset-x-0 top-1/2 -translate-y-1/2 flex justify-between px-0.5">
|
||||
{ORBIT_SPEED_PRESETS.map((preset) => {
|
||||
const pct =
|
||||
((preset.value - ORBIT_SPEED_MIN) /
|
||||
|
||||
Reference in New Issue
Block a user