diff --git a/public/locales/en/common.json b/public/locales/en/common.json index ba9aa59..3e51e9e 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -85,6 +85,7 @@ "departureDate": "Please select a departure date" }, "no_flight_found": "No matching flight found", + "comm_error": "Unable to connect to flight data service. Please try again later.", "inconsistent_data": "The flight data provided by the flight data service is inconsistent and cannot be used for protection purposes.", "airport_not_whitelisted": "Protection for flights from {{dep}} to {{arr}} is not available. Please choose a different flight. Refer to the <0>complete list of approved airports here.", "airport_blacklisted": "Protection for flights from or to {{airport}} is not available. Please choose a different flight. Refer to the <0>complete list of approved airports here.", diff --git a/src/app/api/_utils/api_constants.ts b/src/app/api/_utils/api_constants.ts index 0d3941a..eced42d 100644 --- a/src/app/api/_utils/api_constants.ts +++ b/src/app/api/_utils/api_constants.ts @@ -1,7 +1,11 @@ export const FLIGHTSTATS_BASE_URL = process.env.FLIGHTSTATS_BASE_URL || 'https://api.flightstats.com/flex'; -export const FLIGHTSTATS_APP_ID = process.env.FLIGHTSTATS_APP_ID || '123456789'; -export const FLIGHTSTATS_APP_KEY = process.env.FLIGHTSTATS_APP_KEY || '123456789'; +export const FLIGHTSTATS_APP_ID = process.env.FLIGHTSTATS_APP_ID || ''; +export const FLIGHTSTATS_APP_KEY = process.env.FLIGHTSTATS_APP_KEY || ''; + +if (!FLIGHTSTATS_APP_ID || !FLIGHTSTATS_APP_KEY) { + console.error('WARNING: FLIGHTSTATS_APP_ID and/or FLIGHTSTATS_APP_KEY environment variables are not set. Flightstats API requests will fail.'); +} export const APP_BASE_URL = process.env.NEXT_PUBLIC_APP_BASE_URL || 'https://flightdelay.app'; diff --git a/src/app/api/_utils/proxy.ts b/src/app/api/_utils/proxy.ts index 66df292..b61bd3e 100644 --- a/src/app/api/_utils/proxy.ts +++ b/src/app/api/_utils/proxy.ts @@ -1,4 +1,4 @@ -import axios, { AxiosHeaders } from "axios"; +import axios, { AxiosHeaders, isAxiosError } from "axios"; import { LOGGER } from "../../../utils/logger_backend"; const LOG_API_PROXY = process.env.LOG_API_PROXY ?? "false"; @@ -8,21 +8,35 @@ export async function sendRequestAndReturnResponse(reqId: string, url: string, m LOGGER.debug(`proxy request ==> ${method ?? 'GET'} ${url}`, { reqId }); } const before = Date.now(); - const proxyResponse = await axios({ - url: url, - method: method ?? 'GET', - data: body, - headers: headers, - }); + let proxyResponse; + try { + proxyResponse = await axios({ + url: url, + method: method ?? 'GET', + data: body, + headers: headers, + validateStatus: () => true, // accept all status codes, handle errors explicitly + }); + } catch (error) { + const after = Date.now(); + const errMsg = isAxiosError(error) ? `${error.code}: ${error.message}` : String(error); + LOGGER.error(`proxy error <== ${errMsg}`, { reqId, proxyRequestDuration: after - before }); + return Response.json( + { error: 'proxy_request_failed', message: errMsg }, + { + status: 502, + headers: { 'Content-Type': 'application/json', 'X-Proxy-Request-Id': reqId } + }); + } const after = Date.now(); if (LOG_API_PROXY.toLowerCase() === "true") { LOGGER.debug(`proxy response <== ${proxyResponse.status}`, { reqId, proxyRequestDuration: after - before }); } const respJson = await proxyResponse.data; return Response.json( - respJson, - { - status: proxyResponse.status, - headers: { 'Content-Type': 'application/json', 'X-Proxy-Request-Id': reqId } + respJson, + { + status: proxyResponse.status, + headers: { 'Content-Type': 'application/json', 'X-Proxy-Request-Id': reqId } }); } diff --git a/src/app/api/flightstats/schedule/[carrier]/[flightNumber]/[departureDate]/route.ts b/src/app/api/flightstats/schedule/[carrier]/[flightNumber]/[departureDate]/route.ts index 42889bd..9bce94a 100644 --- a/src/app/api/flightstats/schedule/[carrier]/[flightNumber]/[departureDate]/route.ts +++ b/src/app/api/flightstats/schedule/[carrier]/[flightNumber]/[departureDate]/route.ts @@ -18,8 +18,16 @@ export async function GET( const flightNumber = params.flightNumber; const departureDate = params.departureDate; LOGGER.info(`[${reqId}] fetching flight status for ${carrier} ${flightNumber} ${departureDate}`); - const year = departureDate.split('-')[0]; - const month = departureDate.split('-')[1]; - const day = departureDate.split('-')[2]; + + const dateMatch = departureDate.match(/^(\d{4})-(\d{2})-(\d{2})$/); + if (!dateMatch) { + LOGGER.warn(`[${reqId}] invalid departure date format: ${departureDate}`); + return Response.json( + { error: 'invalid_date', message: 'Departure date must be in YYYY-MM-DD format' }, + { status: 400, headers: { 'Content-Type': 'application/json', 'X-Proxy-Request-Id': reqId } } + ); + } + const [, year, month, day] = dateMatch; + return sendRequestAndReturnResponse(reqId, flightstatsScheduleUrl(carrier, flightNumber, year, month, day)); } diff --git a/src/components/Application/application_error.tsx b/src/components/Application/application_error.tsx index 380795f..245f5fa 100644 --- a/src/components/Application/application_error.tsx +++ b/src/components/Application/application_error.tsx @@ -76,11 +76,17 @@ export function ApplicationError({flightFound, flightData}: {flightFound: boolea return ; - + + case Reason.INCONSISTENT_DATA: + trackEvent(EVENT_API_ERROR, { category: 'flight_search', error: errorReasonApi }); + return + + ; + default: trackEvent(EVENT_API_ERROR, { category: 'flight_search', error: errorReasonApi }); return - + ; } diff --git a/src/hooks/api/use_flightstats_api.tsx b/src/hooks/api/use_flightstats_api.tsx index 3b11e36..6e1b54e 100644 --- a/src/hooks/api/use_flightstats_api.tsx +++ b/src/hooks/api/use_flightstats_api.tsx @@ -8,7 +8,7 @@ export function useFlightstatsApi() { async function fetchFlightData(carrier: string, flightNumber: string, departureDate: dayjs.Dayjs): Promise<{ flights: ScheduledFlight[], airports: Airport[], carrier: string, flightNumber: string}> { console.log("fetching flight data for", carrier, flightNumber, departureDate); const uri = `/api/flightstats/schedule/${encodeURIComponent(carrier)}/${encodeURIComponent(flightNumber)}/${encodeURIComponent(departureDate.format('YYYY-MM-DD'))}`; - const res = await fetch(uri, { cache: 'force-cache' }); + const res = await fetch(uri, { cache: 'no-store' }); if (! res.ok) { throw new Error(`Error fetching flightstats data: ${res.statusText}`); @@ -26,7 +26,7 @@ export function useFlightstatsApi() { return { flights: jsonResponse.scheduledFlights as ScheduledFlight[], - airports: jsonResponse.appendix.airports as Airport[], + airports: jsonResponse.appendix?.airports as Airport[] ?? [], carrier, flightNumber, };