diff --git a/app/api/transport/route.ts b/app/api/transport/route.ts index 6d7797f..63d7e71 100644 --- a/app/api/transport/route.ts +++ b/app/api/transport/route.ts @@ -1,11 +1,52 @@ export const dynamic = 'force-dynamic' +export const runtime = 'nodejs' + import { NextResponse } from 'next/server' -import { Agent } from 'undici' +import * as https from 'node:https' -const ORGP_BASE = 'https://transport.orgp.spb.ru' +const ORGP_HOST = 'transport.orgp.spb.ru' -// ORGP TLS chain fails default verification in Node — match curl -k behaviour. -const insecureAgent = new Agent({ connect: { rejectUnauthorized: false } }) +// ORGP TLS chain fails default verification — accept like curl -k for this one host. +const insecureAgent = new https.Agent({ rejectUnauthorized: false }) + +interface UpstreamResult { + status: number + body: string +} + +function postOrgp(path: string, form: string): Promise { + return new Promise((resolve, reject) => { + const req = https.request( + { + host: ORGP_HOST, + path, + method: 'POST', + agent: insecureAgent, + timeout: 8000, + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Content-Length': Buffer.byteLength(form).toString(), + }, + }, + (res) => { + let data = '' + res.setEncoding('utf8') + res.on('data', (chunk) => { + data += chunk + }) + res.on('end', () => resolve({ status: res.statusCode || 0, body: data })) + } + ) + req.on('timeout', () => { + req.destroy(new Error('upstream timeout')) + }) + req.on('error', reject) + req.write(form) + req.end() + }) +} export async function GET(req: Request) { const { searchParams } = new URL(req.url) @@ -14,40 +55,29 @@ export async function GET(req: Request) { return NextResponse.json({ error: 'stopId required (digits)' }, { status: 400 }) } - const body = new URLSearchParams({ + const form = new URLSearchParams({ sEcho: '1', iColumns: '5', sColumns: 'index,routeNumber,timeToArrive,parkNumber,wheelchair', iDisplayStart: '0', iDisplayLength: '-1', sNames: 'index,routeNumber,timeToArrive,parkNumber,wheelchair', - }) + }).toString() try { - const upstream = await fetch( - `${ORGP_BASE}/Portal/transport/stop/${encodeURIComponent(stopId)}/arriving`, - { - method: 'POST', - headers: { - 'X-Requested-With': 'XMLHttpRequest', - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', - 'Accept': 'application/json, text/javascript, */*; q=0.01', - }, - body: body.toString(), - cache: 'no-store', - // @ts-ignore — undici dispatcher option at runtime - dispatcher: insecureAgent, - } + const { status, body } = await postOrgp( + `/Portal/transport/stop/${encodeURIComponent(stopId)}/arriving`, + form ) - if (!upstream.ok) { + if (status !== 200) { return NextResponse.json( - { error: `upstream_${upstream.status}`, arrivals: [] }, + { error: `upstream_${status}`, arrivals: [] }, { status: 502 } ) } - const data = await upstream.json() + const data = JSON.parse(body) const arrivals = Array.isArray(data?.aaData) ? data.aaData.map((row: any[]) => ({ route: String(row[1] ?? ''),