RouteExplorerClient: send browser-shaped headers (fix 403 on /api/token)

The route-explorer.com proxy gates /api/token and /api/flight-search by
Origin/Referer in production. Without those headers iOS got 403. Mirror
the existing United pattern (applyUnitedBrowserHeaders): set User-Agent,
Accept-Language, Referer, Origin on every request. Also log the token
response body on non-200 so future failures are diagnosable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-04-28 10:26:41 -05:00
parent b403bfd970
commit 4d026ef530
+21 -1
View File
@@ -109,11 +109,14 @@ actor RouteExplorerClient {
let url = baseURL.appendingPathComponent("api/token")
var req = URLRequest(url: url)
req.httpMethod = "GET"
Self.applyBrowserHeaders(to: &req)
req.setValue("application/json", forHTTPHeaderField: "Accept")
let (data, response) = try await session.data(for: req)
let status = (response as? HTTPURLResponse)?.statusCode ?? -1
guard status == 200 else {
if status != 200 {
let bodyStr = String(data: data, encoding: .utf8) ?? "<no body>"
print("[RouteExplorer] /api/token failed status=\(status) body=\(bodyStr.prefix(300))")
throw ClientError.tokenFetchFailed(status: status)
}
@@ -127,6 +130,21 @@ actor RouteExplorerClient {
}
}
/// Browser-shaped headers `/api/token` and `/api/flight-search` are
/// gated by Origin/Referer in production. Without these the server
/// returns 403 even though the endpoints are otherwise anonymous.
private static func applyBrowserHeaders(to request: inout URLRequest) {
request.setValue(
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) "
+ "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 "
+ "Mobile/15E148 Safari/604.1",
forHTTPHeaderField: "User-Agent"
)
request.setValue("en-US,en;q=0.9", forHTTPHeaderField: "Accept-Language")
request.setValue("https://route-explorer.com/", forHTTPHeaderField: "Referer")
request.setValue("https://route-explorer.com", forHTTPHeaderField: "Origin")
}
// MARK: - Flight search proxy
private func callFlightSearch(
@@ -144,6 +162,7 @@ actor RouteExplorerClient {
var req = URLRequest(url: url)
req.httpMethod = "POST"
Self.applyBrowserHeaders(to: &req)
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.setValue("application/json", forHTTPHeaderField: "Accept")
req.setValue(token, forHTTPHeaderField: "X-API-Token")
@@ -181,6 +200,7 @@ actor RouteExplorerClient {
var req = URLRequest(url: url)
req.httpMethod = "POST"
Self.applyBrowserHeaders(to: &req)
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.setValue(token, forHTTPHeaderField: "X-API-Token")
req.httpBody = bodyData